• 正文
    • X即变化
    • 实际应用
    • 更进一步
  • 相关推荐
申请入驻 产业图谱

【LeafC】C语言之宏魔法3:X

14小时前
164
加入交流群
扫码加入
获取工程师必备礼包
参与热点资讯讨论

X即变化

X-Macro早在C语言被创造之前,就已在带预处理器的汇编器中得到运用。也就是说,今天介绍的宏魔法X并不依赖C语言的任何Hack特性,它只是一类纯粹的代码文本处理技巧。所以,这期的案例会更聚焦于X-Macro应用场景而非C语言的各种技巧。

| 基础X-Macro

X-Macro的核心思想是将一组定义放在一个宏列表中,然后通过多次展开这个宏列表,生成重复性代码(例如函数声明、结构体初始化等),从而减少代码重复和维护成本。

可以说,X-Macro技巧是根据不变的表项生成不同的代码,X正是那变化的部分。

大家可以先看看这个简单的MP3程序示例中实现的X-Macro

#define MP3_TABLE X(M_PLAY, play) 
                  X(M_VOLUME, volume) 
                  X(M_SONG, cur_song_id)
// 1. 定义MP3操作枚举
#define X(a, b) a,
enum mp3_option_e {
    MP3_TABLE
    MUSIC_OP_MAX
};
#undef X
// 2. 定义MP3状态结构体
#define X(a, b) int b;
struct mp3_status_s {
    MP3_TABLE
};
#undef X
// 3. 定义MP3操作索引表和操作接口函数
#define X(a, b) {#b, mp3_set_##a},
struct {
    char *op_name;
    int (*handle)(struct mp3_status_s *status);
} g_mp3_op_table[] = {
    MP3_TABLE
};
int mp3_option_handle(enum mp3_option_e op, struct mp3_status_s *status)
{
    int ret = g_mp3_op_table[op].handle(status);
    if (ret != 0) {
        printf("option[%s] failedn");
        return ret;
    }
    return 0;
}
#undef X
// 4. 定义MP3状态打印函数
#define X(a, b) printf("%s: %d", #b, status->b);
void mp3_status_print(struct mp3_status_s *status)
{
    MP3_TABLE
}
#undef X

以上示例展示了X-Macro的2项能力——表项定义代码生成

