FIDL 繫結規格

本文件是 Fuchsia 介面定義語言 (FIDL) 繫結的規格。其旨在提供繫結作者的相關指引與最佳做法,並針對其人體工學用途建議特定做法。

在本文件中,下列關鍵字必須依 RFC2119 中的說明定義:可能必須不得選用建議REQUIREDSHALLSHALL NOT應該

產生的程式碼指標

註解必須置於機器產生的程式碼頂端,以表示該註解是由機器產生。對於具有如何表示產生來源 (而非人工編寫程式碼) 的標準語言,請務必遵守該標準。

例如,在 Go 中,產生的來源必須依照模式使用註解標記

// Code generated by <tool>; DO NOT EDIT.

設定範圍

為了避免與使用者定義的符號衝突,建議您採用機器產生的程式碼。您可以利用語言提供的範圍結構實作,例如 C++ 中的命名空間、Rust 中的模組,或 Go 和 Dart 中的套件。如果產生的範圍可以有名稱,則其名稱必須使用包含所產生程式碼定義的 FIDL 程式庫名稱元件來命名,以便讓每個 FIDL 程式庫都存在於不重複的範圍內。如果無法設定範圍且命名空間已共用,則可能需要處理一些產生的名稱 (請參閱命名一節)。

命名

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

如果是內嵌版面配置,繫結「必須」使用 fidlc 產生的名稱,因為這類名稱保證不會重複。

如果目標語言支援 FIDL 名稱的命名結構定義,則繫結 MAY 產生的限定範圍名稱。以某些 FIDL 為例:

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

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

大小寫

您「必須」根據語言的慣用樣式進行口述變更 (例如使用 snake_case 或 CamelCase)。fidlc 可確保 ID 不重複性會考量潛在的大小寫差異 (請參閱 RFC-0040)。

保留的關鍵字和名稱衝突

產生的程式碼「必須」考量指定語言中的保留關鍵字,以避免在 FIDL 定義中使用來自指定語言的關鍵字時出現非預期的關鍵字。例如,系統會為衝突的名稱加上底線 _ (假設沒有任何關鍵字以底線開頭)。

產生的程式碼「必須」避免產生會造成命名衝突的程式碼。舉例來說,在根據 FIDL 定義產生參數的函式中,「必須」避免產生的本機變數名稱與可能產生的名稱衝突。

序數

方法序數

方法的序數是較大的 64 位元數字。繫結應以十六進位發出這些標準,也就是 0x60e700e002995ef8,而非 6982550709377523448

聯集和資料表序數

uniontable 使用的序數從 1 開始,且必須形成稠密的空間。因此,這些數值通常較小,且繫結「應」以十進位標記法輸出這些序數。

原生類型

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

產生的類型和值

持續支援

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

位元支援

繫結「必須」為每個位元成員產生產生的值。這些變數也會產生值代表未設定標記的位元,以及每個標記組合的位元 (「位元遮罩」)。這些值的範圍必須限定為每組位元。

建議您針對產生的值支援下列運算子:

  • 位元,例如 &
  • 位元,例如 |
  • 位元組互斥或,例如 ^
  • 不是位元,例如 ~
  • 位元差異,亦即一個運算元中的所有位元,但另一個運算元中出現的位元除外。這通常由 - 運算子表示。

FIDL 位元作業的變體是不應導入不明位元,除非來源運算元中有對應的未知位元。所有建議的運算子自然都有這個屬性,但 bitwise not 除外。bitwise not 運算的實作「必須」進一步遮蓋產生的值,所有值的遮罩。在虛擬程式碼中:

~value1   means   mask & ~bits_of(value1)

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

在支援運算子超載的語言 (例如 C++) 中,您必須透過在手冊中超載內建運算子來實作 bitwise not 作業,該運算子會一律取消設定位元欄位的不明成員。在不支援運算子超載的語言 (例如 Go) 中,值「必須」提供 InvertBits() 方法 (以最適合該語言的方式顯示) 來執行遮蓋的反轉版本。

清除位元時,應優先選用位元差異運算子,而非位元的運算子,因為前者會保留不明的位元:

// 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

