本文档介绍了用于编写在 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 的复杂工具。在做出这些决定时,我们考虑了一定的 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 库具有名称。此名称决定了其包含路径(如库命名文档中所述)以及库中的标识符。
在本文档中,该库始终命名为 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_
define 的确切形式如下:
- 获取标头的规范包含路径
- 将所有“.”、“/”和“-”替换为“_”
- 将所有字母转换为大写
- 添加了尾随下划线
例如,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,在成功时返回正值,从而使返回值过载。函数不应使用包含 zircon/errors.h 中未描述的其他值的 zx_status_t 来重载返回值。
状态:退回
最好使用 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 扩展。
模块地图
这些是 Clang 扩展的一部分,适用于类 C 语言,旨在解决基于头文件的编译存在的许多问题。虽然 Fuchsia 工具链团队很可能会在未来投资于这些方面,但我们目前不支持它们。
编译器扩展程序
根据定义,这些变量无法在工具链之间移植。
这尤其包括打包属性或编译指令,但 DDK 除外。
DDK 结构通常反映与系统 ABI 不匹配的外部布局。例如,它可能指的是一个整数字段,其对齐方式不符合语言的要求。这可以通过编译器扩展(例如 pragma pack)来表达。