M0AGX / LB9MG

Amateur radio and embedded systems

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
#define STAT_DEFS(M) \
    M(UPTIME, "System uptime") \
    M(UART_ERR, "UART error count") \
    M(FRAME_ERR, "Wrong frame count")

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
#define EXTRACT_STAT_STRUCT(_name, _label) uint32_t _name;

typedef struct {
    STAT_DEFS(EXTRACT_STAT_STRUCT)
} stat_counters_t;

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
typedef struct {
    uint32_t UPTIME;
    uint32_t UART_ERR;
    uint32_t FRAME_ERR;
} stat_counters_t;

The constants have been turned into regular struct member names. 🙂

The struct can be later declared the regular way in a header:

1
extern stat_counters_t GLOBAL_stats;

Extracting the metadata

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
typedef struct {
    const char *name;
    uint32_t *ptr;
} stat_metadata_t;

#define EXTRACT_STAT_METADATA(_name, _label) {.name = _label, .ptr = &GLOBAL_stats._name},

stat_metadata_t GLOBAL_STATS_METADATA[] = {
    STAT_DEFS(EXTRACT_STAT_METADATA)
};

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
const stat_metadata_t GLOBAL_STATS_METADATA[] = {
    {.name = "System uptime", .ptr = &GLOBAL_stats.UPTIME},
    {.name = "UART error count", .ptr = &GLOBAL_stats.UART_ERR},
    {.name = "Wrong frame count", .ptr = &GLOBAL_stats.FRAME_ERR},
};

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
#define STAT_INC(_name) GLOBAL_stats._name++

that later in the code can be used like a function:

1
2
3
if (some_error_flag){
    STAT_INC(UART_ERR);
}

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
#define STAT_MAX(_name, value) \
    if (value > GLOBAL_stats._name) { \
        GLOBAL_stats._name = value; \
    }

Usage example (FreeRTOS-style):

1
STAT_MAX(EVENT_QUEUE_MAX, uxQueueMessagesWaiting(some_queue_handle));

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
#pragma once
#include <stdint.h>

/* **** HOW TO USE ****
 * 1. Add a field to STAT_DEFS
 * 2. In the relevant place call like: STAT_INC(UART_ERR);
 */

#define STAT_DEFS(M)                  \
    M(UPTIME, "System uptime")        \
    M(UART_ERR, "UART error count")   \
    M(FRAME_ERR, "Wrong frame count")

#define EXTRACT_STAT_NAME(_name, _label) uint32_t _name;

#define STAT_INC(_name) GLOBAL_stats._name++
#define STAT_SET(_name, value) GLOBAL_stats._name = value
#define STAT_GET(_name) GLOBAL_stats._name

#define STAT_MAX(_name, value)        \
    if (value > GLOBAL_stats._name) { \
        GLOBAL_stats._name = value;   \
    }
#define STAT_MIN(_name, value)        \
    if (value < GLOBAL_stats._name) { \
        GLOBAL_stats._name = value;   \
    }

typedef struct {
    STAT_DEFS(EXTRACT_STAT_NAME)
} stat_counters_t;

extern stat_counters_t GLOBAL_stats;

typedef struct {
    const char *name;
    uint32_t *ptr;
} stat_metadata_t;

extern stat_metadata_t GLOBAL_STATS_METADATA[];
extern uint32_t GLOBAL_STATS_METADATA_len;

stats.c

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
#include "stats.h"

stat_counters_t GLOBAL_stats;

#define EXTRACT_STAT_METADATA(_name, _label) {.name = _label, .ptr = &GLOBAL_stats._name},

stat_metadata_t GLOBAL_STATS_METADATA[] = {
    STAT_DEFS(EXTRACT_STAT_METADATA)};

uint32_t GLOBAL_STATS_METADATA_len = sizeof(GLOBAL_STATS_METADATA) / sizeof(GLOBAL_STATS_METADATA[0]);