Реальное использование X-Macros

Я только что узнал о X-Macros. Какие реальные применения X-Macros вы видели? Когда они являются подходящим инструментом для работы?

8 ответов

Решение

Я обнаружил 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()", которая будет вызываться, если какой-либо недопустимый командный байт используется для индексации в моей таблице смещений.

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

Я использую X-макросы для вывода значений перечисления в виде строк. И с тех пор, как я с ней столкнулся, я настоятельно предпочитаю эту форму, в которой для каждого элемента применяется макрос "пользователь". Включение нескольких файлов намного сложнее.

/* x-macro constructors for error and type
   enums and string tables */
#define AS_BARE(a) a ,
#define AS_STR(a) #a ,

#define ERRORS(_) \
    _(noerror) \
    _(dictfull) _(dictstackru) _(dictstackunderflow) \
    _(execstackru) _(execstackunderflow) _(limitcheck) \
    _(VMerror)
enum err { ERRORS(AS_BARE) };
char *errorname[] = { ERRORS(AS_STR) };
/* puts(errorname[(enum err)limitcheck]); */

Я также использую их для отправки функций в зависимости от типа объекта. Опять же, взломав тот же макрос, который я использовал для создания значений перечисления.

#define TYPES(_) \
    _(invalid) \
    _(null) \
    _(mark) \
    _(integer) \
    _(real) \
    _(array) \
    _(dict) \
    _(save) \
    _(name) \
    _(string) \
/*enddef TYPES */

#define AS_TYPE(_) _ ## type ,
enum { TYPES(AS_TYPE) };

Использование макроса гарантирует, что все мои индексы массива будут соответствовать связанным значениям перечисления, потому что они строят свои различные формы, используя пустые токены из определения макроса (макрос TYPES).

typedef void evalfunc(context *ctx);

void evalquit(context *ctx) { ++ctx->quit; }

void evalpop(context *ctx) { (void)pop(ctx->lo, adrent(ctx->lo, OS)); }

void evalpush(context *ctx) {
    push(ctx->lo, adrent(ctx->lo, OS),
            pop(ctx->lo, adrent(ctx->lo, ES)));
}

evalfunc *evalinvalid = evalquit;
evalfunc *evalmark = evalpop;
evalfunc *evalnull = evalpop;
evalfunc *evalinteger = evalpush;
evalfunc *evalreal = evalpush;
evalfunc *evalsave = evalpush;
evalfunc *evaldict = evalpush;
evalfunc *evalstring = evalpush;
evalfunc *evalname = evalpush;

evalfunc *evaltype[stringtype/*last type in enum*/+1];
#define AS_EVALINIT(_) evaltype[_ ## type] = eval ## _ ;
void initevaltype(void) {
    TYPES(AS_EVALINIT)
}

void eval(context *ctx) {
    unsigned ades = adrent(ctx->lo, ES);
    object t = top(ctx->lo, ades, 0);
    if ( isx(t) ) /* if executable */
        evaltype[type(t)](ctx);  /* <--- the payoff is this line here! */
    else
        evalpush(ctx);
}

Использование X-макросов таким способом на самом деле помогает компилятору выдавать полезные сообщения об ошибках. Я опустил функцию evalarray из вышеперечисленного, потому что это отвлекает от моей точки зрения. Но если вы попытаетесь скомпилировать приведенный выше код (закомментировав другие вызовы функций и предоставив фиктивную typedef для контекста, конечно), компилятор пожалуется на отсутствующую функцию. Для каждого нового типа, который я добавляю, мне напоминают добавить обработчик, когда я перекомпилирую этот модуль. Таким образом, X-макрос помогает гарантировать, что параллельные структуры остаются неизменными даже при росте проекта.

Редактировать:

Этот ответ поднял мою репутацию на 50%. Так что вот еще немного. Ниже приведен негативный пример ответа на вопрос: когда не использовать X-Macros?

В этом примере показана упаковка произвольных фрагментов кода в X-"запись". В конце концов я отказался от этой ветви проекта и не использовал эту стратегию в более поздних проектах (и не из-за отсутствия попыток). Как-то странно стало. Действительно, макрос называется X6, потому что в один момент было 6 аргументов, но я устал от изменения имени макроса.

