cstructc-preprocessor

Passing comma-separated struct initializers to C preprocessor macros


Long Preamble

There are many cases where it's useful to associate an enum with array elements so the enum names always stay in sync with the array elements. For this kind of thing, a "macro that defines macros" does the job nicely:

#define EXPAND_COLORS                 \
  DEFINE_COLOR(COLOR_RED, 0xff0000)   \
  DEFINE_COLOR(COLOR_GREEN, 0x00ff00) \
  DEFINE_COLOR(COLOR_BLUE, 0x0000ff)

// The following expands into an enum with a defined symbol for each color:
//     typedef enum { COLOR_RED, COLOR_GREEN, COLOR_BLUE, } color_t;
//
#undef DEFINE_COLOR
#define DEFINE_COLOR(_name, _value) _name,
typedef enum { EXPAND_COLORS } color_t;

// The following expands in to an array of color values:
//     int color_map[] = { 0xff0000, 0x00ff00, 0x0000ff, };
//
#undef DEFINE_COLOR
#define DEFINE_COLOR(_name, _value) _value,
int color_map[] = { EXPAND_COLORS };

// The following expands into an array of strings, one string for each color:
//     const char *color_names[] = { "COLOR_RED", "COLOR_GREEN", "COLOR_BLUE", };
//
#undef DEFINE_COLOR
#define DEFINE_COLOR(_name, _value) #_name,
const char *color_names[] = { EXPAND_COLORS };

You can see how this guarantees that using the symbol COLOR_GREEN as an index will always refer to the value 0x00ff00 in color_map[] and the string "COLOR_GREEN" in color_names[], and you can easily add new colors:

  ...
  DEFINE_COLOR(COLOR_AUBERGENE, 0x693b58)  \

... and you only need to make the addition in one place.

Now for the question

What if instead of a representing color as a single hex value, I want to use a struct:

typedef struct {
  uint8_t r;
  uint8_t g;
  uint8_t b;
} color_t;

I would LIKE to do something like this:

#define EXPAND_COLORS                                              \
  DEFINE_COLOR(COLOR_RED, {.r = 0xff, .g = 0x00, .b = 0x00})       \
  DEFINE_COLOR(COLOR_GREEN, {.r = 0x00, .g = 0xff, .b = 0x00})     \
  DEFINE_COLOR(COLOR_BLUE, {.r = 0x00, .g = 0x00, .b = 0xff})      \
  DEFINE_COLOR(COLOR_AUBERGENE, {.r = 0x69, .g = 0x3b, .b = 0x58})

// I would this to expand to:
//     color_t color_map[] = { {.r = 0xff, .g = 0x00, .b = 0x00}, 
//                             {.r = 0x00, .g = 0xff, .b = 0x00}, 
//                             ...
//                           }; 
#undef DEFINE_COLOR
#define DEFINE_COLOR(_name, _value) _value,
color_t color_map[] = { EXPAND_COLORS };

The above won't work as written because the C preprocessor views the commas as field separators:

error: macro "DEFINE_COLOR" passed 4 arguments, but takes just 2

Is there some syntactical trick I can use to pass the struct initializer to the C preprocessor so it interprets the initializer as a single argument?

(The best I've come up with is to pass the struct values as separate argumements to the macro. This would work, but my actual use case has complex nested structs with lots of preset fields, so it would be preferable to make the nested structure visible.)


Solution

  • You can use ... to accept an "argument" with commas in it:

    #define DEFINE_COLOR(_name, ...) __VA_ARGS__,
    

    That said, it is (IMO) much better to make the macro an argument, so you can give it a reasonable name and don't have to #undef and redefine the same macro, and so it is clearer what is going on:

    #define COLOR_LIST(M)                                   \
      M(COLOR_RED, {.r = 0xff, .g = 0x00, .b = 0x00})       \
      M(COLOR_GREEN, {.r = 0x00, .g = 0xff, .b = 0x00})     \
      M(COLOR_BLUE, {.r = 0x00, .g = 0x00, .b = 0xff})      \
      M(COLOR_AUBERGENE, {.r = 0x69, .g = 0x3b, .b = 0x58})
    
    
    #define COLOR_ENUM_VAL(_name, ...) _name,
    #define COLOR_INIT_VAL(_name, ...) __VA_ARGS__,
    
    enum colors { COLOR_LIST(COLOR_ENUM_VAL) };
    color_t color_map[] = { COLOR_LIST(COLOR_INIT_VAL) };