RFC-0138:处理未知交互

RFC-0138:处理未知交互
状态已接受
领域
  • FIDL
说明

我们扩展了 FIDL 语义,让对等方能够处理未知交互。

Gerrit 更改
作者
  • pascallouis@google.com
审核人
提交日期(年-月-日)2021-05-25
审核日期(年-月-日)2021-10-27

摘要

我们扩展了 FIDL 语义,让对等方能够处理未知交互,即接收未知事件或接收未知方法调用。为此:

  • 我们在 FIDL 语言中引入了灵活交互和严格交互。灵活交互(即使未知)可以由对等方妥善处理。严格互动会导致帐号突然终止。

  • 我们针对协议引入了三种操作模式。封闭协议是指始终不允许未知交互的协议。相反,开放协议允许任何种类的未知交互。最后,ajar 协议仅支持单向未知互动。

纵览 FIDL 对进化的支持

在深入了解此方案的细节之前,最好先了解 FIDL 如何解决演化问题。

此问题涉及两个方面:源代码兼容性 (API) 和二进制兼容性 (ABI)。

API 兼容性旨在确保针对所生成代码编写的用户代码在更改后,仍然可以针对生成的代码进行编译。例如,可以合理预期,向 FIDL 库添加新声明(例如定义新的 type MyNewTable = table {};)不会导致使用此库的现有代码无法编译。

您可以通过三管齐下的方法来解决源代码兼容性问题:

  1. 使尽可能多的更改与来源兼容(例如 RFC-0057:默认无句柄);
  2. 提供明确的保证(例如 RFC-0024:强制性来源兼容性);
  3. 提供版本控制(例如 RFC-0083: FIDL 版本控制)。

单独的 ABI 兼容性旨在提供针对不同版本的库构建的程序的互操作性。例如,两个程序可能对表架构有不同的理解,但能够成功通信。

实现 ABI 兼容性可分为三个部分:

  1. 静态兼容性涉及到在数据级别实现互操作性,即具有同一表架构的两个对等方何时可以互操作?
  2. 动态兼容性假设所有数据类型都兼容,并侧重于在对等方具有不同版本的协议(例如,不同的方法)时实现互操作性;
  3. 最后,在某些情况下,无法使用不同的协议,在这种情况下,解决方案是了解每个对等方的功能(协商),然后根据这一点调整通信(所使用的协议)。

在寻求“本地灵活性”时(例如,向在其他方面几乎未更改的操作模型中添加少量内容),动态兼容性特别适合。在其他情况下,例如 fuchsia.io1 相对于 fuchsia.io2,则需要网域模型偏移。需要具有“全局灵活性”,并且所寻求的解决方案都属于协议协商类别。

我们在本 RFC 中专门讨论的机制(严格且灵活的互动)改进了动态兼容性的现状 (2)。

术语

关于协议的组合模型的提醒。

两个对等方之间的通信属于互动。互动从请求开始,可能需要选择性地要求响应

请求和响应都是事务性消息,以标头(“事务性标头”)表示,后面可以视需要添加载荷1

互动是有方向的,我们将两个对等端分别命名为“客户端”和“服务器”。客户端与服务器交互从客户端到服务器的请求开始,如果存在反向请求,则会提供响应。与此类似,我们会谈论服务器与客户端的交互

我们通常使用“触发并忘记”或“单向”一词表示客户端发起的无响应互动,使用术语“调用”或“双向”表示需要响应的互动(在当前模型中始终为客户端发起的互动)。当服务器是无响应互动的发起对等方时,通常称为“事件”。2

协议是一组交互。我们将“会话”定义为使用协议在客户端与服务器之间通信的特定实例,即客户端与服务器之间的一系列交互。

应用错误是指遵循错误语法的错误。传输错误是指因内核错误(如写入已关闭的通道)或 FIDL 中的错误而发生的错误。

设计初衷

Fuchsia 的核心原则是可更新:软件包设计为彼此独立更新。即使是驱动程序也应该具备二进制文件稳定性,以便设备可以无缝更新到较新版本的 Fuchsia,同时保留其现有驱动程序。FIDL 在实现这种可更新性方面发挥着核心作用,并且其主要设计目的是定义应用二进制接口 (ABI),从而为向前和向后兼容性奠定坚实的基础。

具体而言,我们希望允许对它们之间通信协议的理解略有不同的两个对等方安全地进行互操作。更好的一点是,我们希望获得强有力的静态保证,确保两个对等设备“兼容”。

为了在编码和解码 FIDL 类型方面提供灵活性和保证,我们做了大量的工作,我们称之为“静态兼容性”。我们引入了 table 布局union 布局),选择了显式 union 序数,引入了 strictflexible 布局修饰符,引入了protocol序数哈希处理降低了 protocol 对消息进行事务处理的未来冲突概率,并且对标头进行了前序哈希处理

