RFC-0120:单独使用 FIDL 有线格式

RFC-0120:单独使用 FIDL 有线格式
状态已接受
领域
  • FIDL
说明

此 RFC 规定了在无传输情况下使用(即编码和解码)FIDL 有线格式的要求。它还指定了关于绑定应如何公开此功能的评分准则。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-07-02
审核日期(年-月-日)2021-08-04

摘要

此 RFC 规范了使用(即编码和解码)FIDL 的要求 有线格式。还指定了关于绑定如何 应该公开此功能我们介绍了有线格式的概念 元数据,用于描述线上传输格式的修订版本和特性;以及 要求将其用于编码和解码 API,以便:

  • 绑定正式支持在不使用传输的情况下使用 FIDL 有线格式。
  • 用户必须随编码消息一起传输有线格式元数据。
  • 绑定可支持以消息为前缀的持久性惯例 元数据。

设计初衷

Fuchsia 的一个核心原则是可更新。我们在 ABI 方面进行了大量投资 在 IPC 环境中使用 FIDL 时,例如两个对等体通过 Zircon 通道上的 FIDL 协议。FIDL 线的独立用例 另一方面,由于它们相对较少, 需要注意兼容性。例如,有时 仅传递 FIDL 消息的编码字节会导致 可发展的 ABI。

驱动程序元数据 RFCRFC-0109: Fast datagram sockets 调用通过面向字节的接口发送 FIDL。 现在是正式确定 FIDL 有线格式的独立用途的好时机, 为其提供演变和互操作性保证。

设计

绑定必须支持对 FIDL 有线格式进行编码和解码,而不使用 transport(一种 API,其要求将在下面详细说明)。请注意,许多绑定 已有某种形式的公共编码/解码 API(例如,fidl::Encode 高级 C++ 绑定)。并且应根据上述要求进行调整 RFC。因此,RFC 的这一部分可视为核心 功能,阐明了 FIDL 的分层。

FIDL 传输格式

FIDL 有线格式的重点是二进制兼容性:一组保证 围绕架构演变,以支持读取使用其他 该架构的另一个版本例如,布局为 struct{uint8;uint8;} 的类型 可能会演变为布局 struct{uint16;}。FIDL 则提供可扩展数据 但它们并不支持线缆格式本身的演变, 将 FIDL 表切换为更高效的表示法。两块 事务标头中的信息有助于 FIDL 的二进制文件兼容性 有线格式(通过协议和传输使用时):

  • 魔数:用于标识线上传输格式的修订版本。如果接收方 因为不支持此修订版本,所以它会坚定地拒绝解码,因为 而不是对不匹配的有线格式进行错误解读。
  • 标志:表示此消息中启用的任何软转换。对于 例如,在 union 到 xunion 迁移期间,标记中的某个位 用于表示并集是使用可扩展的 表示。

单独使用 FIDL 传输格式时, 编码结果。我们建议将一部分信息 编码和解码。具体而言:

  • Encoding 将绑定/语言特定的领域对象转换为 FIDL 编码形式和不透明的线缆格式的 blob 元数据,用于说明 。
  • 解码会使用编码形式的 FIDL 消息,并使用 线格式元数据,生成绑定/特定于语言的域对象。

在伪代码中,它们将具有以下函数签名:

function Encode<T>(object: T) -> (EncodedMessage, WireFormatMetadata);
function Decode<T>(message: EncodedMessage, metadata: WireFormatMetadata) -> T;

EncodedMessage 包含编码的字节以及 消息。大多数绑定都会定义具有类似或等效目的的类型。

传输格式元数据本身具有与 64 位架构兼容的 ABI 整数。其布局如下所示(采用伪 C 表示法):

struct fidl_wire_format_metadata_t {
    uint8_t disambiguator;
    uint8_t magic_number;
    uint8_t at_rest_flags[2];
    uint8_t reserved[4];
};

RFC-0138:处理未知交互提议 将事务头文件中的标志细分为 dynamic_flags - 数 涉及协议的请求/响应交互模型的问题,以及 at_rest_flags - 与线路格式相关的参数。此 RFC 假定: 但可以在不丢失密钥的情况下轻松地进行相应调整 属性。

