RFC-0057:无默认句柄

RFC-0057:默认不使用句柄
状态已接受
区域
  • FIDL
说明

我们提议默认禁止在 FIDL 类型声明中使用句柄,并添加一个新的关键字资源来标记允许包含句柄或其他资源类型的类型。添加或移除资源修饰符可能会导致源代码中断。

作者
提交日期(年-月-日)2020-01-16
审核日期(年-月-日)2020-01-23

“妈妈,看,没有把手!”

摘要

我们提议默认禁止在 FIDL 类型声明中使用句柄,并添加一个新关键字 resource1 来标记允许包含句柄或其他资源类型的类型。添加或移除 resource 修饰符可能会破坏源代码。

设计初衷

FIDL 的一个显著特点是支持 Zircon 句柄。句柄是内存中的 32 位整数,但会受到特殊处理:必须移动而不是复制,并且必须关闭以避免资源泄露。在考虑仅对不带手柄的纯数据有意义的特征时,这种特殊处理会导致问题。虽然 FIDL 绑定可以根据句柄的存在情况有条件地启用代码,但这样做是不合适的,因为这会破坏可扩展性保证。例如,向表添加字段通常是安全的,但添加句柄字段会破坏源代码,不仅会破坏该表,还会破坏间接包含该表的所有类型。这会促使绑定变得保守,始终假定类型可能包含句柄,即使库作者从未打算添加它们也是如此。

为了适应手柄,我们做出了一些妥协:

  • 在 Dart 中,我们曾尝试将 FIDL 编码为 JSON,但遭到了反对,因为该方法只能用于没有句柄的类型,这会影响可扩展性。最终,我们使用 MaxHandles 属性构建了该接口,但这只是一个临时解决方案,因为该属性仅适用于最外层类型,而不适用于可从该类型间接访问的所有类型。
  • 在 Rust 中,首次向某个类型添加句柄会破坏源代码,因为该类型将不再派生 Clone trait。(正确克隆句柄需要调用 zx_handle_duplicate 系统调用,该调用可能会失败。)
  • 协议的 Rust 绑定会通过可变引用获取 FIDL 对象并将句柄设为零,而不是明确获取所有权,以便之后可以重复使用没有句柄的对象。

如果我们要求库作者指明某个类型是否可能包含句柄,以及更改该指示是否会破坏源代码,则可以更安全、更人性化地处理所有这些情况。

设计

术语

FIDL 类型是类型或资源类型。资源类型包括:

  • handlehandle<H>,其中 H 是句柄子类型
  • Prequest<P>,其中 P 是协议的名称
  • 使用 resource 修饰符声明的结构体、表或联合体
  • 引用资源类型的类型别名
  • 用于封装资源类型的新类型 RFC-0052
  • T?,其中 T 是不可为 null 的资源类型
  • array<T>vector<T>,其中 T 是资源类型

所有其他类型都是值类型。

正确使用 resource 修饰符时,值类型绝不包含句柄,而资源类型现在或将来可能包含句柄。

语言

新的修饰符 resource 可应用于结构体、表和联合体声明。

如果没有 resource,声明不得包含资源类型。FIDL 编译器必须对此进行验证。它只需检查直接字段:如果 A 包含 B,并且这两个字段都未标记为资源,并且 B 包含句柄,则编译将因 B 而失败,并且无需单独显示有关 A 间接包含句柄的错误消息。

使用 resource 时,声明可以包含资源类型。它声明的新类型也被视为资源类型,即使它不包含资源也是如此。

原则上,该语言可以允许对新类型声明使用 resource RFC-0052。不过,封装值类型的资源新类型没有实际用途,因此新类型会从其封装的类型隐式继承值/资源状态。

语法

此提案修改了 FIDL 语法中的一条规则:

declaration-modifiers = "strict" | "resource" ;

JSON IR

此提案会向 "struct_declarations""table_declarations""union_declarations" 数组中的所有对象添加一个具有布尔值的键 "resource"

请注意,此键与 "max_handles" 并非重复的。值类型必须将 max_handles 设置为零,但资源类型可以有任意数量的 max_handles,因为它反映了声明的实际内容(而非库作者允许使用句柄的意图)。

绑定

此提案不包含对绑定的具体更改。不过,它可以让 FIDL 绑定作者(包括 FIDL 团队)解决动机中所讨论的问题。以下是此 FTP 支持的一些操作示例,但接受此 FTP 并不要求您执行这些操作:

  • 实现 JSON 序列化和对值类型的序列化(更有可能是 FIDL 文本格式,而不是 JSON,如 RFC-0058 中所提议)。
  • 针对值/资源类型使用不同的 C++ Clone() 方法类型签名,以强调只有资源克隆可能会失败。
  • 让 Rust 协议将值类型参数视为 &T,将资源类型参数视为 T,而不是对这两者都使用 &mut T,并且仅修改资源类型。

