Uso no mundo real de X-Macros

Eu acabei de aprender sobre o X-Macros . Que usos do mundo real do X-Macros você viu? Quando eles são a ferramenta certa para o trabalho?

   

Descobri o X-macros há alguns anos quando comecei a usar os pointers de function no meu código. Eu sou um programador embutido e uso máquinas de estado com freqüência. Muitas vezes eu escreveria código assim:

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

O problema era que eu considerava muito propenso a erros ter que manter a ordenação da minha tabela de ponteiro de function de forma que ela correspondesse à ordenação da minha enumeração de estados.

Um amigo meu me apresentou a macros-X e foi como se uma lâmpada tivesse triggersdo na minha cabeça. Sério, onde você esteve toda a minha vida x-macros!

Então agora eu defino a seguinte tabela:

 #define STATE_TABLE \ ENTRY(STATE0, func0) \ ENTRY(STATE1, func1) \ ENTRY(STATE2, func2) \ ... ENTRY(STATEX, funcX) \ 

E eu posso usá-lo da seguinte maneira:

 enum { #define ENTRY(a,b) a, STATE_TABLE #undef ENTRY NUM_STATES }; 

e

 p_func_t jumptable[NUM_STATES] = { #define ENTRY(a,b) b, STATE_TABLE #undef ENTRY }; 

Como bônus, eu também posso fazer com que o pré-processador construa meus protótipos de function da seguinte maneira:

 #define ENTRY(a,b) static void b(void); STATE_TABLE #undef ENTRY 

Outro uso é declarar e inicializar registros

 #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 

Meu uso favorito, no entanto, é quando se trata de manipuladores de comunicação

Primeiro eu crio uma tabela de comunicação contendo cada nome e código de comando:

 #define COMMAND_TABLE \ ENTRY(RESERVED, reserved, 0x00) \ ENTRY(COMMAND1, command1, 0x01) \ ENTRY(COMMAND2, command2, 0x02) \ ... ENTRY(COMMANDX, commandX, 0x0X) \ 

Eu tenho os nomes de maiúsculas e minúsculas na tabela, porque a maiúscula será usada para enums e a minúscula para nomes de function.

Então eu também defino structs para cada comando para definir como cada comando se parece:

 typedef struct {...}command1_cmd_t; typedef struct {...}command2_cmd_t; etc. 

Da mesma forma eu defino estruturas para cada resposta de comando:

 typedef struct {...}command1_resp_t; typedef struct {...}command2_resp_t; etc. 

Então eu posso definir minha enumeração de código de comando:

 enum { #define ENTRY(a,b,c) a##_CMD = c, COMMAND_TABLE #undef ENTRY }; 

Eu posso definir minha enumeração de comprimento de comando:

 enum { #define ENTRY(a,b,c) a##_CMD_LENGTH = sizeof(b##_cmd_t); COMMAND_TABLE #undef ENTRY }; 

Eu posso definir minha enumeração de comprimento de resposta:

 enum { #define ENTRY(a,b,c) a##_RESP_LENGTH = sizeof(b##_resp_t); COMMAND_TABLE #undef ENTRY }; 

Eu posso determinar quantos comandos existem da seguinte forma:

 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) 

NOTA: Na verdade, nunca instancio o offset_struct_t, apenas o uso como uma maneira de o compilador gerar para mim o meu número de definições de comandos.

Note então eu posso gerar minha tabela de pointers de function da seguinte maneira:

 p_func_t jump_table[NUMBER_OF_COMMANDS] = { #define ENTRY(a,b,c) process_##b, COMMAND_TABLE #undef ENTRY } 

E meus protótipos de function:

 #define ENTRY(a,b,c) void process_##b(void); COMMAND_TABLE #undef ENTRY 

Agora, finalmente, para o uso mais legal de todos os tempos, posso fazer o compilador calcular o tamanho do meu buffer de transmissão.

 /* 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 

Novamente, essa união é como minha struct de deslocamento, ela não é instanciada, em vez disso, posso usar o operador sizeof para declarar meu tamanho de buffer de transmissão.

 uint8_t tx_buf[sizeof(tx_buf_t)]; 

Agora meu buffer de transmissão tx_buf é o tamanho ideal e, como adiciono comandos a esse manipulador de comunicação, meu buffer sempre terá o tamanho ideal. Legal!

Um outro uso é criar tabelas de deslocamento: Como a memory é geralmente uma restrição em sistemas incorporados, não quero usar 512 bytes para minha tabela de salto (2 bytes por ponteiro X 256 comandos possíveis) quando ele é um array esparso. Em vez disso, terei uma tabela de deslocamentos de 8 bits para cada comando possível. Esse deslocamento é então usado para indexar na minha tabela de salto real, que agora só precisa ser NUM_COMMANDS * sizeof (ponteiro). No meu caso, com 10 comandos definidos. Minha tabela de salto tem 20 bytes de comprimento e eu tenho uma tabela de deslocamento com 256 bytes de comprimento, que é um total de 276 bytes em vez de 512 bytes. Eu então chamo minhas funções assim:

 jump_table[offset_table[command]](); 

ao invés de

 jump_table[command](); 

Eu posso criar uma tabela de deslocamento como:

 /* 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 

onde offsetof é uma macro de biblioteca padrão definida em “stddef.h”

Como benefício secundário, existe uma maneira muito fácil de determinar se um código de comando é suportado ou não:

 bool command_is_valid(uint8_t command) { /* return false if not valid, or true (non 0) if valid */ return offset_table[command]; } 

É também por isso que no meu COMMAND_TABLE eu reservei o byte de comando 0. Eu posso criar uma function chamada “process_reserved ()” que será chamada se algum byte de comando inválido for usado para indexar em minha tabela de offset.

X-Macros são essencialmente modelos parametrizados. Então, eles são a ferramenta certa para o trabalho se você precisar de várias coisas semelhantes em vários aspectos. Eles permitem que você crie um formulário abstrato e instancie-o de acordo com regras diferentes.

Eu uso macros-X para gerar valores de enum como strings. E desde que o encontrei, eu prefiro fortemente esta forma que leva uma macro “usuário” para aplicar a cada elemento. A inclusão de vários arquivos é muito mais difícil de se trabalhar.

 /* 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) _(dictstackoverflow) _(dictstackunderflow) \ _(execstackoverflow) _(execstackunderflow) _(limitcheck) \ _(VMerror) enum err { ERRORS(AS_BARE) }; char *errorname[] = { ERRORS(AS_STR) }; /* puts(errorname[(enum err)limitcheck]); */ 

Eu também estou usando para despacho de function com base no tipo de object. Novamente, seqüestrando a mesma macro que usei para criar os valores de enum.

 #define TYPES(_) \ _(invalid) \ _(null) \ _(mark) \ _(integer) \ _(real) \ _(array) \ _(dict) \ _(save) \ _(name) \ _(string) \ /*enddef TYPES */ #define AS_TYPE(_) _ ## type , enum { TYPES(AS_TYPE) }; 

Usando a macro garante que todos os meus índices de matriz irão coincidir com os valores enum associados, porque eles constroem seus vários formulários usando os tokens nus da definição de macro (a macro de tipos).

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

O uso de macros X dessa maneira realmente ajuda o compilador a fornecer mensagens de erro úteis. Eu omiti a function evalarray do acima, porque isso me distrairia do meu ponto. Mas se você tentar compilar o código acima (comentando as outras chamadas de function e fornecendo um typedef fictício para o contexto, é claro), o compilador irá reclamar sobre uma function ausente. Para cada novo tipo que eu adiciono, lembro-me de adicionar um manipulador quando eu recompilar este módulo. Assim, a macro X ajuda a garantir que as estruturas paralelas permaneçam intactas, mesmo com o crescimento do projeto.

Editar:

Esta resposta aumentou minha reputação em 50%. Então aqui está um pouco mais. O seguinte é um exemplo negativo , respondendo a pergunta: quando não usar o X-Macros?

Este exemplo mostra o empacotamento de fragments de código arbitrário no X- "record". Eu finalmente abandonei este ramo do projeto e não usei essa estratégia em projetos posteriores (e não por querer tentar). Tornou-se desinteressado, de alguma forma. Na verdade, a macro é chamada X6 porque em um ponto houve 6 argumentos, mas cansei-me de alterar o nome da macro.

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

Um grande problema foram as strings de formato printf. Embora pareça legal, é apenas hocus pocus. Como ele é usado apenas em uma function, o uso excessivo da macro separou informações que deveriam estar juntas; e torna a function ilegível por si só. A ofuscação é duplamente lamentável em uma function de debugging como essa.

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

Então não se empolgue. Como eu fiz.

No Oracle HotSpot Virtual Machine para a linguagem de programação Java®, existe o arquivo globals.hpp , que usa o RUNTIME_FLAGS dessa maneira.

Veja o código fonte:

  • JDK 7
  • JDK 8
  • JDK 9

Eu gosto de usar macros X para criar ‘enumerações avançadas’ que suportam a iteração dos valores de enum, bem como obter a representação de seqüência de caracteres para cada valor de enumeração:

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

Isso não apenas define um enum MouseButton::Value , mas também me permite fazer coisas como

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

Eu uso uma X-macro bastante massiva para carregar o conteúdo do arquivo INI em uma estrutura de configuração, entre outras coisas que giram em torno dessa estrutura.

Isto é o que meu arquivo “configuration.def” se parece:

 #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* , new basic_string\ , 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. */ 

É um pouco confuso, admito. Rapidamente fica claro que eu não quero realmente escrever todas as declarações de tipo após cada macro de campo. (Não se preocupe, há um grande comentário para explicar tudo o que omiti por brevidade).

E é assim que eu declaro a estrutura de configuração:

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

Então, no código, primeiramente os valores padrão são lidos na estrutura de configuração:

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

Em seguida, o INI é lido na estrutura de configuração da seguinte maneira, usando a biblioteca 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 

E as substituições de flags de linha de comando, que também são formatadas com os mesmos nomes (em forma longa GNU), são aplicadas da seguinte maneira, usando a biblioteca 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 ")<ID<  

E assim por diante, eu também uso a mesma macro para imprimir a saída --help -flag e o arquivo ini padrão de amostra, configuration.def está incluído 8 vezes no meu programa. "Peg quadrado em um buraco redondo", talvez; como um programador realmente competente poderia proceder com isso? Muitos e muitos loops e processamento de string?

https://github.com/whunmr/DataEx

Usando xmacros seguinte para gerar uma class c + +, com serializar e deserializar functionlity builtin.

 #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); 

uso:

 TEST_F(t, DataWithNested_should_able_to_encode_struct_with_nested_struct) { DataWithNested xn; xn.a = 0xCAFEBABE; xn.xa = 0x12345678; xn.xb = 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_)); } 

Além disso, outro exemplo está em https://github.com/whunmr/msgrpc