Uso en el mundo real de X-Macros

Acabo de enterarme de X-Macros . ¿Qué usos reales de X-Macros has visto? ¿Cuándo son la herramienta adecuada para el trabajo?

Descubrí X-macros hace un par de años cuando comencé a hacer uso de punteros de función en mi código. Soy un progtwigdor incorporado y uso máquinas de estado con frecuencia. A menudo escribiría un código como este:

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

El problema era que consideraba que era muy propenso a errores tener que mantener el orden de la tabla de mi puntero de función de manera que coincidiera con el orden de mi enumeración de estados.

Un amigo mío me presentó a X-macros y fue como una bombilla encendida en mi cabeza. En serio, ¿dónde has estado toda mi vida x-macros!

Entonces ahora defino la siguiente tabla:

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

Y puedo usarlo de la siguiente manera:

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

y

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

como extra, también puedo hacer que el pre-procesador construya mis prototipos de funciones de la siguiente manera:

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

Otro uso es 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 

Mi uso favorito sin embargo es cuando se trata de controladores de comunicación

Primero creo una tabla de comunicaciones, que contiene cada nombre y código de comando:

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

Tengo los nombres en mayúscula y minúscula en la tabla, porque la mayúscula se utilizará para las enumeraciones y la minúscula para los nombres de las funciones.

Luego también defino structs para cada comando para definir cómo se ve cada comando:

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

Asimismo, defino las estructuras para cada respuesta de comando:

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

Entonces puedo definir mi enumeración del código de comando:

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

Puedo definir mi enumeración de longitud de comando:

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

Puedo definir mi enumeración de longitud de respuesta:

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

Puedo determinar cuántos comandos hay de la siguiente manera:

 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: en realidad nunca instancia el offset_struct_t, solo lo uso como una forma para que el comstackdor genere para mí la definición del número de comandos.

Tenga en cuenta que puedo generar mi tabla de punteros de función de la siguiente manera:

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

Y mis prototipos de funciones:

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

Ahora, por último, para el mejor uso de la historia, puedo hacer que el comstackdor calcule qué tan grande debe ser mi buffer de transmisión.

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

De nuevo, esta unión es como mi estructura offset, no está instanciada, en su lugar puedo usar el operador sizeof para declarar mi tamaño de buffer de transmisión.

 uint8_t tx_buf[sizeof(tx_buf_t)]; 

Ahora mi buffer de transmisión tx_buf es el tamaño óptimo y cuando agregue comandos a este controlador de comunicaciones, mi buffer siempre tendrá el tamaño óptimo. ¡Guay!

Otro uso es crear tablas de compensación: dado que la memoria es a menudo una restricción en los sistemas integrados, no quiero usar 512 bytes para mi tabla de salto (2 bytes por puntero X 256 comandos posibles) cuando se trata de una matriz dispersa. En cambio tendré una tabla de compensaciones de 8 bits para cada comando posible. Este desplazamiento se usa para indexar en mi tabla de salto real que ahora solo necesita ser NUM_COMMANDS * sizeof (puntero). En mi caso con 10 comandos definidos. Mi tabla de saltos tiene 20 bytes de longitud y tengo una tabla de desplazamiento de 256 bytes de longitud, que es un total de 276 bytes en lugar de 512 bytes. Luego llamo a mis funciones de esta manera:

 jump_table[offset_table[command]](); 

en lugar de

 jump_table[command](); 

Puedo crear una tabla de desplazamiento así:

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

donde offsetof es una macro biblioteca estándar definida en “stddef.h”

Como beneficio adicional, hay una manera muy fácil de determinar si un código de comando es compatible o no:

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

Esta es también la razón por la cual en mi comando COMMAND_TABLE I se reservó el byte 0. Puedo crear una función llamada “process_reserved ()” que se invocará si se utiliza un byte de comando no válido para indexar en mi tabla de desplazamiento.

X-Macros son esencialmente plantillas parametrizadas. Entonces son la herramienta correcta para el trabajo si necesita varias cosas similares en varias formas. Le permiten crear un formulario abstracto y crear una instancia de acuerdo con diferentes reglas.

