FIDL 繫結規格

本文件是 Fuchsia 介面定義語言 (FIDL) 繫結的規格。本指南旨在為繫結作者提供指引和最佳做法,並建議符合人體工學的使用方式。

在本文件中,下列關鍵字的解讀方式應符合 RFC2119 的說明:MAY、MUST、MUST NOT、OPTIONAL、RECOMMENDED、REQUIRED、SHALL、SHALL NOT、SHOULD、SHOULD NOT。

生成程式碼指示

機器生成的程式碼頂端必須加上註解,指出該程式碼是由機器生成。如果語言有標準規定如何指出生成來源 (而非人類編寫的程式碼),則必須遵循該標準。

確定範圍

建議您為機器產生的程式碼設定命名空間,避免與使用者定義的符號衝突。您可以使用語言提供的範圍建構函式來實作,例如 C++ 中的命名空間、Rust 中的模組,或 Dart 中的套件。如果產生的範圍可以有名稱,則應使用 FIDL 程式庫名稱的元件命名,其中包含產生的程式碼定義,讓每個 FIDL 程式庫都能存在於專屬範圍中。如果無法限定範圍且命名空間為共用,則可能需要對產生的名稱進行一些處理 (請參閱「命名」)。

命名

一般來說,產生的程式碼中使用的名稱應與 FIDL 定義中使用的名稱相符。以下各節列出可能的例外狀況。

如果是內嵌版面配置,繫結應使用 fidlc 產生的名稱,因為這些名稱保證不重複。

如果譯文語言支援,繫結可能會產生與 FIDL 名稱命名情境對應的限定範圍名稱。舉例來說,對於某些 FIDL:

type Outer = struct {
  middle struct {
    inner struct {};
  };
};

產生的程式碼可讓您使用父項命名結構定義範圍內的名稱 (例如 C++ 中的 Outer::Middle::Inner),參照最內層 FIDL 結構對應的值。

外殼

應變更大小寫,以符合語言的慣用樣式 (例如使用蛇形命名法或 CamelCase)。fidlc 會確保強制執行 ID 唯一性,並考量可能的大小寫差異 (請參閱 RFC-0040)。

保留關鍵字和名稱衝突

產生的程式碼必須考量譯文語言中的保留關鍵字,以免在 FIDL 定義中使用譯文語言的關鍵字時發生非預期的情況。舉例來說,您可以在衝突的名稱前加上底線 _ (假設沒有關鍵字以底線開頭)。

生成的程式碼不得造成命名衝突。舉例來說,如果函式的參數是根據 FIDL 定義產生,則產生的本機變數名稱絕對不能與可能產生的名稱衝突。

Ordinals

方法序數

方法使用的序數是 64 位元的大數字。繫結應以十六進位形式發出這些序數,即 0x60e700e002995ef8,而非 6982550709377523448。

Union 和資料表序數

union 和 table 的序數從 1 開始,且必須形成密集空間。因此,這些數字通常很小,繫結應以十進位標記法發出這些序數。

原生類型

在譯文語言中,將內建 FIDL 類型轉換為原生類型時,建議繫結盡可能使用最明確且符合人體工學的原生類型。舉例來說,Dart 繫結會使用 Int32List 代表 vector<int32>:N 和 array<int32>:N,而非較通用的 List<int>。

生成的類型和值

持續支援

產生的程式碼必須產生變數,其中包含對應 FIDL 中每個 const 定義的相符值。在支援不可變變數的語言中,這些變數應標示為不可變 (例如 C++ 和 Rust 中的 const,或 Dart 中的 final)。

Bits 支援

繫結必須為每個位元成員提供產生的值。也「可能」產生代表未設定旗標的位元值,以及設定每個旗標的位元值 (「位元遮罩」)。這些值應限定在每組位元的範圍內。

建議支援下列運算子,用於處理產生的值:

  • 位元 and,即 &
  • 位元 OR,即 |
  • 位元互斥或,即 ^
  • bitwise not,即 ~
  • 位元差異,即一個運算元中存在的所有位元,但不包括另一個運算元中存在的位元。通常以 - 運算子表示。

FIDL 位元運算的不變量是,除非來源運算元中出現對應的不明位元,否則不應引入不明位元。除了 bitwise not 之外,所有建議的運算子自然都具備這項屬性。bitwise not 運算的實作項目必須進一步使用所有值的遮罩,遮蓋產生的值。虛擬程式碼如下:

~value1   means   mask & ~bits_of(value1)

為方便起見,JSON IR 會提供這個遮罩值。

