| 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 个字节; - 一个
uint32num_bytes ,用于存储信封中的字节数,始终是 8 的倍数,如果信封为 null,则必须为 0; - 一个
uint32num_handles ,用于存储信封中的句柄数,如果信封为 null,则必须为 0; - 一个
uint64data 指针,用于指示是否存在内联数据:- 当信封为 null 时,为
0; - 当信封存在且为下一个内联对象时,为 FIDL_ALLOC_PRESENT (或 UINTPTR_MAX );
- 当信封为 null 时,为
- 在解码以供使用时,如果信封为 null,则此 data 指针为 nullptr ;否则,为指向信封的有效指针 。
- 信封会为紧随内容之后的句柄预留存储空间。
一个 可为 null 的可扩展联合 具有 0 的标记、num_bytes 设置为 0、 num_handles 设置为 0,并且 data 指针为 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/InterfaceAddressChangeEvent
,它仅在
fuchsia.net.stack/InterfaceAddressChange
中使用,其中 InterfaceAddress 可以直接写入,并使用
enum来指明它是添加还是移除。
缺点
- 同时保留静态联合和可扩展的联合会强制编译器、JSON IR、所有后端以及编码/解码的复杂性。 收益很小:大小差异很小,因为 FIDL 编码本身就不是特别高效。 此外,如果需要,可以就地解码可扩展的联合。
- 以下是 fuchsia.io/NodeInfo 的分析,可作为收益有多小的示例:
- 如今,NodeInfo 有 6 个选项:服务(大小 1)、文件(大小 4)、目录(大小 1)、管道(大小 4)、vmofile(大小 24)、设备(大小 4)。
- 因此,NodeInfo 的总大小始终为 32 字节,即标记 + max(选项大小) = 8 + 24 = 32。
- 使用可扩展的联合时,NodeInfo 的大小取决于要编码的选项。 始终有 16 字节的“税”(而不是 8 字节),因此相应的大小为:服务 = 24、文件 = 24、目录 = 24、管道 = 24、vmofile = 40、设备 = 24。
- 因此,在所有情况下,我们都会减少 8 个字节,但在 vmofile 的情况下,我们会额外添加 8 个字节。
- 同时保留静态联合和可扩展的联合也会增加语言的复杂性,这令人担忧。 我们预计库作者会在使用其中一个还是另一个之间犹豫不决,而选择可扩展的联合是一种更安全的长期选择,成本非常低。
总而言之,我们决定将静态联合替换为可扩展的联合。
标记与序号
我们使用序号 来表示分配给字段的内部数值,即通过哈希计算得出的值。
我们使用标记 来表示绑定中的变体:在 Go 中,这可能是类型 alias 的常量;在 Dart 中,这可能是 enum。
fidlc 编译器仅处理序号。开发者很可能只处理标记。
绑定提供从高级标记到低级内部序号的转换。
没有空的可扩展联合
在设计阶段,我们考虑让可扩展的联合为空。 但是,我们最终选择不允许这样做:选择具有单个变体(例如,空结构体)的可为 null 的可扩展联合可以清楚地表达意图。这还可以避免为可扩展的联合提供两个“单元”值,即 null 值和空值。