繫結「不」支援其他運算子,否則可能會造成位元值無效 (或可能導致無法察覺其含義的翻譯),例如:

  • 位元位移,例如 <<>>
  • 位元未帶正負號的位移,例如 >>>

如果產生的程式碼含有包裝基礎數字位元值的類型,則原始值和包裝函式類型之間應該可以轉換。建議您明確提供這項轉換。

繫結 MAY 提供了函式,可將 bits 的基礎類型的原始值轉換為 bits 類型本身。這些轉換器可以是多種變種版本:

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

不明資料

如為「彈性」位元:

  • 繫結「必須」提供可檢查值是否包含任何未知位元的方法,並「可能」提供擷取這些未知位元的方法。
  • 位元運算子不會取消所有不明成員,無論其先前的值為何 (但運作方式適用於已知成員)。其他位元運算子會為已知成員的未知位元保留相同的語意。

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

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

在有經過編譯檢查淘汰警告的語言 (例如 Rust) 中,如果語言具有嚴格的位元,「必須」為嚴格位元提供與資料相關的不明 API,但這些 API 會標示為已淘汰。

支援列舉

繫結「必須」為每個列舉成員提供產生的值。這些值應限定為每個列舉。

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

不明資料

對於彈性列舉:

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

結構支援

繫結「必須」為每個支援以下作業的結構提供類型:

  • 每個成員都有明確值的建構函式。
  • 讀取及寫入成員。

支援聯盟

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

  • 具有明確變化版本集的建構。我們不建議繫結在沒有變化版本的情況下提供建構。這只應在效能考量或指定語言限制的情況下考慮。
  • 讀取/寫入聯集的變數及與該變數相關聯的資料。

針對沒有聯集類型或聯集值常值的語言,建議針對其中一個可能的變體提供一個值,支援建構新聯集的工廠方法。例如,使用 C 等語言取代程式碼:

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

類似如下:

do_stuff(my_union_with_variant(bar));

這些工廠方法應命名為 [Type]-with-[Variant],確保以譯文語言正確大小寫。

這類範例適用於 HLCPPGo 繫結。

不明資料

如為彈性聯集

  • 當序數不明時,「必須」成功解碼,但重新編碼「不得」失敗。
  • 繫結「必須」可讓您判斷聯集是否具有未知的變體。
  • 繫結可讓您存取不明變化版本的序數。
  • 繫結「可以」提供建構函式來建立含有不明變化版本的聯集。
    • 請務必為建構函式命名,避免在實際工作環境程式碼中使用,例如 unknown_variant_for_testing()
    • 建構函式「不得」允許使用者選擇序數。
    • 使用建構函式可避免開發人員以圓形方式建構具有不明變化版本的聯集,例如手動解碼原始位元組。

資料表支援

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

  • 此建構函式為選擇性指定每個成員的值。
  • 讀取及寫入每個成員,包括檢查是否已設定特定成員。這些命名架構必須遵循以下命名配置:get_[member]set_[member]has_[member] (針對譯文語言正確大小寫)。

繫結「可能」提供資料表的建構函式,因為資料表只需要為含有值的欄位指定值。舉例來說,在 Rust 中,您可以使用 ::EMPTY 常數和結構更新語法來完成這項操作。以這種方式支援建構,可讓使用者撰寫程式碼,避免在資料表中加入新欄位。

不明資料

所有資料表都是彈性

出現不明欄位時,「必須」成功解碼及重新編碼。重新編碼時「必須」省略未知的欄位。

繫結 能夠判定資料表在解碼期間是否包含任何不明欄位。而他們或許提供了一個序位的一種存取方式。

繫結「不得」建立具有不明欄位的資料表,或是為現有資料表設定不明欄位。

嚴格和彈性類型

遇到任何不明資料時,嚴格類型必須無法解碼。如要解碼含有不明資料的值,則「必須」使用彈性類型。

以下舉例說明彈性 FIDL 類型及其在未知威脅上的行為:

FIDL 類型 取得未知 重新編碼保真度
彈性位元 原始整數 無損
彈性列舉 原始整數 無損
彈性聯集 布林值或序數 失敗次數
桌子 布林值或序數 有損

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