在支援運算子超載的語言 (例如 C++) 中,bitwise not 作業必須透過超載內建運算子的方式實作,一律取消設定位元欄位中的未知成員。在不支援運算子超載的語言中,值應提供 InvertBits() 方法 (大小寫應符合該語言的慣例),用於執行遮罩反轉。

清除位元時,應優先使用位元差運算子,而非位元 not 運算子,因為前者會保留未知的位元:

// Unknown bits in value1 are preserved.
value1 = value1 - value2

// Unknown bits in value1 are cleared, even if the user may only intend to
// clear just the bits in value2.
value1 = value1 & ~value2

繫結不應支援其他可能導致無效位元值的運算子 (或可能導致其含義不明確),例如:

  • 位元移位,即 << 或 >>
  • 位元無正負號移位,即 >>>

如果產生的程式碼包含包裝基礎數值位元值的類型,則應可在原始值和包裝函式類型之間轉換。建議明確轉換。

繫結「可以」提供函式,將 bits 基礎類型的原始值轉換為 bits 類型本身。這些轉換器可能有多種形式:

  • 如果輸入值包含任何未知位元,可能會失敗 (或傳回空值)。
  • 截斷輸入值中的任何不明位元。
  • 僅適用於彈性位元:保留輸入值中的任何未知位元。

不明資料

彈性位元:

  • 繫結必須提供方法,檢查值是否包含任何未知位元,且可提供方法擷取這些未知位元。
  • 不論先前的值為何,位元 not 運算子都會取消設定所有未知成員 (但對已知成員會正常運作)。其他位元運算子對未知位元成員保留與已知成員相同的語意。

嚴格位元也可能提供上述 API,以簡化嚴格和彈性之間的轉換。

在某些語言中,很難或無法防止使用者從原始類型手動建立 bits 類型的例項,因此繫結設計人員無法將嚴格位元值限制為具有適當限制的網域。在這種情況下,繫結作者應為嚴格位元提供未知資料相關 API。

在具有編譯檢查淘汰警告的語言 (例如 Rust) 中,應為嚴格位元提供與未知資料相關的 API,但標示為已淘汰。

列舉支援

繫結必須為每個列舉成員提供產生的值。這些值應限定在每個列舉的範圍內。

如果產生的程式碼包含包裝基礎數值列舉值的類型,則應可在原始值和包裝函式類型之間轉換。建議明確轉換。

不明資料

彈性列舉:

  • 繫結必須提供方法,讓使用者判斷列舉是否不明,包括可與列舉比對 (適用於支援 switch、match 或類似建構的語言)。
  • 繫結可能會公開列舉的基礎原始值 (可能不明)。
  • 繫結必須提供取得有效不明列舉的方法,使用者不必提供明確的不明原始原始值。如果其中一個列舉成員加上 @unknown 屬性註解,則這個不明列舉建構函式必須使用加上註解的成員值。否則,系統不會指定不明建構函式使用的值。
  • 在任何判斷值是否未知的函式中,@unknown 成員都必須視為未知。

Struct 支援

繫結必須為支援下列作業的每個結構體提供類型:

  • 為每個成員指定明確值的建構。
  • 讀取和寫入成員。

聯集支援

繫結必須為支援下列作業的每個聯集提供類型:

  • 使用明確變異基因段集建構。不建議繫結提供不含變體的建構函式。只有在考量效能或受限於譯文語言時,才應考慮使用這個方法。
  • 讀取/寫入聯集的變體,以及與該變體相關聯的資料。

對於沒有聯集類型或聯集值常值的語言,建議支援工廠方法,以便在提供其中一個可能變體的某個值時,建構新的聯集。舉例來說,在 C 語言中,這可讓您取代下列程式碼:

my_union_t foo;
foo.set_variant(bar);
do_stuff(foo);

改成類似下列內容:

do_stuff(my_union_with_variant(bar));

這些工廠方法應命名為 [Type]-with-[Variant],並根據目標語言正確設定大小寫。

HLCPP 繫結就是一個例子。

不明資料

彈性聯集:

  • 如果序數不明,解碼必須成功,但重新編碼必須失敗。
  • 繫結必須提供方法,判斷聯集是否含有不明變體。
  • 繫結「可以」提供存取未知變體序數的方法。
  • 繫結可以提供建構函式,建立含有未知變體的聯集。
    • 建構函式必須命名,以避免在實際工作環境程式碼中使用,例如 unknown_variant_for_testing()。
    • 建構函式不得允許使用者選擇序數。
    • 有了建構函式,終端開發人員就無法透過迂迴方式 (例如手動解碼原始位元組),建構含有未知變體的聯集。

