# Linux2025q1: lab0-c 貢獻規定速記 本文摘錄自 [CONTRIBUTING.md](https://github.com/Dennis40816/lab0-c/blob/master/CONTRIBUTING.md) : :::danger 不要濫用「重點」一詞,原文沒幾行,你只是「摘錄」。 >已修正。 ::: ## 格式 - 基於 K&R - 註解使用 `/* */` 或 `//` ## 命名 - 使用 snake_case 命名法 ## 型別 - 大小使用 `size_t` - 對可能含錯誤碼 (< 0) 的大小,使用 `ssize_t` - 避免在迭代器變數使用固定長度的類型,如 `uint8_t`,應該使用 `unsigned`,除非有資源限制 - 布林類型使用 `bool`,其他類別要轉換成 `bool` 應使用比較方式達成,而非強轉型 - 對於 `signed int`,不應該在其中宣告 bit-field,以及對其進行 bitwise 操作,後者行為是實作定義的 ## 陣列 - 善用 C99 陣列初始化,如: ```c /* 下例是 tcp 的有限狀態機 */ static const uint8_t tcp_fsm[TCP_NSTATES][2][TCPFC_COUNT] = { [TCPS_CLOSED] = { [FLOW_FORW] = { /* Handshake (1): initial SYN. */ [TCPFC_SYN] = TCPS_SYN_SENT, }, }, ... } ``` ## 巨集 - 應將整個巨集放置在小括號內 - 當單個巨集內有多個陳述式,用 `do-while(0)` 包裝 - 對於巨集參數,使用時放置在小括號內 - 對於巨集參數,**避免** 多餘一次的使用,以防多次操作 (可以用區域變數承接結果,之後調用該區域變數) - ==禁止== 在巨集使用流程控制,如 `return` - 對固定位元寬度的常數,應使用 `UINTXX_C` 或 `INTXX_C` 包起來,請考量該常數的應用場景決定是否是 `signed` - 若使用巨集定義 10 進位數值常數(推薦使用 `const` ),在數字後添加 `U` 使其被視為 `unsigned` :::info :question: 看不懂,如果常數是負數為何要加上 `U`? ::: :::success C11 規格書 §6.3.1.8 中提到 `signed` 和 `unsigned` 整數的運算轉換規則: 1. 先進行整數提升 (integer promotions, 見下),將 `char`、`short` 等較小的類別轉換 `int` 或者是 `unsigned int` 1. 兩者同型別時停止轉換 1. 兩者的 `sign` 相同時,會轉型成兩者中較高階者 (greater integer conversion rank) 1. 兩者的 `sign` 不同時,做以下判斷: 1. `unsigned` 階級 $\ge$ `signed` 階級: 使用 `unsigned` 的類型 1. `signed` 可表達 `unsigned` 所有值: 轉換成 `signed` 的類型 1. 否則,轉換為 `unsigned` 所屬類型 整數提升定義參考 §6.3.1.1-2 > If an int can represent all values of the original type (as restricted by the width, for a bit-field), the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions^58^.) All other types are unchanged by the integer promotions. ::: - 32/64-bit: 會進入 `if`,因為 `uint16_t` 和 `int16_t` 會先被轉型成 `int`,左側相加會是 `int` 類型,再後面的 `4` (`int`) 進行比較,兩者型別一致,且都是 `signed`,所以結果符合預期 - 8/16-bit: 會導致相加部分被轉型成 `unsigned int` (`uint16_t`),因此 `-9` 透過二補數轉換變 `-9 + 2^16`。後續是 `uint16_t` 和 `int` (`int16_t`)不同類型的比較,根據 §6.5.9 節,會發生符合 §6.3.1.8 定義的轉型規則,最終的數值類型為 `uint16_t`,結果不進入 `if` 迴圈 ```c #define SOME_CONSTANT (6U) uint16_t unsigned_a = 6; int16_t signed_b = -9; if (unsigned_a + signed_b < 4) { /* This block might appear logically correct, as -9 + 6 is -3 */ ... } /* but compilers with 16-bit int may legally interpret it as (0xFFFF – 9) + 6. */ ``` - 巨集的名稱通常為大寫,除非有利閱讀或該巨集是某函式的封裝,例如 `new` : <br> ```c #define new(a, n, t) alloc(a, n, sizeof(t), _Alignof(t)) typedef struct { char *begin, *end; } arena_t; void *alloc(arena_t *a, ptrdiff_t count, ptrdiff_t size, ptrdiff_t align) { ptrdiff_t pad = -(uintptr_t)a->begin & (align - 1); assert(count < (a->end - a->begin - pad) / size); void *result = a->begin + pad; a->begin += pad + (count * size); return memset(result, 0, count * size); } ``` ## 關鍵字 - 適當使用 `static` 和 `constant`,前者尤其可應用在模組內私有函式,後者則可應用在: - 不應在初始化後修改的變數 - 結構體不應被修改的字段 - 取代 `#define` 作為數值常數 ## 函式 - 大括號在單獨行 - 參數逗號後要加上一個空格 - 若指標型函式參數不會在函式內被修改,應使用 `const type *p` 修飾 - `printf` formatter 應使用如 `PRIu64` 等格式,而非 `%ull` ## 指標 - 對於所有指標,若沒有明確初始化的值,**必須** 使用 `NULL` 顯式初始化,以避免解引用未初始化指標造成未定義行為 - 盡量 ++**不使用**++ `typedef` 為指標取別名 :::info :question: 是為避免太多指標名稱造成混亂? ::: ## 變數宣告 - for `const` - 若修飾是指標本身(非其值),則 `*` 和變數間要有空白 - 若指標具有關鍵字 `restrict`,則 `*` 和變數間要有空白 - 其餘情況,`*` 緊鄰變數 :::info clang-format 似無法做這種客製化? 嘗試目前 lab0-c 的規則不符合上述情況,會變成: ```c const char *name; /* (正確) */ conf_t *const cfg; /* (錯誤) */ const uint8_t *const charmap; /* (錯誤) */ const void *restrict key; /* (錯誤) */ ``` ::: - 對區域變數,同類型的變數應在 ==同一行== 完成宣告 - 編譯器只會對全域或靜態變數進行初始化,因此 **不要** 將上述兩者初始化為 0 - 因應 C99,區域變數應只在要使用前宣告,而非全部宣告在函式開頭,此舉對程式可讀性有益 ## 結構體 - 永遠在初始化的最後加上**逗號**,以利 clang-format 正確運行 - 用 `typedef` 為結構體命名別名時,別名應以 `_t` 結尾 - 善用 C99 中結構體指定成員初始化 (designated initializer) - 結構體應僅在標頭檔提供 ==宣告==,而在 `.c` 檔案中提供 ==定義==,避免曝露過多不必要實作細節,參考以下: <br> ```c /* private header */ #ifndef _CRYPTO_IMPL_H_ #define _CRYPTO_IMPL_H_ #if !defined(__CRYPTO_PRIVATE) #error "only to be used by the crypto modules" #endif #include "crypto.h" typedef struct crypto { crypto_cipher_t cipher; void *key; size_t key_len; ... } ... #endif ``` ```c /* public api */ #ifndef _CRYPTO_H_ #define _CRYPTO_H_ typedef struct crypto crypto_t; crypto_t *crypto_create(crypto_cipher_t); void crypto_destroy(crypto_t *); ... #endif ``` - 結構體內的佈局是實作定義的,因此對於以下情況進行討論: - 當該結構體映射至硬體或用於通訊時,應確保 **大小**、**排序**符合預期,在 C11 後,使用巨集 `static_assert` - 適當使用 `packed` :::info 下方程式編譯後 (gcc 13.3.0) 出現 `error: missing binary operator before token "("` >已於 commit [9cf7f12](https://github.com/sysprog21/lab0-c/commit/9cf7f12eb329aa73f0f78cd92305b40f00b4380d) 修正。 ```c typedef struct { uint16_t count; /* offset 0 */ uint16_t max_count; /* offset 2 */ uint16_t unused0; /* offset 4 */ uint16_t enable : 2; /* offset 6 bits 15-14 */ uint16_t interrupt : 1; /* offset 6 bit 13 */ uint16_t unused1 : 7; /* offset 6 bits 12-6 */ uint16_t complete : 1; /* offset 6 bit 5 */ uint16_t unused2 : 4; /* offset 6 bits 4-1 */ uint16_t periodic : 1; /* offset 6 bit 0 */ } mytimer_t; /* Preprocessor check of timer register layout byte count. */ #if (sizeof(mytimer_t) != 8) #error mytimer_t struct size incorrect (expected 8 bytes) #endif ``` ::: ## 流程控制 - 多使用提前返回 (early return),簡化巢狀結構 - 使用 `if-else`,不影響邏輯的情況下,越短的判斷語句應該往前放 - `if-else` 深度不宜超過 2 層 - **不要** 為簡化嵌套使邏輯變複雜 - 單行的 `if` 或 `else` 可省略大括號,而當有任何一分支需要大括號,則其他分支也應加上大括號 - 避免不必要的 `else` - `switch` 和 `case` 應在同縮排層級,且若有 case 刻意不使用 `break`,則應加上 `/*fallthrough*/` 的註解: <br> ```c switch (expr) { case A: ... break; case B: /* fallthrough */ case C: ... break; } ``` ## 可移植性 - 避免過度強調可移植性 - 不假設硬體,例如~~端序~~位元組順序、CPU 指令集,使用一致位元操作函式 - 由於有不同位元架構,8 bytes ~~數據~~資料使用 `uint64_t` or `int64_t` 固定長度類型 - ==不要假設== `char` 是 `signed`,例如 ~~ARM~~ Arm 中 `char` 預設是 `unsigned`,顯式使用 `signed char` - 避免假設未對齊的存取 (e.g., 記憶體...) 是安全的,因為在 ARM 架構上(尤其是 ARMv5 前) 是不安全的 ## 工具 - 善用靜態分析工具 - 避免在 commit 之中添加反引號 (\`) - 避免在 commit 標題中包含括號