API 评分标准

API 评分标准应提供有关何时使用 resource 的指导。一些简单的示例:

  • 不应将没有资源类型的结构体标记为 resource,因为结构体不适合扩展(在大多数情况下,日后添加句柄会破坏 ABI)。
  • 不应将没有资源类型的严格表或联合标记为 resource,因为严格性已经表明修改其字段会破坏源代码。

它还应解决最初没有句柄的灵活表和联合的情况。例如,我们可能会建议您选择一种方式,具体取决于库的用途、库的使用范围、所用语言中破坏源代码的更改预计会产生的成本,以及其他因素。

实施策略

简要的实现步骤包括:

  • 解析 fidlc 中的 resource 关键字。
  • 迁移现有 FIDL 库以使用 resource(如需了解详情,请参阅未知)。
  • 使用测试验证 fidlc 中的值/资源类型规则。
  • resource 标志存储在 JSON IR 中,并在 fidlgen 中公开该标志。

工效学设计

由于引入了新概念,因此此提案会使 FIDL 变得更加复杂。与其他 FIDL 结构(例如“结构体”和“协议”)不同,新用户不太可能猜出“资源”的含义,因此需要从文档中了解。

我们可以争论,此提案是否会使 FIDL 语言更加符合人体工学。它有助于吸引注意力,使人关注包含句柄的声明,尤其是在实际句柄值隐藏在嵌套结构中时。任何浏览库的用户都会立即发现结构包含句柄,而不仅仅是数据。另一方面,如果需要担心是否要使用 resource 和输入关键字,可能会降低人体工学舒适度。将一个声明从值更改为资源可能会产生令人头疼的级联效应,其中许多类型都必须成为资源(不过这可以被视为一件好事,因为否则会显示为源代码损坏)。

复杂性的增加是 FIDL 绑定改进的必然结果。由于可以自由为值类型和资源类型提供不同的 API,因此可以使绑定更安全、更符合人体工学。如需查看这些改进的示例,请参阅绑定

文档和示例

需要完成以下任务:

  • 更新涉及句柄的所有文档,以便根据需要使用 resource
  • 更新了 FIDL 语言规范,以说明 resource 修饰符。
  • 在 FIDL 教程中提及 resource。应提供简短的备注来说明所有修饰符(即 strictresource)。
  • 提供有关不带句柄的新类型是否应为资源的指导。
  • 绑定利用值/资源区分后,请更新其文档,以说明值类型和资源类型提供的 API 之间的差异,并提供在两者之间转换的说明(如果可能)。

向后兼容性

此提案对 ABI 兼容性没有影响。

修订版(2021 年 7 月)。在实现过程中,我们发现此提案与 RFC-0033:处理未知字段和严格性的互动存在一个边界情况。在解码表和灵活联合体时,某些绑定会存储未知成员;如果未知成员包含句柄,则值类型无法执行此操作,因此在这种情况下,解码必须失败。如需了解详情,请参阅兼容性指南

修订版(2021 年 10 月)。RFC-0137:丢弃 FIDC 中的未知数据发布后,绑定不再存储未知数据,因此不再存在边缘情况。因此,值/资源的区分对 ABI 兼容性没有影响。

RFC-0024 的意义下,添加或移除 resource 修饰符既不兼容源代码,也无法转换2。绑定明确允许为仅在修饰符存在方面存在差异的两个类型生成不兼容的 API,事实上,可能无法编写在添加/移除修饰符之前和之后都能编译的代码。如果库作者希望以与源代码兼容的方式从 resource 迁移到/从 resource 迁移,则必须创建新类型和方法,而不是更改现有类型和方法。

当绑定作者开始利用值/资源区分时,我们会重新考虑这一决定。不妨要求使用可转换的路径(可能使用具有 [Transitional] 属性的中间阶段)。起初,这并不明确:它可能过于限制,会破坏此提案旨在实现的潜在 API 改进。

性能

此提案对构建性能的影响微不足道:FIDL 编译器需要稍微多做一些工作来解析新关键字并验证其使用情况。它对 IPC 性能没有直接影响。如果绑定使用值/资源区分来创建 API,以避免不必要的复制,则可能会在某些语言中带来一些改进。例如,您无需克隆值类型对象,即可多次发送该对象。

安全