表格支援

繫結必須為支援下列作業的每個資料表提供類型:

  • 建構函式,可選擇是否為每個成員指定值。
  • 讀取及寫入每個成員,包括檢查是否已設定特定成員。這些應遵循命名配置:get_[member]、set_[member] 和 has_[member],並根據目標語言正確使用大小寫。

繫結可以為資料表提供建構函式,但只能為有值的欄位指定值。舉例來說,在 Rust 中,您可以使用 ::EMPTY 常數和結構體更新語法來完成這項操作。這種建構方式可讓使用者編寫程式碼,即使資料表新增欄位,程式碼也能正常運作。

不明資料

所有資料表都可彈性調整。

如果存在未知欄位,解碼和重新編碼作業「必須」成功。重新編碼時「必須」省略不明欄位。

繫結「可以」提供一種方法,判斷資料表在解碼期間是否包含任何不明欄位。「可能」提供存取序數的方法。

繫結不得提供建立含有未知欄位的資料表,或在現有資料表設定未知欄位的方法。

嚴格和彈性類型

Strict 類型在遇到任何未知資料時,必須無法解碼。解碼含有未知資料的值時,彈性類型必須成功。

以下是彈性 FIDL 類型及其對未知值的行為示例:

FIDL 類型 存取未知 重新編碼精確度
彈性位元 原始整數 無損
彈性列舉 原始整數 無損
彈性聯集 布林值或序數 fails
桌子 布林值或序數 有損

一般來說,基礎未知資料可以在解碼期間捨棄,或儲存在解碼類型中。無論是哪種情況,類型都應指出解碼時是否遇到不明資料。如需這些 API 設計的具體指引,請參閱列舉支援、位元支援、聯集支援和資料表支援等章節。

繫結作者應優先最佳化 strict 類型,可能需要犧牲 flexible 類型。舉例來說,如果兩者之間有設計上的取捨,繫結作者應優先最佳化 strict 類型。

將類型從嚴格變更為彈性時,必須可轉換。

值類型和資源類型

值類型「不得」包含控制代碼,資源類型「可以」包含控制代碼。

在值類型和彈性類型之間的互動中,彈性類型需求優先。具體來說,解碼含有不明控制代碼的彈性值類型時,必須成功。

通訊協定支援

活動

繫結必須支援處理或忽略通訊協定中的事件。繫結「可以」允許使用者指定某些事件的處理邏輯,並省略通訊協定中其他事件的處理邏輯。

如果使用者未指定事件的處理邏輯,繫結在收到事件時「必須」繼續正常通訊。換句話說,如果使用者未在用戶端指定對應的處理邏輯,傳送事件並不會造成錯誤。

繫結應盡量減少指定事件處理常式與事件到達端點之間的競爭行為。

繫結在收到不明的嚴格事件時,必須關閉連線。

網域錯誤類型

繫結「可以」為通訊協定方法提供某種形式的特殊支援,其錯誤類型與目標語言中處理錯誤的慣用方式相符。

舉例來說,如果語言提供某種形式的「結果」類型 (即包含「成功」變體和「錯誤」變體的聯集類型),例如 Rust 的 result::Result 或 C++ 中的 fpromise::result,則在接收或傳送具有錯誤類型的方法回應時,可以提供與這些類型之間的自動轉換。

如果語言支援例外狀況,產生的通訊協定方法程式碼可選擇擲回與錯誤類型對應的例外狀況。

如果無法做到這一點,產生的程式碼可能會提供便利函式,直接傳回成功回應或錯誤值,或接收錯誤類型回應,避免使用者程式碼重複初始化結果聯集。

錯誤處理

通訊協定可能會向使用者顯示傳輸錯誤。傳輸錯誤可分為兩類:在原生類型和傳輸格式資料之間轉換時發生的錯誤,或基礎傳輸機制造成的錯誤 (例如呼叫 zx_channel_write_etc 時發生的錯誤)。這些錯誤可能包含錯誤狀態,以及任何其他診斷資訊。

這類傳輸錯誤稱為終端錯誤。本文其餘部分可能會另外指定其他情況為終端錯誤,例如交易 ID 不正確。

  • 編碼期間發生的驗證錯誤 (如有執行驗證)。
  • 解碼錯誤。
  • 基礎傳輸機制發生錯誤。

相較之下,網域錯誤 (在使用 error 語法宣告的方法中) 和架構錯誤 (在 flexible 雙向方法中) 則不會終止。

