RFC-0087:RFC-0050 更新:FIDL 方法参数语法

RFC-0087:RFC-0050 更新:FIDL 方法参数语法
状态已接受
领域
  • FIDL
说明

通过明确定义顶级类型,修改用于指定请求和响应参数的语法。

问题
Gerrit 更改
作者
审核人
提交日期(年-月-日)2021-03-09
审核日期(年-月-日)2021-04-14

摘要

RFC-0050 之后,FIDL 方法请求或响应的参数以 (name1 Type1, name2 Type2) 的形式以内嵌方式指定,该结构隐式定义一个以这些参数作为其成员的结构的请求/响应类型。此 RFC 建议更改语法,以明确指定顶级类型,例如 (struct { name1 Type1; name2 Type2; })。除了结构体之外,用户还可以指定联合或表的请求/响应类型。此 RFC 对传输格式没有任何影响。

另见:

术语

在此 RFC 中,“将类型封装在结构体中”是指接受现有类型并定义由该类型的单个成员组成的新结构体的过程。例如,将 T 类型封装在结构体中是指定义一个新的 type Wrapped = struct { my_t T; } 类型。此方法可用于解决 FIDL 语言的某些限制。例如,uint8 不可为 null,但结构体可以,因此,您可以先将 uint8 封装到一个结构体中,实际上可获得可为 null 的 uint8。另外值得注意的是,TWrapped 的传输格式完全相同。

设计初衷

更改语法以明确指定顶级类型(例如,(struct { name1 Type1; name2 Type2; }))具有以下两大好处:

  • 遵循 FIDL 的设计原则,将 ABI 影响放在语法首位。例如,编写 (struct { name1 Type1; }) 而不是 (name1 Type1) 会明确表明顶级请求或响应类型为结构体,因此添加或移除新参数与 ABI 或 API 不兼容。
  • 因此,用户可以指定不同的顶级类型,而无需将类型封装在结构体中,从而进行额外级别的间接操作。除了通过内嵌定义来提高可读性之外,这还允许 FIDL 编译器选择适当的名称,而不必让开发者负担过重。一种可能需要这样做的场景是,请求参数的可扩展性是优先考虑的方法 - 在本例中,用户可以使用表,而不是结构体。

此 RFC 的推出时间通过以下两种方式与 RFC-0050 相关联:

  • 由于 RFC 中引入了匿名布局,因此您可以重复使用此语法来指定请求/响应类型,而无需为其指定单独的名称。
  • 本 RFC 中提议的语法更改可归入 RFC-0050 所需的现有实现和迁移,从而无需进行单独的迁移。

设计

语法

之前:

protocol Oven {
  StartBake(temp Temperature);
  // message with no payload
  -> OnReady();
};

之后:

protocol Oven {
  StartBake(struct { temp Temperature; });
  // message with no payload
  -> OnReady();
};

全部可能的方法变体如下:

MyMethod(struct { ... }) -> (struct { ... });   // Two-way
MyMethod(struct { ... }) -> ();                 // Two-way, but response is empty
MyMethod() -> (struct { ... });                 // Two-way, but request is empty
MyMethod() -> ();                               // Two-way, but both request and response are empty
MyMethod() -> (struct { ... }) error zx.status; // Two-way; response leverages error syntax
MyMethod() -> () error zx.status;               // Error: must specify a type for success case.
MyMethod(struct { ... });                       // One-way
MyMethod();                                     // One-way, but request is empty
-> MyMethod(struct { ... })                     // Event
-> MyMethod();                                  // Event, but response is empty

更正式地说,

protocol-method = ( attribute-list ) , IDENTIFIER , parameter-list,
                  ( "->" , parameter-list , ( "error" type-constructor ) ) ;
protocol-event = ( attribute-list ) , "->" , IDENTIFIER , parameter-list ;
parameter-list = "(" , ( parameter ( "," , parameter )+ ) , ")" ;
parameter = ( attribute-list ) , type-constructor , IDENTIFIER ;

会变为:

protocol-method = ( attribute-list ) , IDENTIFIER , method-params
                  ( "->" , method-params ( "error" type-constructor ) ) ;
protocol-event = ( attribute-list ) , "->" , IDENTIFIER , method-params;
method-params = "(" , type-constructor , ")"

typeRFC-0050 中的定义进行定义,也就是说,它是对现有类型(如 MyType<args>:constraints)的引用或匿名布局(例如 struct { name Type; }:constraints)。

虽然语法允许将任意类型用作请求和响应,但 FIDL 编译器会验证顶级类型是结构体、联合或表。

