FIDL 總覽

本文件概略說明 Fuchsia 介面定義語言 (FIDL),這種語言用來描述 Fuchsia 上執行程式所使用的處理序間通訊 (IPC) 通訊協定。本總覽介紹 FIDL 背後的概念。如果開發人員已熟悉這些概念,可以參閱教學課程開始編寫程式碼,或參閱語言繫結參考資料,進一步瞭解相關資訊。

什麼是 FIDL?

雖然「FIDL」代表「Fuchsia Interface Definition Language」(Fuchsia 介面定義語言),但這個詞本身可用於參照許多不同概念:

  • FIDL 傳輸格式:FIDL 傳輸格式會指定在記憶體中透過 IPC 傳輸時 FIDL 訊息的方式。
  • FIDL 語言:FIDL 語言是 .fidl 檔案中描述通訊協定的語法
  • FIDL 編譯器:FIDL 編譯器會產生程式碼,讓程式使用及實作通訊協定
  • FIDL 繫結:FIDL 繫結是特定語言的執行階段支援程式庫和程式碼產生器,可提供用於操控 FIDL 資料結構和通訊協定的 API。

FIDL 的主要工作是讓各種用戶端和服務可以互通。用戶端多元性是由將處理序間通訊 (IPC) 機制的實作與定義分離,並透過自動產生的程式碼來簡化。

FIDL 語言提供熟悉 (但經過簡化) 的 C 類宣告語法,可讓服務供應商明確定義通訊協定。整數、浮點數和字串等基本資料類型可整理為更複雜的匯總結構和聯集。固定陣列和動態大小的向量可由基本類型和匯總類型建構,並且可以合併為更複雜的資料結構。

有鑑於用戶端實作目標語言 (C、C++、Rust、Dartt 等) 種類繁多,我們不希望服務的開發人員為每項實作語言提供通訊協定實作,

這時,FIDL 工具鍊就能派上用場。服務的開發人員只會建立一個 .fidl 定義檔案,用來定義通訊協定。接著,FIDL 編譯器就會以任何支援的目標語言產生用戶端和伺服器程式碼。

圖:使用 C++ 編寫的伺服器與以多種語言編寫的用戶端通訊

在許多情況下,伺服器只會實作一種 (例如,C++ 中可能會實作特定服務),而用戶端的實作數量則不限於多種語言。

請注意,Fchsia 作業系統對 FIDL 並無與時俱進的知識。FIDL 繫結會在 Fuchsia 中使用標準管道通訊機制。FIDL 繫結和程式庫會強制執行一組語意行為和持續性格式,說明管道的使用方式。

FIDL 架構

從開發人員的觀點來看,主要元件如下:

  • FIDL 定義檔案 — 定義值的和通訊協定 (以參數結尾的方法) 文字檔 (結尾為 .fidl)。
  • 用戶端程式碼 — 由每個特定譯文語言的 FIDL 編譯器 (fidlc) 工具鍊產生。
  • 伺服器程式碼) 的 ID 以及由 FIDL 編譯器工具鍊產生的程式碼。

作為一個簡單的 FIDL 定義檔案範例,請考慮使用「echo」服務。無論是用戶端傳送到伺服器的任何項目,伺服器都會回應回用戶端。

新增行號以求明確,並非 .fidl 檔案的一部分。

1   library fidl.examples.echo;
2
3   @discoverable
4   protocol Echo {
5       EchoString(struct {
6           value string:optional;
7       }) -> (struct {
8           response string:optional;
9       });
10  };

讓我們逐行掃描。

第 1 行:library 關鍵字用於定義此通訊協定的命名空間。不同程式庫中的 FIDL 通訊協定可能會使用相同的名稱,因此可透過命名空間區分這些通訊協定。

第 3 行:@discoverable 屬性表示應將下列通訊協定開放用戶端連線。

第 4 行:protocol 關鍵字列出通訊協定的名稱,此處稱為 Echo

