本文將概略介紹 Fuchsia 介面定義語言 (FIDL),這種語言用於描述在 Fuchsia 上執行的程式所用的跨程序通訊 (IPC) 協定。這份總覽會介紹 FIDL 背後的概念,熟悉這些概念的開發人員可以按照教學課程開始編寫程式碼,或閱讀語言或繫結參考資料,深入瞭解相關知識。
什麼是 FIDL?
雖然「FIDL」代表「Fuchsia 介面定義語言」,但這個字本身可用來指稱多種不同概念:
- FIDL 線路格式:FIDL 線路格式會指定 FIDL 訊息在記憶體中的表示方式,以便透過 IPC 傳輸
- FIDL 語言:FIDL 語言是描述
.fidl檔案中通訊協定的語法 - FIDL 編譯器:FIDL 編譯器會產生程式碼,供程式使用及實作通訊協定
- FIDL 繫結:FIDL 繫結是語言專屬的執行階段支援程式庫和程式碼產生器,可提供 API 來操控 FIDL 資料結構和通訊協定。
FIDL 的主要工作是讓各種用戶端和服務互通。將 IPC 機制的實作與定義分離,有助於提升用戶端多樣性,而自動生成程式碼則可簡化作業。
FIDL 語言提供類似 C 的宣告語法 (但經過簡化),服務供應商可藉此精確定義通訊協定。整數、浮點數和字串等基本資料類型,可以整理成更複雜的聚合結構和聯集。固定陣列和動態大小的向量可從基本型別和匯總型別建構,這些型別全都可以合併為更複雜的資料結構。
由於用戶端實作目標語言數量眾多 (C、C++、Rust、Dart 等),我們不希望服務開發人員為每種語言提供通訊協定實作。
這時 FIDL 工具鍊就派上用場了。服務開發人員只需建立一個 .fidl 定義檔,定義通訊協定。FIDL 編譯器會使用這個檔案,以任何支援的目標語言產生用戶端和伺服器程式碼。

在許多情況下,伺服器只會有一種實作方式 (例如,特定服務可能以 C++ 實作),但用戶端實作方式則不限數量,且可使用多種語言。
請注意,Fuchsia 作業系統本身並不知道 FIDL。FIDL 繫結會使用 Fuchsia 的標準管道通訊機制。FIDL 繫結和程式庫會對管道的使用方式強制執行一組語意行為和持續性格式。
FIDL 架構
從開發人員的角度來看,主要元件如下:
- FIDL 定義檔案:這是文字檔 (按照慣例以
.fidl結尾),用於定義值和通訊協定 (含參數的方法)。 - 用戶端程式碼:由 FIDL 編譯器 (
fidlc) 工具鍊為每個特定目標語言產生,以及 - 伺服器程式碼 - 也由 FIDL 編譯器工具鍊產生。
以「echo」服務為例,這是一個非常簡單的 FIDL 定義檔案,無論用戶端傳送什麼內容給伺服器,伺服器都會回傳給用戶端。
為求清楚,我們已新增行號,但行號不屬於
.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宣告 (適用於value和response)。string部分表示參數為字串 (字元序列),而optional限制表示參數為選用。 - 參數會包裝在
struct中,這是包含方法參數的頂層型別。 ->部分表示傳回值,會顯示在方法宣告之後,而非之前。與 C++ 或 Java 不同,方法可以傳回多個值。
因此,上述 FIDL 檔案已宣告一個通訊協定 (名為 Echo),其中包含一個方法 (名為 EchoString),該方法會採用可為空值的字串,並傳回可為空值的字串。
上述簡單範例只使用一種資料型別,即 string,做為方法的輸入和輸出。
可能的 FIDL 資料類型非常彈性:
type MyRequest = struct {
serial uint32;
key string;
options vector<uint32>;
};
上述程式碼會宣告名為 MyRequest 的結構體,其中包含三個成員:名為 serial 的未簽署 32 位元整數、名為 key 的字串,以及名為 options 的未簽署 32 位元整數向量。
訊息模型
如要瞭解 FIDL 的訊息,我們需要將內容分成兩層,並釐清一些定義。
在底層 (作業系統層),有一種非同步通訊機制,可讓傳送者和接收者各自獨立運作:
- 寄件者:發送訊息的一方。
- 接收者:接收訊息的一方。
傳送訊息是非封鎖作業:傳送端傳送訊息後,無論接收端正在執行什麼作業,傳送端都可以繼續處理。
接收器可以視需要封鎖,等待訊息傳送。
頂層會實作 FIDL 訊息,並使用底層 (非同步)。處理用戶端和伺服器:
- 用戶端:發出要求 (給伺服器) 的當事人。
- 伺服器:代表用戶端處理要求的當事人。
討論訊息本身時,「傳送者」和「接收者」這兩個詞彙很有意義,因為基礎通訊機制不關心我們指派給當事人的角色,只關心其中一方傳送訊息,另一方接收訊息。
討論雙方扮演的角色時,使用「用戶端」和「伺服器」這兩個詞彙很合理。具體來說,用戶端可以同時是傳送者和接收者,伺服器也是如此。
就用戶端 / 伺服器互動而言,這表示有幾種模型:
- 封鎖呼叫 - 用戶端傳送至伺服器,等待回覆
- 傳送後即忘 - 用戶端傳送至伺服器,不預期會收到回覆
- 回呼或非同步呼叫:用戶端傳送至伺服器,但不封鎖;回覆會在稍後以非同步方式傳送
- 事件:伺服器傳送至用戶端,但用戶端未要求資料
第一個是同步,其餘為非同步。我們將依序討論這些主題。
用戶端傳送至伺服器,等待回覆
這個模型是大多數程式設計語言提供的傳統「封鎖呼叫」或「函式呼叫」,但呼叫是透過管道完成,因此可能會因傳輸層錯誤而失敗。
從用戶端的角度來看,這項呼叫會遭到封鎖,而伺服器會執行一些處理作業。

