Utilisation du monde réel de X-Macros

Je viens d’apprendre de X-Macros . Quelles utilisations réelles des X-Macros avez-vous vues? Quand sont-ils le bon outil pour le travail?

J’ai découvert les macros X il y a quelques années lorsque j’ai commencé à utiliser des pointeurs de fonctions dans mon code. Je suis un programmeur embarqué et j’utilise fréquemment des machines d’état. Souvent, j’écrirais un code comme celui-ci:

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

Le problème était que je le considérais comme très enclin à l’erreur de devoir maintenir l’ordre de ma table de pointeurs de fonction de manière à ce qu’il corresponde à l’ordre de mon énumération d’états.

Un de mes amis m’a présenté à X-Macros et c’était comme si une ampoule s’était allumée dans ma tête. Sérieusement, où as-tu passé toute ma vie x-macros!

Alors maintenant, je définis le tableau suivant:

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

Et je peux l’utiliser comme suit:

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

et

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

en prime, je peux aussi faire en sorte que le pré-processeur construise mes prototypes de fonction comme suit:

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

Une autre utilisation consiste à déclarer et à initialiser des registres

 #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 comstackr 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 

Mon utilisation préférée est cependant quand il s’agit de gestionnaires de communication

D’abord, je crée une table de communications contenant chaque nom et code de commande:

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

J’ai les noms des majuscules et des minuscules dans la table, car les majuscules seront utilisées pour les énumérations et les minuscules pour les noms de fonctions.

Ensuite, je définis également des structures pour chaque commande afin de définir à quoi ressemble chaque commande:

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

De même, je définis des structures pour chaque réponse de commande:

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

Ensuite, je peux définir l’énumération de mon code de commande:

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

Je peux définir mon énumération de longueur de commande:

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

Je peux définir mon énumération de longueur de réponse:

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

Je peux déterminer combien de commandes il y a comme suit:

 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) 

NOTE: Je n’instancie jamais vraiment le offset_struct_t, je l’utilise simplement comme un moyen pour le compilateur de générer pour moi la définition de mon numéro de commandes.

Notez alors que je peux générer ma table de pointeurs de fonctions comme suit:

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

Et mes prototypes de fonction:

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

Maintenant, enfin, pour l’utilisation la plus cool, le compilateur peut calculer la taille de mon tampon de transmission.

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

Encore une fois, cette union est comme ma structure offset, elle n’est pas instanciée, mais je peux utiliser l’opérateur sizeof pour déclarer ma taille de tampon de transmission.

 uint8_t tx_buf[sizeof(tx_buf_t)]; 

Maintenant, mon tampon de transmission tx_buf est la taille optimale et au fur et à mesure que j’ajoute des commandes à ce gestionnaire de communications, ma taille sera toujours optimale. Cool!

Une autre utilisation est de créer des tables de décalage: Comme la mémoire est souvent une contrainte sur les systèmes embarqués, je ne veux pas utiliser 512 octets pour ma table de sauts (2 octets par pointeur X 256 commandes possibles) lorsqu’il s’agit d’un tableau fragmenté. Au lieu de cela, j’aurai une table de décalages de 8 bits pour chaque commande possible. Ce décalage est ensuite utilisé pour indexer dans ma table de saut réelle qui ne doit plus être que NUM_COMMANDS * sizeof (pointeur). Dans mon cas avec 10 commandes définies. Ma table de saut est longue de 20 octets et j’ai une table de décalage longue de 256 octets, soit un total de 276 octets au lieu de 512 octets. J’appelle alors mes fonctions comme ceci:

 jump_table[offset_table[command]](); 

au lieu de

 jump_table[command](); 

Je peux créer une table offset comme ceci:

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

où offsetof est une macro de bibliothèque standard définie dans “stddef.h”

Comme avantage secondaire, il existe un moyen très simple de déterminer si un code de commande est pris en charge ou non:

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

C’est aussi pourquoi dans mon COMMAND_TABLE j’ai réservé l’octet de commande 0. Je peux créer une fonction appelée “process_reserved ()” qui sera appelée si un octet de commande non valide est utilisé pour indexer dans ma table de décalage.

Les macros X sont essentiellement des modèles paramétrés. Donc, ils sont le bon outil pour le travail si vous avez besoin de plusieurs choses similaires sous plusieurs formes. Ils vous permettent de créer une forme abstraite et de l’instancier selon différentes règles.

J’utilise des macros X pour générer des valeurs enum sous forme de chaînes. Et depuis que je le rencontre, je préfère fortement cette forme qui prend une macro “utilisateur” à appliquer à chaque élément. L’inclusion de plusieurs fichiers est beaucoup plus difficile à utiliser.

 /* x-macro constructors for error and type enums and ssortingng 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]); */ 

