本文档介绍了编写 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>
中的 alignas
和 alignof
,而不是 _Alignas
和 _Alignof
。
请注意,编译器支持可能会更改代码 ABI 的标志。例如,GCC 有一个 -m96bit-long-double
标志,用于更改长双精度型的大小。我们假定不使用此类标志。
最后,我们的 IDK 中的一些库(例如 Fuchsia 的 C 标准库)混合了外部定义的接口和特定于 Fuchsia 的扩展程序。在这些情况下,我们允许一些实用主义。例如,libc 定义了 thrd_get_zx_handle
和 dlopen_vmo
等函数。这些名称并不严格遵守以下规则:库的名称不是前缀。这样做会导致名称不太适合与其他函数(如 thrd_current
和 dlopen
)放在一起,因此我们允许出现例外情况。
C++
虽然 C++ 不是 C 的确切超集,但我们仍将 C 库设计为可通过 C++ 使用。Fuchsia C 头文件应与 C++11、C++14 和 C++17 标准兼容。具体而言,函数声明必须是 extern "C"
,如下所述。
不应将 C 和 C++ 接口混合在一个头文件中。请改为创建一个单独的 cpp
子目录,并将 C++ 接口放在各自的头文件中。
库布局和命名
Fuchsia C 库有名称。此名称决定了其包含路径(如库命名文档中所述)以及库中的标识符。
在本文档中,该库始终命名为 tag
,且简称为 tag
、TAG
、Tag
或 kTag
,以反映特定的词法惯例。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 | ...))
请勿添加值计数,因为随着常量集的增加,这种情况很难维护。
浮点常量
浮点常量类似于单个整数常量,区别在于前者采用不同的机制来描述类型。浮点常量必须以 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"
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
),而非大小不确定的类型(例如 int
或 unsigned 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 句柄是许多库通用资源的示例。库还可以定义自己的资源,并指定其生命周期来管理。
所有资源的所有权应明确。资源的转移应在函数名称中明确指定。例如,create
和 take
表示转移所有权的函数。
库应该有严格的内存。由 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)来表示。