Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

1. Overview

2. References

3. Classic X-Macros Code Analysis

3.1 Classic Code

3.2 Code Structure Analysis (Four Steps to Build an Automated System)

3.2.1 Central Data Table (Single Data Source)

3.2.2 Dynamically Generate Enumerations

3.2.3 Automatically Generate String Tables

3.2.4 Function Pointer Table Automatic Mapping (Core Execution Logic)

3.3 Runtime Working Principle

4. Analysis of Advantages and Disadvantages

5. Four Essential Characteristics of X-Macros

5.1 Centralized Data Table

5.2 Redefinable Macro Functions

5.3 Multiple Expansion Mechanism

5.4 Generate Associated Structures

6. Why Can’t VSCode Expand X-Macro?

6.1 Solutions

6.1.1 Use Compiler Preprocessing (Recommended)

6.1.2 VSCode Plugin Assistance

6.2 In-Depth Discussion on the Essential Differences in Macro Processing and IDE Support

6.2.1 All Macros are Preprocessed, but IDE Support is Layered

6.2.2 Key Difference: “Stage Visibility” of Macro Expansion

6.2.3 Why Ordinary Macros are Visible While X-Macros are Not?

7. More Complex Macros than X-Macros

8. Source Code of Siemens Profidrive Using X-Macros Technology

9. Conclusion

1. Overview

  • X-Macros is an ancient code generation technique in C language, not a data structure. In simple terms, it is a coding trick, a complex macro.
  • Introduce the workflow and main features of X-Macros
  • Introduce the runtime working principle of using X-Macros code through classic examples
  • Share actual project code: source code of Siemens Profidrive using X-Macros technology

2. References

  • https://zhuanlan.zhihu.com/p/521073931
  • https://www.drdobbs.com/cpp/the-new-c-x-macros/184401387
  • https://tetzank.github.io/posts/x-macros/
  • https://github.com/garcia/xdata
  • https://github.com/swansontec/map-macro
  • Renesas RZN2L Product – Chapter 22 Analysis of Siemens Profidrive Source Code Using X-Macros Technology

3. Classic X-Macros Code Analysis

3.1 Classic Code

#include <stdio.h>

#define MACROS_TABLE                    \
    X_MACROS(CMD_LED_ON,  led_on)       \
    X_MACROS(CMD_LED_OFF, led_off)      \

/* Define command list */
typedef enum
{
    #define X_MACROS(a, b) a,
    MACROS_TABLE
    #undef X_MACROS
    CMD_MAX
} cmd_e;

/* Define string list for Log printing */
const char* cmd_str[] = 
{
    #define X_MACROS(a, b) #a,
    MACROS_TABLE
    #undef X_MACROS
};


typedef void (*func)(void* p);

static void led_on(void* p)
{
    printf("%s \r\n", (char *)p);
}

static void led_off(void* p)
{
    printf("%s \r\n", (char *)p);
}

/* Define function list */
const func func_table[] = 
{
    #define X_MACROS(a, b) b,
    MACROS_TABLE
    #undef X_MACROS
};

/* Call function directly by index */
static void cmd_handle(cmd_e cmd)
{
    if(cmd < CMD_MAX)
    {
        func_table[cmd]((void*)cmd_str[cmd]);
    }
}

void main(void)
{
    cmd_handle(CMD_LED_ON);
    cmd_handle(CMD_LED_OFF);
}

3.2 Code Structure Analysis (Four Steps to Build an Automated System)

Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

3.2.1 Central Data Table (Single Data Source)

#define MACROS_TABLE \
    X_MACROS(CMD_LED_ON,  led_on) \
    X_MACROS(CMD_LED_OFF, led_off)

This is the only data source of the system, defining:

  • Command enumeration name CMD_LED_ON
  • Corresponding function name led_on
  • Automatically maintain order consistency

3.2.2 Dynamically Generate Enumerations

typedef enum {
    #define X_MACROS(a, b) a,  // Expands to CMD_LED_ON, CMD_LED_OFF
    MACROS_TABLE
    #undef X_MACROS
    CMD_MAX  // Automatically calculates the total number of commands
} cmd_e;

