C 库可读性评估准则

本文档描述了要发布在 Fuchsia SDK 中 C 库的编写方法和规则。

将为 C++ 库单独编写文档。尽管 C++ 几乎是 C 的扩展,但在本文中也有一定的影响,编写 C++ 库的模式将与 C 完全不同。

本文档的大部分内容与 C 头中接口的描述有关。这不是一个完整的 C 风格指南,对于 C 源文件的内容几乎没什么可说的。这也不是文档准则(尽管公开的接口应该有很好的文档记录)。

一些 C 库具有与这些规则相矛盾的外部约束。例如,C 标准库本身不遵循这些规则。在适用的情况下,仍应遵循本文档。

目标

ABI 稳定性

一些带有稳定 ABI 的 Fuchsia 接口将作为 C 库发布。本文档的一个目标是使 Fuchsia 开发人员易于编写和维护一个稳定的 ABI。因此,我们建议不要使用 C 语言的某些特性,这些特性可能会使接口的 ABI 产生潜在问题或复杂的影响。我们也不允许非标准编译器扩展,因为我们不能假定第三方正在使用任何特定的编译器,下面描述的 DDK 有几个例外。

资源管理

本文档的部分内容描述了 C 语言中资源管理的最佳实践。这包括 resources、Zircon handles 和任何其他类型的资源。

标准化

我们也希望对 Fuchsia 的 C 库采用合理统一的标准。这尤其适用于命名方案。另一个标准化的例子是对输出参数排序。

FFI 友好性

对外部函数接口 Foreign Function Interface(FFI)友好性给予了一定的关注。许多非 C 语言支持 C 接口。从基本的 sed 到复杂的基于 libclang 的工具,这些 FFI 系统的复杂程度差别很大。在做出这些决定时,一定程度上考虑了 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 标志,它可以改变 long double 的大小。我们假定不使用这种标志。