/* Object types */
/* "'X'" macros for Object type definitions, declarations and initializers */
// a                      b            c              d
// enum,                  string,      union member,  printf d
#define OBJECT_TYPES \
X6(    nulltype,        "null",     int dummy      ,            ("<null>")) \
X6(    marktype,        "mark",     int dummy2      ,           ("<mark>")) \
X6( integertype,     "integer",     int  i,     ("%d",o.i)) \
X6( booleantype,     "boolean",     bool b,     (o.b?"true":"false")) \
X6(    realtype,        "real",     float f,        ("%f",o.f)) \
X6(    nametype,        "name",     int  n,     ("%s%s", \
        (o.flags & Fxflag)?"":"/", names[o.n])) \
X6(  stringtype,      "string",     char *s,        ("%s",o.s)) \
X6(    filetype,        "file",     FILE *file,     ("<file %p>",(void *)o.file)) \
X6(   arraytype,       "array",     Object *a,      ("<array %u>",o.length)) \
X6(    dicttype,        "dict",     struct s_pair *d, ("<dict %u>",o.length)) \
X6(operatortype,    "operator",     void (*o)(),    ("<op>")) \

#define X6(a, b, c, d) #a,
char *typestring[] = { OBJECT_TYPES };
#undef X6

// the Object type
//forward reference so s_object can contain s_objects
typedef struct s_object Object;

// the s_object structure:
// a bit convoluted, but it boils down to four members:
// type, flags, length, and payload (union of type-specific data)
// the first named union member is integer, so a simple literal object
// can be created on the fly:
// Object o = {integertype,0,0,4028}; //create an int object, value: 4028
// Object nl = {nulltype,0,0,0};
struct s_object {
#define X6(a, b, c, d) a,
    enum e_type { OBJECT_TYPES } type;
#undef X6
unsigned int flags;
#define Fread  1
#define Fwrite 2
#define Fexec  4
#define Fxflag 8
size_t length; //for lint, was: unsigned int
#define X6(a, b, c, d) c;
    union { OBJECT_TYPES };
#undef X6
};

Одной из больших проблем были строки формата printf. Хотя это выглядит круто, это просто фокус-покус. Так как он используется только в одной функции, чрезмерное использование макроса фактически отделяет информацию, которая должна быть вместе; и это делает функцию нечитаемой сама по себе. Запутывание вдвойне неудачно в такой функции отладки.

//print the object using the type's format specifier from the macro
//used by O_equal (ps: =) and O_equalequal (ps: ==)
void printobject(Object o) {
    switch (o.type) {
#define X6(a, b, c, d) \
        case a: printf d; break;
OBJECT_TYPES
#undef X6
    }
}

Так что не увлекайся. Как я и сделал.

В виртуальной машине Oracle HotSpot для языка программирования Java® есть файл globals.hpp, который использует RUNTIME_FLAGS таким образом.

Смотрите исходный код:

Мне нравится использовать X-макросы для создания "богатых перечислений", которые поддерживают итерацию значений перечисления, а также получение строкового представления для каждого значения перечисления:

#define MOUSE_BUTTONS \
X(LeftButton, 1)   \
X(MiddleButton, 2) \
X(RightButton, 4)

struct MouseButton {
  enum Value {
    None = 0
#define X(name, value) ,name = value
MOUSE_BUTTONS
#undef X
  };

  static const int *values() {
    static const int a[] = {
      None,
#define X(name, value) name,
    MOUSE_BUTTONS
#undef X
      -1
    };
    return a;
  }

  static const char *valueAsString( Value v ) {
#define X(name, value) static const char str_##name[] = #name;
MOUSE_BUTTONS
#undef X
    switch ( v ) {
      case None: return "None";
#define X(name, value) case name: return str_##name;
MOUSE_BUTTONS
#undef X
    }
    return 0;
  }
};

Это не только определяет MouseButton::Value enum, это также позволяет мне делать такие вещи, как