以下是逐步說明:
- 用戶端會發出呼叫 (可選擇是否包含資料) 並封鎖。
- 伺服器會接收用戶端的呼叫 (和選用資料),並執行一定量的處理作業。
- 伺服器會視情況回覆用戶端 (可選擇是否附上資料)。
- 伺服器的回覆會導致用戶端解除封鎖。
透過非同步訊息傳遞機制實作這個同步訊息傳遞模型非常簡單。請注意,在通訊協定的底層,用戶端到伺服器和伺服器到用戶端的訊息傳輸都是非同步。同步作業會在用戶端進行,方法是讓用戶端封鎖,直到伺服器訊息送達為止。
基本上,在此模型中,用戶端和伺服器已達成協議:
- 資料流是由用戶端啟動,
- 客戶最多只能有一則未結訊息,
- 伺服器只應在收到用戶端訊息後,才傳送訊息給用戶端
- 用戶端應等待伺服器的回應,再繼續操作。
如果用戶端需要先取得目前要求的相關回覆,才能繼續作業,通常會使用這個封鎖模型。
舉例來說,用戶端可能會向伺服器要求資料,但必須等到資料送達,才能執行任何其他有用的處理作業。
或者,用戶端可能需要依特定順序執行步驟,因此必須確保每個步驟都已完成,才能啟動下一個步驟。如果發生錯誤,用戶端可能需要根據作業進度執行修正動作,這也是同步處理每個步驟完成情況的另一個原因。
用戶端傳送至伺服器,但沒有回覆
這個模型也稱為「啟動即棄用」。在其中,用戶端會將訊息傳送至伺服器,然後繼續執行作業。與封鎖模型不同,用戶端不會封鎖,也不會預期收到回應。
如果用戶端不需要 (或無法) 將要求同步處理至處理程序,就會使用這個模型。

經典範例是記錄系統。用戶端會將記錄資訊傳送至記錄伺服器 (上圖中的圓圈「1」和「2」),但沒有封鎖的理由。伺服器端可能發生許多錯誤:
- 伺服器忙碌中,目前無法處理寫入要求,
- 媒體已滿,伺服器無法寫入資料,
- 伺服器發生故障
- 以此類推。
不過,用戶端無法解決這些問題,因此封鎖只會造成更多問題。
用戶端傳送至伺服器,但未封鎖
這個模型與下一個模型 (「伺服器傳送至用戶端,但用戶端未要求資料」) 類似。
在目前的模型中,用戶端會將訊息傳送至伺服器,但不會封鎖。不過,用戶端會預期伺服器傳回某種回應,但重點是這類回應不會與要求同步。
這項功能可讓用戶端 / 伺服器互動具有極大的彈性。
同步模型會強制用戶端等待伺服器回覆,但目前的模型可讓用戶端在伺服器處理要求時執行其他作業:

