X-Macros (C)
X-Macros are a C preprocessor idiom that can be used to generate code for any data that resembles a plain table. It can be considered a form of table-driven metaprogramming.
Example for an Entity Table
The table structure can be defined in its own header file called entity_table.h
.
Note that the header should not have an include guard or #pragma once
directive,
since it might be included more than once in the same file. Every passed
argument to the macro can be conceptually understood as an entry to a table
column.
// Type Name HP texture_file tex_coords animations #define UV(u,v) {u, v} #define ANIMS(...) {__VA_ARGS__} ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) )
In the C source code, the ENTITY
macro can be defined, upon which the table in
its entirety is #include
'd and then the ENTITY
macro gets undefined.
#define ENTITY(type, name, hp, tex_file, tex_coords, animations) ENTITY_TYPE_##type, enum entity_type_e { //#include "entity_table.h" // Type Name HP texture_file tex_coords animations #define UV(u,v) {u, v} #define ANIMS(...) {__VA_ARGS__} ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) ) ENTITY_TYPE_COUNT }; #undef ENTITY #define ENTITY(type, name, hp, tex_file, tex_coords, animations) #name , const char* entity_names[] = { //#include "entity_table.h" // Type Name HP texture_file tex_coords animations #define UV(u,v) {u, v} #define ANIMS(...) {__VA_ARGS__} ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) ) "entity_count" }; #undef ENTITY #define ENTITY(type, name, hp, tex_file, tex_coords, animations) hp , int entity_hp_table[] = { //#include "entity_table.h" // Type Name HP texture_file tex_coords animations #define UV(u,v) {u, v} #define ANIMS(...) {__VA_ARGS__} ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) ) }; #undef ENTITY #define ENTITY(type, name, hp, tex_file, tex_coords, animations) tex_coords , int entity_tex_coords[][2] = { //#include "entity_table.h" // Type Name HP texture_file tex_coords animations #define UV(u,v) {u, v} #define ANIMS(...) {__VA_ARGS__} ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) ) }; #undef ENTITY typedef struct animation_t { int uv[2][4]; } animation_t; #define ENTITY(type, name, hp, tex_file, tex_coords, animations) animations , animation_t entity_animation_table[5][1] = { //#include "entity_table.h" //// Type Name HP texture_file tex_coords animations //#define UV(u,v) {u, v} //#define ANIMS(...) {__VA_ARGS__} //ENTITY(HERO, "Hero", 100, "hero.png", UV(0,0) , ANIMS(UV(16,0)) ) //ENTITY(MONSTER, "Monster", 40, "enemies.png", UV(16,32) , ANIMS(UV(0,0)) ) //ENTITY(PEASANT, "Peasant", 60, "npcs.png", UV(16,16) , ANIMS(UV(0,0)) ) //ENTITY(PRINCESS, "Princess", 80, "npcs.png", UV(64,16) , ANIMS(UV(0,0)) ) //ENTITY(GOBLIN, "Goblin", 30, "enemies.png", UV(32,32) , ANIMS(UV(0,0)) ) {{16,0}}, {{0,0}}, {{0,0}}, {{0,0}}, {{0,0}} }; #undef ENTITY int main() { for (int i = 0; i < ENTITY_TYPE_COUNT; i++) { printf("%i: %s has %i HP and it's UVs at (%i,%i).\n", i, entity_names[i], entity_hp_table[i], entity_tex_coords[i][0], entity_tex_coords[i][1]); /* for (int j = 0; j < sizeof(entity_animation_table[i]); j++) */ /* { */ /* //printf(" Animation data:", entity_animation_table[i][j] ); */ /* int* uv = entity_animation_table[i][j].uv[0]; */ /* printf("%i %i", uv[0], uv[1]); */ /* } */ /* printf("\n"); */ } }
DLL Function Loading Example
Load functions from a DLL without repeating yourself
/* declare all functions to be loaded from dll */ #define DLL_FUNCTION_TABLE(X) \ X(int, on_load, state_t**) \ X(int, on_reload, state_t*) \ X(void, on_update, state_t*, float dt) /* declare function prototypes */ typedef struct dll_t { #define DLL_FUNCTIONS(ret,func,...) ret (*func)(__VA_ARGS__); DLL_TABLE(DLL_FUNCTIONS) void* handle; time_t last_mod; const char* file; } dll_t; int platform_load_code(dll_t* dll) { /* unload old dll */ if (dll->handle) { /* reset all to null */ #define SET_TO_NULL(ret, func, ...) dll->func = NULL; DLL_TABLE(SET_TO_NULL) dll_unload(dll->handle); dll->handle = NULL; } /* load dll functions (and print out any not found) */ dll->handle = dll_load(dll->file); #define LOAD_FUNCTION(ret, func, ...) \ dll->func = (ret (*)(__VA_ARGS__)) SDL_LoadFunction(dll->handle, #func); \ if (!dll->func) { printf("Error finding function: %s\n", #func); } DLL_TABLE(LOAD_FUNCTION) return 1; }
Localization Example
Idea: Write out a valid C file containing language definitions and localized
strings that can masquerade as a .csv
file:
#define LOCALIZATION(X) /* string_id ,*/ LANGUAGE(EN) /*,*/ LANGUAGE(DE) /*,*/ LANGUAGE(FR) /*,*/ LANGUAGE(CH) /*,*/ LANGUAGE(COUNT)\ X(STRING_HELLO, "Hello there" , "Hey du" , "Bonjour" , "" , )\ X(STRING_BYE, "Goodbye" , "Tschau" , "Au revoir" , "" , )\ X(STRING_TEST, "This is a test string" , "Dies ist ein Test" , "C’est un test" , "" , )\ X(STRING_HERE, "here" , "Hier" , "ici" , "使用的都是" , "..." , )\ X(STRING_END, "End" , "Ende" , "Fin" , "" , "???" , )
Usage code looks like this:
//#include "loc.csv" #define LOCALIZATION(X) /* string_id ,*/ LANGUAGE(EN) /*,*/ LANGUAGE(DE) /*,*/ LANGUAGE(FR) /*,*/ LANGUAGE(CH) /*,*/ LANGUAGE(COUNT)\ X(STRING_HELLO, "Hello there" , "Hey du" , "Bonjour" , "" , )\ X(STRING_BYE, "Goodbye" , "Tschau" , "Au revoir" , "" , )\ X(STRING_TEST, "This is a test string" , "Dies ist ein Test" , "C’est un test" , "" , )\ X(STRING_HERE, "here" , "Hier" , "ici" , "使用的都是" , "..." , )\ X(STRING_END, "End" , "Ende" , "Fin" , "" , "???" , ) #define PLAYER_NAME "Thomas" #define LANGUAGE(lang) LANGUAGE_##lang, #define EXPAND_TO_NOTHING(...) enum { LOCALIZATION(EXPAND_TO_NOTHING) }; #undef LANGUAGE #define LANGUAGE(lang) case LANGUAGE_##lang : return #lang ; const char* language_to_string(int language_id) { switch(language_id) { LOCALIZATION(EXPAND_TO_NOTHING) default: return "Unknown Language"; } } #undef LANGUAGE #define LANGUAGE(lang) // NOTE: define to nothing needed #define EXPAND_ID(id, en, de, fr, ...) id, enum { LOCALIZATION(EXPAND_ID) STRING_ID_COUNT }; extern int language; /* NOTE: update this macro when adding new languages */ #define EXPAND_STRING(id, en, de, fr, ch, ...) case id : switch (language) { case (LANGUAGE_EN) : return en; case (LANGUAGE_DE) : return de; case (LANGUAGE_FR) : return fr; case (LANGUAGE_CH) : return ch; }; const char* loc_str(int string_id) { switch(string_id) { LOCALIZATION(EXPAND_STRING); default : return "Unknown string id!\n"; } } //#define S(s) #s //const char* str = S("Hello there"); int language; #include <stdio.h> int main () { for (int i = 0; i < STRING_ID_COUNT; i++) { for (int j = 0; j < LANGUAGE_COUNT; j++) { language = j; printf("%s: ", language_to_string(language)); printf("%s ", loc_str(i)); printf("\n"); } printf("\n"); } }
Translators can open loc.csv
in a spreadsheet editor (or normal text editor) and
translate the string for every row into their language - without having to know
any C syntax. They can even add new languages (meaning new columns) to the csv
without breaking things - this might, however, require some knowledge about the
syntax to not break things.
N.B.: Even though we bake the pseudo-CSV into the executable, this supports changing the language at runtime.
Drawbacks:
- They can't see their translations in-game without being able to recompile
- The file doesn't look as pretty in a spreadsheet editor as in a text editor.
- Conversely, if you make it look in the spreadsheet editor, it will look bad as C code.
- Newly added languages require manual changes to the
EXPAND_STRING
macro. However, you could just add a column for every language of interest once and avoid this drawback completely. - Every string they add needs to be in valid double quotes
- Some options need to be set/changed for their spreadsheet editor (e.g., the
editor might use curly quotes
“
when typing in"
by default)
Introspection Example
Idea: format a struct definition as an X-Macro table and use its fields to generate a type info array and a printing function for the struct.
#if !defined(_MSC_VER) #define OFFSET_OF(type, member) __builtin_offsetof(type, member) #else #define OFFSET_OF(s,m) ((size_t)&(((s*)0)->m)) #endif /* struct definitions using X-macros */ #define FILL_FIELDS(b,c,d, ...) b c d; #define STRUCT_NAME_START(name) typedef struct name { #define STRUCT_NAME_END(name) } name #define STRUCT(name) STRUCT_NAME_START(name) name(FILL_FIELDS) STRUCT_NAME_END(name) /* array containing serializing data using X-macros */ typedef struct meta_struct_t { size_t offset; size_t size; const char* name; const char* type; } meta_struct_t; #define create_print_function(struct_name) \ void struct_name##_print_info( struct_name foo) { \ int member_count = (sizeof(struct_name##_type_info)/sizeof(struct_name##_type_info[0])); \ printf("member count of %s: %i \n", #struct_name, member_count); \ for (int i = 0; i < member_count; i++) { \ printf(" member %s:\n", struct_name##_type_info[i].name); \ printf(" offset: %zu \n", struct_name##_type_info[i].offset ); \ printf(" size: %zu \n", struct_name##_type_info[i].size ); \ printf(" type: %s \n", struct_name##_type_info[i].type ); \ /*printf(" value: %.*s \n", struct_name##_type_info[i].size, */ \ /*((char*) &foo) + struct_name##_type_info[i].offset);*/ \ } \ } #define META_MEMBER(b,c,d,...) { OFFSET_OF(__VA_ARGS__, c) , sizeof(b), #c, #b#d }, // NOTE workaround #define META(name) meta_struct_t name##_type_info[] = { name(META_MEMBER, name) }; create_print_function(name) /* usage code */ #define entity_t(X, ...) \ X(char*, name, , __VA_ARGS__) \ X(int, hp, , __VA_ARGS__) \ X(float, x_pos, , __VA_ARGS__) STRUCT(entity_t); META(entity_t); #define thing_t(X, ...) \ X(int, foo , , __VA_ARGS__) \ X(float, bar , , __VA_ARGS__) \ X(char, baz , [4], __VA_ARGS__) \ X(entity_t, ent , , __VA_ARGS__) STRUCT(thing_t); META(thing_t); #include <stdio.h> int main() { thing_t thing = { 1, 5, "ab\0" }; printf("%i %f %s\n", thing.foo, thing.bar, thing.baz); entity_t entity = {"monster", 100, 2.4f}; thing_t_print_info(thing); entity_t_print_info(entity); }
Drawbacks:
- You now have to write out or change every struct definition to an X-Macro table if you wish to introspect it.
- Surprisingly, some editor features such as jump-to-definition should still
work, since
struct_name_t
is the identifier for both the macro and structure. So the editor jumps to where we defined our structure table.
Serialization/Printing Example
See
- https://en.wikibooks.org/wiki/C_Programming/Serialization
- https://en.wikibooks.org/wiki/C_Programming/Preprocessor_directives_and_macros
- https://natecraun.net/articles/struct-iteration-through-abuse-of-the-c-preprocessor.html
#define FILL_FIELDS(type,name, ...) type name; #define STRUCT_NAME_START(name) typedef struct name { #define STRUCT_NAME_END(name) } name #define STRUCT(name) STRUCT_NAME_START(name) name(FILL_FIELDS) STRUCT_NAME_END(name) typedef struct entity_t { char foo; float bz; } entity_t; #define entity_t(X, ...) \ X(char, foo) \ X(float, bz) \ //STRUCT(entity_t); #define state_t(X, ...) \ X(char, foo) \ X(entity_t, bz) \ X(int, baz) \ X(char, bar ) STRUCT(state_t); #define primitive_types(X, ...) \ X(char, ) \ X(float, ) \ X(int, ) \ X(char, ) void serialize_char(char c) { printf("%c ", c); } void serialize_float(float f) { printf("%f ", f); } void serialize_int(int i) { printf("%i ", i); } #define SERIALIZE(type, name) serialize_##type(t.name); #define GENERATE_SERIALIZE_FUNCTION(member) \ void serialize_##member(member t) \ { \ member(SERIALIZE) \ } GENERATE_SERIALIZE_FUNCTION(entity_t) GENERATE_SERIALIZE_FUNCTION(state_t) state_t test = {'c', {'a', 4.2f}, 1337, 'b'}; serialize_state_t(test);
Expose variables for tweaking in UI or similar
Suppose you want to have a boolean variable that controls some functionality exposed via a UI, console, etc. so you can control it's value at runtime:
/* code that uses the boolean for functionality */ int kill_at_0_health = 1; if(kill_at_0_health) { if(entity->health <= 0) { /* kill entity */ } } /* code that wants to tweak the boolean (e.g. UI) will need access and a string and a default value for every boolean */ extern int kill_at_0_health; int kill_at_0_health_default_value = 1; const char* kill_at_0_health_string = "kill_at_0_health_string";
Instead of duplicating this code everywhere, consider defining an X-macro table that generates a global table that can be accessed via macros for both the usage code and UI code.
#define TWEAKABLE_BOOLS(X) \ X(KILL_ENTITIES_AT_0_HEALTH, "Kill entities at 0 health?", 1) \ X(UNLIMITED_AMMO, "Unlimited ammo?", 0) /* enum value description string default value*/ #define FILL_ENUM(enum,...) TWEAKABLE_BOOL_##enum, enum { TWEAKABLE_BOOLS(FILL_ENUM) TWEAKABLE_BOOLS_COUNT }; typedef struct tweakable_bool_t { int bool; const char* description; int default_value; } tweakable_bool_t; #define FILL_TABLE(enum,description,default) {default, description}, static tweakable_bool_t g_tweakable_bool_table[TWEAKABLE_BOOLS_COUNT] = { TWEAKABLE_BOOLS(FILL_TABLE) }; #define tweakable_bool(name) (g_tweakable_bool_table[TWEAKABLE_BOOL_##name].bool) /* code that uses the boolean for functionality */ if (tweakable_bool(KILL_ENTITIES_AT_0_HEALTH)) { printf("killed\n"); } if (tweakable_bool(UNLIMITED_AMMO)) { printf("no\n"); }
Same thing could be done for tweakable floats:
#define TWEAKABLE_FLOATS(X) \ /* description default min max */ X(MAX_PLAYER_ATTACK_RANGE, "Max attack range for the player", 1.5f, 0.f, 3.f )
TODO Generating Stacks
/* define stack table */ #define UI_STACK_TABLE(X) \ X(Parent, UI_Box*, &ui_g_nil_box ) \ X(Flags, UI_BoxFlags, 0 ) \ X(FixedX, float, 0 ) \ X(FixedY, float, 0 ) /* name type default */ /* declare stack node types */ typedef struct UI_ParentNode UI_ParentNode; struct UI_ParentNode{UI_ParentNode *next; UI_Box * v;}; typedef struct UI_FlagsNode UI_FlagsNode; struct UI_FlagsNode{UI_FlagsNode *next; UI_BoxFlags v;}; typedef struct UI_FixedXNode UI_FixedXNode; struct UI_FixedXNode{UI_FixedXNode *next; F32 v;}; typedef struct UI_FixedYNode UI_FixedYNode; struct UI_FixedYNode{UI_FixedYNode *next; F32 v;}; /* macro to declare all default stack tops */ #define UI_DeclStackNils \ struct { \ UI_ParentNode parent_nil_stack_top;\ UI_FlagsNode flags_nil_stack_top;\ UI_FixedXNode fixed_x_nil_stack_top;\ UI_FixedYNode fixed_y_nil_stack_top;\ } /* macro to initialize all stack tops to their default values */ #define UI_InitStackNils(state) \ state->parent_nil_stack_top.v = &ui_g_nil_box;\ state->flags_nil_stack_top.v = 0;\ state->fixed_x_nil_stack_top.v = 0;\ state->fixed_y_nil_stack_top.v = 0; /* macro to declare all stack nodes & free lists */ #define UI_DeclStacks \ struct {\ struct { UI_ParentNode *top; UI_ParentNode *free; B32 auto_pop; } parent_stack;\ struct { UI_FlagsNode *top; UI_FlagsNode *free; B32 auto_pop; } flags_stack;\ struct { UI_FixedXNode *top; UI_FixedXNode *free; B32 auto_pop; } fixed_x_stack;\ struct { UI_FixedYNode *top; UI_FixedYNode *free; B32 auto_pop; } fixed_y_stack;\ } /* macro to init all stack nodes */ #define UI_InitStacks(state) \ state->parent_stack.top = &state->parent_nil_stack_top; state->parent_stack.free = 0; state->parent_stack.auto_pop = 0;\ state->flags_stack.top = &state->flags_nil_stack_top; state->flags_stack.free = 0; state->flags_stack.auto_pop = 0;\ state->fixed_x_stack.top = &state->fixed_x_nil_stack_top; state->fixed_x_stack.free = 0; state->fixed_x_stack.auto_pop = 0;\ state->fixed_y_stack.top = &state->fixed_y_nil_stack_top; state->fixed_y_stack.free = 0; state->fixed_y_stack.auto_pop = 0; /* macro to auto-pop all stacks */ #define UI_AutoPopStacks(state) \ if(state->parent_stack.auto_pop) { UI_PopParent(); state->parent_stack.auto_pop = 0; }\ if(state->flags_stack.auto_pop) { UI_PopFlags(); state->flags_stack.auto_pop = 0; }\ if(state->fixed_x_stack.auto_pop) { UI_PopFixedX(); state->fixed_x_stack.auto_pop = 0; }\ if(state->fixed_y_stack.auto_pop) { UI_PopFixedY(); state->fixed_y_stack.auto_pop = 0; } /* decls for the stack function operation headers */ UI_Box * UI_TopParent(void); UI_BoxFlags UI_TopFlags(void); F32 UI_TopFixedX(void); F32 UI_TopFixedY(void); /* defer-loop helpers TODO can't be done with x-macro */ #define UI_Parent(v) DeferLoop(UI_PushParent(v), UI_PopParent()) #define UI_Flags(v) DeferLoop(UI_PushFlags(v), UI_PopFlags()) #define UI_FixedX(v) DeferLoop(UI_PushFixedX(v), UI_PopFixedX()) #define UI_FixedY(v) DeferLoop(UI_PushFixedY(v), UI_PopFixedY()) /* generic stack operation definitions before generating functions */ #define UI_StackTopImpl(state, name_upper, name_lower) return state->name_lower##_stack.top->v; #define UI_StackPushImpl(state, name_upper, name_lower, type, new_value) \ UI_##name_upper##Node *node = state->name_lower##_stack.free;\ if(node != 0) {StackPop(state->name_lower##_stack.free);}\ else {node = PushArray(UI_FrameArena(), UI_##name_upper##Node, 1);}\ type old_value = state->name_lower##_stack.top->v;\ node->v = new_value;\ StackPush(state->name_lower##_stack.top, node);\ state->name_lower##_stack.auto_pop = 0;\ return old_value; #define UI_StackPopImpl(state, name_upper, name_lower) \ UI_##name_upper##Node *popped = state->name_lower##_stack.top;\ if(popped != &state->name_lower##_nil_stack_top) {\ StackPop(state->name_lower##_stack.top);\ StackPush(state->name_lower##_stack.free, popped);\ state->name_lower##_stack.auto_pop = 0;\ }\ return popped->v; #define UI_StackSetNextImpl(state, name_upper, name_lower, type, new_value) \ UI_##name_upper##Node *node = state->name_lower##_stack.free;\ if(node != 0) {StackPop(state->name_lower##_stack.free);}\ else {node = PushArray(UI_FrameArena(), UI_##name_upper##Node, 1);}\ type old_value = state->name_lower##_stack.top->v;\ node->v = new_value;\ StackPush(state->name_lower##_stack.top, node);\ state->name_lower##_stack.auto_pop = 1;\ return old_value; /* decls for the stack operation implementations (top,push,pop,setnext) */ root_function UI_Box * UI_TopParent(void) { UI_StackTopImpl(ui_state, Parent, parent) } root_function UI_BoxFlags UI_TopFlags(void) { UI_StackTopImpl(ui_state, Flags, flags) } root_function F32 UI_TopFixedX(void) { UI_StackTopImpl(ui_state, FixedX, fixed_x) } root_function F32 UI_TopFixedY(void) { UI_StackTopImpl(ui_state, FixedY, fixed_y) } root_function UI_Box * UI_PushParent(UI_Box * v) { UI_StackPushImpl(ui_state, Parent, parent, UI_Box *, v) } root_function UI_BoxFlags UI_PushFlags(UI_BoxFlags v) { UI_StackPushImpl(ui_state, Flags, flags, UI_BoxFlags, v) } root_function F32 UI_PushFixedX(F32 v) { UI_StackPushImpl(ui_state, FixedX, fixed_x, F32, v) } root_function F32 UI_PushFixedY(F32 v) { UI_StackPushImpl(ui_state, FixedY, fixed_y, F32, v) } root_function UI_Box * UI_PopParent(void) { UI_StackPopImpl(ui_state, Parent, parent) } root_function UI_BoxFlags UI_PopFlags(void) { UI_StackPopImpl(ui_state, Flags, flags) } root_function F32 UI_PopFixedX(void) { UI_StackPopImpl(ui_state, FixedX, fixed_x) } root_function F32 UI_PopFixedY(void) { UI_StackPopImpl(ui_state, FixedY, fixed_y) } root_function UI_Box * UI_SetNextParent(UI_Box * v) { UI_StackSetNextImpl(ui_state, Parent, parent, UI_Box *, v) } root_function UI_BoxFlags UI_SetNextFlags(UI_BoxFlags v) { UI_StackSetNextImpl(ui_state, Flags, flags, UI_BoxFlags, v) } root_function F32 UI_SetNextFixedX(F32 v) { UI_StackSetNextImpl(ui_state, FixedX, fixed_x, F32, v) } root_function F32 UI_SetNextFixedY(F32 v) { UI_StackSetNextImpl(ui_state, FixedY, fixed_y, F32, v) }
Avoiding #undef
's
see https://www.embedded.com/reduce-c-language-coding-errors-with-x-macros-part-1/
Instead of defining and then undefining the X-Macro everytime you use it, you can instead make the table accept an X-Macro as an argument:
#define ENTITY_TABLE(X) \ X(HERO, "Hero", 100, "hero.png" ) \ X(MONSTER, "Monster", 40, "enemies.png" ) \ X(PEASANT, "Peasant", 60, "npcs.png" ) \ X(PRINCESS, "Princess", 80, "npcs.png" ) \ X(GOBLIN, "Goblin", 30, "enemies.png" ) #define ENTITY_ENUM_ENTRY(a,b,c,d) ENTITY_TYPE_##a , enum entity_type_e { ENTITY_TABLE(ENTITY_ENUM_ENTRY) ENTITY_TYPE_COUNT }; #define ENTITY_ENUM_STRING_ENTRY(a,b,c,d) case ENTITY_TYPE_##a : return b ; const char* entity_type_as_string(enum entity_type_e type) { switch (type) { ENTITY_TABLE(ENTITY_ENUM_STRING_ENTRY) default: return "invalid type"; } }; enum entity_type_e type1 = ENTITY_TYPE_MONSTER; enum entity_type_e type2 = ENTITY_TYPE_PRINCESS; printf("%s (%i) and %s (%i)\n", entity_type_as_string(type1), type1, entity_type_as_string(type2), type2);
Avoiding compilation errors when adding columns
Suppose you have following simple use-case for a X-Macros:
#define ENTITY_TABLE \ X(HERO, "Hero", 100) \ X(MONSTER, "Monster", 40) \ X(GOBLIN, "Goblin", 30) #define X(a,b,c) ENTITY_TYPE_##a , enum entity_type_e { ENTITY_TABLE ENTITY_TYPE_COUNT }; #undef X #define X(a,b,c) case ENTITY_TYPE_##a : return b ; const char* entity_type_as_string(enum entity_type_e type) { switch (type) { ENTITY_TABLE default: return "invalid type"; } }; #undef X enum entity_type_e type1 = ENTITY_TYPE_MONSTER; enum entity_type_e type2 = ENTITY_TYPE_HERO; printf("%s (%i) and %s (%i)\n", entity_type_as_string(type1), type1, entity_type_as_string(type2), type2);
Now add a column to the table:
#define ENTITY_TABLE \ X(HERO, "Hero", 100, "texture_hero.png") \ X(MONSTER, "Monster", 40, "texture_monster.png") \ X(GOBLIN, "Goblin", 30, "texture_goblin.png")
Which causes compilation errors of the form:
error: macro "X" passed 4 arguments, but takes just 3
To avoid having to go in and add the new argument to every single #define
of
your X-Macro, you can use replace all arguments that are not of interest with an
ellipsis:
#define X(a,...) ENTITY_TYPE_##a , enum entity_type_e { /* ... */ }; #undef X #define X(a,b,...) case ENTITY_TYPE_##a : return b ; const char* entity_type_as_string(enum entity_type_e type) { /* ... */ }; #undef X
This will also work with the passed in X-Macros from before.
Do note however, that you can't place ...
in between arguments. This means you
should always append new columns to the table instead of inserting them between
existing columns. Additionally, the removal of columns is now more error-prone.
Possible Operations
Select rows based on the value of one of their entries
If you want to perform a selection on your table that would be akin to an SQL
query of the form SELECT * FROM table WHERE a = "foo";
, you are out of luck.
To get this functionality, you instead have to break up your table for every
value that you are interested in. Take following example from log.h:
/* entry type name value string */ LOG_ENTRY( SEVERITY, INFO, (1<<0), "[INFO ]") LOG_ENTRY( SEVERITY, WARN, (1<<1), "[WARN ]") LOG_ENTRY( SEVERITY, ERROR, (1<<2), "[ERROR]") LOG_ENTRY( SUBSYSTEM, PLATFORM, (1<<3), "[PLATF]") LOG_ENTRY( SUBSYSTEM, AUDIO, (1<<4), "[AUDIO]") LOG_ENTRY( CATEGORY, INIT, (1<<5), "[INIT ]") LOG_ENTRY( CATEGORY, SHUTDOWN, (1<<6), "[SHUTD]")
Suppose I want to select all entries based on what their type is. Since the
preprocessor is one-pass and I can't use the #if
in a loop checking for what
type equals, I instead needed to break the table up into multiple tables:
/* entry name value string */ LOG_ENTRY_SEVERITY( INFO, (1<<0), "[INFO ]") LOG_ENTRY_SEVERITY( WARN, (1<<1), "[WARN ]") LOG_ENTRY_SEVERITY( ERROR, (1<<2), "[ERROR]") LOG_ENTRY_SUBSYSTEM( PLATFORM, (1<<3), "[PLATF]") LOG_ENTRY_SUBSYSTEM( AUDIO, (1<<4), "[AUDIO]") LOG_ENTRY_CATEGORY( INIT, (1<<5), "[INIT ]") LOG_ENTRY_CATEGORY( SHUTDOWN, (1<<6), "[SHUTD]")
In the codegen, instead of checking for the value of type, I #undef
all the
tables I am not interested in:
enum { #define LOG_ENTRY_SEVERITY(name, value, string) LOG_SEVERITY_##name = value, #define LOG_ENTRY_SUBSYSTEM(name, value, string) #define LOG_ENTRY_CATEGORY(name, value, string) #include LOG_ENTRY_FILE #undef LOG_ENTRY_SEVERITY /* selectively sum up severity values */ #define LOG_ENTRY_SEVERITY(name, value, string) value | LOG_SEVERITY_MASK = #include LOG_ENTRY_FILE 0, #undef LOG_ENTRY_SEVERITY /* repeat for SUBSYSTEM table and CATEGORY table */ }
It's ugly and requires changes all around, but it can get the job done in a pinch.
Sum up all values of a column
If you want to sum up all entries in a column (e.g. table_value1 +
table_value2 + ... + table_valueN
), simply append a + 0;
after expanding the
X-Macros:
#define X(name, value) value + #define ENEMY_XP_TABLE \ X(SPIDER, 20) \ X(GOBLIN, 30) \ X(DRAGON, 150) int value_sum = ENEMY_XP_TABLE + 0; printf("%i\n", value_sum);
Any binary operator can be chained using this idiom, such as concatenating strings or performing a bitwise-or on a bunch of bitfields to compute a mask:
enum { #define LOG_ENTRY_SEVERITY(name, value, string, color) value | LOG_SEVERITY_MASK = #include LOG_ENTRY_FILE 0, };
Compute number of rows
For a table with an unknown number of rows (i.e. the entry count), define a struct with just 1-byte sized members:
#define ENTRY(a,b,c) uint8_t b; typedef struct { MY_TABLE } number_of_entries_t; #undef ENTRY #define NUMBER_OF_ENTRIES sizeof(number_of_entries_t)
Or use an enum (beware of polluting the global namespace):
#define ENTRY(a,b,c) ___##a, enum { MY_TABLE NUMBER_OF_ENTRIES } number_of_entries_t; #undef ENTRY
Compute Max(...)
of a column of integers
Suppose your table looks like the following:
#define MY_TABLE \ ENTRY(foo, 100, "Foo")\ ENTRY(bar, 80, "Bar")\ ENTRY(baz, 120, "Baz")
If you wish to know the value of the biggest integer in the second column, write
out a union
like so:
/* sizeof a union is the size of its largest member */ #define ENTRY(a,b,c) uint8_t __buf_##b[b]; typedef union { MY_TABLE } max_b_union; #undef ENTRY
In your code, simply use the sizeof()
operator to get the biggest integer at
compile-time (without needing to instantiate the union):
int max_b = sizeof(max_b_union); // == 120
Count number of columns for every row
There is a trick to count the number of arguments (up to a limit) given to a (variadic) macro by displacing an integer depending on the number of arguments passed:
#define _NUM_ARGS(X100, X99, X98, X97, X96, X95, X94, X93, X92, X91, X90, X89, X88, X87, X86, X85, X84, X83, X82, X81, X80, X79, X78, X77, X76, X75, X74, X73, X72, X71, X70, X69, X68, X67, X66, X65, X64, X63, X62, X61, X60, X59, X58, X57, X56, X55, X54, X53, X52, X51, X50, X49, X48, X47, X46, X45, X44, X43, X42, X41, X40, X39, X38, X37, X36, X35, X34, X33, X32, X31, X30, X29, X28, X27, X26, X25, X24, X23, X22, X21, X20, X19, X18, X17, X16, X15, X14, X13, X12, X11, X10, X9, X8, X7, X6, X5, X4, X3, X2, X1, N, ...) N #define NUM_ARGS(...) _NUM_ARGS(__VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1)
With that, the number of arguments of a single table entry can be computed:
#define _NUM_ARGS(X100, X99, X98, X97, X96, X95, X94, X93, X92, X91, X90, X89, X88, X87, X86, X85, X84, X83, X82, X81, X80, X79, X78, X77, X76, X75, X74, X73, X72, X71, X70, X69, X68, X67, X66, X65, X64, X63, X62, X61, X60, X59, X58, X57, X56, X55, X54, X53, X52, X51, X50, X49, X48, X47, X46, X45, X44, X43, X42, X41, X40, X39, X38, X37, X36, X35, X34, X33, X32, X31, X30, X29, X28, X27, X26, X25, X24, X23, X22, X21, X20, X19, X18, X17, X16, X15, X14, X13, X12, X11, X10, X9, X8, X7, X6, X5, X4, X3, X2, X1, N, ...) N #define NUM_ARGS(...) _NUM_ARGS(__VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) #define TABLE(X) \ X(FIRST, 2.0f, '3', FOURTH, 5, "6") printf("Has %i arguments\n", TABLE(NUM_ARGS));
Using a modified version of the NUM_ARGS
macro that appends a comma to each
integer, you can have a table that contains the number of arguments (i.e.
columns) for every row:
#define _NUM_ARGS_COMMA(X100, X99, X98, X97, X96, X95, X94, X93, X92, X91, X90, X89, X88, X87, X86, X85, X84, X83, X82, X81, X80, X79, X78, X77, X76, X75, X74, X73, X72, X71, X70, X69, X68, X67, X66, X65, X64, X63, X62, X61, X60, X59, X58, X57, X56, X55, X54, X53, X52, X51, X50, X49, X48, X47, X46, X45, X44, X43, X42, X41, X40, X39, X38, X37, X36, X35, X34, X33, X32, X31, X30, X29, X28, X27, X26, X25, X24, X23, X22, X21, X20, X19, X18, X17, X16, X15, X14, X13, X12, X11, X10, X9, X8, X7, X6, X5, X4, X3, X2, X1, N, ...) N, #define NUM_ARGS_COMMA(...) _NUM_ARGS_COMMA(__VA_ARGS__, 100, 99, 98, 97, 96, 95, 94, 93, 92, 91, 90, 89, 88, 87, 86, 85, 84, 83, 82, 81, 80, 79, 78, 77, 76, 75, 74, 73, 72, 71, 70, 69, 68, 67, 66, 65, 64, 63, 62, 61, 60, 59, 58, 57, 56, 55, 54, 53, 52, 51, 50, 49, 48, 47, 46, 45, 44, 43, 42, 41, 40, 39, 38, 37, 36, 35, 34, 33, 32, 31, 30, 29, 28, 27, 26, 25, 24, 23, 22, 21, 20, 19, 18, 17, 16, 15, 14, 13, 12, 11, 10, 9, 8, 7, 6, 5, 4, 3, 2, 1) #define GAME_STATE_TRANSITION_TABLE(X) \ X(FIRST, "2.0f", three, "four", FIVE) \ X(SECOND, '2', 3 ) \ X(THIRD, 2nd, 0x3, /*empty four*/ ) typedef enum game_state_e { #define STATE_ENUM(state, ...) state, GAME_STATE_TRANSITION_TABLE(STATE_ENUM) GAME_STATE_COUNT } game_state_e; const char* game_state_strings[GAME_STATE_COUNT] = { #define STATE_ENUM_STRING(state, ...) #state, GAME_STATE_TRANSITION_TABLE(STATE_ENUM_STRING) }; const int column_count[GAME_STATE_COUNT] = { GAME_STATE_TRANSITION_TABLE(NUM_ARGS_COMMA) }; int main() { for (int i = 0; i < GAME_STATE_COUNT; i++) { printf("Row \"%s\" has %i columns\n", game_state_strings[i], column_count[i]); } }
Drawbacks
- The autocomplete feature of an editor or IDE might not know about the fields of e.g. an enum, since they only exist after the preprocessor has run.
- Every
#define
of the X-macro needs to be updated for every additional column in the table, although this can be mitigated - Restrictions due to C syntax
- Potentially atrocious compile error messages when you have an error in your X-Macro code
Resources
- https://en.wikipedia.org/wiki/X_Macro
- https://www.rfleury.com/p/table-driven-code-generation
- https://en.wikibooks.org/wiki/C_Programming/Preprocessor_directives_and_macros#X-Macros
- https://www.embedded.com/reduce-c-language-coding-errors-with-x-macros-part-1/
- https://stackoverflow.com/questions/6635851/real-world-use-of-x-macrosd
- https://quuxplusone.github.io/blog/2021/02/01/x-macros/
- https://github.com/NetHack/NetHack/blob/0c3b964/include/artilist.h
- https://github.com/zenorogue/hyperrogue/blob/f95ef39/content.cpp
- https://ph3at.github.io/posts/X-Macros/
- X-Macros Serialization Example