mercredi 28 août 2019

Tracking User-Defined `struct` Members with a Designated Initializer Syntax

For a unit-testing library that I'm writing, rexo, I would like to implement an automatic test registration mechanism compatible with both C99 and C++11.

Automatic test registration usually goes along the lines of:

  • providing macros for the users to define test suites and test cases.
  • having the macros instantiate file-level structures that contain the data needed to fully describe their respective test suites/cases.
  • having some logic that can somehow discover these structure instances at run-time.

I've got most of this sorted out but one bit: providing a nice interface for defining additional data to be attached to each test suite/case.

The (non-public) data structure to attach looks like this:

struct rx__data {
    const char *name;
    int value;
    rx_run_fn run;
};

I managed to get a RX__MAKE_DATA() macro working with a designated initializer syntax, as follows:

/* https://github.com/swansontec/map-macro ----------------------------------- */

#define EVAL0(...) __VA_ARGS__
#define EVAL1(...) EVAL0(EVAL0(EVAL0(__VA_ARGS__)))
#define EVAL2(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__)))
#define EVAL3(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__)))
#define EVAL4(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__)))
#define EVAL(...)  EVAL4(EVAL4(EVAL4(__VA_ARGS__)))

#define MAP_END(...)
#define MAP_OUT

#define MAP_GET_END2() 0, MAP_END
#define MAP_GET_END1(...) MAP_GET_END2
#define MAP_GET_END(...) MAP_GET_END1
#define MAP_NEXT0(test, next, ...) next MAP_OUT
#define MAP_NEXT1(test, next) MAP_NEXT0(test, next, 0)
#define MAP_NEXT(test, next)  MAP_NEXT1(MAP_GET_END test, next)

#define MAP0(f, x, peek, ...) f(x) MAP_NEXT(peek, MAP1)(f, peek, __VA_ARGS__)
#define MAP1(f, x, peek, ...) f(x) MAP_NEXT(peek, MAP0)(f, peek, __VA_ARGS__)

#define MAP(f, ...) EVAL(MAP1(f, __VA_ARGS__, ()()(), ()()(), ()()(), 0))

/* -------------------------------------------------------------------------- */

typedef int (*rx_run_fn)();

struct rx__data {
    const char *name;
    int value;
    rx_run_fn run;
};

int run() { return 999; }

#ifdef __cplusplus
#define RX__WRAP_ASSIGNMENT(x) out x;
#define RX__MAKE_DATA(...)                                                     \
    []() -> struct rx__data {                                                  \
        struct rx__data out = {};                                              \
        MAP(RX__WRAP_ASSIGNMENT, __VA_ARGS__);                                 \
        return out;                                                            \
    }()
#else
#define RX__MAKE_DATA(...) { __VA_ARGS__ }
#endif

static const struct rx__data foo
    = RX__MAKE_DATA(.name = "abc", .value = 123, .run = run);

It's all good except that, since the rx__data struct can be attached to both test suites and test cases, I'd like to have a mechanism that allows me to know if a data member has been explicitely set or not by the user. This way, I can infer the final data to apply to a test case by:

  • retrieving the data to inherit from the parent test suite.
  • overriding only the members from the test suite that were explicitely set onto the test case.

For example

RX_TEST_SUITE(my_suite, .name = "abc", .value = 123, .run = run);

RX_TEST_CASE(my_suite, my_case, .value = 666)
{
    ...
}

would result in ‘my_case’ having the data {.name = "abc", .value = 666, .run = run} attached to it.

For this to work, I thought of adding a boolean value for each field, to keep track of what has been explicitely defined or not by the user:

typedef int (*rx_run_fn)();

struct rx__data {
    const char *name;
    int value;
    rx_run_fn run;

    int name_defined;
    int value_defined;
    int run_defined;
};

int run() { return 999; }

#ifdef __cplusplus
#define RX__ARG(field, value) out.field = value; out.field##_defined = 1
#define RX__MAKE_DATA(...)                                                     \
    []() -> struct rx__data {                                                  \
        struct rx__data out = {};                                              \
        __VA_ARGS__;                                                           \
        return out;                                                            \
    }();
#else
#define RX__ARG(field, value) .field = value, .field##_defined = 1
#define RX__MAKE_DATA(...) { __VA_ARGS__ }
#endif

#define RX_NAME_ARG(x) RX__ARG(name, x)
#define RX_VALUE_ARG(x) RX__ARG(value, x)
#define RX_RUN_ARG(x) RX__ARG(run, x)

static const struct rx__data foo
    = RX__MAKE_DATA(RX_NAME_ARG("abc"), RX_VALUE_ARG(123), RX_RUN_ARG(run));

And it's all working great here again, except that the user now has to set the arguments using macros instead of the previous designated initializer syntax.

So the questions is: how can I keep track of these user-defined struct members while preserving the designated initializer syntax?

Note: if possible, I'd really like to have a robust way of detecting if a member was defined, so no in-band indicators—that is, no ”if this member has this magic value, then it's likely that is wasn't explicitely set”.

Aucun commentaire:

Enregistrer un commentaire