--- tags: NCKU Linux Kernel Internals, C語言 --- # C 語言:前置處理器應用 [你所不知道的 C 語言:前置處理器應用篇](https://hackmd.io/@sysprog/c-preprocessor?type=view) ## 善用 preprocessor ### `#` [Stringizing Operator / Stringizing](https://gcc.gnu.org/onlinedocs/cpp/Stringizing.html): 把一個表示式(statement)變成字串 ```c= #include <stdio.h> #define assertLike(s) if (s) printf(#s " pass\n"); \ else printf(#s " failed\n") int main() { assertLike(0 == 1); assertLike(1 == 1); return 0; } ``` 舉例來說,可以用來簡單實作一個檢查 statement 為 True 或 False 的 macro。可以看到在 `assertLike()` 的 if 中 `s` 被確實執行,而透過 `#s` 可以被 `printf` 印出。 使用 stringizing 需注意其特性: ```c= #include <stdio.h> #define p(s) printf(#s) #define STR "TRY IT?" int main() { p( p = "foo\n"); printf("\n"); printf("p = \"foo\\n\""); p( p = foo l\n); p( p = foo l\n); // p(,); <- this one will fail! p(STR); p( p = foo /*YOU CANT SEE ME*/) return 0; } ``` * stringizing 會刪除參數前後的空格 * preprocessor 並非只是在 statement 前後加上兩個 `"`,而是會適當的加上跳脫字元(backslash-escapes),所以上面程式的第 6 行和第 8 行效果相同。 * statement 中的連續空白會被轉成單一空白,所以上面程式的第 10 行和第 11 行等價。 * 如 `,` `)` 的符號不能被 stringizing * macro argument 不能被 stringizing,因此第 15 行會印出 `STR`,而不是 `TRY IT?` * 註解會轉成空白,因此程式第 17 行中,不會印出註解中的內容。 ### `##` [Token Pasting Operator / Concatenation ](https://gcc.gnu.org/onlinedocs/cpp/Concatenation.html) : 連接兩個 "token"。 這有甚麼用呢? 舉例來說,我們可以用來建立一個複雜的資料結構,讓這個結構有統一的命名規則。 ```c= #include <stdio.h> #define MAKE_STRUCT(name) \ typedef struct name { \ int name##_price; \ } _t_##name int main() { MAKE_STRUCT(apple); _t_apple a; a.apple_price = 10; printf("%d \n",a.apple_price); return 0; } ``` Reference: * [理解C语言中的Stringizing操作符和Token Pasting操作符](http://wangjieqiang.com/2019/09/01/%E7%90%86%E8%A7%A3C%E8%AF%AD%E8%A8%80%E4%B8%AD%E7%9A%84Stringizing%E6%93%8D%E4%BD%9C%E7%AC%A6%E5%92%8CToken-Pasting%E6%93%8D%E4%BD%9C%E7%AC%A6/) ### `_Generic` C 語言中沒有 C++ 裡的 `template`,但我們可以透過 `_Generic` 達到接近的效果。 舉例來說,如果我們要實作一個印出兩個數字的函數 `print_pair()`: ```c= int print_pair_int(int a, int b) { printf("%d %d\n",a,b); } ``` 這個函式可以印出兩個 int,如果呼叫 `print_pair_int(1, 3)`,會印出`1 3`。然而,因為輸入是 int 的原因,呼叫 `print_pair_int(1.1, 3.3)` ,只會印出 `1 3`。如果我們希望輸入浮點數會轉為印出浮點數,就必須再寫另一個 function。 ```c= int print_pair_float(double a, double b) { printf("%f %f\n",a,b); } ``` 這豈不是很麻煩嗎?明明是相同的功能,只是針對不同的型別就必須呼叫不同的 function。難道不能對於不同的型別,都有一個共同的 function 介面嗎? 於是我們可以藉由使用 `_Generic`,達到目的。 ```c= #include <stdio.h> #define print_pair(a, b) \ _Generic((a), \ int: _Generic((b), \ int : print_pair_int, \ double: print_pair_int_double \ ), \ double: _Generic((b), \ int : print_pair_double_int, \ double: print_pair_double \ ) \ )(a, b) int print_pair_int(int a, int b) { printf("%d %d\n",a,b); } int print_pair_double(double a, double b) { printf("%f %f\n",a,b); } int print_pair_double_int(double a, int b) { printf("%f %d\n",a,b); } int print_pair_int_double(int a, double b) { printf("%d %f\n",a,b); } int main() { print_pair(1,3); print_pair(1.1,3.3); print_pair(1,3.3); print_pair(1.1,3); return 0; } ``` ## Example: Cloak [Cloak](https://github.com/pfultz2/Cloak) 專案展示了 C preprocessor 的能耐,讓我們一起來看看這個專案是如何使用前置處理器的各種技巧的。 > Reference: [C Preprocessor tricks, tips, and idioms ](https://github.com/pfultz2/Cloak/wiki/C-Preprocessor-tricks,-tips,-and-idioms) ### Pattern Matching 首先,讓我們來思考如何用 macro 設計一個 if,可以像下面這樣定義: ```cpp #define IIF(cond) IIF_ ## cond #define IIF_0(t, f) f #define IIF_1(t, f) t ``` `IIF` 根據 `cond`,可以擴展成 `IIF_0` 或 `IIF_1`,再往下擴展成輸入的 `f` 或 `t`。因此,例如 `IIF(1)(true, false)` 這樣的 statement,最後會被處理成 true。而 `IIF(0)(true, false)` 則會被處理成 false。 不過這個設計在 `IIF` 參數是另一個 macro 時會無法得到預期的展開,舉例來說: ```cpp #define A() 1 IIF(A())(true, false) ``` 在這個用法下,`A()` 不會先被展開成 1,`IIF(A())` 會展開成 `IIF_A()`,成為未被定義的語法。我們可以透過在中間在插入一層轉換來解決此問題。 ```cpp #define CAT(a, ...) PRIMITIVE_CAT(a, __VA_ARGS__) #define PRIMITIVE_CAT(a, ...) a ## __VA_ARGS__ ``` 然後重新定義 `IIF`。 ```cpp #define IIF(c) PRIMITIVE_CAT(IIF_, c) #define IIF_0(t, ...) __VA_ARGS__ #define IIF_1(t, ...) t ``` 來檢視一下現在 `IIF(A())(true, false)` 的展開步驟: * `IIF(A())` 展開成 `PRIMITIVE_CAT(IIF_, 1)`,最後展開成 `IIF_1` * 因此 `IIF_1(true, false)` 展開得到 true 藉此,`IIF` 可以接受另一個最終可以展開成 0 或 1 的 macro 作為參數。但是更符合實際的 if macro 應該要可以區分 0 和 非 0(這也是其之所以命名為 `IIF` 的原因),後面我們會探討如何再增加一層轉換來解決此問題。 總之,藉由類似的手法,我們也可以建立其他的 macro,作為 macro 中的 operator 來使用: * 將 0 / 1 反轉的 complement ```cpp #define COMPL(b) PRIMITIVE_CAT(COMPL_, b) #define COMPL_0 1 #define COMPL_1 0 ``` * bit and ```cpp #define BITAND(x) PRIMITIVE_CAT(BITAND_, x) #define BITAND_0(y) 0 #define BITAND_1(y) y ``` * increase / decrease ```cpp #define INC(x) PRIMITIVE_CAT(INC_, x) #define INC_0 1 #define INC_1 2 #define INC_2 3 #define INC_3 4 #define INC_4 5 #define INC_5 6 #define INC_6 7 #define INC_7 8 #define INC_8 9 #define INC_9 9 #define DEC(x) PRIMITIVE_CAT(DEC_, x) #define DEC_0 0 #define DEC_1 0 #define DEC_2 1 #define DEC_3 2 #define DEC_4 3 #define DEC_5 4 #define DEC_6 5 #define DEC_7 6 #define DEC_8 7 #define DEC_9 8 ``` ### Detection Detection 技巧用來區分輸入參數的類型,展開成 0 或者 1 的結果。讓我們先來檢視該技巧的核心 macro: ```cpp #define CHECK_N(x, n, ...) n #define CHECK(...) CHECK_N(__VA_ARGS__, 0,) #define PROBE(x) x, 1, ``` * 當 `CHECK` 的輸入參數是 `PROBE` 時,舉例來說,`CHECK(PROBE(~))`,會展開成 `CHECK_N(~, 1, 0, )`,因此最後展開成 1 * 但假如是 `CHECK(xxx)`,則會擴展成 `CHECK_N(xxx, 0, )` 因此會變成 0 基於這個技巧,可以建立一些檢查用的 macro。例如以下的 macro 可以用來判斷輸入的參數是否是括號。 ```cpp #define IS_PAREN(x) CHECK(IS_PAREN_PROBE x) #define IS_PAREN_PROBE(...) PROBE(~) IS_PAREN(()) // Expands to 1 IS_PAREN(xxx) // Expands to 0 ``` * `IS_PAREN(())` 展開成 `CHECK(IS_PAREN_PROBE())` 再展開成 `CHECK(PROBE(~))`,如前所述會變成 1 * 而 `IS_PAREN(xxx)` 會展開成 `CHECK(IS_PAREN_PROBE xxx)` 直接被展開成 `CHECK_N(IS_PAREN_PROBE xxx, 0,)`,因此最後會變成 0 透過 detection 技巧,我們可以更進前面不夠完整的 if,建立更通用的 `IF` macro。 首先,我們需要一個 `NOT` macro,結合 `CHECK` 和 `PROBE` 的技巧,讓 0 成為 1,而非 0 則展開成 0: ```cpp #define NOT(x) CHECK(PRIMITIVE_CAT(NOT_, x)) #define NOT_0 PROBE(~) ``` 然後再定義 `BOOL`,透過 `COMPL` 將 0/1 反轉。如此一來,condition 0 維持為 0,而非 0 皆會轉換成 1,作為 `IIF` 的參數,建立出 `IF`。 ```cpp #define BOOL(x) COMPL(NOT(x)) #define IF(c) IIF(BOOL(c)) ``` 如果有些混亂的話,可以藉由實際的案例來看看為甚麼這個設計可行: 以 `IF(n)(true, false)` 為例,展開的順序如下: 1. `IF(n)` 2. -> `IIF(BOOL(n))` 3. -> `IIF(COMPL(NOT(n)))` 4. -> `IIF(COMPL(CHECK(NOT_n)))` 當 n = 0 時,NOT_0 會展開成 `PROBE`,導致 `CHECK(NOT_n)` 變成 1,因此繼續往下展開的話: 5. -> `IIF(COMPL(1))` 6. -> `IIF(0)` 7. -> false 反之,當 n != 0,NOT_n 不能再往下展開,於是 `CHECK(NOT_n)` 變成 0,因此繼續往下展開的話: 5. -> `IIF(COMPL(0))` 6. -> `IIF(1)` 7. -> true 於是,現在我們得到一個更泛用的 if macro,可以區別 0 和 非 0 的 condition。 然後就可以在此之上建立一個 `WHEN` macro,當 condition 為 true 時繼續向下展開,否則就停止。 ```cpp #define EAT(...) #define EXPAND(...) __VA_ARGS__ #define WHEN(c) IF(c)(EXPAND, EAT) ``` ### Recursion :::warning 個人覺得 recursion 部份有點複雜,所以細節部份我還沒辦法很清楚的理解,因此內容僅供參考,建議閱讀原文QQ ::: 一般而言,macro 是不能被遞迴展開的。當 macro 被展開時,會變成 [painted blue](https://en.wikipedia.org/wiki/Painted_blue) 狀態,被標示的 token 不能繼續往下展開。舉例來說,`#define A A()` 在展開 `A` 時並不會變成 `A()()()()...`。 為了透過 macro 實現遞迴的效果。我們需要有技巧的防止 macro 被 painted blue。其次,我們可以透過判斷 macro 是否 painted blue,來決定是否要延伸展開到另一個 macro。 #### Deferred expression Deferred expression 所指是一段需要被更多的 scan 才能完整展開的 expression。下面是一個例子: ```cpp #define EMPTY() #define DEFER(id) id EMPTY() #define OBSTRUCT(...) __VA_ARGS__ DEFER(EMPTY)() #define EXPAND(...) __VA_ARGS__ #define A() 123 A() // Expands to 123 DEFER(A)() // Expands to A () because it requires one more scan to fully expand EXPAND(DEFER(A)()) // Expands to 123, because the EXPAND macro forces another scan ``` 透過 `DEFER` macro,`A()` 不會立即被展開,直到我們再用一層 `EXPAND` 才可以將 `A()` 展開為其定義的 123。 `OBSTRUCT` 則可以 defer 兩次,舉例來說: ```cpp #define RR() 3 OBSTRUCT(RR)() ``` * `OBSTRUCT(RR)()` 的展開為 `RR EMPTY EMPTY() ()()` 然後再展開為 `RR EMPTY ()()` * 因此 `EXPAND(OBSTRUCT(RR)())` 展開為 `RR()` * 因此 `EXPAND(EXPAND(OBSTRUCT(RR)()))` 展開為 `3` 為甚麼需要這個技巧呢?如同一開始所說,當 macro 被掃描然後展開,就會成為 painted blue 狀態,preprocessor 不會將 painted blue 的 token 繼續展開,這也是為甚麼 macro 沒辦法遞迴展開的緣故。為了不讓某個特定的 macro 直接被 painted blue,我們可以透過 deferring expansion 的技巧讓 macro 需要足夠次數的 scan 才能被展開。 透過 `EVAL` macro 可以達到多次 scan 的目的。如下所示,從 `EVAL` 展開會有3個 `EVAL1`,再展開有 3^2 個 `EVAL2` ,以此類推,展開到 `EVAL5` 時有 3^5 個,這表示最多可以 scan $1+3+3^1+...3^5 = 355$ 次。 ```cpp #define EVAL(...) EVAL1(EVAL1(EVAL1(__VA_ARGS__))) #define EVAL1(...) EVAL2(EVAL2(EVAL2(__VA_ARGS__))) #define EVAL2(...) EVAL3(EVAL3(EVAL3(__VA_ARGS__))) #define EVAL3(...) EVAL4(EVAL4(EVAL4(__VA_ARGS__))) #define EVAL4(...) EVAL5(EVAL5(EVAL5(__VA_ARGS__))) #define EVAL5(...) __VA_ARGS__ ``` 基於這個技巧,就可以定義 `REPEAT` macro。在這個 macro 中,`REPEAT_INDIRECT` 使得 `REPEAT` 得以遞迴自己。並透過 `OBSTRUCT` defer 兩次(因為 scan 包含 `WHEN` 和 `EVAL` 兩次)。 ```cpp #define REPEAT(count, macro, ...) \ WHEN(count) \ ( \ OBSTRUCT(REPEAT_INDIRECT) () \ ( \ DEC(count), macro, __VA_ARGS__ \ ) \ OBSTRUCT(macro) \ ( \ DEC(count), __VA_ARGS__ \ ) \ ) #define REPEAT_INDIRECT() REPEAT //An example of using this macro #define M(i, _) i EVAL(REPEAT(8, M, ~)) // 0 1 2 3 4 5 6 7 ``` 也可以定義 `WHILE`,會遞迴展開 `op` 直到 `pred` 為 true。 ```cpp #define WHILE(pred, op, ...) \ IF(pred(__VA_ARGS__)) \ ( \ OBSTRUCT(WHILE_INDIRECT) () \ ( \ pred, op, op(__VA_ARGS__) \ ), \ __VA_ARGS__ \ ) #define WHILE_INDIRECT() WHILE // example #define PRED(state, ...) BOOL(state) #define OP(state, ...) DEC(state), state, __VA_ARGS__ EVAL(WHILE(PRED, OP, 8,)) // 0, 1, 2, 3, 4, 5, 6, 7, 8, ``` ### Comparison 要比較兩個 token 是否相同,我們可以善用 painted blue 的狀態。透過將 macro 做為另一個 macro 的參數,並利用如果兩個 macro 不同,則展開會繼續往下的特性(見以下範例),則可以實作出比較的效果。 假如我想比較的 token 有 foo 跟 bar,那麼可以如以下定義。 ```cpp #define COMPARE_foo(x) x #define COMPARE_bar(x) x ``` 透過 `PRIMITIVE_COMPARE` ```cpp #define PRIMITIVE_COMPARE(x, y) IS_PAREN \ ( \ COMPARE_ ## x ( COMPARE_ ## y) (()) \ ) ``` `PRIMITIVE_COMPARE(foo, bar)` 1. -> `IS_PAREN(COMPARE_foo(COMPARE_bar)(()))` 2. -> `IS_PAREN(COMPARE_bar(()))` 3. -> `IS_PAREN(())` 4. 1 `PRIMITIVE_COMPARE(foo, foo)` 1. -> `IS_PAREN(COMPARE_foo(COMPARE_foo)(()))` 2. -> `IS_PAREN(COMPARE_foo(()))` 3. 0 (因為從 1 到 2 時 `COMPARE_foo(x)` 已經 painted blue) 大部分的情況下,`PRIMITIVE_COMPARE` 都如預期的運作。唯一的小問題是只有當兩個 token 都有關聯的 `COMPARE_` 定義時才能成立。 ```cpp PRIMITIVE_COMPARE(foo, unfoo) // Should expand to 1, but it expands to 0 ``` 解決的方法是再增加一層,`IS_COMPARABLE` 先判斷是否兩個 token 都有定義 `COMPARE_`,如果都有再額外使用 `PRIMITIVE_COMPARE` 去比較。 ```cpp #define IS_COMPARABLE(x) IS_PAREN( CAT(COMPARE_, x) (()) ) #define NOT_EQUAL(x, y) \ IIF(BITAND(IS_COMPARABLE(x))(IS_COMPARABLE(y)) ) \ ( \ PRIMITIVE_COMPARE, \ 1 EAT \ )(x, y) ``` 透過反轉的 `COMPL` 也可以延伸定義 `EQUAL`。 ```cpp #define EQUAL(x, y) COMPL(NOT_EQUAL(x, y)) ``` ## Example: generic-print [generic-print](https://github.com/exebook/generic-print) 是一個有趣的專案,可以用類似 python 的 print 語法來印出字串,搭配容易觀看的顏色功能。雖然它還存在許多使用的限制,但其中的 preprocessor 技巧仍然很值得探討。 ### `print` ```cpp #define fprint(fd, a...) ({ \ int count = __print_count(a); \ unsigned short stack[count], *_p = stack + count; \ __print_types(a); \ __print_func(fd, count, _p, a); \ }) #define print(a...) fprint(stdout, a) ``` * `({})` 是 [Statements and Declarations in Expressions](https://gcc.gnu.org/onlinedocs/gcc/Statement-Exprs.html),因為 `__print_func` 不回傳值,在這裡的作用其實可以透過 `do...while(0)` 取代 * `a...` 是 [Variadic Macros](https://gcc.gnu.org/onlinedocs/cpp/Variadic-Macros.html) 語法,表示將一系列由逗號組織起來的參數命名為 `a` ### `__print_count` ```cpp #define __print_count_int(q,w,e,r,t,y,u,i,o,p,a,s,d,f,g,h,j,k,l,z,x,c,v,b,n,m,...) m #define __print_count(a...)__print_count_int(a,25,24,23,22,21,20,19,18,17,16,15,14,13,12,11,10,9,8,7,6,5,4,3,2,1,0) ``` `fprint` 第一個使用到的 macro 是 `__print_count`,其作用是計算參數的數量,讓我們來看看這是如何作用的。上面的作法稍微有點繁瑣,所以下面我們用一個簡化的版本來說明: ```cpp #define __print_count_int(q,w,e,r,...) r #define __print_count(a...)__print_count_int(a,3,2,1,0) ``` 因為 `r` 總是取第4個參數的內容,因此可以想像給的參數愈多,`3, 2, 1, 0` 就會放的越靠後,讓 `r` 拿到更大數字,巧妙的設計就可以剛會對上正確的數量。舉例來說,讓我們假設 `a` 有兩個參數,例如是 `arg1, arg2`,那麼 `__print_count` 的展開變成 `__print_count_int(arg1, arg2, 3, 2, 1, 0)`,`r` 的位置是第四個參數,所以剛好就是 2。 ### `__print_types` 整個 `__print_types` 有點長,這裡就放一個縮減版的,不難猜到省略層層的 pattern 就是減少 `print` 可以接受的參數數量而已。 ```cpp #define __builtin_choose_expr __builtin_choose_expr #define __print_is_type(a, t) __builtin_types_compatible_p(typeof(a), t) #define __print_code(a, cont) __builtin_choose_expr(__print_is_type(a, void), \ 0, \ __print_push(__print_typeid(a), (sizeof(a)<(1<<16>>5)?sizeof(a):(1<<16>>5)-1), cont)) #define __print_types_int(q,w,e,r,...)\ __print_code(q,__print_code(w,__print_code(e,__print_code(r,0)))) #define __print_types(a...) __print_types_int(a, (void)0,1,1,1) ``` * `__builtin_choose_expr(const_exp, exp1, exp2)` 類似於 `? :` 的概念,只是參數可以是一個 expression,當 `const_exp` 成立時就回傳 `exp1`,反之回傳 `exp2` * `__print_is_type(a, t)` 用到 `__builtin_types_compatible_p`,後者的作用是比較 `a` 的 `t` 是否 compatible(可參考 C 語言規格書 6.2.7 Compatible type and composite type) > [Other Built-in Functions Provided by GCC](https://gcc.gnu.org/onlinedocs/gcc/Other-Builtins.html) `__print_types` 後面的 `(void)0,1,1,1` 是為了填滿 `__print_types_int` 的參數數量。同時也因應 `__print_is_type` 的設計,可以從 `__print_code` 看到 `__print_is_type` 的用法是 `__print_is_type(a, void)`,當 `a` 是 `(void)0` 的時候 `__print_is_type` 會得到 1,而 `__print_code` 的 `cont` 只有在 `__print_is_type` 回傳到 1 時會停止展開。換句話說,在 `(void)0` 以前的參數都會經過 `__print_push` 處理,後者是在 stack 中填入第 `k` 個參數的類型代號和 byte 數量。 ### `__print_push` ```cpp #define __print_push(c,size,cont) (cont, *--_p = c | (size << 5)) ``` 在前一節可以看到 `__print_push` 的參數來自: * 第一個參數 `c` 來自 `__print_typeid` 是根據類別所對應的 `type` 編號 * 第二個參數 `size` 是計算自 `(sizeof(a)<(1<<16>>5)?sizeof(a):(1<<16>>5)-1)` * 第三個參數是 `cont`,是下一個 `__print_code` 的展開,這裡的語法叫作 comma expression,`(exp1, exp2)` 中 `exp1` 會被計算,而 `exp2` 會被計算且回傳(可參見 C 語言規格書 6.5.17 Comma operator) 可以看到 `size` 是有限制的,因為作者通過一個 16 bits short 表達 type 類型和真正的 size,後 5 bits 儲存類型編碼,所以剩下可以用來儲存 `size` 的範圍只能表達到最大 $2^(16 - 5)$,也就是 `1<<(16>>5)`。 ### `__print_func` ```cpp void __print_func (FILE *fd, int count, unsigned short types[], ...) { va_list v; va_start(v, types); ``` 這裡可以看到再次看到 C 語言的不定長度的參數技巧,簡單說明其使用方式,細節則可以參考 C 語言規格書 7.16 Variable arguments <stdarg.h>。 可以看到首先定義 `va_list`,然後透過 `va_start` 去初始化。`va_start` 透過最後一個參數 `types` 找到不定長度參數的第一個之位址。 ```cpp ... for (int i = 0; i < count; i++) { if (i > 0) fprintf(fd, " "); char type = types[i] & 0x1F; char size = types[i] >> 5; if (type == 1) { __print_color(fd, __print_color_float); double d = va_arg(v, double); fprintf(fd, "%'G", d); } ... ``` 然後就是 for 迴圈逐個參數印出,首先取出正確的 `type` 和 `size` 數值。印出的內容可以大致區分成陣列和單個變數的類型,對單個變數來說,先用 `__print_color` 加上顏色的 [ANSI escape code](https://en.wikipedia.org/wiki/ANSI_escape_code),然後用 `va_arg` 得到變數實際內容後,`fprintf` 印出。 ```cpp else if (type == 11) { __print_array(fd, int, "%i", __print_color_number); } ``` 對於陣列類型,舉例來說 type 11 的 `int` array,讓我們來看看 `_print_array` 的實作: ```cpp #define __print_array(fd, T, qual, color) \ __print_color(fd, __print_color_normal); \ int max_len = 16; \ int n = size/sizeof(T); \ T *m = va_arg(v, T*); \ fprintf(fd, "["); \ __print_color(fd, color); \ for (int i = 0; i < (n < max_len ? n : max_len); i++) { \ if (i > 0) fprintf(fd, " "); \ fprintf(fd, qual, m[i]); \ } \ __print_color(fd, __print_color_normal); \ if (n > max_len) fprintf(fd, "..."); \ fprintf(fd, "]"); ``` 首先,用預設的 foreground color (`__print_color_normal`) 印出 `[`(最後的 `]` 也是同樣道理)。接著計算 elements 的數量 `n = size/sizeof(T)`,然後根據指定的顏色一個個輸出,且最多輸出 16 個。