RFC-0023:协议的组合模型

RFC-0023:协议的组合模型
状态已接受
区域
  • FIDL
说明

关键字接口已被关键字协议取代。扩展协议被明确定义为一种组合模型,其中一个协议可以定义为一组消息,并由一个或多个其他协议进行增强。协议扩展所用的语法从类似于继承的语法更改为类似于 mixin 的语法。

作者
提交日期(年-月-日)2018-12-10
审核日期(年-月-日)2019-01-09

摘要

我们建议进行以下更改:

  • 关键字 interface 替换为关键字 protocol。 (在本文档的其余部分,我们将使用“协议”一词。)
  • 扩展协议被明确定义为一种组合模型,其中一个协议可以定义为一组消息,并由一个或多个其他协议进行增强。
  • 用于协议扩展的语法从类似于继承的语法更改为类似于 mixin 的语法。
  • 在目标语言中表示组合协议时,绑定作者必须避免包含(例如“is-a”层次结构、继承、子类型)。

动机

术语接口附带的上下文包袱包括方法重载、构造函数和析构函数、作为消息接收者的对象模型等。

不过,FIDL 的目标更为温和,旨在描述两个对等方之间的协议,即可以交换的一组消息。

我们首先明确指出 FIDL API,例如“虽然语法类似于面向对象的接口的定义,但设计考虑因素更类似于网络协议,而不是对象系统”。当面临引入更多“面向对象”功能的选项时,我们已经回避了这一点(例如,最近在 RFC-0020:序数哈希中关于重载的评论)。

我们希望在语言中更清楚地体现这种区别,并建议通过将关键字 interface 替换为关键字 protocol 来更改语法。

此外,借用继承语法所暗示的“is-a”关系并不合理,会导致错误的预期。 (为清晰起见,FIDL 不提供此类继承语义,但语法表明了这一点。) 如需了解详情,请参阅“Is A”关系被认为有害部分。

设计

此提案引入了正式语义来描述进程交互和协议。

此提案更改了 FIDL 源代码语言,以阐明协议扩展的语义,并为绑定作者提供了新的指导。

目前,继承关系不会在 JSON IR 中表示,因此绑定作者无法利用这些关系。 因此,我们预计,除了改进文档之外,这项新指南对生成的绑定代码的修改不会有太大变化。

此提案不会更改传输格式。

此提案不会更改 JSON IR,但我们确实希望在未来进行更大规模的更改时,将键重命名作为其中的一部分。

协议模型

Zircon 通道不需要为其承载的载荷指定特定架构。 FIDL 基于此基元构建,并限制通道传输特定协议。 这样,FIDL 就可以为通道的两端赋予含义和名称。 我们称其中一个为客户端,另一个为服务器。

我们的模型将协议描述为一组定向交互,并带有可选的墓志铭。我们将会话称为客户端与服务器之间使用协议进行通信的特定实例。

方向可以是从客户端到服务器,也可以是从服务器到客户端。

互动以请求开始,并且可能需要响应。 我们通常使用术语“即发即弃”或“单向”来表示无响应的互动,并使用术语“调用”来表示需要响应的请求。

请求和响应都是消息,表示为标头,后跟结构的载荷,即请求或响应的参数。

目前,我们限制服务器到客户端的消息不能有响应。 简单来说,“事件只是即发即弃”。

epitaph 是结束会话的服务器到客户端互动。如需了解详情,请参阅 RFC-0053:墓志铭。

此模型中不包含更复杂的交互,例如 TCP 的 SYN/SYN-ACK/ACK 三向握手。我们认为这超出了范围,并且不太可能通过未来对模型的优化来涵盖。

组合模型

现在,协议既可以定义交互,也可以扩展一个或多个协议。 生成的协议(“组合协议”)会承载直接定义的所有互动,并继承其前身(直接或间接)定义的所有互动。

例如,Child 协议定义如下:

protocol Parent1 { Method1(); };
protocol Parent2 { Method2(); };
protocol Child { compose Parent1; compose Parent 2; Method3(); };

将包含所有三次互动:Method1、Method2 和 Method3。

不过,Method1 和 Method2 是通过组合还是直接在 Child 中定义的,并不会传递到特定于语言的后端,即这不会在 JSON IR 中表示。

“Is A”关系被认为有害

由于协议可以双向传输请求,因此建立子类型关系需要更加谨慎。 因此,我们不允许协议与其扩展的协议具有“is a”关系。

