RFC-0061:可扩展的联合体 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 为了提供更多方法来表达可能需要随时间推移而演变的载荷,我们提议将现有的联合体替换为可扩展的联合体。 |
作者 | |
提交日期(年-月-日) | 2018-09-26 |
审核日期(年-月-日) | 2018-10-11 |
“Catering to Hawaii and Alaska”(面向夏威夷和阿拉斯加州提供餐饮服务)
摘要
为了提供更多方法来表达可能需要随时间推移而演变的载荷,我们建议将现有的联合体替换为可扩展的联合体。
设计初衷
目前,联合体无法随时间推移而演变,我们甚至警告说,“通常,更改联合体的定义会破坏二进制兼容性”。
目前定义了许多需要可扩展性的联合体,例如 fuchsia.modular/TriggerCondition,其中字段已废弃但未移除,或 fuchsia.modular/Interaction。
正如后面所述,还有许多工会,由于它们不太可能在近期发生演变,因此目前的表示方式是适当的。不过,同时保留 static unions
和 extensible unions
会带来不必要的复杂性,请参阅利弊。
设计
为了引入可扩展的联合体,我们需要修改 FIDL 的多个部分:语言和 fidlc
、JSON IR、线格格式和所有语言绑定。我们还需要在多个位置记录这项新功能。
我们将逐一讨论每项更改。
语言
从语法上讲,可扩展联合体与静态联合体完全相同:
union MyExtensibleUnion {
Type1 field1;
Type2 field2;
...
TypeN fieldN;
}
在后台,系统会为每个字段分配一个序数:这与表为每个字段分配序数以及方法的序数自动分配的方式类似。
具体而言:
- 我们使用与方法序数相同的算法计算序数(详情),将库名称“
.
”、可扩展联合体名称“/
”和成员名称串联起来,然后计算 SHA256 值并使用0x7fffffff
进行掩码。 - 序数为
uint32
,任何两个字段都不能声明相同的序数,并且我们不允许使用0
。如果出现序数冲突,应使用[Selector]
属性提供备用名称(或重命名成员)。 - 基数可以是稀疏的,也就是说,与表的运作方式不同,表需要稠密的基数。
- 不允许在可扩展的联合体上使用可为 null 的字段。
- 可扩展联合体必须至少有一个成员。
可扩展的联合体可用于当前可在该语言中使用联合体的任何位置。具体而言:
- 结构体、表和可扩展联合体可以包含可扩展联合体;
- 可扩展的联合体可以包含结构体、表和可扩展的联合体;
- 接口参数或返回值可以是可扩展的联合体;
- 可扩展的联合体可以为 null。
JSON IR
在下表中,我们将在每个联合体字段声明“ordinal”中添加一个键。
线格格式
在线路上,可扩展的联合体由用于区分选项的序数(填充为 8 字节)表示,后跟生产方已知的各种成员的封装容器。具体而言:
- 一个
uint32
标记,其中包含要编码的成员的序数; - 一个
uint32
内边距,以对齐到 8 个字节; - 一个
uint32
num_bytes,用于存储封装容器中的字节数,始终为 8 的倍数,如果封装容器为 null,则必须为 0; - 一个
uint32
num_handles,用于存储封套中的句柄数量;如果封套为 null,则必须为 0; - 一个
uint64
data 指针,用于指示是否存在离线数据:- 当封装容器为 null 时为
0
; - 当封装容器存在且下一个线下对象时,为 FIDL_ALLOC_PRESENT(或 UINTPTR_MAX);
- 当封装容器为 null 时为
- 在解码以供使用时,如果封装容器为 null,此数据指针为 nullptr;否则,此指针为指向封装容器的有效指针。
- 封套会为紧随内容之后的句柄预留存储空间。
可为 null 的可扩展联合体的标记为 0,num_bytes 设置为 0,num_handles 设置为 0,并且数据指针为 FIDL_ALLOC_ABSENT,即0。 从本质上讲,一个 null 可扩展联合体是 24 个字节的 0。
语言绑定
可扩展联合与联合类似,但在读取联合时,还需要处理“未知”情况。理想情况下,大多数语言绑定都会将
union Name { Type1 field1; ...; TypeN fieldN; };
与可扩展的联合体一样,这样代码就可以轻松地从一种转换为另一种,除了支持未知情况之外(这在可扩展的联合体情况下才有意义)。
首先,我们建议任何语言绑定都不应公开预留成员:虽然这些成员出于完整性目的而存在于 JSON IR 中,但我们不认为在语言绑定中公开它们有用。
实施策略
实现将分两个步骤完成。
首先,我们将构建对可扩展联合支持:
- 在语言 (
fidlc
) 中引入该功能,使用不同的关键字 (xunion
) 来区分静态联合体和可扩展联合体。 - 实现各种核心语言绑定(C、C++、Rust、Go、Dart)。 相应地扩展兼容性测试和其他测试。
其次,我们将所有静态联合体迁移到可扩展联合体:
为静态联合体生成序数,并将其放入 JSON IR。后端最初应忽略这些序数。
在读取路径上,同时以两种模式读取联合体,就像它们是静态联合体一样,也像它们是可扩展联合体一样(需要有序数才能实现这一点)。根据事务消息标头中的标志来选择其中一种。
更新写入路径以将联合编码为可扩展联合,并通过在事务消息标头中设置标志来指明这一点。
更新、部署和传播所有写入器后,移除静态联合处理和软过渡的脚手架代码。
文档和示例
这需要至少在以下位置提供文档:
向后兼容性
可扩展的联合体明确不与“静态”联合体向后兼容。
性能
不使用时不会影响性能。构建期间对性能的影响微不足道。
安全
对安全性没有影响。
测试
编译器中的单元测试、各种语言绑定中的编码/解码单元测试,以及用于检查各种语言绑定的兼容性测试。
缺点、替代方案和未知情况
可扩展联合体比不可扩展联合体的效率低。 此外,该语言中无法通过其他方式表达不可扩展的联合体。因此,我们建议同时保留这两项功能。
不过,我们可以决定只允许存在可扩展的联合体,并废弃目前定义的联合体。这与 Fuchsia 中的许多地方相悖,在这些地方,联合体代表对性能至关重要的消息,并且扩展预期很少,例如 fuchsia.io/NodeInfo
、fuchsia.net/IpAddress
。
保留静态联合体的优缺点
优点
- 与联合体相比,可扩展联合体会产生 8 字节的开销(封装容器的大小和句柄数量)。此外,可扩展联合体的始终以离线方式存储(即,为数据指针额外分配 8 字节),而只有可为 null 的联合体的始终以离线方式存储。
- 由于联合体的编码方式,无法在 FIDL 中使用其他基元来表示它们。因此,如果将它们从该语言中移除,某些类的消息将无法再以紧凑且高效的方式表达。
- 在某些情况下(具体取决于用途),可以以其他高效的方式表示联合体,但这属于例外情况,而不是常规做法。一个无需使用联合体即可重写的示例是仅在 fuchsia.net.stack/InterfaceAddressChange 中使用的 fuchsia.net.stack/InterfaceAddressChangeEvent,其中可以直接写入 InterfaceAddress,并使用
enum
指示是添加还是移除。
缺点
- 同时保留静态联合体和可扩展联合体会导致编译器、JSON IR、所有后端以及编码/解码变得复杂。收益微乎其微:在 FIDL 编码本身就不是特别节省空间的情况下,大小差异微乎其微。此外,如果需要,可就地解码可扩展联合体。
- 下面是 fuchsia.io/NodeInfo 的分析,可见改进幅度非常小:
- 目前,NodeInfo 有 6 个选项:服务(大小为 1)、文件(大小为 4)、目录(大小为 1)、管道(大小为 4)、vmofile(大小为 24)、设备(大小为 4)。
- 因此,NodeInfo 的总大小始终为 32 字节,即标记 + 选项大小上限 = 8 + 24 = 32。
- 使用可扩展的联合时,NodeInfo 大小取决于要编码的选项。始终存在 16 字节的“税费”(与 8 字节相比),因此相应大小为:服务 = 24、文件 = 24、目录 = 24、管道 = 24、vmofile = 40、设备 = 24。
- 因此,在所有情况下,我们都会减少 8 个字节,但 vmofile 除外,我们会额外添加 8 个字节。
- 同时使用静态联合体和可扩展联合体的语言复杂性也是一个问题。我们希望库作者在使用这两者之间犹豫不决,因为选择可扩展的联合体是长期而言更安全的选择,而且成本非常低。
总而言之,我们决定将静态联合体替换为可扩展联合体。
标记与序数
我们使用序数来表示分配给字段的内部数字值,即通过哈希计算得出的值。我们使用标记来表示绑定中的变体表示法:在 Go 中,这可能是类型为 alias
的常量;在 Dart 中,这可能是 enum
。
fidlc
编译器仅处理序数。开发者很可能只会处理代码。绑定可将高级别标记转换为低级别内部有序数。
无空的扩展可组合
在设计阶段,我们考虑过让可扩展联合体为空。不过,我们最终选择禁止这样做:选择具有单个变体(例如空结构体)的可为 null 的可扩展联合体可以清晰地对 intent 进行建模。这还可避免为可扩展的联合体提供两个“单位”值,即 null 值和空值。