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
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.
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
- To see what a macro expanded into:
gcc -E -P. - An X-macro keeps the list once:
ERROR_LISTgave an enumeration (ERR_COUNT= 4), texts and names,PIN_LISTgave a pin table. #turns the text into a string,XSTRthe value:BAUDand115200;##builds names —get_speed._Genericpicks a function by type at build time:int 5,double 2.5,строка abc.__LINE__in a macro is the line of the call:[59] temperature=23.- A macro of several statements needs
do { } while (0), otherwise "'else' without a previous 'if'". #errorchecks the arithmetic of the setup: the UART divider is 625 on F103 and 729 on F407, and 921600 baud from 8 MHz stopped the build._Static_assertchecks sizes and tables and costs nothing in the firmware.
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.