繫結作者應優先最佳化 strict 類型,這可能涉及 flexible 類型。舉例來說,如果兩者在設計方面有取捨,繫結作者「建議」將 strict 類型最佳化。

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

值類型和資源類型

值類型「不得」含有帳號代碼,且資源類型「有可能」包含帳號代碼。

在值類型與彈性類型的互動中,系統會以彈性類型要求為優先。具體來說,如果將包含未知控點的彈性值類型解碼,則「必須」成功。

通訊協定支援

事件

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

如果使用者未指定事件的處理邏輯,接收事件時,繫結必須執行一般通訊。換句話說,傳送事件時,使用者未在用戶端指定對應的處理邏輯,則不算是錯誤。

繫結「應」盡量減少使用者在指定事件處理常式和抵達端點的事件之間發生的兒童不宜行為。

收到未知嚴格事件時,繫結「必須」關閉連線。

網域錯誤類型

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

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

具例外狀況的語言可以產生系統產生的通訊協定方法程式碼,可選擇引發與錯誤類型對應的例外狀況。

如果無法這樣做,產生的程式碼「可能」可提供便利的功能,以便直接透過成功的回應或錯誤值回應,或接收錯誤類型回應,避免樣板使用者程式碼來初始化結果聯集。

處理錯誤

通訊協定可能將傳送錯誤傳回給使用者。傳輸錯誤可歸類為在原生類型和傳輸格式資料之間轉換時發生的錯誤,也可能歸類為來自基礎傳輸機制的錯誤 (例如呼叫 zx_channel_write_etc 後取得的錯誤)。這些錯誤可能包括錯誤狀態和其他診斷資訊。

我們將這些傳輸錯誤定義為終端機。文件的其餘部分也可能會指定終端機錯誤等其他情況,例如不正確的交易 ID。

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

相較之下,網域錯誤 (在以 error 語法宣告的方法中) 和架構錯誤 (在 flexible 雙向方法中) 並非終端機。

終端機錯誤處理

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

由於 FIDL 的 IPC 傳輸模型不含暫時性錯誤,因此不會有值,例如重試傳送相同的回覆。因錯誤而觸發拆解,可以鼓勵這種繫結方式使用,並簡化錯誤處理流程。

繫結「可能」提供同步用戶端與伺服器 API。在同步 API 中,在發生終端機錯誤時關閉端點,通常必須採取鎖定措施。如果出於效能考量導致這些 API 不適合,這些 API 可能會在終端機發生錯誤時保持連線開啟,並應據此清楚記錄。非同步變種版本「應」是建議的 API 變種版本。

繫結「可以」提供不具備基礎端點的用戶端和伺服器 API,以便滿足低階用途。這些 API 無法在終端機發生錯誤時關閉端點,應按規定明確記錄。自有變種版本「應」建議採用的 API 變種版本。

對等點關閉特殊處理

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

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

這是為了與管道的讀取語意保持一致:指定一組端點 A <-> B 假設有數個訊息寫入 B,然後 B 會關閉。使用者可以從 A 繼續讀取,而不會觀察對等點關閉錯誤,直到所有待處理訊息都清空為止。換句話說,除非無法再從端點讀取任何訊息,否則「對等點關閉」並非嚴重錯誤。

處理類型和權利檢查

繫結「必須」對傳入和傳出方向強制執行控制代碼類型和權限檢查。這表示務必使用 zx_channel_write_etczx_channel_read_etczx_channel_call_etc,而非其他其他的對等項目。

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

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

假設控制代碼 h 已讀取,且該帳號代碼在 FIDL 檔案中為 R

  • 處理 h 缺少權利 R 中缺少的權限會發生錯誤。出現這個狀況時,必須關閉管道。
  • 如果帳號代碼的 h 權利超過 R 的權利,則其權利必須降為 Rzx_handle_replace

此外,h 類型不正確也會發生錯誤。發生這個條件時,管道必須關閉。

如需詳細範例,請參閱「帳號代碼的壽命」一文。

Iovec 支援