现在,我们开始研究动态灵活性和保证,我们称之为“动态兼容性”。假设两个对等体在静态时兼容,即它们用于进行交互的所有类型在静态兼容下,动态兼容性是指这两个对等体能够成功互操作,其中任何对等端都不会因意外交互而中止通信。

利益相关方

  • 教员:jamesr@google.com。
  • 审核者
    • abarth@google.com (FEC)
    • bprosnitz@google.com (FIDL)
    • ianloic@google.com (FIDL)
    • yifeit@google.com (FIDL)
  • 咨询人员
    • jamesr@google.com
    • jeremymanson@google.com
    • jsankey@google.com
    • tombergan@google.com
  • 社交:RFC 草稿已与 FIDL 团队共享,并与 Fuchsia 团队的各个成员进行讨论。该指南在工程委员会讨论邮件名单 (eng-council-discuss@fuchsia.dev) 上获得了广泛共享。

设计

我们引入了“灵活互动”和“严格互动”的概念。简而言之,即使未知,灵活交互也可以由对等方妥善处理。相反,如果接收方对此不了解,则会导致该对等方突然终止会话的严格交互。我们这里所说的互动“严格程度”是指灵活互动还是严格互动。请参阅灵活和严格交互的语义

如果没有安全措施,灵活交互可能会在无意中以危害隐私的方式使用:

  • 例如,考虑使用旨在改进的渲染引擎。新版本添加了 flexible SetAlphaBlending(...); 单向交互,目的在于表明以旧版渲染程序为目标平台的新客户端会直接忽略其设置(但大部分渲染仍然有效)。现在,如果该新方法与特殊的 PII 呈现模式 StartPIIRendering(); 相关,则停止处理旧版渲染器至关重要,而不是忽略这一点,因此适合使用 strict 互动。
  • 另一个示例是恶意对等方试图发送各种消息,以反思方式发现暴露的表面,以查看可理解的信息。通常,反射功能会带来额外的性能成本,并且会导致隐私问题(所公开的信息可能比您意识到的要多)。按照原则,FIDL 选择禁止反射,或者要求明确选择启用。

因此,我们另外引入了三种协议运行模式:

  • 封闭协议是指允许或期望没有灵活交互的协议,无法接收灵活交互。
  • 开放式协议是指允许进行任意灵活交互(单向或双向)的协议。此类协议提供最大的灵活性。
  • ajar 协议允许进行灵活的单向交互(即“即发即弃”调用和事件),但不允许灵活的双向交互(如果对等方不了解此方法,则不能进行方法调用)。

如需了解详情,请参阅协议的语义

严格且灵活的交互的语义

严格交互的语义非常简单:当收到未知请求(即接收方不知道请求顺序的请求)时,对等方会突然终止会话(通过关闭通道)。

灵活交互的目标是让收件人能够妥善处理未知交互。这对设计具有一些指导意义。

灵活互动的发送者必须知道其请求可能会被接收者忽略(因为它无法理解)。

接收者必须能够看出此请求是灵活的(而不是严格),并采取相应的措施。

由于双向交互需要接收方响应发送者,因此未知请求的接收方必须能够构建不含任何其他详细信息的响应。接收方必须告知发送者无法理解该请求。为了满足此要求,灵活双向交互的响应为结果联合(请参阅详细信息)。

它基于以下语义:在单向交互中,发送方无法判断接收方的请求是已知还是未知。使用灵活的单向交互时,FIDL 编写者应注意其整体协议的语义。

值得注意的是,单向交互在一定程度上属于“尽力而为”,因为发送方无法判断对等方是否收到了互动。不过,渠道可提供排序保证,以确保互动的排序是确定的并且是已知的。严格的单向交互可确保当且仅当了解之前的交互时,某些交互才会发生。例如,日志记录协议可能具有 StartPii()StopPii() 严格交互,以确保任何对等方都不会忽略这些交互。

如需了解在选择严格互动和灵活互动时要考虑的权衡,另请参阅:

开放、封闭和 ajar 协议的语义

closed 协议的语义存在限制,只有严格的交互,没有灵活的交互。如果 closed 协议有任何 flexible 交互,这种情况属于编译时错误。

ajar 协议的语义允许严格的交互,并且有一种灵活的交互方式。如果 ajar 协议具有任何 flexible 双向交互,这种情况属于编译时错误。

open 协议没有任何限制,既严格又灵活,允许单向交互和双向交互。

如需了解在选择封闭协议、ajar 协议或开放式协议时要考虑的权衡的进一步讨论,另请参阅:

语言更改

