RFC-0138:处理未知互动 | |
---|---|
状态 | 已接受 |
区域 |
|
说明 | 我们扩展了 FIDL 语义,以允许对等方处理未知互动。 |
Gerrit 更改 | |
作者 | |
审核人 | |
提交日期(年-月-日) | 2021-05-25 |
审核日期(年-月-日) | 2021-10-27 |
摘要
我们扩展了 FIDL 语义,以允许对等方处理未知互动,例如接收未知事件或接收未知方法调用。为此,请执行以下操作:
我们向 FIDL 语言引入了灵活互动和严格互动。即使是未知的灵活互动,对等方也能妥善处理。严格的互动会导致突然终止。
我们为协议引入了三种操作模式。封闭协议绝不允许未知互动。相反,开放协议允许任何类型的未知互动。最后,ajar 协议仅支持单向未知互动。
从宏观上了解 FIDL 对演进的支持
在深入探讨此提案的具体内容之前,了解 FIDL 如何解决演化问题会很有帮助。
该问题有两个方面:源代码兼容性 (API) 和二进制兼容性 (ABI)。
API 兼容性旨在保证在发生更改之前针对生成的代码编写的用户代码在发生更改后仍可针对生成的代码进行编译。例如,我们可以合理地预期,向 FIDL 库添加新声明(例如定义新的 type MyNewTable = table {};
)不会导致使用此库的现有代码无法编译。
解决源代码兼容性问题的方法有三种:
- 尽可能使尽可能多的更改与源代码兼容(例如 RFC-0057:默认不使用句柄);
- 提供明确的保证(例如 RFC-0024:强制性源代码兼容性);
- 提供版本控制(例如 RFC-0083:FIDL 版本控制)。
此外,ABI 兼容性旨在实现针对不同版本库构建的程序的互操作性。例如,两个程序可以对表的架构有不同的理解,但仍能成功通信。
实现 ABI 兼容性可分为三个部分:
- 在休眠状态下,兼容性涉及在数据级实现互操作性,即当两个具有相同表架构但架构不同的对等方何时可以互操作?
- 动态兼容性假定所有数据类型都兼容,并侧重于在对等方使用不同版本的协议(例如不同的方法)时实现互操作性;
- 最后,在某些情况下,使用不同的协议是不可行的,解决方案是了解每个对等方的功能(协商),然后根据这些功能调整通信(使用哪种协议)。
当需要“本地灵活性”时,动态兼容性尤为适用,例如,在操作模型基本保持不变的情况下进行小幅添加。在其他情况下(例如 fuchsia.io1 与 fuchsia.io2 相关),则需要进行域名模型转换。需要“全球灵活性”,所寻求的解决方案属于协议协商类别。
我们在本 RFC 中专门讨论的机制(严格和灵活的互动)改善了动态兼容性 (2) 的现状。
术语
关于协议的组合模型的提醒。
两个对等方之间的通信是一种交互。互动始于请求,并且可以选择需要响应。
请求和响应都是事务消息,以标头(“事务标头”)的形式表示,可选地后跟载荷。1
互动是定向的,我们分别将两个对等方命名为客户端和服务器。客户端与服务器之间的交互始于客户端向服务器发出请求,并在相反方向发出响应(如果有)。同样,我们也提到了服务器与客户端的互动。
对于由客户端发起且无需响应的互动,我们通常使用火花式或单向一词;对于需要响应的互动(在当前模型中始终由客户端发起),我们通常使用调用或双向一词。当服务器是无响应互动的发起方时,通常称为“事件”。2
协议是一组互动。我们将会话定义为客户端和服务器之间使用协议进行通信的特定实例,即客户端和服务器之间的一系列交互。
应用错误是指遵循错误语法的错误。传输错误是指因内核错误(例如写入已关闭的通道)而发生的错误,或 FIDL 中发生的错误。
设计初衷
Fuchsia 的一项核心原则是可更新:软件包的设计是彼此独立更新的。即使是驱动程序也应是二进制稳定的,这样设备就可以在保留现有驱动程序的情况下,无缝更新到较新版本的 Fuchsia。FIDL 在实现这种可更新性方面发挥着核心作用,其最初的设计目的是定义应用二进制接口 (ABI),从而为向前和向后兼容性奠定坚实的基础。
具体而言,我们希望允许两个对等方对彼此之间的通信协议有略微不同的理解,以便安全地进行互操作。更好的是,我们希望能够获得强大的静态保证,确保两个对等方是“兼容”的。
我们做了很多工作来确保灵活地编码和解码 FIDL 类型,我们称之为休眠状态下的兼容性。我们引入了 table
布局、union
布局、选择了显式 union
序数、引入了 strict
和 flexible
布局修饰符、引入了 protocol
序数哈希、降低了 protocol
序数哈希的碰撞概率,并改进了事务消息标头格式,使其能够适应未来需求。
现在,我们来谈谈动态灵活性和保证,我们称之为动态兼容性。假设两个对等方在静态模式下是兼容的,即它们用于互动的所有类型在静态模式下是兼容的,那么动态兼容性是指这两个对等方能够成功互操作,并且不会因意外互动而中止通信。
利益相关方
- 主持人:jamesr@google.com。
- 审核者:
- abarth@google.com(联邦选举委员会)
- 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(...);
单向互动,其 intent 会忽略定位到旧版渲染程序的新客户的设置(但大多数渲染仍会正常运行)。现在,如果该新方法涉及特殊的 PII 呈现模式StartPIIRendering();
,则旧版渲染程序必须停止处理,而不是忽略此操作,因此使用strict
互动是恰当的。 - 另一个示例是,恶意对等方尝试通过发送各种消息来反射地发现公开的接口,以了解哪些消息可被理解。通常,反射功能会带来额外的性能开销,并会导致隐私问题(您可能会泄露的远远超出您的预期)。按照原则,FIDL 选择禁止反射,或要求明确选择启用。
因此,我们还引入了协议可采用的三种模式:
- 封闭协议是指不允许或不预期任何灵活互动,如果收到灵活互动,则属于异常情况。
- 开放协议允许任何灵活的互动(无论是单向还是双向)。此类协议提供了最大的灵活性。
- ajar 协议允许灵活的单向交互(触发即忘的调用和事件),但不允许灵活的双向交互(如果对等方不了解此方法,则无法进行方法调用)。
如需了解详情,请参阅协议的语义。
严格和灵活互动的语义
严格互动语义非常简单:当收到未知请求(即接收方不知道其序数)时,对等方会突然终止会话(通过关闭通道)。
灵活互动的目标是让收件人能够妥善处理未知互动。这对设计有几个影响。
灵活交互的发送方必须知道,其请求可能会被接收方忽略(因为接收方不理解)。
接收者必须能够看出此请求是灵活的(而非严格的),并据此采取行动。
由于双向互动需要接收方回复发件人,因此接收未知请求的接收方必须能够在没有任何其他详细信息的情况下构建回复。收件人必须告知发件人,自己不明白该请求。为了满足此要求,灵活的双向互动响应是结果联合(详情请参阅此处)。
从语义上看,在单向互动的情况下,发送方无法判断接收方是否知道其请求。使用灵活的单向交互时,FIDL 作者应注意其整体协议的语义。
值得注意的是,单向互动在某种程度上是一种“尽力”互动,因为发送方无法确定对等方是否收到了互动。不过,渠道可提供有序保证,以便互动顺序是确定的且已知的。通过严格的单向互动,您可以确保仅当系统理解了先前的互动时,才会发生某些互动。例如,日志记录协议可能具有 StartPii()
和 StopPii()
严格互动,以确保任何对等方都不会忽略这些互动。
如需进一步了解在选择严格和灵活互动时应考虑的权衡,请参阅以下内容:
打开、关闭和半开协议的语义
closed
协议的语义具有限制性,仅支持严格的互动,不支持灵活的互动。如果 closed
协议有任何 flexible
交互,则会出现编译时错误。
ajar
协议的语义允许严格的互动和单向灵活互动。如果 ajar
协议存在任何 flexible
双向互动,则会出现编译时错误。
open
协议没有任何限制,既可以是严格的,也可以是灵活的,允许单向和双向交互。
如需详细了解在选择封闭、半开放或开放协议时应考虑的权衡,请参阅以下内容:
语言变更
我们引入了修饰符 strict
和 flexible
,用于将互动标记为严格或灵活:
protocol Example {
strict Shutdown();
flexible Update(value int32) -> () error UpdateError;
flexible -> OnShutdown(...);
};
默认情况下,互动是灵活的。
根据样式指南,建议始终明确指明互动严格性,即应为每次互动设置严格性。3
我们引入了修饰符 closed
、ajar
和 open
,用于将协议标记为关闭、半开(部分打开)或打开:
closed protocol OnlyStrictInteractions { ...
ajar protocol StrictAndOneWayFlexibleInteractions { ...
open protocol AnyInteractions { ...
在封闭协议中,无法定义灵活的互动。闭源协议只能组合其他闭源协议。
在 ajar 协议中,无法定义双向灵活互动。一个 ajar 协议只能组合 closed 或 ajar 协议。
(对开放协议没有限制。)
默认情况下,协议处于开放状态。
此提案的早期版本将 ajar 指定为默认值。不过,如果声明的双向方法没有明确的修饰符,则会导致开放度修饰符的默认值“ajar”与严格性修饰符的默认值“flexible”发生冲突。这意味着,如果不对协议或方法添加修饰符,则无法编译包含双向方法的协议。请参阅下文:开放度的默认值以粗体显示,严格度的默认值以斜体显示。
为解决此问题,我们将开放性的默认值从 ajar 更改为 open,这样协议就可以在不对协议或方法使用修饰符的情况下编译双向方法。
根据样式指南,建议始终明确指明协议的模式,即应为每个协议设置模式。[^default-debate]
线格格式变更:事务性消息标头标志
我们将事务消息标头修改为:
- 交易 ID (
uint32
) - 休眠标志 (
array<uint8>:2
,即 2 个字节) - 动态标志 (
uint8
) - 魔法数字 (
uint8
) - 序数 (
uint64
)
即标志字节分为两部分,静态标志占两个字节,动态标志占一个字节。
动态标志字节的结构如下:
- 第 7 位,第一个 MSB“严格性位”:严格方法 0,灵活方法 1。
- 第 6 到第 0 位未用,设为 0。
有关使用“动态标志”的更多详细信息:
我们在第三版事务消息标头中添加了标志。这些标志旨在“暂时用于软迁移”。例如,在从严格联合迁移到可扩展联合期间,就使用了 1 位。不过,没有任何计划需要同时使用这么多标志,因此我们可以将这些标志的用途从仅限临时使用更改为作为线格格式的一部分使用。
在接收器不知道
strict
互动的情况下,发送器需要使用严格性位向接收器指明strict
互动。在这种情况下,预期的语义是通信会突然终止。如果没有此严格性位,发送器和接收器之间的这种偏差可能会被忽略。例如,假设某个 ajar(或开放)协议包含新添加的strict StopSomethingImportant();
单向互动。如果没有严格性位,接收器就必须猜测未知互动是严格还是灵活,鉴于本 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)的错误变体会被标记为 reserved
。4
一些精确度: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_SUPPORTED
的transport_err
值。通过指定类型为严格枚举,我们可以明确定义接收无法识别的transport_err
值的绑定的语义;然后,系统会将其视为解码错误。
为简单起见,我们将“结果联合”称为单数,但实际上,我们描述的是共享一个共同结构的一类联合类型,即三个序数,第一个变体不受约束(成功类型可以是任何类型),第二个变体必须是
int32
、uint32
或其枚举,第三个变体必须是fidl.transport_err
。
JSON IR 的变更
我们在 JSON IR 中公开了互动严格性。在实践中,我们会更新 #/definitions/interface-method
类型,并将 strict
布尔值添加为 ordinal
、name
、is_composed
等的兄弟。
我们在 JSON IR 中公开协议的模式。在实践中,我们会更新 #/definitions/interface
类型,并添加一个 mode
枚举,其中成员 closed
、ajar
和 open
是 composed_protocols
、methods
等的兄弟。
对绑定的更改
我们希望绑定能够直观地体现自动处理请求。例如,虽然绑定可能能够自动构建一个请求来指明请求未知,但请务必同时提示收到了未知请求(可能包含有关请求的一些元数据),并选择以“请求未知”进行响应或突然终止通信。
静态数据安全问题。
对于灵活的互动,绑定应通过与呈现其他传输级错误(例如
zx_channel_write
中的错误或解码期间的错误)相同的机制,向客户端呈现结果联合的transport_err
变体。应以与绑定以严格方式声明方法时呈现这些类型相同的方式,向客户端呈现结果联合的err
和response
变体。例如,在 Rust 绑定中,
Result<T, fidl::Error>
用于显示调用中的其他传输级错误,因此transport_err
应折叠到fidl::Error
中。同样,在低级 C++ 绑定中,fit::result<fidl::Error>
用于传达传输层错误,因此transport_err
应合并到fidl::Error
。response
和err
变体将以与严格方法相同的方式传达。在 Rust 中,这意味着对于存在语法错误的方法,Result<Result<T, ApplicationError>, fidl::Error>
;对于不存在语法错误的方法,Result<T, fidl::Error>
;response
值为T
,err
值为ApplicationError
。对于将错误折叠到
zx.status
中的绑定,transport_err
值UNKNOWN_METHOD
必须转换为ZX_ERR_NOT_SUPPORTED
。
动态问题。
- 使用
zx_channel_write
、zx_channel_call
或其同级兄弟发送请求时,必须按如下方式设置动态标志:- 对于严格的互动,必须将严格性位(第 7 位)设置为 0;对于灵活的互动,必须将其设置为 1。
- 接下来的 6 位必须设置为 0。
- 收到已知互动时:
- 与目前的绑定方式没有任何变化。
- 具体而言,绑定不应验证严格性,以便从严格互动迁移到灵活互动(或反之)。
- 收到未知互动(即未知序数)时:
- 如果互动是严格的(如收到的严格性标志所示):
- 绑定必须关闭通信(即关闭通道)。
- 如果互动是灵活的(如收到的严格性标志所示):
- 对于封闭协议,绑定必须关闭通道。
- 如果互动是单向的(交易 ID 为零):
- 绑定必须向应用引发此未知互动(详见下文)。
- 如果互动是双向的(交易 ID 不为零):
- 对于 ajar 协议,绑定必须关闭通道。
- 对于开放协议,绑定必须向应用引发此未知交互(详见下文)。
- 有关发起未知互动详情:
- 如果交互是双向的,则绑定必须通过发送包含所选第三个变体的结果联合以及
UNKNOWN_METHOD
的fidl.TransportErr
来响应请求。必须先执行此操作,然后才能将未知互动引发到用户代码。 - 绑定应向应用引发未知互动,可能通过调用之前注册的处理程序(或类似操作)来实现。
- 建议绑定要求注册未知互动处理脚本,以避免内置可能被误解的“默认行为”。绑定可以提供“无操作处理脚本”或类似内容,但建议明确使用此类脚本。
- 绑定可以选择在处理未知互动时向应用提供关闭通道的选项。
- 如果交互是双向的,则绑定必须通过发送包含所选第三个变体的结果联合以及
当未知消息包含句柄时,服务器必须关闭传入消息中的句柄。在以下情况下,服务器必须先关闭传入消息中的所有句柄:
- 关闭通道(如果是严格方法)、在封闭协议上使用灵活方法,或在 ajar 协议上使用灵活的双向方法
- 回复消息(如果是基于开放协议的灵活双向方法)
- 在 open 或 ajar 协议上使用灵活的单向方法时,通知用户代码未知方法调用。
同样,当客户端收到包含句柄的未知事件时,客户端必须关闭传入消息中的句柄。客户端必须先关闭传入消息中的所有句柄,然后才能执行以下操作:
- 关闭信道(如果是严格事件或在封闭协议上发生的灵活事件)。
- 在 open 或 ajar 协议上发生灵活事件时,向用户代码通知未知事件。
通常,在处理未知互动时,操作顺序如下。
- 关闭传入消息中的句柄。
- 关闭频道(如果适用)或发送
UNKNOWN_METHOD
回复。 - 将未知互动提交给未知互动处理脚本,或报告错误。
在可能有多个线程同时尝试在通道上发送/接收消息的异步环境中,可能无法或不切实际地保证在报告未知方法错误之前关闭通道。因此,当未知方法或事件的互动是严重的错误时,无需先关闭通道,即可报告错误。不过,对于本 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 的先前版本要求在新的魔法数字后面控制未知互动功能的发布。不过,如前所述,未知互动与现有协议向后兼容,因为用于指示严格性的标头位之前未使用/预留,并且线格格式仅针对灵活的双向方法而更改,而这些方法只能存在于开放协议中。我们将采用两阶段发布模式,而不是更改魔法数字,在第一阶段,我们启用未知互动支持,但将默认修饰符设置为 closed
和 strict
,然后将这些修饰符明确添加到现有 FIDL 文件中,最后将默认值更改为 open
和 flexible
。
性能注意事项
对 closed
协议没有影响。如对绑定的更改部分所述,封闭式协议无需检查严格性位。
对 ajar
和 open
协议的影响较小:
- 处理未知互动与处理已知互动类似,系统会调用预注册的处理脚本,并运行应用代码。
- 此外,在双向未知互动(仅限
open
协议)的情况下,绑定将构建并发送响应。
我们预计,性能方面很少有需要考虑的事项,在协议模式之间进行选择时,主要应遵循安全注意事项。
工效学设计
这使得 FIDL 更难理解,但解决了可扩展性方面的一个非常重要的需求,而可扩展性一直是其优势所在。
向后兼容性
此功能不向后兼容,并且需要对所有 FIDL 客户端和服务器进行软迁移。
安全注意事项
添加向对等方发送未知请求的功能(例如在灵活互动的情况下)会带来安全问题。
对于特别敏感的协议,可能需要因需要非常严格的互动而抢先解决演变问题,因此建议使用 closed
协议。Fuchsia 的大部分内部结构预计都依赖于 closed
协议(例如 fuchsia.ldsvc
)。
在考虑 ajar
或 open
协议时,FIDL 作者需要考虑以下两个问题:
- 恶意对等方发送包含大量载荷的未知请求。(这与使用
flexible
类型时存在的问题类似,后者也可能携带大量未知载荷。)如大小会影响 ABI中所述,需要进一步的功能才能为 FIDL 作者提供控制功能,我们将在日后的工作中解决此问题。 - 为协议嗅探打开大门,在这种攻击中,对等方会尝试在不具备先验知识的情况下发现实现了哪些方法,然后尝试构造消息来利用发现的方法。如果实现公开的方法超出预期,这可能会出现问题。例如,打算公开父级协议,但却绑定了构成父级的子级协议。请注意,灵活互动不会改变攻击矢量,但由于对等方能够依次尝试多个序数,而无需重新连接(在某些情况下,重新连接的开销可能非常高),因此攻击矢量可能更容易被利用。
- 在选择
ajar
协议还是open
协议时,请考虑到对等方无法确定是处理了单向互动还是忽略了它,而对于双向未知互动(如open
协议允许),处理对等方会披露其无法理解互动,这样做可能会向恶意对等方泄露有价值的信息。
隐私注意事项
允许协议嗅探可能会导致隐私问题。如安全注意事项部分所述,此 RFC 不会改变此威胁模型,但攻击者可能会更容易利用此模型。
测试
若要开发本 RFC 中所述的新功能,关键在于确保所有绑定都遵循相同的规范,并且所有绑定的行为都类似。为此,您需要能够在测试中表达规范,例如“发送此请求,使用正确的交易 ID 进行响应,但序数有误,预计发送方通道会关闭”。根据我们的经验,如果更加注重流畅地表达规范,则会增加测试次数,从而提高所有绑定到规范的代码的规范合规性,并增强回归保护。
我们将采用与编码和解码相同的方法,最终开发出 GIDL:首先手动编写测试,尽可能多地运用绑定,然后逐步推广可采用声明式测试方法的部分。虽然我们希望能够构建一个与 GIDL 类似的工具来处理动态问题,并且我们会为此努力,但我们不会将其视为最终结果,而是可能更倾向于手动编写流畅表达的测试。
文档
我们将为此功能提供详尽的文档。在规范方面:
FIDL API Rubric 中将添加更多条目,涵盖协议演变。
关于在给定目标语言中具体使用此功能,我们希望每个绑定都更新其文档,并提供有效示例。
缺点、替代方案和未知情况
缺点:消息大小上限会影响 ABI
处理未知内容(无论是 flexible
类型可能出现的未知载荷,还是此处介绍的未知互动)时会遇到一个问题,即预计由对等方读取的消息的大小上限会影响 ABI,但此限制从未明确说明,也未进行静态验证。
目前,无法对通道进行向量化读取,也无法执行部分读取。因此,消息可以发送到满足所有要求(例如,在对等方预期时进行灵活互动)的对等方,但会导致通信失败,从而破坏 ABI。如果相关消息太大,以至于对等方无法读取(因为对等方预期消息小于 1KiB),则系统将永远不会读取超出此限制的新消息,而是会关闭通道,并中止两个对等方之间的通信。
引入灵活的互动会增加出现此类问题的可能性,而此类问题已因 flexible
类型而存在。
以下是一些未来发展方向的想法:
- 矢量化通道读取,例如,接收方可以仅读取消息的标头,然后决定是读取载荷的其余部分还是舍弃该消息(这还需要新的系统调用)。
- 将消息大小上限作为协议的显式属性,可能包含预定义的大小类别,例如
small
、medium
、large
或unbounded
。
替代方案:与命令模式进行比较
命令模式非常有用,可让客户端将许多请求批量处理,以便由服务器处理。您还可以使用命令模式来实现本 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
是互动),并提供了联合序数(用于标识所选的联合变体,即 FirstMethodRequest
为 1,SecondMethodRequest
为 2)。
同样,有人可能会说,如果所有方法都遵循命令模式(即所有方法的请求和响应都是联合体),我们就不需要在事务消息标头中添加序数。从本质上讲,上述灵活协议将使用命令模式“编译为”封闭协议。联合体的线格格式要求统计变体的字节和句柄,并且要求这些计数由合规的解码器进行验证。这在两个方面都存在问题:
事务消息标头允许的严格性(不对载荷进行描述,请尽可能进行解码)是联合线格格式无法比拟的(实际上,这是有意为之)。这种严格性和简单性特别适用于 FIDL 过度转向的低级用途。
组合模型没有任何“协议分组”的概念。这非常强大,因为我们可以(并且确实)通过同一个通道多路复用多个协议。我们会尽可能使用结构化组合(即
compose
诗节),还会采用动态组合(例如服务发现)。如果我们认为“所有内容都会编译为联合体”,则会强制执行严格的分组。
最后,某些 FIDL 作者希望实现“自动批量处理请求”。例如,fuchsia.ui.scenic
库因在 fuchsia.ui.scenic/Session.Enqueue
方法中使用命令模式而闻名。不过,提供“自动批量处理请求”功能是一个危险的功能,因为如何在一个单元中处理多个命令的语义往往因应用而异。如何处理未知命令?如何处理失败的命令?是否应忽略命令、停止执行、导致中止和回滚?即使是围绕“批处理工作单元”(事务)概念设计的 RDBMS 系统,也往往会提供许多批处理模式([隔离级别](https://en.wikipedia.org/wiki/Isolation_(database_systems)))。简而言之,FIDL 无意支持“自动批处理请求”。
总而言之,虽然从表面上看,严格和灵活互动的语义与命令模式相同,但它们存在明显差异,因此需要使用特殊的语义。
替代方案:协议协商
什么是协议协商
协议协商是一个广义术语,用于描述一组用于实现对等方互动以逐步建立对方上下文的技术,从而使对等方能够更正确、更快速、更高效地进行通信。
例如,假设您随机拨打一个电话号码。对等方可能会以“某某某,是吗?”开头。您从对对等方一无所知,到获得了一些身份信息。我们可以接着说:“哦,某某某。我没听错吧?”鉴于营销电话的普遍性,您现在可能会遇到“您好!请问您是哪位?”Who are you?"。依此类推。双方会逐渐了解对方是谁,以及对方拥有哪些能力。
- 哪些数据元素受支持?例如,向对等方指明所需的表字段,并谨慎避免对等方生成大量复杂数据,以免在收到后被忽略。
- 对等方支持哪些方法?在渲染引擎中,您可以想象一下,询问 alpha 混合是否可用作为一项功能,如果不可用,则调整与渲染程序的互动(可能通过发送不同的内容)。
- 应使用哪些性能特性?通常,需要协商缓冲区大小或允许的调用频率(例如配额)。
每种类型通常都需要略有不同的解决方案,但所有这些解决方案都本质上将互动模型的抽象描述(例如“对等方理解的一组方法”)转换为可交换的数据。
为了妥善解决协议协商问题,第一步是提供一种描述这些概念(“协议”“方法 foo 的响应类型”)的方法。由于同级参与者从低情境世界开始(即彼此不了解),并且必须假定他们对世界的定义不同,因此概念的描述往往依赖于结构属性。例如,“响应类型为 MyCoolType
”没有意义,需要根据上下文进行解释,但“响应类型为 struct { bool; }
”本身就具有意义,可以不考虑上下文进行解释。
协议协商与严格和灵活的互动有何关系
此 RFC 中提出的严格而灵活的互动方式为不断演变的协议提供了一些回旋余地。现在,您可以添加或移除方法了。可能还有更多。但是,如果滥用演化功能,最终会得到一个变得模糊不清的协议,其领域很难从其形状中理解。这类表格类似于随着时间的推移而包含无数字段的表格,因为它们现在代表了一种“汇总结构体”,汇总了随着时间推移而发生变化的多组要求。
在合同协议协商中,如果使用得当,可以隔离版本控制负担,并在进行一些动态选择(协商)后,获得更清晰、更严格的协议(可能是 closed
协议)。
这两种演化方法各有用途,在演化工具箱中都不可或缺。
替代方案:将严格性位放置在事务标识符中
使用事务标识符传达严格且灵活的互动所需的位有一个重要缺点。某些事务标识符由内核生成,即 zx_channel_call
会将消息的前四个字节视为类型为 zx_txid_t
的事务标识符。将更多信息打包到事务标识符中会强制内核与 FIDL 之间建立更强的耦合,这是不理想的。通过改用事务标头标志,使用 zx_channel_call
的 FIDL 代码可以继续对标头中的所有内容(标识符除外)进行结构化。
替代方案:互动模式位
此 RFC 的早期版本要求添加“互动模式”位,以区分单向互动和双向互动,并且预计会扩展到更复杂的互动,例如终端互动)。
主要缺点是,互动模式位与事务标识符中提供的信息重复:单向互动具有零事务标识符,而双向互动具有非零事务标识符。由于信息冗余,这为使用冗余位子集的不同实现(例如绑定)打开了大门,以决定如何处理消息。这反过来又为恶意构造消息打开了方便之门,系统的不同部分会对此类消息做出不同的解读。
虽然我们希望为所有互动分配事务标识符并扩展互动模式,但这两项更改都需要额外的位(如互动模式中所述),因此我们更倾向于在设计这些功能时再讨论此设计。
替代方案:命名
在 RFC 迭代过程中,我们就如何为引入的新概念命名进行了大量讨论。下面总结了其中的一些讨论内容。
如需区分可以是“未知”的互动与需要是“已知”的互动,请执行以下操作:
- 选择了
open
和closed
作为原始名称。 (none)
和required
,这意味着您的对等方必须实现该方法,否则协议将终止。- 入围者:
flexible
和strict
借鉴了 RFC-0033:处理未知字段和严格性。
为了区分永远无法接收未知互动的协议、可以接收单向未知互动的协议以及可以接收单向和双向互动的协议,请参阅下文:
static
、standard
、dynamic
所选的原始名称。“静态”和“动态”的一点缺点是,我们一直在使用“休眠状态”和“动态”来指代 FIDL 的线格格式和消息传递方面。例如,本 RFC 的部分内容提及了“动态问题”,其中“动态”一词的含义与“动态协议”不同。strict
、(none)
、flexible
再次借鉴了 RFC-0033。- 使用
sealed
代替static
,以突出显示该协议无法轻松扩展。 - 使用
hybrid
或mixed
代替standard
。 - 入围者:
closed
、ajar
和open
。由于“open”和“closed”不用于互动,因此我们可以将它们用于协议修饰符。jar 的定义在字面上是“部分打开”,这正是我们要描述的概念。是的,所有相关人员都觉得这有点诡异。
在先技术和参考文档
(如文中所述。)
-
令人困惑的是,消息(而非事务消息)是指 FIDL 值的编码形式。 ↩
-
对于
fidlc
和 JSON IR 爱好者,请注意,编译器的内部会将事件表示为maybe_request_payload
等于nullptr
,maybe_response_payload
为present
。不过,从模型的角度来看,我们将此载荷称为请求,但方向为服务器到客户端。我们应遵循组合模型,更改fidlc
和 JSON IR。这超出了本 RFC 的范围,但为了完整起见,我们还是要提及。 ↩ -
我们更倾向于采用宽松的语法,并通过 lint 强制执行样式指南。之所以做出这一设计选择,是因为我们希望为新手提供更易于上手的语言,同时为 Fuchsia 平台提供非常明确(因此也非常冗长)的标准。 ↩
-
值得注意的是,向
flexible
互动添加error
可以作为软 ABI 兼容更改进行。 ↩ -
我们后来将
transport_err
和TransportErr
分别重命名为framework_err
和FrameworkErr
。如需了解详情,请参阅 https://fxbug.dev/42061151。 ↩