After expansion, it is equivalent to:

typedef enum {
    CMD_LED_ON, 
    CMD_LED_OFF,
    CMD_MAX
} cmd_e;

3.2.3 Automatically Generate String Tables

const char* cmd_str[] = {
    #define X_MACROS(a, b) #a,  // Stringify: CMD_LED_ON -> "CMD_LED_ON"
    MACROS_TABLE
    #undef X_MACROS
};

Expansion effect:

const char* cmd_str[] = {
    "CMD_LED_ON", 
    "CMD_LED_OFF"
};

3.2.4 Function Pointer Table Automatic Mapping (Core Execution Logic)

const func func_table[] = {
    #define X_MACROS(a, b) b,  // Extract function names: led_on, led_off
    MACROS_TABLE
    #undef X_MACROS
};

Equivalent to:

const func func_table[] = {
    led_on, 
    led_off
};

3.3 Runtime Working Principle

Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

4. Analysis of Advantages and Disadvantages

  • Zero synchronization costAll associated data (enumerations/strings/functions) are automatically synchronized, avoiding manual maintenance inconsistencies
  • Extremely simple to extendAdding a new command only requires adding a line in the macro table:
    X_MACROS(CMD_DOOR_OPEN, door_open)
    
  • Safety guaranteeCMD_MAX automatically calculates array boundaries, preventing out-of-bounds access
  • Self-documentingString tables are automatically generated, log output is completely consistent with the code
  • Reduced code readability
  • Most reading software or IDEs cannot preview/expandRequires the compiler to expand macros during the preprocessing stage

5. Four Essential Characteristics of X-Macros

Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

5.1 Centralized Data Table

A vertically structured table defined with #define, each entry contains associated data

Typical form:

#define COLOR_TABLE \
    ENTRY(RED,    0xFF0000) \
    ENTRY(GREEN,  0x00FF00) \
    ENTRY(BLUE,   0x0000FF)

5.2 Redefinable Macro Functions

Generic processing macros named ENTRY or X

Change their behavior by redefining

/* Stage 1: Generate enumerations */
#define ENTRY(name, value) COLOR_##name,

/* Stage 2: Generate strings */
#define ENTRY(name, value) #name,

5.3 Multiple Expansion Mechanism

The same data table is used multiple times, generating different code each time

// First expansion: generate enumerations
enum Colors {
    COLOR_TABLE  // Expands to COLOR_RED, COLOR_GREEN...
};

// Second expansion: generate string array
const char* color_names[] = {
    COLOR_TABLE  // Expands to "RED", "GREEN"...
};

5.4 Generate Associated Structures

Output logically related but syntactically different code structures

6. Why Can’t VSCode Expand X-Macro?

  • IDE Limitations:VSCode’s C/C++ extension is based on Clang/IntelliSenseThese tools do not simulate the complete preprocessing process (especially for macros with multiple redefinitions)Can only display macro definitions in the current scope, unable to show multi-stage expansions

  • Macro Working Mechanism:

      #define X_MACROS(a, b) a,  // Stage 1: Define enumeration
      MACROS_TABLE             // Expands here
      #undef X_MACROS          // Immediately undefine
    
      #define X_MACROS(a, b) #a, // Stage 2: Define strings
      MACROS_TABLE             // Re-expand
    

    VSCode can only display the result of the last expansion

  • Preprocessing vs. Editing:

    • X-Macro is a feature of the preprocessing stage
    • VSCode is a source code editor and does not perform complete preprocessing

6.1 Solutions

6.1.1 Use Compiler Preprocessing (Recommended)

# GCC/Clang
gcc -E your_file.c -o expanded.c
// After enumeration expansion
typedef enum
{
    CMD_LED_ON,
    CMD_LED_OFF,
    CMD_MAX
} cmd_e;

// After string table expansion
const char* cmd_str[] = 
{
    "CMD_LED_ON",
    "CMD_LED_OFF"
};

