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 系统的复杂程度千差万别,从本质上讲,从 sed 到基于 libclang 的复杂工具,不一而足。在做出这些决定时,我们在一定程度上考虑了对外金融机构的适用性。

语言版本

C

Fuchsia C 库根据 C11 标准编写(有少数例外情况,例如 unix 信号支持,这些例外情况与我们的 C 库 ABI 无关)。C99 合规性不是目标。

特别是,Fucsia 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 库有名称。此名称决定了其包含路径(如库命名文档中所述)以及库中的标识符。

在本文档中,该库始终命名为 tag,且简称为 tagTAGTagkTag,以反映特定的词法惯例。tag 应该是不带下划线的单个标识符。标记全小写的形式由正则表达式 [a-z][a-z0-9]* 指定。标记可以替换为较短版本的库名称,例如,可以用 zx 代替 zircon

头文件 foo.h 的包含路径(如库命名文档中所述)应为 lib/tag/foo.h

标题布局

C 库中的单个头文件包含多种内容。

  • 版权横幅
  • 标头防护装置
  • 包含的文件列表
  • 外部 C 级后卫
  • 常量声明
  • 外部符号声明
    • 包含外部函数声明
  • 静态内嵌函数
  • 宏定义

标头保护程序

在标头中使用 #ifndef 防护程序。这些按钮如下所示:

#ifndef SOMETHING_MUMBLE_H_
#define SOMETHING_MUMBLE_H_

// code
// code
// code

#endif // SOMETHING_MUMBLE_H_

定义的确切形式如下所示:

  • 采用指向标头的规范 include 路径
  • 将 .、/ 和 - 替换为 _
  • 将所有字母转换为大写
  • 在末尾添加 _

例如,位于 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 | ...))

请勿添加值计数,因为随着常量集的增加,这种情况很难维护。

浮点常量

浮点常量类似于单个整数常量,区别在于前者采用不同的机制来描述类型。浮点常量必须以 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" Guard 中。这些代码是使用 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),而非大小不确定的类型(例如 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;

函数类型定义符

允许使用函数类型的 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

库不应公开全局变量。而应提供用于操控该状态的函数。具有进程全局状态的库必须是动态链接的,而不是静态的。一种常见的模式是将库拆分为一个无状态静态部分,其中包含几乎所有代码和一个保持全局状态的小型动态库。

特别是,在新代码中应避免使用 errno 接口(这是一个全局线程局部全局接口)。

关联

库中的默认符号可见性应处于隐藏状态。请使用导出的符号的许可名单,或要导出的符号的显式可见性注解。

C 库不得导出 C++ 符号。

进化

弃用

已弃用的函数应使用 compiler.h 中的 __DEPRECATED 属性进行标记,还应该对其添加注释,说明应改为做什么,以及跟踪弃用情况的 bug。

禁止或不鼓励使用的语言功能

本部分介绍了不能或不应在 Fuchsia C 库的接口中使用的语言功能,以及禁止这些库的理由。

枚举

不允许使用 C 枚举。从 ABI 的角度来看,它们很脆弱。

  • 用于表示枚举类型的常量的整数大小取决于编译器(和编译器标记)。
  • 枚举的有符号很脆弱,因为向枚举添加负值可能会更改基础类型。

比特字段

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 扩展。

模块映射

这些是针对类 C 语言的 Clang 扩展的一部分,旨在解决标头驱动型编译中的许多问题。虽然 Fuchsia 工具链团队未来很可能会在这些方面进行投资,但我们目前不支持它们。

编译器扩展

根据定义,它们不能跨工具链移植。

这包括打包的属性或 pragma,但 DDK 有一个例外情况。

DDK 结构通常会反映与系统 ABI 不匹配的外部布局。例如,它可能引用对齐程度低于语言要求的整数字段。这可以通过编译器扩展(例如 pragma pack)来表示。