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 的复杂工具。在做出这些决定时,我们对 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> 中的 alignasalignof,而不是 _Alignas_Alignof

请注意,编译器支持可能会更改代码 ABI 的标志。例如,GCC 有一个 -m96bit-long-double 标志,用于更改 long double 的大小。我们假定未使用此类标志。

最后,我们的 IDK 中的某些库(例如 Fuchsia 的 C 标准库)是外部定义的接口和 Fuchsia 特定扩展程序的混合体。在这些情况下,我们允许一些实用主义。例如,libc 定义了 thrd_get_zx_handledlopen_vmo 等函数。这些名称并不严格符合以下规则:库的名称不是前缀。这样做会使这些名称与 thrd_currentdlopen 等其他函数不太匹配,因此我们允许这些例外情况。

C++

虽然 C++ 不是 C 的确切超集,但我们仍将 C 库设计为可从 C++ 使用。Fuchsia C 头文件应与 C++17、C++20 和 C++23 标准兼容。具体而言,函数声明必须是 extern "C", 如下所述。

C 和 C++ 接口不应混合在一个头文件中。相反,请创建一个单独的 cpp 子目录,并将 C++ 接口放在其自己的头文件中。

库布局和命名

Fuchsia C 库有一个名称。此名称决定了其 include 路径 (如库命名文档中所述)以及库中的标识符 。

在本文档中,库始终命名为 tag,并以 tagTAGTagkTag 的形式引用,以反映特定的词法约定。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 | ...))

请勿包含值的计数,因为随着常量集的增长,该计数很难维护。

浮点常量

浮点常量与单个整数常量类似,只不过使用不同的机制来描述类型。浮点 常量必须以 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" 保护中。这些 保护通常通过使用 __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),而不是非显式大小的类型(例如 intunsigned 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 句柄是许多库中常见的资源示例。库还可以定义自己的资源,并管理其生命周期。

所有资源的所有权都应明确。资源转移应在函数名称中明确说明。例如,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 结构在引用符合此头文件加有效负载模式的外部布局时,允许使用此模式。

同样禁止使用类似的 GCC 扩展程序来声明大小为 0 的数组成员。

模块映射

这些是 Clang 对类 C 语言的扩展的一部分,旨在解决许多与头文件驱动的编译相关的问题。虽然 Fuchsia 工具链团队很可能会在未来投资这些内容,但我们目前不支持它们。

编译器扩展程序

根据定义,这些扩展程序无法跨工具链移植。

这尤其包括打包属性或编译指示,但 DDK 有一个例外。

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