| RFC-0087:对 RFC-0050 的更新: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。另请注意,T 和 Wrapped 具有完全相同的线格式。
设计初衷
更改语法以明确指定顶级类型(例如 (struct {
name1 Type1; name2 Type2; }))主要有以下两项好处:
- 将 ABI 影响放在语法的最前面,遵循 FIDL 的设计原则。例如,编写
(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 , ")"
type 是根据 RFC-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 影响(假设 FooRequest 和 FooResponse 是编译器保留的名称)。
绑定
绑定中的主要影响是,在某些情况下,与一组请求/响应参数对应的 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::MyRequest 或 MyProtocol::MyResponse),则顶级结构体请求/响应或顶级联合或表请求/响应的 API 之间将没有这种区别。
JSON IR
maybe_request 和 maybe_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 格式之外,不需要进行重大更改。如果不是这种情况,即绑定中的编码和解码代码依赖于顶级类型是结构体的假设,则有两种可能的方法:
- 第一种方法是在编码和解码之前,先将所有表和并集封装到结构中。这可能不受欢迎,因为它需要生成额外的类型,并为编码和解码添加额外的步骤。
- 另一种方法是修改编码/解码代码,以支持非结构体的输入。目前,至少有一些代码假设输入始终是结构体(例如,LLCPP 中的正确特征仅针对结构体生成,而 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 系列/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 中使用的语法,在 gRPC 中,方法请求和响应使用单个 protobuf 消息指定。
ctiller@google.com 之前曾提出过与此 RFC 类似的想法,即允许使用序数语法(例如 MyMethod(1: foo Foo; 2: bar Bar))来表示顶级类型是表而不是结构。主要区别在于,此 RFC 除了支持表和结构之外,还支持顶级联合,这使得最初建议的语义变得模糊不清,因为联合也使用序号。