1

I'm using X Macros in my project, so as to not repeat myself in places where a list of names all need to have identical operations performed on them (such as creation, initialization, population, and destruction).

As the data I'm trying to specify once involves a few of the same macro-able translations (ignoring arguments, prefixing vs. suffixing), I decided to enhance my original single-fit X Macros by rewriting them as generalized Meta X-Macros, from which multiple derivative end-use-case-fitting X Macros could be defined, using transformation macros that interpret the meta-macro's arguments:

// Meta-macros //

#define MAIN_WINDOW_TEXT_LAYERS_METAMACRO(macro, tr) \
  macro(tr(hour_layer)) \
  macro(tr(min_layer)) \
  macro(tr(date_layer))

#define MAIN_WINDOW_LAYERS_METAMACRO(macro, tr) \
  macro(tr(colon_layer)) \
  macro(tr(phone_batt_layer)) \
  macro(tr(watch_batt_layer))

#define GBITMAPS_WITH_RESOURCE_IDS_METAMACRO(macro, tr) \
  macro(tr(watch_icon, ICON_WATCH_6X11)) \
  macro(tr(watch_charging_icon, ICON_WATCH_CHARGING_6X11)) \
  macro(tr(phone_icon, ICON_PHONE_6X11)) \
  macro(tr(phone_charging_icon, ICON_PHONE_CHARGING_6X11))

#define GFONTS_WITH_RESOURCE_IDS_METAMACRO(macro, tr) \
  macro(tr(time_font, FONT_ARVO_BOLD_48)) \
  macro(tr(date_font, FONT_ARVO_BOLD_20))

// Transformation macros //

#define IDENTITY_MACRO(x) x
#define STATIC_PREFIX_MACRO(x) s_ ## x
#define STATIC_PREFIX_DISCARD_MACRO(x, _) s_ ## x
#define STATIC_PREFIX_RESOURCE_ID_PREFIX_MACRO(x, id) \
  s_ ## x, RESOURCE_ID_ ## s

// Derived X-Macros //

#define FOR_MAIN_WINDOW_STATIC_TEXT_LAYER_POINTERS(macro) \
  MAIN_WINDOW_TEXT_LAYERS_METAMACRO(macro, STATIC_PREFIX_MACRO)

#define FOR_MAIN_WINDOW_STATIC_LAYER_POINTERS(macro) \
  MAIN_WINDOW_LAYERS_METAMACRO(macro, STATIC_PREFIX_MACRO)

#define FOR_MAIN_WINDOW_LAYER_NAMES(macro) \
  MAIN_WINDOW_LAYERS_METAMACRO(macro, IDENTITY_MACRO)

#define FOR_STATIC_GFONTS(macro) \
  GFONTS_WITH_RESOURCE_IDS_METAMACRO(macro, STATIC_PREFIX_DISCARD_MACRO)

#define FOR_STATIC_GFONTS_WITH_RESOURCE_IDS(macro) \
  GFONTS_WITH_RESOURCE_IDS_METAMACRO(macro, STATIC_PREFIX_RESOURCE_ID_PREFIX_MACRO)

#define FOR_STATIC_GBITMAP_POINTERS_WITH_RESOURCE_IDS(macro) \
  GBITMAPS_WITH_RESOURCE_IDS_METAMACRO(macro, STATIC_PREFIX_RESOURCE_ID_PREFIX_MACRO)

#define FOR_STATIC_GBITMAP_POINTERS(macro) \
  GBITMAPS_WITH_RESOURCE_IDS_METAMACRO(macro, STATIC_PREFIX_DISCARD_MACRO)

This works, in most use cases: however, there are a few edge cases where I run into trouble. Firstly, trying to concatenate arguments results in the transformation macro's name getting concatenated, rather than the transformed name:

#define X(name) layer_set_update_proc(s_ ## name, name ## _update_proc);
FOR_MAIN_WINDOW_LAYER_NAMES(X)
#undef X
error: implicit declaration of function 's_IDENTITY_MACRO'

Secondly, macros that transform two arguments aren't getting expanded - they're being passed to X as a single token (the call to the transformation macro):

#define X(name, id) name = fonts_load_custom_font(resource_get_handle(id));
FOR_STATIC_GFONTS_WITH_RESOURCE_IDS(X)
#undef X
error: macro "X" requires 2 arguments, but only 1 given
error: unknown type name 'X'
#define X(name, id) name = gbitmap_create_with_resource(id);
FOR_STATIC_GBITMAP_POINTERS_WITH_RESOURCE_IDS(X)
#undef X
error: macro "X" requires 2 arguments, but only 1 given
error: expected '=', ',', ';', 'asm' or '__attribute__' before 'X'

How can I make these work the way I want them to?

Stuart P. Bentley
  • 10,195
  • 10
  • 55
  • 84

1 Answers1

1

As alluded to in other Stack Overflow questions about the C Preprocessor, the first problem (of the macro's identifier being concatenated rather than its content) can be avoided by introducing another layer of indirect evaulation to the macro:

#define X_(name) layer_set_update_proc(s_ ## name, name ## _update_proc);
#define X(name) X_(name)
FOR_MAIN_WINDOW_LAYER_NAMES(X)
#undef X
#undef X_

The second issue is more insidious, but it has a similar solution: another layer of indirection. The key trick here is that, rather than copying the signature of the final macro, the first layer must take a single argument to pass to the next layer, which will then be expanded out to multiple arguments:

#define X_(name, id) name = fonts_load_custom_font(resource_get_handle(id));
#define X(args) X_(args)
FOR_STATIC_GFONTS_WITH_RESOURCE_IDS(X)
#undef X
#undef X_

Of course, offloading these sorts of workarounds to the point-of-use, rather than fixing them at the point where they're defined, is not robust engineering, so one ought to define this layer of indirection as part of the meta-macro itself:

#define APPLY_MACRO(x, t) x(t)

#define MAIN_WINDOW_TEXT_LAYERS_METAMACRO(X, tr) \
  APPLY_MACRO(X,tr(hour_layer)) \
  APPLY_MACRO(X,tr(min_layer)) \
  APPLY_MACRO(X,tr(date_layer))

/* etc... */

Note, however, that whatever token you choose to use for APPLY_MACRO, unlike an identifier like X or X_ that can be #defined and immediately #undefined right around the point of invocation, the indirection macro used here must remain defined for as long as these macros themselves may be used (obviously), so one should pick a name that isn't liable to cause conflicts with other portions of the codebase (ie, not just X_). Even something like APPLY_MACRO, as I used here, isn't particularly advisable: larger projects (especially any code that may be redistributed for use in other solutions) should consider prefixing the name used with some sort of namespace prefix to limit its scope from interfering with other contexts (as described in this SoftwareEngineering.se answer).

Ultimately, though, for my purposes, I ended up solving this problem by re-writing my transformation macros to take the macro to apply and call it with the transformed arguments, flattening the interpretation and allowing me to refactor out the APPLY_MACRO macro described above altogether.

Community
  • 1
  • 1
Stuart P. Bentley
  • 10,195
  • 10
  • 55
  • 84