# 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 標題中包含括號