終端錯誤處理

繫結必須提供擁有基礎端點的非同步用戶端和伺服器 API。連線發生終端錯誤時,用戶端和伺服器 API 必須關閉基礎端點,終止連線。

由於 FIDL 的 IPC 傳輸模型不含暫時性錯誤,因此重試傳送相同回覆等操作並無價值。發生錯誤時觸發拆除作業,可鼓勵這種繫結使用方式,並簡化錯誤處理。

繫結可提供同步用戶端和伺服器 API。在同步 API 中,關閉終端錯誤的端點通常需要鎖定。如果基於效能考量而不希望發生這種情況,這些 API 可能會在發生終端錯誤時保持連線開啟,且應據此清楚記錄。建議使用非同步類型的 API。

繫結「可以」提供不具備基礎端點的用戶端和伺服器 API,以因應低階用途。這些 API 無法在發生終端錯誤時關閉端點,因此應清楚記錄。擁有者變體版本應為建議的 API 變體版本。

對等互連關閉特殊處理

如果基礎傳輸機制回報對等端點在訊息傳送期間關閉 (例如寫入管道時收到 ZX_ERR_PEER_CLOSED 錯誤),用戶端/伺服器必須先讀取並處理所有剩餘訊息,再向使用者顯示傳輸錯誤並關閉連線。

當基礎傳輸機制通知對等端點在訊息等待期間關閉 (例如在等待管道上的信號時觀察到 ZX_CHANNEL_PEER_CLOSED 信號),用戶端/伺服器必須先讀取並處理任何剩餘的訊息,然後再向使用者顯示傳輸錯誤並關閉連線。

這是為了與管道的讀取語意保持一致:假設有一對端點 A <-> B,多則訊息寫入 B,然後 B 關閉。使用者可以繼續從 A 讀取,不會看到對等端關閉錯誤,直到所有留存的訊息都清空為止。換句話說,除非無法再從端點讀取訊息,否則「對等互連關閉」並非重大錯誤。

處理類型和權限檢查

繫結必須在傳入和傳出方向強制執行控制代碼類型和權限檢查。也就是說,您必須使用 zx_channel_write_etc、zx_channel_read_etc 和 zx_channel_call_etc,而非非 etc 對等項目。

在傳出方向,必須根據 FIDL 定義填入權限和類型資訊。具體來說,這項中繼資料應放在 zx_handle_disposition_t 中,以便叫用 zx_channel_write_etc 或 zx_channel_call_etc。這些系統呼叫會代表呼叫端執行類型和權限檢查。

在傳入方向,zx_channel_read_etc 和 zx_channel_call_etc 會以 zx_handle_info_t 物件的形式提供類型和權限資訊。繫結本身必須執行適當的檢查,如下所示:

假設讀取控制代碼 h,且其在 FIDL 檔案中的權限為 R:

  • 如果控制代碼 h 缺少權限 R 中存在的權限,則會發生錯誤。如果遇到這種情況,請務必關閉管道。
  • 如果控制代碼 h 的權限多於權限 R,則必須透過 zx_handle_replace 將其權限縮減為 R。

此外,如果 h 的類型錯誤,也會發生錯誤。如果遇到這種情況,則「必須」關閉管道。

如需詳細範例,請參閱「控制代碼的生命週期」。

Iovec 支援

繫結可視需要使用向量化的 zx_channel_write_etc 和 zx_channel_call_etc syscall。使用這些項目時,第一個 iovec 項目必須存在,且大小足以容納 FIDL 交易訊息標頭 (16 個位元組)。

屬性

繫結必須支援下列屬性:

  • @transitional

最佳做法

替代輸出

繫結可選擇是否提供 FIDL 傳輸格式以外的輸出方法。

其中一種輸出內容可能是容易使用的偵錯列印,用於生成的類型。例如,列印位元值:

type Mode = strict bits {
    READ = 1;
    WRITE = 2;
};

可以輸出字串 "Mode.Read | Mode.Write",而非原始值 "0b11"。

您可以為每個產生的 FIDL 類型,實作類似的易用列印功能。

訊息記憶體配置

繫結「可以」提供選項,讓使用者在傳送或接收訊息時提供自己的記憶體,以便控制記憶體配置。

線路格式記憶體配置

繫結「可以」讓產生的 FIDL 類型記憶體內版面配置與該類型的線路格式相符。理論上,這樣做可以避免額外的副本,因為資料可以直接用作交易訊息,反之亦然。在實際操作中,傳送 FIDL 訊息可能仍涉及複製步驟,其中訊息的元件會組合成連續的記憶體區塊 (稱為「線性化」)。這種做法的缺點是繫結會變得更僵化:變更 FIDL 線路格式會變得更複雜。

