RFC-0026:所有信封

RFC-0026:Envelopes everywhere
状态已拒绝
区域
  • FIDL
说明

通过使信封的紧凑程度提高到原来的两倍以上,提高现有信封格式的效率。将封套用作引用所有离线对象的唯一方式。这可以提高线格格式的一致性,以及协议设计和实现的一致性。

作者
提交日期(年-月-日)2019-01-19
审核日期(年-月-日)2019-02-04

拒绝理由

鉴于收到大量有关此 RFC 的反馈和意见,我们决定撤消(即自行拒绝)该提案。尽管如此,该草案仍包含一些非常棒的想法:我们将这些想法作为范围更小的单独 RFC 发布,以便进行更清晰的讨论,并将独立的功能分到各自的 RFC 中。

RFC-0032 是从此 RFC 派生出来的。

摘要

此 RFC 有两个目标:

  1. 通过使封装容器的紧凑程度提高到原来的两倍以上,提高现有 envelope 格式的效率。
  2. 将封套用作引用所有线下对象的唯一方式。这可以提高线格格式的一致性,以及协议设计和实现的一致性。

(1) 和 (2) 的一个副作用是,可选性(可为 null)不仅适用于结构体、句柄、矢量、字符串、表和(可扩展的)联合体,还适用于所有类型1

设计初衷

封装容器是可扩展、可演化的数据结构(表和可扩展联合体)的基础。提高封装容器的效率,可让这些可扩展的结构在性能和线大小至关重要的更多情境中使用。

FIDL 还具有多种用于动态大小数据的普遍类型:矢量和字符串。由于 FIDL 主要对象的大小应是静态已知的,因此这些类型必须是线下类型。如果封装容器可用于表示所有离线数据,我们就可以简化协议和实现,从而降低实现成本并减少出错的可能性。

此外,FIDL 还可以从全面、一致的可选性方法中受益。这可以带来更好的人体工学体验,并支持比当前机制允许的更多类型的可选性,同时简化用户的心理模型。封装容器通过以统一的方式为所有类型启用可选性来实现这些目标。

设计

信封可以是以下任一类型的数据:

  • 离线(类似于现有的信封格式),或
  • inline,即数据存储在封装容器本身中。这适用于固定大小且小于 64 位的“小型”类型。

离线信封

离线信封:

图:线下信封,64 位小端,低 48 位大小,最低有效位为零,16 位 handle_count

作为 C 结构体:

typedef struct {
  uint64_t size:48;  // Low bit will be 0
  uint16_t handle_count;
} fidl_out_of_line_envelope_t;

现有信封格式相比,离线信封有以下变化:

  • 大小(num_bytes)为 48 位,而不是 32 位,支持更大的载荷。
    • 大小包括可能递归编码的任何子对象的大小。
      • 例如,vector<string> 的大小包括外部矢量内部字符串子对象的大小。
      • 这与当前封装容器实现的 size 字段的现有行为一致。
    • 由于线下对象是八字节对齐的,因此合法的线下对象的大小始终是 8 的倍数。这意味着 size % 8 == 0,这意味着
      • 大小字段的最低三位(即大小字段的最低有效位)将为零,因此
      • 封装容器的最低有效位(由于大小字段位于封装容器的最低有效位)也始终为零。
      • 这一点非常重要,下文的标记位部分对此进行了讨论。
    • 如需了解计算递归大小对性能的影响,请参阅下文中的线下封装容器的编码大小
  • handle_count 为 16 位,而非 32 位。
    • 目前无法通过 Zircon 通道发送超过 64 个句柄;我们认为 16 位足以满足未来的需求。
    • handle_count 包含所有递归子对象的句柄数。
  • “存在/不存在”字段已被舍弃。
    • 存在性由 sizehandle_count 字段中的非零值表示。
    • 缺失表示 sizehandle_count 字段均为零。
      • 我们将其称为“零封装容器”。

解码器可以使用指向封套数据的指针覆盖封套,前提是它们知道封套内容的静态类型(架构)。如需有关如何在内容类型未知的情况下处理封装容器的建议,请参阅解码器回调部分。

标记位

线下封装容器明确将大小占据最低有效位,将句柄计数占据最高有效位。如封装容器部分所述,

  • 由于大小字段的最低位始终为零(因为大小为 8 的倍数),
  • 封装容器的最低位也始终为零。