根据 RFC-0050 中的规定,编译器会为任何内嵌的顶级请求或响应类型预留一个名称,以便在需要时能够弃用内嵌样式(例如,在参数数量增加时提高可读性)。例如,可以从以下设置更改为:

protocol MyProtocol {
    Foo(struct {
      // input param
      input uint32;
    }) -> (struct {
      // output param
      output uint32;
    });
};

更改为:

type FooRequest = struct {
  // input param
  input uint32;
};

type FooResponse = struct {
  // output param
  output uint32;
};

protocol MyProtocol {
    Foo(FooRequest) -> (FooResponse);
}

且不会对 API 或 ABI 产生影响(假设 FooRequestFooResponse 是编译器预留的名称)。

绑定关系

绑定的主要影响是,在某些情况下,与一组请求/响应参数对应的 API 可能会扁平化,或者不取决于请求或响应的顶级类型。目前,已生成扁平化 API 和非扁平化 API 的实例。

这里的“扁平化”API 是指绑定中直接使用请求和响应参数的任何 API,这会抽象化它们封装在结构体中的事实。例如,与 HLCPP 中的 FIDL 方法 GetName(struct { id uint32; }) -> (struct { name string; }) 对应的客户端调用的函数签名为:void GetName(uint32_t id, GetNameCallback callback)。FIDL 中指定的参数直接对应于 C++ 中的函数参数。

“非扁平化”API 是指向用户公开顶级类型本身的情况。在前面的示例中,代码应如下所示:void GetName(GetNameRequest req, GetNameCallback callback)GetNameRequest 对应于顶级结构体类型,并且会有一个 uint32 id 字段。

在当前语法中,所有顶级请求或响应类型都是隐式结构体,因此可以扁平化参数,使其直接对应于函数签名的参数,因为添加或移除结构体成员都会导致 ABI 和 API 不兼容(即,生成的绑定中的此内联 API 不会为 FIDL 提供的保证增加额外的限制)。但这不适用于支持添加和移除成员的表和联合等。因此,如果用于表示方法的语言结构的兼容性保证(在此示例中为 C++ 中的位置函数参数)比顶级类型(例如表或联合)提供的限制更为严格,则可能会出现扁平化的情况。我们再来看上面的示例,这意味着 GetName(table { 1: id uint32; }) -> (table { 1: name string;}) 将需要生成 void GetName(GetNameRequest req, GetNameCallback callback) 形式的非扁平化签名,以保持表的顶级类型提供的兼容性保证。

对于生成的函数或方法,一些编程语言(如 Dart)可以通过在发送端使用具名实参来解决此问题,但由于必须相应地向接收方法添加新参数,因此这在接收端仍然与源代码不兼容。

总而言之,如果顶级类型是表或联合体,则使用针对结构体的扁平化 API 的绑定代码可能需要提供不同的非扁平化 API。如果绑定当前已生成非扁平化 API(例如 LLCPP 中的 MyProtocol::MyRequestMyProtocol::MyResponse),那么顶级结构体请求/响应或顶级联合或表请求/响应的 API 之间没有太大区别。

JSON IR

maybe_requestmaybe_response 的 JSON 条目将更改。旧版架构:

"maybe_request": {
    "description": "Optional list of interface method request parameters",
    "type": "array",
    "items": {
        "$ref": "#/definitions/interface-method-parameter"
    }
},

会变为:

"maybe_request_payload": {
    "description": "Optional type of the request",
    "$ref": "#/definitions/compound-identifier"
},

maybe_response 的更改相同)

已存在与此形状匹配的 "maybe_request_payload" 字段,但尚未在 JSON IR 中指定(这是“更改消息的表示方式”工作的一部分)。在实践中,此 RFC 的 JSON IR 更改将完成从 "maybe_request""maybe_request_payload" 的迁移(请参阅实现)。

实现

此 RFC 的实现包含两个部分:第一部分是纯美观的更改,即修改所有现有文件以符合此处提出的新语法;第二部分是更改 FIDL 编译器和绑定,以允许表和联合作为顶级类型。语法更改将在更广泛的 RFC-0050 FIDL 语法转换过程中进行,但对联合和表顶级类型的支持可以推迟,以避免成为 FIDL 语法改进项目的阻碍。所有使用“新”语法编写的 FIDL 文件都将符合此 RFC 中规定的变更,并且正式的 FIDL 语法将与 RFC-0050 的其余部分同时更新以反映其设计。