這張圖與上方的類似圖表略有不同,在圓圈「1」之後,用戶端仍在執行。用戶端會選擇何時放棄 CPU,這與訊息不同步。
這裡實際上包含兩個子案例,一個是用戶端只收到一則回應,另一個是用戶端可以收到多則回應。(用戶端未收到任何回應的模式是「啟動後便不須控制」模式,我們稍早討論過這個模式)。
單一請求,單一回應
單一回應案例最接近同步模型:用戶端會傳送訊息,伺服器最終會回覆。舉例來說,如果您知道用戶端在等待伺服器回覆時可以執行有用的工作,就可以使用這個模型,而不是多執行緒。
單一請求,多個回應
多重回覆案例可用於「訂閱」模式。用戶端訊息會「啟動」伺服器,例如要求在發生任何事件時傳送通知。
然後,用戶端會繼續執行其業務。
過一段時間後,伺服器發現用戶端感興趣的條件已發生,因此會傳送訊息給用戶端。從用戶端/伺服器角度來看,這則訊息是「回覆」,用戶端會非同步接收這則訊息。

當發生其他感興趣的事件時,伺服器沒有理由無法傳送另一則訊息;這是模型的「多重回應」版本。請注意,第二個 (和後續) 回應會在用戶端未傳送任何其他訊息的情況下傳送。
請注意,用戶端「不需要」等待伺服器傳送訊息。在上圖中,我們在圓圈「3」之前顯示處於封鎖狀態的用戶端,但用戶端也可能正在執行。
伺服器傳送給用戶端,但用戶端未要求資料
這個模型也稱為「事件」模型。