第 5-9 行:方法、參數和傳回值。這一行有兩個少見的面向:

  • 請注意宣告 string:optional (適用於 valueresponse)。string 部分表示參數是字串 (字元序列),optional 限制則指出參數為選用。
  • 這些參數會納入 struct 中,這是包含方法參數的頂層類型。
  • -> 部分代表回傳,會顯示在方法宣告之後,而非之前。與 C++ 或 Java 不同,一個方法可以傳回多個值。

接著,上述 FIDL 檔案已宣告名為 Echo 的通訊協定,其中有一個稱為 EchoString 的方法,該方法會使用可為空值的字串,並傳回可為空值的字串。

上述簡易範例只使用一種資料類型:string 同時是方法的輸入和輸出內容。

可能的 FIDL 資料類型非常彈性:

type MyRequest = struct {
    serial uint32;
    key string;
    options vector<uint32>;
};

上述程式碼會宣告一個稱為 MyRequest 的結構,其中包含三個成員:一個未經簽署的 32 位元整數,稱為 serial,字串稱為 key,無正負號 32 位元整數的向量稱為 options

訊息傳遞模型

為了瞭解 FIDL 的訊息,我們必須將內容分成兩個層,並釐清一些定義。

最下方 (作業系統層) 是以「傳送者」和「接收端」獨立進度為基準,有非同步通訊配置:

  • sender - 傳送訊息的一方
  • 接收端 - 接收訊息的一方,

傳送訊息屬於非阻塞作業:傳送者傳送訊息後,無論接收端執行什麼動作,都可以自由繼續處理。

接收者可以視需要封鎖以便等待訊息。

頂層會實作 FIDL 訊息,並使用底部 (非同步) 層。負責處理「用戶端」和「伺服器」

  • client:提出要求的一方 (伺服器),
  • server:處理要求的一方 (代表用戶端)。

在討論訊息本身時,「寄件者」和「接收者」這兩個詞都很合理,因為底層通訊配置並非考量我們指派給各方的角色,只是這個人正在傳送,而另一個人正在接收。

而「用戶端」和「伺服器」這兩個詞彙指的是各方扮演的角色。特別是,用戶端一次可以做為寄件人,而接收端也可以是不同時間的接收者;對伺服器來說也是如此。

實際上,在用戶端 / 伺服器互動情境中,有數種模型:

  1. blocking Call - 用戶端傳送至伺服器,等待回覆
  2. 啟動並忘記 - 用戶端傳送至伺服器後,不會預期回覆
  3. 「回呼」或「非同步呼叫」:用戶端傳送至伺服器,但不會封鎖;回覆會在一段時間後以非同步方式傳送
  4. event - 伺服器傳送至用戶端,用戶端不必取得資料

第一種是同步性質,其餘的都是非同步。我們將依序討論。

用戶端傳送至伺服器,等待回覆

在大部分的程式設計語言中,這種模型都是傳統的「封鎖呼叫」或「函式呼叫」,只是叫用是透過管道完成,因此可能會因為傳輸層級錯誤而失敗。

從用戶端的觀點來看,其中包含區塊的呼叫,而伺服器則會執行一些處理作業。

圖:用戶端和伺服器

以下是逐步說明:

  1. 用戶端發出呼叫 (選擇性包含資料) 和區塊。
  2. 伺服器收到用戶端的呼叫 (和選用資料),並執行一些處理作業。
  3. 伺服器會自行斟酌是否回應用戶端 (附上選用資料)。
  4. 伺服器的回覆會導致用戶端解除封鎖。

如要透過非同步訊息傳遞配置實作這個同步訊息模型,方法非常簡單。提醒您,用戶端對伺服器和伺服器對用戶端的訊息傳輸都是在通訊協定的底層,以非同步的方式執行。同步處理作業會在用戶端執行,方法是讓用戶端封鎖,直到伺服器訊息送達為止。

基本上,在這個模式中,用戶端和伺服器必須達成協議:

  • 資料流程是由用戶端啟動
  • 客戶最多只能有一則訊息
  • 伺服器只有在回應用戶端訊息時,才會 將訊息傳送給用戶端
  • 用戶端必須等待伺服器回應才能繼續。

這個封鎖模型經常用於用戶端,需要先取得目前要求的回覆才能繼續。

舉例來說,用戶端可能會向伺服器要求資料,直到資料送達前,無法執行其他任何實用的處理作業。