// After function table expansion
const func func_table[] = 
{
    led_on,
    led_off
};

6.1.2 VSCode Plugin Assistance

  • C/C++ Advanced Lint: Displays macro expansion warnings
  • Preprocessor Macro Expander: Manually expand selected macros
  • Clangd (alternative to the default C++ extension): Better macro support

6.2 In-Depth Discussion on the Essential Differences in Macro Processing and IDE Support

6.2.1 All Macros are Preprocessed, but IDE Support is Layered

Macro Type Preprocessing Stage IDE Support Level VSCode Hover Effect
Simple Object Macro<span>#define PI 3.14</span> ✅ Fully Supported ★★★★★ Displays <span>3.14</span>
Function-like Macro<span>#define MIN(x,y) ((x)<(y)?(x):(y))</span> ✅ Fully Supported ★★★★☆ Displays expanded expression
Chained Macro(Multi-layer nested macros) ✅ Fully Supported ★★★☆☆ Partially expanded
X-Macro(Depends on redefinitions and multiple expansions) ✅ Fully Supported ★☆☆☆☆ Usually cannot display

6.2.2 Key Difference: “Stage Visibility” of Macro Expansion

  • Preprocessor: Executes complete recursive expansion (depth-first)

  • IDE Syntax Parser: Executes shallow expansion (usually only expands 1-2 layers)

    Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

6.2.3 Why Ordinary Macros are Visible While X-Macros are Not?

  • Ordinary macros can be displayed by simple replacement in the IDE
#define CIRCLE_AREA(r) (PI * (r) * (r))

// Hovering over CIRCLE_AREA(5) displays:
// (3.1415926 * (5) * (5))
  • X-Macro example (difficult for VSCode to parse):IDE cannot simultaneously retain two ENTRY definition states
    ENTRY(red) \
    ENTRY(blue)

// First expansion
#define ENTRY(color) COLOR_##color,
enum Colors {
    TABLE
};
// Expected: COLOR_red, COLOR_blue

// Second expansion (different definition)
#define ENTRY(color) #color,
const char* color_names[] = {
    TABLE
};
// Expected: "red", "blue"
  • Complete workflow of the preprocessorEmbedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing
  • Real-time parsing of the IDEEmbedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

7. More Complex Macros than X-Macros

  • X-Macros is just a “mid-level stage” of macro technologyEmbedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing
  • Alternatives to macros:

    • C++ Templates: Type-safe generics

    • C++ constexpr: Compile-time computation

    • Code Generators: Python/Lua scripts

    • LLVM Plugins: Compile-time metaprogramming

    • Dedicated Preprocessors: Such as Qt’s moc

8. Source Code of Siemens Profidrive Using X-Macros Technology

/* for parameter descriptions see PDRV V4.2 table 144 and others */

/* PNU00001..PNU00899: device specific, insert your parameters here -------------------------------------------------*/

/** PNU00100: gradient of ramp in [%/ms] for the Ramp Function Generator
 * @details Setting parameter for the Ramp Function Generator (PDRV V4.2 Figure 29)
 *          application specific, can be changed by the PDRV user
 */
PDRV_PARAMETER(
    100U,               /**< parameter number */
    (PDRV_PARID_N4),    /**< identifier */
    0U,                 /**< number of elements or length of string */
    PDRV_UNIT_PCT,      /**< variable attribute */
    60000U,             /**< DO IO DATA reference parameter */
    0x801EU,            /**< DO IO DATA normalisation */
    FLOAT_N4_FAC,       /**< standardisation factor */
    0U,                 /**< low limit */
    0x10000000U,        /**< high limit */
    "Ramp Gradient",    /**< pointer at name (16 valid characters) */
    PDRV_NULL_T,        /**< function pointer - function is called if additional text is read */
    uPdrv_RfPnu00100,   /**< function pointer - function is called before value is read */
    uPdrv_WfPnu00100    /**< function pointer - function is called after value is written */
)

