本文档介绍了用于编写在 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 代码可以使用 <threads.h> 和
<stdatomic.h> 头文件,以及
_Thread_local 和对齐语言功能。
线程本地变量应使用 thread_local 拼写,而不是内置的 _Thread_local。<threads.h>同样,
最好使用 <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 和 C++23 标准兼容。具体而言,函数声明必须是 extern "C",
如下所述。
C 和 C++ 接口不应混合在一个头文件中。相反,请创建一个单独的 cpp 子目录,并将 C++ 接口放在其自己的头文件中。
库布局和命名
Fuchsia C 库有一个名称。此名称决定了其 include 路径 (如库命名文档中所述)以及库中的标识符 。
在本文档中,库始终命名为 tag,并以
tag 或 TAG 或 Tag 或 kTag 的形式引用,以反映特定的词法约定。tag 应为不带下划线的单个标识符。标记的全小写形式由正则表达式 [a-z][a-z0-9]* 给出。 标记可以替换为库名称的较短版本,例如 zx 而不是 zircon。
如 库
命名文档中所述,头文件 foo.h 的 include 路径应为 lib/tag/foo.h。
头文件布局
C 库中的单个头文件包含几种内容。
- 版权横幅
- 头文件保护
- 文件包含列表
- Extern C 保护
- 常量声明
- 外部符号声明
- 包括外部函数声明
- 静态内联函数
- 宏定义
头文件保护
在头文件中使用 #ifndef 保护。这些保护如下所示:
#ifndef SOMETHING_MUMBLE_H_
#define SOMETHING_MUMBLE_H_
// code
// code
// code
#endif // SOMETHING_MUMBLE_H_
define 的确切形式如下:
- 获取头文件的规范 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" 保护中。这些
保护通常通过使用 __BEGIN_CDECLS 和
__END_CDECLS 宏从 compiler.h 提供。
函数形参
函数形参必须命名。例如,
// 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 等 typedef 时,int 例外。
如果可能,接口中提及的指针类型应引用特定类型。这包括指向不透明结构的指针。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 结构在引用符合此头文件加有效负载模式的外部布局时,允许使用此模式。
同样禁止使用类似的 GCC 扩展程序来声明大小为 0 的数组成员。
模块映射
这些是 Clang 对类 C 语言的扩展的一部分,旨在解决许多与头文件驱动的编译相关的问题。虽然 Fuchsia 工具链团队很可能会在未来投资这些内容,但我们目前不支持它们。
编译器扩展程序
根据定义,这些扩展程序无法跨工具链移植。
这尤其包括打包属性或编译指示,但 DDK 有一个例外。
DDK 结构通常反映与系统 ABI 不匹配的外部布局。例如,它可能引用一个整数字段,该字段的对齐方式低于语言要求的对齐方式。这可以通过编译器扩展程序(例如 pragma pack)来表示。