本文說明編寫 C 程式庫的啟發式方法和規則,這些程式庫會發布在 Fuchsia SDK 中。
C++ 程式庫會另外撰寫文件。C++ 幾乎是 C 的擴充功能,且對本文有部分影響,但編寫 C++ 程式庫的模式與 C 截然不同。
本文件的大部分內容都與 C 標頭中的介面說明有關。這並非完整的 C 樣式指南,而且很少提及 C 來源檔案的內容。這也不是文件評分標準 (但公開介面應妥善記錄)。
部分 C 程式庫有與這些規則相衝突的外部限制。舉例來說,C 標準程式庫本身就不遵守這些規則。在適用情況下,仍應遵循這份文件。
目標
ABI 穩定性
部分具有穩定 ABI 的 Fuchsia 介面會發布為 C 程式庫。這份文件的目標之一,是讓 Fuchsia 開發人員輕鬆編寫及維護穩定的 ABI。因此,我們建議不要使用 C 語言的特定功能,因為這些功能可能會對介面的 ABI 造成令人意外或複雜的影響。我們也不允許使用非標準的編譯器擴充功能,因為我們無法假設第三方使用任何特定編譯器,但 DDK 有少數例外情況,詳情請參閱下文。
資源管理
本文部分內容說明 C 語言的資源管理最佳做法。包括資源、Zircon 控制代碼和任何其他類型的資源。
標準化
我們也希望為 Fuchsia C 程式庫採用合理一致的標準。命名架構尤其如此。輸出參數排序是標準化的另一個例子。
FFI 友善程度
我們對外部函式介面 (FFI) 的友善程度給予一定程度的重視。許多非 C 語言都支援 C 介面。這些 FFI 系統的複雜程度差異極大,從本質上來說,到以 libclang 為基礎的複雜工具都有。這些決策已考量到 FFI 友善程度。
語言版本
C
Fuchsia C 程式庫是根據 C11 標準編寫 (有一小部分例外狀況,例如 Unix 訊號支援,與我們的 C 程式庫 ABI 並不特別相關)。C99 合規性並非目標。
具體來說,Fuchsia C 程式碼可以使用 C11 標準程式庫的 <threads.h> 和 <stdatomic.h> 標頭,以及 _Thread_local 和對齊語言功能。
執行緒區域變數應使用 <threads.h> 中的 thread_local 拼字,而非內建的 _Thread_local。同樣地,請優先使用 <stdalign.h> 中的 alignas 和 alignof,而非 _Alignas 和 _Alignof。
請注意,編譯器支援可能會變更程式碼 ABI 的標記。舉例來說,GCC 有一個 -m96bit-long-double 旗標,可變更 long double 的大小。我們假設您未使用這類標記。
最後,IDK 中的某些程式庫 (例如 Fuchsia 的 C 標準程式庫) 混合了外部定義的介面和 Fuchsia 專屬擴充功能。在這些情況下,我們允許一些實用主義。舉例來說,libc 定義了 thrd_get_zx_handle 和 dlopen_vmo 等函式。這些名稱並未嚴格遵守下列規則:資料庫名稱不是前置字元。這樣一來,這些名稱就比較不適合與 thrd_current 和 dlopen 等其他函式並列,因此我們允許例外狀況。
C++
雖然 C++ 並非 C 的確切超集,但我們仍將 C 程式庫設計為可從 C++ 使用。Fuchsia C 標頭應與 C++17 和 C++20 標準相容。特別是函式宣告必須是 extern "C",如下所述。
C 和 C++ 介面不應混用在同一個標頭中。請改為建立個別的 cpp 子目錄,並將 C++ 介面放在各自的標頭中。
程式庫版面配置和命名
Fuchsia C 程式庫有名稱。這個名稱會決定程式庫的包含路徑 (如程式庫命名文件所述),以及程式庫中的 ID。
本文一律將程式庫命名為 tag,並以 tag、TAG、Tag 或 kTag 表示,以反映特定詞彙慣例。tag 應為單一 ID,且不得包含底線。標記的小寫形式由規則運算式 [a-z][a-z0-9]* 提供。標記可以替換為較短的程式庫名稱,例如 zx 而不是 zircon。
如程式庫命名文件所述,標頭 foo.h 的包含路徑應為 lib/tag/foo.h。
標題版面配置
C 程式庫中的單一標頭包含幾種項目。
- 著作權橫幅
- 標頭防護措施
- 檔案包含項目清單
- Extern C guards
- 常數宣告
- 外部符號宣告
- 包括外部函式宣告
- 靜態內嵌函式
- 巨集定義
標頭防護
在標頭中使用 #ifndef 防護措施。這些看起來像:
#ifndef SOMETHING_MUMBLE_H_
#define SOMETHING_MUMBLE_H_
// code
// code
// code
#endif // SOMETHING_MUMBLE_H_
定義的確切形式如下:
- 將標準包含路徑帶到標頭
- 將所有「.」、「/」和「-」替換為「_」
- 將所有字母轉換為大寫
- 新增尾端 _
舉例來說,SDK 中位於 lib/tag/object_bits.h 的標頭應有標頭防護 LIB_TAG_OBJECT_BITS_H_。
包含項目
標頭應包含他們使用的內容。具體來說,程式庫中的任何公開標頭都應可安全地先納入來源檔案。
程式庫可以依附 C 標準程式庫標頭。
部分程式庫也可能依附於 POSIX 標頭的子集。哪些項目合適,則有待即將進行的 libc API 審查。
常數定義
程式庫中的大多數常數都是編譯時間常數,透過 #define 建立。此外,還有透過 extern const TYPE NAME; 宣告的唯讀變數,因為有時需要儲存常數 (特別是某些形式的 FFI)。本節說明如何在標頭中提供編譯時間常數。
編譯時間常數的類型有很多種。
- 單一整數常數
- 列舉整數常數
- 浮點常數
單一整數常數
單一整數常數在程式庫 TAG 中具有一些 NAME,其定義如下所示。
#define TAG_NAME EXPR
其中 EXPR 採用下列其中一種形式 (適用於 uint32_t)
((uint32_t)23)((uint32_t)0x23)((uint32_t)(EXPR | EXPR | ...))
列舉整數常數
假設程式庫 TAG 中有名為 NAME 的整數常數列舉集,相關的編譯時間常數集包含下列部分。
首先,typedef 會為型別提供名稱、大小和正負號。typedef 應為明確大小的整數型別。舉例來說,如果使用 uint32_t:
typedef uint32_t tag_name_t;
每個常數的格式隨後會變成
#define TAG_NAME_... EXPR
其中 EXPR 是少數幾種編譯時間整數常數類型之一 (一律以半形括號括住):
((tag_name_t)23)((tag_name_t)0x23)((tag_name_t)(TAG_NAME_FOO | TAG_NAME_BAR | ...))
請勿加入值計數,因為隨著常數集增加,這類計數會難以維護。
浮點常數
浮點常數與單一整數常數類似,但描述類型時會使用不同的機制。浮點常數必須以 f 或 F 結尾;雙精度常數沒有後置字元;長雙精度常數必須以 l 或 L 結尾。允許使用浮點常數的十六進位版本。
// A float constant
#define TAG_FREQUENCY_LOW 1.0f
// A double constant
#define TAG_FREQUENCY_MEDIUM 2.0
// A long double constant
#define TAG_FREQUENCY_HIGH 4.0L
函式宣告
函式宣告的名稱開頭都應為 tag_...。
函式宣告應放在 extern "C" 防護措施中。這些巨集是透過 compiler.h 中的 __BEGIN_CDECLS 和 __END_CDECLS 巨集以標準方式提供。
函式參數
函式參數必須命名。例如:
// Disallowed: missing parameter name
zx_status_t tag_frob_vmo(zx_handle_t, size_t num_bytes);
// Allowed: all parameters named
zx_status_t tag_frob_vmo(zx_handle_t vmo, size_t num_bytes);
應清楚說明哪些參數會耗用,哪些參數會借用。請避免使用介面,因為用戶端在函式呼叫後可能擁有或不擁有資源。如果無法這麼做,請考慮在函式名稱或其中一個參數中,註明擁有權危害。例如:
zx_status_t tag_frobinate_subtle(zx_handle_t foo);
zx_status_t tag_frobinate_if_frobable(zx_handle_t foo);
zx_status_t tag_try_frobinate(zx_handle_t foo);
zx_status_t tag_frobinate(zx_handle_t maybe_consumed_foo);
按照慣例,輸出參數會放在函式簽章的最後,且應命名為 out_*。
可變長度引數函式
除了類似 printf 的函式,其他所有函式都應避免使用可變長度引數函式。這些函式應使用 compiler.h 中的 __PRINTFLIKE 屬性,記錄格式字串合約。
靜態內嵌函式
允許使用靜態內嵌函式,且這類函式比類似函式的巨集更合適。僅限內嵌 (也就是不含 static) 的 C 函式具有複雜的連結規則,且應用實例不多。
類型
請優先使用明確大小的整數型別 (例如 int32_t),而非不明確大小的型別 (例如 int 或 unsigned long int)。如果參照 POSIX 檔案描述元,則 int 可做為例外狀況,C 或 POSIX 標頭中的 typedef (例如 size_t) 也是如此。
如有可能,介面中提及的指標類型應參照特定類型。包括指向不透明結構體的指標。void* 可用於參照原始記憶體,以及傳遞不透明使用者 Cookie 或環境的介面。
不透明/明確型別
建議您定義不透明的結構體,而非使用 void*。不透明結構體應宣告如下:
typedef struct tag_thing tag_thing_t;
公開結構體應宣告如下:
typedef struct tag_thing {
} tag_thing_t;
保留欄位
結構體中的任何保留欄位都應記錄保留用途。
這份文件的未來版本將提供指引,說明如何在 C 介面中描述字串參數。
匿名型別
系統不允許使用頂層匿名型別。匿名結構體和聯集可位於其他結構體和函式主體內,因為這時它們不屬於頂層命名空間。舉例來說,下列內容包含允許使用的匿名聯集。
typedef struct tag_message {
tag_message_type_t type;
union {
message_foo_t foo;
message_bar_t bar;
};
} tag_message_t;
函式 typedef
允許使用函式類型的 Typedef。
函式不應在失敗時以 zx_status_t 多載傳回值,並傳回正數成功值。函式不應使用包含其他值的 zx_status_t,使回傳值過載,這些值未在 zircon/errors.h 中說明。
狀態:退貨
建議使用 zx_status_t 做為傳回值,說明與 Zircon 基本型別和 I/O 相關的錯誤。
資源管理
程式庫可以放送多種資源。記憶體和 Zircon 控制代碼是許多程式庫通用的資源。程式庫也可以定義自己的資源,並管理生命週期。
所有資源的擁有權都應明確無誤。函式名稱應明確指出資源轉移作業。舉例來說,create 和 take 代表轉移擁有權的函式。
程式庫應節省記憶體。透過 tag_thing_create 等函式分配的記憶體應透過 tag_thing_destroy 或類似函式釋放,而非 free。
程式庫不應公開全域變數。請改為提供可操控該狀態的函式。具有程序全域狀態的程式庫必須動態連結,而非靜態連結。常見模式是將程式庫分割為無狀態的靜態部分 (包含幾乎所有程式碼),以及保存全域狀態的小型動態程式庫。
特別是新程式碼應避免使用 errno 介面 (這是全域執行緒本機全域)。
連結
媒體庫中的預設符號瀏覽權限應為隱藏。使用匯出符號的允許清單,或匯出符號的明確可見性註解。
C 程式庫不得匯出 C++ 符號。
演化
淘汰
已淘汰的函式應使用 compiler.h 中的 __DEPRECATED 屬性標示,並加上註解,說明替代做法和追蹤淘汰作業的錯誤。
不允許或不建議使用的語言功能
本節說明介面中無法或不應使用的語言功能,以及禁止使用這些功能的理由。
列舉
禁止使用 C 列舉。從 ABI 的角度來看,這些項目相當脆弱。
- 用來表示列舉型別常數的整數大小取決於編譯器 (和編譯器旗標)。
- 列舉的帶正負號性質很脆弱,因為在列舉中加入負值可能會變更基礎型別。
Bitfields
禁止使用 C 的位元欄位。從 ABI 的角度來看,它們很脆弱,而且有許多不直覺的尖銳邊緣。
請注意,這項功能適用於 C 語言功能,而非公開位元旗標的 API。C 位元欄位功能如下所示:
typedef struct tag_some_flags {
// Four bits for the frob state.
uint8_t frob : 4;
// Two bits for the grob state.
uint8_t grob : 2;
} tag_some_flags_t;
我們偏好將位元旗標公開為編譯時間整數常數。
空白參數清單
C 允許使用函式 with_empty_parameter_lists(),這與 functions_that_take(void) 不同。第一個表示「接受任意數量和類型的參數」,第二個則表示「接受零個參數」。我們禁止使用空白參數清單,因為這太危險了。
彈性陣列成員
這是 C99 功能,可讓您將不完整的陣列宣告為具有多個參數的結構體最後一個成員。例如:
typedef struct foo_buffer {
size_t length;
void* elements[];
} foo_buffer_t;
例外狀況是,DDK 結構參照符合此標頭加酬載模式的外部版面配置時,可以使用此模式。
同樣地,宣告大小為 0 的陣列成員時,也不允許使用類似的 GCC 擴充功能。
模組地圖
這些是 Clang 對類似 C 語言的擴充功能,嘗試解決標頭導向編譯的許多問題。雖然 Fuchsia 工具鍊團隊很可能在未來投入這些項目,但我們目前不支援這些項目。
編譯器擴充功能
根據定義,這些項目無法在工具鍊之間移植。
這特別包括封裝屬性或 Pragma,但 DDK 有一個例外。
DDK 結構通常會反映與系統 ABI 不符的外部版面配置。舉例來說,這可能是指與語言要求對齊程度較低的整數字段。這可以透過編譯器擴充功能 (例如 pragma pack) 表示。