• 正文
    • X即變化
    • 實際應用
    • 更進一步
  • 相關推薦
申請入駐 產業(yè)圖譜

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

01/23 07:44
1684
加入交流群
掃碼加入
獲取工程師必備禮包
參與熱點資訊討論

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項能力——表項定義、代碼生成

讓我們來解析這段代碼:

  1. MP3_TABLE:作為X宏的主宏,利用X宏定義3組MP3相關表項信息
  2. X(a, b) a,:定義MP3操作枚舉。在使用完畢后#undef X
  3. X(a, b) int b;:定義MP3狀態(tài)結構體
  4. X(a, b) {#b, mp3_set_##a},:定義MP3操作索引表和操作接口函數(shù)
  5. 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

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文件+生成腳本實現(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

相關推薦

登錄即可解鎖
  • 海量技術文章
  • 設計資源下載
  • 產業(yè)鏈客戶資源
  • 寫文章/發(fā)需求
立即登錄