Uso en el mundo real de X-Macros

Resuelto Agnius Vasiliauskas asked hace 13 años • 8 respuestas

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

Agnius Vasiliauskas avatar Jul 09 '11 22:07 Agnius Vasiliauskas
Aceptado

Descubrí las X-macros hace un par de años cuando comencé a utilizar punteros de función en mi código. Soy un programador integrado y uso máquinas de estados con frecuencia. A menudo escribía 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 muy propenso a errores tener que mantener el orden de mi tabla de punteros de función de modo que coincidiera con el orden de mi enumeración de estados.

Un amigo mío me presentó las X-macros y fue como si se me encendiera una bombilla en la 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 beneficio adicional, también puedo hacer que el preprocesador 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

Sin embargo, mi uso favorito es cuando se trata de controladores de comunicación.

Primero creo una tabla de comunicaciones que contiene el nombre y el código de cada 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úsculas y minúsculas en la tabla, porque las mayúsculas se usarán para enumeraciones y las minúsculas para nombres de funciones.

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

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

etc.

Asimismo, defino estructuras para cada respuesta de comando:

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

etc.

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

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

Puedo definir la enumeración de la longitud de mi comando:

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

Puedo definir la enumeración de la longitud de mi 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 creo una instancia de offset_struct_t, solo lo uso como una forma para que el compilador genere mi número de definición 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 posible, puedo hacer que el compilador calcule qué tan grande debe ser mi búfer 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

Nuevamente, esta unión es como mi estructura de compensación, no se crea una instancia de ella; en su lugar, puedo usar el operador sizeof para declarar el tamaño de mi búfer de transmisión.

uint8_t tx_buf[sizeof(tx_buf_t)];

Ahora mi búfer de transmisión tx_buf tiene el tamaño óptimo y, a medida que agrego comandos a este controlador de comunicaciones, mi búfer siempre tendrá el tamaño óptimo. ¡Fresco!

Otro uso es crear tablas de compensación: dado que la memoria suele ser una limitació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 su lugar, tendré una tabla de compensaciones de 8 bits para cada comando posible. Este desplazamiento luego 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 salto tiene una longitud de 20 bytes y tengo una tabla de compensación de 256 bytes, que es un total de 276 bytes en lugar de 512 bytes. Luego llamo a mis funciones así:

jump_table[offset_table[command]]();

en lugar de

jump_table[command]();

Puedo crear una tabla de compensación 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 de biblioteca estándar definida en "stddef.h"

Como beneficio adicional, existe una manera muy sencilla 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 que en mi COMMAND_TABLE reservé el byte de comando 0. Puedo crear una función llamada "process_reserved()" que se llamará si se usa algún byte de comando no válido para indexar en mi tabla de compensación.

ACRL avatar Feb 21 '2012 20:02 ACRL

Las X-Macros son esencialmente plantillas parametrizadas. Por lo tanto, son la herramienta adecuada para el trabajo si necesita varias cosas similares en varias formas. Le permiten crear una forma abstracta y crear una instancia de ella según diferentes reglas.

Utilizo X-macros para generar valores de enumeración como cadenas. Y desde que lo encontré, prefiero esta forma que requiere una macro de "usuario" para aplicar a cada elemento. Es mucho más doloroso trabajar con la inclusión de múltiples archivos.

/* 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 el envío de funciones según el tipo de objeto. Nuevamente secuestrando la misma macro que usé para crear los valores de enumeración.

#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 los índices de mi matriz coincidirán con los valores de enumeración asociados, porque construyen sus diversas formas utilizando los tokens básicos de la definición de la macro (la macro 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);
}

El uso de X-macros de esta manera ayuda al compilador a mostrar mensajes de error útiles. Omití la función evalarray de lo anterior porque distraería mi punto. Pero si intenta compilar el código anterior (comentando las otras llamadas a funciones y proporcionando una definición de tipo ficticia para el contexto, por supuesto), el compilador se quejará de que falta una función. Para cada nuevo tipo que agrego, se me recuerda que debo agregar un controlador cuando vuelvo a compilar este módulo. Por lo tanto, la X-macro ayuda a garantizar que las estructuras paralelas permanezcan intactas incluso a medida que crece el proyecto.

Editar:

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

Este ejemplo muestra el empaquetado de fragmentos de código arbitrarios en el "registro" X. Finalmente abandoné esta rama del proyecto y no utilicé esta estrategia en diseños posteriores (y no por falta de intentarlo). De alguna manera se volvió incómodo. Efectivamente 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 */
// 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
};

Un gran problema fueron las cadenas en formato printf. Si bien se ve genial, es solo un truco. Dado que solo se usa en una función, el uso excesivo de la macro en realidad separó 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 ésta.

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

luser droog avatar Jul 09 '2011 18:07 luser droog

Algunos usos reales de X-Macros en proyectos grandes y populares:

Punto de acceso Java

En la máquina virtual Oracle HotSpot para el lenguaje de programación Java®, existe el archivo globals.hpp, que utiliza RUNTIME_FLAGSde esa manera.

Ver el código fuente:

  • JDK 7
  • JDK 8
  • JDK 9

Cromo

La lista de errores de red en net_error_list.h es una lista muy, muy larga de expansiones de macros de esta forma:

NET_ERROR(IO_PENDING, -1)

Lo utiliza net_errors.h desde el mismo directorio:

enum Error {
  OK = 0,

#define NET_ERROR(label, value) ERR_ ## label = value,
#include "net/base/net_error_list.h"
#undef NET_ERROR
};

El resultado de esta magia del preprocesador es:

enum Error {
  OK = 0,
  ERR_IO_PENDING = -1,
};

Lo que no me gusta de este uso en particular es que el nombre de la constante se crea dinámicamente agregando el archivo ERR_. En este ejemplo, NET_ERROR(IO_PENDING, -100)define la constante ERR_IO_PENDING.

Usando una simple búsqueda de texto ERR_IO_PENDING, no es posible ver dónde se definió esta constante. En cambio, para encontrar la definición, hay que buscar IO_PENDING. Esto hace que el código sea difícil de navegar y, por lo tanto, aumenta la ofuscación de toda la base del código.

Roland Illig avatar Jul 09 '2011 16:07 Roland Illig

Me gusta usar macros X para crear 'enumeraciones ricas' que admitan la iteración de los valores de enumeración, así como la obtención de la representación de cadena para cada valor de enumeración:

#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 sólo define una MouseButton::Valueenumeración, 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";
}
Frerich Raabe avatar Feb 20 '2015 09:02 Frerich Raabe