我们将封装容器的最低位称为标记位

  • 如果标记位为零,则封装容器的数据为离线
  • 如果标记位为 1,则封装容器的数据为inline

由于内嵌数据的标记位为 1,因此在需要 64 位对齐的架构上,内嵌封装容器也不能是实际指针,因为指针将是 8 的倍数,并且最低三位也需要为零。这对于解码器能够区分内嵌封装容器和实际指针非常有用,因为解码器通常会使用指向封装容器内容的指针覆盖外部封装容器(但不覆盖内嵌封装容器)。

内嵌信封

内嵌信封的编码方式如下:

图:在线条封装容器中,采用 64 位小端字节序,最低有效位为值 1 表示标记,31 位预留,然后是 8 位、16 位或 32 位内嵌数据

作为 C 结构体:

typedef struct {
  uint8_t tag:1;  // == 1
  uint32_t reserved:31;
  union {
    _Bool bool;
    uint32_t uint32;
    int32_t int32;
    uint16_t uint16;
    int16_t int16;
    uint8_t uint8;
    int8_t int8;
    float float32;
    zx_handle_t handle;  // Only when decoded (see Handles for more details)
  };
} fidl_inline_envelope_t;
  • 内嵌封装容器的 LSB 设置为 1,这将其与线外封装容器和实际指针区分开来。
  • 封装容器的 32 个高位用于表示内嵌值,该值可以是 int8uint8int16uint16int32uint32float32bool 或句柄。
    • 如果值的宽度小于 32 位,则上 32 位中的最低位用于表示该值,这是标准的低字节序表示法。
  • 编码器必须将预留位编码为零,除非未来的 RFC 指定了如何解读这些位。
  • 解码器和验证程序必须忽略预留位,除非未来的 RFC 指定了如何解读这些位。
  • 解码器应在解码过程中保持内嵌封装容器的原样。
    • 由于内嵌数据包含内嵌数据,而无需引用外部数据,因此解码器在原地解码时无需将其替换为指针(与外部封装容器不同)。

编码器应以离线还是内嵌方式进行编码?

编码器必须:

  • 如果类型为 bool、(u)int8、(u)int16、(u)int32float32 或句柄,则内嵌编码数据。(非正式定义:如果类型是固定大小且小于等于 32 位。)
  • 对所有其他类型的数据进行离线编码。 (非正式定义:如果类型大于或等于 64 位或可变大小。)

句柄

有三种上下文可用于声明句柄:

  1. 不可扩展容器中不可选的句柄,例如 struct S { handle h; };
  2. 不可扩展容器中的可选手柄,例如 struct S { handle? h; };
  3. 可扩展容器中的手柄,例如 table T { handle h; }

对于 (1),即不可扩展容器中的非可选句柄,我们建议保留现有线格格式,即 uint32。非可扩展容器中的非选用句柄无需作为封装容器,因为封装容器旨在携带可选数据或动态大小的数据。

对于 (3),可扩展容器中的句柄:由于封装容器是可扩展容器的基础,因此必须使用封装容器对句柄进行编码。如需对句柄进行编码,编码器必须将其编码为离线封装容器,并将 size 设为 0,将 handle_count 设为 1:

图:小端 64 位数据字段,其中底部 48 位大小设置为零,下一个 16 位表示 handle_count 设置为 1

此编码会指示解码器在线外句柄表中查找句柄值。如果解码器希望就地解码,则应:

  • 在线外句柄表中查找句柄,以确定实际句柄值。
  • 将标记位设置为 1,将封装容器从离线更改为内嵌。
  • 将 fidl_inline_envelope_t 结构体的句柄字段设置为实际句柄值。

图:小端 64 位数据字段,最低有效位标记设为 1,接下来的 31 位预留,接下来的 32 位为 handle_value

如需查看编码/解码句柄示例,请参阅示例部分。

我们之所以选择这种双重编码/解码形式,是因为它与线下封装容器编码和内嵌封装容器编码都兼容。虽然这确实会导致封套中出现专门的句柄代码,但我们认为,与需要更多编码的更简单的代码相比,采用更统一的数据编码(即编码更少)是一个更好的折衷。