我们引入了 strictflexible 修饰符,用于将交互标记为严格或灵活:


protocol Example {
    strict Shutdown();
    flexible Update(value int32) -> () error UpdateError;
    flexible -> OnShutdown(...);
};

默认情况下,互动方式是灵活的。

在样式指南方面,建议您始终明确指明互动的严格程度,即应该为每次互动设置严格程度。3

我们引入了 closedajaropen 修饰符,用于将协议标记为已关闭、ajar(部分打开)或打开:


closed protocol OnlyStrictInteractions { ...
ajar protocol StrictAndOneWayFlexibleInteractions { ...
open protocol AnyInteractions { ...

封闭式协议不会定义灵活的交互。一个封闭的协议只能构成其他封闭的协议。

在 ajar 协议中,不能定义双向的灵活交互。ajar 协议只能编写封闭式或 ajar 协议。

(对开放协议没有任何限制。)

默认情况下,协议都是开放的。

该提案的上一版本将 ajar 指定为默认版本。但是,如果双向方法没有显式修饰符声明,这就会引发冲突,其中,开放性修饰符的默认值 ajar 与严格度修饰符的默认值(即柔性环境)相冲突。这意味着,如果没有针对协议或方法的修饰符,则无法编译包含双向方法的协议。如下所示:开放度的默认值以粗体显示,严格程度的默认值用斜体显示。

可视化:网格显示哪些开放式/ajar/闭合组合使用严格/灵活编译。

为了解决此问题,我们将开放性的默认值从 ajar 更改为 open,以允许协议在协议或方法上无需修饰符即可编译双向方法。

在样式指南方面,建议您始终明确指明协议的模式,即应该为每个协议进行设置。[^default-debate]

传输格式更改:事务性消息标头标记

我们将事务性消息标头修改为:

  • 交易 ID (uint32)
  • 静态标志(array<uint8>:2,即 2 个字节)
  • 动态标志 (uint8)
  • 魔数 (uint8)
  • 序数 (uint64)

也就是说,标记字节分为两部分,静态标记字节为 2 个字节,动态标记字节为 1 字节。

动态标记字节的结构如下:

  • 位 7,第一个 MSB“严格度位”:严格方法 0、灵活方法 1。
  • 位 6 到 0,未使用,设置为 0。

有关使用“动态标志”的更多详细信息:

  1. 我们在第三个版本的事务性消息标头中添加了标志。这些标志旨在“暂时用于软迁移”。例如,在“严格”到可扩展的联合迁移期间使用了一个位。不过,没有任何计划需要同时使用这么多标记,因此我们可以将这些标记的 intent 从仅临时使用更改为作为传输格式的一部分使用。

  2. 如果接收方不知道 strict 互动,那么发送者需要使用严格度位来向接收者指明该互动。在这种情况下,预期的语义是通信突然终止。如果没有这个严格程度位,发送方和接收方之间的这种偏差可能会被忽略。例如,考虑使用新添加的 strict StopSomethingImportant(); 单向交互的 ajar(或开放)协议。如果没有严格程度位,接收器将必须猜测未知交互是严格级还是灵活交互,根据本 RFC 中寻求的预期演变性改进来选择灵活交互。因此,FIDL 编写者在扩展协议时被迫依赖于双向严格交互。

另请参阅在事务标识符中放置严格性位,以讨论替代表示法;另请参阅互动模式位,了解未来可能需要的替代表示法。

传输格式更改:结果并集

现在有两个变体(用于成功响应的序数 1,用于错误响应的序数 2)的结果并集已扩展为第三个变体,即序数 3,它将携带一个新的枚举 fidl.TransportError,用于指明“传输级别”错误。

例如,互动:

open protocol AreYouHere {
    flexible Ping() -> (struct { pong Pong; }) error uint32;
};

具有响应载荷:

type result = union {
    1: response struct { pong Pong; };
    2: err uint32;
    3: transport_err fidl.TransportError;
};

具体而言,如果灵活方法使用 error 语法,系统会相应地设置成功类型和错误类型(序数分别为 1 和 2)。否则,如果灵活方法不使用 error 语法,则结果联合(序数 2)的错误变体将被标记为 reserved4

部分精确度:5

  • 我们之所以选择 transport_err 这个名称,是因为从应用的角度来看,该错误来自何处应该难以区分。依次为应用错误和“传输错误”,这是由于 FIDL 编码/解码、FIDL 协议错误、内核错误等导致的混合错误。从本质上讲,“传输错误”是框架(包含多层软件层)中可能出现的各种错误的集合。

  • 我们将 fidl.TransportErr 类型定义为具有单个变体 UNKNOWN_METHOD 的严格 int32 枚举。此变体的值与 ZX_ERR_NOT_SUPPORTED 相同,即 -2:

    type TransportErr = strict enum : int32 {
      UNKNOWN_METHOD = -2;
    };
    

    向客户端提供传输错误时,如果绑定提供了一种为未知互动 transport_err 获取 zx.status 的方法,则必须绑定使用 ZX_ERR_NOT_SUPPORTED。不过,如果未知交互transport_err映射到 zx.status 不适用于客户端显示错误的方式,则不需要绑定。

    另一种方法是仅使用 zx.status,并始终使用 ZX_ERR_NOT_SUPPORTED 作为值来指示未知方法,但这样做有两个重大缺点:

    • 它需要依赖于 zx 库,但很多库可能不会直接使用该库。这使得在 IR 中定义结果并集变得困难,因为我们需要自动插入对 zx 的依赖项或在 IR 中将该类型降级为 int32,但生成的绑定将其视为 zx.status

    • 它没有定义绑定应如何处理不属于 ZX_ERR_NOT_SUPPORTEDtransport_err 值。通过将类型指定为严格枚举,我们可以明确定义绑定的语义,这些绑定收到无法识别的 transport_err 值,然后会被视为解码错误。

  • 为简单起见,我们之所以称之为“结果并集”,事实上,我们介绍了一个共用一个通用结构的并集类型的类,即三个序数,第一个变体不受限制(成功类型可以是任何内容),第二个变体必须是 int32uint32 或其枚举,第三个变体必须是 fidl.transport_err

JSON IR 的更改

我们在 JSON IR 中公开了互动的严格程度。在实践中,我们会更新 #/definitions/interface-method 类型,并添加一个 strict 布尔值作为 ordinalnameis_composed 等的同级。

我们在 JSON IR 中公开了协议的模式。在实践中,我们会更新 #/definitions/interface 类型,并添加一个 mode 枚举(成员为 closedajaropen 作为 composed_protocolsmethods 等的同级)。

对绑定的更改

我们希望自动处理请求的绑定清单。例如,虽然绑定可以自动构造指示请求未知的请求,但重要的是引发已接收到未知请求(可能包含有关请求的一些元数据),并选择使用“未知请求”进行响应或突然终止通信。

静止时。

  • 对于灵活交互,绑定应使用其呈现其他传输级错误(例如来自 zx_channel_write 错误或解码期间错误)的相同机制,将结果联合的 transport_err 变体提供给客户端。应将结果并集的 errresponse 变体呈现给客户端,具体方式与绑定将该方法声明为“严格”时绑定这些类型的方式相同。

    • 例如,在 Rust 绑定中,Result<T, fidl::Error> 用于显示调用导致的其他传输级错误,因此 transport_err 应折叠为 fidl::Error。同样,在低级别 C++ 绑定中,fit::result<fidl::Error> 用于传递传输级错误,因此 transport_err 应合并到 fidl::Error 中。responseerr 变体的传达方式与严格方法相同。在 Rust 中,这意味着 Result<Result<T, ApplicationError>, fidl::Error> 表示带有错误语法的方法,或 Result<T, fidl::Error> 表示没有错误语法的方法,其中 response 值为 Terr 值为 ApplicationError

    • 对于折叠错误放入 zx.status 的绑定,必须将 transport_errUNKNOWN_METHOD 转换为 ZX_ERR_NOT_SUPPORTED

动态问题

  • 使用 zx_channel_writezx_channel_call 或其同级元素发送请求时,必须按如下方式设置动态标志:
    • 对于严格交互,严格度位(位 7)必须设置为 0;对于灵活交互,必须设置为 1。
    • 接下来的 6 位必须设置为 0。
  • 收到已知互动时:
    • 与目前的绑定方式相比没有变化。
    • 具体而言,绑定不应验证用于简化从严格交互到灵活交互(反之亦然)的严格程度。
  • 收到未知互动(即未知序数)时:
    • 如果互动是严格的(如收到的严格程度标记指示):
    • 绑定必须关闭通信(即关闭通道)。
    • 如果互动较为灵活(如收到的严格程度标记所示):
    • 对于关闭的协议,绑定必须关闭通道。
    • 如果互动是单向(交易 ID 为零):
      • 绑定必须向应用引发此未知交互(详情如下)。
    • 如果互动是双向(交易 ID 不为零):
      • 对于 ajar 协议,绑定必须关闭通道。
      • 对于开放协议,绑定必须向应用引发此未知交互(详情如下)。
    • 有关引发未知互动的详细信息:
      • 如果互动是双向的,绑定必须通过发送一个与所选的第三个变体且 fidl.TransportErrUNKNOWN_METHOD 的结果并集来响应该请求。此操作必须在用户代码引发未知交互之前发生。
      • 绑定应通过调用先前注册的处理程序(或类似处理程序)向应用引发未知交互。
      • 建议让绑定要求注册未知互动处理程序,以避免构建可能被误解的“默认行为”。绑定可以提供“空操作处理程序”或类似工具,但建议将其用于显式用途。
      • 绑定可以选择在处理未知互动时为应用提供关闭通道的选项。

当未知消息包含句柄时,服务器必须关闭传入消息中的句柄。服务器必须先关闭传入消息中的所有句柄,然后才能:

  • 关闭通道(对于严格方法、封闭协议上的灵活方法或 ajar 协议的灵活双向方法)
  • (如果是基于开放式协议的灵活双向方法)
  • 在 Open 或 ajar 协议上使用灵活的单向方法的情况下,通知用户代码未知方法调用。

同样,当客户端收到包含句柄的未知事件时,客户端必须关闭传入消息中的句柄。客户端必须先关闭传入消息中的所有句柄,然后才能:

  • 关闭通道(如果是严格事件,或关于封闭协议的灵活事件)。
  • 通知用户代码有关未知事件(如果是针对 open 或 ajar 协议的灵活事件)。

通常,处理未知互动时,操作顺序如下。

  1. 关闭收到的消息中的句柄。
  2. 如果适用,请关闭渠道或发送 UNKNOWN_METHOD 回复。
  3. 向未知互动处理程序引发未知互动或报告错误。

在异步环境中,如果多个线程可能同时尝试在通道上发送/接收消息,则在报告未知方法错误之前可能无法确保通道关闭或无法操作。因此,当该互动严重时,无需关闭渠道即可报告未知方法或事件的错误。不过,对于此 RFC 中指定的可恢复未知互动,需要先关闭句柄并回复(如果适用),然后才能分派未知互动处理程序。

此 RFC 的先前版本未指定传入消息中的关闭句柄、响应未知的双向方法以及向用户发起未知互动之间的排序。

兼容性影响

ABI 兼容性

将互动从 strict 更改为 flexible,或将 flexible 更改为 strict 不兼容 ABI。

更改协议模式(例如,从 closed 更改为 ajar)与 ABI 不兼容。虽然看似从限制性更强的模式更改为限制更宽松的模式可能与 ABI 兼容,但实际上并不是由于协议同时定义了发送方和接收器端(即“即发即弃”和“事件”)。

所有更改都可以进行软转换。您可以根据需要对修饰符进行版本控制

源代码兼容性

将互动从 strict 更改为 flexible,或将 flexible 更改为 strict 可能与源代码兼容。无论互动的严格程度如何,建议绑定通过折叠现有的传输错误 API 来提供相同的 API。

更改协议模式(例如,从 closed 更改为 ajar)与源代码不兼容。建议绑定专门用于其提供的 API,具体取决于协议模式。例如,封闭式协议不需要提供“未知方法”处理程序,并且建议不要提供不会使用的此类处理程序。

与平台版本控制的关系

RFC-0002 的改进部分中所述,“每当平台对 Fuchsia 系统接口的语义进行向后不兼容的更改时,我们都会更改 ABI 修订版本”。

我们达成可更新目标的程度有一个指标,即我们创建新 ABI 修订版本的速度。由于可以通过向后兼容的方式添加或移除灵活互动,因此此功能将有助于提高 Fuchsia 的可更新性。

实现

  • 我们可以想象这样的情况:绑定仅实现规范的严格部分,这样做很安全,因为通信会提前停止,就像对端遇到其他错误或 bug 一样。
  • 鉴于 FIDL 进化性作为首要目标的重要性,这并非可取的未来,因此我们要求绑定遵守此规范。
  • 为了符合绑定规范,绑定必须实现严格灵活的交互语义,以及协议的三种模式。
  • 为此,我们详细介绍了绑定规范的更改。这破坏了 ABI,并且也是传输格式(涵盖“静态”和“动态”问题)的重大演变。

此 RFC 的先前版本需要通过新魔数来控制未知互动的发布。不过,如上文所述,未知互动可向后兼容现有协议,因为用于指示严格程度的标头位以前未被使用/预留,且传输格式只会针对灵活的双向方法发生变化,而双向方法只能存在于开放协议中。我们将使用两阶段部署来启用未知互动支持,但将默认修饰符设置为 closedstrict,然后将这些修饰符显式添加到现有 FIDL 文件中,然后将默认值更改为 openflexible,而不是更改具体数字。

性能注意事项

closed 协议没有影响。如绑定的更改部分中所述,封闭式协议无需检查严格程度位。

ajaropen 协议的影响较小:

  • 处理未知交互与处理已知交互类似,系统会调用预注册的处理程序并运行应用代码。
  • 此外,如果是双向未知交互(仅限 open 协议),绑定将构造并发送响应。

我们希望性能方面的考虑因素很少,并且选择协议模式主要以安全注意事项为指导。

工效学设计

这使得 FIDL 更加难以理解,但同时满足了关于进化的一个非常重要的需求,而演化性一直是前所未有的尖锐优势。

向后兼容性

此功能不向后兼容,需要对所有 FIDL 客户端和服务器进行软迁移。

安全注意事项

添加向对等方发送未知请求的功能(即在灵活交互的情况下)可带来安全问题。

对于特别敏感的协议,由于需要非常严格的交互,可能需要提前处理演化问题,因此最好使用 closed 协议。紫红色的大多数内肠都依赖于 closed 协议(例如 fuchsia.ldsvc)。

在考虑 ajaropen 协议时,FIDL 编写者需要考虑两个问题:

  • 恶意对等方发送具有大型载荷的未知请求。(这与使用 flexible 类型时存在的问题类似,这种类型也可以携带大型未知载荷。)如大小会影响 ABI 中所述,需要为 FIDL 作者提供控制权,并且将在未来的工作中实现更多功能。
  • 打开协议嗅探大门,即对等方尝试在不事先了解的情况下发现实现了哪些方法,然后努力编写消息以利用发现的方法。如果实现公开的方法超出预期,就可能会出现问题。例如,想要公开父协议,但改为绑定构成父项的子协议。请注意,灵活交互不会改变攻击途径,但由于对等方能够连续尝试多个序数,而无需重新连接(在某些情况下,费用可能非常高昂),因此更容易被利用。
  • 在选择 ajar 协议与 open 协议之间进行权衡时,应考虑对等方无法判断单向交互是已处理还是被忽略;而对于双向未知交互(open 协议允许),处理对等方会披露自己无法理解交互,因此可能会向恶意对等方透露有价值的信息。

隐私注意事项

如果打开协议嗅探,可能会导致隐私问题。如安全注意事项部分所述,此威胁模型不会通过此 RFC 更改,但可能更容易被利用。

测试

若要开发此 RFC 中介绍的新功能集,关键在于确保所有绑定都遵循相同的规范,并且所有绑定的行为都类似。为此,需要能够在测试中表达该规范,例如“发送此请求,使用正确的交易 ID 进行响应,但序号错误,期望发送者通道关闭”。根据我们的经验,如果能够更清楚地表达规范,可以增加测试,进而提高对规范的所有绑定,以及增强回归保护。

我们将遵循相同的编码和解码方法,这些方法在 GIDL 的开发中得到了成功:首先,手动编写测试,执行尽可能多的绑定,然后稍微泛化一些可以着眼于基于声明式测试方法的部分。虽然我们希望构建一款与 GIDL 类似的工具来解决动态问题,我们也将努力实现这一目标,但我们并非仅将其作为最终结果,而是倾向于手动编写的流利表达测试。

文档

我们会提供关于此功能的大量文档。在规范方面:

我们将在 FIDL API 评分准则中添加用于介绍协议演变的其他条目。

关于此功能在指定目标语言的具体用法中,我们希望每个绑定都更新其文档,并提供有效的示例。

缺点、替代方案和问题

缺点:消息的大小上限会影响 ABI

处理未知载荷(无论是使用 flexible 类型时可能会遇到的未知载荷,还是此处引入的未知交互)时的一个问题是,对等方预计可读取的消息的大小上限会影响 ABI,无需明确说明此限制,也不会进行静态验证。

目前,尚无通道的矢量化读取,也无法执行部分读取。因此,消息可以发送给满足所有要求(例如,当对等方期望具有灵活交互时)的对等方,但会导致通信失败,从而破坏 ABI。如果相关消息太大而无法读取(因为对等方预计消息小于 1KiB),则永远不会读取超过该限制的新消息,而将关闭相应通道,并取消两个对等方之间的通信。

引入灵活交互会增加出现此类问题的可能性,该问题是由于 flexible 类型而已经存在的。

关于未来发展方向,您可以参考以下几点:

  • 矢量化通道读取,使得接收者可以只读取消息的标头,然后决定是读取载荷的其余部分还是舍弃该消息(这也需要新的系统调用)。
  • 将消息的大小上限设为协议的明确属性(可能会使用预定义的大小类别,例如 smallmediumlargeunbounded)。

替代方案:与命令模式进行比较

命令模式有助于客户端批量处理要由服务器处理的许多请求。您也可以使用命令模式来实现此 RFC 中描述的这种演进性。

例如:

open protocol AnOpenProtocol {
    flexible FirstMethod(FirstMethodRequest) -> (FirstMethodResponse);
    flexible SecondMethod(SecondMethodRequest) -> (SecondMethodResponse);
};

这可以通过紧随其后的封闭式协议进行近似处理,也就是说,若要实现相同程度的演变,就必须使用 FIDL 功能集:

closed protocol SimulateAnOpenProtocol {
    strict Call(Request) -> (Response);
};

type Request = flexible union {
    1: first FirstMethodRequest;
    2: second SecondMethodRequest;
    ...
};

type Response = flexible union {
    1: first FirstMethodResponse;
    2: second SecondMethodResponse;
    ...
    n: transport_err zx.status;
};

不出所料,命令模式方法并不令人满意。

由于我们必须将每个请求与联合中的响应相匹配,因此我们失去了“匹配对”的语法强制执行,这反过来也会导致语法局部性丢失。

由于不受约束的服务器可能会对 FirstMethodRequest 做出 SecondMethodResponse 响应,因此我们也会失去类型安全。有人可能会认为,智能绑定可能会注意到这种模式(也许是在 @command 属性的帮助下),并提供我们现在为方法采用的相同工效学设计。

在传输层中,命令模式会强制执行“两种方法判别器”排序。我们在事务消息标头中提供了序数(标识 Call 为交互),并且有联合序数(标识选择联合的哪个变体,即 1 表示 FirstMethodRequest,2 表示 SecondMethodRequest)。

再次强调,如果所有方法都遵循命令模式(即所有方法的请求和响应都是联合的),则不需要事务消息标头中的序数。从本质上讲,上述灵活协议会使用命令模式“编译为”封闭式协议。并集的传输格式需要统计变体的字节数和句柄,并且需要由合规解码器验证这些计数。这在以下两个方面都存在问题:

  • 事务消息标头允许的刚性(不说明载荷,如果可以解码,则允许)与联合传输格式(实际上从设计上来说)不匹配。这种刚性和简单性特别适用于低级别用例,FIDL 会转到该级别。

  • 组合模型无法体现“协议分组”。这项功能非常强大,因为我们能(并且确实)能通过同一信道复用多个协议。我们会尽可能使用结构化组合(例如 compose 节),还会采用动态组合(例如服务发现)。如果我们选择“所有编译为联合体”的视图,则会强制进行严格的分组。

最后,某些 FIDL 作者一直希望实现“自动批处理请求”。例如,fuchsia.ui.scenic 库因在 fuchsia.ui.scenic/Session.Enqueue 方法中使用了命令模式而闻名。但是,提供“自动批处理请求”是一项需要考虑的危险功能,因为如何在一个单元中处理多个命令的语义之间往往会有很大的差异。如何处理未知命令?我们应该如何处理失败的命令?是否应该忽略命令、停止执行、导致中止和回滚?即使是围绕“批处理工作单元”(事务)的概念设计的 RDBM 系统也往往会提供许多批处理模式([隔离级别](https://en.wikipedia.org/wiki/Isolation_(database_systems)))。可以说,FIDL 没有支持“自动批处理请求”的计划。

总而言之,虽然表面上看起来严格而灵活交互的语义与命令模式相同,但它们明显不同,需要特殊语义。

替代方案:协议协商

什么是协议协商

协议协商是一个广义术语,描述了对等方相互交互的一系列技术,用于逐步构建彼此的上下文,从而使其能够进行正确、更快、更高效的通信。

例如,想象一下随机拨打一个电话号码。也许同事会用“是这样,是吗?”开头。您将从没有对等方的背景信息转变为某种识别。我们可以继续说“哦,是的。我说的对吗?” 鉴于营销电话越来越普遍,您现在可能遇到一个“这通电话是关于什么的?”你是谁?”依此类推。两个对等体都很难发现彼此是谁以及它们都具备哪些能力。

  • 可以理解哪些数据元素?就像向对等方指示表中所需的字段一样,请务必小心,避免对等方生成大量复杂数据后才被忽略。
  • 对等方支持哪些方法?在渲染引擎中,您可以想象一下,假设 Alpha 混合是否作为一项功能可用;如果没有,您可以调整与渲染程序的互动(可能通过发送不同的内容)。
  • 应使用哪些性能特征?通常的做法是协商缓冲区的大小,或允许进行的调用频率(考虑配额)。

每种类型所需的解决方案往往略有不同,但所有解决方案本质上都是将交互模型的抽象描述(例如“对等方理解的方法集”)转换为可以交换的数据。

为了顺利解决协议协商问题,第一步是提供一种描述这些概念的方法(“协议”、“方法 foo 的响应类型”)。并且,由于对等设备从低上下文世界开始,即它们之间不了解彼此,并且必须假定它们对世界有不同的定义,因此对这些概念的描述往往依赖于结构属性。例如,说“响应类型为 MyCoolType”没有意义,需要依靠解释来解释;而“响应类型为 struct { bool; }”则是言辞的表象,可以不经上下文解释。

协议谈判如何建立严格而灵活的交互

此 RFC 中提议的内容(严格而灵活的交互方式)为不断演变的协议带来了一些转变。现在可以添加或移除方法了。甚至更多。但是,滥用情况的演变会让人们产生难以置信的协议,其领域从其形状难以理解。这类似于那些随时间推移具有大量字段的表,因为它们现在表示一种“聚合结构体”,结合了随时间变化的多个要求集。

在合同协议协商中,如果使用得当,可以隔离版本控制负担,并在进行一些动态选择(协商)之后,采用更简洁、更严格的协议(可能是 closed 协议)。

这两种进化技术都有自己的位置,并且它们都是进化工具箱中需要的。

替代方案:在事务标识符中放置严格位

使用事务标识符来传达严格且灵活的互动所需的位有一个重要的缺点。一些事务标识符由内核生成,即 zx_channel_call 将消息的前 4 个字节视为 zx_txid_t 类型的事务标识符。将更多信息打包到事务标识符中会强制内核与 FIDL 之间建立更强的耦合,而这是不可取的。通过改用事务标头标志,使用 zx_channel_call 的 FIDL 代码可以继续构建标头中的所有内容(标识符除外)。

替代方案:交互模式位

此 RFC 的早期版本要求添加“交互模式”位,以区分双向交互中的单向交互,并且预计会扩展到更复杂的交互,例如终端交互

主要缺点是,如果交互模式位与事务标识符中提供的信息重复:单向交互的事务标识符为零,而双向交互的事务标识符不为零。由于信息冗余,这使得不同实现(例如绑定)可以使用冗余位的不同子集来决定如何处理消息。这反过来又会打开恶意创建消息的大门,该消息被系统的不同部分以不同的方式解读。

虽然我们有抱负既为所有互动分配事务标识符,又能扩展互动模式,但这两项更改都需要额外位(如互动模式中所述),但我们更倾向于在设计这些功能时列出此设计讨论。

替代方案:命名时

随着此 RFC 不断迭代,讨论了很多关于如何正确命名引入的新概念的讨论。我们在这里总结了其中的部分讨论内容。

如需区分可能“未知”的互动和需要“已知”的互动,请执行以下操作:

  • 已选择 openclosed 个原始名称。
  • (none)required,因为对等方必须实现该方法,否则协议会终止。
  • 入围者:借鉴了 RFC-0033:处理未知字段和严格程度flexiblestrict

如需从可以接收单向未知交互的协议和可以同时接收单向交互和双向交互的协议中区分永不接收未知交互的协议,请执行以下操作:

  • 已选择“static”、“standard”和 dynamic 个原始名称。“静态”和“动态”的一个小缺点是,我们一直使用“静态”和“动态”这两个术语来指代 FIDL 的传输格式和消息传递方面。例如,此 RFC 中有一部分提到了“动态问题”,与“动态协议”相比,它具有不同的“动态”含义。
  • strict(none)flexible 再次借自 RFC-0033
  • 代替 static,使用 sealed 突出显示协议无法轻松扩展。
  • 使用 hybridmixed 代替 standard
  • 入围者closedajaropen。由于打开和关闭不用于互动,因此我们可以将其用于协议修饰符。ajar 的定义从字面上看是“部分打开”,这正是我们要描述的概念。是的,所有担心都觉得它有点诡异的气氛。

现有艺术和参考资料

(如文中提到的那样。)


  1. 令人困惑的是,消息(而不是事务性消息)指的是 FIDL 值的编码形式

  2. 对于 fidlc 和 JSON IR 爱好者,请注意,编译器的内部表示事件为 maybe_request_payload 等于 nullptrmaybe_response_payloadpresent。然而,从模型的角度来看,我们将此载荷称为请求,但采用服务器到客户端的方向。我们应与组合模型保持一致,更改 fidlc 和 JSON IR。这不在此 RFC 的讨论范围之内,但为了完整而备注。

  3. 我们更倾向于使用自由式语法,同时通过执行 lint 请求来强制执行样式指南。之所以选择这种设计,是因为我们希望这两款产品都能为新手提供更易于使用的语言,同时又为 Fuchsia 平台设定非常明确(进而冗长)的标准。 

  4. 值得注意的是,可以向 flexible 交互添加 error 可以作为与软 ABI 兼容的更改。 

  5. 我们稍后将 transport_errTransportErr 分别重命名为 framework_errFrameworkErr。如需了解详情,请参阅 https://fxbug.dev/42061151。