繫結可以選擇使用向量化 zx_channel_write_etczx_channel_call_etc 系統呼叫。使用這些方法時,第一個 iovec 項目「必須」存在且大小足以保留 FIDL 交易訊息標頭 (16 個位元組)。

屬性

繫結必須支援下列屬性

  • @transitional

最佳做法

替代輸出內容

繫結為 FIDL 線格式的替代輸出方法為選用。

針對產生的類型,其中一種輸出可能是易於使用的輸出偵錯。例如,輸出位元的值:

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

可能會列印字串 "Mode.Read | Mode.Write",而非原始值 "0b11"

每種產生的 FIDL 類型都可以實作類似的列印操作,方便使用者閱讀。

訊息記憶體配置

繫結功能可讓使用者在傳送或接收訊息時提供自己的記憶體,讓使用者控管記憶體配置。

線路格式記憶體配置

繫結「可能」對於所產生 FIDL 類型的記憶體配置符合該類型的傳輸格式。這種做法在理論上可以避免產生多餘的副本,因為資料可直接做為交易訊息使用,反之亦然。在實務上,傳送 FIDL 訊息可能仍會包含複製步驟,這樣訊息的元件會組合成連續的記憶體區塊 (稱為「線性化」)。這種方法的缺點是,繫結會讓繫結更嚴謹:對 FIDL 線路格式的變更會變得更加複雜,無法實作。

C++ 線繫結繫結是採用此方法的唯一繫結。

相等性比較

針對結構類型、資料表和聯集等匯總類型,繫結 MAY 提供了相等運算子,可針對相同類型的兩個執行個體執行深度比較。這些運算子「不得」提供資源類型 (請參閱 RFC-0057深度相等),因為無法比較帳號代碼。避免為資源類型公開等等運算子,可在在類型中新增帳號代碼時,避免因等式運算「消失」而導致來源毀損。

複製中

對於結構、資料表和聯集等匯總類型,繫結「可以」提供複製這些類型執行個體的功能。請「勿」針對資源類型 (請參閱 RFC-0057) 提供複製功能,因為我們不保證能成功建立帳號。避免公開資源類型的複製運算子,可避免複製作業「消失」所造成的來源損毀,或在將控制代碼加入類型時出現簽章變更的情況。

測試公用程式

繫結是選用項目,可產生測試期間專用的其他程式碼。舉例來說,繫結可以產生每個通訊協定的虛設常式實作,讓使用者只需要過度執行於測試中練習的特定方法。

麻黃

繫結「必須」支援代表音符,意即產生的程式碼可讓伺服器傳送 Epitaph 和用戶端,以便接收及處理重要內容。

setter 和 getter

繫結 MAY 會針對匯總類型 (結構體、聯集和資料表) 的欄位提供 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;
};

繫結「可以」提供用來寫入回應的回應者物件。在上述範例中,這意味著在函式引數中傳遞額外的回應器物件,且函式傳回無效值:

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

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

使用回應者物件有以下優點:

  • 改善人體工學:回應者可用於提供任何與用戶端的互動。舉例來說,回應者可以提供以孔道關閉管道的方法,或是提供傳送事件的 API。對於雙向方法,回應者可提供傳送回應的機制。
  • 增加彈性:在單一類型中封裝所有這些行為,就可以透過變更回應者物件 (而非通訊協定物件) 的方式,在繫結中新增或移除行為,而不必對繫結使用者進行破壞性變更。

提供回應者物件時,如果在其他執行緒上叫用了回應者,而不是要求處理要求的執行緒,繫結應謹慎處理。比起處理要求的時間 (例如實作手部取得模式時),也可能叫用回應者。實作時,您可以允許使用者將回應者的擁有權移出要求處理常式類別,例如為非同步函式的回呼。

物件「可能」未必稱為回應者。舉例來說,依據該方法是觸發和忘記,也可能有不同的名稱:

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 擁有更多所需的處理權利,系統可能會呼叫帳號。發生這種情況時,系統會關閉初始輸入控點,然後建立新的控制代碼,以減少的權利取代。

因此,您無法直接比較處理值。相對地,可以透過檢查消費性與否 (核心 ID) 的型式、型別及確保其權利,來比較帳號代碼。