/** PNU00110: +- allowed speed tolerance for ZSW1 bit 8 "speed error within tolerance range"
 * @details Setting parameter for calculation of ZSW1 bit 8 (PDRV V4.2 Figure 29)
 *          application specific, can be changed by the PDRV user
 */
PDRV_PARAMETER(
    110U,               /**< parameter number */
    (PDRV_PARID_N4),    /**< identifier */
    0U,                 /**< number of elements or length of string */
    PDRV_UNIT_PCT,      /**< variable attribute */
    60000U,             /**< DO IO DATA reference parameter */
    0x801EU,            /**< DO IO DATA normalisation */
    FLOAT_N4_FAC,       /**< standardisation factor */
    0U,                 /**< low limit */
    0x7FFFFFFFU,        /**< high limit */
    "range speederror", /**< pointer at name (16 valid characters) */
    PDRV_NULL_T,        /**< function pointer - function is called if additional text is read */
    uPdrv_RfPnu00110,   /**< function pointer - function is called before value is read */
    uPdrv_WfPnu00110    /**< function pointer - function is called after value is written */
)



/** pre declaration of parameter object */
typedef struct PDRV_PAR_OBJ PDRV_PAR_OBJ;

/** parameter object inclusive description elements (see PDRV V4.2 table 17)
 * @details no reserved datas, different order from PDRV description, order is arbitrary (consider alignment)
 */
struct PDRV_PAR_OBJ
{
    PDRV_UINT16 uPnu;           /**< parameter number */
    PDRV_UINT16 uIdentifier;    /**< identifier */
    PDRV_UINT16 uNrOfElements;  /**< number of elements or length of string */
    PDRV_UINT16 uVarAttrib;     /**< variable attribute */
    PDRV_UINT16 uRefPar;        /**< DO IO DATA reference parameter */
    PDRV_UINT16 uNormalisation; /**< DO IO DATA normalisation */
    PDRV_FLT32  fStdFactor;     /**< standardisation factor */
    PDRV_UINT32 uLoLimit;       /**< low limit */
    PDRV_UINT32 uHiLimit;       /**< high limit */
    const char* puName;         /**< pointer at name */
    char* (*pfnText)(const PDRV_PAR_OBJ *p_ptParObj, PDRV_UINT16 p_uSubindex); /**< function pointer - function is called if additional text is read */
    PDRV_UINT32 (*pfnRead)(const PDRV_PAR_OBJ *p_ptParObj, PDRV_UINT16 p_uSubindex, PDRV_UINT16 p_uNrOfElements, PDRV_ParValues * p_ptValues); /**< function pointer - function is called before value is read */
    PDRV_UINT32 (*pfnWrite)(const PDRV_PAR_OBJ *p_ptParObj, PDRV_UINT16 p_uSubindex, PDRV_UINT16 p_uNrOfElements, PDRV_ParValues * p_ptValues); /**< function pointer - function is called after value is written */
};



/*------------  extern  functions  ------------*/
/** declaration of all text functions */
#define PDRV_PARAMETER(Pnu, Identifier, NrOfElements, VarAttrib, RefPar, Normalisation, StdFactor, LoLimit, HiLimit, Name, TextFunc, ReadFunc, WriteFunc) \
        extern char * TextFunc (PDRV_PAR_OBJ const *p_ptParObj, PDRV_UINT16 p_uSubindex);

#include "pdrv_parameter_ac4.inc"

#undef PDRV_PARAMETER

/** declaration of all read functions */
#define PDRV_PARAMETER(Pnu, Identifier, NrOfElements, VarAttrib, RefPar, Normalisation, StdFactor, LoLimit, HiLimit, Name, TextFunc, ReadFunc, WriteFunc) \
        extern PDRV_UINT32 ReadFunc (PDRV_PAR_OBJ const *p_ptParObj, PDRV_UINT16 p_uSubindex, PDRV_UINT16 p_uNrOfElements, PDRV_ParValues * p_ptValues);

#include "pdrv_parameter_ac4.inc"

