X
即變化
X-Macro
早在C語言被創(chuàng)造之前,就已在帶預處理器的匯編器中得到運用。也就是說,今天介紹的宏魔法X
并不依賴C語言的任何Hack特性,它只是一類純粹的代碼文本處理技巧。所以,這期的案例會更聚焦于X-Macro
的應用場景而非C語言的各種技巧。
| 基礎X-Macro
X-Macro
的核心思想是將一組定義放在一個宏列表中,然后通過多次展開這個宏列表,生成重復性代碼(例如函數(shù)聲明、結構體初始化等),從而減少代碼重復和維護成本。
可以說,X-Macro
技巧是根據(jù)不變
的表項生成不同的代碼,X
正是那變化
的部分。
大家可以先看看這個簡單的MP3程序示例中實現(xiàn)的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狀態(tài)結構體
#define X(a, b) int b;
struct mp3_status_s {
MP3_TABLE
};
#undef X
// 3. 定義MP3操作索引表和操作接口函數(shù)
#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狀態(tài)打印函數(shù)
#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項能力——表項定義、代碼生成。
讓我們來解析這段代碼:
-
MP3_TABLE
:作為X
宏的主宏
,利用X
宏定義3組MP3相關表項信息 -
X(a, b) a,
:定義MP3操作枚舉。在使用完畢后#undef X
-
X(a, b) int b;
:定義MP3狀態(tài)結構體 -
X(a, b) {#b, mp3_set_##a},
:定義MP3操作索引表和操作接口函數(shù) -
X(a, b) printf("%s: %d", #b, status->b);
:定義MP3狀態(tài)打印函數(shù)
我們根據(jù)應用場景中那些不變
的約束,篩選出了MP3操作表項信息中的有效信息,并通過X
宏將這些有效信息生成出程序中所需要生成的各種信息,做到了以不變,應萬變
。
這么做的好處是使代碼更聚焦和規(guī)范,當表項中需要新增操作時不容易遺漏相關聯(lián)處代碼更改,同時通過代碼生成,減少了代碼行數(shù)。
當看到這里后,如果你想用
X
宏重構你的代碼,請十分注意以下幾點:
1、使用X
宏的程序中不變
的約束能夠滿足需求嗎?
例如:當MP3新增操作功能時,能否滿足當前的約束?
① 當新增播放模式
功能時,仍可以滿足約束,只需新增X(M_MODE, mode)
② 但新增快進/快退
功能時,由于MP3不存在快進/快退狀態(tài),新增此功能會破壞原來新增表項即為MP3狀態(tài)結構體字段
這一約束,所以如果需求中有此功能,MP3狀態(tài)結構體應手動定義而非由X
宏生成
2、使用X
宏帶來的收益
是否超過其帶來的成本
?
X
宏帶來的好處是有成本的:
① 規(guī)范和約束了代碼框架,同時降低了框架的靈活性
② 宏間接生成文本,使編輯器全局搜索變量和函數(shù)更困難,程序調試成本也會上升
| X
作為參數(shù)
上一節(jié)基礎X-Macro
的案例中,為了避免宏命名沖突,我們在使用完X
宏后都需要有#undef X
的步驟,這里仍有優(yōu)化空間。
以下程序通過將MP3程序示例中的X
宏作為參數(shù)傳入,可以避免命名沖突,提高代碼可讀性:
#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 {
// 后續(xù)MP3_TABLE替換同理,節(jié)約篇幅考慮僅列第1個
MP3_TABLE(MP3_ENUM)
MUSIC_OP_MAX
};
// 2. 定義MP3狀態(tài)結構體
#define MP3_STRUCT_FIELD(a, b) int b;
// 3. 定義MP3操作索引表和操作接口函數(shù)
#define MP3_OP_ELEMENT(a, b) {#b, mp3_set_##a},
// 4. 定義MP3狀態(tài)打印函數(shù)
#define MP3_STATUS_PRINT(a, b) printf("%s: %d", #b, status->b);
實際應用
實際項目中,X-Macro
一般在表行數(shù)較多的場景使用,有時為了與一般頭文件做區(qū)分,會將表放在.def
或.tbl
文件中。為了實現(xiàn)X-Macro
效果,該文件不應加入一般頭文件的僅單次包含保護,而應在X
宏定義后,在需要的地方引用.def
或.tbl
文件。
| LLVM
LLVM
是C++
項目,其大量運用了X-Macro
,以LLVM IR
所定義的指令為例,以下列出核心部分:
拓展知識:
前端:?源代碼分析,主要是Clang
,為LLVM IR
屏蔽了高級語言差異。
后端:?目標代碼生成,主要是LLVM CodeGen
和LLVM Target
,為LLVM IR
屏蔽了硬件平臺差異。
LLVM IR:LLVM
編譯器框架的核心部分,其被設計為與平臺無關,起到連接前端與后端之間的橋梁作用,不同前端與后端可以使用相同的LLVM IR
。
llvm/include/llvm/IR/Instruction.def
:HANDLE_TERM_INST
或HANDLE_INST
為X
#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文件+生成腳本
實現(xiàn)了代碼生成功能。
例如,Linux
的系統(tǒng)調用表(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
內核中系統(tǒng)調用表卻選擇使用腳本+文本
生成呢?
以下是筆者的理解:
LLVM IR
指令表與Linux
內核系統(tǒng)調用表的處境差異很大:
1、LLVM IR
指令表不需要考慮不同平臺差異,僅需維護一份指令表即可;而Linux
內核系統(tǒng)調用表需要考慮不同平臺差異,其.tbl文件+生成腳本
的代碼生成方式能夠很好地維護不同平臺系統(tǒng)調用表差異
2、X-Macro
本質上是集成在代碼中的文本操作,使用的核心是找到1套
不變的表項信息,并由X
來應對編寫表項信息在代碼不同處的使用方式;而Linux
內核需要面對多套
表項信息,無法使用X-Macro