在 C 语言中,有一种较为高级的宏用法,称为 X-Macros,本文就是讲述 X-Macros 的应用。

X-Macros 用在一些需要“表驱动法”的地方,针对编译期间已知的数据集合,生成对应的接口方法,当修改数据时, 只需要考虑修改“表”本身,而已有的接口方法会自动适应。

一般来说 X-Macros 核心就是一个列表宏,形如:

1
2
3
#define LIST_XMACROS_LIST(x) \
    X(a, b, c ,d, e) \
    X(f, g, h)

LIST_XMACROS_LIST 是一个列表宏,一行定义一个元素,其宏参数 X 是一个操作宏,当需要使用列表的时候,传入不同的操作宏,来实现针对列表数据的不同处理。

最常见的,就是枚举类型的字面字符串和值之间的匹配,参见如下代码:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
#define MAIN_STATE_LIST(X) \
    X(ERROR, "something error") \
    X(INIT, "initialize device") \
    X(BUSY, "busy moving") \
    X(READY, "ready for scan") \
    X(SCAN, "scanning")

#define MAIN_STATE(NAME)    MST_##NAME
#define ENUM_MAIN_STATE(NAME, ...) MAIN_STATE(NAME),
enum MainStateEnum {
    MAIN_STATE_LIST(ENUM_MAIN_STATE)
    MAIN_STATE__ENUM_CNT
};

static const struct MainStateBundles {
    int state;
    const char *str;
    const char *desc;
} M_STATE_BUNDLES[] = {
#define BUNDLE_MAIN_STATE(NAME, DESC, ...) \
{ \
    .state = MAIN_STATE(NAME), \
    .str = #NAME, \
    .desc = DESC, \
},
    MAIN_STATE_LIST(BUNDLE_MAIN_STATE)
};
#undef BUNDLE_MAIN_STATE

代码最终基于列表生成一个常量结构体数组,并按照枚举值、枚举名字面量和描述来自动填充,后续函数就可以根据这个常量结构体数组,来实现枚举值、字符串和描述之间的匹配和转换。

本质上,宏是递归地字符串替换,那么,就可以利用 X-Macros 实现更复杂的功能,比如说,参数的序列化和反序列化。可以将用到的参数,定义一个列表宏,将其类型、名称、限制、是否必选等属性填入其中,然后利用该列表来完成序列化/反序列化的接口函数。

一个样例代码如下,这其中,还将参数按照 SECTION 来区分分组,目前的限制就是不支持超过2级的层次参数:

  1
  2
  3
  4
  5
  6
  7
  8
  9
 10
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
// 编译 gcc test.c -o test $(pkg-config --cflags --libs libcjson)
//
#include <stdio.h>
#include <stddef.h>
#include <stdbool.h>
#include <string.h>
#include <cjson/cJSON.h>

#define CFG_STR_MAXCHARS    32

#define LTS_CFG_CONTROL_LIST(X) \
    X(STR, serial, CFG_STR_MAXCHARS, "/dev/ttyS4", "control serial device", true) \
    X(INT, baudrate, 460800, "control serial baudrate", false) \
    X(FLOAT, scan_speed, 5.0, "scan speed deg/s", false)

#define LIDAR_ROT_DEFAULT ((double[]){ 0, 0, -1, -1, 0, 0, 0, 1, 0 })

#define LTS_CFG_LIDAR_LIST(X) \
    X(STR, ip, CFG_STR_MAXCHARS, "192.168.0.240", "lidar ip addr", false) \
    X(INT, port, 2111, "lidar port", false) \
    X(FLOAT_N, p_rot, 9, LIDAR_ROT_DEFAULT, "rot matrix", true)

#define LTS_CFG_CAMERA_LIST(X) \
    X(STR, device, CFG_STR_MAXCHARS, "/dev/video0", "camera device", false) \
    X(INT, width, 1920, "camera frame width", false) \
    X(INT, height, 1080, "camera frame height", false)

#define LTS_CFG_SECTIONS_LIST(SECTION, X) \
    SECTION(control,  LtsConfigControl,   LTS_CFG_CONTROL_LIST(X)) \
    SECTION(lidar,    LtsConfigLidar,     LTS_CFG_LIDAR_LIST(X)) \
    SECTION(camera,   LtsConfigCamera,    LTS_CFG_CAMERA_LIST(X))

#define X_INT__STDEF(N, DEF, DESC, REQD)               int     N;
#define X_FLOAT__STDEF(N, DEF, DESC, REQD)             double  N;
#define X_FLOAT_N__STDEF(N, L, DEF, DESC, REQD)        double  N[L];
#define X_STR__STDEF(N, L, DEF, DESC, REQD)            char    N[L + 1];

#define X_STDEF(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__STDEF(N, __VA_ARGS__)

#define X_SECTION__STRUCT(name, stname, list) \
    struct stname { \
        list \
    } name;

struct LtsConfig {
    LTS_CFG_SECTIONS_LIST(X_SECTION__STRUCT, X_STDEF)
};

