Embedded HandbookПриложения
Все разделы › XV. Язык C глубже
STM32F103, STM32F407 · уровень средний · 14 мин чтения
Открыть в интерактивном справочнике

127. Препроцессор как инструмент: X-макросы, _Generic, static_assert

Основы препроцессора — скобки в макросах, #, ##, условная компиляция — разобраны в разделе 71. Здесь следующий шаг: препроцессор как генератор кода и как проверяющий. Один список превращается в перечисление и таблицы, которые не могут разойтись; макрос выбирает функцию по типу аргумента; неверная настройка UART останавливает сборку, а не выясняется на столе с осциллографом. Всё показанное собрано gcc, clang и gcc для ARM с одинаковым выводом, а файл для платы — armclang для F103 и F407.

Как это называется в документации

Оригинал Перевод
preprocessor препроцессор: обработка текста до компиляции
macro expansion подстановка макроса
X macro X-макрос: список, который разворачивают разными макросами
stringizing (#) превращение в строку
token pasting (##) склейка лексем
variadic macro, __VA_ARGS__ макрос с переменным числом аргументов
generic selection (_Generic) выбор по типу
static assertion статическое утверждение: проверка при сборке
translation unit единица трансляции: файл после препроцессора
predefined macro предопределённый макрос: __LINE__, __FILE__

127.1 Что видит компилятор

Препроцессор работает раньше компилятора и ничего не знает о типах: он только заменяет текст. Поэтому лучший способ понять сложный макрос — посмотреть результат. Ключ -E останавливает gcc после препроцессора, -P убирает служебные пометки строк:

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

armclang понимает тот же ключ -E. Смотреть на развёрнутый текст полезно всякий раз, когда макрос ведёт себя не так, как задуман.


127.2 X-макрос: один список — три таблицы

Один список ERROR_LIST разворачивается в перечисление, таблицу текстов и таблицу имён

Типичная беда прошивки: перечисление кодов ошибок в одном месте, тексты сообщений в другом, имена для журнала в третьем. Добавили код — забыли текст, и сообщение сдвинулось на соседнее. X-макрос хранит список один раз:

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

Параметр X — имя другого макроса, который подставят при разворачивании. Три разных X дают три разных результата:

#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) };

Вот что получил компилятор после gcc -E:

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

Последний элемент ERR_COUNT сам стал равен числу пунктов — 4. На всех трёх машинах error_text[ERR_CRC] — «ошибка CRC», error_name[ERR_BUSY] — строка ERR_BUSY. Добавили строку в список — изменились все три таблицы. Страховка на случай, если таблицу всё-таки пишут руками:

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

127.3 X-макрос на плате: таблица выводов

Тот же приём описывает выводы платы. Список — имя, порт, номер:

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

Из него — перечисление с приставкой, склеенной ##, и таблица структур:

#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) };

Получились PIN_LED, PIN_BUTTON, PIN_TX, PIN_RX и PIN_COUNT. Таблица объявлена const и ляжет во флеш-память (раздел 125). Перенос светодиода на другой вывод — одна правка в списке, а не поиск по всем файлам. Так же удобно описывать команды протокола, состояния автомата, пункты меню и регистры датчика.


127.4 Строка и имя: # и ##

Раздел 71.9 показал оба оператора. На опыте разница одной и двух ступеней:

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

STR(BAUD) дал строку BAUD, XSTR(BAUD) — 115200. Оператор # берёт текст аргумента до подстановки; лишняя ступень сначала раскрывает BAUD как обычный аргумент. Так пишут сообщения со значением настройки внутри: «скорость 115200» соберётся на этапе сборки, без printf. Но препроцессор ничего не вычисляет, а подставляет текст: при #define BAUD 115200u строка будет 115200u, при #define BAUD (72000000 / 625) — (72000000 / 625). Для строки со значением BAUD задают голым числом.

## строит имена. Макрос, который создаёт функцию доступа к полю:

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

Появилась функция get_speed, и она вернула 42. Минус такого приёма — имени get_speed нет в исходнике, поиск по файлам его не найдёт. Пользуйтесь им там, где имена строятся по одному правилу для многих полей.


127.5 _Generic: выбор по типу

C11 добавил выбор по типу выражения — то, что в C++ делает перегрузка. Макрос смотрит на тип аргумента и подставляет нужную функцию:

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

SHOW(5), SHOW(2.5) и SHOW("abc") вывели int 5, double 2.5 и строка abc. Выбор делает компилятор, в прошивке остаётся прямой вызов нужной функции — никаких проверок во время работы. Этим же приёмом устроен <tgmath.h>: sqrt от float зовёт sqrtf, а не более медленную функцию для double.

Две тонкости. Тип 2.5f — float, у литерала 2.5 — double; если ветки float нет, а default не задан, сборка остановится. Строковый литерал в Си — массив char, и в _Generic он превращается в char *, а не в const char *; поэтому в макросе есть обе ветки.


127.6 Переменное число аргументов и место в коде

Макрос журнала принимает формат и сколько угодно значений:

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

Вызов LOG("temperature=%d", 23) вывел:

[59] temperature=23

__LINE__ подставился номером строки, где стоит вызов, а не где объявлен макрос, — поэтому такие макросы и пишут макросами, а не функциями. Соседние строковые литералы "[%d] " fmt "\n" компилятор склеил в один формат. Для журнала на плате printf направляют в UART (раздел 16.6), а в рабочей сборке макрос можно сделать пустым условной компиляцией.

Если значений нет совсем, LOG("старт") не соберётся: после формата останется лишняя запятая. C23 и gcc решают это через __VA_OPT__(,) — запятая появляется, только если аргументы есть:

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

До C23 в gcc и clang то же делает расширение , ##__VA_ARGS__.


127.7 Почему do { } while (0)

Раздел 71.8 объяснил правило словами, здесь — опыт. Макрос из двух операторов в фигурных скобках, но без обёртки:

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

После подстановки точка с запятой за } становится пустым оператором, if на нём заканчивается, и else остаётся ни с чем. gcc отказался собирать:

error: 'else' without a previous 'if'

С обёрткой

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

тот же код собрался на всех трёх машинах, и при ready = 0 сработала ветка else: led_state = -1, счётчик не тронут. do { } while (0) превращает макрос в один оператор, который требует точку с запятой и не ломает if.

Что видит компилятор после подстановки LED_ON() в if/else: с фигурными скобками и с do { } while (0)


127.8 Проверка настройки при сборке: #if и #error

Делитель скорости UART считается из частоты шины (раздел 16). Ошибка в частоте — это «мусор в терминале», который ищут часами. Препроцессор умеет целочисленную арифметику и может посчитать делитель и ошибку сам:

#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

Проверок две. Первая — сам делитель: при передискретизации 16 (как по умолчанию у F1 и F4) он не может быть меньше 16, то есть скорость не выше частоты шины, делённой на 16. Вторая — ошибка скорости, в сотых долях процента, потому что дробей в #if нет. Что вышло на armclang:

Плата Частота шины USART1 (APB2) Делитель Настоящая скорость
F103 72 МГц 625 ровно 115 200
F407 84 МГц 729 115 226, ошибка 0,02 %

USART2 и USART3 сидят на APB1 — у F103 это 36 МГц: делитель 313, скорость 115 016, ошибка 0,16 %. У F407 на APB1 42 МГц.

Функция uart_div() в объектном файле F103 — загрузка готового числа MOV r0, #0x271 и возврат: делитель посчитан при сборке, в прошивке его нет.

Теперь неудачные сочетания. Сборка с ключами -DPCLK_HZ=8000000u -DBAUD=921600u — 8 МГц и 921 600 бод — остановилась: делитель 9, меньше допустимых 16, и выше 500 000 бод от 8 МГц не получить вовсе. Скорость 460 800 проходит по делителю (17), но даёт 470 588 бод — ошибка 2,12 %, и сборку остановила вторая проверка. Без них прошивка собралась бы и молча не работала. Те же расчёты делает калькулятор UART в приложении.


127.9 _Static_assert: проверка там, где препроцессор слеп

#if не знает sizeof и типов — он работает до компилятора. _Static_assert (C11; в C23 — static_assert) проверяет выражение, которое компилятор может вычислить: размеры структур, смещения полей, длины таблиц. Нарочно неверное утверждение:

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

Структура из uint8_t и uint32_t длиннее пяти байт — после первого поля стоит заполнение (раздел 126), — и сборка остановилась:

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

Утверждение ничего не стоит: в прошивку не попадает ни байта. Хорошие места для него — раскладка пакетов и регистров, длина таблиц из X-макросов, размер буфера DMA, кратный слову, и договорённость о модели данных (раздел 122).


127.10 Грабли

Сообщение об ошибке сдвинулось на соседнее. Таблица текстов и перечисление написаны отдельно. X-макрос или _Static_assert на длину.

В строке имя вместо значения. STR(BAUD) дал BAUD. Нужна вторая ступень — XSTR.

else без if после макроса. Макрос из нескольких операторов без do { } while (0).

#if sizeof(...) не собирается. Препроцессор не знает типов. Такие проверки — _Static_assert.

LOG("старт") без значений не собирается. Лишняя запятая перед пустым __VA_ARGS__. __VA_OPT__(,) или отдельный макрос.

_Generic не находит ветку. Тип выражения не тот, что кажется: 2.5 — double, 'a' — int, строка — char *. Добавить ветку или default.

Макрос нельзя найти поиском. Имена, собранные ##, в исходнике не встречаются. Смотреть gcc -E и не злоупотреблять склейкой.


127.11 Итоги

Полные исходники

pre127.c — X-макросы, превращение в строку, склейка имён, _Generic и макрос журнала — опыт на компьютере.

/* Раздел 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 — препроцессор на плате: проверка настройки UART через #if и #error и таблица выводов из X-макроса.

/* Раздел 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;
}

Дальше: раздел 128 «Стандарты C99–C23» — что из новых версий языка пригодится в прошивке.

Открыть в интерактивном справочнике