或者,用戶端可能需要按特定順序執行步驟,因此必須在開始下一項步驟前確保每個步驟已完成。如果發生錯誤,用戶端可能需要執行要依作業完成程度的修正操作,也就是與其他步驟同步完成的另一個原因。

用戶端傳送至伺服器,無回覆

這種模式又稱為「射後不理」。在範例中,用戶端傳送訊息至伺服器,然後繼續處理其作業。與封鎖模型相比,用戶端「並未」封鎖,「也沒有」預期回應。

當用戶端不需要 (或無法) 同步處理到處理要求時,就會選用此模型。

圖:Fire and Forget;用戶端會傳送至伺服器,但不會出現回覆

以記錄系統為例,用戶端將記錄資訊傳送至記錄伺服器 (上圖中的「1」和「2」圓圈),但沒有原因進行封鎖。許多伺服器都可能發生這個問題:

  1. 伺服器忙碌中,目前無法處理寫入要求
  2. 媒體檔案已滿,伺服器無法寫入資料
  3. 伺服器發生錯誤
  4. 依此類推

不過,用戶端並未設法處理這些問題,因此封鎖功能只會產生更多問題。

用戶端傳送至伺服器,但不會封鎖

這個模型和下一個模型 (「伺服器傳送至用戶端,不會要求資料」) 很類似。

在目前的模型中,用戶端會傳送訊息至伺服器,但不會封鎖。不過,用戶端會預期有一些來自伺服器的回應,但這裡的鍵值是不與要求「同步」

這樣就能在用戶端 / 伺服器互動時享有極大彈性。

雖然同步模型會強制用戶端等待伺服器回覆,但現在的模型在伺服器處理要求時,會釋出用戶端來執行其他作業:

圖:用戶端傳送至伺服器,但之後不會封鎖

本圖表與上方類似規則的細微差異是,圓形「1」之後,用戶端仍在執行中。用戶端會選擇佔用 CPU 的時機;但不會與訊息同步。

這裡實際上有兩個子案例:一個用戶端只收到一個回應,另一個則用戶端可以取得多個回應。(用戶端收到零回應時,即為我們先前討論的「啟動後忘記」模式)。

單一要求、單一回應

單一回應案例最接近同步模型:用戶端會傳送訊息,最後則伺服器會回覆。例如,如果您知道用戶端在等待伺服器的回覆時,工作可能會很有用,就會使用這個模型,而非多執行緒。

單一要求,多個回應

您可以在「訂閱」模型中使用多個回應案例。例如,用戶端的訊息會「質疑」伺服器,例如,系統會在發生狀況時要求通知。

接著,客戶進入了業務。

一段時間後,伺服器會注意到用戶端感興趣的條件已經發生,因此會向用戶端傳送訊息。從用戶端/伺服器的角度來看,這則訊息稱為「回覆」,用戶端以非同步方式接收到其要求。

圖:用戶端傳送至伺服器,伺服器多次回覆

沒有原因為何,當發生其他感興趣的事件時,伺服器無法傳送其他訊息;這就是模型的「多個回應」版本。請注意,送出第二個 (及後續) 回應時,「不會」用戶端傳送任何其他訊息。

請注意,用戶端不需要等待伺服器傳送訊息。在上圖中,我們已在圓形「3」前顯示用戶端處於封鎖狀態,而用戶端也可能一直在執行中。

伺服器傳送至用戶端,不會要求資料

此模型又稱為「事件」模型。

圖:從伺服器傳送到用戶端的來路不明的郵件

在這個例子中,用戶端準備接收來自伺服器的訊息,但不知道預定的時間 — 訊息不僅與用戶端進行非同步傳送,還 (來自用戶端 / 伺服器觀點)「主動」是指用戶端未明確要求這些訊息 (如同在前文模型中的要求)。

用戶端指定訊息從伺服器送達時要呼叫的函式 (「事件處理函式」),其他部分則繼續處理其業務。

如果是由伺服器自行決定 (上圖中的「1」和「2」圓圈),訊息會以非同步方式傳送至用戶端,然後由用戶端的指定函式處理。