在现有绑定中,在某些情况下,除了处理新的 JSON IR 格式之外,为请求和响应启用顶级表和联合类型不需要进行重大更改。如果无法做到这一点(即绑定中的编码和解码代码依赖于顶级类型是结构体的假设),则有两种可能的方法:

  • 第一种方法是先将所有表和联合封装到一个结构体中,然后再进行编码和解码。这可能没有吸引力,因为它需要生成额外的类型,并增加额外的编码和解码步骤。
  • 另一种方法是修改编码/解码代码,以支持非结构体输入。目前,至少有一些代码假定输入始终是结构体(例如,LLPP 中的正确特征仅针对结构体生成,Rust 中的请求和响应编码是通过元组而非结构体进行的),但此假设目前未知的位置数量,需要确定这一点,以便了解权衡并最终在两者之间做出决定。后一种方法可能具有方法调用以外的优势,例如,它不需要将类型封装在永久性数据用例中的结构体中。

JSON IR

https://fxbug.dev/42157011 中,一项迁移已在进行中,以将 "maybe_request""maybe_response" 字段移出 JSON IR,以使对请求和响应类型的任何特殊处理都仅在 FIDL 后端中进行。这项工作在完成之前已暂停,但为了实现此 RFC,将恢复工作。目前,C++ 后端是唯一一个使用 "maybe_request""maybe_response" 的 fidlgen 后端(不过,其他使用 JSON IR 的库,例如 FIDL 编解码器,也需要进行更新)。

安全和隐私保护

此 RFC 不会修改 FIDL 传输格式,因此对安全性和隐私性没有影响。

测试

此 RFC 将使用现有基础架构进行测试:单元测试、黄金测试和集成测试(例如 FIDL 兼容性测试)。

文档

启用此功能后,应添加文档(包括示例)来描述新功能。

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

语法

此 RFC 中建议的语法使得将结构体用作顶级类型的常用路径更加冗长,因为需要明确指定。替代方案可能包括针对常见情况引入语法糖(例如,保留结构体的当前语法,以及为表和联合使用新的显式语法),但我们认为在所有情况下都以显式方式编写的可读性比降低详细程度更重要。

另一个可能被认为没有吸引力的语法部分是用方括号括起来的冗余部分:(struct { ... }),这也是在 FTP-058 中讨论过的问题。这里存在一种倾向于保持一致性:使用大括号可确保请求内某个类型的语法与 FIDL 文件中其他位置的类型的语法相同。FTP-058 中采用的用空格替换多余的大括号以避免冗余的方法(例如 MyMethod struct { ... } -> union { ... };)在这里同样有效。在 FIDL 文本中,这种功能性更强的样式与方案的其余部分一致,并且与 Fuchsia shell 中使用的语法一致,而在这里,它与 FIDL 更多的基于 C-family/Go 的语法并不一致。

最后,建议的另一种替代方案是更改用于指定类型的语法,使其与方法参数语法保持一致:结构体将使用类似以下语法的元组/记录来指定:type MyStruct = (foo Foo, bar Bar);。这样一来,当顶级类型是结构体时,我们可以省略一对额外的圆括号 MyMethod(foo Foo, bar Bar);,以便保持相同的参数语法。作为完整示例,此建议应如下所示:

// Declare a struct with two fields foo, bar.
type SomeStruct = (foo Foo, bar Bar);

protocol MyProtocol {
  // Declare a method with two request parameters.
  // The two parameters are stored in a struct.
  MyStructMethod(foo Foo, bar Bar);

  // Declare a method with two optional parameters.
  // The two parameters are stored in a table.
  MyTableMethod table { 1: foo Foo, 2: bar Bar };
};

绑定关系

设计中所述,在很多情况下,绑定在为表和联合生成的 API 中,无法像处理结构体一样扁平化或内嵌顶级类型的成员,以免引入额外的兼容性限制。关于某个方法的顶级类型成员何时会被内嵌的规则可能并不容易记住,这意味着他们需要依靠文档或生成的代码检查来确定每个 FIDL 协议方法的结果 API。这增加了当前情况的复杂性,因为当前绑定 API 始终内嵌/扁平化顶级类型成员。

从理论上讲,可以通过不扁平化请求或响应参数来提供一致的 API,但这在实践中被认为不可行,因为这需要迁移依赖于此 API 的所有用户代码实例(大多数与 FIDL 方法交互的用户代码实例)。

现有艺术和参考资料

此 RFC 中建议的语法更接近 gRPC 中使用的语法,其中方法请求和响应使用单个 protobuf 消息指定。

ctiller@google.com 之前提出了与此 RFC 类似的想法,这允许使用序数语法(例如 MyMethod(1: foo Foo; 2: bar Bar))暗示顶层类型是表而不是结构体。主要区别在于,此 RFC 不仅支持表和结构体,还支持顶级联合,但由于并集也使用序数,因此最初建议的语义不明确。