Я обнаружил X-макросы пару лет назад, когда начал использовать указатели функций в своем коде.Я программист встраиваемых систем и часто использую конечные автоматы.Часто я писал такой код:
/* declare an enumeration of state codes */
enum{ STATE0, STATE1, STATE2, ... , STATEX, NUM_STATES};
/* declare a table of function pointers */
p_func_t jumptable[NUM_STATES] = {func0, func1, func2, ... , funcX};
Проблема заключалась в том, что я считал очень склонным к ошибкам поддерживать порядок в моей таблице указателей на функции так, чтобы она соответствовала порядку моего перечисления состояний.
Мой друг познакомил меня с X-макросами, и у меня в голове погасла лампочка.Серьезно, где ты был всю свою жизнь x-macros!
Так что теперь я определяю следующую таблицу:
#define STATE_TABLE \
ENTRY(STATE0, func0) \
ENTRY(STATE1, func1) \
ENTRY(STATE2, func2) \
...
ENTRY(STATEX, funcX) \
И я могу использовать ее следующим образом:
enum
{
#define ENTRY(a,b) a,
STATE_TABLE
#undef ENTRY
NUM_STATES
};
и
p_func_t jumptable[NUM_STATES] =
{
#define ENTRY(a,b) b,
STATE_TABLE
#undef ENTRY
};
в качестве бонуса, я также могу сделать предварительный процессор для создания прототипов моих функций следующим образом:
#define ENTRY(a,b) static void b(void);
STATE_TABLE
#undef ENTRY
Другое использование - объявление и инициализация регистров
#define IO_ADDRESS_OFFSET (0x8000)
#define REGISTER_TABLE\
ENTRY(reg0, IO_ADDRESS_OFFSET + 0, 0x11)\
ENTRY(reg1, IO_ADDRESS_OFFSET + 1, 0x55)\
ENTRY(reg2, IO_ADDRESS_OFFSET + 2, 0x1b)\
...
ENTRY(regX, IO_ADDRESS_OFFSET + X, 0x33)\
/* declare the registers (where _at_ is a compiler specific directive) */
#define ENTRY(a, b, c) volatile uint8_t a _at_ b:
REGISTER_TABLE
#undef ENTRY
/* initialize registers */
#define ENTRY(a, b, c) a = c;
REGISTER_TABLE
#undef ENTRY
Однако мое любимое использование - когда дело доходит до обработчиков связи
Сначала я создаю таблицу связи, содержащую имя и код каждой команды:
#define COMMAND_TABLE \
ENTRY(RESERVED, reserved, 0x00) \
ENTRY(COMMAND1, command1, 0x01) \
ENTRY(COMMAND2, command2, 0x02) \
...
ENTRY(COMMANDX, commandX, 0x0X) \
У меня естьИ имена в верхнем и нижнем регистре в таблице, потому что верхний регистр будет использоваться для перечислений и нижний регистр для имен функций.
Затем я также определяю структуры для каждой команды, чтобы определить, как каждая команда выглядит:
typedef struct {...}command1_cmd_t;
typedef struct {...}command2_cmd_t;
etc.
Аналогично я определяю структуры для каждого ответа команды:
typedef struct {...}command1_resp_t;
typedef struct {...}command2_resp_t;
etc.
Затем я могу определить перечисление кода моей команды:
enum
{
#define ENTRY(a,b,c) a##_CMD = c,
COMMAND_TABLE
#undef ENTRY
};
Я могу определить перечисление длины команды:
enum
{
#define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t);
COMMAND_TABLE
#undef ENTRY
};
Я могу определить перечисление длины моего ответа:
enum
{
#define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t);
COMMAND_TABLE
#undef ENTRY
};
Я могу определить количество команд следующим образом:
typedef struct
{
#define ENTRY(a,b,c) uint8_t b;
COMMAND_TABLE
#undef ENTRY
} offset_struct_t;
#define NUMBER_OF_COMMANDS sizeof(offset_struct_t)
ПРИМЕЧАНИЕ: я никогда не создаю экземпляр offset_struct_t, я просто использую егокак способ для компилятора генерировать для меня определение моего числа команд.
Обратите внимание, тогда я могу сгенерировать свою таблицу указателей функций следующим образом:
p_func_t jump_table[NUMBER_OF_COMMANDS] =
{
#define ENTRY(a,b,c) process_##b,
COMMAND_TABLE
#undef ENTRY
}
И мои прототипы функций:
#define ENTRY(a,b,c) void process_##b(void);
COMMAND_TABLE
#undef ENTRY
Теперь, наконец, для самого крутого использования, я могу заставить компилятор вычислить, насколько большим должен быть мой буфер передачи.
/* reminder the sizeof a union is the size of its largest member */
typedef union
{
#define ENTRY(a,b,c) uint8_t b##_buf[sizeof(b##_cmd_t)];
COMMAND_TABLE
#undef ENTRY
}tx_buf_t
Опять же, это объединение похоже на мою структуру смещения, этоне создан, вместо этого я могу использовать оператор sizeof для объявления размера моего буфера передачи.
uint8_t tx_buf[sizeof(tx_buf_t)];
Теперь мой буфер передачи tx_buf - оптимальный размер, и когда я добавляю команды в этот обработчик связи, мой буфер всегда будетоптимальный размер.Круто!
Еще одно использование - создание таблиц смещения: поскольку память часто является ограничением для встроенных систем, я не хочу использовать 512 байт для моей таблицы переходов (2 байта на указатель X 256 возможных команд)когда это редкий массив.Вместо этого у меня будет таблица 8-битных смещений для каждой возможной команды.Это смещение затем используется для индексации в моей реальной таблице переходов, которая теперь должна быть только NUM_COMMANDS * sizeof (указатель).В моем случае с 10 определенными командами.Моя таблица переходов имеет длину 20 байт, и у меня есть таблица смещений длиной 256 байт, что в сумме составляет 276 байт вместо 512 байт.Затем я вызываю свои функции следующим образом:
jump_table[offset_table[command]]();
вместо
jump_table[command]();
Я могу создать таблицу смещения следующим образом:
/* initialize every offset to 0 */
static uint8_t offset_table[256] = {0};
/* for each valid command, initialize the corresponding offset */
#define ENTRY(a,b,c) offset_table[c] = offsetof(offset_struct_t, b);
COMMAND_TABLE
#undef ENTRY
где offsetof являетсямакрос стандартной библиотеки, определенный в "stddef.h"
Дополнительным преимуществом является очень простой способ определить, поддерживается ли код команды:
bool command_is_valid(uint8_t command)
{
/* return false if not valid, or true (non 0) if valid */
return offset_table[command];
}
Это такжепочему в моем COMMAND_TABLE я зарезервировал командный байт 0. Я могу создать одну функцию с именем "process_reserved ()", которая будет вызываться, если какой-либо недопустимый командный байт используется для индексации в моей таблице смещений.