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-макрос: один список — три таблицы
Типичная беда прошивки: перечисление кодов ошибок в одном месте, тексты сообщений в другом, имена для журнала в третьем. Добавили код — забыли текст, и сообщение сдвинулось на соседнее. 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.
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 Итоги
- Смотреть, во что развернулся макрос:
gcc -E -P. - X-макрос хранит список один раз:
ERROR_LISTдал перечисление (ERR_COUNT= 4), тексты и имена,PIN_LIST— таблицу выводов. #превращает в строку текст,XSTR— значение:BAUDи115200;##строит имена —get_speed._Genericвыбирает функцию по типу при сборке:int 5,double 2.5,строка abc.__LINE__в макросе — строка вызова:[59] temperature=23.- Макрос из нескольких операторов —
do { } while (0), иначе «'else' without a previous 'if'». #errorпроверяет арифметику настройки: делитель UART 625 на F103 и 729 на F407, а 921 600 бод от 8 МГц сборку остановили._Static_assertпроверяет размеры и таблицы и ничего не стоит в прошивке.
Полные исходники
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» — что из новых версий языка пригодится в прошивке.