X-macros: Not all C macros are evil
Macros in C have a bad reputation because the preprocessor does not have any notion of types
and operates only on raw text. A disaster is just one missing do { ... } while (0) away.
The standard recommendation is to always favor inline functions over function-like macros
and static const over #define to give the compiler better chance of enforcing
the already weak type system.
Nevertheless, they are part of the C ecosystem and are used to get things done that can't be done in pure C, and without having to pull in external code generation tools.
One special kind of macros are function-like macros ie. macros that take arguments. Function-like macros can also be passed to each other. But why would you ever want to do that?! The answer is a technique called X-macros!
X-macros
are a useful hack when you want to generate multiple "things" (lines of code) from a single line of code.
Classic example: a struct and metadata. The struct holds some runtime data so it must be kept in RAM.
The metadata is static and can be stored in flash. In C these two have to be declared separately.
I will use a snippet containing
statistical counters as a real-world example.
I use them to store and track runtime metrics of the firmware.
In this simple example every "logical" counter has only its own name (a constant used everywhere in the code)
and a string that describes it in a human-readable way. There could also be more entities associated with every counter,
for example fields for limits, high (or low) water marks (like in FIFOs), error callbacks etc.
Defining the counters
Let's start by defining the counter "objects" using this macro:
1 2 3 4 | |
Here we have defined a constant and an accompanying string that describes it.
The magic will happen when another macro is passed as the M argument.
Any other letter (or longer text) can also work. A single letter is just easy to repeat on every line.
There can be multiple fields in every line, they can be strings, constants, numbers, basically anything. The preprocessor does not care. Of course the order in every line must stay the same. What can it be used for? We can extract some of the fields from every "object" for example to create a struct (drop me a message if the "object" has a proper name).
Extracting a struct
1 2 3 4 5 | |
This is where the wacky part start. We are passing the extract macro to the definition macro and the preprocessed code looks like this:
1 2 3 4 5 | |
The constants have been turned into regular struct member names. 🙂
The struct can be later declared the regular way in a header:
1 | |
Extracting the metadata
1 2 3 4 5 6 7 8 9 10 | |
The metadata is an array of structs. The entries are extracted using another magical macro. The comma at the end of the extract macro is important (the spaces are not). The preprocessed code becomes:
1 2 3 4 5 | |
You can now iterate over this metadata struct to print/dump/serialize every counter alongside its name.
Using the counters
To increment every counter I declare a simple macro:
1 | |
that later in the code can be used like a function:
1 2 3 | |
Other operations on a counter are also possible. A counter can be used to track a maximum value (for example latency, stack usage, or number of elements in a queue). In that case the usage macro can look like this:
1 2 3 4 | |
Usage example (FreeRTOS-style):
1 | |
Important feature of X-macros
The most important feature of the X-macro technique is that the first macro (STAT_DEFS(M) in my example)
becomes a single source of truth. Adding, removing, or reshuffling the timers
becomes as simple as updating this single macro in just one place. All "downstream"
code magically updates itself during build. Not all C macros are evil. 🙂
Without X-macros the code would have to be generated outside the C world. For example using a Python script with its own dictionary. It is entirely feasible but requires external tooling, testing, and integrating with the build process.
Complete code
stats.h
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 | |
stats.c
1 2 3 4 5 6 7 8 9 10 | |