RFC-0047:表格

RFC-0047:表格
状态已接受
区域
  • FIDL
说明

向 FIDL 语言添加了一种可实现向前和向后兼容复合数据类型的机制。

作者
提交日期(年-月-日)2018-07-27
审核日期(年-月-日)2018-09-20

摘要

向 FIDL 语言添加了一种可实现向前和向后兼容复合数据类型的机制。

与其他 RFC 的关系

此 RFC 后来被以下 RFC 修订:

设计初衷

FIDL 结构体不提供任何机制来随着时间的推移更改架构。表与结构体类似,但会向每个字段添加序数,以允许结构演变:

  • 可以添加新字段,并且现有代码会忽略这些字段
  • 较新代码可以跳过旧(已废弃)字段

表必然比结构体更复杂,因此处理它们的速度会更慢,将它们序列化也会占用更多空间。因此,最好保持结构体不变,并引入新内容。

此外,具有可演变的架构为实现可合理序列化到磁盘或通过网络的 FIDL 变体打开了道路。

示例表格可能如下所示:

table Station {
    1: string name;
    3: bool encrypted;
    2: uint32 channel;
};

设计

源语言

table_declaration 添加到 FIDL 语法中:

declaration = const-declaration | enum-declaration | interface-declaration |
              struct-declaration | union-declaration | table-declaration ;

table-declaration = ( attribute-list ) , "table" , IDENTIFIER , "{" , ( table-field , ";" )* , "}" ;

table-field = table-field-ordinal , table-field-declaration ;

table-field-ordinal = ordinal , ":" ;

table-field-declaration = struct-field | "reserved" ;

注意:

  • 序数必须从 1 开始,序数空间中不得有空缺(如果最大的序数为 7,则必须包含 1、2、3、4、5、6、7)。
  • 任何两个字段都不能声明相同的序数。
  • 在发生序号冲突检查后,编译器会舍弃“预留”字段。它允许注解某个字段在某个早期版本的表格中使用过,但已被舍弃,以免未来的修订版本意外重复使用该序数。
  • 不允许使用可为 null 的字段。

表可在该语言中当前可使用结构体的位置使用。具体而言:

  • 结构体和联合体可以包含表
  • 表可以包含结构体和联合体
  • 接口参数可以是表
  • 表可以设为可选

线格格式

表格存储为一个压缩的 vector<envelope>,向量的每个元素都是一个序数元素(因此索引 0 是序数 1,索引 1 是序数 2,依此类推)。我们将在下文中介绍信封。

表只能存储最新的封装容器(即最大集序数)之前的封装容器。这可确保规范化表示。例如,如果未设置任何字段,正确的编码为空矢量。对于序数为 5 但字段设置仅限于序数 3 的表,正确的编码是 3 个封套的向量。

信封

envelope 会将可变大小的未解读载荷存储在线下。载荷可以包含任意数量的字节和句柄。 这种组织方式允许将一个 FIDL 消息封装在另一个消息中。

信封存储为由以下内容组成的记录:

  • num_bytes:封装容器中的字节数(32 位无符号数),始终为 8 的倍数,如果封装容器为 null,则必须为零
  • num_handles:封套中的句柄数(32 位无符号整数),如果封套为 null,则必须为 0
  • data:64 位存在指示或指向离线数据的指针

data 字段有两种不同的行为。

编码以进行传输时,data 表示内容是否存在:

  • FIDL_ALLOC_ABSENT(所有位均为 0):封装容器为 null
  • FIDL_ALLOC_PRESENT(所有位均为 1):封装容器不为 null,数据是下一个非线性对象

在解码以供使用时,data 是指向内容的指针。

  • 0:封装容器为 null
  • <valid pointer>:封装容器不为 null,数据位于指定的内存地址

对于手柄,封套会为紧随内容后面的手柄预留存储空间。解码时,假设 data 不为 null,data 会指向数据的第一个字节。

封装容器会填充到下一个 8 字节对象对齐位置(在实践中,这意味着没有额外的填充)。

语言绑定

表不会像结构体那样生成数据字段,而是为每个字段生成一组方法。例如,在 C++ 中,我们将使用以下代码:

class SampleTable {
 public:
  // For "1: int32 foo;"
  const int32* foo();         // getter, returns nullptr if foo not present
  bool has_foo();             // presence check
  int32* mutable_foo();       // mutable getter, forces a default value if not set
  void set_foo(int32 x);      // set value
  void clear_foo();           // remove from structure
  optional<int32> take_foo(); // get foo if present, remove from structure
};

