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 类型是 value 类型或 resource 类型。资源类型包括:

  • 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 时,允许声明包含资源类型。它声明的新类型也被视为资源类型,即使它不包含资源也是如此。

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

语法

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

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

JSON IR

此方案将带有布尔值的键 "resource" 添加到 "struct_declarations""table_declarations""union_declarations" 数组中的所有对象。

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

绑定关系

此提案不包含对绑定的特定更改。但是,它使 FIDL 绑定作者(包括 FIDL 团队)能够解决动机中讨论的问题。以下是一些可以使用此 FTP 的示例,但不要求接受它:

  • 对值类型(或更可能是 FIDL 文本格式,而不是 JSON,而非 RFC-0058 中所建议)实现 JSON 序列化和序列化。
  • 对值/资源类型的 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: 废弃 FIDL 中的未知数据之后,绑定不再存储未知数据,因此不会再出现极端情况。因此,值/资源区别对 ABI 兼容性没有影响。

添加或移除 resource 修饰符既不符合源代码兼容,也不可转换2(按照 RFC-0024 意义)。这些绑定明确允许为两种类型(仅在修饰符存在时才有所不同)生成不兼容的 API,实际上,在添加/移除修饰符之前和之后可能无法编写会编译的代码。希望以与源代码兼容的方式过渡到 resource 的库作者必须创建新的类型和方法,而不是更改现有类型和方法。

一旦绑定作者开始利用值/资源的区别,我们将重新审视这一决定。可能有必要要求提供可转换路径(也许需要使用具有 [Transitional] 属性的中间阶段)。最初,这一点并不明确:它可能的限制过于严格,破坏了此方案旨在实现的潜在 API 改进。

性能

此方案对 build 性能的影响微乎其微:FIDL 编译器在解析新关键字并验证其使用情况时会略有增加。对 IPC 性能没有直接影响。对于某些语言,如果绑定使用值/资源区分来创建用于阻止不必要复制的 API,则可能会略有改进。例如,不应为了多次发送某个值类型的对象而克隆该对象。

安全性

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

测试

我们将通过以下方式测试此功能:

  • 在 fidlc 中添加用于解析和验证代码路径的测试。这些测试应执行各种情况,即标记为 resource(或不)的声明不符合资源类型(或值类型)的定义。
  • 除了修复需要 resource 的现有声明之外,还向 goldens 添加了一些资源类型声明。
  • 更新 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 本身添加注解。是否存在更有助于推导某个类型是否为资源的其他情况?

现有技术和参考资料

此方案的目标是允许在更改类型的值/资源状态时进行对源代码的破坏性更改。RFC-0024 与此目标相关,因为它确立了 FIDL 的源代码兼容性标准。此外,我们还探讨了句柄的问题,这使得在 Rust 中使用 Clone trait 变得困难,而此方案可以解决此问题。

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


  1. 该提案的早期版本已改为使用关键字“entity”。

  2. 此方案的早期版本要求可以转换的更改。