RFC-0057:默认无标识名 | |
---|---|
状态 | 已接受 |
领域 |
|
说明 | 我们建议默认禁止在 FIDL 类型声明中使用句柄,并添加新的关键字资源来标记允许包含句柄或其他资源类型的类型。添加或移除资源修饰符可能属于破坏源代码的更改。 |
作者 | |
提交日期(年-月-日) | 2020-01-16 |
审核日期(年-月-日) | 2020-01-23 |
“看马,没有手!”
总结
我们提议在默认情况下禁止在 FIDL 类型声明中使用句柄,并添加新的关键字 resource
1 来标记允许包含句柄或其他资源类型的类型。添加或移除 resource
修饰符可能属于破坏源代码的更改。
设计初衷
FIDL 支持 Zircon 句柄。句柄是内存中的 32 位整数,但系统会对其进行特殊处理:必须移动句柄而非复制句柄,并且必须关闭句柄以避免泄露资源。在考虑那些仅对没有句柄的普通数据有意义的特征时,这种特殊处理方式会导致问题。虽然 FIDL 绑定可以根据句柄的存在有条件地启用代码,但这是不可取的,因为这会破坏可演化性保证。例如,向表添加字段通常是安全的,但添加句柄字段会破坏源代码 - 不仅对于该表而言,对于以传递方式包含该字段的所有类型而言。这会使绑定变得保守,始终假定类型可能包含句柄(即使库作者根本不打算添加句柄)。
需要适应句柄的需要已导致危害:
- 在 Dart 中,将 FIDL 编码为 JSON 编码的一项努力收到了反响,因为它仅适用于没有句柄的类型,这会损害可演化性。它最终是使用
MaxHandles
属性构建的,但这是一个临时解决方案,因为该属性仅适用于最外层的类型,而不适用于可从其传递的所有类型。 - 在 Rust 中,首次向某个类型添加句柄会破坏源代码,因为该类型将不再派生
Clone
trait。(正确克隆句柄需要调用 zx_handle_duplicate 系统调用,这可能会失败。) - 协议的 Rust 绑定通过可变引用和零出句柄获取 FIDL 对象,而不是显式获得所有权,因此没有句柄的对象之后可以重复使用。
如果我们要求库作者指明某个类型是否包含句柄,以及更改此指示是否会导致源代码被破坏,那么所有这些情况都可以通过更安全、更符合工效学的方式处理。
设计
术语
FIDL 类型是 value 类型或 resource 类型。资源类型包括:
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
时,允许声明包含资源类型。它声明的新类型也被视为资源类型,即使它不包含资源也是如此。
原则上,该语言允许对新类型声明 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
。其中应该会添加一条简短的说明,解释所有修饰符(例如strict
和resource
)。 - 提供关于没有句柄的新类型是否应成为资源的指南。
- 绑定充分利用值/资源的区别后,请更新其文档,注意值类型和资源类型提供的 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>
类型,那么当A
或B
是资源类型时,该类型在逻辑上应该是资源类型,而不必为Pair
本身添加注解。是否存在更有助于推导某个类型是否为资源的其他情况?
现有技术和参考资料
此方案的目标是允许在更改类型的值/资源状态时进行对源代码的破坏性更改。RFC-0024 与此目标相关,因为它确立了 FIDL 的源代码兼容性标准。此外,我们还探讨了句柄的问题,这使得在 Rust 中使用 Clone
trait 变得困难,而此方案可以解决此问题。
我们不知道是否有其他 IPC 系统可以解决这一确切问题(区分可能包含句柄或系统资源的类型)。不过,以“感染”所有用例的方式为类型添加注释的概念在编程语言中很常见。例如,JavaScript、Python 和 Rust 中的异步函数以及 Haskell 中的 IO monad 也有此行为。