125. Объявления и указатели на функции: таблицы обработчиков и обратные вызовы
Объявления в Си читаются не слева направо, и void (*const handlers[3])(void)
пугает даже тех, кто пишет на Си годами. Но именно такие конструкции
держат прошивку: таблица векторов прерываний — массив указателей
на функции, драйверы отдают данные обратными вызовами, конечные автоматы
переключают состояния указателем на функцию. Раздел учит читать
объявления по одному правилу, проверяя себя самим компилятором, и строить
таблицы обработчиков так, чтобы они жили во флеш-памяти.
Как это называется в документации
Оригинал Перевод declarator описатель: имя вместе с *, [] и () pointer to function указатель на функцию function pointer table, dispatch table таблица обработчиков callback обратный вызов context pointer ( void *user)указатель на контекст typedef псевдоним типа generic selection ( _Generic)выбор по типу выражения state machine конечный автомат tail call вызов в конце функции, ставший переходом indirect branch непрямой переход по адресу из регистра
125.1 Правило чтения: от имени наружу
Объявление читают от имени, сначала вправо ([] — массив, () —
функция), потом влево (* — указатель), а скобки меняют порядок, как
в арифметике. void (*const handlers[3])(void): handlers — справа [3],
массив из трёх; слева *const — константных указателей; за скобкой
справа (void) — на функции без аргументов; слева void — возвращающие
void.
Проверять себя удобно не глазами, а компилятором: _Generic выбирает
ветку по типу выражения, и программа сама называет тип.
int (*)[4]: "указатель на массив из 4 int", \
void (**)(void): "указатель на указатель на функцию", \
125.2 Скобки меняют всё
Опыт — три объявления, их типы по _Generic и размеры на двух машинах:
| Объявление | Что это | Cortex-M, байт | x86-64, байт |
|---|---|---|---|
int *ptrs[4] |
массив из 4 указателей на int | 16 | 32 |
int (*pa)[4] |
указатель на массив из 4 int | 4 | 8 |
void (*table[3])(void) |
массив из 3 указателей на функцию | 12 | 24 |
int *ptrs[4] и int (*pa)[4] отличаются одной парой скобок, а по смыслу —
всем: первое занимает четыре указателя, второе — один. Названия типов
у gcc, clang и ARM совпали полностью, размеры на Cortex-M вдвое меньше —
указатель там 4 байта (раздел 122).
Сам массив _Generic не назовёт: выражение внутри проходит обычные
преобразования, и массив превращается в указатель на первый элемент —
_Generic(ptrs, …) видит int **, а _Generic(table, …) —
void (**)(void). Поэтому опыт спрашивает тип элемента (ptrs[0])
и адреса (&table[0]), а тип самого массива проверяют по его адресу:
_Generic(&ptrs, int *(*)[4]: …).
125.3 typedef делает объявления читаемыми
Длинное объявление разбивают псевдонимом типа:
typedef void (*handler_t)(void);
После этого handler_t table[3] читается сразу. Правило для прошивки:
тип обработчика или обратного вызова — всегда через typedef, с именем,
которое говорит о назначении, а не об устройстве.
125.4 Таблица команд
Классический разбор команд из UART или CAN: имя и обработчик в одной таблице, отсортированной по имени, поиск — двоичный.
static const struct cmd commands[] = { /* по алфавиту — для двоичного поиска */
{ "led", cmd_led },
{ "reset", cmd_reset },
{ "speed", cmd_speed },
};
const struct cmd *c = bsearch(name, commands, sizeof commands / sizeof commands[0],
sizeof commands[0], by_name);
return c ? c->fn(arg) : -1;
В опыте reset 7 дал 207, speed 1 — 301, неизвестная команда — -1.
Добавить команду — одна строка в таблице, а не новая ветка в switch
на двести строк. Главное условие двоичного поиска — порядок строк; его
удобно проверять тестом на компьютере.
125.5 Таблица во флеш-памяти
Для платы важно, где лежит таблица. const у самого указателя —
void (*const handlers[3])(void) — делает таблицу неизменяемой, и armclang
кладёт её прямо за кодом функции, во флеш-память:
if (event < sizeof handlers / sizeof handlers[0])
handlers[event](); /* непрямой вызов: BLX по адресу из таблицы */
В объектном файле функция вызова — это проверка границы CMP #2, чтение
адреса из таблицы LDR r0, [r1, r0, LSL #2] и переход BX r0: вызов стал
переходом, потому что он последний в функции. Три адреса обработчиков лежат
сразу за этими командами.
Та же таблица без const оказывается в .data — в ОЗУ. Там её может
испортить любая ошибка записи за границу массива, и следующий вызов уйдёт
по мусорному адресу — прямой путь в HardFault. Проверка границы перед
вызовом обязательна в обоих случаях: индекс чаще всего приходит извне.
125.6 Обратные вызовы с контекстом
Драйвер не знает, что делать с принятыми данными, — он вызывает функцию,
которую ему дали. Чтобы одна и та же функция обслуживала несколько
экземпляров, ей передают контекст через void *:
typedef void (*visit_fn)(int value, void *ctx);
static void sum_up(int value, void *ctx)
{
*(int *)ctx += value;
}
Обход for_each не знает, что за ctx, — в опыте он сложил 3 + 1 + 4 + 1 + 5
и получил 14, а та же функция обхода с другим обратным вызовом ищет
максимум. Так устроены обратные вызовы HAL (HAL_UART_RegisterCallback),
очереди FreeRTOS и почти любые библиотеки на Си.
125.7 Конечный автомат на указателях
Состояние автомата — это функция, которая по входу возвращает следующее состояние:
typedef const struct state *(*step_fn)(uint8_t input);
current = current->step(in);
Автомат из опыта ищет в потоке байтов пару «AB». Добавить состояние — одна
функция и одна структура; switch по номеру состояния с каждым новым
случаем растёт и путается. Состояния объявлены const — они тоже лежат
во флеш-памяти.
Второй способ — таблица «состояние × событие». Когда состояний и событий немного, а значение имеет каждое сочетание, автомат удобнее записать одной таблицей: строка — текущее состояние, столбец — событие, в ячейке — следующее состояние и действие.
struct cell {
uint8_t next;
void (*act)(void);
};
static const struct cell fsm[ST_N][EV_N] = {
/* пуск стоп перегрев сброс */
[ST_IDLE] = { {ST_RUN, motor_start}, {ST_IDLE, none}, {ST_FAULT, none}, {ST_IDLE, none} },
[ST_RUN] = { {ST_RUN, none}, {ST_IDLE, motor_stop}, {ST_FAULT, motor_stop}, {ST_RUN, none} },
[ST_FAULT] = { {ST_FAULT, none}, {ST_FAULT, none}, {ST_FAULT, none}, {ST_IDLE, none} },
};
static void feed(enum event ev)
{
if (ev >= EV_N) /* событие пришло извне — проверить */
return;
const struct cell *c = &fsm[st][ev];
if (c->act) /* пропущенная ячейка — NULL */
c->act();
st = (enum state)c->next;
}
Каждая пара записана явно. В switch забытый case молча уходит в
default, а здесь незаполненная ячейка видна при чтении. При работе она
нулевая: указатель на функцию — NULL, следующее состояние — 0. Поэтому во всех
ячейках стоит хотя бы none, а feed проверяет и указатель, и номер события,
пришедший извне (125.5). В опыте двигатель
проходит цепочку «пуск → перегрев → пуск → сброс → пуск → стоп»: перегрев
в работе останавливает двигатель и переводит в аварию, пуск в аварии
ничего не делает, выйти из неё можно только сбросом. Таблица из 12 ячеек
занимает 96 байт на Cortex-M — ячейка 8 байт: номер состояния и указатель
на функцию с выравниванием — и 192 байта на 64-разрядном компьютере.
С const она лежит во флеш-памяти.
125.8 Таблица векторов — такой же массив
Стартовый файл Cortex-M — массив: первое слово — начальный указатель стека,
дальше — адреса обработчиков (раздел 7). Написанный на Си, он выглядит так
же, как таблица из 125.5, только компоновщик кладёт её в начало флеш-памяти
(0x08000000), а по адресу 0 её показывает отображение памяти при загрузке;
приложение за загрузчиком указывает её адрес в VTOR (11.7).
Поэтому опечатка в имени обработчика прерывания не ошибка компиляции:
в таблицу попадает обработчик по умолчанию, и прерывание «уходит в пустоту».
125.9 Опасности
- Нулевой указатель на функцию. Вызов по адресу 0 на Cortex-M — переход в таблицу векторов и HardFault. Проверять перед вызовом, если обратный вызов необязателен.
- Несовпадение сигнатуры. Приведение указателя к другому типу функции и вызов через него — неопределённое поведение.
- Таблица в ОЗУ. Испорченный адрес — непредсказуемый переход.
const. - Индекс извне без проверки. Прямой путь к переходу по чужому адресу.
125.10 Грабли
Не могу прочитать объявление. От имени: вправо [] и (), влево *,
скобки меняют порядок. Разбить typedef. Проверить _Generic.
HardFault при вызове обработчика. Нулевой указатель или испорченная
таблица в ОЗУ. Проверка перед вызовом, таблица const.
Команда не находится, хотя есть в таблице. Таблица для двоичного поиска не отсортирована. Тест порядка на компьютере.
Обработчик прерывания не вызывается. Опечатка в имени — в векторах остался обработчик по умолчанию.
sizeof массива в функции — 4. Массив в параметре стал указателем (раздел 68). Передавать размер отдельно.
125.11 Итоги
- Объявление читают от имени: вправо
[]и(), влево*, скобки меняют порядок;_Genericназывает тип без ошибок. int *ptrs[4]— 16 байт на Cortex-M,int (*pa)[4]— 4, таблица из трёх функций — 12.- Таблица команд с
bsearch:reset 7→ 207,speed 1→ 301, неизвестная → -1. const-таблица обработчиков — во флеш-памяти прямо за кодом, вызов —CMP #2,LDR,BX r0; безconst— в ОЗУ.- Обратный вызов с
void *контекстом — одна функция обхода, разные задачи: сумма в опыте — 14.
Полные исходники
decl125.c — сложные объявления, таблица команд и обратный вызов с контекстом — опыт на трёх машинах.
/* Раздел 125: объявления и указатели на функции — опыт на трёх машинах.
* Типы сложных объявлений называет сама программа через _Generic: так нельзя
* ошибиться в чтении — компилятор решает, что это за тип. Дальше — таблица
* команд с поиском по имени и обратный вызов с указателем на контекст. */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#define KIND(x) _Generic((x), \
int *: "указатель на int", \
int **: "указатель на указатель на int", \
int (*)[4]: "указатель на массив из 4 int", \
void (*)(void): "указатель на функцию", \
void (**)(void): "указатель на указатель на функцию", \
default: "другое")
static void on_start(void) { }
/* ── таблица команд: имя и обработчик, отсортирована по имени ─────────── */
typedef int (*cmd_fn)(int arg);
static int cmd_led(int arg) { return 100 + arg; }
static int cmd_reset(int arg) { return 200 + arg; }
static int cmd_speed(int arg) { return 300 + arg; }
struct cmd {
const char *name;
cmd_fn fn;
};
static const struct cmd commands[] = { /* по алфавиту — для двоичного поиска */
{ "led", cmd_led },
{ "reset", cmd_reset },
{ "speed", cmd_speed },
};
static int by_name(const void *key, const void *elem)
{
return strcmp((const char *)key, ((const struct cmd *)elem)->name);
}
static int run_command(const char *name, int arg)
{
const struct cmd *c = bsearch(name, commands, sizeof commands / sizeof commands[0],
sizeof commands[0], by_name);
return c ? c->fn(arg) : -1;
}
/* ── обратный вызов с контекстом: функция не знает, что лежит за void * ── */
typedef void (*visit_fn)(int value, void *ctx);
static void for_each(const int *a, int n, visit_fn visit, void *ctx)
{
for (int i = 0; i < n; i++)
visit(a[i], ctx);
}
static void sum_up(int value, void *ctx)
{
*(int *)ctx += value;
}
int main(void)
{
int x = 0, *p = &x, arr[4] = { 0 };
int *ptrs[4] = { 0 };
void (*h)(void) = on_start;
void (*table[3])(void) = { on_start, on_start, on_start };
printf("kind_p=%s\n", KIND(p));
printf("kind_pp=%s\n", KIND(&p));
printf("kind_arr_addr=%s\n", KIND(&arr));
printf("kind_ptrs_elem=%s\n", KIND(ptrs[0]));
printf("kind_h=%s\n", KIND(h));
printf("kind_table_elem_addr=%s\n", KIND(&table[0]));
printf("size_ptrs=%zu\n", sizeof ptrs); /* массив из 4 указателей */
printf("size_arr_ptr=%zu\n", sizeof(&arr)); /* один указатель на массив */
printf("size_table=%zu\n", sizeof table); /* 3 указателя на функцию */
printf("cmd_reset=%d\n", run_command("reset", 7));
printf("cmd_speed=%d\n", run_command("speed", 1));
printf("cmd_unknown=%d\n", run_command("stop", 0));
int data[5] = { 3, 1, 4, 1, 5 }, total = 0;
for_each(data, 5, sum_up, &total);
printf("callback_sum=%d\n", total);
printf("done=1\n");
return 0;
}
decl125_arm.c — указатели на функции на Cortex-M: таблица во флеш-памяти и автомат на указателях.
/* Раздел 125: указатели на функции на Cortex-M — armclang для F103 и F407.
* Таблица обработчиков с const лежит во флеш-памяти, вызов через неё — BLX;
* конечный автомат, где состояние — это указатель на функцию. */
#include <stdint.h>
typedef void (*handler_t)(void);
volatile uint32_t g_log;
static void on_idle(void) { g_log = 1; }
static void on_run(void) { g_log = 2; }
static void on_error(void) { g_log = 3; }
/* Таблица только для чтения: массив константных указателей на функции. */
static void (*const handlers[3])(void) = { on_idle, on_run, on_error };
/* Та же таблица без const — окажется в ОЗУ и её можно испортить. */
handler_t handlers_ram[3] = { on_idle, on_run, on_error };
void dispatch(unsigned event)
{
if (event < sizeof handlers / sizeof handlers[0])
handlers[event](); /* непрямой вызов: BLX по адресу из таблицы */
}
/* ── конечный автомат: состояние — функция, возвращающая следующее ─────── */
struct state;
typedef const struct state *(*step_fn)(uint8_t input);
struct state {
step_fn step;
};
static const struct state st_wait, st_got_a;
static const struct state *wait_step(uint8_t in)
{
return in == 'A' ? &st_got_a : &st_wait;
}
static const struct state *got_a_step(uint8_t in)
{
if (in == 'B')
g_log = 0xAB; /* найдена пара «AB» */
return &st_wait;
}
static const struct state st_wait = { wait_step };
static const struct state st_got_a = { got_a_step };
static const struct state *current = &st_wait;
void feed(uint8_t in)
{
current = current->step(in);
}
fsm125.c — автомат таблицей «состояние × событие».
/* Подраздел 125.7: автомат таблицей «состояние × событие».
* Двигатель: ожидание, работа, авария; события — пуск, стоп, перегрев, сброс.
* В ячейке — следующее состояние и действие. Каждая пара «состояние, событие»
* записана явно: забыть случай нельзя, а таблица const лежит во флеш-памяти. */
#include <stdint.h>
#include <stdio.h>
enum state { ST_IDLE, ST_RUN, ST_FAULT, ST_N };
enum event { EV_START, EV_STOP, EV_HOT, EV_RESET, EV_N };
static int motor_on;
static void none(void) { }
static void motor_start(void) { motor_on = 1; }
static void motor_stop(void) { motor_on = 0; }
struct cell {
uint8_t next;
void (*act)(void);
};
static const struct cell fsm[ST_N][EV_N] = {
/* пуск стоп перегрев сброс */
[ST_IDLE] = { {ST_RUN, motor_start}, {ST_IDLE, none}, {ST_FAULT, none}, {ST_IDLE, none} },
[ST_RUN] = { {ST_RUN, none}, {ST_IDLE, motor_stop}, {ST_FAULT, motor_stop}, {ST_RUN, none} },
[ST_FAULT] = { {ST_FAULT, none}, {ST_FAULT, none}, {ST_FAULT, none}, {ST_IDLE, none} },
};
static enum state st = ST_IDLE;
static void feed(enum event ev)
{
if (ev >= EV_N) /* событие пришло извне — проверить */
return;
const struct cell *c = &fsm[st][ev];
if (c->act) /* пропущенная ячейка — NULL */
c->act();
st = (enum state)c->next;
}
int main(void)
{
static const char *const sname[ST_N] = {"idle", "run", "fault"};
static const char *const ename[EV_N] = {"start", "stop", "hot", "reset"};
static const enum event trace[] = {EV_START, EV_HOT, EV_START, EV_RESET, EV_START, EV_STOP};
for (unsigned i = 0; i < sizeof trace / sizeof trace[0]; i++) {
feed(trace[i]);
printf("step_%u=%s>%s motor=%d\n", i, ename[trace[i]], sname[st], motor_on);
}
printf("cell=%u table=%u\n", (unsigned)sizeof(struct cell), (unsigned)sizeof fsm);
printf("done=1\n");
return 0;
}
Дальше: раздел 126 «Данные в памяти» — выравнивание, упаковка, битовые поля и порядок байт.