請注意,傳送訊息時 (如圓形「1」所示),用戶端可能已在執行訊息,或者用戶端可能沒有執行任何作業,因此可能在等待訊息傳送後 (如圓圈「2」中)。

客戶不一定要等候訊息。

非同步訊息傳遞複雜度

將非同步訊息傳遞到上述 (某些任意) 類別是為了顯示一般使用模式,但並非詳盡無遺。

在最常見的非同步訊息傳遞情況中,您有零或多個用戶端訊息,都沒有與零或多個伺服器回覆過於關聯。這就是「鬆散關聯」,可以增加設計程序的複雜度。

FIDL 中的 IPC 模型

現在,我們已瞭解處理序間通訊 (IPC) 模型,以及這些模型如何與 FIDL 的非同步訊息互動,讓我們來看看定義方式。

我們會將其他模型 (啟動和忘記,以及非同步呼叫或事件) 新增至通訊協定定義檔案:

1   library fidl.examples.echo;
2
3   @discoverable
4   protocol Echo {
5       EchoString(struct {
6           value string:optional;
7       }) -> (struct {
8           response string:optional;
9       });
10
11      SendString(struct { value string:optional; });
12
13      ->ReceiveString(struct { response string:optional; });
14  };

「第 5-9 行」是之前討論的 EchoString 方法,這是傳統函式呼叫訊息,其中用戶端會使用選用字串呼叫 EchoString,然後封鎖並封鎖伺服器透過其他選用字串回覆。

第 11 行SendString 方法。程式碼沒有 -> 傳回宣告,導致這個模型變成「啟動後忘記」模型 (僅限傳送),因為已告知 FIDL 編譯器這個特定方法沒有相關聯的回傳。

請注意,這並非缺少傳回「參數」,而是缺少傳回做為鍵的傳回「宣告」。在 SendString 後方加上「-> ()」,這樣做就會改變意義,因為宣告觸發的函式呼叫樣式方法沒有任何傳回引數。

第 13 行ReceiveString 方法。這有些差異,因為第一部分沒有方法名稱,而是在 -> 運算子之後指定。此做法會告知 FIDL 編譯器這是「非同步呼叫」或「事件」模型宣告。

FIDL 繫結

FIDL 工具鍊會納入 FIDL 通訊協定和類型定義 (如上例所示),並且以每個可「朗讀」這些通訊協定的指定語言產生程式碼。產生的程式碼稱為 FIDL 繫結,根據語言的不同,有多種版本皆可使用:

  • 原生繫結:專為高度敏感的結構定義 (例如裝置驅動程式和高處理量伺服器) 設計,利用就地存取、避免記憶體配置,但可能需要對開發人員的部分通訊協定限制有所瞭解。
  • 慣用繫結:設計為更容易使用的資料類型,例如將傳輸格式中的資料複製到易於使用的資料類型 (例如堆積字串或向量),但效率相對較低。

繫結提供多種叫用通訊協定方法的方法,視語言而定:

  • 傳送/接收:直接讀取或寫入訊息至管道,無內建等待迴圈 (C)
  • 以回呼為基礎的:收到的訊息會以非同步方式分派為事件迴圈 (C++、Dart) 的回呼
  • 通訊埠型:收到的郵件會傳送至通訊埠或未來 (Rust)
  • 同步呼叫:等待回覆並傳回 (Go、C++ 單元測試)

繫結提供下列部分或所有主體作業:

  • 編碼:就地將原生資料結構轉換為傳輸格式 (搭配驗證)
  • 解碼:就地將傳輸格式資料轉換為原生資料結構 (結合驗證)
  • 複製/移動到慣用格式:將原生資料結構的內容複製到慣用資料結構,並移動控點
  • 複製/移動到原生表單:將慣用資料結構的內容複製到原生資料結構,並移動控點
  • Clone:複製原生或慣用的資料結構 (不含移動專用類型)
  • 呼叫:叫用通訊協定方法

用戶端實作

無論譯文語言為何,fidlc FIDL 編譯器都會產生具有以下基本結構的用戶端程式碼。

