C黑魔法 X-Macro 宏

最近在写一个简易的内存管理逻辑用于 CCM_RAM 以及其余通用 RAM 管理。过程中涉及到了内存池的配置:如果需要添加一个新类型的内存池,常规做法是在相关 .c/.h 中再定义一个数组以及维护一套描述头结构。但是这涉及到多个文件的修改,如果漏改一个地方那么将导致程序编译不过。而且如果后续逻辑写大之后,维护将及其不便。此时便想起了可以使用 X-Macro 来解决这个问题。

什么是 X-Macro ?

X-Macro 是一种用宏来定义数据列表,然后反复重定义 X 来生成不同代码的预处理器技巧。是一种可靠维护代码或数据的并行列表的技术,你只需要维护一份数据列表,编译器自动帮你生成枚举、数组、函数声明、结构体、初始化代码等所有重复性内容。

X-Macro 并不是C语言的新特性,它本质其实就是普通的宏定义与展开,在这个基础之上才衍生出了 X-Macro,因此 X-Macro 准确来说应该是一个宏的使用技巧。

使用方法

单看概念会觉得十分抽象,这里我们给出一个简单的示例:

1
2
3
4
#define MY_DATA_LIST \
X(ITEM_A, 100, "hello") \
X(ITEM_B, 200, "world") \
X(ITEM_C, 300, "foo")

上面我们定义了一个数据表,现在我们想要利用这个数据表,一次性定义3个变量,并使用表中的数值参数对其进行初始化,该怎么做? 这里可以使用 X-Macro 来完成:

1
2
3
#define X(name, val, str) int name##_value = val;
MY_DATA_LIST
#undef X

上面逻辑中,我们定义了 X(参数) 为我们想做的操作(变量定义):int 参数1##_value = 参数2;

在预处理阶段,MY_DATA_LIST 展开后得到下列程序:

1
2
3
int ITEM_A_value = 100;
int ITEM_B_value = 200;
int ITEM_C_value = 300;

发现没有?我们仅仅通过一次展开 MY_DATA_LIST 宏就得到了所有的变量定义,如果此刻我们想要加入第四个变量该怎么做?很简单,只需要在 MY_DATA_LIST 中多引入一个表项就行。

1
2
3
4
5
6
7
8
9
10
11
#define MY_DATA_LIST \
X(ITEM_A, 100, "hello") \
X(ITEM_B, 200, "world") \
X(ITEM_C, 300, "foo") \
X(ITEM_D, 400, "happy")

// 展开后:
int ITEM_A_value = 100;
int ITEM_B_value = 200;
int ITEM_C_value = 300;
int ITEM_D_value = 400;

我们通过一个简单的变量定义场景就阐述出了 X-Macro 宏的用法。相信你此刻已经见识到了这玩意儿的强大之处:没错,我们只需在 conf.h 中维护一个全局表项,后续的 数组声明/枚举定义 等等均采用 X-Macro 进行展开。如果后期需要对表项做出修改,只需要改 conf.h 文件就行,其余模块内部自适应变化!极大降低了维护工作量,且不易出错。


这里我就不以更多的示例来进行阐述,直接来看一个实际的应用场景:

目前需要设计一个内存池,且该内存池类型不固定(也就是说需要支持不同类型的内存池),且用户侧对于内存池的类型删减必须足够简便。很明显,这就是一个典型的 X-Macro 的用武之地了,一起来看。

我们先以一个默认的类型来入手,默认池采用每块512字节,供划分12块的配置。

1
2
3
4
5
6
/* This is default setting. */
#define DEFAULT_BLOCK_SIZE (512ul)
#define DEFAULT_BLOCK_NUM (12u)

#define MEM_POOL_LIST \
X(DEFAULT, DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_NUM)

X(DEFAULT, DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_NUM)

这就是我们的数据列表了,后续用户所有对于内存池配置的操作都在此进行。

列表定义好之后首先我们需要的就是再定义一个枚举,该枚举是后续我们访问不同类型内存池的关键(也可以理解为池索引),接下来我们使用 X-Macro 进行第一次展开!

1
2
3
4
5
6
7
// 第一次 X-Macro 展开.
#define X(name, size, depth) POOL_##name,
typedef enum {
MEM_POOL_LIST
POOL_MAX
} pool_id_t;
#undef X

展开后如下:

1
2
3
4
typedef enum {
POOL_DEFAULT,
POOL_MAX
} pool_id_t;

Ps:这里有个小细节要注意一下:POOL_MAX 是根据枚举型的递增特性,用来统计池类型数目的,后续用于循环计数。

现在索引有了,我们需要再定义物理数组,也就是这片内存池真正的空间来源。这是我们第二次 X-Macro 展开,注意看。