风格指南

我应该使用结构体还是表?

结构体和表提供的语义类似,因此在选择哪种结构时,可能会觉得很复杂。

对于非常高级别的 IPC 或永久性存储,序列化性能通常不是问题:

  • 表格提供了一些向前和向后兼容性,因此具有一定的未来保障因素:对于大多数概念,应优先使用表格。
  • 仅针对未来不太可能发生变化的概念(例如 struct Vec3 { float x; float y; float z }Ipv4Address)利用结构体带来的性能优势。

一旦序列化性能成为首要问题(例如,在设备驱动程序的数据路径上很常见),我们就可以开始仅使用结构体,并依赖于向接口添加新方法来应对未来的更改。

向后兼容性

不过,此次变更引入了两个关键字:tablereserved。没有向后兼容性问题。

性能

此功能需用户选择启用,如果不使用,则不会对 IPC 性能产生影响。 我们预计 build 性能差异在可测量的噪声范围内。

安全

对安全性没有影响。

测试

您需要针对每个语言绑定进行额外的测试,以及针对 fidlc 进行测试。

使用表格的扩展版 echo 套件会很合适。

为表格编码/解码添加模糊测试工具会很有帮助,因为解析中总会出现棘手的情况。

缺点、替代方案和未知情况

我们需要回答以下两个大问题:

  • 用于字段标识的序数与字符串(序数会强制发布架构)
  • 如果是序数:每条消息的稀疏序数空间与稠密序数空间

将表格视为矢量并集

有人建议我们将 table 视为 vector<union>。这会带来两个问题:

  • 此格式的读取器最有效的实现方式的效率必须低于建议表格格式的读取器最有效的实现方式,因此我们会永久限制峰值性能。
  • 它不保证任何线兼容性! 矢量必须携带长度和正文,因此根据此提案,联合体永远无法在线路上转换为表(而且我们希望进行此转换的次数似乎很低)。

相反,通过引入封装容器基元,我们可以以相同的方式编写和推理兼容性保证...我们可以在表和可扩展联合体(正在开发中)之间共享一些棘手的实现细节,并且几乎可以免费在该语言中公开一个实用的基元。

序数与字符串

使用序数需要在编译时提供架构,但可以实现更高效的实现(字符串处理始终比整数处理慢)。由于 FIDL 在编译期间已要求存在架构,因此希望此处对字符串的序数是无争议的。

密集封装与稀疏封装

密集有序空间与稀疏有序空间的问题可能更具争议性。现有实践分为两派:

  • Thrift 和 Protobuf 使用稀疏有序空间,可为字段指定任何有序值。
  • FlatBuffers 和 Cap'n'Proto 使用稠密的序数空间,必须为字段分配连续的序数。

当 Protobuf 线格格式与解析为固定大小结构体的典型 Protobuf 实现搭配使用时,会出现一个 bug,即解码内存使用的内存量与线路上传输的字节数无关。为此,假设消息包含 10, 000 个(可选)int64 字段。发件人可以选择只发送一个,这样一来,消息在线路上只有几字节,但在内存中却有近 100KB。通过以 RPC 的形式发送许多此类请求,通常很容易破坏流控制实现并导致 OOM。

稀疏有序数的另一种实现策略(如前面讨论中所建议)是发送一个有序的 (ordinal, value) 元组数组。选择就地解码的实现必须依赖于对数据进行二进制搜索来查找序数。它避免了前面提到的流控制 bug,但由于我们执行的二元搜索可能非常多,因此在运行时可能会引入一些严重的低效问题。

Cap'n'Proto 实现了一个非常复杂的算法来处理序数,但我们希望避免这种复杂性,因此不会在此处进一步讨论。

FlatBuffers 的线格格式与本文档中提出的格式非常相似:利用其稠密有序空间提供单个数组查找,以查找字段的数据(或它为 null)。

在先技术和参考文档

  • FlatBuffers 算法与此类似,但在此处已进行了调整,以更好地适应 FIDL 惯例。
  • 我们认为,Protobuf 最早普及了序数/值表示法,而它在 Google 内部的大规模使用多年来也证明了该方案的稳健性。
  • Cap'n'Proto 和 Thrift 对上述内容各有小改动。