线上传输格式元数据的对齐方式应为 8 个字节, 就地解码消息。绑定必须在外部表示元数据 不透明结构,长度为 8 个字节,对齐方式为 8 个字节 (例如,具有单个 uint64 字段的结构体)。这使得元数据本身可以 通过防止用户依赖 元数据。

绑定必须检查 reserved 字节是否为零。绑定不得依赖于 对 at_rest_flags 字节的处理具有任意特定值。绑定必须 验证 magic_number 是否代表 支持

绑定必须检查 disambiguator 字节是否为零。在 Cloud Storage 中 元数据的前面可以防止节目将 FIDL 消息误认为 以文件形式保存消息(请参阅 数据持久性惯例)。

请注意,FIDL 事务标头中包含的信息是一个超集 在线上传输格式元数据中保留这部分内容at_rest_flags 字段的语义 和 magic_number 字段与事务标头和 线上传输格式元数据。

每条消息只能与其对应的元数据部分结合使用。在 换句话说,应用不得共享元数据(例如,使用元数据 A 来 对消息 A 和消息 B 进行解码,或交换元数据(例如使用元数据 A) 对消息 B 进行解码,并使用元数据 B 解码消息 A。这样, 要在运行时(如连接期间)更改的消息线格式修订版本 格式软迁移

绑定必须支持单独使用下列顶级类型:

  • 结构体
  • 表格
  • 联合

对于任何其他数据类型,编码和解码函数必须失败。失败 应尽可能在编译时进行。

FIDL 语言未规定传输线格式元数据的方式 与经过编码的消息相关联。例如,元数据可以是 从生产环境中使用 FIDL 时的事务性消息标头衍生而来 IPC 上下文。

数据持久性惯例

为了更好地支持激励用例,我们要指定 将元数据附加到编码消息的惯例,其中字节 以元数据为前缀。绑定应支持此项 独立有线格式用法的带前缀,称为 持久性

以下持久性用例在测试范围内:

  • 将单个 FIDL 对象写入网络、磁盘或其他面向字节/数据包 界面不支持在不选择启用 Zircon 标识名的情况下, 转变为请求/响应范式换言之,数据是“静态”的。
  • 支持大于 64 KiB 的消息。消息大小限制为 64 KiB Zircon 通道传输的属性。以字节形式保留消息时 则此类限制不适用。现有的 Rust 持久性 API 支持 大型消息,已用于解决通道消息大小限制, 待处理内置 FIDL 支持(通过手动保留大型消息) 复制到 VMO 中

以下用例不在范围内:

  • 内置对编码同一类型的消息序列的支持。 应用可以定义自定义流式传输方法,该方法更适合 应用场景。

使用以下带前缀的 API 变种可在许多方面改善工效学设计和安全性 方式:

  • 用户不必手动跟踪数据之间的关联 和元数据数据只跟随元数据的后面,可以作为一个整体发送, 单位。相比之下,带外元数据传递会增加 版本不匹配。如果接收方需要处理 多路复用到同一持久性媒介中的多个有线格式版本: <ph type="x-smartling-placeholder">
      </ph>
    • 当流式传输 API 中的发送方更改身份时,新发送方 与原始发送方说出的线路格式修订版本不同。
    • 考虑使用一个从多个组件接收持久性消息的代理 并将它们存储到数据库中。通过 代理必须将带外口味转换回带前缀的口味 以便保留不同的线上传输格式修订。
  • 缓冲区管理得以简化,性能可能会提高,这 如果是在热路径中使用独立的有线格式,则比较有利。例如: 绑定可以分配一个缓冲区来同时存储元数据和 载荷,或者描述单个矢量化写入,其中第一个元素指向 添加到元数据中
  • 用户无需重新实现用于传递元数据的相同逻辑 支持多种语言和客户端库,因为 FIDL 已提供 实施。

持久性 API 必须支持以下顶级类型:

  • 非资源结构体
  • 非资源表
  • 非资源并集

对于任何其他数据类型,持久性必须失败。失败应发生在 编译时尽量缩短。

在伪代码中,持久性 API 具有以下函数签名:

function Persist<T>(object: T) -> vector<uint8>;
function Unpersist<T>(bytes: vector<uint8>) -> T;

绑定可以使用 符合目标语言的要求,只要符合 API 的形状即可 从数据流角度来看

绑定可以支持支持矢量化的矢量化 Persist 变体 例如生成一个 zx_channel_iovec_tzx_iovec_t 链接至 缓冲区数量,或与目标的惯用写入接口集成 语言。绑定应提供矢量化变体(如果已使用) 到这里就结束了

绑定可以支持接受矢量化的矢量化 Unpersist 变体 输入,例如使用链接到多个缓冲区的 zx_iovec_t,或者 集成目标语言的惯用阅读器界面。

请注意,持久性与独立磁盘相比,以字节为单位 编码/解码,这可能会导致生成句柄。

绑定必须支持保留会导致编码消息的较大值 超过 64 KiB。

应更新 FIDL 样式指南和 API 评分准则,以纳入持久性 注意事项:

  • 明确指出二进制 blob 使用的是持久性约定还是 自定义/带外机制来传递元数据。

FIDL 源语言

此 RFC 不会更改 FIDL 源语言。

实现

绑定应调整其独立的编码/解码 API,以与 涉及元数据的提议设计。他们不必严格遵循 函数签名,只要函数与提案要求一致即可 从数据依赖关系的角度来看例如,解码器的行为 必须通过元数据的方式进行配置

应使用相同的独立编码/解码 API 来实现消息传递, 例如通过 Zircon 通道进行事务性消息分派。

可以单独添加对持久性 API 变种的绑定支持。

已经存在持久性 API 的实现 ,但数据格式和 API 与 此 RFC。将调整 Rust 实现,以与已接受的 设计。

Rust 变更

目前,Rust 绑定提供以下函数:

fn create_persistent_header() -> PersistentHeader;
fn encode_persistent_header(header: &mut PersistentHeader) -> Result<Vec<u8>>;
fn encode_persistent<T: Persistable>(body: &mut T) -> Result<Vec<u8>>;
fn encode_persistent_body<T: Persistable>(body: &mut T, header: &PersistentHeader) -> Result<Vec<u8>>;
fn decode_persistent<T: Persistable>(bytes: &[u8]) -> Result<T>;
fn decode_persistent_header(bytes: &[u8]) -> Result<PersistentHeader>;
fn decode_persistent_body<T: Persistable>(bytes: &[u8], header: &PersistentHeader) -> Result<T>;

这些签名应替换为以下内容(确切签名可能会因 借鉴和终生微妙之处):

fn persist<T: Persistable, W: std::io::Write>(body: &mut T, writer: W) -> Result<()>;
fn unpersist<T: Persistable, R: std::io::Read>(reader: R) -> Result<T>;

fn standalone_encode<T: TopLevel, W: std::io::Write, H: core::iter::Extend<HandleDisposition>>(body: &mut T, writer: W, out_handles: &mut H) -> Result<WireMetadata>;
fn standalone_decode<T: TopLevel, R: std::io::Read>(reader: R, handles: &mut [HandleInfo], metadata: &WireMetadata) -> Result<T>;

struct WireMetadata { /* private fields */ }

TopLevel trait 是为结构体、联合和表实现的。

特别是,用户无法再凭空创建永久性标头 并重复使用同一标头对多条消息进行编码。

此外,绑定应提供一种序列化/反序列化的方法。 WireMetadata 与字节之间,以支持带外传递元数据。

性能

独立编码和解码是 FIDL 事务用途的一部分, 而持久性 API 应共享大部分代码路径。因此,我们 可以重复使用相同的标准和效果基准。

工效学设计

设计约束条件下的工效学设计应鼓励持久性 。例如,绑定可以使用更简洁、更符合编程习惯的 函数名称来表示持久性变种(例如 fidl::persist),并将 一个更长、更明确的函数名称,用于表示公开独立 编码/解码 API(例如 fidl::standalone::encode)。

向后兼容性

此更改本身可向后兼容,因为它完全是累加的, Rust FIDL 持久性实现除外。据我们所知, Rust FIDL 持久性的当前读取者、写入者和存储数据始终在演变 同步。

添加线格式元数据可提高未来的向后兼容性, 敬请期待即将进行的 FIDL 有线格式迁移。

线上格式元数据包含 5 个预留字节。这些字节可能是 经过重新调整,以在将来具有其他含义。例如,我们可能会 使用一个字节来描述特定于持久性的问题。

安全注意事项

FIDL 传输格式的验证要求在此处适用, 并具有相同的安全属性

请注意,FIDL 不是一种自描述格式。已成功将 使用一种消息类型的持久化消息,并不能保证 最初使用相同的消息类型进行序列化:

  • 程序可能会混淆 FIDL 消息是否包含前缀 或如果元数据是通过带外方式传递的,则会导致 输入解析有误。我们认为,这些错误往往 测试阶段。再加上明确的文档, 应该尽量减少用户误解

  • 恶意行为者可能会诱使程序覆盖持久的 FIDL Foo 类型的消息以及另一个 Bar 类型的消息, 通过在路径处理过程中利用漏洞进行控制。这个 让恶意操作者能够间接影响 Foo 的内容 消息。

“替代内容”部分提供了一种更复杂的格式,可缓解这种情况 使用有关邮件类型的信息扩展元数据标头。

隐私注意事项

FIDL 传输格式中的填充字节必须为零, 有助于避免泄露敏感信息。

与临时数据相比,持久性数据往往会引发更大的隐私问题 我们会尽快使用 IPC 数据 组件保留它或通过网络发送它。因此, IPC 和持久性 API 之间的隐私权问题是类似的。

值得注意的是,开发者始终可以通过 其他方式,例如 JSON 或 XML。平常 如果未来的设计提及涉及 持久性用户或其他敏感数据,无论方法是否通过 FIDL 持久化。

FIDL API 元素中的隐私权注解将简化隐私权审核和 支持更好的下游工具(例如自动隐去);CANNOT TRANSLATE 此 RFC 的范围(重点介绍传输 FIDL 的特定方法) 消息。

测试

我们将扩展 FIDL 一致性测试套件 GIDL,以测试编码和 永久性格式的解码。

文档

  • 扩充了绑定规范,以包含所添加的 要求(例如 LLCPP)。

  • 创建有关 FIDL 的参考页面以进行独立编码/解码,并 持久性以及两个 API 之间的关系。

    • 已使用 RustLLCPP 有相关文档。现有文档将进行更新。
  • 将所有语言添加到 //examples/fidl/,以演示独立编码 解码和持久性,并添加相应的教程。

缺点、替代方案和未知问题

替代方案 1:仅支持 Persistent API

我们可以在惯例的基础上更进一步,将它规定为标准: 所有元数据都必须在消息载荷之前。足以供使用 这种做法可能会使代码变得过于僵化, 。通过提供非预期的独立编码/解码 API 和自定义的持久性 API,用户能够自行挑选 与自己的设计最相符。

替代方案 2:允许共享线上传输格式的元数据

我们可以允许为会话中的所有消息共享相同的元数据。 这样一来,元数据就可在开头发送一次,然后针对 两个对等体之间的其余通信这可用于在线播放 为一个永久性媒介实例指定多个 FIDL 对象。例如: 快速数据报套接字 RFC 可以避免向 每个 UDP 数据报,其首先通过套接字发送元数据,然后发送 以编码形式显示多个对象。

在此过程中,我们采用了一个限制条件,即元数据必须与 只依赖于 FIDL 的版本, 将运行时编译到消息的生产方中这也意味着 编码器不得在运行时随意切换有线格式的表示法; 应该支持多种线上格式表示法。

元数据加收的 8 个字节的额外税费似乎并不严格 价格高昂。始终在消息中包含它可减少许多额外的元数据 来跟踪用户操作复杂性end 的值。

适用于真正需要原始性能的用例 元数据,我们可以考虑在 FIDL 中以原生方式添加流式传输功能。对于 例如,可以想象定义一种通过套接字进行的传输, 一种类型的值。可以通过实现绑定来跳过发送 元数据,并禁止更改发送者/接收者身份( 因此,每次有新的对等设备连接时,都必须重新建立 )。

@stream
protocol UdpSocketPayload over zx.socket {
    Send(SendMsgPayload);
    -> Recv(RecvMsgPayload);
};

通过使元数据始终特定于某个消息,这也支持在 按消息进行比对例如,我们可以使用一个标志 经过压缩处理。还可以想到,我们可以设计出更紧凑的电线 较少以 内存中的 IPC(例如,无需为指针或对齐预留空间)。通过 备用格式可以由 元数据。

替代方案 3:使用事务性消息标头

持久化期间的传输格式元数据可以与 事务性消息标头

为此,我们将持久化构建为一种传输, 写入对象:

@persistence
type Metadata = table {
    1: foo int32;
    2: bar vector<int64>;
};

// desugars to
protocol MetadataSink over persistence {
    // Ordinal is the hash of `mylib/MetadataSink.Metadata`.
    Metadata(Metadata);
};

这种方法具有以下优点:

  • 重复使用现有的 FIDL 功能。持久性与通过 只不过您要将字节写入某种其他类型的接收器(vmo、 file, socket)。我们还可以稍后添加流式传输或多消息支持 通过添加控制数据包(控制序号)来告知消息大小, 等等
  • 通过重复使用序数哈希检查来减少串扰并提高安全性: 攻击者无法通过在 数据平面(载荷)。此策略与 FIDL 方法的属性。

这看起来是一个很好的泛化传输用例, 一种非常奇怪的协议,只有单向和一次性的: 只发送一种值。接收方没有机会 做出响应。这不会与我们要添加的进化功能很好地搭配使用 例如开放式和封闭式互动。

替代方案 4:使用消息类型信息扩展线上格式元数据

与替代选项 3 相比,这种方法没有那么冒险,我们可以对完全限定的 输入保留消息的名称,并将其添加到线上传输格式元数据 确定将要保留的消息的类型,而不引入提示 代表着成熟的运输技术

这可以减少串扰,但仍然存在其他细微的复杂性:

  • 如何处理类型的重命名:保留类型的名称现在变为 是 ABI 的一部分,因为它会影响元数据中的哈希。
  • 如何使此机制与 FIDL 的事务性 IPC 用途协调一致:主要提案 将独立的编码/解码 API 规定为较低级别的核心功能 FIDL 的代码,其中可构建事务性 IPC 功能 顶部。这种替代方案实现两个独立的功能。具体而言, 电汇格式元数据无法从事务消息派生 标头中,因为后一种方法使用的方法序哈希值对两者都相同 请求和响应类型。

总体而言,我们认为,这一替代方案所带来的安全风险 值得额外进行复杂的工作。

替代方案 5:将独立 API 限制为非资源类型

主要方案推荐了两种用于处理 FIDL 线的公共 API 格式:

  • 独立编码/解码:可以对资源类型进行编码并生成句柄。 由 FIDL 事务性消息传递实现(客户端和服务器)共享 绑定)。
  • 持久性:不允许资源类型;结果都是纯数据。电汇格式 元数据始终作为一个单位,

这是因为我们观察到,使用持久性惯例 FIDL 目前的所有独立用例。

将来的用例可能更适合单独传输或存储 有线格式元数据和载荷。他们可以覆盖独立广告 编码/解码 API,但有一个缺点,那就是允许在 API 中使用句柄, 这种情况并无充分依据。

另一种方法是提供三种类型的 API:

  • 绑定内部独立编码/解码:可对资源类型进行编码。已使用 事务性消息传递实现
  • 公开独立编码/解码:不允许资源类型。
  • 持久性:不允许资源类型。始终为线上格式元数据 。

当用例需要单独提供时,这可以改进非资源保证 传输或存储有线格式元数据,但会导致 API 令人困惑, 因为我们最终有两类编码/解码 API 唯一不同的是 对资源类型的支持这与目标语言因素有关 在有时难以对公众隐藏 API 时存在局限性 。

主提案采用简化路径,并合并前两种类型的提案 。

先验技术和参考资料