C++ Wire 繫結是唯一採用這種做法的繫結。

相等比較

對於 struct、table 和 union 等匯總類型,繫結「可以」提供等式運算子,對相同類型的兩個執行個體執行深度比較。請勿為資源類型提供這些運算子 (請參閱 RFC-0057 和深度相等),因為無法比較控制代碼。避免公開資源類型的等號運算子,可防止在類型中加入控制代碼時,因等號運算「消失」而導致來源中斷。

複製中

對於結構體、資料表和聯集等匯總類型,繫結「可以」提供複製這些類型例項的功能。不應為資源類型提供複製功能 (請參閱 RFC-0057),因為不保證能成功複製控制代碼。避免公開資源類型的複製運算子,可防止因複製作業「消失」或在類型中加入控制代碼時簽章變更,而導致來源中斷。

測試公用程式

繫結可以選擇是否產生專門用於測試的額外程式碼。舉例來說,繫結可以產生每個通訊協定的虛設常式實作,讓使用者只需要覆寫測試中要執行的特定方法。

墓誌銘

繫結應支援 epitaph,也就是讓伺服器傳送 epitaph,以及讓用戶端接收和處理 epitaph 的生成程式碼。

Setter 和 Getter

繫結可以為匯總類型 (結構、聯集和資料表) 的欄位提供 setter 和 getter。即使在 getter/setter 方法不符合慣例的語言中,使用這些方法也能重新命名內部欄位名稱,而不會破壞該欄位的使用方式。

要求「應答者」

使用譯文語言的 FIDL 繫結實作 FIDL 通訊協定時,繫結會提供 API 來讀取要求參數,以及寫入回應參數 (如有) 的方法。舉例來說,要求參數可以做為函式的引數,回應參數則可做為函式的傳回類型。

FIDL 通訊協定:

protocol Hasher {
    Hash(struct {
        value string;
    }) -> (struct {
        result array<uint8, 10>;
    });
};

繫結可能會產生:

// Users would implement this interface to provide an implementation of the
// Hasher FIDL protocol
interface Hasher {
  // Respond to the request by returning the desired response
  hash: (value: string): Uint8Array;
};

繫結可能會提供用於寫入回應的回應器物件。在上述範例中,這表示在函式引數中傳遞額外的回應器物件,並讓函式傳回 void:

interface Hasher {
  hash: (value: string, responder: HashResponder): void;
};

interface HashResponder {
  sendResponse(value: Uint8Array);
};

使用回應者物件有下列好處:

  • 改善人體工學:回應器可用於提供與用戶端的任何互動類型。舉例來說,回應者可以採用關閉管道的方法,並提供傳送事件的 API。如果是雙向方法,回應者可以提供傳送回應的機制。
  • 提高彈性:將所有這些行為封裝在單一類型中,即可在繫結中新增或移除行為,而不會對繫結使用者造成破壞性變更,只要變更回應器物件,而非通訊協定物件。

提供回應器物件時,繫結應注意回應器是否在與處理要求的執行緒不同的執行緒上叫用。回應器也可能在要求處理完畢後很久才叫用,例如實作處理 get 模式時。在實際操作中,您可以允許使用者將回應工具的擁有權移出要求處理常式類別,例如移至非同步函式的回呼。

物件不一定稱為 responder。舉例來說,根據方法是「fire and forget」或雙向,方法可能會有不同的名稱:

interface Hasher {
  // the Hash method is a two-way method, so the object is called a responder
  hash: (value: string, responder: HashResponder): void;
  // the SetSeed method is a fire and forget method, so it gets a different name
  setSeed: (seed: number, control: HasherControlHandle): void;
}

測試

GIDL 符合性測試

GIDL 是一種測試定義語言和工具,可與 FIDL 搭配使用,定義一致性測試。這些測試在繫結之間標準化,可確保編碼器和解碼器的實作一致,並涵蓋極端情況。

解碼物件的深度相等

比較物件是否相等可能很棘手,尤其是解碼的 FIDL 物件。在解碼期間,如果控制代碼的權限超出所需,系統可能會對控制代碼呼叫 zx_handle_replace。發生這種情況時,系統會關閉初始輸入控制代碼,並建立新的控制代碼,以降低權限取代。

因此,控制代碼值無法直接比較。如要比較控制代碼,請檢查其 koid (核心 ID)、類型,並確保權限相同。