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 , ")"

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(假设 FooRequestFooResponse 是编译器预留的名称)。

绑定

绑定的主要影响在于,在某些情况下,与一组请求/响应参数对应的 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 格式之外,为请求和响应启用顶级表和联合类型不需要进行重大更改。如果不是这种情况,即绑定中的编码和解码代码基于假设顶级类型为结构体,则有两种可能的方法:

  • 第一种方法是先将所有表和联合体封装到结构体中,然后再进行编码和解码。这可能不太吸引人,因为它需要生成额外的类型,并在编码和解码中添加额外的步骤。
  • 另一种方法是修改编码/解码代码,以支持非结构体输入。目前,至少有一些代码假定输入始终是结构体(例如,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 壳中使用的语法保持一致,但在这里,它与 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 中使用的语法,其中方法请求和响应是使用单个 protobuf 消息指定的。

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