例如,假设我们有以下两个协议:

protocol Parent { Method(); };
protocol Child { ->Event(...); };

如果我们允许将承载协议 Child 的通道视为 Parent(即“Child 是 Parent”关系),则客户端可能会收到 Event,而它们无法处理这些消息。 如需查看具体示例,请参阅下一部分。

相反,我们将寻求支持特定的协议注解(例如“仅客户端到服务器交互”),以支持并允许“is a”关系。 发生这种情况时,此类关系会传递到 JSON IR,以供后端使用。

目前对“Is A”关系的依赖

举一个具体的例子,fuchsia.media 库将各种协议组合在一起。具体而言:

AudioCapturer 和 AudioRenderer 均未定义事件,即它们纯粹是“客户端到服务器协议”,是单向的。 (StreamSource 定义了两个事件,但这里我们专门讨论每个协议自己的定义。)

因此,如果客户端知道如何与 StreamBufferSet 或 StreamSource(分别为 StreamBufferSet 或 StreamSink)交互,那么它也可以与 AudioCapturer(分别为 AudioRenderer)交互,即客户端只会忽略公开的额外方法。 在这里,我们可以按照预期定义“is a”关系。

但是,如果向任一接口添加事件,则此“is a”关系将不复存在。 假设客户端正在与 StreamBufferSet 交互,而后者实际上是服务器端的 AudioRenderer。如果 AudioRenderer 触发事件,会发生什么情况? 该客户会如何处理?

由于我们(尚)无法在 fidlc 中提供这种区分,因此我们确认不支持“is a”关系。此提案实质上是对现状的澄清。

与 fuchsia.media 示例一样,知道某些关系为 true 的作者可以根据需要调整绑定(使用类型转换等)。

在后续提案中,我们希望引入属性或新关键字来捕获此方向性限制,并在此基础上在绑定中提供“is a”关系。在提出此类提案之前,我们无法在 FIDL 工具链中提供更好的支持。

语法变更

在设计阶段,我们提出了几种不同的替代方案,请参阅下文的缺点、替代方案和未知因素

使用接受的语法的扩展协议如下所示:

protocol Parent1 {
  Method1OfParent1();
  Method2OfParent1();
};

protocol Parent2 {
  Method1OfParent2();
  Method2OfParent2();
};

protocol Child {
  compose Parent1;
  compose Parent2;
  Method1OfChild();
  Method2OfChild();
};

具体而言,语法更改如下:

declaration = const-declaration | enum-declaration | protocol-declaration |
              struct-declaration | union-declaration | table-declaration ;

protocol-declaration = ( attribute-list ) , "protocol" , IDENTIFIER ,
                       "{" , ( method-or-compose-declaration , ";" )*  , "}";

method-or-compose = method-declaration | compose-declaration ;

method-declaration = ( ordinal , ":" ) , method-parameters ;

method-parameters = IDENTIFIER , parameter-list , ( "->" , parameter-list )
                     | "->" , IDENTIFIER , parameter-list ;

compose-declaration = "compose", compound-identifier ;

组合协议只能提及一次。

可能延期

我们预计后续提案还会允许服务器到客户端的交互需要响应,从而在通道上启用多路复用协议,可能采用相反的顺序。例如,coordinator.fidl 定义了两个命令响应协议,一个从 devmgr -> devhost,另一个从 devhost -> devmgr。目前,这些是手动多路复用的,依赖于序数分派来区分。

我们可能会在 compose 块中使用“->”语法,以便稍后引入反向多路复用。 另一种方法是,仅在扩展包含反向协议时才需要明确的方向,这样做的好处是目前无需引入任何方向语法,因为我们会推迟具有反向协议的扩展。

我们允许将 compose 块放置在协议定义中的任何位置,并且还允许使用多个 compose 块。 我们也可以只设置一个代码块,并要求将其放在顶部。 在这里,我们选择保持开放,而是依靠自动格式设置和/或样式指南来提供建议,而不是将强制执行融入语言本身。

JSON IR

我们不会在此次变更中更改 JSON IR。

相反,作为一系列更大更改的一部分,我们会将“interface_declarations”键重命名为“protocol_declarations”。 这一系列较大的更改需要采用多步骤方法,将架构版本从 0.0.1 升级到 0.0.2,并为后端提供一个过渡期以进行调整。

远程破坏和 [FragileBase] 的使用