// Print names of all supported mouse buttons
for ( const int *mb = MouseButton::values(); *mb != -1; ++mb ) {
    std::cout << MouseButton::valueAsString( (MouseButton::Value)*mb ) << "\n";
}

Я использую довольно массивный X-макрос для загрузки содержимого INI-файла в структуру конфигурации, помимо прочего, вращаясь вокруг этой структуры.

Вот как выглядит мой файл "configuration.def":

#define NMB_DUMMY(...) X(__VA_ARGS__)
#define NMB_INT_DEFS \
   TEXT("long int") , long , , , GetLongValue , _ttol , NMB_SECT , SetLongValue , 

#define NMB_STR_DEFS NMB_STR_DEFS__(TEXT("string"))
#define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path"))

#define NMB_STR_DEFS__(ATYPE) \
  ATYPE ,  basic_string<TCHAR>* , new basic_string<TCHAR>\
  , delete , GetValue , , NMB_SECT , SetValue , *

/* X-macro starts here */

#define NMB_SECT "server"
NMB_DUMMY(ip,TEXT("Slave IP."),TEXT("10.11.180.102"),NMB_STR_DEFS)
NMB_DUMMY(port,TEXT("Slave portti."),TEXT("502"),NMB_STR_DEFS)
NMB_DUMMY(slaveid,TEXT("Slave protocol ID."),0xff,NMB_INT_DEFS)
.
. /* And so on for about 40 items. */

Это немного сбивает с толку, я признаю. Быстро стало ясно, что я не хочу писать все эти объявления типов после каждого макроса поля. (Не волнуйтесь, есть большой комментарий, чтобы объяснить все, что я опущен для краткости.)

И вот как я объявляю структуру конфигурации:

typedef struct {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) TYPE ID;
#include "configuration.def"
#undef X
  basic_string<TCHAR>* ini_path;  //Where all the other stuff gets read.
  long verbosity;                 //Used only by console writing functions.
} Config;

Затем в коде сначала значения по умолчанию считываются в структуру конфигурации:

#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,...) \
  conf->ID = CONSTRUCTOR(DEFVAL);
#include "configuration.def"
#undef X

Затем INI считывается в структуру конфигурации следующим образом, используя библиотеку SimpleIni:

