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 不可為空值,但結構體可以,因此您可以先將 uint8 包裝成結構體,有效設為可為空值。此外,TWrapped 的線路格式完全相同,這點也值得注意。

提振精神

將語法變更為明確指定頂層型別 (例如 (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 (假設 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 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 使用的語法較為接近,其中方法要求和回應是使用單一 protobuf 訊息指定。

ctiller@google.com 先前曾提出類似的 RFC,允許序數語法 (例如 MyMethod(1: foo Foo; 2: bar Bar)) 隱含頂層型別為資料表,而非結構體。主要差異在於,除了資料表和結構體,這項 RFC 也支援頂層聯集,這會導致原本建議的語意模稜兩可,因為聯集也會使用序數。