对于 (2),不可扩展容器中的可选句柄:我们还建议为线格格式使用与上下文 (3) 相同的封装容器表示法,即双线外编码/内嵌解码形式。遗憾的是,这种可选手柄表示法不如现有可选手柄线格格式(即 uint32)紧凑。不过,我们仍建议使用基于封装容器的表示法,因为

  • 为可选句柄使用封套与为任何可选类型使用封套是一致的,
  • 与其他消息类型相比,可选句柄在 FIDL 消息中相对较少2,因此额外的 4 字节封装容器开销不应显著影响消息大小,
  • 如果为可选句柄保留现有的 uint32 线格格式,则会导致句柄有三种编码和三个单独的代码路径:非可选、可选和封装容器中的句柄。对可选项使用封装容器表示法可消除一个编码和一个代码路径,从而提高一致性并减少专用代码。

下面的设计决策部分明确列出了 (2) 的编码(不可扩展容器中的可选句柄),因为可选句柄的更紧凑的 uint32 表示法值得考虑。

字符串和矢量

非 null 的字符串矢量的当前线格格式存储为 16 个字节:

  • 一个 uint64(用于表示元素数 [向量] 或字节数 [字符串])
  • 一个 uint64,用于表示存在/不存在/指针。

我们建议使用封装容器来表示可为 null 或不可为 null 的字符串和矢量:

  • 元素数(向量)或字节数(字符串)移出了行外
    • 这样,矢量/字符串便可(仅)由封装容器表示,因此封装容器成为所有 FIDL 类型引用任何离线数据的唯一方式,从而为所有离线数据实现一致的表示。
    • 矢量/字符串内容位于单独的离线对象中,紧随元素/字节数之后。
  • 存在/不存在取决于封装容器为零还是非零。

对于矢量,请注意矢量元素数与封装容器的大小不同:

  • 封装容器的大小为向量元素数乘以元素大小。
  • 如果矢量包含子对象(例如 vector<Table>vector<vector<string>>),封装容器的大小包括所有递归子对象的大小。

可为 null 的字符串/矢量以及可扩展容器中的字符串/矢量与非 null 字符串和矢量采用相同的表示方式:零封装容器用于表示不存在的字符串/矢量。

反之,如果字符串/矢量不可为 null,则验证器在遇到零封装容器时必须出错。

对于使用 C 绑定的代码,这可能成为一项对源代码的破坏性更改,因为此类代码会预期 fidl_vector_tfidl_string_t 的内存布局与线格格式完全匹配。不过,我们可以在线格格式更改(例如,将 C API 更改为使用函数或宏)之前实施过渡计划,以实现平滑过渡。

请注意,您仍然可以通过灵活的数组成员(例如 struct { uint64 element_count; element_type data[]; };)将这种新的字符串/矢量布局表示为 C 结构体。

可选(可为 null)类型

目前,结构体、字符串、矢量、句柄、联合体、表和可扩展联合体可以是可选的(可为 null)。

在所有位置使用封装容器后,所有类型均可为可选

  • 当前可选数据随封装容器一起存储,可以离线也可以内嵌。
  • 缺少的可选数据会存储为零封装容器。

请注意,对于小型类型,内嵌数据可以像非可选类型一样紧凑地存储可选类型,具体取决于容器对对齐的要求。

适用于编码/解码表单的 C/C++ 结构体

封装容器的编码形式可以由内嵌封装容器或外部封装容器的并集表示。同样,解码后的封装容器可以是内嵌的、指向封装容器数据的指针,也可以是回调确定的值(如需了解详情,请参阅解码器回调部分)。

typedef union {
  fidl_inline_envelope_t inline;            // Low bit is 1
  fidl_out_of_line_envelope_t out_of_line;  // Low bit is 0
} fidl_encoded_envelope_t;

typedef union {
  fidl_inline_envelope_t inline;  // Low bit is 1
  void* data;                     // Low bit is 0
  uintptr_t callback_data;  // Value determined by callback (see Decoder Callback)
} fidl_decoded_envelope_t;

static_assert(sizeof(fidl_encoded_envelope_t) == sizeof(void*));
static_assert(sizeof(fidl_decoded_envelope_t) == sizeof(void*));

未知数据