#define X_INT__INIT(N, DEF, DESC, REQD)                 cfg->N = DEF;
#define X_FLOAT__INIT(N, DEF, DESC, REQD)               cfg->N = DEF;
#define X_FLOAT_N__INIT(N, L, DEF, DESC, REQD)          memcpy(cfg->N, DEF, sizeof(cfg->N));
#define X_STR__INIT(N, L, DEF, DESC, REQD)              snprintf(cfg->N, sizeof(cfg->N), "%s", DEF);

#define X_INIT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__INIT(N, __VA_ARGS__)

#define X_SECTION__INIT(name, stname, list) \
    { \
        struct stname *cfg = &config->name; \
        list \
    }

static inline void lts_config_init(struct LtsConfig *config)
{
    LTS_CFG_SECTIONS_LIST(X_SECTION__INIT, X_INIT)
}

#define X_INT__PRINT(N, DEF, DESC, REQD)      fprintf(fp, "  %-10s = %d\n", #N, sec->N);
#define X_FLOAT__PRINT(N, DEF, DESC, REQD)    fprintf(fp, "  %-10s = %f\n", #N, sec->N);
#define X_FLOAT_N__PRINT(N, L, DEF, DESC, REQD) \
    { \
        fprintf(fp, "  %-10s = [", #N); \
        for (size_t i = 0; i < (L); i++) { \
            fprintf(fp, "%f%s", sec->N[i], (i + 1 < (L)) ? ", " : ""); \
        } \
        fprintf(fp, "]\n"); \
    }
#define X_STR__PRINT(N, L, DEF, DESC, REQD)   fprintf(fp, "  %-10s = \"%s\"\n", #N, sec->N);
#define X_PRINT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__PRINT(N, __VA_ARGS__)

#define X_SECTION__PRINT(name, stname, list) \
    { \
        const struct stname *sec = &config->name; \
        fprintf(fp, "[%s]\n", #name); \
        list \
        fprintf(fp, "\n"); \
    }

static inline void lts_config_fprint(FILE *fp, const struct LtsConfig *config)
{
    LTS_CFG_SECTIONS_LIST(X_SECTION__PRINT, X_PRINT)
}


#define X_INT__JSON(N, DEF, DESC, REQD) \
    cJSON_AddNumberToObject(sec_json, #N, sec->N);
#define X_FLOAT__JSON(N, DEF, DESC, REQD) \
    cJSON_AddNumberToObject(sec_json, #N, sec->N);
#define X_FLOAT_N__JSON(N, L, DEF, DESC, REQD) \
    { \
        cJSON *arr = cJSON_CreateArray(); \
        for (size_t i = 0; i < (L); i++) { \
            cJSON_AddItemToArray(arr, cJSON_CreateNumber(sec->N[i])); \
        } \
        cJSON_AddItemToObject(sec_json, #N, arr); \
    }
#define X_STR__JSON(N, L, DEF, DESC, REQD) \
    cJSON_AddStringToObject(sec_json, #N, sec->N);

#define X_JSON(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__JSON(N, __VA_ARGS__)
#define X_SECTION__JSON(name, stname, list) \
    { \
        const struct stname *sec = &config->name; \
        cJSON *sec_json = cJSON_CreateObject(); \
        list \
        cJSON_AddItemToObject(root, #name, sec_json); \
    }

// 主函数
static inline char *lts_config_dump_json(const struct LtsConfig *config)
{
    cJSON *root = cJSON_CreateObject();
    if (!root) return NULL;

    LTS_CFG_SECTIONS_LIST(X_SECTION__JSON, X_JSON)

    char *json_str = cJSON_Print(root);
    cJSON_Delete(root);
    return json_str; // 调用者需要 free(json_str)
}

int main(int argc, char *argv)
{
    struct LtsConfig config;
    lts_config_init(&config);
    lts_config_fprint(stdout, &config);
    char *json = lts_config_dump_json(&config);
    printf("JSON len=%zu:\n %s\n", strlen(json), json);
    return 0;
}

这里需要注意的是,不同类型的参数,其参数数量是不同的,然后根据类型来实现不同的操作宏,最终利用占位宏来实现不同类型的不同处理宏方法:

1
2
// 基于二次展开的展位宏,利用 ## 拼接真正的操作宏
#define X_INIT(TYPE_MACRO, N, ...) X_##TYPE_MACRO##__INIT(N, __VA_ARGS__)

如果想在这个参数中,实现无 SECTION 全局参数,可选的办法是,除了 SECTION 外,还提供一个 MAIN 宏,来针对处理全局参数, 这里不再赘述。

X-Macros 虽然不能让你像用库一样写很少的接口代码,但是允许只用写一次代码的代价来实现后续修改的自适应。其难点在于理解起来稍微有点绕,还有就是,编译时候出错的话,错误信息会较难理解。但是对于 C 语言而言,静态地类似方法,除了宏外,别无选择。