| RFC-0047:表格 | |
|---|---|
| 状态 | 已接受 |
| 区域 |
|
| 说明 | 向 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 的字段。
在相应语言中,表可用于目前可使用结构体的任何位置。具体而言:
- 结构和联合可以包含表
- 表可以包含结构和联合
- 接口实参可以是表
- 表格可以设为可选
Wire 格式
表格以打包的 vector<envelope> 形式存储,向量的每个元素都是一个序数元素(因此索引 0 是序数 1,索引 1 是序数 2,依此类推)。下面将介绍信封。
一个表必须仅存储信封,直至最后一个信封(即最大集合序号)。 这可确保规范的表示形式。例如,如果未设置任何字段,则正确的编码为空向量。对于一个序号为 5 的字段,但仅设置了序号最高为 3 的字段,正确的编码是包含 3 个信封的向量。
红包
envelope 存储的是可变大小的未解释的带外载荷。载荷可以包含任意数量的字节和句柄。这种组织方式允许将一个 FIDL 消息封装在另一个 FIDL 消息内。
信封存储为一条记录,其中包含:
num_bytes:信封中的字节数(32 位无符号),始终是 8 的倍数,如果信封为 null,则必须为零num_handles:信封中句柄的 32 位无符号数量,如果信封为 null,则必须为零data:64 位存在指示或指向带外数据的指针
data 字段有两种不同的行为。
在编码以进行传输时,data 表示存在内容:
FIDL_ALLOC_ABSENT(所有位均为 0):信封为 nullFIDL_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)采用结构体带来的性能优势。
一旦序列化性能成为首要考虑因素(例如,在设备驱动程序的数据路径上很常见),我们就可以开始仅偏好结构,并依靠向接口添加新方法来应对未来的变化。
向后兼容性
虽然此更改引入了两个关键字(table 和 reserved)。
不存在向后兼容性问题。
性能
此功能采用选择启用模式,如果不使用,则不应影响 IPC 性能。 我们预计 build 性能差异会在可测量的噪声范围内。
安全
对安全性没有影响。
测试
需要针对每种语言绑定进行额外的测试,并针对 fidlc 进行测试。
使用表的扩展版 echo 套件会比较合适。
为表格编码/解码添加模糊测试工具会很有帮助 - 解析中总是存在棘手的情况。
缺点、替代方案和未知因素
在此空间中,我们需要回答两个大问题:
- 用于字段标识的序号与字符串(序号会强制发送架构)
- 如果是序数:每条消息的稀疏与密集序数空间
以并集向量形式表示的表格
有人提议将 table 视为 vector<union>。
这会带来两个问题:
- 这种格式的读取器的最有效实现必须不如所提议的表格格式的读取器的最有效实现有效,因此我们永久限制了峰值性能。
- 它不提供任何线材兼容性保证! 向量必然需要携带长度和正文,因此根据此提案,联合永远无法在线上转换为表(我们希望进行这种转换的次数似乎很少)。
相反,通过引入信封原语,我们可以以相同的方式写下并推理兼容性保证...并且我们可以在表格和可扩展的联合(正在开发中)之间共享一些棘手的实现细节,并且我们可以近乎免费地在语言中公开一个有用的原语。
序数与字符串
使用序数需要在编译时提供架构,但可以实现更高效的实现(字符串处理始终比整数处理慢)。 由于 FIDL 在编译期间已要求存在架构,因此希望此处使用序号而非字符串不会引起争议。
密集打包与稀疏打包
密集与稀疏有序空间的问题可能更具争议性。在现有实践中,存在两种阵营:
- Thrift 和 Protobuf 使用稀疏的序号空间,字段可以指定任何序号值。
- FlatBuffers 和 Cap'n'Proto 使用密集的序号空间,因此必须为字段指定连续的序号。
Protobuf 有线格式与解析为固定大小结构的典型 Protobuf 实现搭配使用时,存在一个 bug,即解码内存使用的内存量与通过有线传输的字节数无关。为了说明这一点,不妨想象一下包含 10, 000 个(可选)int64 字段的消息。发件人可以选择仅发送一个,这样一来,消息在网络中仅占用几个字节,但在内存中却占用近 100 KB。
通过将许多此类请求作为 RPC 发送,很容易挫败流量控制实现并导致 OOM。
稀疏序数的替代实现策略(如之前的对话中所建议的那样)是发送一个有序的 (ordinal, value) 元组数组。选择就地解码的实现必须依赖于通过数据进行二分搜索来查找序号。
它避免了之前提到的流量控制 bug,但引入了在运行时可能导致严重低效的问题,因为我们执行的二分搜索次数可能非常多。
Cap'n'Proto 实现了一个非常复杂的算法来处理序数,由于我们希望避免这种复杂性,因此在此不再进一步讨论。
FlatBuffers 的网络格式与本文档中提出的格式非常相似:利用其密集的序号空间提供单个数组查找来查找字段的数据(或该字段是否为 null)。
在先技术和参考资料
- FlatBuffers 算法与此算法类似,但已在此处进行调整,以更好地符合 FIDL 惯例。
- 我们认为,Protobuf 最初普及了序号/值表示法,并且其在 Google 内的大规模使用已证明该方案多年来的稳健性。
- Cap'n'Proto 和 Thrift 在上述基础上分别提供了一些小变化。