当接收器(验证器和解码器)在可演化数据结构(例如表格或可扩展联合体)中使用时,它们可能不知道封装容器的类型。如果收件人不知道信封的类型,请执行以下操作:

  • 您可以放心地忽略内嵌信封。
    • 必须使用外部封装容器(而非内嵌封装容器)编码手柄,这样所有内嵌封装容器都可以安全地忽略。
  • 可以最少地解析并跳过离线信封。
    • 封装容器的大小决定了要跳过的离线数据量。
    • 如果封装容器的句柄数不为零,验证程序必须处理指定数量的句柄。
      • 默认处理行为必须是关闭所有句柄。
    • 如果解码器希望就地解码,则可以使用指向封套内容的指针覆盖未知封套。
      • 如果解码器确实使用指针覆盖封装容器,则会丢失封装容器中的大小和句柄计数信息。如果存在此问题,请参阅解码器回调部分,了解替代方案。

请注意,如果需要跳过许多未知类型,则通过在线外封装容器中嵌入大小,可以快速在 FIDL 消息中进行线性跳转。

解码器回调

未知数据部分所述,解码器可能会覆盖未知封装容器:如果发生这种情况,解码器将丢失大小和句柄计数信息。或者,解码器可以附加一个回调,该回调可以处理封装容器并替换默认行为。回调 API 可能类似于以下函数原型:

void set_unknown_envelope_callback(
    unknown_envelope_callback_t callback,  // a callback
    void* context                          // client-specific data storage
);

typedef uintptr_t (*unknown_envelope_callback_t)(
    const void* message,  // pointer to the envelope's containing message
    size_t offset,        // offset in the message where the unknown envelope is
    size_t size,          // the envelope's size
    size_t handle_count,  // the envelope's handle count
    const char* bytes,    // pointer to the envelope's data
    void* context         // a context pointer set via set_unknown_envelope_callback()
);

该回调会返回一个 uintptr_t,解码器可以使用该值覆盖未知封装容器。这样,解码器就可以从未知封装容器中复制大小和句柄计数,并使用指向解码器自己的自定义数据结构的指针覆盖封装容器。

线下信封的编码大小

此 RFC 要求线下信封具有适当的(递归)大小,以便呈现线下数据。此要求可能会给编码器带来额外的负担,因为如果接收器预计知道封装容器的类型,则不需要大小字段,因为解码器可以计算大小 3。因此,编码器执行额外工作似乎没有明显的好处。此参数也适用于句柄数。

不过,我们仍建议您务必提供大小和手柄数量,原因如下:

  1. 一致性:要求大小意味着封装容器编码对于所有用例(无论是否位于可扩展容器内)都是一致的。一致性提高有助于减少代码量,并简化认知模型。
  2. 我们日后可以更改此设置。 未来的 RFC 可以选择为大小使用标记值(例如 UINT48_MAX),也可以预留大小字段中的三个 LSB 之一,以指示大小未知;在这种情况下,解码器必须遍历线下载荷并自行计算大小。由于字段的结构保持不变,因此此更改不会影响线格格式。它也可以作为软过渡进行发布,因为解码器可以在更新编码器之前先实现该逻辑。

总体而言,RFC 作者认为,要求对未知大小进行编码可能是过早优化,并建议从简单、更一致、统一的设计开始。如果我们日后认为应重新考虑这一决定(例如,可用零拷贝矢量化 I/O 编码器,因此编码器无需修补封装容器即可写入正确的大小),则可以明确地将其作为软过渡来实现。

示例

可选的 uint,以内嵌方式存储:

uint32? u = 0xdeadbeef;  // an optional uint: stored inline.

C++ 表示法:

    vector<uint8_t> object{
        0x01, 0x00, 0x00, 0x00,                          // inline tag
                                0xEF, 0xBE, 0xAD, 0xDE,  // inline data
    };

可选的 vector<uint16>(存储在线下):

vector<uint16>? v = { 10, 11, 12, 13, 14 };  // an optional vector<uint16>; stored out-of-line.

离线大小为 24:

  • 8 个字节用于存储为自己的次要对象存储在行外的元素数量,
  • + 10(用于向量内容,5 个元素 * sizeof(uint16_t)),
  • = 18,向上舍入为 24 以便对齐。