最后,我们 中的一些库(比如 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 防护。这些看起来像:

  1. #ifndef SOMETHING_MUMBLE_H_
  2. #define SOMETHING_MUMBLE_H_
  3. // code
  4. // code
  5. // code
  6. #endif // SOMETHING_MUMBLE_H_

定义的确切形式如下:

  • 在头中使用规范的引入路径
  • _ 替换所有的 . /-
  • 将所有字母转换为大写
  • 末尾添加 _

例如,SDK 中位于 lib/tag/object_bits.h 的头应该有一个头部防护 LIB_TAG_OBJECT_BITS_H_

包含

头应该包括它们使用的内容。特别是,库中的任何公共头都应该安全地优先包含在源文件中。

库可以依赖于 C 标准库的头。

有些库还可能依赖于 POSIX 头的子集。究竟哪一个是合适的取决于未来 libc API 审查。

常量声明

库中的大多数常量都是编译时常量,通过 #define 创建。也有只读变量,通过 extern const TYPE NAME; 来声明,因为有时需要存储一个常数(特别是对一些类型的 FFI)。本节描述了如何在头中提供编译时常量。

编译时常量有几种类型。

  • 单整型常量
  • 枚举整型常量
  • 浮点型常量

单整数常量

单个整数常量在库 TAG 中有一些 NAME,其定义如下。

  1. #define TAG_NAME EXPR

其中 EXPR 具有以下形式之一(对于 uint32_t

  • ((uint32_t)23)
  • ((uint32_t)0x23)
  • ((uint32_t)(EXPR | EXPR | ...))

枚举整数常量

给一个库 TAG 中定义名为 NAME 的整数常量枚举集,一组相关的编译时常量包含以下部分。

首先,用 typedef 给该类型一个名称、一个大小和一个符号。typedef 应为显式大小的整数类型。例如,如果使用 uint32_t

  1. typedef uint32_t tag_name_t;

每个常数都有

  1. #define TAG_NAME_... EXPR

其中 EXPR 是少数几种编译时整型常量之一(总是用括号括起来):

  • ((tag_name_t)23)
  • ((tag_name_t)0x23)
  • ((tag_name_t)(TAG_NAME_FOO | TAG_NAME_BAR | ...))

不要包含太多值,因为随着常数集的增长会很难维护。

浮点常量

浮点常量类似于单整数常量,除了用不同的机制来描述以。浮点常量必须以 fF 结尾;double 常量不需要后缀;long double 常量必须以 lL 结尾。允许十六进制的浮点常量。

  1. // 浮点常量
  2. #define TAG_FREQUENCY_LOW 1.0f
  3. // double 常量
  4. #define TAG_FREQUENCY_MEDIUM 2.0
  5. // long double 常量
  6. #define TAG_FREQUENCY_HIGH 4.0L

函数声明

函数声明的名称都应该以 tag_... 开头。

函数声明应该放在 extern "C" 防护区中。这些是通过使用 compiler.h 中的 __BEGIN_CDECLS__END_CDECLS 宏来规范地提供的。

函数参数

函数参数必须命名。例如,

  1. // 不允许:缺少参数名
  2. zx_status_t tag_frob_vmo(zx_handle_t, size_t num_bytes);
  3. // 允许:所有参数都有命名
  4. zx_status_t tag_frob_vmo(zx_handle_t vmo, size_t num_bytes);

应该明确哪些参数是消费的,哪些是借用的。避免在函数调用后客户端可能拥有或可能不拥有资源的接口。如果这是不可行的,考虑在函数或参数命名中体现该所有权风险。例如:

  1. zx_status_t tag_frobinate_subtle(zx_handle_t foo);
  2. zx_status_t tag_frobinate_if_frobable(zx_handle_t foo);
  3. zx_status_t tag_try_frobinate(zx_handle_t foo);
  4. zx_status_t tag_frobinate(zx_handle_t maybe_consumed_foo);

按照惯例,输出参数在函数签名中位于最后,应该命名为 out_*

变量函数

除了 printf 类函数外,其他所有函数都应避免使用变量函数。这些函数应该用 compiler.h 中的 __PRINTFLIKE 属性记录它们的格式字符串协定。

静态内联函数

允许使用静态内联函数,并且比宏之类的函数更可取。仅内联(也就是说,不是静态的)C 函数有复杂的链接规则和很少的用例。

类型

与非显式大小的类型(例如 intunsigned long int)相比,更倾向显式大小的整数类型(例如 int32_t)。在引用 POSIX 文件描述符时,对 int 和 C 或 POSIX 头中的 size_t 等 typedef 作了排除。

如果可能,接口中提到的指针类型应该引用特定类型。这包括指向隐式的结构指针。void* 可用于引用原始内存和传递隐式的用户 cookie 的或上下文的接口。

隐式/显式类型

定义隐式结构比使用 void* 更可取。隐式结构应该声明如下:

  1. typedef struct tag_thing tag_thing_t;

显示的结构应该声明如下:

  1. typedef struct tag_thing {
  2. } tag_thing_t;

保留字段

结构中的任何保留字段都应记录保留的目的。

本文档的未来版本将指导如何在 C 接口中描述字符串参数。

匿名类型

不允许顶级匿名类型。匿名结构和联合允许在其他结构内和函数体内,因为它们不属于顶级命名空间的一部分。例如,下面包含允许的匿名联合。

  1. typedef struct tag_message {
  2. tag_message_type_t type;
  3. union {
  4. message_foo_t foo;
  5. message_bar_t bar;
  6. };
  7. } tag_message_t;

函数类型定义

允许使用函数类型的 typedef。

函数不应重载失败时的 zx_status_t 返回值和成功时的正值返回值。函数不应重载具有 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 接口(它是全局的 thread-local)。

链接

应该隐藏库中默认符号的可见性。对要暴露的符号使用要导出符号的白名单或使用显式的可见性注解。

C 库不可以导出 C++ 的符号。

演进

过时

不推荐使用的函数应该用 compiler.h 中的 __DEPRECATED 属性标记。还应该对它们用什么代替和可能导致的 bug 进行注释说明。

不允许或不鼓励的语言特性

本节描述在 Fuchsia 的 C 库接口中不能或不应该使用的语言特性,以及禁止他们的决定背后的原因。

枚举

C 枚举是被禁止的。从 ABI 的角度来看,它们是不可靠的。

  • 用于表示枚举类型常量的整数大小取决于编译器(和编译器标志)。
  • 枚举的签名是不可靠的,因为向枚举中添加负值可以更改基础类型。

位字段

C 的位字段是被禁止的。从 ABI 的角度来看,它们是不可靠的,而且有很多 nonintuitive sharp edges。

注意,这适用于 C 语言特性,而不适于暴露位标志的 API。C 位字段功能看起来像:

  1. typedef struct tag_some_flags {
  2. // Four bits for the frob state.
  3. uint8_t frob : 4;
  4. // Two bits for the grob state.
  5. uint8_t grob : 2;
  6. } tag_some_flags_t;

我们更倾向将位标志公开为编译时整数常量。

空参数列表

C 函数允许 with_empty_parameter_lists(),这与 functions_that_take(void) 不同。第一个表示“取任意数量和类型的参数”,第二个表示“取零个参数”。我们禁止空参数列表因为它太危险了。

动态数组成员

这是 C99 的特性,它允许将不完整数组声明为具有多个参数的结构的最后一个成员。例如:

  1. typedef struct foo_buffer {
  2. size_t length;
  3. void* elements[];
  4. } foo_buffer_t;

作为一个例外,DDK 结构允许在引用适合此 header-plus-payload 模式的外部布局时使用此模式。

类似 GCC 的扩展同样被禁止声明 0 大小数组成员。

模块地图

这是类 C 语言的一个 Clang 扩展,它试图解决许多头驱动编译的问题。虽然 Fuchsia 工具链团队很可能在未来对这些工具有投入,但我们目前不支持它们。

编译器扩展

根据定义,这些是不可跨工具链移植的。

这尤其包括压缩属性或 pragmas,但 DDK 有一个例外。

DDK 结构通常表示与系统 ABI 不匹配的外部布局。例如,它可以引用一个整数字段,该字段的对齐度小于语言所需的对齐度。这可以通过编译扩展来处理,比如 pragma pack。 <!—

—>