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 也会大有裨益。 这样可以更好地实现人体工程学,获得比当前机制所允许的更多类型的可选性,并简化用户思维模型。信封通过以统一方式为所有类型启用可选性来实现这些目标。

设计

信包可以指代以下任一数据:

  • out of-line(与现有的信封格式类似),或者
  • 内嵌:在这种情况下,数据存储在信封本身中。这可用于固定大小且小于 64 位的“小尺寸”类型。

外页信封

外行信封是指:

图:行外包,64 位小端字节序,低 48 位大小,最低有效位 0,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)将为零,因此
      • 信封的 LSB(由于大小字段位于信封的 LSB 中)也将始终为零。
      • 这一点很重要,如下面的标记位中所述。
    • 如需了解计算递归大小对性能的影响,请参阅下文的外行信封的编码大小
  • handle_count 是 16 位,而不是 32 位。
    • 目前无法通过 Zircon 通道发送超过 64 个句柄;我们认为 16 位可以提供足够的提升空间来满足未来的需求。
    • handle_count 包含所有递归子对象的句柄计数。
  • 在线/缺失字段会被丢弃。
    • 在线状态在 sizehandle_count 字段中由非零值表示。
    • 不存在由 sizehandle_count 字段均表示为零。
      • 我们称之为零信封

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

标记位

外联信包明确其大小占据最低有效位,而句柄计数占据最高有效位。如信封部分中所述,

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

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

  • 如果标记位为零,则信包的数据“不在线”。
  • 如果标记位为 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 位表示句柄计数设为 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. 软过渡通过所有层后,所有紫红色均可使用新的有线格式。我们可以移除交易邮件标头中的位设置。
    4. 删除旧传输格式的代码,并取消预留事务性消息标头位。
  2. 我们可以使用 [WireFormat=EnvelopeV2] 属性(或类似属性)来修饰特定的 FIDL 消息类型和/或接口,指示消息/接口应使用新的传输格式。
    1. 虽然使用 [WireFormat] 属性来装饰接口似乎更适合于传输格式更改,但对结构体实现 WireFormat 更改应该更容易,因为结构体可以在不同的接口中使用,并且绑定需要额外的逻辑来确定该结构体适用的上下文。
    2. 我们建议接口 [WireFormat] 属性仅影响接口的方法参数的线路格式,而不会递归影响参数的结构。
    3. 这实现了部分迁移并选择启用新的传输格式,并让团队可以按照自己的节奏推进工作。
    4. 一旦所有结构体和接口都具有 [WireFormat] 属性,我们就可以丢弃旧的传输格式,假设所有结构体和接口都使用新的传输格式,并忽略该属性。

这两种软转换方法都涉及大量的开发时间、测试时间和出错空间。实现代码以正确执行任一方法、执行计划并成功跟进以移除旧代码,这都是一项庞大的工作。

我们很可能会有同时处理新旧有线格式的代码;否则,由于我们实现了对新有线格式的支持,将无法逐步推出 CL。鉴于处理这两种传输格式的代码都会存在,我们建议使用任一方法进行软转换的原型设计。否则,c'est la vie;硬转换。

对于软转换或硬转换,在 Fuchsia 中手动滚动 FIDL 消息的任何实例也需要升级到新的有线格式。

我们还应该使用此传输格式更改来折叠需要发生的其他更改(例如建议的序数大小更改)。

请注意,这比 FIDL1 到 FIDL2 的过渡更容易,因为 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 位以编码形式右移 2 位来编码最高 50 位的大小,因为信封大小始终是 8 的倍数。(我们无法右移 3 位,因为这无法保证标记位为 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. 信封用于为所有类型启用可选性;但是,您可以(并且应该)单独向最终用户公开此可选性。enable

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

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

  4. 不过,如果我们希望同时执行操作,允许选择更多类型即可。