C++ 表示法:

    vector<uint8_t> object{
      0x18, 0x00, 0x00, 0x00, 0x00, 0x00,              // envelope size (24)
                                          0x00, 0x00,  // handle count
    };

    vector<uint8_t> sub_objects{
      // element count
      0x05, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // vector data
      0x0A, 0x00, 0x0B, 0x00, 0x0C, 0x00, 0x0D, 0x00,
      0x0E, 0x00,
      // padding
                  0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
    };

一个包含三个字段的 table

table T { 1: int8 i; 2: reserved; 3: int64 j; } = { .i: 241, .j: 71279031231 };

C++ 表示法:

    // a table is a vector<envelope>, which is represented with an
    // out-of-line envelope
    vector<uint8_t> object{
      0x28, 0x00, 0x00, 0x00, 0x00, 0x00,              // envelope size (40)
                                          0x00, 0x00,  // handle count
    };

    vector<uint8_t> sub_objects{
      // vector element count (max table ordinal)
      0x03, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,
      // vector[0], 1: int8, stored inline
      0x01, 0x00, 0x00, 0x00,                          // inline tag
                              0xF1, 0x00, 0x00, 0x00   // 241
      // vector[1], 2: reserved
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00,  // zero envelope
      // vector[2], 3: int64, stored out-of-line
      0x08, 0x00, 0x00, 0x00, 0x00, 0x00,              // envelope size
                                          0x00, 0x00,  // handle count
      // vector[2] content
      0xBF, 0xB3, 0x8F, 0x98, 0x10, 0x00, 0x00, 0x00   // 71279031231
    };

标识名:

handle h;  // decoded to 0xCAFEF00D

C++ 表示法:

    vector<uint8_t> encoded_form{
      0x00, 0x00, 0x00, 0x00, 0x00, 0x00,              // envelope size
                                          0x01, 0x00,  // handle count
    };

    vector<uint8_t> decoded_form{
      0x01, 0x00, 0x00, 0x00,                          // inline tag
                              0x0D, 0xF0, 0xFE, 0xCA,  // inline data
    };

实施策略

此 RFC 是一项重大的线格格式更改。两个 FIDL 对等方都需要了解新线格格式,并将其理解传达给对等方,以便双方都使用新格式。

可以进行平滑过渡。 两种方法如下:

  1. 事务性消息标头中有一个 uint32 预留/标志字段。我们可以为发起方端预留 1 位,以指明它了解新线格格式,并分阶段进行软过渡:
    1. 确保所有客户端和服务器都能理解旧版和新版线格格式。我们将继续使用旧的线程格式。
    2. 通过让对等方在事务消息标头中设置位,启用新的线格格式。如果双方都设置了该位,则双方都可以切换到新线格格式。
    3. 在柔和过渡完成所有层的过渡后,Fuchsia 的所有部分都可以使用新线框格式。我们可以移除在事务性邮件标头中设置该位。
    4. 删除旧线格格式的代码,并取消预留事务消息标头位。
  2. 我们可以使用 [WireFormat=EnvelopeV2] 属性(或类似属性)修饰特定的 FIDL 消息类型和/或接口,以指明消息/接口应使用新的线格格式。
    1. 虽然使用 [WireFormat] 属性修饰接口似乎更适合进行线格格式更改,但对结构体实现 WireFormat 更改应该更容易,因为结构体可用于不同的接口,而绑定需要额外的逻辑来确定结构体的使用上下文。
    2. 我们建议接口 [WireFormat] 属性仅影响接口方法实参的线格格式,而不会递归地影响实参的结构体。
    3. 这样一来,团队就可以选择部分迁移并选择启用新线框格式,从而按照自己的节奏进行迁移。
    4. 所有结构体和接口都具有 [WireFormat] 属性后,我们就可以舍弃旧的线格格式,假定所有结构体和接口都使用新的线格格式,并忽略该属性。

这两种软过渡方法都需要大量的开发时间和测试时间,并且存在出错的可能性。实现代码以正确执行任一方法、按计划执行以及成功跟进以移除旧代码,需要付出大量努力。

我们可能会同时拥有用于处理旧版和新版线程格式的代码;否则,在我们实现对新线程格式的支持时,将无法逐步提交 CL。鉴于系统中将存在用于处理这两种线程格式的代码,我们建议您通过原型设计来确定是否可以使用任一方法实现平滑过渡。如果没有,那就只能接受c'est la vie(这就是生活);这是一个艰难的转变。

