最近在写一个简易的内存管理逻辑用于 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
| #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
| #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
| #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
| static uint8_t pool_DEFAULT_heap[((512ul) + 4) * (12u)];
|
继续,要管理不同类型的池子,需要对应的描述头结构。这里我们描述头定义为:pool_desc_t,具体结构就不详细给出了。每个池子类型都要对应自己的描述头,因此描述头结构应该是一个对应结构数组中的表项。
这里我们第三次使用 X-Macro 展开:
1 2 3 4 5 6 7
| 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 的适用场景其实很明确:同一份数据,需要在多个地方以不同形式出现——枚举、数组、字符串表、初始化代码。一旦用上,原来需要手工同步的重复代码全部可以删掉。
其技巧本身并不难,难在能不能够在具体业务场景中识别并运用。