本文档是 Fuchsia 接口定义语言 (FIDL) 绑定的规范。旨在为绑定作者提供指导和最佳做法,并针对他们工效学使用提出具体方法。
在本文档中,以下关键字的含义如 RFC2119 中所述:可以、必须、不得、可选、建议、必需、不会、不会、不应、不应。
生成的代码指示
注释必须放置在机器生成的代码的顶部,以表明该代码是机器生成的。对于针对如何指示生成的源代码(而不是人工编写的代码)设定了标准的语言,必须遵循该标准。
例如,在 Go 中,生成的源代码必须用注释遵循以下模式进行标记
// Code generated by <tool>; DO NOT EDIT.
界定范围
建议为机器生成的代码设置命名空间,以避免与用户定义的符号发生冲突。这可以通过语言提供的作用域结构来实现,例如 C++ 中的命名空间、Rust 中的模块或 Go 和 Dart 中的软件包。如果生成的作用域可以有名称,则应使用 FIDL 库的名称组件为其命名,该库包含所生成代码的定义,这使每个 FIDL 库都可以存在于唯一作用域中。如果无法限定范围且命名空间共享,则可能需要对生成的名称进行一些处理(请参阅命名)。
命名
通常,生成的代码中使用的名称应与 FIDL 定义中使用的名称一致。以下各部分列出了可能的例外情况。
对于内嵌布局,绑定应使用 fidlc
生成的名称,因为它们一定是唯一的。
如果目标语言支持 FIDL 名称的命名上下文,绑定可以生成与 FIDL 名称的命名上下文相对应的限定范围的名称。例如,对于某些 FIDL:
type Outer = struct {
middle struct {
inner struct {};
};
};
生成的代码将允许使用作用域限定为父命名上下文的名称来引用与最内层 FIDL 结构体对应的值(例如,在 C++ 中,类似于 Outer::Middle::Inner
)。
大小写
应进行大小写更改以符合语言的惯用样式(例如,使用蛇形命名法或驼峰命名法)。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 和 Go 中的 const
,或 Dart 中的 final
)。
位支持
绑定必须为每个位成员提供生成的值。它们还可以生成表示未设置标志的位以及设置了每个标志的位(“位掩码”)的值。这些值应限定为每组位。
建议对生成的值支持以下运算符:
- 按位运算,即
&
- 按位或,即
|
- 按位异或,即
^
- 按位非,即
~
- 按位差,即一个运算数中出现的所有位,但不在另一个运算数中出现的位。这通常由
-
运算符表示。
FIDL 按位运算的变体是,它们不应引入未知位,除非源运算数中出现相应的未知位。除 bitwise not
外,所有推荐的运算符自然都有此属性。实现 bitwise not
操作必须使用所有值的掩码进一步遮盖生成的值。在伪代码中:
~value1 means mask & ~bits_of(value1)
为方便起见,在 JSON IR 中提供此掩码值。
在支持运算符重载的语言(例如 C++)中,必须通过重载内置运算符以始终取消设置位字段的未知成员的方式实现 bitwise
not
操作。在不支持运算符重载的语言(例如 Go)中,值应提供 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,但应标记为已废弃。
枚举支持
绑定必须为每个枚举成员提供生成的值。这些值的作用域应限定为每个枚举。
如果生成的代码包含封装底层数字枚举值的类型,应该可以在原始值和封装容器类型之间进行转换。建议将此转换设为显式转换。
未知数据
对于 flexible 枚举:
- 绑定必须为用户提供一种确定枚举是否未知的方法,包括使枚举能够与枚举匹配(对于支持
switch
、match
或类似构造的语言)。 - 绑定可以公开(可能未知)枚举的底层原始值。
- 绑定必须提供一种途径来获取有效的未知枚举,而无需用户需要提供显式的未知原始原始值。如果其中某个枚举成员使用
@unknown
属性进行了注解,则此未知枚举构造函数必须使用添加了此类注解的成员的值。否则,未知构造函数使用的值未指定。 - 在任何确定值是否未知的函数中,都必须将
@unknown
成员视为未知。
结构支持
绑定必须为支持以下操作的每个结构体提供一个类型:
- 每个成员具有明确值的结构。
- 读取和写入成员。
联盟支持
绑定必须为支持以下操作的每个联合提供一个类型:
- 具有显式变体集的构造。不建议绑定在没有变体的情况下提供构造。仅出于性能原因或因目标语言的限制而考虑执行此操作。
- 读取/写入联合的变体以及与该变体关联的数据。
对于没有联合类型或联合值字面量的语言,建议支持在给定某个可能变体的值的情况下构建新联合的工厂方法。例如,在类似 C 的语言中,这将允许替换如下代码:
my_union_t foo;
foo.set_variant(bar);
do_stuff(foo);
例如:
do_stuff(my_union_with_variant(bar));
这些工厂方法应命名为 [Type]-with-[Variant]
,并针对目标语言正确大小写。
未知数据
对于灵活联合:
- 如果序数未知,解码必须成功,但重新编码必须失败。
- 绑定必须提供一种方式来确定联合体是否具有未知变体。
- 绑定可以提供一种访问未知变体序数的方式。
- 绑定可以提供构造函数来创建与未知变体的并集。
- 必须命名构造函数,以避免在正式版代码中使用相应构造函数,例如
unknown_variant_for_testing()
。 - 构造函数不得允许用户选择序数。
- 具有构造函数可防止最终开发者以环岛方式(例如通过手动解码原始字节)构建具有未知变体的联合体。
- 必须命名构造函数,以避免在正式版代码中使用相应构造函数,例如
表支持
绑定必须为支持以下操作的每个表提供一个类型:
- 为每个成员指定值可选的结构。
- 读取和写入每个成员,包括检查是否设置了给定成员。这些符号应采用以下命名方案:
get_[member]
、set_[member]
和has_[member]
,并针对目标语言正确大小写。
如果表只需要为具有值的字段指定值,绑定可以为这些表提供构造函数。例如,在 Rust 中,可以使用 ::EMPTY
常量和结构体更新语法来实现此目的。支持以这种方式构建时,用户可以编写强大的代码,使其能够有效地防止向表中添加新字段。
未知数据
所有表均为灵活表。
如果存在未知字段,解码和重新编码必须成功。 重新编码必须省略未知字段。
绑定可以提供一种方式,以便在解码期间确定表中是否包含任何未知字段。它们可以提供用于访问其序数的方法。
绑定不得提供创建包含未知字段的表或在现有表上设置未知字段的方法。
严格和灵活的类型
遇到任何未知数据时,Strict 类型必须无法解码。 对包含未知数据的值进行解码时,灵活类型必须成功。
灵活 FIDL 类型及其在未知方面的行为的示例:
FIDL 类型 | 访问未知文件 | 对保真度进行重新编码 |
---|---|---|
灵活位 | 原始整数 | 无损 |
灵活枚举 | 原始整数 | 无损 |
灵活联盟 | 布尔值或序数 | 失败 |
桌子 | 布尔值或序数 | 有损 |
一般来说,底层的未知数据可以在解码期间被舍弃,或者存储在已解码类型中。在任何一种情况下,该类型都应指明在解码时是否遇到了未知数据。如需了解有关设计这些 API 的具体指导,请参阅枚举支持、位支持、联合支持和表支持部分。
绑定作者应倾向于优化 strict
类型,但可能以牺牲 flexible
类型为代价。例如,如果两者在设计之间进行了权衡,则绑定作者应优先优化 strict
类型。
将类型从“严格”更改为“灵活”必须是可转换的。
值类型和资源类型
值类型不得包含句柄,且资源类型可以包含句柄。
在值类型和灵活类型之间的交互中,灵活类型要求优先。具体而言,解码包含未知句柄的灵活值类型必须成功。
协议支持
事件
绑定必须支持处理或忽略协议中的事件。 绑定可以允许用户指定某些事件的处理逻辑,而省略协议中一些其他事件的处理逻辑。
如果用户未指定事件的处理逻辑,则绑定必须在收到事件后继续进行正常通信。换言之,如果用户未在客户端针对相应事件指定相应处理逻辑,那么发送相应事件并不会导致错误。
绑定应设法最大限度地减少指定事件处理脚本和到达端点的事件之间的少儿不宜行为。
绑定必须在收到未知的严格事件时关闭连接。
网域错误类型
可以选择绑定为协议方法提供某种形式的特殊支持,其中错误类型与使用目标语言处理惯用错误的方式相符。
例如,提供某种形式的“result”类型(即包含“success”变体和“error”变体的联合类型)的语言(如 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
,而不是它们的非等同值。
在传出方向中,必须根据 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 的权限,则其权利必须降为 R 到
zx_handle_replace
。
此外,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)提供复制功能,因为创建句柄副本并不保证一定会成功。避免为资源类型公开复制运算符,可防止在向该类型添加句柄时因复制操作“消失”或其签名发生更改而导致的源代码损坏。
测试实用程序
绑定可以生成专门在测试期间使用的额外代码,这是可选操作。例如,绑定可以生成每个协议的桩实现,这样用户只需要在测试中执行的特定方法过于验证即可。
墓碑
绑定应支持署名(即,允许服务器发送拼音,以及允许客户端接收和处理牌照的生成代码)。
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。对于双向方法,响应器可以提供发送响应的机制。
- 更高的灵活性:通过将所有这些行为封装在一个类型中,您只需更改响应者对象,而不更改协议对象,从而可在绑定中添加或移除行为,而无需对绑定用户进行破坏性更改。
提供响应程序对象时,绑定应注意在与处理请求所在的线程不同的线程上调用响应程序。此外,响应程序的调用时间可能比请求处理时间晚得多,例如在实现处理获取模式时。在实践中,这可以通过允许用户将响应者的所有权从请求处理程序类中移出(例如移到异步函数的回调中)来实现。
该对象不一定要称为响应者。例如,根据方法是触发和忘记还是两种触发方式,此函数的名称可能有所不同:
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)和类型并确保其权限相同来比较句柄。