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

RFC-0087:更新 RFC-0050:FIDL 方法參數語法
狀態已接受
領域
  • FIDL
說明

您可以明確定義頂層類型,藉此修改指定要求和回應參數的語法。

問題
更小鳥
作者
審查人員
提交日期 (年月分)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,例如 LLC 中的 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 格式之外,針對要求和回應啟用頂層類型的資料表和聯集,不需要大幅變更。如果情況不同 (例如繫結中的編碼和解碼程式碼需要假設頂層類型為結構),則有兩種可能的方法:

  • 第一種方法是先將所有資料表和聯集納入結構中,再進行編碼和解碼。這可能會不吸引人,因為會需要產生其他類型,並加入額外的編碼和解碼步驟。
  • 另一種方法是修改編碼/解碼程式碼,支援非結構體的輸入。目前至少有部分程式碼假設輸入內容一律為結構體 (例如,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 殼層中使用的語法保持一致,但在這裡,與 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 中使用的語法較 gRPC 使用,其中方法要求和回應會使用單一 protobuf 訊息指定。

與此 RFC 類似的概念先前是由 ctiller@google.com 推薦,允許序數語法 (例如 MyMethod(1: foo Foo; 2: bar Bar)),暗示頂層類型是資料表,而不是結構體。主要的差別在於,除了資料表和結構,這個 RFC 還支援頂層聯集,由於聯集也會使用一般項目,因此原先建議的語意會模糊不清。