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

125. Declarations and function pointers: handler tables and callbacks

C declarations are not read from left to right, and void (*const handlers[3])(void) scares even people who have written C for years. Yet these are exactly the constructs that hold firmware together: the interrupt vector table is an array of pointers to functions, drivers hand over data through callbacks, state machines switch states with a function pointer. This section teaches you to read declarations by one rule, checking yourself with the compiler itself, and to build handler tables so that they live in flash memory.

Terms used in the documentation

Term Meaning
declarator the name together with *, [] and ()
pointer to function a pointer that holds the address of a function
function pointer table, dispatch table a table of handlers
callback a function the caller hands over to be called back
context pointer (void *user) a pointer to the caller's data
typedef a type alias
generic selection (_Generic) a choice by the type of an expression
state machine a finite state machine
tail call a call at the end of a function that became a jump
indirect branch a jump to an address held in a register

125.1 The reading rule: from the name outwards

How to read the declaration of a handler table: from the name — first to the right, then to the left; parentheses change the order

You read a declaration from the name: first to the right ([] — array, () — function), then to the left (* — pointer), and parentheses change the order, as in arithmetic. void (*const handlers[3])(void): handlers — on the right [3], an array of three; on the left *const — of constant pointers; past the parenthesis on the right (void) — to functions without arguments; on the left void — returning void.

It is easier to check yourself with the compiler than by eye: _Generic picks a branch by the type of an expression, and the program names the type itself.

int (*)[4]: "указатель на массив из 4 int",                  \
void (**)(void): "указатель на указатель на функцию",        \

125.2 Parentheses change everything

The experiment: three declarations, their types from _Generic and their sizes on two machines:

Declaration What it is Cortex-M, bytes x86-64, bytes
int *ptrs[4] an array of 4 pointers to int 16 32
int (*pa)[4] a pointer to an array of 4 int 4 8
void (*table[3])(void) an array of 3 pointers to function 12 24

int *ptrs[4] and int (*pa)[4] differ by one pair of parentheses, but in meaning they differ in everything: the first takes four pointers, the second takes one. The type names from gcc, clang and ARM matched completely; the sizes on Cortex-M are half as large — a pointer there is 4 bytes (section 122).

_Generic will not name the array itself: the expression inside goes through the usual conversions, and the array decays to a pointer to its first element — _Generic(ptrs, …) sees int **, and _Generic(table, …) sees void (**)(void). That is why the experiment asks for the element type (ptrs[0]) and the addresses (&table[0]), and the type of the array itself is checked through its address: _Generic(&ptrs, int *(*)[4]: …).

An array of four pointers to int and one pointer to a whole array of four int


125.3 typedef makes declarations readable

You break a long declaration up with a type alias:

typedef void (*handler_t)(void);

After that, handler_t table[3] reads at once. The rule for firmware: the type of a handler or a callback always goes through typedef, with a name that says what it is for, not how it is built.


125.4 A command table

The classic parsing of commands from UART or CAN: the name and the handler in one table, sorted by name, with binary search.

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;

In the experiment reset 7 gave 207, speed 1 gave 301, an unknown command gave -1. Adding a command is one line in the table, not a new branch in a switch two hundred lines long. The key condition of binary search is the order of the rows; it is convenient to check it with a test on the computer.


125.5 The table in flash memory

On a board it matters where the table lives. const on the pointer itself — void (*const handlers[3])(void) — makes the table read-only, and armclang places it right after the code of the function, in flash memory:

if (event < sizeof handlers / sizeof handlers[0])
    handlers[event]();               /* непрямой вызов: BLX по адресу из таблицы */

In the object file the calling function is a bound check CMP #2, a read of the address from the table LDR r0, [r1, r0, LSL #2] and a jump BX r0: the call became a jump because it is the last one in the function. The three handler addresses lie right after these instructions.

The same table without const ends up in .data — in RAM. There it can be corrupted by any write past the end of an array, and the next call goes to a garbage address — a straight road to HardFault. A bound check before the call is mandatory in both cases: the index most often comes from outside.


125.6 Callbacks with a context

A driver does not know what to do with the received data — it calls the function it was given. So that one and the same function can serve several instances, it gets a context through void *:

typedef void (*visit_fn)(int value, void *ctx);
static void sum_up(int value, void *ctx)
{
    *(int *)ctx += value;
}

The for_each walk does not know what ctx is — in the experiment it added up 3 + 1 + 4 + 1 + 5 and got 14, and the same walk function with a different callback finds the maximum. This is how HAL callbacks (HAL_UART_RegisterCallback), FreeRTOS queues and almost any C library work.


125.7 A state machine on pointers

A state of the machine is a function that takes the input and returns the next state:

typedef const struct state *(*step_fn)(uint8_t input);
current = current->step(in);

The machine from the experiment looks for the pair "AB" in a byte stream. Adding a state takes one function and one structure; a switch on the state number grows and gets tangled with every new case. The states are declared const — they also live in flash memory.

The second way is a "state × event" table. When there are few states and events, and every combination matters, the machine is easier to write as one table: a row is the current state, a column is the event, the cell holds the next state and the action.

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

Every pair is written out explicitly. In a switch a forgotten case silently falls into default, while here an empty cell is visible when you read the table. At run time it is zero: the function pointer is NULL, the next state is 0. That is why every cell holds at least none, and feed checks both the pointer and the event number that came from outside (125.5). In the experiment the motor goes through the chain "start → overheat → start → reset → start → stop": an overheat while running stops the motor and moves it to the fault state, a start in the fault state does nothing, and the only way out of it is a reset. The table of 12 cells takes 96 bytes on Cortex-M — a cell is 8 bytes: the state number and a pointer to a function with alignment — and 192 bytes on a 64-bit computer. With const it lives in flash memory.


125.8 The vector table is the same kind of array

The Cortex-M startup file is an array: the first word is the initial stack pointer, then come the addresses of the handlers (section 7). Written in C, it looks just like the table from 125.5, except that the linker places it at the start of flash memory (0x08000000), and at address 0 it is shown by the memory remapping at boot; an application behind a bootloader writes its address into VTOR (11.7). That is why a typo in the name of an interrupt handler is not a compile error: the default handler gets into the table, and the interrupt "goes nowhere".


125.9 Dangers


125.10 Pitfalls

I cannot read a declaration. From the name: to the right [] and (), to the left *, parentheses change the order. Break it up with typedef. Check it with _Generic.

HardFault when a handler is called. A null pointer or a corrupted table in RAM. A check before the call, a const table.

A command is not found although it is in the table. The table for binary search is not sorted. A test of the order on the computer.

An interrupt handler is not called. A typo in the name — the default handler stayed in the vectors.

sizeof of an array inside a function is 4. An array parameter became a pointer (section 68). Pass the size separately.


125.11 Summary

Full sources

decl125.c — complex declarations, a command table and a callback with a context — the experiment on three machines.

/* Раздел 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 — function pointers on Cortex-M: a table in flash memory and a state machine on pointers.

/* Раздел 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 — a state machine as a "state × event" table.

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

Next: section 126 "Data in memory" — alignment, packing, bit fields and byte order.

Open in the interactive handbook