RFC-0087:RFC-0050 更新:FIDL 方法參數語法

RFC-0087:更新 RFC-0050:FDI 方法參數語法
狀態已接受
區域
  • 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 無法為可為空值,但結構體可以,因此只要先將 uint8 包裝成結構體,即可有效地取得可為空值的 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 是指綁定中直接使用要求和回應參數的任何 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 語法改善專案。所有以「new」語法編寫的 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 中使用的語法較為相似,在 gRPC 中,方法要求和回應會使用單一 protobuf 訊息指定。

與這項 RFC 相似的想法先前曾由 ctiller@google.com 提出,允許序數語法 (例如 MyMethod(1: foo Foo; 2: bar Bar)) 暗示最上層類型是資料表,而非結構體。主要差異在於,除了資料表和結構體之外,這個 RFC 也支援頂層聯集,這會使原本建議的語意模糊不清,因為聯集也會使用序數。