#undef PDRV_PARAMETER

/** declaration of all read functions */
#define PDRV_PARAMETER(Pnu, Identifier, NrOfElements, VarAttrib, RefPar, Normalisation, StdFactor, LoLimit, HiLimit, Name, TextFunc, ReadFunc, WriteFunc) \
        extern PDRV_UINT32 WriteFunc (PDRV_PAR_OBJ const *p_ptParObj, PDRV_UINT16 p_uSubindex, PDRV_UINT16 p_uNrOfElements, PDRV_ParValues * p_ptValues);

#include "pdrv_parameter_ac4.inc"

#undef PDRV_PARAMETER

/*------------  extern  data  ------------*/

/*------------  type definitions, constants, enums  ------------*/

/** complete list of all parameters for implementation of parameters PNU00980 to PNU00989 */
static const PDRV_O2 m_tParList[] =
{
#define PDRV_PARAMETER(Pnu, Identifier, NrOfElements, VarAttrib, RefPar, Normalisation, StdFactor, LoLimit, HiLimit, Name, TextFunc, ReadFunc, WriteFunc) \
                       Pnu,
#include "pdrv_parameter_ac4.inc"
#undef PDRV_PARAMETER
};

#define PDRV_NOOFPARAMETERS (sizeof(m_tParList)/sizeof(m_tParList[0]))  /**< number of all parameters */

/** table with parameter objects */
static const PDRV_PAR_OBJ m_tParObjDatas[] =
{
#define PDRV_NULL_T PDRV_NULL   /**< redefinition to NULL pointer */
#define PDRV_NULL_R PDRV_NULL   /**< redefinition to NULL pointer */
#define PDRV_NULL_W PDRV_NULL   /**< redefinition to NULL pointer */
#define PDRV_PARAMETER(Pnu, Identifier, NrOfElements, VarAttrib, RefPar, Normalisation, StdFactor, LoLimit, HiLimit, Name, TextFunc, ReadFunc, WriteFunc) \
       {.uPnu = Pnu, \
        .uIdentifier = Identifier, \
        .uNrOfElements = NrOfElements, \
        .uVarAttrib = VarAttrib, \
        .uRefPar = RefPar, \
        .uNormalisation = Normalisation, \
        .fStdFactor = StdFactor, \
        .uLoLimit = LoLimit, \
        .uHiLimit = HiLimit, \
        .puName = Name, \
        .pfnText = TextFunc, \
        .pfnRead = ReadFunc, \
        .pfnWrite = WriteFunc},

#include "pdrv_parameter_ac4.inc"

#undef PDRV_PARAMETER
#undef PDRV_NULL_W
#undef PDRV_NULL_R
#undef PDRV_NULL_T
};


/** PROFIdrive search for parameter number and get pointer of found parameter object
 *  @details
 *  @return     pointer with parameter object, PDRV_NULL if not found
*/
const PDRV_PAR_OBJ * ptPdrvPar_GetParObj
    (PDRV_UINT16 p_uPnu     /**< [in] search this parameter number */
    )
{
    PDRV_PAR_OBJ const *ptParObj = PDRV_NULL;
    PDRV_UINT uI;

    for(uI = 0U; uI < PDRV_NOOFPARAMETERS; uI++)
    {
        /* parameter found? */
        if (m_tParList[uI] == p_uPnu)
        {
            ptParObj = &m_tParObjDatas[uI];
            break;
        }
    }

    return ptParObj;
}

9. Conclusion

  • X-Macros is a coding technique/trick suitable for small to medium-sized projects.
  • X-Macros are very common in the Linux kernel and industrial automation fields (such as Siemens SINAMICS, Beckhoff TwinCAT)
  • It is recommended to use the gcc -E command to expand during the preprocessing stage
  • In the subsequent Renesas RZN2L product – Chapter 22, we will analyze the source code of Siemens Profidrive using X-Macros technology

Embedded Science (38) In-Depth Analysis of C Language Preprocessing X-Macros and Practical Project Code Sharing

Leave a Comment