让我们来解析这段代码:

  1. MP3_TABLE:作为X宏的主宏,利用X宏定义3组MP3相关表项信息
  2. X(a, b) a,:定义MP3操作枚举。在使用完毕后#undef X
  3. X(a, b) int b;:定义MP3状态结构体
  4. X(a, b) {#b, mp3_set_##a},:定义MP3操作索引表和操作接口函数
  5. X(a, b) printf("%s: %d", #b, status->b);:定义MP3状态打印函数

我们根据应用场景中那些不变的约束,筛选出了MP3操作表项信息中的有效信息,并通过X宏将这些有效信息生成出程序中所需要生成的各种信息,做到了以不变,应万变

这么做的好处是使代码更聚焦和规范,当表项中需要新增操作时不容易遗漏相关联处代码更改,同时通过代码生成,减少了代码行数。

当看到这里后,如果你想用X宏重构你的代码,请十分注意以下几点:
1、使用X宏的程序中不变的约束能够满足需求吗?
例如:当MP3新增操作功能时,能否满足当前的约束?
① 当新增播放模式功能时,仍可以满足约束,只需新增X(M_MODE, mode)
② 但新增快进/快退功能时,由于MP3不存在快进/快退状态,新增此功能会破坏原来新增表项即为MP3状态结构体字段这一约束,所以如果需求中有此功能,MP3状态结构体应手动定义而非由X宏生成
2、使用X宏带来的收益是否超过其带来的成本
X宏带来的好处是有成本的:
① 规范和约束了代码框架,同时降低了框架的灵活性
② 宏间接生成文本,使编辑器全局搜索变量和函数更困难,程序调试成本也会上升

| X作为参数

上一节基础X-Macro的案例中,为了避免宏命名冲突,我们在使用完X宏后都需要有#undef X的步骤,这里仍有优化空间。

以下程序通过将MP3程序示例中的X宏作为参数传入,可以避免命名冲突,提高代码可读性:

#define MP3_TABLE(X) X(M_PLAY, play) 
                     X(M_VOLUME, volume) 
                     X(M_SONG, cur_song_id)
// 1. 定义MP3操作枚举
#define MP3_ENUM(a, b) a,
enum mp3_option_e {
    // 后续MP3_TABLE替换同理,节约篇幅考虑仅列第1个
    MP3_TABLE(MP3_ENUM)
    MUSIC_OP_MAX
};
// 2. 定义MP3状态结构体
#define MP3_STRUCT_FIELD(a, b) int b;
// 3. 定义MP3操作索引表和操作接口函数
#define MP3_OP_ELEMENT(a, b) {#b, mp3_set_##a},
// 4. 定义MP3状态打印函数
#define MP3_STATUS_PRINT(a, b) printf("%s: %d", #b, status->b);

实际应用

实际项目中,X-Macro一般在表行数较多的场景使用,有时为了与一般头文件做区分,会将表放在.def.tbl文件中。为了实现X-Macro效果,该文件不应加入一般头文件的仅单次包含保护,而应在X宏定义后,在需要的地方引用.def.tbl文件。

| LLVM

LLVMC++项目,其大量运用了X-Macro,以LLVM IR所定义的指令为例,以下列出核心部分:

拓展知识:
前端: 源代码分析,主要是Clang,为LLVM IR屏蔽了高级语言差异。
后端: 目标代码生成,主要是LLVM CodeGenLLVM Target,为LLVM IR屏蔽了硬件平台差异。
LLVM IR:LLVM编译器框架的核心部分,其被设计为与平台无关,起到连接前端与后端之间的桥梁作用,不同前端与后端可以使用相同的LLVM IR

llvm/include/llvm/IR/Instruction.defHANDLE_TERM_INSTHANDLE_INSTX

#ifndef HANDLE_TERM_INST
#ifndef HANDLE_INST
#define HANDLE_TERM_INST(num, opcode, Class)
#else
#define HANDLE_TERM_INST(num, opcode, Class) HANDLE_INST(num, opcode, Class)
#endif
#endif

 FIRST_TERM_INST  ( 1)
HANDLE_TERM_INST  ( 1, Ret           , ReturnInst)
...
HANDLE_TERM_INST  (11, CallBr        , CallBrInst) // A call-site terminator
  LAST_TERM_INST  (11)

以下为2个典型的X-Macro应用:

llvm/include/llvm/IR/Instruction.h

  enum TermOps {       // These terminate basic blocks
#define  FIRST_TERM_INST(N)             TermOpsBegin = N,
#define HANDLE_TERM_INST(N, OPC, CLASS) OPC = N,
#define   LAST_TERM_INST(N)             TermOpsEnd = N+1
#include "llvm/IR/Instruction.def"
  };

llvm/lib/IR/Core.cpp

static LLVMOpcode map_to_llvmopcode(int opcode)
{
    switch (opcode) {
      default: llvm_unreachable("Unhandled Opcode.");
#define HANDLE_INST(num, opc, clas) case num: return LLVM##opc;
#include "llvm/IR/Instruction.def"
#undef HANDLE_INST
    }
}

| Linux

Linux内核代码中并未使用X-Macro技术,而是通过.tbl文件+生成脚本实现了代码生成功能。

例如,Linux的系统调用表(syscall.tbl)的定义就采用了此方式,这里仅以其中一个为例:

拓展知识:
Linux内核支持多种硬件架构(x86、arm、risc-v、mips、powerpc等),通过为不同硬件平台编写一套syscall.tbl屏蔽不同硬件平台差异。事实上,Linux内核主干代码中有十来种架构对应的syscall.tbl,不同硬件平台间差异很大。

arch/x86/entry/syscalls/syscall_64.tbl

# The format is:
# <number> <abi> <name> <entry point> [<compat entry point> [noreturn]]
0 common read   sys_read
1 common write   sys_write
2 common open   sys_open
...
该文件经过scripts/syscalltbl.sh的解析后,会生成类似以下内容:
asmlinkage long (*sys_call_table[])(const struct pt_regs *) = {
    [0] = sys_read,
    [1] = sys_write,
    [2] = sys_open,
    [3] = sys_close,
    ...
};

更进一步

读者可以思考下,为什么同样是大型的C/C++项目,面对代码生成的需求时,LLVM IR对指令表使用了X-Macro技巧,而Linux内核中系统调用表却选择使用脚本+文本生成呢?

以下是笔者的理解:
LLVM IR指令表与Linux内核系统调用表的处境差异很大:
1、LLVM IR指令表不需要考虑不同平台差异,仅需维护一份指令表即可;而Linux内核系统调用表需要考虑不同平台差异,其.tbl文件+生成脚本的代码生成方式能够很好地维护不同平台系统调用表差异
2、X-Macro本质上是集成在代码中的文本操作,使用的核心是找到1套不变的表项信息,并由X来应对编写表项信息在代码不同处的使用方式;而Linux内核需要面对多套表项信息,无法使用X-Macro

相关推荐

登录即可解锁
  • 海量技术文章
  • 设计资源下载
  • 产业链客户资源
  • 写文章/发需求
立即登录

个人博客:嵌入式技术&生活分享&开源项目

微信公众号