Embedded HandbookApps
All sections › XV. C in depth
STM32F103, STM32F407 · level intermediate · 14 min read
Open in the interactive handbook

127. The preprocessor as a tool: X-macros, _Generic, static_assert

The basics of the preprocessor — parentheses in macros, #, ##, conditional compilation — are covered in section 71. Here is the next step: the preprocessor as a code generator and as a checker. One list turns into an enumeration and tables that cannot drift apart; a macro picks a function by the type of its argument; a wrong UART setting stops the build instead of being discovered on the bench with an oscilloscope. Everything shown here is built by gcc, clang and gcc for ARM with identical output, and the file for the board by armclang for F103 and F407.

Terms used in the documentation

Term Meaning
preprocessor text processing before compilation
macro expansion replacing a macro with its body
X macro a list that is expanded by different macros
stringizing (#) turning an argument into a string
token pasting (##) gluing tokens together
variadic macro, __VA_ARGS__ a macro with a variable number of arguments
generic selection (_Generic) a choice by type
static assertion a check at build time
translation unit a source file after the preprocessor
predefined macro a macro the compiler defines itself: __LINE__, __FILE__

127.1 What the compiler sees

The preprocessor runs before the compiler and knows nothing about types: it only replaces text. That is why the best way to understand a complex macro is to look at the result. The -E option stops gcc after the preprocessor, -P removes the line markers:

gcc -std=c11 -E -P pre127.c

armclang understands the same -E option. Looking at the expanded text is useful every time a macro does not behave the way it was meant to.


127.2 An X-macro: one list, three tables

One ERROR_LIST list expands into an enumeration, a table of texts and a table of names

A typical firmware trouble: the enumeration of error codes is in one place, the message texts in another, the names for the log in a third. Someone added a code and forgot the text, and the message shifted to the neighbouring one. An X-macro keeps the list once:

#define ERROR_LIST(X)          \
    X(ERR_OK, "нет ошибки")    \
    X(ERR_TIMEOUT, "тайм-аут") \
    X(ERR_CRC, "ошибка CRC")   \
    X(ERR_BUSY, "занято")

The parameter X is the name of another macro that is substituted during expansion. Three different X give three different results:

#define AS_ENUM(name, text) name,
#define AS_TEXT(name, text) text,
#define AS_NAME(name, text) #name,

enum error { ERROR_LIST(AS_ENUM) ERR_COUNT };
static const char *const error_text[] = { ERROR_LIST(AS_TEXT) };
static const char *const error_name[] = { ERROR_LIST(AS_NAME) };

This is what the compiler got after gcc -E:

enum error { ERR_OK, ERR_TIMEOUT, ERR_CRC, ERR_BUSY, ERR_COUNT };

The last element ERR_COUNT itself became equal to the number of items — 4. On all three machines error_text[ERR_CRC] is "ошибка CRC" ("CRC error"), error_name[ERR_BUSY] is the string ERR_BUSY. Add a line to the list and all three tables change. A safeguard for the case when a table is still written by hand:

_Static_assert(sizeof error_text / sizeof error_text[0] == ERR_COUNT, "таблица и перечисление из одного списка");

127.3 An X-macro on the board: a pin table

The same technique describes the pins of the board. The list holds the name, the port, the number:

#define PIN_LIST(X)   \
    X(LED, 'C', 13)   \
    X(BUTTON, 'A', 0) \
    X(TX, 'A', 9)     \
    X(RX, 'A', 10)

From it come an enumeration with a prefix glued on by ##, and a table of structures:

#define AS_ENUM(name, port, num) PIN_##name,
#define AS_ENTRY(name, port, num) { port, num },
enum pin_id { PIN_LIST(AS_ENUM) PIN_COUNT };
const struct pin pins[] = { PIN_LIST(AS_ENTRY) };

The result is PIN_LED, PIN_BUTTON, PIN_TX, PIN_RX and PIN_COUNT. The table is declared const and will go to flash memory (section 125). Moving the LED to another pin is one edit in the list, not a search through all the files. It is just as convenient to describe protocol commands, state machine states, menu items and sensor registers this way.


127.4 A string and a name: # and ##

Section 71.9 showed both operators. Here is the difference between one and two stages in an experiment:

#define STR(x) #x
#define XSTR(x) STR(x)
#define BAUD 115200

STR(BAUD) gave the string BAUD, XSTR(BAUD) gave 115200. The # operator takes the text of the argument before substitution; the extra stage first expands BAUD as an ordinary argument. This is how you write messages with a setting value inside: "speed 115200" is assembled at build time, without printf. But the preprocessor computes nothing, it substitutes text: with #define BAUD 115200u the string will be 115200u, with #define BAUD (72000000 / 625) it will be (72000000 / 625). For a string with the value, BAUD is defined as a bare number.

## builds names. A macro that creates an accessor function for a field:

#define DEFINE_GETTER(field) static int get_##field(void) { return field; }
static int speed = 42;
DEFINE_GETTER(speed)

A function get_speed appeared, and it returned 42. The downside of this technique is that the name get_speed does not appear in the source, and a search through the files will not find it. Use it where names are built by one rule for many fields.


127.5 _Generic: a choice by type

C11 added selection by the type of an expression — what overloading does in C++. A macro looks at the type of its argument and substitutes the right function:

#define SHOW(x) _Generic((x), int: show_int, double: show_double, char *: show_str, const char *: show_str)(x)

SHOW(5), SHOW(2.5) and SHOW("abc") printed int 5, double 2.5 and строка abc ("string abc"). The choice is made by the compiler, and the firmware keeps a direct call to the right function — no checks at run time. <tgmath.h> is built with the same technique: sqrt of a float calls sqrtf, not the slower function for double.

Two subtleties. The type of 2.5f is float, the literal 2.5 is a double; if there is no float branch and no default, the build stops. A string literal in C is an array of char, and in _Generic it decays to char *, not to const char *; that is why the macro has both branches.


127.6 A variable number of arguments and the place in the code

A log macro takes a format and any number of values:

#define LOG(fmt, ...) printf("[%d] " fmt "\n", __LINE__, __VA_ARGS__)

The call LOG("temperature=%d", 23) printed:

[59] temperature=23

__LINE__ was replaced by the number of the line where the call stands, not where the macro is declared — that is exactly why such things are written as macros and not as functions. The compiler glued the adjacent string literals "[%d] " fmt "\n" into one format. For a log on the board, printf is redirected to UART (section 16.6), and in the release build the macro can be made empty by conditional compilation.

If there are no values at all, LOG("старт") will not build: a stray comma remains after the format. C23 and gcc solve this with __VA_OPT__(,) — the comma appears only if there are arguments:

#define LOG(fmt, ...) printf("[%d] " fmt "\n", __LINE__ __VA_OPT__(,) __VA_ARGS__)

Before C23, gcc and clang do the same with the , ##__VA_ARGS__ extension.


127.7 Why do { } while (0)

Section 71.8 explained the rule in words; here is the experiment. A macro of two statements in braces, but without a wrapper:

#define LED_ON() { led_state = 1; led_count++; }
    if (ready)
        LED_ON();
    else
        led_state = -1;

After substitution, the semicolon after } becomes an empty statement, the if ends on it, and the else is left with nothing. gcc refused to build it:

error: 'else' without a previous 'if'

With the wrapper

#define LED_ON() do { led_state = 1; led_count++; } while (0)

the same code built on all three machines, and with ready = 0 the else branch ran: led_state = -1, the counter untouched. do { } while (0) turns the macro into one statement that requires a semicolon and does not break if.

What the compiler sees after substituting LED_ON() into if/else: with braces and with do { } while (0)


127.8 Checking the setup at build time: #if and #error

The UART baud rate divider is computed from the bus frequency (section 16). A mistake in the frequency means "garbage in the terminal", which people hunt for hours. The preprocessor can do integer arithmetic and can compute the divider and the error itself:

#define UART_DIV ((PCLK_HZ + BAUD / 2u) / BAUD)
#define UART_REAL (PCLK_HZ / UART_DIV)
#define UART_ERR_X100 (((UART_REAL > BAUD ? UART_REAL - BAUD : BAUD - UART_REAL) * 10000u) / BAUD)
#if UART_DIV < 16
#error "скорость UART выше частоты шины / 16 — при передискретизации 16 её не получить"
#endif
#if UART_ERR_X100 > 200
#error "ошибка скорости UART больше 2 % — поменяйте частоту шины или скорость"
#endif

There are two checks. The first is the divider itself: with oversampling by 16 (the default on F1 and F4) it cannot be less than 16, that is, the baud rate cannot exceed the bus frequency divided by 16. The second is the baud rate error, in hundredths of a percent, because #if has no fractions. Here is what came out with armclang:

Board USART1 bus frequency (APB2) Divider Actual baud rate
F103 72 MHz 625 exactly 115200
F407 84 MHz 729 115226, error 0.02%

USART2 and USART3 sit on APB1 — on the F103 that is 36 MHz: divider 313, baud rate 115016, error 0.16%. On the F407, APB1 runs at 42 MHz.

The uart_div() function in the F103 object file is a load of a ready-made number MOV r0, #0x271 and a return: the divider was computed at build time, the firmware does not contain it.

Now the bad combinations. The build with the options -DPCLK_HZ=8000000u -DBAUD=921600u — 8 MHz and 921600 baud — stopped: the divider is 9, less than the permitted 16, and you cannot get above 500000 baud from 8 MHz at all. The rate 460800 passes the divider check (17), but gives 470588 baud — an error of 2.12%, and the second check stopped the build. Without them the firmware would have built and silently not worked. The UART calculator in the app does the same calculations.


127.9 _Static_assert: a check where the preprocessor is blind

#if knows neither sizeof nor types — it works before the compiler. _Static_assert (C11; in C23 — static_assert) checks an expression that the compiler can evaluate: structure sizes, field offsets, table lengths. A deliberately false assertion:

_Static_assert(sizeof(struct packet) == 5, "packet must be 5 bytes");

A structure of uint8_t and uint32_t is longer than five bytes — there is padding after the first field (section 126) — and the build stopped:

error: static assertion failed: "packet must be 5 bytes"

The assertion costs nothing: not a single byte gets into the firmware. Good places for it are the layout of packets and registers, the length of tables from X-macros, the size of a DMA buffer being a multiple of a word, and the agreement on the data model (section 122).


127.10 Pitfalls

An error message shifted to the neighbouring one. The table of texts and the enumeration are written separately. An X-macro or a _Static_assert on the length.

The string holds the name instead of the value. STR(BAUD) gave BAUD. You need a second stage — XSTR.

else without if after a macro. A macro of several statements without do { } while (0).

#if sizeof(...) does not build. The preprocessor does not know types. Such checks belong in _Static_assert.

LOG("старт") without values does not build. A stray comma before an empty __VA_ARGS__. __VA_OPT__(,) or a separate macro.

_Generic does not find a branch. The type of the expression is not what it seems: 2.5 is double, 'a' is int, a string is char *. Add a branch or default.

A macro cannot be found by searching. Names assembled with ## do not appear in the source. Look at gcc -E and do not overuse token pasting.


127.11 Summary

Full sources

pre127.c — X-macros, stringizing, name pasting, _Generic and a log macro — the experiment on the computer.

/* Раздел 127: препроцессор как инструмент — опыт на трёх машинах.
 * X-макросы, двухступенчатое превращение в строку, склейка имён, _Generic,
 * макрос журнала с переменным числом аргументов. Неверные варианты (макрос без
 * do { } while (0), проваленный static_assert) лежат в bad127_*.c: проверка
 * требует, чтобы компилятор их ОТВЕРГ. */
#include <stdio.h>

/* ── X-макрос: список ошибок пишется один раз ─────────────────────────── */
#define ERROR_LIST(X)          \
    X(ERR_OK, "нет ошибки")    \
    X(ERR_TIMEOUT, "тайм-аут") \
    X(ERR_CRC, "ошибка CRC")   \
    X(ERR_BUSY, "занято")

#define AS_ENUM(name, text) name,
#define AS_TEXT(name, text) text,
#define AS_NAME(name, text) #name,

enum error { ERROR_LIST(AS_ENUM) ERR_COUNT };
static const char *const error_text[] = { ERROR_LIST(AS_TEXT) };
static const char *const error_name[] = { ERROR_LIST(AS_NAME) };

_Static_assert(sizeof error_text / sizeof error_text[0] == ERR_COUNT, "таблица и перечисление из одного списка");

/* ── строка из макроса: одна ступень — имя, две — значение ─────────────── */
#define STR(x) #x
#define XSTR(x) STR(x)
#define BAUD 115200

/* ── склейка имён ──────────────────────────────────────────────────────── */
#define DEFINE_GETTER(field) static int get_##field(void) { return field; }
static int speed = 42;
DEFINE_GETTER(speed)

/* ── _Generic: выбор функции по типу аргумента ─────────────────────────── */
static void show_int(int v) { printf("int %d\n", v); }
static void show_double(double v) { printf("double %.1f\n", v); }
static void show_str(const char *v) { printf("строка %s\n", v); }
#define SHOW(x) _Generic((x), int: show_int, double: show_double, char *: show_str, const char *: show_str)(x)

/* ── журнал: переменное число аргументов и номер строки ────────────────── */
#define LOG(fmt, ...) printf("[%d] " fmt "\n", __LINE__, __VA_ARGS__)

/* ── макрос из нескольких операторов: do { } while (0) ─────────────────── */
static int led_state, led_count;
#define LED_ON() do { led_state = 1; led_count++; } while (0)

int main(void)
{
    printf("err_count=%d\n", ERR_COUNT);
    printf("err_text=%s\n", error_text[ERR_CRC]);
    printf("err_name=%s\n", error_name[ERR_BUSY]);
    printf("str=%s\n", STR(BAUD));
    printf("xstr=%s\n", XSTR(BAUD));
    printf("getter=%d\n", get_speed());
    SHOW(5);
    SHOW(2.5);
    SHOW("abc");
    LOG("temperature=%d", 23);
    int ready = 0;
    if (ready)
        LED_ON();
    else
        led_state = -1;
    printf("led=%d,%d\n", led_state, led_count);
    printf("done=1\n");
    return 0;
}

#if 0   /* вариант журнала для пустого списка значений (C23 и gcc), в конце файла — номера строк выше не сдвигаются */
#define LOG(fmt, ...) printf("[%d] " fmt "\n", __LINE__ __VA_OPT__(,) __VA_ARGS__)
#endif

pre127_arm.c — the preprocessor on the board: checking the UART setup with #if and #error, and a pin table from an X-macro.

/* Раздел 127: препроцессор на плате — armclang для F103 и F407.
 * Проверка настройки UART на этапе сборки (#if и #error), таблица выводов
 * из X-макроса во флеш-памяти, счётчик элементов, который не разойдётся со списком. */
#include <stdint.h>

#ifndef PCLK_HZ                          /* можно задать ключом -DPCLK_HZ=… */
#if defined(STM32F407xx_BUILD)
#define PCLK_HZ 84000000u
#else
#define PCLK_HZ 72000000u
#endif
#endif
#ifndef BAUD
#define BAUD 115200u
#endif

/* Делитель с округлением и настоящая скорость, которая из него получится. */
#define UART_DIV ((PCLK_HZ + BAUD / 2u) / BAUD)
#define UART_REAL (PCLK_HZ / UART_DIV)
/* Ошибка скорости в сотых долях процента — целыми, препроцессор умеет только их. */
#define UART_ERR_X100 (((UART_REAL > BAUD ? UART_REAL - BAUD : BAUD - UART_REAL) * 10000u) / BAUD)

#if UART_DIV < 16
#error "скорость UART выше частоты шины / 16 — при передискретизации 16 её не получить"
#endif
#if UART_ERR_X100 > 200
#error "ошибка скорости UART больше 2 % — поменяйте частоту шины или скорость"
#endif

#if PCLK_HZ == 72000000u || PCLK_HZ == 84000000u
_Static_assert(UART_DIV == (PCLK_HZ == 84000000u ? 729u : 625u), "делитель: 625 на F103, 729 на F407");
#endif

/* ── таблица выводов из одного списка ──────────────────────────────────── */
#define PIN_LIST(X)   \
    X(LED, 'C', 13)   \
    X(BUTTON, 'A', 0) \
    X(TX, 'A', 9)     \
    X(RX, 'A', 10)

struct pin {
    char port;
    uint8_t number;
};

#define AS_ENUM(name, port, num) PIN_##name,
#define AS_ENTRY(name, port, num) { port, num },

enum pin_id { PIN_LIST(AS_ENUM) PIN_COUNT };
const struct pin pins[] = { PIN_LIST(AS_ENTRY) };

_Static_assert(sizeof pins / sizeof pins[0] == PIN_COUNT, "таблица и перечисление из одного списка");

uint8_t pin_number(enum pin_id id)
{
    return id < PIN_COUNT ? pins[id].number : 0xFFu;
}

uint32_t uart_div(void)
{
    return UART_DIV;
}

Next: section 128 "The C99–C23 standards" — what from the new versions of the language is useful in firmware.

Open in the interactive handbook