Uso del mondo reale di X-Macros

Ho appena appreso di X-Macros . Quali usi reali di X-Macros hai visto? Quando sono lo strumento giusto per il lavoro?

Ho scoperto X-macros un paio di anni fa quando ho iniziato a utilizzare i puntatori di funzione nel mio codice. Sono un programmatore incorporato e utilizzo spesso le macchine a stati. Spesso scrivo codice come questo:

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

Il problema era che consideravo molto incline agli errori mantenere l’ordine della tabella dei puntatori delle funzioni in modo tale che corrispondesse all’ordine della mia enumerazione di stati.

Un mio amico mi ha fatto conoscere X-macros ed è stato come se una lampadina si fosse spenta nella mia testa. Seriamente, dove sei stato per tutta la mia vita x-macros!

Quindi ora definisco la seguente tabella:

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

E posso usarlo come segue:

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

come bonus, posso anche fare in modo che il pre-processore costruisca i miei prototipi di funzioni come segue:

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

Un altro uso è quello di dichiarare e inizializzare i registri

 #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 

Il mio utilizzo preferito è comunque quando si tratta di gestori di comunicazione

Per prima cosa creo una tabella delle comunicazioni, contenente ogni nome e codice di comando:

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

Ho entrambi i nomi maiuscoli e minuscoli nella tabella, perché il maiuscolo verrà utilizzato per le enumerazioni e il minuscolo per i nomi delle funzioni.

Quindi definisco anche le strutture per ciascun comando per definire l’aspetto di ciascun comando:

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

Allo stesso modo, definisco le strutture per ciascuna risposta di comando:

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

Quindi posso definire la mia enumerazione del codice comando:

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

Posso definire la mia enumerazione di lunghezza di comando:

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

Posso definire la mia lunghezza di risposta enumerazione:

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

Posso determinare quanti comandi ci sono come segue:

 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: Non ho mai istanziato l’offset_struct_t, lo uso solo come un modo per il compilatore di generare per me il mio numero di definizione dei comandi.

Nota quindi posso generare la mia tabella dei puntatori di funzione come segue:

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

E i miei prototipi di funzioni:

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

Ora, infine, per il miglior utilizzo ansible, posso fare in modo che il compilatore calcoli quanto dovrebbe essere grande il mio buffer di trasmissione.

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

Ancora una volta questa unione è come la mia struct offset, non è istanziata, invece posso usare l’operatore sizeof per dichiarare la dimensione del mio buffer di trasmissione.

 uint8_t tx_buf[sizeof(tx_buf_t)]; 

Ora il mio buffer di trasmissione tx_buf è la dimensione ottimale e mentre aggiungo comandi a questo gestore di comunicazioni, il mio buffer sarà sempre la dimensione ottimale. Freddo!

Un altro uso è creare tabelle di offset: poiché la memoria è spesso un vincolo sui sistemi incorporati, non voglio usare 512 byte per la mia tabella di salto (2 byte per puntatore X 256 possibili comandi) quando si tratta di una matrice sparsa. Invece avrò una tabella di offset a 8 bit per ogni comando ansible. Questo offset viene quindi utilizzato per indicizzare la mia tabella di salto effettiva che ora deve essere NUM_COMMANDS * sizeof (puntatore). Nel mio caso con 10 comandi definiti. La mia tabella di salto è lunga 20 byte e ho una tabella di offset lunga 256 byte, che è un totale di 276 byte invece di 512 byte. Quindi chiamo le mie funzioni in questo modo:

 jump_table[offset_table[command]](); 

invece di

 jump_table[command](); 

Posso creare una tabella di offset in questo modo:

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

dove offsetof è una macro di libreria standard definita in “stddef.h”

Come vantaggio collaterale, c’è un modo molto semplice per determinare se un codice di comando è supportato o meno:

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

Questo è anche il motivo per cui nel mio byte di comando riservato COMMAND_TABLE ho 0. Posso creare una funzione chiamata “process_reserved ()” che verrà chiamata se qualsiasi byte di comando non valido viene usato per indicizzare nella mia tabella di offset.

Gli X-Macros sono essenzialmente modelli parametrizzati. Quindi sono lo strumento giusto per il lavoro se hai bisogno di molte cose simili in diverse forms. Ti permettono di creare una forma astratta e di istanziarla secondo regole diverse.

