| 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 不可為空值,但結構體可以,因此您可以先將 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 使用的語法較為接近,其中方法要求和回應是使用單一 protobuf 訊息指定。
ctiller@google.com 先前曾提出類似的 RFC,允許序數語法 (例如 MyMethod(1: foo Foo; 2: bar Bar)) 隱含頂層型別為資料表,而非結構體。主要差異在於,除了資料表和結構體,這項 RFC 也支援頂層聯集,這會導致原本建議的語意模稜兩可,因為聯集也會使用序數。