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 实现配合使用)
转换为固定大小的结构体
与通过有线传输的字节数无关。
为此,假设有一条消息包含 10000 个(可选)int64
字段。
发件人可以选择只发送一个,导致邮件只有几个字节
但内存几乎是 100kB。
通过以 RPC 的形式发送许多这样的内容,往往很容易阻碍流控制。
并导致 OOM。
稀疏序数的替代实现策略(如前面的对话所建议)
发送 (ordinal, value)
元组的有序数组。
选择就地解码的实现必须依赖于通过数据的二进制搜索
来查找序数。
它避免了前面提到的流控制 bug,但也带来了一些
执行二元搜索的次数可能相当惊人,因此在运行时效率低下。
Cap'n'Proto 采用一种非常复杂的算法来处理序数, 由于我们要避免这种复杂性,因此这里就不作进一步讨论了。
FlatBuffers 的传输格式与本文档中提议的非常相似: 利用其密集的序数空间进行单个数组查找, 数据(或者为 null)。
先验技术和参考资料
- FlatBuffers 算法与此类似,但在此处 更好地符合 FIDL 惯例。
- Protobuf(我们认为)最初推广了序数/值表示法, 多年来,它在 Google 内部的大量使用证明了该方案的稳健性。
- Cap'n'Proto 和 Thrift 均对上述内容进行了细微的调整。