第一部分包含管理和背景處理,並由下列項目組成:

  1. 我們提供了一些伺服器連線方式
  2. 非同步 (「背景」) 訊息處理迴圈
  3. 非同步呼叫樣式和事件樣式方法 (如果有的話) 都會繫結至訊息迴圈

第二部分包含實作傳統函式呼叫或啟動,以及忘記樣式方法的實作 (適合目標語言)。一般而言,這包含:

  1. 建立可呼叫的 API 和宣告
  2. 為每個 API 產生程式碼,可將呼叫的資料整理成可傳輸至伺服器的 FIDL 格式緩衝區
  3. 產生程式碼來將資料傳送到伺服器
  4. 如果是函式呼叫樣式呼叫,則會產生以下程式碼:
    1. 等待伺服器回應
    2. 將 FIDL 格式緩衝區中的資料解壓縮,以及
    3. 透過 API 函式傳回資料。

請注意,實際步驟可能會因語言實作差異而有所不同,但這是基本大綱。

伺服器實作

fidlc FIDL 編譯器也會為特定指定語言產生伺服器程式碼。如同用戶端程式碼,此程式碼具有共同結構,無論目標語言為何。程式碼:

  1. 建立 物件,以便用戶端連接該物件、
  2. 會啟動主要處理迴圈,在發生以下情形時:
    1. 等待訊息
    2. 藉由呼叫實作函式來處理訊息
    3. 如果有指定,系統會向用戶端發出非同步呼叫,以便傳回輸出內容

在下一章中,我們會針對各語言實作的用戶端和伺服器程式碼,顯示相關詳細資料。

為什麼要使用 FIDL?

Fuchsia 廣泛使用 IPC,因為大多數功能都是在核心以外的使用者空間中實作,包括裝置驅動程式等特殊權限元件。因此處理序間通訊 (IPC) 機制必須兼顧效率、確定性、完善且易於使用:

IPC 效率涉及在程序之間產生、傳輸和耗用訊息時所需的運算負擔。處理序間通訊 (IPC) 會涉及系統運作的所有層面,因此必須有效率地進行。FIDL 編譯器必須產生緊湊的程式碼,而不多提供間接或隱藏費用。但至少應該像手動捲動程式碼一樣重要。

IPC 確定性是指在已知資源信封內執行交易的能力。重要系統服務 (例如檔案系統) 將會大量使用處理序間通訊 (IPC),這類服務會為許多用戶端提供服務,且必須以可預測的方式執行。FIDL 傳輸格式必須提供強而有力的靜態保證,例如確保結構大小和版面配置保持不變,以減輕動態記憶體配置或複雜驗證規則的需求。

IPC 穩定性有助於將處理序間通訊 (IPC) 視為作業系統 ABI 的重要部分。維持二進位檔的穩定性非常重要。通訊協定演進的機制必須在設計上保守,以免違反現有服務及其用戶端的演變,特別是在有確定性的需求時。FIDL 繫結必須有效、輕量且嚴格驗證。

IPC 易用性說明處理序間通訊 (IPC) 通訊協定是作業系統 API 不可或缺的一部分。為透過 IPC 存取服務提供良好的開發人員體工學至關重要。FIDL 程式碼產生器可省去手動寫入 IPC 繫結的負擔。此外,FIDL 程式碼產生器也可以產生不同的繫結,以滿足不同目標對象的需求及其慣用語。

目標

FIDL 是專為上述特點而設計。請特別注意,FIDL 的設計旨在達成下列目標:

優先權

  • 說明 Zircon IPC 使用的資料結構和通訊協定。
  • 已針對處理序間通訊進行最佳化。雖然 FIDL 也用於保存磁碟及進行網路傳輸,但其設計並未針對這些次要用途進行最佳化。
  • 在同一部裝置上執行的程序之間,有效率地傳輸含有資料 (位元組) 和功能 (處理) 的訊息。
  • 專為提升 Zircon 基本功能而設計。雖然 FIDL 在其他平台上 (例如透過 ffx 使用),但其設計以 Fuuchsia 為優先。
  • 提供便利的 API,可用於建立、傳送、接收及使用訊息。
  • 執行充足的驗證以維持通訊協定不變 (但不要超過)。

