RFC-0061:可扩展联合

RFC-0061:可扩展的联合
状态已接受
区域
  • FIDL
说明

为了提供更多方式来表达可能需要随时间演变的载荷,我们建议用可扩展的联合替换当前存在的联合。

作者
提交日期(年-月-日)2018-09-26
审核日期(年-月-日)2018-10-11

“为夏威夷和阿拉斯加量身打造”

摘要

为了提供更多方式来表达形状可能需要随时间演变的载荷,我们建议将当前存在的联合替换为可扩展的联合

设计初衷

目前,联合无法随时间演变,我们甚至警告说“一般来说,更改联合的定义会破坏二进制兼容性”。

目前,有许多需要可扩展性的联合,例如 fuchsia.modular/TriggerCondition(其中的字段已弃用但未移除)或 fuchsia.modular/Interaction

下文所述,还有许多联合的当前表示形式是合适的,因为它们在不久的将来不太可能发生变化。不过,同时保留 static unionsextensible unions 会引入不必要的复杂性,请参阅优缺点

设计

为了引入可扩展的联合,我们需要修改 FIDL 的多个部分:语言和 fidlc、JSON IR、有线格式和所有语言绑定。 我们还需要在多个位置记录此新功能。 我们会逐一讨论各项变更。

语言

从语法上讲,可扩展联合与静态联合完全相同:

union MyExtensibleUnion {
    Type1 field1;
    Type2 field2;
     ...
    TypeN fieldN;
}

在幕后,每个字段都会分配一个序号:这类似于表格为每个字段分配序号,以及方法的序号如何自动分配。

具体而言:

  • 序号是使用与方法序号相同的算法计算的详情),我们会将库名称“.”、可扩展的联合名称“/”和成员名称连接起来,然后计算 SHA256,并使用 0x7fffffff 进行屏蔽。
  • 序号为 uint32任何两个字段都不能声明相同的序号,并且我们禁止使用 0。 如果出现序号冲突,应使用 [Selector] 属性提供备用名称(或重命名成员)。
  • 序号可以是稀疏的,也就是说,与需要密集序号的表不同。
  • 可扩展的联合中不允许使用可为 null 的字段
  • 可扩展的并集必须至少有一个成员

可扩展的联合可在当前语言中可使用联合的任何位置使用。 具体而言:

  • 结构体、表和可扩展的联合可以包含可扩展的联合;
  • 可扩展的联合可以包含结构、表和可扩展的联合;
  • 接口实参或返回值可以是可扩展的联合;
  • 可扩展的联合可为 null。

JSON IR

在下表中,我们将在每个联合字段声明中添加一个键“ordinal”。

Wire 格式

在线上,可扩展的联合由用于区分选项的序数(填充为 8 字节)表示,后跟生产者已知的各种成员的信封。 具体而言,即:

  • 一个 uint32 标记,其中包含正在编码的成员的序号;
  • 用于对齐到 8 字节的 uint32 填充
  • 一个 uint32 num_bytes,用于存储信封中的字节数,始终是 8 的倍数,如果信封为 null,则必须为 0;
  • 一个 uint32 类型的 num_handles,用于存储信封中的句柄数,如果信封为 null,则必须为 0;
  • 一个 uint64 数据指针,用于指示是否存在带外数据:
    • 当信封为 null 时,值为 0
    • 当信封存在时,为 FIDL_ALLOC_PRESENT(或 UINTPTR_MAX),以及下一个带外对象;
  • 在解码以供使用时,如果信封为 null,此数据指针为 nullptr;否则,此指针为指向信封的有效指针
  • 信封会为紧随内容之后的句柄预留存储空间。

可为 null 的可扩展联合的标记为 0num_bytes 设置为 0num_handles 设置为 0,并且数据指针为 FIDL_ALLOC_ABSENT,即0。 从本质上讲,可扩展的 null 联合是 24 字节的 0。

语言绑定

可扩展的联合与联合类似,只不过在读取联合时还需要处理“未知”情况。理想情况下,大多数语言绑定都会将

union Name { Type1 field1; ...; TypeN fieldN; };

一样,这样代码就可以轻松地从一种切换到另一种,但未知情况的支持除外,这仅在可扩展的联合类型中才有意义。

首先,我们建议任何语言绑定都不应公开预留的成员:虽然这些成员会出现在 JSON IR 中以保持完整性,但我们认为在语言绑定中公开它们并无用处。

实施策略

实施将分两步完成。

首先,我们将构建对可扩展联合的支持:

  1. 在语言 (fidlc) 中引入该功能,使用不同的关键字 (xunion) 来区分静态联合和可扩展联合。
  2. 实现各种核心语言绑定(C、C++、Rust、Go、Dart)。 相应地扩展兼容性测试和其他测试。

其次,我们将所有静态联合迁移到可扩展联合:

  1. 为静态联合生成序号,并将其放置在 JSON IR 中。后端应最初忽略这些序号。

  2. 在读取路径上,同时具有读取联合的两种模式,一种是将其视为静态联合,另一种是将其视为可扩展联合(需要序号才能实现)。根据交易消息标头中的标志选择其中一个。

  3. 更新写入路径以将联合编码为可扩展的联合,并通过在交易消息标头中设置标志来尽可能多地指示。

  4. 当所有写入器都已更新、部署和传播后,移除静态联合处理和用于软过渡的脚手架代码。

文档和示例

这需要在至少以下位置添加文档:

向后兼容性

可扩展的联合显式地向后兼容“静态”联合。

性能

不使用时不会对性能产生影响。 在构建期间对性能的影响微乎其微。

安全

对安全性没有影响。

测试

编译器中的单元测试、各种语言绑定的编码/解码单元测试,以及用于一起检查各种语言绑定的兼容性测试。

缺点、替代方案和未知因素

可扩展的并集比不可扩展的并集效率低。 此外,非可扩展的联合在语言中无法通过其他方式表达。 因此,我们建议这两个功能并存。

不过,我们也可以决定只保留可扩展的联合,并废弃当前定义的联合。这会与 Fuchsia 中使用联合表示性能关键型消息的各种位置相冲突,并且这些位置几乎没有扩展预期,例如 fuchsia.io/NodeInfofuchsia.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 个字节。
  • 同时具有静态联合和可扩展联合的语言的复杂性也是一个令人担忧的问题。我们预计,库作者在选择使用可扩展的联合类型时,会在使用其中一种类型与使用另一种类型之间犹豫不决,但从长远来看,选择可扩展的联合类型是一种更安全的选择,而且成本非常低。

总而言之,我们决定用可扩展的联合替换静态联合。

标记与序数

我们使用 ordinal 来表示分配给字段的内部数值,即通过哈希计算出的值。 我们使用标记来表示绑定中的变体:在 Go 中,这可能是 alias 类型的常量;在 Dart 中,这可能是 enum

fidlc 编译器仅处理序号。 开发者很可能只会处理标记。 绑定提供从高级别标记到低级别内部序号的转换。

没有空的可扩展联合

在设计阶段,我们曾考虑让可扩展的联合为空。不过,我们最终选择禁止这样做:选择具有单个变体(例如,空结构体)的可为 null 的可扩展联合体可以清晰地表达意图。 这样还可以避免可扩展联合出现两个“单位”值,即 null 值和空值。

在先技术和参考资料

  • Protocol Buffers 具有 oneof
  • FlatBuffers 的 union 在特殊情况下可以扩展,但一般情况下不能扩展。