| RFC-0057:默认情况下不允许使用句柄 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 我们建议默认情况下不允许在 FIDL 类型声明中使用句柄,并添加新的关键字资源来标记允许包含句柄或其他资源类型的类型。添加或移除资源修饰符可能会导致源代码中断。 |
| 作者 | |
| 提交日期(年-月-日) | 2020-01-16 |
| 审核日期(年-月-日) | 2020-01-23 |
“妈妈,你看,没有句柄!”
摘要
我们建议默认情况下不允许在 FIDL 类型声明中使用句柄,并添加一
个新关键字 resource1 来标记允许包含句柄或
其他资源类型的类型。添加或移除 resource 修饰符可能会导致源代码中断。
设计初衷
FIDL 的一个显著特点是支持 Zircon 句柄。句柄是内存中的 32 位整数,但它们经过特殊处理:必须移动而不是复制,并且必须关闭以避免资源泄露。这种特殊处理在考虑仅对没有句柄的纯数据有意义的功能时会导致问题。虽然 FIDL 绑定 可以根据句柄的存在有条件地启用代码,但这样做是不可取的,因为它会破坏可演变性保证。例如,向表中添加字段通常是安全的,但添加句柄字段会成为源代码中断,不仅对于该表,而且对于所有以传递方式包含该表的类型也是如此。这促使绑定变得保守,始终假设类型可能包含句柄,即使库作者从未打算添加它们也是如此。
为了适应句柄,我们做出了妥协:
- 在 Dart 中,实现 FIDL 到 JSON 编码的尝试受到了阻碍,因为它仅适用于没有句柄的类型,从而损害了可演变性。它最终是使用
MaxHandles属性构建的,但这是一种临时解决方案,因为该属性仅适用于最外层的类型,而不适用于以传递方式从该类型可访问的所有类型。 - 在 Rust 中,首次向类型添加句柄会导致源代码中断,因为该类型将不再派生
Clone特征。(正确克隆 句柄需要调用 zx_handle_duplicate 系统调用, 该调用可能会失败。) - 协议的 Rust 绑定通过可变引用获取 FIDL 对象并将句柄归零,而不是显式获取所有权,以便以后可以重复使用没有句柄的对象。
如果我们要求库作者指明类型是否可以包含句柄,并且更改指示预计会导致源代码中断,那么所有这些情况都可以以更安全、更符合人体工程学的方式处理。
设计
术语
FIDL 类型可以是值 类型,也可以是资源 类型。资源类型包括:
handle和handle<H>,其中H是句柄子类型P和request<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 结构(例如“struct”和“protocol”)不同,新用户不太可能猜到“resource”的含义,因此他们需要从文档中学习。
此提案是否使 FIDL 语言更符合人体工程学,这一点有争议。它有助于引起人们对包含句柄的声明的注意,尤其是在实际句柄值隐藏在嵌套结构中的情况下。任何浏览库的人都会立即看到结构体携带的是句柄,而不仅仅是数据。另一方面,担心是否使用 resource 和输入关键字可能会让人感觉不太符合人体工程学。将一个声明从值更改为资源可能会产生令人痛苦的级联效应,即许多类型必须成为资源(尽管这可以被视为一件好事,因为否则它会显示为源代码中断)。
FIDL 绑定的改进证明了复杂性的增加是合理的。由于可以自由地为值类型和资源类型提供不同的 API,因此可以使绑定更安全、更符合人体工程学。如需查看这些改进的示例,请参阅 绑定。
文档和示例
需要完成以下任务:
- 更新所有涉及句柄的文档,以便根据需要使用
resource。 - 更新 FIDL 语言规范以解释
resource修饰符。 - 在 FIDL 教程中提及
resource。应该有一条简短的注释来解释所有修饰符(即strict和resource)。 - 提供有关没有句柄的新类型是否应为资源的指导。
- 绑定利用值/资源区别后,更新其文档以说明值类型和资源类型提供的 API 之间的差异,并提供在它们之间转换的说明(如果可能)。
向后兼容性
此提案对 ABI 兼容性没有影响。
修正案(2021 年 7 月) 。在实施过程中,我们发现此提案与 RFC-0033:处理未知字段和严格性之间的交互存在一个极端情况。某些绑定在解码表和灵活联合时存储未知成员;如果未知成员包含句柄,则值类型无法执行此操作,因此在这种情况下解码必须失败。如需了解详情,请参阅 兼容性指南。
修正案(2021 年 10 月) 。在 RFC-0137:丢弃 FIDL 中的未知数据之后,绑定不再存储未知数据,因此不再存在极端情况。因此,值/资源区别对 ABI 兼容性没有影响。
就
RFC-0024 而言,添加或移除 resource 修饰符既不与源代码兼容,也不
可转换,2。
绑定明确允许为仅在修饰符存在与否方面不同的两种类型生成不兼容的 API,并且实际上可能无法编写在添加/移除修饰符前后编译的代码。希望以与源代码兼容的方式转换为 resource 或从 resource 转换的库作者必须创建新类型和方法,而不是更改现有类型和方法。
绑定作者开始利用值/资源区别后,我们将重新审视此决定。可能需要提供可转换的路径(或许使用带有 [Transitional] 属性的中间阶段)。一开始,这一点尚不清楚:它可能过于受限,从而破坏此提案旨在实现的潜在 API 改进。
性能
此提案对 build 性能的影响微乎其微:FIDL 编译器将稍微多做一些工作来解析新关键字并验证其使用情况。它对 IPC 性能没有直接影响。如果绑定使用值/资源区别来创建阻止不必要副本的 API,则可能会在某些语言中实现小幅改进。例如,无需克隆值类型对象即可多次发送该对象。
安全
此提案不会直接影响安全性,但它使绑定能够提供更安全的 API。例如,C++ 可以使用 [[nodiscard]] 对资源类型的 Clone() 强制执行错误处理,或者 Rust 可以通过移动来获取资源类型方法参数,以防止以后意外使用更改后的对象。
这些类型的更改可以防止 bug,包括安全 bug。
测试
此功能将通过以下方式进行测试:
- 在 fidlc 中为解析和验证代码路径添加测试。这些测试应涵盖各种情况,在这些情况下,标记为
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>类型,如果A或B是资源类型,则它在逻辑上应该是 资源类型,而不是必须 注释Pair本身。在其他情况下,最好派生类型是否为资源吗?
在先技术和参考文档
此提案的目标是允许在更改类型的“值/资源”状态时发生中断性更改。
RFC-0024 与此
目标相关,因为它为 FIDL 建立了源代码兼容性标准。它还涉及句柄导致难以在 Rust 中使用 Clone 特征的问题,此提案解决了这个问题。
我们不知道其他 IPC 系统是否解决了这个确切的问题(区分可能包含句柄或系统资源的类型)。但是,以“感染”所有使用站点的方式注释类型的概念在编程语言中很常见。例如,JavaScript、Python 和 Rust 中的异步函数以及 Haskell 中的 IO monad 都具有此行为。