RFC-0026:所有信封

RFC-0026:随处可见的信封
状态已拒绝
区域
  • FIDL
说明

通过使信封的紧凑程度提高一倍以上,提高现有信封格式的效率。使用信封作为引用所有内嵌对象的唯一方式。这提高了线格式的一致性,以及协议设计和实现的一致性。

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

拒绝理由

鉴于此 RFC 收到了大量反馈和评论,我们决定撤回(即自行拒绝)此提案。 不过,它仍然包含一些很棒的想法:我们将采纳这些想法,并以范围较小的单独 RFC 发布,以便更清晰地进行讨论,并将单独的独立功能分别纳入各自的 RFC 中。

RFC-0032 是从该 RFC 中分离出来的。

摘要

此 RFC 有两个目标:

  1. 通过将信封压缩到原来的不到一半,提高现有 envelope 格式的效率。
  2. 使用信封作为引用所有内嵌外对象的唯一方式。这提高了线格式的一致性,以及协议设计和实现的统一性。

(1) 和 (2) 的一个副作用是,可以为所有类型(而不仅仅是结构体、句柄、向量、字符串、表和 [可扩展] 联合)高效实现可选性(可为 null)1

设计初衷

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

FIDL 还有几种用于动态调整大小的数据的普遍类型:向量和字符串。这些类型必须是内联的,因为 FIDL 主要对象的大小应是静态已知的。 如果信封可用于表示所有带外数据,我们可以简化协议和实现,从而降低实现成本并减少出错空间。

此外,FIDL 还可以受益于全面、一致的可选性方法。这可带来更好的人体工学体验,并为更多类型提供比当前机制更多的选择,同时简化用户心理模型。 信封以统一的方式为所有类型实现可选性,从而实现这些目标。

设计

信封可以指以下数据:

  • 内嵌,类似于现有的信封格式,或
  • 内嵌,数据存储在信封本身中。 这可用于固定大小且小于 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;

现有信封格式相比,带外信封具有以下变化:

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

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

标记位

带外信封显式具有占用最低有效位的大小和占用最高有效位的句柄数。如信封部分所述,

  • 由于 size 字段的最低位始终为零(因为 size 是 8 的倍数),
  • 信封的最低位将始终为零。

我们将信封的最低位称为标记位

  • 如果标记位为零,则信封的数据为内联
  • 如果标记位为 1,则信封的数据为内嵌

由于内嵌数据的标记位为 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 的 StringVector 的当前有线格式存储为 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),也可以预留大小字段中的三个最低有效位来指示大小未知,在这种情况下,解码器必须遍历带外载荷并自行计算大小。此更改不会影响有线格式,因为字段的结构保持不变。由于解码器可以在编码器更新之前先实现该逻辑,因此也可以作为软过渡来落地。

总而言之,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。鉴于处理这两种有线格式的代码将存在,我们建议使用任一方法来测试软过渡是否可行。如果不是,那就只能顺其自然,进行硬过渡。

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

我们还应利用此线框格式更改来纳入需要进行的其他更改(例如,提议的序数大小更改)。

请注意,与大幅更改了语言绑定的从 FIDL1 到 FIDL2 的过渡相比,此过渡更加轻松。我们不建议将此 FIDL 称为 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 位的类型;我们可以更积极地进行内联。
    • 我们可以内嵌任何不超过 63 位的数据,因为标记位在 64 位封装中仅使用一位。
    • 我们可以使用专门的表示法内嵌小字符串和向量,例如使用一个字节表示元素/字节数,后跟字符串/向量数据。(如需获取灵感,请参阅现有技术)。
    • 我们舍弃了这些方法,尽管它们更高效,因为基于内容而非类型进行内联意味着:(1) 解码器无法提前知道是内联还是外联信封;(2) 更改字段内容意味着可以采用不同的编码方式,这似乎与 FIDL 的目标和静态重点相悖。

在先技术和参考资料

作者从现有的标记指针用法中汲取了大量灵感,这些用法在动态语言和函数式语言中有着悠久的历史。特别是,Objective-C 64 位运行时会大量使用它们,以获得更好的性能(甚至使用专门的 5/6 位编码来处理内嵌字符串)。

由于当前的 64 位平台往往使用 48 位(或更少)来编码指针,因此我们考虑通过位移从解码后的指针中窃取更多位,以尝试在指针中编码离线对象的大小。 不过,有些架构已将物理地址空间扩展到 48 位以上(ARM64x64-64 5 级分页),因此窃取更多指针位可能无法很好地应对未来变化。


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

  2. 截至 2019 年 1 月 28 日,Fuchsia 代码库中似乎有 37 处使用了可选句柄。这是一个保守的数字,因为它不包括可选的协议句柄和协议请求句柄。 

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

  4. 除非我们希望同时允许更多类型的可选性。