Search code examples
cserializationpreprocessorx-macros

How to get reflection-like functionality in C, without x-macros


Related to this question on Software Engineering about easily serializing various struct contents on demand, I found an article which uses x-macros to create struct metadata needed for "out of the box" struct serialization. I've also seen similar techniques for "smart enums", but it boils down to the same principle, getting a string representation of an enum, or a struct's field value by its name, or something similar.

However experienced C programmers on Stack Overflow state that the x-macros should be avoided as the "last resort":

I could probably find many more related threads, but unfortunately I didn't bookmark them so this is just some Google-fu.

Perhaps the correct answer is something like Protocol Buffers? But why would creating struct definition in a different language (.proto definitions) and then running a build step to generate C files be preferable to using the built-in preprocessor for the same thing? And the issue is that these techniques still don't let me retrieve a single struct by name, I must share the same definition between two projects and keep them in sync.

So the question is then: If x-macros are "last resort", which approach for my problem (easily serializing various internal data when requested from a different device) would be "first resort", or anything before resorting to macro hell?


Solution

  • With a bit of preprocessor magic taken from Boost we can make a macro able to generate reflectable enums.

    I managed to construct a simple proof-of-concept implementation provided below.


    First, the usage. Following:

    ReflEnum(MyEnum,
        (first)
        (second , 42)
        (third)
    )
    

    Gets expanded to:

    enum MyEnum
    {
        first,
        second = 42,
        third,
    };
    
    const char *EnumToString_MyEnum(enum MyEnum param)
    {
        switch (param)
        {
          case first:
            return "first";
          case second:
            return "second";
          case third:
            return "third";
          default:
            return "<invalid>";
        }
    }
    

    Thus a complete program could look like this:

    #include <stdio.h>
    
    /*
     * Following is generated by the below ReflEnum():
     *   enum MyEnum {first, second = 42, third};
     *   const char *EnumToString_MyEnum(enum MyEnum value) {}
    */
    ReflEnum(MyEnum,
        (first)
        (second , 42)
        (third)
    )
    
    int main()
    {
        enum MyEnum foo = second;
        puts(EnumToString_MyEnum(foo));  // -> "second"
        puts(EnumToString_MyEnum(43));   // -> "third"
        puts(EnumToString_MyEnum(9001)); // -> "<invalid>"
    }
    

    And here is the implementation itself.

    It consists of two parts. The code itself and a preprocessor magic header shamelessly ripped off from Boost.

    The code:

    #define ReflEnum_impl_Item(...) PPUTILS_VA_CALL(ReflEnum_impl_Item_, __VA_ARGS__)(__VA_ARGS__)
    #define ReflEnum_impl_Item_1(name)        name,
    #define ReflEnum_impl_Item_2(name, value) name = value,
    
    #define ReflEnum_impl_Case(...) case PPUTILS_VA_FIRST(__VA_ARGS__): return PPUTILS_STR(PPUTILS_VA_FIRST(__VA_ARGS__));
    
    #define ReflEnum(name, seq) \
        enum name {PPUTILS_SEQ_APPLY(seq, ReflEnum_impl_Item)}; \
        const char *EnumToString_##name(enum name param) \
        { \
            switch (param) \
            { \
                PPUTILS_SEQ_APPLY(seq, ReflEnum_impl_Case) \
                default: return "<invalid>"; \
            } \
        }
    

    It shouldn't be too hard to extend the code to support string->enum conversion; ask in the comments if you're not sure.

    The magic:

    Note that the preprocessor magic has to be generated by a script, and you have to choose a maximum enum size when generating it. The generation is easy and left as an exercise to the reader.

    Boost defaults the size to 64, the code below was generated for size 4.

    #define PPUTILS_E(...) __VA_ARGS__
    
    #define PPUTILS_VA_FIRST(...) PPUTILS_VA_FIRST_IMPL_(__VA_ARGS__,)
    #define PPUTILS_VA_FIRST_IMPL_(x, ...) x
    
    #define PPUTILS_PARENS(...) (__VA_ARGS__)
    #define PPUTILS_DEL_PARENS(...) PPUTILS_E __VA_ARGS__
    
    #define PPUTILS_CC(a, b) PPUTILS_CC_IMPL_(a,b)
    #define PPUTILS_CC_IMPL_(a, b) a##b
    
    #define PPUTILS_CALL(macro, ...) macro(__VA_ARGS__)
    
    #define PPUTILS_VA_SIZE(...) PPUTILS_VA_SIZE_IMPL_(__VA_ARGS__,4,3,2,1,0)
    #define PPUTILS_VA_SIZE_IMPL_(i1,i2,i3,i4,size,...) size
    
    #define PPUTILS_STR(...) PPUTILS_STR_IMPL_(__VA_ARGS__)
    #define PPUTILS_STR_IMPL_(...) #__VA_ARGS__
    
    #define PPUTILS_VA_CALL(name, ...) PPUTILS_CC(name, PPUTILS_VA_SIZE(__VA_ARGS__))
    
    #define PPUTILS_SEQ_CALL(name, seq) PPUTILS_CC(name, PPUTILS_SEQ_SIZE(seq))
    
    #define PPUTILS_SEQ_DEL_FIRST(seq) PPUTILS_SEQ_DEL_FIRST_IMPL_ seq
    #define PPUTILS_SEQ_DEL_FIRST_IMPL_(...)
    
    #define PPUTILS_SEQ_FIRST(seq) PPUTILS_DEL_PARENS(PPUTILS_VA_FIRST(PPUTILS_SEQ_FIRST_IMPL_ seq,))
    #define PPUTILS_SEQ_FIRST_IMPL_(...) (__VA_ARGS__),
    
    #define PPUTILS_SEQ_SIZE(seq) PPUTILS_CC(PPUTILS_SEQ_SIZE_0 seq, _VAL)
    #define PPUTILS_SEQ_SIZE_0(...) PPUTILS_SEQ_SIZE_1
    #define PPUTILS_SEQ_SIZE_1(...) PPUTILS_SEQ_SIZE_2
    #define PPUTILS_SEQ_SIZE_2(...) PPUTILS_SEQ_SIZE_3
    #define PPUTILS_SEQ_SIZE_3(...) PPUTILS_SEQ_SIZE_4
    #define PPUTILS_SEQ_SIZE_4(...) PPUTILS_SEQ_SIZE_5
    // Generate PPUTILS_SEQ_SIZE_i
    #define PPUTILS_SEQ_SIZE_0_VAL 0
    #define PPUTILS_SEQ_SIZE_1_VAL 1
    #define PPUTILS_SEQ_SIZE_2_VAL 2
    #define PPUTILS_SEQ_SIZE_3_VAL 3
    #define PPUTILS_SEQ_SIZE_4_VAL 4
    // Generate PPUTILS_SEQ_SIZE_i_VAL
    
    #define PPUTILS_SEQ_APPLY(seq, macro) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, seq)(macro, seq)
    #define PPUTILS_SEQ_APPLY_0(macro, seq)
    #define PPUTILS_SEQ_APPLY_1(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq))
    #define PPUTILS_SEQ_APPLY_2(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
    #define PPUTILS_SEQ_APPLY_3(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
    #define PPUTILS_SEQ_APPLY_4(macro, seq) PPUTILS_CALL(macro, PPUTILS_SEQ_FIRST(seq)) PPUTILS_SEQ_CALL(PPUTILS_SEQ_APPLY_, PPUTILS_SEQ_DEL_FIRST(seq))(macro, PPUTILS_SEQ_DEL_FIRST(seq))
    // Generate PPUTILS_SEQ_APPLY_i