| 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 时,声明可以包含资源类型。它声明的新类型也被视为资源类型,即使它不包含资源也是如此。
原则上,该语言可以允许在 newtype 声明中使用 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 兼容性没有影响。
添加或移除 resource 修饰符既不具备源代码兼容性,也不具备可过渡性,2 从 RFC-0024 的意义上来说是这样。
绑定明确允许为仅在修饰符存在与否方面不同的两种类型生成不兼容的 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<A, B>在逻辑上应该是资源类型,而不是必须对Pair本身进行注释。在其他情况下,是否也最好推导类型是否为资源?
在先技术和参考资料
此提案的目标是允许在更改类型的值/资源状态时发生源代码破坏性更改。RFC-0024 与此目标相关,因为它为 FIDL 确立了源代码兼容性标准。它还涉及了句柄导致难以在 Rust 中使用 Clone 特征的问题,而此提案解决了该问题。
我们不了解是否有其他 IPC 系统解决了这个确切的问题(区分可能包含句柄或系统资源的类型)。不过,以“感染”所有使用位置的方式为类型添加注释的概念在编程语言中很常见。例如,JavaScript、Python 和 Rust 中的异步函数以及 Haskell 中的 IO monad 都具有这种行为。