1
2
3
4
5
6
// 第二次 X-Macro 展开.
#define X(name, size, depth) \
static uint8_t __attribute__((section(POOL_SECTION))) __attribute__((aligned(4))) \
pool_##name##_heap[(size + sizeof(void *)) * (depth)];
MEM_POOL_LIST
#undef X

展开后,得到:

1
2
/* 这里 sizeof(void *)在32位下是4字节.一个小细节 */
static uint8_t pool_DEFAULT_heap[((512ul) + 4) * (12u)];

继续,要管理不同类型的池子,需要对应的描述头结构。这里我们描述头定义为:pool_desc_t,具体结构就不详细给出了。每个池子类型都要对应自己的描述头,因此描述头结构应该是一个对应结构数组中的表项。

这里我们第三次使用 X-Macro 展开:

1
2
3
4
5
6
7
// 第三次 X-Macro 展开.
static pool_desc_t pool_table[POOL_MAX] =
{
#define X(name, size, depth) { (size) + sizeof(void*), depth, NULL },
MEM_POOL_LIST
#undef X
};

展开后得到:

1
2
3
4
static pool_desc_t pool_table[POOL_MAX] = 
{
{ (512ul) + 4, (12u), NULL },
};

至此,三次展开已经全部结束。 我们来总览一下 X-Macro 给我们带来了什么。

1
2
3
4
5
6
7
8
9
10
typedef enum {
POOL_DEFAULT,
POOL_MAX
} pool_id_t;

static uint8_t pool_DEFAULT_heap[((512ul) + 4) * (12u)];
static pool_desc_t pool_table[POOL_MAX] =
{
{ (512ul) + 4, (12u), NULL },
};

看到了吧,一行数据列表给我们从结构声明到数据结构定义全部搞定。这就是 X-Macro 最大的意义:

零运行时开销,所有枚举、数组大小、地址排布在编译期就定了。且用户只需改一行列表,所有相关代码自动同步,不需要挨个模块去加实现。

假如我们目前需要加入 TEST 类型池子, 大小1024,深度15,怎么做呢?很简单,多加一个列表的事。

1
2
3
#define MEM_POOL_LIST   \
X(DEFAULT, DEFAULT_BLOCK_SIZE, DEFAULT_BLOCK_NUM) \
X(TEST, (1024ul), (15u))

X-Macro 将自动生成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef enum {
POOL_DEFAULT,
POOL_TEST,
POOL_MAX
} pool_id_t;

static uint8_t pool_DEFAULT_heap[((512ul) + 4) * (12u)];
static uint8_t pool_TEST_heap[((1024ul) + 4) * (15u)];

static pool_desc_t pool_table[POOL_MAX] =
{
{ (512ul) + 4, (12u), NULL },
{ (1024ul) + 4, (15u), NULL },
};

无需多言了,这就是 X-Macro 的魅力所在。


一些使用场景

我最早接触到 X-Macro 是在 LwIP 的内存管理算法源码中了解到的。LwIP 在内存池逻辑中就使用了 X-Macro 进行不同池类型的管理,其原理与我上述所说的本质是一样的:通过不断展开 X-Macro 来声明与定义数据结构,后续内存池类型的增删(比如 UDP/TCP 等等类型控制块其内存来源都是内存池)直接通过数据列表进行配置,无需改动内部逻辑。

这里我给一些 LwIP 源码中的具体实现,作为参考学习:

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
LWIP_MEMPOOL(RAW_PCB,        1,   100, "RAW_PCB")
LWIP_MEMPOOL(TCP_PCB, 5, 200, "TCP_PCB")
LWIP_MEMPOOL(TCP_SEG, 10, 50, "TCP_SEG")
// ... 后面还有很多

#define LWIP_MEMPOOL(name,num,size,desc) MEMP_##name,

// 展开后.
typedef enum {
MEMP_RAW_PCB,
MEMP_TCP_PCB,
MEMP_TCP_SEG,
// ... 继续展开所有池
MEMP_MAX
} memp_t;

/* **************************************************** */
const struct memp_desc *const memp_pools[MEMP_MAX] = {
#define LWIP_MEMPOOL(name,num,size,desc) &memp_ ## name,
#include "lwip/priv/memp_std.h"
};

// 展开后.
const struct memp_desc *const memp_pools[] = {
&memp_RAW_PCB,
&memp_TCP_PCB,
&memp_TCP_SEG,
// ...
};

X-Macro 的适用场景其实很明确:同一份数据,需要在多个地方以不同形式出现——枚举、数组、字符串表、初始化代码。一旦用上,原来需要手工同步的重复代码全部可以删掉。

其技巧本身并不难,难在能不能够在具体业务场景中识别并运用。

作者

R6bandito

发布于

2026-09-11

更新于

2026-10-08

许可协议

评论

音乐
0:000:00