本文档是 Fuchsia 接口定义语言 (FIDL) 绑定的规范。它旨在为绑定作者提供指导和最佳实践,并推荐符合人体工程学的使用方法。
在本文档中,以下关键字应按照 RFC2119 中的说明进行解释:可以、必须、不得、可选、建议、必需、会、不会、应、不应。
生成的代码指示
必须在机器生成的代码顶部放置一条注释,指明该代码是由机器生成的。对于有标准来指明生成式来源(而非人工编写的代码)的语言,必须遵循该标准。
范围设置
建议为机器生成的代码设置命名空间,以避免与用户定义的符号发生冲突。这可以使用语言提供的作用域构造来实现,例如 C++ 中的命名空间、Rust 中的模块或 Dart 中的软件包。如果生成的范围可以有名称,则应使用包含所生成代码的定义的 FIDL 库名称的组件来命名,这样每个 FIDL 库都可以存在于唯一的范围内。如果无法限定范围且命名空间是共享的,则可能需要对生成的名称进行一些处理(请参阅命名)。
命名
通常,生成的代码中使用的名称应与 FIDL 定义中使用的名称一致。以下各部分列出了可能的例外情况。
对于内嵌布局,绑定应使用 fidlc 生成的名称,因为这些名称保证是唯一的。
如果目标语言支持,绑定可能会生成与 FIDL 名称的命名上下文对应的作用域名称。例如,对于某些 FIDL:
type Outer = struct {
middle struct {
inner struct {};
};
};
生成的代码允许使用限定到父级命名上下文的名称(例如,在 C++ 中,类似于 Outer::Middle::Inner)来引用与最内层 FIDL 结构体对应的值。
大小写
应更改大小写,以符合语言的惯用样式(例如,使用蛇形命名法或 CamelCase)。fidlc 将确保在考虑潜在的大小写差异的情况下强制执行标识符唯一性(请参阅 RFC-0040)。
预留关键字和名称冲突
生成的代码必须考虑目标语言中的预留关键字,以避免在 FIDL 定义中使用目标语言中的关键字时出现意外情况。一个示例方案是在冲突名称前加上下划线 _(假设没有关键字以下划线开头)。
生成的代码不得导致命名冲突。例如,在根据 FIDL 定义生成参数的函数中,生成的局部变量的名称绝不能与可能生成的名称发生冲突。
序数
方法序数
用于方法的序数是 64 位大数字。绑定应以十六进制形式发出这些序数,即 0x60e700e002995ef8,而不是 6982550709377523448。
联合和表序数
用于 union 和 table 的序数从 1 开始,并且必须形成一个密集空间。
因此,这些数字通常很小,绑定应以十进制表示法发出这些序数。
原生类型
在将内置 FIDL 类型转换为目标语言的原生类型时,建议绑定尽可能使用最具体、最符合人体工程学的原生类型。例如,Dart 绑定使用 Int32List 来表示 vector<int32>:N 和 array<int32>:N,而不是更通用的 List<int>。
生成的类型和值
持续支持
生成的代码必须为相应 FIDL 中的每个 const 定义生成包含匹配值的变量。在支持不可变变量的语言中,这些变量应标记为不可变(例如,C++ 和 Rust 中的 const,或 Dart 中的 final)。
Bits 支持
绑定必须为每个位成员提供生成的值。它们还可能生成表示未设置任何标志的位的值,以及表示设置了每个标志的位(“位掩码”)的值。这些值应限定为每组位。
建议支持对生成的值使用以下运算符:
- 按位与,即
& - 按位或,即
| - 按位异或,即
^ - 按位非,即
~ - 按位差,即一个操作数中存在的所有位,但另一个操作数中存在的位除外。这通常由
-运算符表示。
FIDL 按位运算的一个不变量是,它们不应引入未知位,除非源操作数中出现相应的未知位。
除了 bitwise not 之外,所有推荐的运算符自然都具有此属性。bitwise not 操作的实现必须使用所有值的掩码进一步遮盖生成的值。在伪代码中:
~value1 means mask & ~bits_of(value1)
为方便起见,此掩码值在 JSON IR 中提供。
在支持运算符重载的语言(例如 C++)中,bitwise
not 操作必须通过重载内置运算符来实现,并且始终取消设置位字段的未知成员。在不支持运算符重载的语言中,值应提供 InvertBits() 方法(采用最适合该语言的大小写形式)来执行掩码反转。
在清除位时,应首选按位差运算符,而不是按位非运算符,因为前者会保留未知位:
// Unknown bits in value1 are preserved.
value1 = value1 - value2
// Unknown bits in value1 are cleared, even if the user may only intend to
// clear just the bits in value2.
value1 = value1 & ~value2
绑定不应支持可能导致位值无效(或可能导致其含义不明显)的其他运算符,例如:
- 按位移位,即
<<或>> - 按位无符号移位,即
>>>
如果生成的代码包含封装底层数字位值的类型,则应该可以在原始值和封装容器类型之间进行转换。建议明确执行此转换。
绑定可以提供用于将 bits 的底层类型的原始值转换为 bits 类型本身的函数。这些转换器可能有多种类型:
- 如果输入值包含任何未知位,则可能会失败(或返回 null)。
- 从输入值中截断任何未知位。
- 仅适用于灵活位:保留输入值中的任何未知位。
未知数据
对于灵活位:
- 绑定必须提供用于检查值是否包含任何未知位的方法,此外还可以提供用于检索这些未知位的方法。
- 按位非运算符会取消设置所有未知成员,无论其先前的值如何(但对于已知成员,其行为符合预期)。其他按位运算符对未知位成员保留与已知成员相同的语义。
严格位也可以提供上述 API,以简化严格和灵活之间的转换。
在某些语言中,很难或无法阻止用户从基元手动创建 bits 类型的实例,因此无法阻止绑定设计器将严格位值限制为具有适当限制的网域。在这种情况下,绑定作者应为严格位提供未知数据相关 API。
在具有编译检查弃用警告的语言(例如 Rust)中,应为严格位提供与未知数据相关的 API,但应将其标记为已弃用。
枚举支持
绑定必须为每个枚举成员提供生成的值。这些值应限定在每个枚举的范围内。
如果生成的代码包含封装底层数字枚举值的类型,则应该可以在原始值和封装容器类型之间进行转换。建议明确执行此转换。
未知数据
对于灵活枚举:
- 绑定必须为用户提供一种方法来确定枚举是否未知,包括能够与枚举进行匹配(对于支持
switch、match或类似构造的语言)。 - 绑定可以公开枚举的(可能未知的)底层原始值。
- 绑定必须提供一种方法来获取有效的未知枚举,而无需用户提供明确的未知原始基元值。如果其中一个枚举成员带有
@unknown属性注解,则此未知枚举构造函数必须使用带注解的成员的值。否则,未知构造函数使用的值未指定。 - 在任何确定值是否未知的函数中,
@unknown成员都必须被视为未知。
结构体支持
绑定必须为支持以下操作的每个结构体提供类型:
- 使用每个成员的显式值进行构造。
- 读取和写入成员。
Union 支持
绑定必须为支持以下操作的每个联合提供一个类型:
- 使用显式变体集进行构建。不建议绑定提供不带变体的构造。只有在出于性能原因或由于目标语言的限制时,才应考虑这样做。
- 读取/写入联合体的变体以及与该变体关联的数据。
对于没有联合类型或联合值字面量的语言,建议支持工厂方法,以便在给定某个可能变体的值得情况下构造新的联合。例如,在 C 语言中,这将允许替换如下代码:
my_union_t foo;
foo.set_variant(bar);
do_stuff(foo);
替换为:
do_stuff(my_union_with_variant(bar));
这些工厂方法应命名为 [Type]-with-[Variant],并根据目标语言正确设置大小写。
HLCPP 绑定就存在这样的示例。
未知数据
对于灵活联合:
- 如果序数未知,则解码必须成功,但重新编码必须失败。
- 绑定必须提供一种方法来确定联合是否具有未知变体。
- 绑定可以提供一种方法来访问未知变体的序数。
- 绑定可以提供一个构造函数,用于创建具有未知变体的联合。
- 必须为构造函数命名,以阻止在生产代码中使用,例如
unknown_variant_for_testing()。 - 构造函数不得允许用户选择序数。
- 通过使用构造函数,可以防止最终开发者以迂回方式(例如通过手动解码原始字节)构造具有未知变体的联合。
- 必须为构造函数命名,以阻止在生产代码中使用,例如
表格支持
绑定必须为支持以下操作的每个表提供类型:
- 在构造时,为每个成员指定值是可选的。
- 读取和写入每个成员,包括检查是否设置了给定成员。这些名称应遵循以下命名方案:
get_[member]、set_[member]和has_[member],并根据目标语言正确设置大小写。
绑定可以为表提供构造函数,这些构造函数只需为具有值的字段指定值。例如,在 Rust 中,可以使用 ::EMPTY 常量以及结构体更新语法来实现此目的。通过这种方式支持构造,用户可以编写代码,从而在向表中添加新字段时保持代码的稳健性。
未知数据
所有表格都是灵活的。
当存在未知字段时,解码和重新编码必须成功。 重新编码时必须省略未知字段。
绑定可以提供一种方法,用于确定表在解码期间是否包含任何未知字段。它们可以提供一种访问其序数的方法。
绑定不得提供创建具有未知字段的表或在现有表上设置未知字段的方法。
严格和灵活的类型
严格类型在遇到任何未知数据时必须无法解码。在解码包含未知数据的值时,灵活类型必须成功。
灵活 FIDL 类型及其在未知类型方面的行为示例:
| FIDL 类型 | 访问未知内容 | 重新编码保真度 |
|---|---|---|
| 灵活位 | 原始整数 | 无损 |
| 灵活枚举 | 原始整数 | 无损 |
| 灵活联合 | 布尔值或序数 | 失败 |
| 桌子 | 布尔值或序数 | 有损 |
一般来说,底层未知数据可以在解码期间舍弃,也可以存储在解码类型中。在任一情况下,类型都应指明在解码时是否遇到未知数据。如需了解这些 API 的具体设计指南,请参阅枚举支持、位支持、联合支持和表支持部分。
绑定作者应优先优化 strict 类型,可能需要牺牲 flexible 类型。例如,如果两者之间存在设计权衡,则绑定作者应优先优化 strict 类型。
将类型从严格更改为灵活必须是可转换的。
值类型和资源类型
值类型不得包含句柄,而资源类型可以包含句柄。
在值类型和灵活类型之间的交互中,灵活类型要求优先。具体而言,解码包含未知句柄的灵活值类型必须成功。
协议支持
事件
绑定必须支持处理或忽略协议中的事件。 绑定可以允许用户为某些事件指定处理逻辑,并省略协议中某些其他事件的处理逻辑。
如果用户未指定事件的处理逻辑,则绑定在收到事件后必须继续正常通信。换句话说,如果用户未在客户端指定相应的处理逻辑,则发送事件不会出错。
绑定应尽量减少指定事件处理脚本与事件到达端点之间的竞争行为。
绑定在收到未知的严格事件时必须关闭连接。
域名错误类型
绑定可以选择为协议方法提供某种形式的特殊支持,这些方法的错误类型与目标语言中处理错误的惯用方式相匹配。
例如,如果语言提供某种形式的“结果”类型(即包含“成功”变体和“错误”变体的联合类型),例如 Rust 的 result::Result 或 C++ 中的 fpromise::result,则在接收或发送具有错误类型的方法响应时,可以提供与这些类型之间的自动转换。
对于具有异常的语言,生成的协议方法代码可以选择性地引发与错误类型对应的异常。
如果无法做到这一点,生成的代码可以提供便捷函数,以便直接返回成功响应或错误值,或者接收错误类型响应,从而避免用户编写样板代码来初始化结果联合。
错误处理
协议可能会向用户显示传输错误。传输错误可以归类为在原生类型和网络格式数据之间进行转换时遇到的错误,或底层传输机制中的错误(例如,从调用 zx_channel_write_etc 获得的错误)。这些错误可能包含错误状态以及任何其他诊断信息。
我们将这些传输错误定义为终端错误。本文档的其余部分可能会另外指定其他情况作为终止错误,例如交易 ID 不正确。
- 编码期间的验证错误(如果执行了验证)。
- 解码错误。
- 底层传输机制中的错误。
相比之下,网域错误(在使用 error 语法声明的方法中)和框架错误(在 flexible 双向方法中)不是终止错误。
终端错误处理
绑定必须提供拥有底层端点的异步客户端和服务器 API。当连接发生严重错误时,客户端和服务器 API 必须通过关闭底层端点来断开连接。
由于 FIDL 的 IPC 传输模型不包含暂时性错误,因此重试发送相同的回复等操作没有任何价值。在出错时触发拆解可鼓励这种绑定使用方式,并简化错误处理。
绑定可以提供同步客户端和服务器 API。在同步 API 中,关闭端点以处理终端错误通常需要获取锁。如果出于性能原因不希望这样做,这些 API 可以在发生终端错误时保持连接打开,并且应相应地清楚记录。异步变体应该是 API 的推荐变体。
绑定可以提供不拥有底层端点的客户端和服务器 API,以满足低级用例的需求。这些 API 无法在发生终端错误时关闭端点,因此应明确记录。所属变体应为 API 的推荐变体。
对等互连关闭特殊处理
当底层传输机制报告在消息发送期间对等端点已关闭(例如,在写入通道时收到 ZX_ERR_PEER_CLOSED 错误)时,客户端/服务器必须先读取并处理所有剩余消息,然后再向用户显示传输错误并关闭连接。
当底层传输机制在消息等待期间通知对等端点已关闭(例如,在等待通道上的信号时观察到 ZX_CHANNEL_PEER_CLOSED 信号)时,客户端/服务器必须先读取并处理所有剩余消息,然后再向用户显示传输错误并关闭连接。
这是为了与通道的读取语义保持一致:给定一对端点 A <-> B,假设将多条消息写入 B,然后关闭 B。用户可以继续从 A 读取数据,而不会观察到对等方关闭错误,直到所有延迟消息都耗尽。换句话说,在无法再从端点读取消息之前,“对等方已关闭”并非严重错误。
处理类型和权限检查
绑定必须在传入和传出方向强制执行句柄类型和权限检查。这意味着,必须使用 zx_channel_write_etc、zx_channel_read_etc 和 zx_channel_call_etc,而不是其非 etc 等效项。
在传出方向,必须根据 FIDL 定义填充权限和类型信息。具体而言,此元数据应放置在 zx_handle_disposition_t 中,以便调用 zx_channel_write_etc 或 zx_channel_call_etc。这些系统调用将代表调用方执行类型和权限检查。
在传入方向,zx_channel_read_etc 和 zx_channel_call_etc 以 zx_handle_info_t 对象的形式提供类型和权限信息。绑定本身必须执行适当的检查,如下所示:
假设读取了句柄 h,并且其在 FIDL 文件中的权限为 R:
- 如果句柄 h 缺少权限 R 中存在的权限,则会出错。如果遇到这种情况,必须关闭通道。
- 如果句柄 h 拥有的权限多于权限 R,则必须通过
zx_handle_replace将其权限减少到 R。
此外,如果 h 的类型不正确,则会出错。如果遇到这种情况,必须关闭通道。
如需查看详细示例,请参阅句柄的生命周期。
Iovec 支持
绑定可以选择使用矢量化 zx_channel_write_etc 和 zx_channel_call_etc 系统调用。使用这些时,第一个 iovec 条目必须存在,并且足够大,可以容纳 FIDL 事务性消息头(16 字节)。
属性
绑定必须支持以下属性:
@transitional
最佳做法
其他输出
绑定可以选择是否提供 FIDL 传输格式的替代输出方法。
一种输出类型可能是针对所生成类型的易于使用的调试输出。例如,输出位的值:
type Mode = strict bits {
READ = 1;
WRITE = 2;
};
可能会输出字符串 "Mode.Read | Mode.Write",而不是原始值 "0b11"。
可以为每种生成的 FIDL 类型实现类似的人性化输出。
消息内存分配
绑定可以为用户提供选项,让用户在发送或接收消息时提供自己的内存,从而控制内存分配。
传输格式内存布局
绑定可能会使生成的 FIDL 类型的内存布局与该类型的传输格式相匹配。理论上,这样做可以避免额外的副本,因为数据可以直接用作交易消息,反之亦然。在实践中,发送 FIDL 消息可能仍涉及复制步骤,其中消息的组件会组装成连续的内存块(称为“线性化”)。这种方法的缺点是,它会使绑定更加僵化:FIDL 传输格式的更改会变得更加复杂。
C++ 传输绑定是唯一采用这种方法的绑定。
等值比较
对于结构体、表和联合等聚合类型,绑定可以提供对同一类型的两个实例执行深度比较的等式运算符。不应为资源类型提供这些运算符(请参阅 RFC-0057 和深度相等),因为无法比较句柄。避免为资源类型公开相等运算符,可防止在向类型添加句柄时因相等运算“消失”而导致源代码损坏。
正在复制
对于结构体、表和联合体等聚合类型,绑定可以提供复制这些类型的实例的功能。不应为资源类型提供复制功能(请参阅 RFC-0057),因为无法保证复制句柄一定会成功。避免公开资源类型的复制运算符可防止因复制操作“消失”或在向类型添加句柄时其签名发生更改而导致源代码损坏。
测试实用程序
绑定可以选择生成专门用于测试的其他代码。例如,绑定可以生成每个协议的桩实现,以便用户只需替换将在测试中执行的特定方法。
Epitaphs
绑定应提供对墓志铭的支持,即生成的代码,允许服务器发送墓志铭,并允许客户端接收和处理墓志铭。
Setter 和 Getter
绑定可以为聚合类型(结构体、联合体和表)上的字段提供 setter 和 getter。即使在不习惯使用 getter/setter 方法的语言中,使用这些方法也可以在不破坏该字段用法的情况下重命名内部字段名称。
请求“响应者”
使用目标语言的 FIDL 绑定实现 FIDL 协议时,绑定会提供一个 API 来读取请求参数,并提供一种方法来写入响应参数(如果有)。例如,请求参数可以作为函数的实参提供,响应参数可以作为函数的返回值类型提供。
对于 FIDL 协议:
protocol Hasher {
Hash(struct {
value string;
}) -> (struct {
result array<uint8, 10>;
});
};
绑定可能会生成:
// Users would implement this interface to provide an implementation of the
// Hasher FIDL protocol
interface Hasher {
// Respond to the request by returning the desired response
hash: (value: string): Uint8Array;
};
绑定可以提供用于写入响应的响应器对象。在上面的示例中,这意味着在函数参数中传递一个额外的响应器对象,并让函数返回 void:
interface Hasher {
hash: (value: string, responder: HashResponder): void;
};
interface HashResponder {
sendResponse(value: Uint8Array);
};
使用响应器对象具有以下优势:
- 改进了工效学设计:响应器可用于提供与客户端的任何类型的交互。例如,响应器可以具有使用墓志铭关闭通道的方法,或提供用于发送事件的 API。对于双向方法,响应方可以提供发送响应的机制。
- 灵活性更高:将所有这些行为封装在单个类型中,这样就可以在不造成绑定用户破坏性更改的情况下,通过仅更改响应器对象(而不是协议对象)来添加或移除绑定中的行为。
提供响应器对象时,绑定应注意响应器在与处理请求的线程不同的线程上调用。响应器的调用时间也可能比请求的处理时间晚得多,例如在实现挂起获取模式时。在实践中,这可以通过允许用户将响应器的所有权移出请求处理程序类(例如,移入异步函数的回调)来实现。
该对象不一定被称为 responder。例如,根据方法是“即发即弃”还是双向,它可能具有不同的名称:
interface Hasher {
// the Hash method is a two-way method, so the object is called a responder
hash: (value: string, responder: HashResponder): void;
// the SetSeed method is a fire and forget method, so it gets a different name
setSeed: (seed: number, control: HasherControlHandle): void;
}
测试
GIDL 一致性测试
GIDL 是一种测试定义语言和工具,可与 FIDL 结合使用来定义一致性测试。这些测试在各种绑定中进行了标准化,可确保编码器和解码器实现的一致性以及极端情况的覆盖率。
解码对象的深度相等性
比较对象是否相等可能很棘手,在解码 FIDL 对象的情况下尤其如此。在解码期间,如果句柄具有超出所需的句柄权限,则可以对句柄调用 zx_handle_replace。发生这种情况时,初始输入句柄将关闭,系统会创建一个新句柄来替换它,并降低权限。
因此,无法直接比较句柄值。相反,可以通过检查句柄的 koid(内核 ID)、类型并确保其权限相同来比较句柄。