FIDL 绑定规范

本文档是 Fuchsia 接口定义语言的规范 (FIDL) 绑定。旨在提供相关指南和最佳实践, 绑定作者,并推荐了其工效学用途的特定方法。

在本文档中,以下关键字在 RFC2119可以必须不得可选推荐必需不会 不能。

生成的代码指示

必须在机器生成的代码顶部添加注解,以表明 生成的内容。对于有关于如何指明已生成 (而不是人工编写的代码),则必须遵循该标准。

例如,在 Go 中,生成的源代码必须是 标记有一条遵循 格式的注释

// Code generated by <tool>; DO NOT EDIT.

范围

建议为机器生成的代码设置命名空间,以避免与 用户定义的符号。这可以通过使用由 Google 提供的 例如 C++ 中的命名空间、Rust 中的模块或 Go 中的软件包 Dart。如果生成的范围可以有名称,则应使用 FIDL 库名称的组件,其中包含 生成的代码,使每个 FIDL 库都存在于唯一的范围内。在 在无法限定范围且共享命名空间的情况下, (请参阅命名)。

命名

一般来说,生成的代码中使用的名称应与 FIDL 定义。以下各部分列出了可能的例外情况。

对于内嵌布局,绑定应使用由 fidlc 生成的名称,因为 它们是唯一的

绑定可以生成与 FIDL 名称的命名上下文相对应的范围名称 目标语言是否支持这一功能。例如,对于某些 FIDL:

type Outer = struct {
  middle struct {
    inner struct {};
  };
};