Io uso X-macros per produrre valori di enum come stringhe. E dal momento che l’ho incontrato, preferisco fortemente questo modulo che accetta una macro “utente” da applicare a ciascun elemento. L’inclusione di più file è molto più doloroso con cui lavorare.

 /* 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]); */ 

Li sto anche usando per la spedizione delle funzioni in base al tipo di object. Di nuovo dirottando la stessa macro ho usato per creare i valori enum.

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

L’utilizzo della macro garantisce che tutti i miei indici di array corrisponderanno ai valori enum associati, poiché costruiscono le loro varie forms utilizzando i token nudi dalla definizione di macro (la macro 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); } 

L'utilizzo di X-macros in questo modo aiuta effettivamente il compilatore a fornire utili messaggi di errore. Ho omesso la funzione di evalarray da quanto sopra perché sarebbe distratto dal mio punto. Ma se si tenta di compilare il codice precedente (commentando le altre chiamate di funzione e fornendo un typedef fittizio per il contesto, ovviamente), il compilatore si lamenterebbe di una funzione mancante. Per ogni nuovo tipo che aggiungo, mi viene in mente di aggiungere un gestore quando ricompilino questo modulo. Quindi l'X-macro aiuta a garantire che le strutture parallele rimangano intatte anche quando il progetto cresce.

Modificare:

Questa risposta ha aumentato la mia reputazione del 50%. Quindi ecco un po 'di più. Quello che segue è un esempio negativo , rispondendo alla domanda: quando non usare X-Macros?

Questo esempio mostra l'imballaggio di frammenti di codice arbitrario nel "record" X. Alla fine ho abbandonato questo ramo del progetto e non ho usato questa strategia nei progetti successivi (e non per mancanza di tentativi). È diventato spiacevole, in qualche modo. In effetti la macro è chiamata X6 perché a un certo punto c'erano 6 argomenti, ma mi sono stancato di cambiare il nome della 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 }; 

Un grosso problema erano le stringhe in formato printf. Mentre sembra bello, è solo un fuoco d'artificio. Poiché è utilizzato solo in una funzione, l'uso eccessivo della macro separa effettivamente le informazioni che dovrebbero essere insieme; e rende la funzione illeggibile da sola. L'offuscamento è doppiamente sfortunato in una funzione di debug come questa.

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

Quindi non essere portato via. Come ho fatto io

In Oracle HotSpot Virtual Machine per Java® Programming Language, c’è il file globals.hpp , che utilizza RUNTIME_FLAGS in questo modo.

Guarda il codice sorgente:

  • JDK 7
  • JDK 8
  • JDK 9

Mi piace usare le macro X per creare “enumerazioni complete” che supportano l’iterazione dei valori enum e ottenere la rappresentazione della stringa per ogni valore enum:

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

Questo non solo definisce una MouseButton::Value , ma mi permette anche di fare cose come

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

Io uso una X-macro piuttosto massiccia per caricare il contenuto del file INI in una struttura di configurazione, tra le altre cose che ruotano attorno a quella struttura.

Questo è il mio file “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* , 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. */ 

È un po ‘confuso, lo ammetto. Diventa subito chiaro che in realtà non voglio scrivere tutte quelle dichiarazioni di tipo dopo ogni macro di campo. (Non preoccuparti, c’è un grande commento per spiegare tutto ciò che ho omesso per brevità.)

E questo è il modo in cui dichiaro la struttura di configurazione:

 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; 

Quindi, nel codice, in primo luogo i valori predefiniti vengono letti nella struttura di configurazione:

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

Quindi, l’INI viene letto nella struttura di configurazione come segue, usando la libreria 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 sovrascrive dai flag a riga di comando, che sono anche formattati con gli stessi nomi (in GNU long form), si applicano come segue nel modo più semplice usando la libreria 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 così via, uso anche la stessa macro per stampare l'output di --help -flag e il file di esempio predefinito ini, configuration.def è incluso 8 volte nel mio programma. "Piolo quadrato in un buco rotondo", forse; come potrebbe procedere un programmatore veramente competente? Un sacco di loop e elaborazione delle stringhe?

https://github.com/whunmr/DataEx

usando i seguenti xmacros per generare una class c ++, con serializzazione e deserializzazione di functionlity incorporato.

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

utilizzo:

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

inoltre, un altro esempio è in https://github.com/whunmr/msgrpc