Uso X-macros para dar salida a los valores enum como cadenas. Y desde que lo encuentro, prefiero este formulario que toma una macro de “usuario” para aplicar a cada elemento. La inclusión de archivos múltiples es mucho más dolorosa de trabajar.

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

También los estoy usando para despacho de funciones en función del tipo de objeto. Nuevamente secuestrando la misma macro que utilicé para crear los valores enum.

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

El uso de la macro garantiza que todos mis índices de matriz coincidirán con los valores enum asociados, porque construyen sus diversas formas usando los tokens simples de la definición de 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); } 

Usar X-macros de esta manera en realidad ayuda al comstackdor a dar mensajes de error útiles. Omití la función evalarray de lo anterior porque distraería mi punto. Pero si intenta comstackr el código anterior (comentando las otras llamadas a funciones, y proporcionando un typedef ficticio para el contexto, por supuesto), el comstackdor se quejaría de una función faltante. Para cada nuevo tipo que agrego, me recuerda agregar un controlador cuando recompilo este módulo. Entonces, la X-macro ayuda a garantizar que las estructuras paralelas permanezcan intactas incluso a medida que el proyecto crece.

Editar:

Esta respuesta ha elevado mi reputación al 50%. Así que aquí hay un poco más. El siguiente es un ejemplo negativo , respondiendo a la pregunta: ¿ cuándo no usar X-Macros?

Este ejemplo muestra el empaquetado de fragmentos de código arbitrarios en el "registro X". Finalmente abandoné esta twig del proyecto y no usé esta estrategia en diseños posteriores (y no por falta de bashs). De alguna manera, se convirtió en poco edificante. De hecho, la macro se llama X6 porque en un momento hubo 6 argumentos, pero me cansé de cambiar el nombre de la 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 gran problema fueron las cadenas de formato printf. Si bien parece genial, es solo hocus pocus. Como solo se usa en una función, el uso excesivo de la macro realmente separa la información que debería estar junta; y hace que la función sea ilegible por sí misma. La ofuscación es doblemente desafortunada en una función de depuración como esta.

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

Así que no te dejes llevar. Como yo lo hice.

En Oracle HotSpot Virtual Machine para Java® Programming Language, está el archivo globals.hpp , que usa RUNTIME_FLAGS de esa manera.

Ver el código fuente:

  • JDK 7
  • JDK 8
  • JDK 9

Me gusta usar macros X para crear ‘enumeraciones ricas’ que soportan iterar los valores enum y obtener la representación de cadena para cada valor 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; } }; 

Esto no solo define un MouseButton::Value enum, sino que también me permite hacer cosas 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"; } 

Utilizo un X-macro bastante masivo para cargar contenidos de archivo INI en una estructura de configuración, entre otras cosas girando en torno a esa estructura.

Así es como se ve mi archivo “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. */ 

Es un poco confuso, lo admito. Rápidamente queda claro que no quiero escribir todas esas declaraciones de tipo después de cada campo-macro. (No se preocupe, hay un gran comentario para explicar todo lo que omité por brevedad).

Y así es como declaro la estructura de configuración:

 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; 

Luego, en el código, primero se leen los valores predeterminados en la estructura de configuración:

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

Luego, el INI se lee en la estructura de configuración de la siguiente manera, utilizando la 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 

Y las anulaciones de los indicadores de la línea de comandos, que también tienen el mismo nombre (en forma larga de GNU), se aplican de la siguiente manera en la manera siguiente usando la librería 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<  

Y así sucesivamente, también utilizo la misma macro para imprimir el archivo --help -flag y muestrear el archivo ini predeterminado, configuration.def se incluye 8 veces en mi progtwig. "Clavija cuadrada en un agujero redondo", tal vez; ¿Cómo podría un progtwigdor realmente competente proceder con esto? Montones y montones de bucles y procesamiento de cadenas

https://github.com/whunmr/DataEx

utilizando los siguientes xmacros para generar una clase c ++, con serializar y deserializar functionlity incorporado.

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

también, otro ejemplo está en https://github.com/whunmr/msgrpc