C 程式庫可讀性評分量表

本文件說明編寫在 Fuchsia SDK 中發布的 C 程式庫的經驗法則和規則。

系統會為 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>alignasalignof,而非 _Alignas_Alignof

請注意,編譯器支援的標記可能會改變程式碼的 ABI。舉例來說,GCC 的 -m96bit-long-double 旗標會修改長雙精度的大小。我們假設未使用這類旗標。

最後,我們 IDK 中的部分程式庫 (例如 Fuchsia 的 C 標準程式庫) 是外部定義的介面和 Fuchsia 專用的擴充功能。在這種情況下,我們會允許一些實用做法。舉例來說,libc 定義了 thrd_get_zx_handledlopen_vmo 等函式。這些名稱並非嚴格遵守以下規則:程式庫名稱不是前置字串。這樣做可以避免名稱緊貼在其他函式 (例如 thrd_currentdlopen) 旁,因此允許例外狀況。

C++

雖然 C++ 並非確切的 C 超集合,但我們仍會設計 C 程式庫,以供 C++ 使用。Fuchsia C 標頭應與 C++11、C++14 和 C++17 標準相容。請特別注意,函式宣告必須是 extern "C",如下所述。

C 和 C++ 介面不得混合在同一個標頭中。請改為建立單獨的 cpp 子目錄,並將 C++ 介面放在專屬的標頭中。

程式庫版面配置和命名

Fuchsia C 程式庫有名稱。此名稱決定其包含路徑 (如程式庫命名文件所述) 以及程式庫中的 ID。

在本文件中,程式庫一律命名為 tag,且為了反映特定詞法慣例,一般稱為 tagTAGTagkTagtag 應為不含底線的單一 ID。標記的全小寫形式是由規則運算式 [a-z][a-z0-9]* 提供。標記可替換為較短的程式庫名稱版本,例如 zx 而非 zircon

程式庫命名文件所述,標頭 foo.h 的 include 路徑應為 lib/tag/foo.h

標頭版面配置

C 程式庫中的單一標頭包含幾種內容,

  • 版權橫幅
  • 守護車
  • 檔案納入項目清單
  • 前 C 後衛
  • 常數宣告
  • 外文符號宣告
    • 包含 Extern 函式宣告
  • 靜態內嵌函式
  • 巨集定義

標頭護具

在標頭中使用 #ifndef 防護 ,如下所示:

#ifndef SOMETHING_MUMBLE_H_
#define SOMETHING_MUMBLE_H_

// code
// code
// code

#endif // SOMETHING_MUMBLE_H_

確切的定義形式如下:

  • 以標準包含路徑前往標頭
  • 將所有 .、/ 和 - 替換為 _
  • 將所有字母轉換成大寫
  • 新增結尾 _

例如,位於 lib/tag/object_bits.h 的 SDK 標頭應含有標頭防護機制 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 | ...))

請勿加入值的數量,因為隨著常數組合的增加,值會變得難以維護。

浮點常數

浮點常數與單一整數常數類似,但會使用不同的機制來描述類型。浮點常數必須以 fF 結尾;雙常數沒有字尾;長雙常數必須以 lL 結尾。可使用十六進位浮點數版本。

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

Variadic 函式

除了類似 printf 的函式之外,應避免使用 Variadic 函式。這些函式應使用 compiler.h__PRINTFLIKE 屬性,記錄格式字串合約。

靜態內嵌函式

可使用靜態內嵌函式,且建議使用類似函式的巨集。僅限內嵌 (也不是 static) C 函式有複雜的連結規則,而且用途不多。

類型

最好將大小明確且大小的整數類型 (例如 int32_t) 優先用於非明確大小的類型 (例如 intunsigned long int)。在提及 POSIX 檔案描述元時,適用於 int 的豁免項目,以及 C 或 POSIX 標頭中的 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;

函式 typedefs

允許使用函式類型的 Typedef。

針對失敗和正成功值,函式不應使用 zx_status_t 超載傳回值。函式不應含有 zx_status_t 的超載值,且該值包含 zircon/errors.h 中未說明的其他值。

狀態退貨

建議使用 zx_status_t 做為傳回值,以描述與 Zircon 基元和 I/O 相關的錯誤。

資源管理

程式庫可在數種資源中投放流量。記憶體和 Zircon 控制代碼是許多程式庫中常見的資源範例。程式庫也可以定義自己的資源及生命週期管理。

所有資源的擁有權都應該要清楚明確。移轉資源應在函式名稱中明確。例如,createtake 代表轉移擁有權的函式。

程式庫應綁定記憶體。tag_thing_create 這類函式分配的記憶體應透過 tag_thing_destroy 或某些方式釋出,而不是透過 free 釋出。

程式庫不應公開全域變數。請改為提供函式來操控該狀態。具有 process-global 狀態的程式庫必須以動態方式連結,而非以靜態方式連結。常見的做法是將程式庫分割成無狀態的靜態部分,其中包含幾乎所有的程式碼,以及一個保存全域狀態的小型動態程式庫。

請特別注意,在新程式碼中應避免使用 errno 介面 (即全域執行緒本機全域)。

連結

系統會隱藏程式庫中的預設符號瀏覽權限。使用已匯出符號的許可清單,或對要匯出的符號上明確顯示瀏覽權限註解。

C 程式庫不得匯出 C++ 符號。

演進

淘汰

已淘汰的函式應以 compiler.h 的 __DEPRECATED 屬性標示。此外,這些函式也應該加註說明做法,以及追蹤淘汰項目的錯誤說明。

不允許或不建議的語言功能

本節說明不得或不應在 Fuchsia 的 C 程式庫介面中使用的語言功能,以及禁止使用這些功能的理由。

列舉

C 列舉已遭禁。從 ABI 的角度出發

  • 用於代表列舉類型常數的整數大小取決於編譯器 (和編譯器標記)。
  • 列舉的正負號並不容易,因為在列舉中新增負數可能會變更基礎類型。

位元欄位

C 的 位元欄位遭禁。這些項目與 ABI 的意思相當夠脆,而且有很多非直覺的尖銳邊緣。

請注意,這適用於 C 語言功能,而非提供位元標記的 API。Cbitfield 功能如下所示:

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 允許使用與 functions_that_take(void) 區分的 with_empty_parameter_lists() 函式。第一個方式是「擷取任何數量和類型的參數」,第二個則代表「採用零參數」。我們會因為太過危險而停用空白參數清單。

彈性陣列成員

這是 C99 功能,可允許將未完成的陣列宣告為含有多個參數的結構體中的最後一個成員。例如:

typedef struct foo_buffer {
    size_t length;
    void* elements[];
} foo_buffer_t;

做為例外狀況,當 DDK 結構在參照符合此標頭-加號酬載模式的外部版面配置時,即可使用此模式。

同樣地,也不允許宣告 0 大小陣列成員的類似 GCC 擴充功能。

模組地圖

這些是 Clang 擴充功能中 Clang 擴充功能的一部分,可嘗試解決標頭導向編譯的許多問題。雖然 Fuchsia 工具鍊團隊日後極有可能投資這類工具,但我們目前並未提供相關支援。

編譯器擴充功能

基本上,這些元件的定義不適用於跨工具鍊。

尤其是封裝屬性或 pragmas,但 DDK 有一項例外狀況。

DDK 結構通常會反映與系統 ABI 不符的外部版面配置。例如,它所參照的整數欄位,可能不符合語言的要求。這可以透過 pragma 套件等編譯器擴充功能表示。