#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,SETTER,DEREF...)\
  DESTRUCTOR (conf->ID);\
  conf->ID  = CONSTRUCTOR( ini.GETTER(TEXT(SECT),TEXT(#ID),DEFVAL,FALSE) );\
  LOG3A(<< left << setw(13) << TEXT(#ID) << TEXT(": ")  << left << setw(30)\
    << DEREF conf->ID << TEXT(" (") << DEFVAL << TEXT(").") );
#include "configuration.def"
#undef X

А переопределения из флагов командной строки, которые также отформатированы с одинаковыми именами (в расширенной форме GNU), применяются следующим образом с использованием библиотеки SimpleOpt:

enum optflags {
#define X(ID,...) ID,
#include "configuration.def"
#undef X
  };
  CSimpleOpt::SOption sopt[] = {
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,...) {ID,TEXT("--") #ID TEXT("="), SO_REQ_CMB},
#include "configuration.def"
#undef X
    SO_END_OF_OPTIONS
  };
  CSimpleOpt ops(argc,argv,sopt,SO_O_NOERR);
  while(ops.Next()){
    switch(ops.OptionId()){
#define X(ID,DESC,DEFVAL,ATYPE,TYPE,CONSTRUCTOR,DESTRUCTOR,GETTER,STRCONV,SECT,...) \
  case ID:\
    DESTRUCTOR (conf->ID);\
    conf->ID = STRCONV( CONSTRUCTOR (  ops.OptionArg() ) );\
    LOG3A(<< TEXT("Omitted ")<<left<<setw(13)<<TEXT(#ID)<<TEXT(" : ")<<conf->ID<<TEXT(" ."));\
    break;
#include "configuration.def"
#undef X
    }
  }

И так далее, я также использую тот же макрос для печати вывода --help -flag и примера файла ini по умолчанию, файл configuration.def включен в мою программу 8 раз. "Квадратный колышек в круглое отверстие", может быть; Как на самом деле поступит компетентный программист? Много-много циклов и обработка строк?

В Chromium есть интересный вариант X-макроса на dom_code_data.inc. Только вот это не просто макрос, а совершенно отдельный файл. Этот файл предназначен для сопоставления ввода с клавиатуры между скан-кодами различных платформ, кодами USB HID и строковыми именами.

Файл содержит такой код:

DOM_CODE_DECLARATION {

  //            USB     evdev    XKB     Win     Mac   Code
  DOM_CODE(0x000000, 0x0000, 0x0000, 0x0000, 0xffff, NULL, NONE), // Invalid
...
};

Каждый вызов макроса фактически передает 7 аргументов, и макрос может выбирать, какие аргументы использовать, а какие игнорировать. Одно из применений - сопоставление кодов клавиш ОС и платформенно-независимых сканкодов и строк DOM. В разных ОС используются разные макросы для выбора кодов клавиш, подходящих для этой ОС.

// Table of USB codes (equivalent to DomCode values), native scan codes,
// and DOM Level 3 |code| strings.
#if defined(OS_WIN)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
  { usb, win, code }
#elif defined(OS_LINUX)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
  { usb, xkb, code }
#elif defined(OS_MACOSX)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
  { usb, mac, code }
#elif defined(OS_ANDROID)
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
  { usb, evdev, code }
#else
#define DOM_CODE(usb, evdev, xkb, win, mac, code, id) \
  { usb, 0, code }
#endif
#define DOM_CODE_DECLARATION const KeycodeMapEntry usb_keycode_map[] =
#include "ui/events/keycodes/dom/dom_code_data.inc"
#undef DOM_CODE
#undef DOM_CODE_DECLARATION

https://github.com/whunmr/DataEx

использование следующего xmacros для генерации класса C++ со встроенными функциями сериализации и десериализации.

#define __FIELDS_OF_DataWithNested(_)  \
  _(1, a, int  )                       \
  _(2, x, DataX)                       \
  _(3, b, int  )                       \
  _(4, c, char )                       \
  _(5, d, __array(char, 3))            \
  _(6, e, string)                      \
  _(7, f, bool)

DEF_DATA(DataWithNested);

использование:

TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) {
  DataWithNested xn;
  xn.a = 0xCAFEBABE;
  xn.x.a = 0x12345678;
  xn.x.b = 0x11223344;
  xn.b = 0xDEADBEEF;
  xn.c = 0x45;
  memcpy(&xn.d, "XYZ", strlen("XYZ"));

  char buf_with_zero[] = {0x11, 0x22, 0x00, 0x00, 0x33};
  xn.e = string(buf_with_zero, sizeof(buf_with_zero));
  xn.f = true;

  __encode(DataWithNested, xn, buf_);

  char expected[] = { 0x01, 0x04, 0x00, 0xBE, 0xBA, 0xFE, 0xCA
                             , 0x02, 0x0E, 0x00 /*T and L of nested X*/
                             , 0x01, 0x04, 0x00, 0x78, 0x56, 0x34, 0x12
                             , 0x02, 0x04, 0x00, 0x44, 0x33, 0x22, 0x11
                             , 0x03, 0x04, 0x00, 0xEF, 0xBE, 0xAD, 0xDE
                             , 0x04, 0x01, 0x00, 0x45
                             , 0x05, 0x03, 0x00, 'X', 'Y', 'Z'
                             , 0x06, 0x05, 0x00, 0x11, 0x22, 0x00, 0x00, 0x33
                             , 0x07, 0x01, 0x00, 0x01};

  EXPECT_TRUE(ArraysMatch(expected, buf_));
}

также, другой пример находится в https://github.com/whunmr/msgrpc

Мой скромный пример:

Один из шагов по ускорению декодера FFmpeg HEVC — хардкодить матрицу, состоящую всего из трех строк мелких целочисленных коэффициентов, которая используется в нескольких местах :8c65aa37510be2621e7b5a550a33c445b4c85607a789c9b483c2e78cdffcd65bL607

Другие вопросы по тегам