生成的代码允许引用与 使用作用域限定为父级命名上下文的名称的最内层 FIDL 结构(例如 (类似于 Outer::Middle::Inner)。

大小写

应进行大小写更改,以符合语言的惯用样式(例如 (采用 snake_case 或 CamelCase)。fidlc 将确保标识符的唯一性 会考虑潜在的大小写差异(请参阅 RFC-0040)。

预留的关键字和名称冲突

生成的代码必须考虑目标中的预留关键字 ,以避免在非预期语言中使用目标语言的关键字 FIDL 定义。例如,为有冲突的名称添加前缀: 下划线 _(假设所有关键字都以下划线开头)。

生成的代码必须避免生成会导致命名冲突的代码。对于 例如,在一个函数中,其参数是根据 FIDL 生成的 定义中,不得使用 与可能的生成名称冲突。

序数

方法序数

方法的序数为 64 位数字。绑定应发出这些 采用十六进制序数,即 0x60e700e002995ef8,而不是 6982550709377523448

联合和表序数

用于 uniontable 的序数从 1 开始,必须形成密集空间。 因此,这些数字通常很小,绑定应发出这些数字。 以十进制表示法表示序数。

原生类型

建议绑定使用最具体且最符合人体工程学的原生类型 在目标环境中将内置 FIDL 类型转换为原生类型时,尽可能确保 语言。例如,Dart 绑定使用 Int32List 来表示 vector<int32>:Narray<int32>:N,而不是更通用的 List<int>

生成的类型和值

持续支持

生成的代码必须生成包含 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 枚举:

  • 绑定必须提供一种方式,供用户确定枚举是否未知。 包括使之可以匹配枚举( 支持 switchmatch 或类似结构的语言)。
  • 绑定可以公开枚举的(可能未知的)底层原始值。
  • 绑定必须提供一种方式来获取有效的未知枚举,并且不包含 用户需要提供明确的未知原始原始值。如果某个 枚举成员带有 @unknown 属性注解, 则该未知枚举构造函数必须使用带注解的值 成员。否则,未知构造函数使用的值将未指定。
  • 在任何情况下,@unknown 成员都必须被视为未知成员。 函数。

结构支持

绑定必须为支持以下各项的每个结构体提供一个类型 操作:

  • 每个成员具有明确值的结构。
  • 读取和写入成员。

联盟支持

绑定必须为每个联合提供一个类型,该类型支持以下 操作:

  • 使用显式变体集的构建。不建议为绑定使用 提供没有变体的构造。只有在 效果原因或目标语言的限制。
  • 读取/写入并集的变体以及与其关联的数据 变体。

对于没有并集类型或并集值字面量的语言,建议 支持在为以下某个指定值的情况下构建新并集的工厂方法 可能的变体。例如,在类似 C 语言的语言中,这将允许将 例如:

my_union_t foo;
foo.set_variant(bar);
do_stuff(foo);

例如:

do_stuff(my_union_with_variant(bar));

这些工厂方法应命名为 [Type]-with-[Variant],并采用正确的大小写形式 目标语言。

这方面的例子适用于 HLCPPGo 绑定。

未知数据

对于灵活联合

  • 当序数未知时,解码必须成功,但重新编码必须失败。
  • 绑定必须提供一种方法来确定联合体是否具有未知变体。
  • 绑定可以提供一种方式来访问未知变体的序数。
  • 绑定可以提供构造函数来创建包含未知变体的并集。
    • 构造函数必须命名,以防止在生产代码中使用,例如 unknown_variant_for_testing()
    • 构造函数不得允许用户选择序数。
    • 拥有构造函数可防止最终开发者使用 以环岛方式(如手动解码原始字节)的形式传递未知变体。

表支持

绑定必须为每个表提供一个类型,以支持以下内容 操作:

  • 在构造中为每个成员指定值是可选的。
  • 读取和写入每个成员,包括检查给定成员是否 。这些按钮应遵循命名方案:get_[member]set_[member]、 和 has_[member](针对目标语言正确大小写)。

绑定可以为只需要指定值的表提供构造函数 。例如,在 Rust 中,可以 ::EMPTY 常量和结构体更新语法。支持 这种构建方式使用户能够编写 在表中添加新字段

未知数据

所有表格均为灵活表格。

如果存在未知字段,解码和重新编码必须成功。 重新编码必须省略未知字段。

绑定可以提供一种方式来确定表中是否包含任何未知 字段。它们可以提供一种访问序数的方法。

绑定不得提供用于创建包含未知字段的表或设置 现有表中的未知字段。

严格和灵活的类型

严格类型在遇到任何未知数据时必须无法解码。 在解码包含未知数据的值时,灵活类型必须成功。

灵活的 FIDL 类型及其在未知方面的行为的示例:

FIDL 类型 访问未知数据 对保真度进行重新编码
灵活位 原始整数 无损
灵活枚举 原始整数 无损
灵活联合 布尔值或序数 不及格
布尔值或序数 有损

一般来说,底层的未知数据可能会在解码期间被丢弃, 或者存储在解码类型中。无论是哪种情况,类型都应指明 以确定它在解码时是否遇到未知数据。请参阅枚举 support位支持union 支持表支持部分 有关这些 API 设计的具体说明。

绑定作者应优先考虑优化 strict 类型,可能以牺牲 共有 flexible 种。例如,如果这两者之间存在设计权衡点, 绑定作者应首选优化 strict 类型。

将类型从“严格”更改为“灵活”必须是可转换的

值类型和资源类型

值类型不得包含句柄,资源类型可以包含句柄。

在值类型和灵活类型之间的交互中,灵活类型 优先考虑。具体来说,解码一个灵活的值类型, 包含未知标识名的设备必须成功。

协议支持

事件

绑定必须支持处理或忽略协议中的事件。 绑定可以允许用户为某些事件指定处理逻辑,并省略 处理协议中其他一些事件的逻辑。

如果用户未指定事件的处理逻辑,则绑定 必须在收到事件后进行正常通信。也就是说, 发送用户未指定的事件不属于错误 相应的处理逻辑。

绑定应设法最大限度地减少在指定事件之间的少儿不宜行为。 处理程序和事件。

绑定必须在收到未知事件后关闭连接 严格事件

域错误类型

可以选择绑定为协议提供某种形式的特殊支持 并且其错误类型与处理 API 中的 目标语言。

例如,提供某种形式的“结果”的语言类型(即并集) 包含“成功”的类型并且出现“错误”变体),例如 Rust 的 result::Result 或 C++ 中的 fpromise::result 可以提供自动转换 接收或发送包含错误的方法响应时,在这两个类型之间来回传输 类型。

例外情况的语言可以选择使用生成的协议方法代码 引发与错误类型对应的异常。

如果无法做到这一点,生成的代码可以提供便利 用于直接响应成功响应或错误值的函数,或 用于接收错误类型的响应,以免在调用时提供样板用户代码 初始化结果联合。

错误处理

协议可以向用户显示传输错误。传输错误 分类为在原生类型和 或者作为底层传输机制的错误(针对 例如,通过调用 zx_channel_write_etc 获得的错误)。这些错误 可以包含错误状态以及任何其他诊断信息。

我们将这些传输错误定义为终端错误。文件的其余部分可能 另外还要指明终端错误等其他情况 交易 ID。

  • 编码期间出现验证错误(如果执行验证)。
  • 解码错误。
  • 底层传输机制产生的错误。

相比之下,域错误(在使用 error 语法声明的方法中)和 框架错误(flexible 双向方法中)不是终端错误。

终端错误处理

绑定必须提供拥有 底层端点。当某个连接出现终端错误时,客户端 和服务器 API 必须通过关闭底层端点来拆解连接。

由于 FIDL 的 IPC 传输模型不包含暂时性错误, 不是值,例如:再次尝试发送相同的回复。触发拆解时间 error 鼓励以这种方式使用绑定,并简化错误处理。

绑定可以提供同步客户端 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_etczx_channel_call_etc,而非其 非等效项。

在传出方向上,权限和类型信息必须根据 。具体而言,这些元数据应放置在 zx_handle_disposition_t,以便调用 zx_channel_write_etczx_channel_call_etc。这些系统调用会执行类型和权利检查 代表调用方。

在传入方向上,zx_channel_read_etczx_channel_call_etczx_handle_info_t 对象的形式提供类型和权限信息。 绑定本身必须执行适当的检查,如下所示:

假设已读取句柄 h,且该句柄在 FIDL 文件中的权限为 R

  • 如果标识名 h 缺少权利中拥有的权利,会导致出现错误。 R。如果遇到此情况,必须关闭该频道。
  • 如果标识名 h 的权利比权利 R 多,则其权利必须降为 Rzx_handle_replace

此外,如果 h 具有错误的类型,也是一种错误。频道 如果遇到此情况,必须关闭。

如需查看详细示例,请参阅句柄的生命周期

Iovec 支持

绑定可以选择使用矢量化 zx_channel_write_etczx_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

绑定可以为聚合类型(结构体、 联合和表)。即使是在支持 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。对于 双向方法,响应方可以提供发送响应的机制。
  • 更高的灵活性:将所有这些行为封装到单一类型中 因此,无需 通过仅更改响应者,对绑定用户进行破坏性更改 对象,而不是 Protocol 对象。

提供响应程序对象时,绑定应谨慎处理响应程序 不是在处理请求的线程上调用的。 对于 实例。在实践中, 实现方法是允许用户将回复者的所有权从 请求处理程序类,例如转换为异步函数的回调。

对象不一定是响应方。例如,它可以 一个不同的名称,具体取决于该方法是触发后忘记还是双向触发:

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)、类型和 确保他们享有同等的权利。