Try   HackMD

Linux2025q1: lab0-c 貢獻規定速記

本文摘錄自 CONTRIBUTING.md :

不要濫用「重點」一詞,原文沒幾行,你只是「摘錄」。

已修正。

格式

  • 基於 K&R
  • 註解使用 /* *///

命名

  • 使用 snake_case 命名法

型別

  • 大小使用 size_t
  • 對可能含錯誤碼 (< 0) 的大小,使用 ssize_t
  • 避免在迭代器變數使用固定長度的類型,如 uint8_t,應該使用 unsigned,除非有資源限制
  • 布林類型使用 bool,其他類別要轉換成 bool 應使用比較方式達成,而非強轉型
  • 對於 signed int,不應該在其中宣告 bit-field,以及對其進行 bitwise 操作,後者行為是實作定義的

陣列

  • 善用 C99 陣列初始化,如:

    ​​​​/* 下例是 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_CINTXX_C 包起來,請考量該常數的應用場景決定是否是 signed
  • 若使用巨集定義 10 進位數值常數(推薦使用 const ),在數字後添加 U 使其被視為 unsigned

    :question: 看不懂,如果常數是負數為何要加上 U?

    C11 規格書 §6.3.1.8 中提到 signedunsigned 整數的運算轉換規則:

    1. 先進行整數提升 (integer promotions, 見下),將 charshort 等較小的類別轉換 int 或者是 unsigned int
    2. 兩者同型別時停止轉換
    3. 兩者的 sign 相同時,會轉型成兩者中較高階者 (greater integer conversion rank)
    4. 兩者的 sign 不同時,做以下判斷:
      1. unsigned 階級
        signed 階級: 使用 unsigned 的類型
      2. signed 可表達 unsigned 所有值: 轉換成 signed 的類型
      3. 否則,轉換為 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 promotions58.) All other types are unchanged by the integer promotions.

    • 32/64-bit: 會進入 if,因為 uint16_tint16_t 會先被轉型成 int,左側相加會是 int 類型,再後面的 4 (int) 進行比較,兩者型別一致,且都是 signed,所以結果符合預期
    • 8/16-bit: 會導致相加部分被轉型成 unsigned int (uint16_t),因此 -9 透過二補數轉換變 -9 + 2^16。後續是 uint16_tint (int16_t)不同類型的比較,根據 §6.5.9 節,會發生符合 §6.3.1.8 定義的轉型規則,最終的數值類型為 uint16_t,結果不進入 if 迴圈
    ​​​​#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 :

    ​​​​#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);
    ​​​​}
    

關鍵字

  • 適當使用 staticconstant,前者尤其可應用在模組內私有函式,後者則可應用在:
    • 不應在初始化後修改的變數
    • 結構體不應被修改的字段
    • 取代 #define 作為數值常數

函式

  • 大括號在單獨行
  • 參數逗號後要加上一個空格
  • 若指標型函式參數不會在函式內被修改,應使用 const type *p 修飾
  • printf formatter 應使用如 PRIu64 等格式,而非 %ull

指標

  • 對於所有指標,若沒有明確初始化的值,必須 使用 NULL 顯式初始化,以避免解引用未初始化指標造成未定義行為
  • 盡量 不使用 typedef 為指標取別名

    :question: 是為避免太多指標名稱造成混亂?

變數宣告

  • for const
    • 若修飾是指標本身(非其值),則 * 和變數間要有空白
    • 若指標具有關鍵字 restrict,則 * 和變數間要有空白
    • 其餘情況,* 緊鄰變數

    clang-format 似無法做這種客製化? 嘗試目前 lab0-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 檔案中提供 定義,避免曝露過多不必要實作細節,參考以下:

    ​​​​/* 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
    
    ​​​​/* 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

    下方程式編譯後 (gcc 13.3.0) 出現 error: missing binary operator before token "("

    已於 commit 9cf7f12 修正。

    ​​​​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 層
  • 不要 為簡化嵌套使邏輯變複雜
  • 單行的 ifelse 可省略大括號,而當有任何一分支需要大括號,則其他分支也應加上大括號
  • 避免不必要的 else
  • switchcase 應在同縮排層級,且若有 case 刻意不使用 break,則應加上 /*fallthrough*/ 的註解:

    ​​​​switch (expr) {
    ​​​​case A:
    ​​​​    ...
    ​​​​    break;
    ​​​​case B:
    ​​​​    /* fallthrough */
    ​​​​case C:
    ​​​​    ...
    ​​​​    break;
    ​​​​}
    

可移植性

  • 避免過度強調可移植性
  • 不假設硬體,例如端序位元組順序、CPU 指令集,使用一致位元操作函式
  • 由於有不同位元架構,8 bytes 數據資料使用 uint64_t or int64_t 固定長度類型
  • 不要假設 charsigned,例如 ARM Arm 中 char 預設是 unsigned,顯式使用 signed char
  • 避免假設未對齊的存取 (e.g., 記憶體) 是安全的,因為在 ARM 架構上(尤其是 ARMv5 前) 是不安全的

工具

  • 善用靜態分析工具
  • 避免在 commit 之中添加反引號 (`)
  • 避免在 commit 標題中包含括號