对于软过渡或硬过渡,Fuchsia 中手动创建 FIDL 消息的任何实例都需要升级到新的线格格式。

我们还应利用此线程格式更改来整合需要进行的其他更改(例如,建议的序数大小更改)。

请注意,与从 FIDL1 迁移到 FIDL2 相比,这项迁移更为简单,因为后者会显著更改语言绑定。我们不建议将其称为 FIDL3,因为没有任何对用户可见的更改4

向后兼容性

我们提议的线格格式更改与 API(源代码)兼容,但有一个例外情况:如果我们将矢量/字符串元素计数移出行,C 绑定将成为破坏性 API 更改。在新的线格格式发布之前,我们可以通过提前规划并使用宏或函数提取当前的 C 绑定来缓解此问题。

线格格式更改与 ABI 不兼容,但可以通过实现策略部分中列出的策略实现与现有代码的 ABI 兼容性。

性能

此 RFC 显著缩减了信封所需的大小,这似乎总体上会带来显著的净效益。不过,整体效果影响尚不明确。为了获得更好的性能,请执行以下操作:

  • 使用可扩展数据结构(表和可扩展联合体)的 FIDL 消息将变得更加紧凑。
  • 为封装容器和可选性提供统一的表示法可能会缩减代码大小并提高缓存局部性,因为封装容器代码可以共享。

不过:

  • 如果可扩展的数据结构因效率更高而变得更加普遍,那么与使用不可扩展的数据结构相比,这可能不利于减少消息的占用空间,并且会增加动态分配。
  • 为所有类型引入可选性可能会使 FIDL 消息略微变大,因为用户可能会使用此功能将之前的一些非可选类型设为可选。
  • 如果我们决定对可选句柄使用封装容器编码,可选句柄的效率会降低。
  • 对线外封装容器进行大小编码中所述,在封装容器中对接收器将知道的类型进行大小和句柄数编码,是与当前行为相比的性能回归。

工效学设计

  • 可为所有 FIDL 类型启用可选性。 这是一种人体工学改进,因为可选性变得一致,而不仅仅适用于特定类型。
  • 可扩展数据结构的效率更高,因此可在效率至关重要的更多情境中使用,这样一来,用户无需过多担心其性能,并且可以在之前需要使用不可扩展结构的地方获享可扩展性带来的好处。
    • 我们甚至可能建议默认使用表来处理 FIDL 数据结构,并将结构体保留用于高性能情境。
    • 可扩展的联合体 (RFC-0061) 已在尝试移除静态联合体。

文档

  • 线上传输格式文档需要更新。
  • 更新文档时,应将封套解释为一种一流的概念:这样,当读者遇到可选性和可扩展数据结构的线格格式时,便能更好地进行认知分块
  • 我们应更新 FIDL 样式指南,以便就何时应使用可选类型(而不是具有标记值的非可选类型)提供建议。

安全

  • 此 RFC 不应对安全性产生重大影响。
  • 不过,用于操控线下封装容器格式和内嵌封装容器格式的位操作应经过充分测试且谨慎使用,以确保代码能够妥善处理边界情况。我们确实认为,使用标准 C/C++ 结构体/联合体来表示封装容器(而不是手动位移和掩码)大大提高了我们对代码正确性的信心。

测试

  • 由于此 RFC 会更改封装容器的线格格式,因此我们认为现有的 FIDL 测试套件(尤其是兼容性测试)将能够充分测试使用封装容器的所有场景。
  • 我们将添加用于对线下表单和内嵌表单进行封装解析、编码和解码的单元测试,因为这是一个容易出错的方面。
  • 如果我们同意以软过渡的方式实施线缆格式更改(请参阅实施策略部分),我们将添加测试,以便对等方进行协商,并可能改用新线缆格式。
  • 如果我们同意在此更改中公开所有类型的可选性,则需要为所有可变为可选的类型添加测试。

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

  • 如果我们认为此提案中的效率提升不值得付出实现成本,则可以保留现有线格格式。如果是,我们需要找到一种替代策略,以便为所有类型实现可选性。
  • 与在所有情况下都使用封装容器相比,为可扩展容器和可选类型使用专用表示法可能更高效。不过,既然有了这个 RFC,我们显然认为封装容器提供的更高通用性和一致性胜过专用表示法带来的效率提升。