睡眠效率

  • 與使用手動捲動資料結構的效率 (速度和記憶體) 一樣。
  • 傳輸格式使用未壓縮的原生資料類型,搭配極少的完整性和正確的對齊方式,以便直接存取訊息內容。
  • 當訊息的大小是靜態已知或受限時,不需要動態記憶體配置產生或耗用訊息。
  • 使用僅移動的語意明確處理擁有權。
  • 資料結構的封裝順序為標準、不明確,且設有最小邊框間距。
  • 避免修補指標。
  • 避免高昂的驗證作業。
  • 避免計算過度溢位的計算。
  • 運用通訊協定要求的管道進行非同步作業。
  • 結構大小固定;變形大小資料是以換行的方式儲存。
  • 結構物不會自我說明,FIDL 檔案會描述其內容。
  • 沒有結構版本管理,但通訊協定可透過新方法擴充。

人體工學

  • Fuchsia 團隊維護的程式設計語言繫結:
    • C、New C++、高階 C++ (舊)、Dart、Go、Rust
  • 請注意,我們日後可能會希望支援其他語言,例如:
    • Java、JavaScript 等
  • 視預期的應用程式而定,繫結和產生的程式碼可分為原生或慣用的變種版本。
  • 使用編譯時間程式碼產生功能,將訊息序列化、去序列化和驗證最佳化。
  • FIDL 語法熟悉、易於存取,而且各種程式設計語言都適用。
  • FIDL 提供程式庫系統,可簡化其他開發人員的部署及使用流程。
  • FIDL 會提供系統 API 最常見的資料類型,不會試圖針對所有程式設計語言提供的所有類型,提供全面的一對一對應。

工作流程

本節會回顧使用 FIDL 所述 IPC 通訊協定的作者、發布者與消費者的工作流程。

編寫 FIDL

FIDL 型通訊協定的作者會建立一或多個 *.fidl 檔案,用於描述其資料結構、通訊協定和方法。

FIDL 檔案會由作者分成一或多個 FIDL 程式庫。每個程式庫都代表一組邏輯相關功能,且具有專屬的程式庫名稱。相同程式庫中的 FIDL 檔案以隱含方式存取同一個程式庫中的所有其他宣告。組成程式庫的 FIDL 檔案內的宣告順序並不重要。

其中一個程式庫的 FIDL 檔案可以匯入其他 FIDL 模組,以存取另一個 FIDL 程式庫中的宣告。匯入其他 FIDL 程式庫後就能使用這些符號,進而建構從這些程式庫衍生的通訊協定。匯入的符號必須使用程式庫名稱或別名限定,以免命名空間衝突。

正在發布 FIDL

以 FIDL 為基礎的通訊協定發布者負責為消費者提供 FIDL 程式庫。例如,作者可以在公開原始碼存放區中散佈 FIDL 程式庫,或將其做為 SDK 的一部分發布。

取用者只需要將 FIDL 編譯器指向包含程式庫 FIDL 檔案 (及其依附元件) 的目錄,即可產生該程式庫的程式碼。而消費者的建構系統通常會處理具體的詳細操作方式。

使用 FIDL

以 FIDL 為基礎的通訊協定時,會使用 FIDL 編譯器產生適合與其語言執行階段專屬繫結搭配使用的程式碼。針對特定語言執行階段,使用者可選擇幾種不同的產生的程式碼變種版本,這些變種版本可在傳輸格式層級互通,但不一定在來源層級。

在 Fuchsia 世界建構環境中,系統會針對每個程式庫的個別 FIDL 建構目標,自動為所有相關語言產生 FIDL 程式庫的程式碼。

在 Fuchsia SDK 環境中,系統會在編譯使用這些程式庫的應用程式的過程中,從 FIDL 程式庫產生程式碼。

踏出第一步

如要進一步瞭解如何使用 FIDL,請參閱「指南」章節提供許多開發人員指南教學課程,可供您嘗試。如果您在 Fuchsia 上進行開發,並想瞭解如何針對現有 FIDL API 使用繫結,請參閱 FIDL 繫結參考資料。最後,如要進一步瞭解 FIDL 或想提供翻譯內容,請參閱 FIDL 語言參考資料提供文件