Je les utilise également pour la répartition des fonctions en fonction du type d’object. Encore une fois en piratant la même macro que j’ai utilisé pour créer les valeurs enum.

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

L’utilisation de la macro garantit que tous mes index de tableau correspondent aux valeurs d’énumération associées, car ils construisent leurs différents formulaires en utilisant les jetons nus de la définition 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 *evalssortingng = evalpush; evalfunc *evalname = evalpush; evalfunc *evaltype[ssortingngtype/*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); } 

Utiliser les macros X de cette façon aide en fait le compilateur à donner des messages d'erreur utiles. J'ai omis la fonction evalarray de ce qui précède car cela détournerait mon attention. Mais si vous tentez de comstackr le code ci-dessus (en commentant les autres appels de fonction et en fournissant un typedef pour le contexte, bien sûr), le compilateur se plaindra d'une fonction manquante. Pour chaque nouveau type que j'ajoute, je suis invité à append un gestionnaire lorsque je recomstack ce module. Ainsi, la macro X permet de garantir que les structures parallèles restnt intactes au fur et à mesure de la croissance du projet.

Modifier:

Cette réponse a augmenté ma réputation de 50%. Alors, voici un peu plus. Voici un exemple négatif , répondant à la question: quand ne pas utiliser X-Macros?

Cet exemple montre la mise en paquet de fragments de code arbitraires dans le X-"enregistrement". J'ai finalement abandonné cette twig du projet et n'ai pas utilisé cette stratégie dans les conceptions ultérieures (et non pas pour essayer). Cela devenait fou en quelque sorte. En effet, la macro s'appelle X6 car à un moment donné, il y avait 6 arguments, mais j'en ai eu assez de changer le nom de la macro.

 /* Object types */ /* "'X'" macros for Object type definitions, declarations and initializers */ // abcd // enum, ssortingng, 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( ssortingngtype, "ssortingng", 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 *typessortingng[] = { 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 gros problème était les chaînes de format printf. Alors que ça a l'air cool, c'est juste du hocus pocus. Comme il n'est utilisé que dans une fonction, la surutilisation de la macro sépare les informations qui doivent être réunies; et cela rend la fonction illisible par elle-même. L'obscurcissement est doublement regrettable dans une fonction de débogage comme celle-ci.

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

Alors ne vous laissez pas emporter. Comme j'ai fait.

Dans la machine virtuelle Oracle HotSpot pour le langage de programmation Java®, il y a le fichier globals.hpp , qui utilise RUNTIME_FLAGS de cette manière.

Voir le code source:

  • JDK 7
  • JDK 8
  • JDK 9

J’aime utiliser les macros X pour créer des «énumérations riches» qui prennent en charge l’itération des valeurs d’énumération ainsi que l’obtention de la représentation sous forme de chaîne pour chaque valeur d’énumération:

 #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 *valueAsSsortingng( 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; } }; 

Cela ne définit pas seulement une énumération MouseButton::Value , cela me permet aussi de faire des choses comme

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

J’utilise une macro X assez massive pour charger le contenu du fichier INI dans une structure de configuration, entre autres choses qui tournent autour de cette structure.

Voici à quoi ressemble mon fichier “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("ssortingng")) #define NMB_PATH_DEFS NMB_STR_DEFS__(TEXT("path")) #define NMB_STR_DEFS__(ATYPE) \ ATYPE , basic_ssortingng* , new basic_ssortingng\ , 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. */ 

C’est un peu déroutant, je l’avoue. Il devient vite évident que je ne veux pas réellement écrire toutes ces déclarations de type après chaque macro de champ. (Ne vous inquiétez pas, il y a un gros commentaire pour expliquer tout ce que j’ai omis pour être bref).

Et voici comment je déclare la structure de configuration:

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

Ensuite, dans le code, les valeurs par défaut sont d’abord lues dans la structure de configuration:

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

Ensuite, l’INI est lu dans la structure de configuration comme suit, en utilisant la bibliothèque 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 

Et les substitutions à partir des drapeaux de ligne de commande, qui sont également formatés avec les mêmes noms (sous forme longue GNU), sont appliquées de la manière suivante avec la bibliothèque 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< 

Et ainsi de suite, j'utilise également la même macro pour imprimer la sortie --help -flag et l'exemple de fichier ini par défaut, configuration.def est inclus 8 fois dans mon programme. "Carré dans un trou rond", peut-être; Comment un programmeur réellement compétent pourrait-il procéder avec cela? Beaucoup et beaucoup de boucles et de traitement de cordes?

https://github.com/whunmr/DataEx

utiliser xmacros suivant pour générer une classe c ++, avec sérialisation et désérialisation de la fonctionnalité intégrée.

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

usage:

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

aussi, un autre exemple est dans https://github.com/whunmr/msgrpc