设计决策

虽然此 RFC 提供了建议,但我们仍在积极征求有关以下决策的反馈和共识:

  • 如需了解如何将元素数(矢量)和字节数(字符串)移出行(这会影响 C 绑定),请参阅字符串和矢量部分。我们可以选择不这样做,但代价是一致性会降低:字符串和矢量会成为对用于所有离线引用的信封的例外情况。(封装容器仍然可用于引用离线矢量/字符串数据。)
  • 我们是考虑软过渡还是硬过渡?如需了解优点和缺点,请参阅实现策略部分。
  • 我们建议在线下封装容器中使用 48 位大小和 16 位手柄。相比之下,当前信封格式使用 32/32 位。48 位大小是否合理?
    • 对于大小,我们可以通过在编码形式中右移 2 位来编码大小(最多 50 位),因为封装容器大小始终是 8 的倍数。(我们不能右移三位,因为这样无法保证标记位为 0。)解码器会向左移两位以确定大小。 这样一来,我们就失去了两个可能用于标志或更多标记的额外位。
    • 虽然当前的 64 位架构通常不允许对整个 64 位内存空间进行寻址,并且通常最多允许 48 位,但某些架构已经支持最多 57 位的地址空间。如需了解详情,请参阅参考文档
    • 使用 16 位句柄是否合理?
  • 我们建议使用封装容器在不可扩展的容器中编码可选句柄,这比当前的可选句柄编码更不紧凑(8 字节对比 4 字节)。
    • 在紧凑性和更专业的代码与一致性之间,需要进行权衡。我们认为一致性和统一性比专用且更紧凑的表示法更重要,因为可选手柄可能是一种相对较少的用例。(代码中 37 次可选用法,而非可选用法为 187 次。)
  • 我们是否立即启用可选功能?
    • 我们建议在升级线格格式后单独进行一次转换,以便为所有类型公开可选性,因为此更改可以逐步完成。
    • 实现此类可选性需要更改解析器、编码器、验证器和解码器,这似乎足够大,值得单独进行过渡。
  • 我们建议内嵌小于等于 32 位的类型;我们可以更积极地内嵌。
    • 由于标记位在 64 位封装容器中只占用一个位,因此我们可以内嵌任何小于等于 63 位的位数据。
    • 我们可以通过为小字符串和矢量使用专用表示法(例如,一个字节用于表示元素/字节数,后跟字符串/矢量数据)来内嵌小字符串和矢量。(如需寻找灵感,请参阅之前的海报图片)。
    • 虽然这些方法效率更高,但我们还是舍弃了它们,因为根据内容(而非类型)进行内嵌意味着:(1) 解码器无法根据类型提前知道是否应预期内嵌封装容器还是外部封装容器;(2) 更改字段的内容意味着可以采用不同的编码方式,这似乎与 FIDL 的目标和静态重点相悖。

在先技术和参考文档

作者从标记指针的现有用法中获得了很多灵感,这些用法在动态语言和函数语言中已有悠久的历史。具体而言,Objective-C 64 位运行时会大量使用这些函数来提升性能(甚至会为内嵌字符串使用专用的 5/6 位编码)。

由于当前的 64 位平台通常使用 48 位(或更少)来编码指针,因此我们考虑通过位移从解码的指针中窃取更多位,以尝试在指针中编码线外对象的大小。不过,某些架构已经将其物理地址空间扩展到了 48 位以上(ARM64x64-64 5 级分页),因此盗用更多指针位可能不太适合未来。


  1. 封装容器可为所有类型启用可选性;不过,可以(也许应该)单独向最终用户公开此可选性。 

  2. 截至 2019 年 1 月 28 日,Fuchsia 代码库中似乎有 37 次使用了可选句柄。这个数字是保守估计的,因为其中未统计可选协议句柄和协议请求句柄。 

  3. 这仅适用于不可扩展容器(即结构体和静态联合体)中的封装容器。可扩展容器必须编码递归大小,因为解码器可能不知道类型,并且需要知道要忽略多少数据。 

  4. 除非我们希望同时允许对更多类型进行选择,