在其中,用戶端準備接收來自伺服器的訊息,但不知道何時會收到訊息,因為訊息不僅與用戶端非同步,而且 (從用戶端 / 伺服器角度來看) 也是「未經要求」,因為用戶端並未明確要求訊息 (如上述先前模型所示)。
用戶端會指定一個函式 (「事件處理函式」),在收到伺服器傳來的訊息時呼叫,但除此之外,用戶端會繼續執行其他作業。
伺服器會視情況 (上圖中的圓圈「1」和「2」) 將訊息非同步傳送至用戶端,並由用戶端指定的函式處理。
請注意,傳送訊息時,用戶端可能已在執行 (如圓圈「1」所示),也可能無事可做,正在等待傳送訊息 (如圓圈「2」所示)。
用戶端不一定要等待訊息。
非同步訊息傳遞複雜度
將非同步訊息分為上述 (有些隨意) 類別,是為了顯示一般使用模式,但並非詳盡列舉。
在最一般的情況下,非同步訊息傳送會導致零或多個用戶端訊息與零或多個伺服器回覆鬆散地建立關聯。正是這種「鬆散的關聯性」,讓設計程序變得複雜。
FIDL 中的 IPC 模型
我們已瞭解 IPC 模型,以及這些模型如何與 FIDL 的非同步訊息傳遞互動,現在來看看如何定義這些模型。
我們會在通訊協定定義檔案中加入其他模型 (fire and forget,以及非同步呼叫或事件):
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後方加上「-> ()」會將意義從宣告 fire-and-forget 樣式方法,變更為宣告沒有任何傳回引數的函式呼叫樣式方法。
第 13 行是 ReceiveString 方法。這有點不同,因為第一個部分沒有方法名稱,而是放在 -> 運算子後面。這會告知 FIDL 編譯器,這是「非同步呼叫」或「事件」模型宣告。
FIDL 繫結
FIDL 工具鍊會接收 FIDL 通訊協定和型別定義 (如上例所示),並以各目標語言生成可「說」出這些通訊協定的程式碼。產生的程式碼稱為 FIDL 繫結,可依語言提供不同版本:
- 原生繫結:專為高度敏感的情境設計,例如裝置驅動程式和高輸送量伺服器,可善用就地存取權、避免記憶體配置,但開發人員可能需要對通訊協定的限制有更多瞭解。
- 慣用繫結:從連線格式複製資料到更容易使用的資料型別 (例如堆積支援的字串或向量),設計上更貼近開發人員需求,但相對來說效率較低。
視語言而定,繫結提供多種不同的通訊協定方法叫用方式:
- 傳送/接收:直接讀取或寫入訊息至管道,沒有內建等待迴圈 (C)
- 以回呼為基礎:收到的訊息會以非同步方式,在事件迴圈 (C++、Dart) 中做為回呼分派
- 以埠為準:收到的訊息會傳送至埠或未來 (Rust)
- 同步呼叫:等待回覆並傳回 (Go、C++ 單元測試)
繫結會提供下列部分或全部主體作業:
- 編碼:將原生資料結構就地轉換為連線格式 (並進行驗證)
- 解碼:將線路格式資料轉換為原生資料結構 (並進行驗證)
- 複製/移至慣用形式:將原生資料結構的內容複製到慣用資料結構,並移動控制代碼
- 複製/移至原生表單:將慣用資料結構的內容複製到原生資料結構,並移動控點
- 複製:複製原生或慣用資料結構 (不含僅限移動的型別)
- 呼叫:叫用通訊協定方法
用戶端實作
無論目標語言為何,fidlc FIDL 編譯器都會產生具有下列基本結構的用戶端程式碼。
第一部分包含管理和背景處理,以及:
- 提供連線至伺服器的某種方式
- 啟動非同步 (「背景」) 訊息處理迴圈
- 非同步呼叫樣式和事件樣式方法 (如有) 會繫結至訊息迴圈
第二部分包含傳統函式呼叫或「啟動後即忘」樣式方法的實作項目,視目標語言而定。一般來說,這包括:
- 建立可呼叫的 API 和宣告
- 為每個 API 產生程式碼,將呼叫中的資料封送至適合傳輸至伺服器的 FIDL 格式緩衝區
- 產生程式碼,將資料傳輸至伺服器
- 如果是函式呼叫樣式的呼叫,則會產生程式碼來:
- 等待伺服器回應
- 從 FIDL 格式的緩衝區取消封送處理資料,以及
- 透過 API 函式傳回資料。
顯然,確切步驟可能因語言實作差異而異,但這就是基本流程。
伺服器導入
fidlc FIDL 編譯器也會為指定目標語言產生伺服器程式碼。與用戶端程式碼一樣,無論目標語言為何,這段程式碼都有通用結構。代碼:
- 建立用戶端可連線的物件,
- 啟動主要處理迴圈,該迴圈會:
- 等待訊息
- 呼叫實作函式來處理訊息
- 如果指定,會向用戶端發出非同步回呼,以傳回輸出內容
在接下來的章節中,我們將詳細說明各語言的用戶端和伺服器程式碼實作方式。
為什麼要使用 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 管道有效傳輸由資料 (位元組) 和功能 (控制代碼) 組成的訊息。
- 專為有效使用 Zircon 基本元素而設計。雖然 FIDL 也用於其他平台 (例如透過 ffx),但其設計以 Fuchsia 為優先。
- 提供便利的 API,可建立、傳送、接收及使用訊息。
- 執行充分的驗證,以維護通訊協定不變量 (但不要超過此範圍)。
睡眠效率
- 與使用手動捲動資料結構一樣有效率 (速度和記憶體)。
- 線路格式使用未壓縮的原生資料型別,並採用小端序和正確對齊方式,支援訊息內容的就地存取。
- 當訊息大小為靜態已知或有界限時,產生或使用訊息不需要動態記憶體配置。
- 使用僅限移動的語意明確處理擁有權。
- 資料結構封裝順序是標準、明確,且具有最少的填補。
- 避免回溯修補指標。
- 避免耗費資源的驗證。
- 避免可能溢位的計算。
- 利用通訊協定要求管道,進行非同步作業。
- 結構大小固定,可變大小的資料會儲存在行外。
- 結構不會自我描述,FIDL 檔案會說明結構的內容。
- 結構沒有版本控管,但通訊協定可透過新方法擴充,以利演進。
人體工學
- 由 Fuchsia 團隊維護的程式設計語言繫結:
- C、新版 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 語言參考資料或貢獻文件。