此提案不会改变远距离破坏的可能性,因此我们重申,对于任何正在扩展的协议,都应使用 [FragileBase]1。

绑定作者指南

  • 在目标语言中表示组合协议时,绑定必须避免包容(例如“is-a”层次结构、继承、子类型)。
  • 接收未知序数应被视为错误。 绑定应将其作为“未知序数错误”进行冒泡,并关闭通道。

实施策略

三个步骤:

  1. 添加对新语法的支持;
  2. 转换所有 FIDL 文件以使用新语法;
  3. 不再支持旧语法。

工效学设计

此变更使 FIDL 更易于理解,请参阅动机部分。 这项变更可能不会让 FIDL 一开始就更容易理解,但可以避免日后出现误解和预期不一致的情况。

文档和示例

我们需要更新语言、语法、评分标准和其他此类文档。

向后兼容性

此变更会破坏与当前使用继承的 FIDL 文件的源代码兼容性。 如实现中所述,我们将采用分阶段的方法来引入新语法、迁移所有 FIDL 文件,然后移除对旧语法的支持。

此更改不会更改 FIDL 传输格式,因此它是向后兼容的 ABI 更改。

性能

不影响性能。

安全

我们或许能够利用更严格的类型语义来保护通道或观察通道。 这不是本提案的目标,不会导致现状倒退,而且可以说是一种改进。

测试

您可以在 fidlc 级别完全通过单元测试来测试此更改。

缺点、替代方案和未知因素

以下部分记录了设计阶段提出的替代语法。

Alternative Syntax (pascallouis@)

示例:

protocol Parent1 { Method1(); };
protocol Parent2 { Method2(); };
protocol Child {
  compose {
    -> Parent1();
    -> Parent2();
  };
  Method1OfChild();
}

注意:这是最初提议的语法。 使用 compose 代码块似乎不自然,并且与现有语言相差太大。 这使得组合多个协议成为首选方法,而组合单个协议则显得冗长。 此外,也不清楚是否允许多个 compose 代码块,以及这些代码块会是什么样子。 最后,我们选择不在协议中添加方向性“->”指示器,而是倾向于在未来与多方向多路复用(如果考虑使用此类功能)一起引入。

替代语法 (jeremymanson@)

原因:为了阐明我们希望实现的方法列表与定义通信协议的方法列表之间的区别:

示例:

protocol Parent1 {
  Method1OfParent1();
  Method2OfParent1();
};

protocol Parent2 {
  Method1OfParent2();
  Method2OfParent2();
};

interface Child {
  compose {
    -> Parent1();
    -> Parent2();
  };
  Method1OfChild();
};

注意:“interface”关键字表示每个方法都必须有实现,“protocol”关键字表示对符合协议和包含协议的接口的要求。我们不一定期望 StreamSource 具有自己的实现。 这让我们进一步远离实现继承,因为我们明确表示不会发生任何实现继承。 您无法将一个接口组合到另一个接口中。

替代语法:类似 Go 的接口组合 (proppy@)

原因:看起来不像继承,熟悉 Golang 语法的人会知道这是接口嵌入

示例:

protocol Parent1 { Method1(); };
protocol Parent2 { Method2(); };
protocol Child {
    Parent1;
    Parent2;
    Method3();
};

备注:Go 语言规范(关于接口和嵌入)。

替代语法:使用声明 (jeffbrown@)

原因:看起来不像继承,而是重用现有关键字来指示引入作用域的名称。 不太可能与方法声明或“属性2”混淆。

示例:

protocol Parent1 { Method1(); };
protocol Parent2 { Method2(); };
protocol Child {
    using Parent1;
    using Parent2;
    Method3();
};

备注:FIDL、C++、Rust 和其他语言中的先例。

备选关键字

“compose”关键字的替代方案:

  • extends (pascallouis@)
  • contains (smklein@)

在先技术和参考资料

没有具体内容。

Cap'n Proto 具有支持继承(包括多重继承,采用 mixin 样式)的接口。


  1. 为方法引入序数哈希,再加上未来提案中计划将方法序数从 32 位更改为 64 位,这可能会使这种远程破坏(在实际中)不复存在,届时我们将重新考虑使用 FragileBase。 ↩

  2. 属性:一种假设的 FIDL 扩展,用于促进观察/数据绑定。简单来说,绑定会生成用于访问、修改和/或观察接口公开的值的方法。 ↩