Search code examples
cmacrosc-preprocessorx-macros

Real-world use of X-Macros


I just learned of X-Macros. What real-world uses of X-Macros have you seen? When are they the right tool for the job?


Solution

  • 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 */
    #define 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 {...}command1_resp_t;
    typedef struct {...}command2_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 generate for me my number of commands definition.

    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!

    One other use is to create offset tables: Since memory is often a constraint on embedded systems, I don't want to use 512 bytes for my jump table (2 bytes per pointer X 256 possible commands) when it is a sparse array. Instead I will have a table of 8bit offsets for each possible command. This offset is then used to index into my actual jump table which now only needs to be NUM_COMMANDS * sizeof(pointer). In my case with 10 commands defined. My jump table is 20bytes long and I have an offset table that is 256 bytes long, which is a total of 276bytes instead of 512bytes. I then call my functions like so:

    jump_table[offset_table[command]]();
    

    instead of

    jump_table[command]();
    

    I can create an offset table like so:

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

    where offsetof is a standard library macro defined in "stddef.h"

    As a side benefit, there is a very easy way to determine if a command code is supported or not:

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

    This is also why in my COMMAND_TABLE I reserved command byte 0. I can create one function called "process_reserved()" which will be called if any invalid command byte is used to index into my offset table.