RFC-0047:表 | |
---|---|
状态 | 已接受 |
领域 |
|
说明 | 添加了向 FIDL 语言向前和向后兼容的复合数据类型的机制。 |
作者 | |
提交日期(年-月-日) | 2018-07-27 |
审核日期(年-月-日) | 2018-09-20 |
总结
添加了向 FIDL 语言向前和向后兼容的复合数据类型的机制。
与其他 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 消息封装到另一条 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 使用稀疏序数空间,可以为字段赋予任何序值。
- FlatBuffer 和 Cap'n'Proto 使用密集的序数空间,字段必须为连续序数。
Protobuf 有线格式在与解析为固定大小结构体的典型 Protobuf 实现配对时,存在一个 bug,即解码内存使用的内存量与通过线上传输的字节数无关。为此,请想象一条包含 10, 000 个(可选)int64
字段的消息。发送者可以选择只发送一条信息,导致消息在网络上只有几个字节,但内存中几乎是 100 kB。
通过将其中许多作为 RPC 发送,往往很容易阻碍流控制实现并导致 OOM。
稀疏序数的替代实现策略(如之前的对话中所建议)是发送 (ordinal, value)
元组的有序数组。选择就地解码的实现必须依赖于对数据进行二进制搜索来查找序数。它避免了之前提到的流控制 bug,但引入了一些在运行时可能造成巨大效率低下的问题,因为我们执行的二进制搜索数量可能非常多。
Cap'n'Proto 采用一种非常复杂的算法来处理序数,由于我们希望避免这种复杂性,因此本文未作进一步讨论。
FlatBuffer 与本文档中提出的传输格式非常相似:利用其密集的序数空间提供单个数组查询来查找字段的数据(或者其为 null)。
早期技术和参考资料
- FlatBuffers 算法与此类似,但在此处进行了调整,以更好地符合 FIDL 规范。
- Protobuf(我们相信)最初流行了序数/值表示法,其在 Google 内部得到大规模使用,证明了此方案多年来的稳健性。
- Cap'n'Proto 和 Thrift 在以上方面都有些小的转变。