此提案不会直接影响安全性,但可让绑定提供更安全的 API。例如,C++ 可以使用 [[nodiscard]] 对资源类型的 Clone() 强制执行错误处理,或者 Rust 可以通过移动来接受资源类型的方法参数,以防止之后意外使用经过更改的对象。这类更改可以防止 bug,包括安全 bug。

测试

此功能将通过以下方式进行测试:

  • 在 fidlc 中添加了用于解析和验证代码路径的测试。这些测试应演练各种情况,其中标记为 resource(或未标记为 resource)的声明不符合资源类型(或值类型)的定义。
  • 除了修复需要 resource 的现有声明之外,还向金标准添加了一些资源类型声明。
  • 更新了 fidl-changes 测试套件,以演示从值类型转换为资源类型以及反之的步骤。

缺点、替代方案和未知

此提案引入了一个新关键字,这会使语言变得更加复杂。关键字过多可能会造成问题;“严格的资源联合”这个词有点拗口。

此提案在以下两方面削弱了 FIDL 可扩展性保证:

  • 之前,向某个类型添加句柄预计不会破坏源代码。现在,系统明确允许并期望这种情况(除非类型已标记为 resource,以便预计需要添加句柄)。
  • 以前,可以声明一个类型,并期望未来能够 (1) 向其添加句柄,以及 (2) 将其作为字段包含在任何其他类型中。现在,库作者必须在开始时选择 (1) 或 (2)。

此提案有两个主要替代方案:

  • 不执行任何操作。允许在任何位置使用句柄,并接受以下事实:在添加或移除句柄时,绑定必须保留源代码兼容性。
  • 默认允许使用句柄。与此提案类似,但假定声明默认是资源类型,并且需要使用 value 关键字来禁止在声明中使用资源类型。

动机人体工学部分反对不采取任何行动。对于另一种替代方案,经验表明,大多数消息都不包含句柄,并且在协议中传递句柄需要谨慎和提前规划。换句话说,值类型是常见情况,事后添加手柄的功能可能并不像看起来那么实用。这表明,不允许使用句柄是更好的默认设置。

此提案主要对使用 FIDL 绑定的最终开发者有益,而其缺点则适用于设计 API 的库作者。这种权衡符合 Fuchsia API 委员会章程,该章程将最终开发者置于 API 设计者和实现者之上。

我们还建议了另一种替代方案:将句柄用作引用。它不会禁止在值类型中使用句柄,而是会通过将句柄表示为引用来解决值/资源问题。克隆包含句柄的结构体只会对同一句柄进行另一个引用。这可以在 C++ 中使用 shared_ptr 实现,并且可以大大简化事情,而无需添加 resource 关键字。不过,它也存在一些挑战:

  • 所有绑定都需要一个记账机制,以确保仅在其最后一个引用消失后关闭句柄。在某些语言中,这可能很难做到。
  • 向另一个进程发送句柄后,对该句柄的所有其他引用都会失效,就像悬空指针一样。将句柄视为普通值的便利性意味着,在这些情况下,编译时安全性会降低。
  • 由于这涉及更改所有手柄的类型,因此在所有语言中都可能会是一个破坏性更改。要想顺利过渡,需要付出大量努力。

此提案仍有几个悬而未决的问题:

  • 我们应如何迁移现有 FIDL 库?使用 resource 标记所有现有声明是安全的,但不会反映库作者的意图。仅标记最低限度(即包含句柄的类型)即可,但假定不含句柄的任何内容都永远不会包含句柄,这种假设可能过于激进。
  • 如果采用通用数据类型,此功能将如何与这些类型互动? 例如,如果我们定义了 Pair<A, B> 类型,那么在逻辑上,如果 AB 是资源类型,则 Pair<A, B> 也应是资源类型,而无需对 Pair 本身进行注解。在其他情况下,是否最好派生类型是否为资源?

在先技术和参考文档

此提案的目标是允许在更改类型的值/资源状态时发生破坏源代码的更改。RFC-0024 与此目标相关,因为它为 FIDL 建立了源代码兼容性标准。它还提到了导致在 Rust 中使用 Clone trait 变得困难的句柄问题,而该提案解决了这个问题。

我们不清楚是否有其他 IPC 系统可以解决这个确切问题(区分可能包含句柄或系统资源的类型)。不过,以“感染”所有使用情形的方式为类型添加注解的概念在编程语言中很常见。例如,JavaScript、Python 和 Rust 中的异步函数以及 Haskell 中的 IO monad 都具有这种行为。


  1. 此提案的早期版本则调用了关键字 entity。 

  2. 此提案的早期版本要求更改可过渡。