What is a good reference documenting patterns of use of X-Macros in C (or possibly C++)?

前端 未结 3 990
时光取名叫无心
时光取名叫无心 2020-11-30 02:02

A basic definition and example and a few references for \"X-Macros\" is given in this wikipedia entry on the C pre-processor:

An X-Macro is a header

相关标签:
3条回答
  • 2020-11-30 02:39

    Dr. Dobb's has an article on this.

    0 讨论(0)
  • 2020-11-30 02:40

    I use X Macros() in code a lot. The value comes from only adding new data only to the "X list" and not modifying any other code.

    The most common use of X Macros() is for associating error text with error codes. When new error codes are added, programmers must remember to add the code and the text, typically in separate places. The X Macro allows the new error data to be added in a single place and get automatically populated anywhere it is needed.

    Unfortunately, the mechanisms use a lot of pre-compiler magic that can make the code somewhat hard to read (e.g. string joining with token1##token2, string creation with #token). Because of this I typically explain what the X Macro is doing in the comments.

    Here is an example using the error/return values. All new data gets added to the "X_ERROR" list. None of the other code hast to be modified.

    /* 
     * X Macro() data list
     * Format: Enum, Value, Text
     */
    #define X_ERROR \
      X(ERROR_NONE,   1, "Success") \
      X(ERROR_SYNTAX, 5, "Invalid syntax") \
      X(ERROR_RANGE,  8, "Out of range")
    
    /* 
     * Build an array of error return values
     *   e.g. {0,5,8}
     */
    static int ErrorVal[] =
    {
      #define X(Enum,Val,Text)     Val,
       X_ERROR
      #undef X
    };
    
    /* 
     * Build an array of error enum names
     *   e.g. {"ERROR_NONE","ERROR_SYNTAX","ERROR_RANGE"}
     */
    
    static char * ErrorEnum[] = {
      #define X(Enum,Val,Text)     #Enum,
       X_ERROR
      #undef X
    };
    
    /* 
     * Build an array of error strings
     *   e.g. {"Success","Invalid syntax","Out of range"}
     */
    static char * ErrorText[] = {
      #define X(Enum,Val,Text)     Text,
       X_ERROR
      #undef X
    };
    
    /* 
     * Create an enumerated list of error indexes
     *   e.g. 0,1,2
     */
    enum {
      #define X(Enum,Val,Text)     IDX_##Enum,
       X_ERROR
      #undef X
      IDX_MAX   /* Array size */
    };
    
    void showErrorInfo(void)
    {
        int i;
    
        /* 
         * Access the values
         */
        for (i=0; i<IDX_MAX; i++)
            printf(" %s == %d [%s]\n", ErrorEnum[i], ErrorVal[i], ErrorText[i]);
    
    }
    

    You can also use X Macros() to generate code. For example to test if an error value is "known", the X Macro can generate cases in a switch statement:

     /*
      * Test validity of an error value
      *      case ERROR_SUCCESS:
      *      case ERROR_SYNTAX:
      *      case ERROR_RANGE:
      */
    
      switch(value)
      {
    
      #define X(Enum,Val,Text)     case Val:
       X_ERROR
      #undef X
             printf("Error %d is ok\n",value);
             break;
          default:
             printf("Invalid error: %d\n",value);
             break;
      }
    
    0 讨论(0)
  • 2020-11-30 02:52

    I discovered X-macros a couple of years ago when I started making use of function pointers in my code. I am an embedded programmer and I use state machines frequently. Often I would write code like this:

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

    The problem was that I considered it very error prone to have to maintain the ordering of my function pointer table such that it matched the ordering of my enumeration of states.

    A friend of mine introduced me to X-macros and it was like a light-bulb went off in my head. Seriously, where have you been all my life x-macros!

    So now I define the following table:

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

    And I can use it as follows:

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

    and

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

    as a bonus, I can also have the pre-processor build my function prototypes as follows:

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

    Another usage is to declare and initialize registers

    #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 */
    #def ENTRY(a, b, c) a = c;
        REGISTER_TABLE
    #undef ENTRY
    

    My favourite usage however is when it comes to communication handlers

    First I create a comms table, containing each command name and code:

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

    I have both the uppercase and lowercase names in the table, because the upper case will be used for enums and the lowercase for function names.

    Then I also define structs for each command to define what each command looks like:

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

    Likewise I define structs for each command response:

    typedef struct {...}response1_resp_t;
    typedef struct {...}response2_resp_t;
    
    etc.
    

    Then I can define my command code enumeration:

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

    I can define my command length enumeration:

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

    I can define my response length enumeration:

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

    I can determine how many commands there are as follows:

    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: I never actually instantiate the offset_struct_t, I just use it as a way for the compiler to generator for me my number of commands.

    Note then I can generate my table of function pointers as follows:

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

    And my function prototypes:

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

    Now lastly for the coolest use ever, I can have the compiler calculate how big my transmit buffer should be.

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

    Again this union is like my offset struct, it is not instantiated, instead I can use the sizeof operator to declare my transmit buffer size.

    uint8_t tx_buf[sizeof(tx_buf_t)];
    

    Now my transmit buffer tx_buf is the optimal size and as I add commands to this comms handler, my buffer will always be the optimal size. Cool!

    0 讨论(0)
提交回复
热议问题