一般建議
本節介紹在 Fuchsia 介面定義語言中定義通訊協定的技術、最佳做法和一般建議。
另請參閱 FIDL 樣式指南。
通訊協定,而非物件
FIDL 是用於定義處理序間通訊協定的語言。雖然語法類似於物件導向介面的定義,但設計考量更接近網路通訊協定,而非物件系統。舉例來說,如要設計高品質的通訊協定,您必須考量頻寬、延遲時間和流量控制。此外,您也應考量到,通訊協定不只是邏輯上的作業分組:通訊協定也會對要求強制執行 FIFO 排序,而將通訊協定拆分為兩個較小的通訊協定,意味著在兩個不同通訊協定上提出的要求可以相互重新排序。
著重於類型
設計 FIDL 通訊協定時,建議先設計通訊協定會使用的資料結構。舉例來說,與網路相關的 FIDL 通訊協定可能包含各種 IP 位址的資料結構,而與圖形相關的 FIDL 通訊協定可能包含各種幾何概念的資料結構。您應該能查看類型名稱,並對通訊協定操控的概念,以及這些概念的結構方式有一些直覺。
語言中立
FIDL 後端支援多種語言,請避免針對任何特定譯文語言過度特化 FIDL 定義。隨著時間推移,您的 FIDL 通訊協定可能會用於許多不同的語言,甚至可能包括目前不支援的語言。FIDL 是系統的黏著劑,可讓 Fuchsia 支援多種語言和執行階段。如果過度專精於自己偏好的語言,就會削弱這項核心價值主張。
Ordinals
通訊協定包含多種方法。系統會自動為每個方法指派專屬的 64 位元 ID,稱為序數。伺服器會使用序數值,判斷應調度哪個通訊協定方法。
編譯器會對程式庫、通訊協定和方法名稱進行雜湊處理,藉此判斷序數值。在極少數情況下,相同通訊協定中的序數可能會發生衝突。如果發生這種情況,您可以使用 Selector 屬性,變更編譯器用於雜湊的方法名稱。以下範例將使用方法名稱「C」而非方法名稱「B」來計算雜湊:
protocol A {
@selector("C")
B(struct {
s string;
b bool;
});
};
如果開發人員想變更方法名稱,也可以使用選取器,維持與傳輸格式的回溯相容性。
診斷
有時,您可能需要公開實用資訊,以便對程式進行偵錯或診斷。這類資料可能包含統計資料和指標 (例如錯誤數量、呼叫、大小等)、對開發有用的資訊、元件的健康狀態等。
您可能會想在測試通訊協定或正式環境通訊協定的偵錯方法中公開這項資訊。不過,Fuchsia 提供獨立機制來公開這類資訊:Inspect,應納入考量,以便做出最佳決策,決定如何公開這類資料。如果需要公開程式的診斷資訊,且該資訊可用於測試偵錯、開發工具,或透過當機報告或指標在現場擷取,則應使用 Inspect,而非 FIDL 方法/通訊協定,前提是沒有其他程式使用該資訊做出執行階段決策。
如果其他程式會根據診斷資訊做出執行階段決策,就應使用 FIDL。Inspect 絕不能用於程式之間的通訊,這是一個盡力而為的系統,不得用於在正式環境中執行時做出決策或改變行為。
決定使用 Inspect 或 FIDL 的經驗法則如下:
其他程式是否在正式環境中使用這項資料?
- 是:使用 FIDL。
當機報告或指標會使用這些資料嗎?
- 是:使用「檢查」功能。
資料是否用於測試或開發人員工具?這項功能是否會用於實際工作環境?
- 是:使用 FIDL。
- 否:擇一使用。
程式庫結構
將 FIDL 宣告分組至 FIDL 程式庫有兩個特定目標:
- 協助 FIDL 開發人員 (使用 FIDL 程式庫的人員) 瀏覽 API 介面。
- 提供結構,在 FIDL 程式庫中以階層方式定義 FIDL 宣告的範圍。
請仔細考量如何將類型和通訊協定定義劃分為程式庫。如何將這些定義分解成程式庫,對這些定義的使用者有很大的影響,因為 FIDL 程式庫是類型和通訊協定的依附元件和發布單位。
FIDL 編譯器要求程式庫之間的依附元件圖表必須是有向無環圖 (DAG),也就是說,您無法跨程式庫邊界建立循環相依。不過,您可以在程式庫中建立 (部分) 循環相依關係。
如要決定是否將程式庫分解成較小的程式庫,請考量下列問題:
程式庫的使用者是否會細分為不同的角色,而這些角色會想要使用程式庫中的部分功能或宣告?如果是,請考慮將程式庫拆分為多個程式庫,分別指定各個角色。
程式庫是否對應到具有一般理解結構的業界概念?如果是,請考慮將程式庫結構調整為符合業界標準。舉例來說,藍牙會分為
fuchsia.bluetooth.le和fuchsia.bluetooth.gatt,以符合業界對這些概念的一般理解。同樣地,fuchsia.net.http對應至業界標準 HTTP 網路通訊協定。是否有許多其他程式庫依附於該程式庫?如果是,請檢查這些傳入依附元件是否真的需要依附於整個程式庫,或者是否有「核心」定義集可以從程式庫中排除,以接收大部分的傳入依附元件。
理想情況下,我們會為 Fuchsia 產生一個 FIDL 程式庫結構,整體而言是全域最佳。不過,康威定律指出:「設計系統 [...] 的組織,其設計成果必然會複製這些組織的溝通結構。」我們應該花適量的時間對抗康威定律。
存取控管機制以通訊協定為單位
決定要在哪個程式庫定義通訊協定時,請勿考量存取控管。一般來說,存取控管是以通訊協定精細程度表示。定義通訊協定的程式庫與存取控管無關,無法用於判斷是否可以存取。
舉例來說,程序可能會存取 fuchsia.logger.LogSink,或程序會獲得 fuchsia.media.StreamSource 通訊協定的用戶端。不過,FIDL 並非設計用來表示對 fuchsia.logger 程式庫的存取權,也無法用於防止存取 fuchsia.ldsvc 程式庫。
fuchsia 命名空間
在平台來源樹狀結構 (即 fuchsia.googlesource.com) 中定義的 FIDL 程式庫必須位於 fuchsia 頂層命名空間 (例如 fuchsia.ui),除非符合下列任一條件:
- 程式庫定義 FIDL 語言本身或其一致性測試套件的部分,在這種情況下,頂層命名空間必須是
fidl。 - 程式庫僅用於內部測試,不包含在 SDK 或正式版中,在這種情況下,頂層命名空間必須是
test。
強烈建議頂層命名空間 fuchsia 中的 FIDL 程式庫不超過四個元件,即 fuchsia.<api-namespace>、fuchsia.<api-namespace>.<name> 或 fuchsia.<api-namespace>.<name>.<subname>。請選擇適當的 api-namespace,必要時可向 API Council 成員尋求協助。
舉例來說,在平台來源樹狀結構中定義的 FIDL 程式庫,如果目的是向應用程式公開硬體功能,就必須位於 fuchsia.hardware 命名空間。舉例來說,用於公開乙太網路裝置的通訊協定可能命名為 fuchsia.hardware.ethernet.Device。建構在這些 FIDL 通訊協定之上的高階功能不屬於 fuchsia.hardware 命名空間。舉例來說,網路通訊協定較適合放在 fuchsia.net,而非 fuchsia.hardware。
避免過度巢狀結構
建議使用包含三個元件的程式庫名稱 (例如 fuchsia.hardware.network),並避免使用包含四個以上元件的程式庫名稱 (例如 fuchsia.apps.foo.bar.baz)。如果您使用四個以上的元件,應有特定原因。
程式庫依附元件
建議您從名稱較具體的程式庫,引入依附元件至名稱較不具體的程式庫。舉例來說,fuchsia.foo.bar 可能依附於 fuchsia.foo,但 fuchsia.foo 不應依附於 fuchsia.foo.bar。這種模式更適合擴充,因為隨著時間推移,我們可以新增更多名稱更具體的程式庫,但名稱較不具體的程式庫數量有限。
匯入程式庫的瀏覽權限
為進一步實現第二個目標,也就是將 FIDL 宣告分組至 FIDL 程式庫,我們預計會發展 FIDL,提供瀏覽權限規則,改變元素是否可由匯入程式庫 (「子程式庫」) 使用,例如 public 或 private 修飾符。
internal 程式庫元件名稱應視為特殊名稱,表示瀏覽權限規則的本機限制。舉例來說,fuchsia.net.dhcp.internal.foo 程式庫中的公開宣告可能只會顯示在其父項 fuchsia.net.dhcp 或其同層級 (例如 fuchsia.net.dhcp.internal.bar)。
使用多字詞程式庫元件
雖然程式庫名稱可以包含多個單字組成的元件 (例如 fuchsia.modular.storymodel),但應盡量避免使用。如果程式庫名稱違反巢狀結構規則,或在階層式思考程式庫位置時,兩個字詞都不應優先於另一個字詞,則程式庫作者可以選擇將多個字詞結合在一起。
版本字串
如果程式庫需要版本編號,應在結尾加上單一版本號碼,例如 fuchsia.io2 或 fuchsia.something.something4.。版本號碼不應有多個部分,例如 fuchsia.io2.1 不可接受,應改為 fuchsia.io3。任何程式庫元件都可以建立版本,但強烈建議不要有多個版本元件,例如 fuchsia.hardware.cpu2.ctrl,但不要 fuchsia.hardware.cpu2.ctrl4。
版本編號應僅表示程式庫的較新版本,而非實質上不同的網域。相反地,fuchsia.input 程式庫用於較低層級的裝置處理,而 fuchsia.ui.input{2,3} 則用於與 Scenic 互動的輸入,以及用於轉譯 UI 的軟體元件。如果只著重於版本管理,則使用 fuchsia.ui.scenic.input 和 fuchsia.ui.scenic.input2 會更清楚,以便與 fuchsia.input 提供的其他網域區分。
類型
如「一般建議」所述,您應特別注意通訊協定定義中使用的類型。
保持一致
相同概念請使用一致的類型。舉例來說,在整個程式庫中,請一律使用 uint32 或 int32 來表示特定概念。如果您為某個概念建立 struct,請一律使用該結構體來代表該概念。
在理想情況下,類型也應在程式庫邊界之間一致使用。請查看相關程式庫是否有類似概念,並與這些程式庫保持一致。如果程式庫之間共用許多概念,請考慮將這些概念的類型定義納入通用程式庫。舉例來說,fuchsia.mem 和 fuchsia.math 分別包含許多常用類型,用於表示記憶體和數學概念。
偏好語意類型
建立 struct 來命名常用概念,即使這些概念可以使用原始類型表示。舉例來說,IPv4 位址是網路程式庫中的重要概念,即使資料可以使用原始類型表示,也應使用 struct 命名:
type Ipv4Address = struct {
octets array<uint8, 4>;
};
在效能關鍵的譯文語言中,struct 會以內嵌方式表示,這可降低使用 struct 命名重要概念的成本。
zx.Time 有明確定義的時間基準
zx.Time 類型會單調測量裝置專屬時基的奈秒數。zx.Time 的用途可以假設這個時間基準,不需要明確說明。
謹慎使用匿名類型
匿名類型非常實用,可更流暢地描述 API。特別是,如果您事先知道具名類型的子元素與該具名類型有內在關聯,且在包含具名容器的內容之外使用時,不會有用或有意義,則匿名類型非常適合。
舉例來說,假設有一個聯集變體,其中匯總了幾個項目。聯集變體極少單獨使用,也就是說,我們事先知道聯集變體只有在特定使用情境下才有意義。因此,建議您使用匿名類型做為聯集變體。
理想情況下,類型應與 API 的重要概念一對一對應,且不應有兩個類型具有相同定義。不過,有時可能無法同時達成這兩項目標,尤其是當類型命名 (引入不同概念1) 的意義不只是做為 API 介面元素時。舉例來說,假設有兩個具名 ID type EntityId = struct { id
uint64; }; 和 type OtherEntityId = struct { id uint64; };,分別代表不同概念,但除了名稱以外,兩者的類型定義相同。
使用匿名類型會建立多個類型,彼此不相容。因此,如果使用多個匿名類型來表示同一個概念,會導致 API 過於複雜,且無法在大多數目標語言中進行一般處理。
因此,使用匿名類型時,請務必避免多個匿名類型代表相同概念。舉例來說,如果 API 的演進可能導致多個匿名類型代表相同概念,則不得使用匿名類型。
建議使用虛擬記憶體物件 (VMO)
虛擬記憶體物件 (VMO) 是核心物件,代表虛擬記憶體的連續區域和邏輯大小。請使用這種類型在 FIDL 訊息中傳輸記憶體,並使用 ZX_PROP_VMO_CONTENT_SIZE 屬性追蹤物件中包含的資料量。
指定向量和字串的界限
所有 vector 和 string 宣告都應指定長度限制。宣告通常可分為兩類:
- 資料本身存在限制。例如,包含檔案系統名稱元件的字串長度不得超過
fuchsia.io.MAX_NAME_LENGTH。 - 除了「盡可能」之外,沒有其他限制。在這些情況下,您應使用內建常數
MAX。
每當您使用 MAX 時,請考量訊息接收者是否真的想處理任意長度的序列,或極長序列是否代表濫用行為。
請注意,透過 zx::channel 傳送時,所有宣告都會隱含受限於訊息長度上限。如果確實有任意長度序列的用途,單純使用 MAX 可能無法解決這些用途,因為嘗試提供極長序列的用戶端可能會達到訊息長度上限。
如要處理任意大型序列的用途,請考慮使用下列其中一種分頁模式,將序列拆分為多個訊息,或考慮將資料移出訊息本身,例如移至 VMO。
FIDL 食譜:大小限制
FIDL 向量和字串可能帶有大小限制,指定該類型可包含的成員數量上限。如果是向量,則是指向量中儲存的元素數量;如果是字串,則是指字串包含的位元組數。
強烈建議您使用大小限制,因為這會為原本無限制的大型類型設定上限。
這份文件。鍵/值儲存庫有一項實用作業,稱為依序疊代:也就是說,在指定鍵後,系統會依序傳回該鍵之後出現的元素清單 (通常會分頁)。
推理
在 FIDL 中,最好使用疊代器來完成這項操作,這通常會實作為獨立通訊協定,以便進行疊代。使用獨立通訊協定 (也就是獨立管道) 有許多好處,包括將疊代提取要求與透過主要通訊協定執行的其他作業分開。
通訊協定 P 的管道連線用戶端和伺服器端,可分別以 FIDL 資料類型 client_end:P 和 server_end:P 表示。這些類型統稱為「通訊協定端」,代表將 FIDL 用戶端連線至對應伺服器的另一種 (非 @discoverable) 方式:透過現有的 FIDL 連線!
通訊協定端是通用 FIDL 概念的特定執行個體:資源類型。資源類型旨在包含 FIDL 控制代碼,因此必須對類型的使用方式施加額外限制。類型必須一律不重複,因為基礎資源是由其他能力管理工具 (通常是 Zircon 核心) 協調。如果沒有管理員介入,就無法透過簡單的記憶體內複製來複製這類資源。為避免重複,FIDL 中的所有資源類型一律只能移動。
最後,Iterator 通訊協定本身的 Get() 方法會對傳回的酬載使用大小限制。這會限制單次提取可能傳輸的資料量,進而控制資源使用量。此外,這項機制也會建立自然的分頁邊界:伺服器不必一次傾倒所有結果,只要一次準備少量批次即可。
實作
FIDL、CML 和領域介面定義如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, { config: "fuchsia.examples.keyvaluestore.additerator.WriteItems", key: "write_items", type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, availability: "transitional", default: [], }, // A key to iterate from, after all items in `write_items` have been written. { config: "fuchsia.examples.keyvaluestore.additerator.IterateFrom", key: "iterate_from", type: "string", max_size: 64, availability: "transitional", default: "", }, ], }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
Realm
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
接著,您可以使用任何支援的語言編寫用戶端和伺服器實作:
荒漠油廠
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use futures::join; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>(); // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use regex::Regex; use std::sync::LazyLock; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use fuchsia_sync::Mutex; use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::ops::Bound::*; use std::sync::Arc; static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile") }); /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++ (自然)
用戶端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.伺服器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.C++ (Wire)
用戶端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.伺服器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
字串編碼、字串內容和長度限制
FIDL string 會以 UTF-8 編碼,這是一種可變寬度編碼,每個萬國碼 (Unicode) 碼點使用 1、2、3 或 4 個位元組。
繫結會對字串強制執行有效的 UTF-8,因此字串不適用於任意二進位資料。請參閱「我應該使用 string 還是 vector<byte>?」。
長度繫結宣告的目的是為 FIDL 訊息的總位元組大小提供容易計算的上限,因此 string 繫結會指定欄位中的最大位元組數。為保險起見,一般建議預算設為 (4 bytes · code points in
string)。(如果您確定文字只使用單一位元組 ASCII 範圍內的碼點,例如電話號碼或信用卡號碼,則每個碼點 1 個位元組就足夠)。
字串中有多少個碼點?這個問題可能很難回答,尤其是使用者產生的字串內容,因為 Unicode 碼點與使用者可能認為的「字元」之間不一定有一對一的對應關係。
例如,字串
á
會算做一個使用者認知的「字元」,但實際上是由兩個碼點組成:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
在 Unicode 術語中,這種使用者感知的「字元」稱為「字元叢集」。
單一字素叢集可由任意數量的碼點組成。請參考以下較長的範例:
á🇨🇦b👮🏽♀️
如果系統和字型支援,您應該會在上方看到四個字元集群:
1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone
這四個字素叢集會編碼為十個碼點:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
3. REGIONAL INDICATOR SYMBOL LETTER C (U+1F1E8)
4. REGIONAL INDICATOR SYMBOL LETTER A (U+1F1E6)
5. LATIN SMALL LETTER B (U+0062)
6. POLICE OFFICER (U+1F46E)
7. EMOJI MODIFIER FITZPATRICK TYPE-4 (U+1F3FD)
8. ZERO WIDTH JOINER (U+200D)
9. FEMALE SIGN (U+2640)
10. VARIATION SELECTOR-16 (U+FE0F)
在 UTF-8 中,這個字串會佔用 28 個位元組。
從這個範例可以清楚看出,如果應用程式的使用者介面顯示文字輸入方塊,允許 N 個任意字元集群 (使用者認為的「字元」),且您打算透過 FIDL 傳輸這些使用者輸入的字串,則必須在 FIDL string 欄位中預算 4·N 的倍數。
這個倍數應該是多少?這取決於您的資料。如果處理的是相當受限的用途 (例如人名、郵寄地址、信用卡號碼),則可假設每個字元叢集有 1 到 2 個碼點。如果您要建構的即時通訊用戶端會大量使用表情符號,每個字元叢集 4 到 5 個碼點可能較為安全。無論如何,輸入驗證 UI 都應顯示清楚的視覺回饋,讓使用者在空間不足時不會感到意外。
整數類型
請根據您的用途選取適當的整數類型,並以一致的方式使用。如果您的值最適合視為資料位元組,請使用 byte。如果負值沒有意義,請使用不帶正負號的類型。一般來說,如果不確定,請使用 32 位元值表示少量,64 位元值表示大量。
如果可能出現更多狀態,請避免使用布林值
新增布林值欄位時,如果該欄位日後可能擴充,用來代表其他狀態,請考慮改用列舉。舉例來說,布林值 is_gif 欄位可能更適合
type FileType = strict enum {
UNKNOWN = 0;
GIF = 1;
};
如有需要,可使用 JPEG = 2 擴充列舉。
如何表示錯誤?
請根據您的用途選取適當的錯誤類型,並以一致的方式回報錯誤。
使用 error 語法清楚記錄及傳達可能錯誤的傳回值,並利用量身打造的目標語言繫結。
(不建議使用含有錯誤列舉模式的選用值)。
使用錯誤語法
方法可採用選用的 error <type> 指定碼,指出方法會傳回值,或發生錯誤並產生 <type>。範例如下:
// Only erroneous statuses are listed
type MyErrorCode = flexible enum {
MISSING_FOO = 1; // avoid using 0
NO_BAR = 2;
};
protocol Frobinator {
Frobinate() -> (struct {
value SuccessValue;
}) error MyErrorCode;
};
使用這個模式時,您可以使用 int32、uint32 或其列舉來代表傳回的錯誤類型。在大多數情況下,傳回列舉是首選方法。
建議在通訊協定的所有方法中,使用單一錯誤類型。
建議使用特定領域的列舉
定義及控管網域時,請使用專門建構的列舉錯誤類型。舉例來說,如果通訊協定是專為特定用途而建構,且傳達錯誤語意是唯一的設計限制,請定義列舉。如列舉一節所述,建議您避免使用 0 值。
在某些情況下,一開始可能適合使用空白的彈性列舉:
type MyEmptyErrorCode = flexible enum {};
protocol Frobinator2 {
Frobinate() -> (struct {
value SuccessValue;
}) error MyEmptyErrorCode;
BadFrobinate() -> (struct {
value SuccessValue;
}) error flexible enum {}; // avoid anonymous enum
};
彈性列舉具有預設不明成員。因此,空白的彈性列舉是類型化預留位置,可提供可擴充性。使用這個模式時,建議定義獨立類型,供通訊協定 (或程式庫) 內的多個方法重複使用,而非使用匿名列舉。使用匿名列舉會建立多個彼此不相容的類型,導致 API 過於複雜,且無法在大多數目標語言中一般處理錯誤。
如果您遵循定義明確的規格 (例如 HTTP 錯誤代碼),請使用網域專屬的列舉錯誤類型,列舉應以符合人體工學的方式,表示規格規定的原始值。
特別是,請使用 zx.Status 類型處理與核心物件或 IO 相關的錯誤。舉例來說,fuchsia.process 使用 zx.Status,因為程式庫主要與操控核心物件有關。另一個例子是,fuchsia.io 廣泛使用 zx.Status,因為該程式庫與 IO 有關。
搭配錯誤列舉使用選用值
過去,定義具有兩個傳回值 (一個選用值和一個錯誤代碼) 的方法,可稍微提升效能。例如:
type MyStatusCode = strict enum {
OK = 0; // The success value should be 0,
MISSING_FOO = 1; // with erroneous status next.
NO_BAR = 2;
};
protocol Frobinator3 {
Frobinate() -> (struct {
value box<SuccessValue>;
err MyStatusCode;
});
};
不過,這個模式現已淘汰,改用錯誤語法:在信封中內嵌小值已淘汰現有的效能優勢,且現在普遍支援聯集。
避免在錯誤中顯示訊息和說明
在某些不常見的情況下,如果可能發生的錯誤條件範圍很大,且描述性錯誤訊息可能對用戶端有用,則通訊協定除了 status 或列舉值外,可能還會包含錯誤的字串說明。不過,加入字串會造成一些問題。舉例來說,用戶端可能會嘗試剖析字串來瞭解發生什麼事,這表示字串的確切格式會成為通訊協定的一部分,當字串本地化時,這會特別有問題。
安全性注意事項:同樣地,向用戶端回報堆疊追蹤或例外狀況訊息,可能會不小心洩漏權限資訊。
本地化字串和錯誤訊息
如果您建構的服務是 UI 的後端,請使用結構化、有類型的訊息,並將轉譯作業交給 UI 層。
如果所有訊息都很簡單且不含參數,請使用 enum 進行錯誤報告和一般 UI 字串。如要顯示更詳細的訊息,並包含名稱、數字和位置等參數,請使用 table 或 xunion,並將參數做為字串或數字欄位傳遞。
您可能會想在服務中產生訊息 (英文),並以字串形式提供給 UI,UI 只會接收字串,並彈出通知或錯誤對話方塊。
不過,這種較簡單的做法有一些嚴重缺點:
- 您的服務是否知道 UI 使用的語言代碼 (語言和地區)?您必須在每個要求中傳遞語言代碼 (請參閱範例),或追蹤每個已連結用戶端的狀態,才能以正確的語言提供訊息。
- 服務的開發環境是否能妥善支援本地化?如果您使用 C++ 編寫,可以輕鬆存取 ICU 程式庫和
MessageFormat,但如果您使用 Rust,目前程式庫支援會受到更多限制。 - 是否有任何錯誤訊息需要包含 UI 已知但服務未知的參數?
- 您的服務是否只提供單一 UI 實作?服務是否知道 UI 有多少空間可顯示訊息?
- 錯誤是否僅以文字顯示?您可能也需要錯誤專屬的警告圖示、音效或文字轉語音提示。
- 使用者是否可以在 UI 執行時變更顯示語言代碼?如果發生這種情況,預先本地化的字串可能難以更新至新的語言代碼,尤其是當這些字串是某些非冪等運算的結果時。
除非您建構的是與單一 UI 實作緊密結合的高度專業化服務,否則不應在 FIDL 服務中公開使用者可見的 UI 字串。
是否應定義 struct 來封裝方法參數 (或回應)?
定義方法時,您必須決定要個別傳遞參數,還是將參數封裝在 struct 中。如要做出最佳選擇,必須權衡多項因素。請思考下列問題,協助您做出決策:
是否有明確的封裝界線?如果一組參數在這種方法之外具有某種凝聚力,因此適合以單位的形式傳遞,您可能需要將這些參數封裝在結構體中。(希望您在開始設計通訊協定時,已按照上述「一般建議」並提早著重於類型,因此已找出這些內聚群組。)
除了呼叫的方法之外,struct 是否還有其他用途?如果沒有,請考慮分別傳遞參數。
您是否在多個方法中重複使用相同的參數群組?如果是,請考慮將這些參數分組為一或多個結構。您也可以考慮重複是否表示這些參數具有凝聚力,因為它們代表通訊協定中的一些重要概念。
是否有大量參數是選用的,或通常會指定預設值?如果是,請考慮使用 struct 減少呼叫端的樣板。
是否有某些參數群組一律同時為空值或非空值?如果是,請考慮將這些參數分組為可為空值的結構,在通訊協定本身強制執行該不變量。舉例來說,上述定義的
FrobinateResult結構體包含的值,在error不是MyError.OK時一律為空值。
我應該使用 string 還是 vector<byte>?
在 FIDL 中,string 資料必須是有效的 UTF-8,這表示字串可以代表 Unicode 碼點序列,但無法代表任意二進位資料。相較之下,vector<byte> 或 array<byte, N> 可代表任意二進位資料,不代表 Unicode。
使用 string 處理文字資料:
請使用
string代表套件名稱,因為套件名稱必須是有效的 UTF-8 字串 (不含特定排除字元)。請使用
string代表套件中的檔案名稱,因為套件中的檔案名稱必須是有效的 UTF-8 字串 (不含特定排除字元)。請使用
string代表媒體轉碼器名稱,因為媒體轉碼器名稱是從有效的 UTF-8 字串固定詞彙中選取。使用
string代表 HTTP 方法,因為 HTTP 方法是由一組固定字元組成,一律為有效的 UTF-8。
使用 vector<byte> 或 array<byte, N> 儲存小型非文字資料:
請針對 HTTP 標頭欄位使用
vector<byte>,因為 HTTP 標頭欄位未指定編碼,因此不一定能以 UTF-8 表示。MAC 位址是二進位資料,因此請使用
array<byte, 6>。請使用
array<byte, 16>儲存 UUID,因為 UUID (幾乎) 是任意二進位資料。
使用 blob 的共用記憶體原始物件:
- 如果需要完整緩衝資料,請使用
zx.Handle:VMO處理圖片和 (大型) protobuf。 - 請對音訊和影片串流使用
zx.Handle:SOCKET,因為資料可能會隨著時間陸續傳入,或是在完全寫入或可用之前處理資料。
我應該使用 vector 還是 array?
vector 是可變長度的序列,在傳輸格式中以離線方式表示。array 是固定長度的序列,在傳輸格式中以內嵌方式表示。
使用 vector 處理可變長度資料:
- 請在記錄訊息中使用
vector做為標記,因為記錄訊息可以有零到五個標記。
使用 array 處理固定長度的資料:
- MAC 位址一律為六個位元組,因此請使用
array。
我應該使用 struct 還是 table?
結構體和資料表都代表具有多個具名欄位的物件。差別在於 struct 在傳輸格式中具有固定版面配置,也就是說,如果修改 struct,就會破壞二進位相容性。相較之下,表格在線路格式中具有彈性的版面配置,這表示欄位「可以」隨著時間新增至表格,而不會破壞二進位相容性。
請將 struct 用於效能關鍵的通訊協定元素,或日後不太可能變更的通訊協定元素。舉例來說,您可以使用 struct 來代表 MAC 位址,因為 MAC 位址的結構在未來不太可能變更。
針對日後可能變更的通訊協定元素使用表格。舉例來說,您可以使用資料表來表示相機裝置的中繼資料資訊,因為中繼資料中的欄位可能會隨著時間而演變。
如何表示常數?
常數有三種表示方式,取決於常數的類型:
- 使用
const表示特殊值,例如 PI 或 MAX_NAME_LEN。 - 如果值是集合的元素,請使用
enum,例如媒體播放器的重複模式:OFF、SINGLE_TRACK 或 ALL_TRACKS。 - 使用
bits建立一組旗標的常數,例如介面的功能:WLAN、SYNTH 和 LOOPBACK。
const
如果您想以符號表示某個值,而不是每次都輸入該值,請使用 const。最經典的例子是 PI,通常會編碼為 const,因為這樣就不必在每次使用這個值時輸入 3.141592653589,非常方便。
或者,當值可能變更,但需要一律使用時,您可以使用 const。例如,指定欄位可提供的字元數上限 (例如 MAX_NAME_LEN)。使用 const 可集中定義該數字,因此不會在整個程式碼中產生不同的值。
選擇 const 的另一個原因是,您可以用它來限制訊息,並在稍後的程式碼中使用。例如:
const MAX_BATCH_SIZE int32 = 128;
protocol Sender {
Emit(struct {
batch vector<uint8>:MAX_BATCH_SIZE;
});
};
接著,您可以在程式碼中使用常數 MAX_BATCH_SIZE 來組合批次。
enum
如果列舉值組合受 Fuchsia 專案限制和控管,請使用列舉。舉例來說,Fuchsia 專案會定義指標事件輸入模型,因此會控制 PointerEventPhase 列舉的值。
在某些情況下,即使 Fuchsia 專案本身不控管列舉值組合,您也應該使用列舉,前提是我們合理預期想要註冊新值的使用者會向 Fuchsia 來源樹狀結構提交修補程式,以註冊其值。舉例來說,Fuchsia 圖形驅動程式必須能解讀紋理格式,這表示即使紋理格式組合是由圖形硬體供應商控制,開發人員仍可透過這些驅動程式新增紋理格式。反例:請勿使用列舉來代表 HTTP 方法,因為我們無法合理預期使用新 HTTP 方法的使用者會向平台來源樹狀結構提交修補程式。
對於先驗無界限集合,如果您預期要動態擴充集合,string 可能是更合適的選擇。舉例來說,請使用 string 表示媒體轉碼器名稱,因為中介者或許能以新穎的媒體轉碼器名稱執行合理操作。
如果列舉值組合是由外部實體控制,請使用整數 (適當大小) 或 string。舉例來說,請使用整數 (特定大小) 代表 USB HID ID,因為 USB HID ID 組合是由產業聯盟控制。同樣地,請使用 string 表示 MIME 類型,因為 MIME 類型是由 IANA 註冊資料庫控管 (至少在理論上是如此)。
建議開發人員盡可能避免使用 0 做為列舉值。由於許多譯文語言都使用 0 做為整數的預設值,因此很難區分 0 值是刻意設定,還是因為是預設值而設定。舉例來說,fuchsia.module.StoryState 定義了三個值:RUNNING 的值為 1、STOPPING 的值為 2,以及 STOPPED 的值為 3。
在以下兩種情況下,適合使用 0 值:
- 列舉具有自然預設、初始或不明狀態;
- 這個列舉定義了含有錯誤列舉模式的可選值中使用的錯誤代碼。
位元
如果通訊協定有位元欄位,請使用 bits 值表示其值 (詳情請參閱 RFC-0025:「位元旗標」)。
例如:
type InfoFeaturesHex = strict bits : uint32 {
WLAN = 0x00000001; // If present, this device represents WLAN hardware
SYNTH = 0x00000002; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0x00000004; // If present, this device receives all messages it sends
};
這表示 InfoFeatures 位元欄位是由不帶正負號的 32 位元整數支援,然後繼續定義使用的三個位元。
您也可以使用 0b 標記法,以二進位 (而非十六進位) 表示值:
type InfoFeaturesBits = strict bits : uint32 {
WLAN = 0b00000001; // If present, this device represents WLAN hardware
SYNTH = 0b00000010; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0b00000100; // If present, this device receives all messages it sends
};
這與前一個範例相同。
我該使用 resource 嗎?
FIDL 編譯器會強制要求,任何已包含 resource 的類型都必須標示為此類型。
如果 flexible 類型不含資源,但日後可能會有,則應預先新增 resource 修飾符,以免日後轉換困難。這種情況很少見:經驗顯示,大多數訊息不包含資源,且在通訊協定中傳遞資源需要謹慎和預先規劃。
我應該在類型上使用 strict 還是 flexible?
將類型標示為 flexible,即可處理目前 FIDL 結構定義未知的資料,但使用者必須考量這種可能性。具體來說,使用者無法對彈性位元、列舉或聯集執行詳盡的模式比對。strict 類型通常較容易使用和推論,但不允許日後擴充。
選擇 strict 或 flexible 時,請思考下列問題:
- 我日後是否可能需要為這個類型新增成員?如果是,請使用
flexible。 - 如果日後我確實需要為這個類型新增成員,那麼最好是引入新類型,而不是為這個類型新增成員嗎?如果是,請使用
strict。
類型可以從 strict 轉換為 flexible,但過程緩慢,必須等到該類型為 strict 的所有 API 級別都淘汰後,才能將新成員新增至該類型。如要開始這項轉換,請指明自 NEXT 起,類型不再是 strict,而是 flexible:
type Color2 = strict(removed=NEXT) flexible(added=NEXT) enum { // NEVER_REPLACE_NEXT
RED = 1;
};
如果類型允許,一律指定這個修飾符是時尚的做法。Fuchsia 專案會透過 Linter 檢查強制執行這種樣式。
使用 strict 或 flexible 不會對效能造成任何重大影響。
帳號代碼權利
本節說明在 FIDL 中指派控制代碼權限限制的最佳做法。
如要進一步瞭解繫結中的權限使用方式,請參閱 FIDL 繫結規格或 RFC-0028。
如需 Zircon 權限定義,請參閱核心權限。FIDL 會使用 rights.fidl 解決權限限制。
一律指定帳號代碼的權利
所有控制代碼都應指定權限,明確指出預期用途。這項規定會強制預先決定要傳遞哪些權利,而不是根據觀察到的行為。明確的權限也有助於稽核 API 介面。
僅授予收件者必要的最低權限
決定要提供哪些權限時,請盡量減少權限,也就是只提供達到所需功能所需的最低權限。舉例來說,如果已知只需要 zx.Rights.READ 和 zx.Rights.WRITE,則應只指定這兩項權利。
請勿根據推測的需求新增權限。如果日後需要新增權限,可以從來源開始新增,並沿著呼叫路徑新增到每個位置,直到最終使用點。
謹慎使用 zx.Rights.SAME_RIGHTS
zx.Rights.SAME_RIGHTS 非常適合用於轉送未知權限控制代碼的通訊協定,但在大多數情況下,應改用一組特定權限。部分原因是 zx.Rights.SAME_RIGHTS 會指示繫結略過權限檢查,因此會停用控制代碼權限可能提供的安全防護。此外,zx.Rights.SAME_RIGHTS 會使權限集變成動態,這表示程序可能會收到比實際需要更少或更多的權限。
值得注意的是,zx.Rights.SAME_RIGHTS 與為類型設定的預設權限不同,例如 zx.DEFAULT_CHANNEL_RIGHTS。前者會略過權限檢查,後者則需要指定物件類型的所有一般權限。
優良設計模式
本節說明許多 FIDL 通訊協定中重複出現的幾種良好設計模式。
我應該在方法和事件中使用 strict 還是 flexible?
將方法或事件標示為 flexible,可讓您在不同元件可能以不同版本建構時,更輕鬆地處理移除方法或事件,例如某些元件認為方法或事件存在,而其他元件則不認為。由於一般建議為不斷演進的通訊協定保留彈性,因此建議您為方法和事件選擇 flexible,除非有充分理由選擇 strict。
將方法設為 flexible 不會對單向方法或事件造成負擔。如果是雙向方法,選擇 flexible 會在訊息中加入少量額外負擔 (16 位元組或更少),並可能增加訊息解碼的時間。整體而言,讓雙向方法具有彈性的成本應該夠低,幾乎所有用途都不需要考慮。
只有在方法和事件對通訊協定的正確行為至關重要,以致於接收端缺少該方法或事件會造成嚴重後果,導致兩端之間的所有通訊都應中止且連線關閉時,才應將方法和事件設為 strict。
在設計前饋資料流時,這項功能特別實用。請考慮使用這個記錄器通訊協定,其支援安全處理含有個人識別資訊 (PII) 記錄的模式。它使用前饋模式新增記錄,讓用戶端可以依序啟動多項作業,而不必等待封包往返時間,最後只要清除待處理作業即可。
open protocol Logger {
flexible AddRecord(struct {
message string;
});
strict EnablePIIMode();
flexible DisablePIIMode();
flexible Flush() -> ();
};
除了 EnablePIIMode 之外,這裡的所有方法都是 flexible。請考慮如果伺服器無法辨識任何方法,會發生什麼情況:
AddRecord:伺服器只是無法將資料新增至記錄輸出。傳送應用程式會正常運作,但記錄檔記錄的實用性會降低。雖然不方便,但很安全。EnablePIIMode:伺服器無法啟用 PII 模式,表示可能無法採取安全預防措施,並洩漏 PII。這是嚴重問題,因此如果伺服器無法辨識這個方法,建議關閉管道。DisablePIIMode:伺服器會對不需要記錄 PII 的訊息採取不必要的安全防護措施。這可能會造成讀取記錄檔的人不便,但對系統來說很安全。Flush:伺服器無法按照要求清除記錄,這可能會造成不便,但仍安全。
另一種設計這個通訊協定的方法是讓 EnablePIIMode 成為雙向方法 (flexible EnablePIIMode() -> ();),這樣用戶端就能知道伺服器是否沒有該方法。請注意,這會為用戶端提供額外的彈性;透過這項設定,用戶端可以選擇是否要關閉連線,或選擇不記錄 PII,來回應伺服器無法辨識 EnablePIIMode 的情況,而使用 strict 時,通訊協定一律會自動關閉。不過,這會中斷前饋流程。
請注意,嚴格程度取決於寄件者。假設您在第 1 版中有某個方法 strict A();,然後在第 2 版中將其變更為 flexible A();,接著在第 3 版中將其刪除。如果以第 1 版建構的用戶端嘗試在以第 3 版建構的伺服器上呼叫 A(),系統會將該方法視為嚴格,因為第 1 版的用戶端認為該方法是嚴格的,而第 3 版的伺服器會接受用戶端的說法,因為它完全不認識該方法。
建議一律指定這個修飾符。Fuchsia 專案會透過 Linter 檢查強制執行此樣式。
我應該使用 open、ajar 還是 closed?
將通訊協定標示為 open,可更輕鬆地處理移除方法或事件,因為不同元件可能是在不同版本建構,因此每個元件對存在的方法和事件都有不同的檢視。由於一般來說,最好能靈活因應不斷演進的通訊協定,因此建議您選擇 open 做為通訊協定,除非有理由選擇更封閉的通訊協定。
決定使用 ajar 或 closed 時,應考量通訊協定演進的預期限制。使用 closed 或 ajar 不會阻礙通訊協定演進,但會要求更長的推出期,期間方法和事件存在但不會使用,以確保所有用戶端和伺服器都同意存在哪些方法。flexible 的彈性適用於新增和移除方法及事件,具體取決於用戶端或伺服器是否先更新。
ajar 適用於使用前饋資料流的通訊協定,但其演進僅限於單向方法。舉例來說,這可能適用於代表交易的拆分通訊協定,其中唯一的雙向方法是必須嚴格執行的修訂作業,而交易的其他作業可能會演變。
ajar protocol BazTransaction {
flexible Add(resource struct {
bars vector<client_end:Bar>;
});
strict Commit() -> (struct {
args Args;
});
};
open protocol Foo3 {
flexible StartBazTransaction(resource struct {
transaction server_end:BazTransaction;
});
};
closed 適用於重要通訊協定,其中任何不明方法都是嚴重問題,應關閉管道,而非繼續處於可能不佳的狀態。如果通訊協定
不太可能變更,或至少任何變更都可能涉及
極長的推出週期,那麼使用 方法也是合理的,因為在推出週期中,
變更 strict 方法所涉及的額外成本已在預期之中。
通訊協定要求管道
通訊協定要求管道是最實用且廣泛採用的設計模式之一。用戶端不會傳回支援通訊協定的管道,而是傳送管道,並要求伺服器將通訊協定的實作繫結至該管道:
// GOOD:
protocol GoodFoo {
GetBar(resource struct {
name string;
bar server_end:Bar;
});
};
// BAD:
protocol BadFoo {
GetBar(struct {
name string;
}) -> (resource struct {
bar client_end:Bar;
});
};
這個模式很實用,因為用戶端不需要等待來回,就能開始使用 Bar 通訊協定。用戶端可以立即將 Bar 的訊息排入佇列。這些訊息會由核心緩衝處理,並在 Bar 的實作繫結至通訊協定要求後,最終處理。相反地,如果伺服器傳回 Bar 通訊協定的執行個體,用戶端就必須等待整個往返過程,才能將 Bar 的訊息排入佇列。
如果要求可能失敗,請考慮擴充這個模式,並回覆說明作業是否成功:
protocol CodecProvider {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec;
}) -> (struct {
succeed bool;
});
};
為了處理失敗情況,用戶端會等待回覆,並在要求失敗時採取其他動作。另一種方法是讓通訊協定在開始時,由伺服器傳送事件:
protocol Codec2 {
-> OnReady();
};
protocol CodecProvider2 {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec2;
});
};
如要處理失敗情況,用戶端會等待 OnReady 事件,並在 Codec2 管道於事件到達前關閉時採取其他動作。
不過,如果要求可能成功,則任何一種成功信號都可能有害,因為信號可讓用戶端區分不同的失敗模式,而這些模式通常應以相同方式處理。舉例來說,如果服務在建立連線後立即失敗,用戶端應將其視為一開始就無法連線的服務。在這兩種情況下,服務都無法使用,用戶端應產生錯誤或尋找其他方式來完成工作。
流量控制
FIDL 訊息會由核心緩衝處理。如果某個端點產生的訊息多於另一個端點消耗的訊息,訊息就會在核心中累積,占用記憶體,使系統更難復原。相反地,設計良好的通訊協定應限制訊息的產生速度,以符合這些訊息的消耗速度,這種屬性稱為「流量控制」。
流量控制是廣泛而複雜的主題,有許多有效的設計模式。本節將討論一些較常見的流量控制模式,但並未涵蓋所有模式。模式會依偏好程度遞減排序。如果其中一種模式適合特定用途,就應該使用。如果沒有,通訊協定可以自由使用下列未列出的替代流量控制機制。
偏好提取而非推送
如果沒有仔細設計,伺服器將資料推送至用戶端的通訊協定,通常會導致流量控制不佳。如要進一步控管流量,其中一種做法是讓用戶端從伺服器提取一個或一系列訊息。提取模型內建流量控制功能,因為用戶端會自然限制伺服器產生資料的速率,並避免因伺服器推送的訊息而超出負荷。
使用懸置 GET 延遲回應
如要實作提取式通訊協定,最簡單的方法是使用懸置 GET 模式,在伺服器上「停放回呼」:
protocol FooProvider {
WatchFoo(struct {
args Args;
}) -> (resource struct {
foo client_end:Foo;
});
};
在這個模式中,用戶端會傳送 WatchFoo 訊息,但伺服器要等到有新資訊要傳送給用戶端時,才會回覆。用戶端會使用 foo,並立即傳送另一個掛起 GET。用戶端和伺服器會針對每個資料項目執行一個工作單位,也就是說,兩者都不會領先對方。
如果傳輸的資料項目集大小有限,且伺服器端狀態簡單,則懸置 GET 模式的效果很好,但如果用戶端和伺服器需要同步處理工作,則效果不佳。
舉例來說,伺服器可能會使用每個用戶端的「dirty」位元,為某些可變動狀態 foo 實作懸置 GET 模式。它會將這個位元初始化為 true,在每個 WatchFoo 回應中清除,並在每次 foo 變更時設定。只有在設定 dirty bit 時,伺服器才會回應 WatchFoo 訊息。
使用確認訊息限制推送
在採用推送機制的通訊協定中,提供流量控制的一種方法是「確認模式」,其中呼叫端會提供確認回應,供呼叫端用於流量控制。舉例來說,假設有以下一般事件監聽器通訊協定:
protocol Listener {
OnBar(struct {
args Args;
}) -> ();
};
監聽器應在收到 OnBar 訊息後,立即傳送空白的回應訊息。回應不會向呼叫端傳送任何資料。相反地,回應可讓呼叫端觀察被呼叫端使用訊息的速率。呼叫端應限制產生訊息的速率,以符合被呼叫端使用訊息的速率。例如,呼叫端可能會安排只有一則 (或固定數量) 訊息在傳輸中 (即等待確認)。
FIDL 方案:確認模式
確認模式是一種簡單的流量控制方法,適用於原本為單向呼叫的方法。這個方法不會是單向呼叫,而是會變成雙向呼叫,並傳回缺席回應,也就是俗稱的 ack。確認訊息存在的唯一理由,是通知傳送者訊息已收到,傳送者可以根據這項資訊決定如何繼續。
這項確認作業的代價是管道上的額外通訊。如果用戶端在繼續下一個呼叫前等待確認,這種模式也可能導致效能降低。
這份文件。來回傳送不限流量的單向呼叫,可產生簡單的設計,但可能會有潛在問題:如果伺服器處理更新的速度比用戶端傳送更新的速度慢得多,該怎麼辦?舉例來說,用戶端可能會從某個文字檔載入由數千行組成的繪圖,並嘗試依序傳送所有繪圖。如何對用戶端施加反壓,防止伺服器因這波更新而超載?
使用確認模式,並將單向呼叫 AddLine(...); 轉換為雙向 AddLine(...) -> ();,即可向用戶端提供意見回饋。這樣一來,用戶端就能視情況限制輸出。在這個範例中,我們會讓用戶端等待確認,再傳送下一則等待中的訊息,不過更複雜的設計可能會樂觀地傳送訊息,並只在收到非同步確認的頻率低於預期時進行節流。
首先,我們需要定義介面定義和測試控管工具。FIDL、CML 和領域介面定義會設定鷹架,可供任意實作使用:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.addlinemetered; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add a line to the canvas. /// /// This method can be considered an improvement over the one-way case from a flow control /// perspective, as it is now much more difficult for a well-behaved client to "get ahead" of /// the server and overwhelm. This is because the client now waits for each request to be acked /// by the server before proceeding. This change represents a trade-off: we get much greater /// synchronization of message flow between the client and the server, at the cost of worse /// performance at the limit due to the extra wait imposed by each ack. flexible AddLine(struct { line Line; }) -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.addlinemetered.Instance" }, // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "WAIT". The former // calls `AddLine(...)`, while the latter pauses execution until the next `->OnDrawn(...)` // event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; WaitEnum}` // here. { config: "fuchsia.examples.canvas.addlinemetered.Script", key: "script", type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, availability: "transitional", default: [], }, ], }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.addlinemetered.Instance" }, ], expose: [ { protocol: "examples.canvas.addlinemetered.Instance", from: "self", }, ], }
Realm
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.addlinemetered.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
接著,您可以使用任何支援的語言編寫用戶端和伺服器實作:
荒漠油廠
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error, format_err}; use config::Config; use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); for action in config.script.into_iter() { // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } continue; } // If the action is not a "WAIT", we need to draw a line instead. Parse the string input, // making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let line = [from, to]; // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. println!("AddLine request sent: {:?}", line); // By awaiting on the reply, we prevent the client from sending another request before the // server is ready to handle, thereby syncing the flow rate between the two parties over // this method. instance.add_line(&line).await.context("Error sending request")?; println!("AddLine response received"); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_sync::Mutex; use futures::future::join; use futures::prelude::*; use std::sync::Arc; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, bounding_box: BoundingBox, } impl CanvasState { /// Handler for the `AddLine` method. fn add_line(&mut self, line: [Point; 2]) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut self.bounding_box; for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. self.changed = true } } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock(); if !state.changed { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change tracker. state.changed = false } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLine { line, responder } => { println!("AddLine request received: {:?}", line); state_ref.lock().add_line(line); // Because this is now a two-way method, we must use the generated `responder` // to send an in this case empty reply back to the client. This is the mechanic // which syncs the flow rate between the client and server on this method, // thereby preventing the client from "flooding" the server with unacknowledged // work. responder.send().context("Error responding")?; println!("AddLine response sent"); } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This await does not complete, and thus the function does not return, unless the server errors // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first // wait for the updater task to realize the connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++ (自然)
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override { auto top_left = event.top_left(); auto bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::Point(x, y); } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.is_ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.error_value().FormatDescription(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override { auto points = request.line(); FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request.line()) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override { auto top_left = event->top_left; auto bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine( const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override { auto points = request->line; FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request->line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. weak->state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
使用事件推送有界限資料
在 FIDL 中,伺服器可以向用戶端傳送來路不明的訊息,稱為「事件」。使用事件的通訊協定需要特別注意流量控制,因為事件機制本身不提供任何流量控制。
如果管道生命週期內最多只會傳送一個事件例項,就很適合使用事件。在這種模式中,通訊協定不需要對事件進行任何流量控制:
protocol DeathWish {
-> OnFatalError(struct {
error_code zx.Result;
});
};
另一個適合使用事件的情況是,用戶端要求伺服器產生事件,且伺服器產生的事件總數有限。這個模式是懸置 get 模式的更複雜版本,伺服器可以回應「get」要求有限次數 (而非僅一次):
protocol NetworkScanner {
ScanForNetworks();
-> OnNetworkDiscovered(struct {
network string;
});
-> OnScanFinished();
};
使用確認訊息限制事件
如果事先不知道事件數量的上限,請考慮讓用戶端傳送訊息來確認事件。這種模式是確認模式的較不自然版本,其中用戶端和伺服器的角色會互換。與其他模式一樣,伺服器應限制事件產生速度,以配合用戶端使用事件的速度:
protocol View1 {
-> OnInputEvent(struct {
event InputEvent;
});
NotifyInputEventHandled();
};
與一般確認模式相比,這種模式的優點在於用戶端可以更輕鬆地透過單一訊息確認多個事件,因為確認與確認的事件是分開的。這種模式可減少確認訊息量,提高批次處理效率,並適合用於依序處理多種事件類型:
protocol View2 {
-> OnInputEvent(struct {
event InputEvent;
seq uint64;
});
-> OnFocusChangedEvent(struct {
event FocusChangedEvent;
seq uint64;
});
NotifyEventsHandled(struct {
last_seq uint64;
});
};
與使用確認訊息的節流推送不同,這個模式不會以 FIDL 語法表示要求與回應之間的關係,因此容易遭到濫用。只有在用戶端正確實作通知訊息傳送功能時,流量控制才會生效。
FIDL 方案:節流事件模式
事件是從伺服器發起的 FIDL 呼叫。由於這些呼叫沒有內建的用戶端回應,因此不受流量控制:伺服器可能會將大量這類呼叫排入佇列,並傳送至用戶端。節流事件模式是解決這個問題的方法之一。這種模式涉及新增用戶端呼叫的 FIDL 方法,做為一或多個事件同步處理的確認點。
伺服器應避免傳送更多受限事件 (此處的確切語意取決於實作通訊協定),直到收到用戶端的下一個確認呼叫。同樣地,如果伺服器在用戶端確認前,傳送的節流事件超過允許數量,用戶端應關閉連線。這些限制並未內建於 FIDL 執行階段,需要用戶端/伺服器實作者手動實作,才能確保行為正確。
這份文件。如要提升 Instance 通訊協定的效能,其中一個方法是允許批次處理線條:每次有新線條要加入畫布時,我們不傳送單一 AddLine(...);、等待回覆,然後再對下一條線重複這個過程,而是可以將多條線批次處理,一次叫用新的 AddLines(...); 呼叫。用戶端現在可以決定如何以最佳方式區隔要繪製的大量線條。
如果只是單純實作,伺服器和用戶端可能會完全不同步:用戶端可能會向伺服器傳送大量無限制的 AddLines(...); 呼叫,而伺服器也可能會向用戶端傳送超出其處理能力的 -> OnDrawn(...); 事件。這兩個問題的解決方法是新增簡單的 Ready() -> (); 方法,以便進行同步處理。每當用戶端準備好接收下一個繪圖更新時,就會呼叫這個方法,而伺服器的回應會指出用戶端可以繼續提出更多要求。
現在,我們在兩個方向都有一些流量控制。該通訊協定現在會實作前饋模式,允許在某些同步「commit」呼叫觸發伺服器上的實際工作之前,進行許多不受控制的呼叫。這可防止用戶端向伺服器傳送過多工作。同樣地,伺服器也不再允許傳送未繫結的 -> OnDrawn(...); 事件:每個事件都必須遵循用戶端的信號 (Ready() -> (); 呼叫),表示已準備好執行更多工作。這稱為節流事件模式。
具體實作必須手動套用部分規則:如果用戶端收到未透過 Ready() -> (); 方法要求的 -> OnDrawn(...); 事件,則必須關閉連線。
FIDL、CML 和領域介面定義如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.clientrequesteddraw; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a /// time. flexible AddLines(struct { lines vector<Line>; }); /// Rather than the server randomly performing draws, or trying to guess when to do so, the /// client must explicitly ask for them. This creates a bit of extra chatter with the additional /// method invocation, but allows much greater client-side control of when the canvas is "ready" /// for a view update, thereby eliminating unnecessary draws. /// /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to /// immediately close. flexible Ready() -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; Ready}` here. { config: "fuchsia.examples.canvas.clientrequesteddraw.Script", key: "script", type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, availability: "transitional", default: [], }, ], }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, ], expose: [ { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "self", }, ], }
Realm
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
接著,您可以使用任何支援的語言編寫用戶端和伺服器實作:
荒漠油廠
用戶端
// Copyright 2025 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error, format_err}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); let mut batched_lines = Vec::<[Point; 2]>::new(); for action in config.script.into_iter() { // If the next action in the script is to "PUSH", send a batch of lines to the server. if action == "PUSH" { instance.add_lines(&batched_lines).context("Could not send lines")?; println!("AddLines request sent"); batched_lines.clear(); continue; } // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. println!("Ready request sent"); instance.ready().await.context("Could not send ready call")?; println!("Ready success"); continue; } // Add a line to the next batch. Parse the string input, making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let line: [Point; 2] = [from, to]; // Batch a line for drawing to the canvas using the two points provided. println!("AddLines batching line: {:?}", line); batched_lines.push(line); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error, anyhow}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_sync::Mutex; use futures::future::join; use futures::prelude::*; use std::sync::Arc; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, // Tracks whether or not the client has declared itself ready to receive more updated. ready: bool, bounding_box: BoundingBox, } /// Handler for the `AddLines` method. fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut state.bounding_box; for line in lines { println!("AddLines printing line: {:?}", line); for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. state.changed = true } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, ready: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second, but only if the client has declared that it // is ready to receive one. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock(); if !state.changed || !state.ready { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change and ready trackers. state.ready = false; state.changed = false; } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLines { lines, .. } => { println!("AddLines request received"); add_lines(&mut state_ref.lock(), lines); } InstanceRequest::Ready { responder, .. } => { println!("Ready request received"); // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` // event; if two "consecutive" `Ready() -> ();` calls are received, this // interaction has entered an invalid state, and should be aborted immediately. let mut state = state_ref.lock(); if state.ready == true { return Err(anyhow!("Invalid back-to-back `Ready` requests received")); } state.ready = true; responder.send().context("Error responding")?; } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This line will only be reached if the server errors out. The stream will await indefinitely, // thereby creating a long-lived server. Here, we first wait for the updater task to realize the // connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++ (自然)
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override { ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left(); ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::Point(x, y); } using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fit::result<fidl::Error> result = client->AddLines(batched_lines); if (!result.is_ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) { // Check if the FIDL call succeeded or not. if (result.is_ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request.lines()) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override { ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left; ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y}; } using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines)); if (!status.ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>& result) { // Check if the FIDL call succeeded or not. if (result.ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request->lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; weak->state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
前饋資料流程
部分通訊協定具有前饋資料流,可讓資料主要朝一個方向流動 (通常是從用戶端流向伺服器),避免來回延遲。只有在必要時,通訊協定才會同步處理兩個端點。前饋資料流也會增加總處理量,因為執行特定工作所需的總情境切換次數較少。
前饋資料流的關鍵在於,用戶端不必等待先前方法呼叫的結果,即可傳送後續訊息。舉例來說,通訊協定要求管道化可讓用戶端不必等待伺服器回覆通訊協定,即可使用通訊協定。同樣地,用戶端指派的 ID (請見下方) 可讓用戶端不必等待伺服器指派 ID,即可取得伺服器保留的狀態。
一般來說,前饋通訊協定會涉及用戶端提交一連串單向方法呼叫,而不等待伺服器的回應。提交這些訊息後,用戶端會呼叫有回覆的方法 (例如 Commit 或 Flush),明確與伺服器同步。回覆可能是空白訊息,也可能包含提交的序列是否成功等資訊。在更複雜的通訊協定中,單向訊息會以指令物件的聯集表示,而非個別方法呼叫;請參閱下方的指令聯集模式。
使用前饋資料流程的通訊協定,適合搭配樂觀錯誤處理策略。請勿讓伺服器以狀態值回覆每個方法,因為這會導致用戶端在每則訊息之間等待來回,而應只在方法可能因用戶端無法控制的原因而失敗時,才加入狀態回覆。如果用戶端傳送的訊息無效,且用戶端應已知曉 (例如參照無效的用戶端指派 ID),請關閉連線來表示發生錯誤。如果用戶端傳送的訊息無效,但用戶端無法得知,請提供表示成功或失敗的回應 (需要用戶端同步處理),或記住錯誤並忽略後續的依附要求,直到用戶端同步處理並以某種方式從錯誤中復原。
範例:
protocol Canvas {
Flush() -> (struct {
code zx.Result;
});
Clear();
UploadImage(struct {
image_id uint32;
image Image;
});
PaintImage(struct {
image_id uint32;
x float32;
y float32;
});
DiscardImage(struct {
image_id uint32;
});
PaintSmileyFace(struct {
x float32;
y float32;
});
PaintMoustache(struct {
x float32;
y float32;
});
};
FIDL 食譜:大小限制
FIDL 向量和字串可能帶有大小限制,指定該類型可包含的成員數量上限。如果是向量,則是指向量中儲存的元素數量;如果是字串,則是指字串包含的位元組數。
強烈建議您使用大小限制,因為這會為原本無限制的大型類型設定上限。
這份文件。鍵/值儲存庫有一項實用作業,稱為依序疊代:也就是說,在指定鍵後,系統會依序傳回該鍵之後出現的元素清單 (通常會分頁)。
推理
在 FIDL 中,最好使用疊代器來完成這項操作,這通常會實作為獨立通訊協定,以便進行疊代。使用獨立通訊協定 (也就是獨立管道) 有許多好處,包括將疊代提取要求與透過主要通訊協定執行的其他作業分開。
通訊協定 P 的管道連線用戶端和伺服器端,可分別以 FIDL 資料類型 client_end:P 和 server_end:P 表示。這些類型統稱為「通訊協定端」,代表將 FIDL 用戶端連線至對應伺服器的另一種 (非 @discoverable) 方式:透過現有的 FIDL 連線!
通訊協定端是通用 FIDL 概念的特定執行個體:資源類型。資源類型旨在包含 FIDL 控制代碼,因此必須對類型的使用方式施加額外限制。類型必須一律不重複,因為基礎資源是由其他能力管理工具 (通常是 Zircon 核心) 協調。如果沒有管理員介入,就無法透過簡單的記憶體內複製來複製這類資源。為避免重複,FIDL 中的所有資源類型一律只能移動。
最後,Iterator 通訊協定本身的 Get() 方法會對傳回的酬載使用大小限制。這會限制單次提取可能傳輸的資料量,進而控制資源使用量。此外,這項機制也會建立自然的分頁邊界:伺服器不必一次傾倒所有結果,只要一次準備少量批次即可。
實作
FIDL、CML 和領域介面定義如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, { config: "fuchsia.examples.keyvaluestore.additerator.WriteItems", key: "write_items", type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, availability: "transitional", default: [], }, // A key to iterate from, after all items in `write_items` have been written. { config: "fuchsia.examples.keyvaluestore.additerator.IterateFrom", key: "iterate_from", type: "string", max_size: 64, availability: "transitional", default: "", }, ], }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
Realm
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
接著,您可以使用任何支援的語言編寫用戶端和伺服器實作:
荒漠油廠
用戶端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use config::Config; use fuchsia_component::client::connect_to_protocol; use std::{thread, time}; use fidl::endpoints::create_proxy; use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}; use futures::join; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>(); // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
伺服器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fuchsia_component::server::ServiceFs; use futures::prelude::*; use regex::Regex; use std::sync::LazyLock; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use fuchsia_sync::Mutex; use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::ops::Bound::*; use std::sync::Arc; static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile") }); /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++ (自然)
用戶端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.伺服器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.C++ (Wire)
用戶端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.伺服器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
隱私保護設計
通訊協定中的用戶端和伺服器通常可以存取不同的機密資料集。如果透過通訊協定不小心洩漏過多資料,可能會導致隱私權或安全性問題。
設計通訊協定時,請特別注意通訊協定中的欄位:
- 包含個人識別資訊,例如姓名、電子郵件地址或付款資料。
- 由使用者提供,因此可能包含個人資訊。例如裝置名稱和註解欄位。
- 做為專屬 ID,可跨供應商、使用者、裝置或重設作業進行關聯。例如序號、MAC 位址、IP 位址和全域帳戶 ID。
這類欄位會經過徹底審查,且包含這類欄位的通訊協定可能無法使用。請確認通訊協定不含不必要的資訊。
如果 API 的某個用途需要個人或可連結資料,而其他用途不需要,請考慮使用兩種不同的通訊協定,以便分別控管較敏感用途的存取權。
以下舉兩個假設例子,說明 API 設計選擇造成的隱私權違規:
範例 1:周邊裝置控制 API 中的序號
假設周邊裝置控制 API 包含 USB 周邊裝置的序號。序號不含個人資料,但它是非常穩定的 ID,很容易建立關聯。在這個 API 中加入序號會引發許多隱私權疑慮:
- 任何可存取 API 的用戶端,都能使用同一個 Fuchsia 裝置建立不同帳戶之間的關聯。
- 任何可存取 API 的用戶端,都能建立帳戶中不同角色的關聯。
- 不同的軟體供應商可能會串通,瞭解是否由相同使用者或在相同裝置上使用。
- 如果周邊裝置在裝置之間移動,任何可存取 API 的用戶端都可以建立周邊裝置共用的裝置和使用者組合。
- 如果周邊裝置售出,有權存取 API 的用戶端可能會將周邊裝置的新舊擁有者建立關聯。
- 部分製造商會在序號中編碼資訊。這可能會讓有權存取 API 的用戶端推斷出使用者購買周邊裝置的地點或時間。
在這個範例中,序號的用途是讓用戶端偵測同一個 USB 周邊裝置何時重新連線。為滿足這項意圖,您需要穩定的 ID,但不需要全域 ID。不同用戶端不一定要收到相同的 ID,同一個用戶端在不同 Fuchsia 裝置上不一定要收到相同的 ID,而且 ID 在恢復原廠設定後不一定要保持不變。
在這個例子中,建議改為傳送僅保證在單一裝置上對單一用戶端穩定的 ID。這個 ID 可能是周邊裝置序號、Fuchsia 裝置 ID 和連線路徑名稱的雜湊值。
範例 2:裝置設定 API 中的裝置名稱
假設裝置設定 API 包含用於協助設定裝置的手機型號。在多數情況下,手機的型號字串是由原始設備製造商 (OEM) 設定,但部分手機會將使用者提供的裝置名稱回報為型號。這會導致許多模型字串包含使用者的真實姓名或假名。因此,這個 API 可能會連結不同身分或裝置的使用者。即使使用者未提供,罕見或預先發布版模型字串仍可能揭露機密資訊。
在某些情況下,您可以使用模型字串,但限制哪些用戶端可以存取 API。或者,API 可以使用使用者無法控制的欄位,例如製造商字串。另一種替代方案是將型號字串與熱門手機型號的許可清單進行比較,並將不常見的型號字串替換為一般字串,藉此清理型號字串。
用戶端指派的 ID
通訊協定通常會讓用戶端操控伺服器保留的多個狀態。設計物件系統時,這個問題的典型做法是為伺服器保留的每個連貫狀態建立個別物件。不過,在設計通訊協定時,為每個狀態使用不同的物件有幾個缺點。
為每個邏輯物件建立個別通訊協定執行個體會耗用核心資源,因為每個執行個體都需要個別管道物件。每個執行個體都會維護獨立的 FIFO 訊息佇列。如果為每個邏輯物件使用不同的執行個體,傳送至不同物件的訊息可能會重新排序,導致用戶端和伺服器之間的互動順序錯亂。
用戶端指派的 ID 模式可避免這些問題,因為用戶端會將 uint32 或 uint64 ID 指派給伺服器保留的物件。用戶端和伺服器之間交換的所有訊息都會透過單一通訊協定執行個體傳送,為整個互動提供一致的 FIFO 排序。
由用戶端 (而非伺服器) 指派 ID,可實現前饋資料流,因為用戶端可以為物件指派 ID,然後立即對該物件執行操作,不必等待伺服器回覆物件的 ID。在這種模式中,ID 僅在目前連線範圍內有效,且通常會保留零 ID 做為標記。安全性注意事項:用戶端不應使用位址空間中的位址做為 ID,因為這些位址可能會洩漏位址空間的版面配置。
用戶端指派的 ID 模式有一些缺點。舉例來說,用戶端需要管理自己的 ID,因此較難編寫。開發人員通常會想建立用戶端程式庫,為服務提供物件導向的外觀,以隱藏管理 ID 的複雜性,這本身就是一種反模式 (請參閱下方的用戶端程式庫)。
如果您想使用核心的物件能力系統保護該物件的存取權,就表示您應該建立獨立的通訊協定執行個體來代表物件,而不是使用用戶端指派的 ID。舉例來說,如果您希望用戶端能與某個物件互動,但不想讓用戶端與其他物件互動,則建立獨立的通訊協定執行個體,代表您可以使用基礎管道做為控制該物件存取權的能力。
指令聯集
在使用前饋資料流程的通訊協定中,用戶端通常會先傳送多則單向訊息給伺服器,再傳送雙向同步訊息。如果通訊協定涉及特別大量的訊息,傳送訊息的負擔可能會變得明顯。在這種情況下,建議您使用指令聯集模式,將多個指令批次處理成單一訊息。
在這個模式中,用戶端會傳送指令的 vector,而不是為每個指令傳送個別訊息。向量包含所有可能指令的聯集,除了使用方法序號外,伺服器還會使用聯集標記做為指令調度的選取器:
type PokeCmd = struct {
x int32;
y int32;
};
type ProdCmd = struct {
message string:64;
};
type MyCommand = strict union {
1: poke PokeCmd;
2: prod ProdCmd;
};
protocol HighVolumeSink {
Enqueue(struct {
commands vector<MyCommand>;
});
Commit() -> (struct {
result MyStatus;
});
};
用戶端通常會在本機的位址空間中緩衝指令,並批次傳送至伺服器。用戶端應在達到位元組和控制代碼的管道容量限制前,將批次排清至伺服器。
如果通訊協定的訊息量更大,請考慮在資料層的 zx.Handle:VMO 中使用環形緩衝區,並在控制層使用相關聯的 zx.Handle:FIFO。這類通訊協定會對用戶端和伺服器造成較高的實作負擔,但適合用於需要最高效能的情況。舉例來說,區塊裝置通訊協定會使用這種方法來最佳化效能。
分頁
FIDL 訊息通常會透過管道傳送,訊息大小有上限。在許多情況下,訊息大小上限足以傳輸合理數量的資料,但也有傳輸大量 (甚至無限) 資料的使用案例。如要傳輸大量或無限量的資訊,其中一種方法是使用分頁模式。
分頁寫入
如要將寫入伺服器的作業分頁,簡單的做法是讓用戶端以多則訊息傳送資料,然後使用「finalize」方法,讓伺服器處理傳送的資料:
protocol Foo1 {
AddBars(resource struct {
bars vector<client_end:Bar>;
});
UseTheBars() -> (struct {
args Args;
});
};
舉例來說,fuchsia.process.Launcher 會使用這個模式,讓用戶端傳送任意數量的環境變數。
這個模式的更複雜版本會建立代表交易的通訊協定,通常稱為「tear-off 通訊協定」:
protocol BarTransaction {
Add(resource struct {
bars vector<client_end:Bar>;
});
Commit() -> (struct {
args Args;
});
};
protocol Foo2 {
StartBarTransaction(resource struct {
transaction server_end:BarTransaction;
});
};
如果用戶端可能同時執行多項作業,且將寫入作業拆分為個別訊息會失去原子性,則適合採用這種方法。請注意,BarTransaction 不需要 Abort 方法。中止交易的較佳做法是讓用戶端關閉 BarTransaction 通訊協定。
分頁讀取
如要對伺服器讀取作業進行分頁,最簡單的方法是讓伺服器使用事件,對單一要求傳送多個回應:
protocol EventBasedGetter {
GetBars();
-> OnBars(resource struct {
bars vector<client_end:Bar>;
});
-> OnBarsDone();
};
視網域專屬語意而定,這個模式可能也需要第二個事件,在伺服器完成資料傳送時發出信號。這種方法適用於簡單的情況,但有許多擴充問題。舉例來說,該通訊協定缺乏流量控制,如果用戶端不再需要額外資料,就無法停止伺服器 (除非關閉整個通訊協定)。
更穩健的做法是使用 tear-off 通訊協定建立疊代器:
protocol BarIterator {
GetNext() -> (resource struct {
bars vector<client_end:Bar>;
});
};
protocol ChannelBasedGetter {
GetBars(resource struct {
iterator server_end:BarIterator;
});
};
呼叫 GetBars 後,用戶端會使用通訊協定要求管道,立即將第一個 GetNext 呼叫
排入佇列。之後,用戶端會重複呼叫 GetNext,從伺服器讀取其他資料,並限制未完成的 GetNext 訊息數量,以提供流量控制。請注意,疊代器不需要「done」回應,因為伺服器可以回覆空白向量,並在完成時關閉疊代器。
另一種分頁讀取的方法是使用權杖。在這種方法中,伺服器會以不透明權杖的形式,將疊代器狀態儲存在用戶端,而用戶端會在每次部分讀取時,將權杖傳回伺服器:
type Token = struct {
opaque array<uint8, 16>;
};
protocol TokenBasedGetter {
/// If token is null, fetch the first N entries. If token is not null,
/// return the N items starting at token. Returns as many entries as it can
/// in results and populates next_token if more entries are available.
GetEntries(struct {
token box<Token>;
}) -> (struct {
entries vector<Entry>;
next_token box<Token>;
});
};
如果伺服器可以將所有分頁狀態託管給用戶端,因此完全不需要維護分頁狀態,這種模式就特別有吸引力。伺服器應記錄用戶端是否可以保留權杖,並在通訊協定的執行個體中重複使用。安全性注意事項:無論是哪種情況,伺服器都必須驗證用戶端提供的權杖,確保用戶端只能存取自己的分頁結果,不包含其他用戶端的結果。
事件配對關聯
使用用戶端指派的 ID 時,用戶端會使用僅在與伺服器連線時有意義的 ID,識別伺服器保留的物件。不過,某些用途需要跨用戶端關聯物件。舉例來說,在 fuchsia.ui.scenic 中,用戶端主要使用用戶端指派的 ID 與場景圖中的節點互動。不過,從其他程序匯入節點時,必須跨程序邊界建立該節點的參照關聯。
事件配對關聯模式使用前饋資料流,並依賴核心提供必要的安全性,藉此解決這個問題。首先,想要匯出物件的用戶端會建立 zx::eventpair,並將其中一個糾纏事件連同用戶端指派的物件 ID 傳送至伺服器。接著,用戶端會將另一個糾纏事件傳送至另一個用戶端,後者會將事件轉送至伺服器,並附上用戶端為目前共用物件指派的 ID:
protocol Exporter {
ExportThing(resource struct {
client_assigned_id uint32;
export_token zx.Handle:EVENTPAIR;
});
};
protocol Importer {
ImportThing(resource struct {
some_other_client_assigned_id uint32;
import_token zx.Handle:EVENTPAIR;
});
};
為了建立物件之間的關聯,伺服器會使用 ZX_INFO_HANDLE_BASIC 呼叫 zx_object_get_info,並比對糾纏事件物件中的 koid 和 related_koid 屬性。
取消 Eventpair
使用 tear-off 通訊協定交易時,用戶端可以關閉通訊協定的用戶端,取消長時間執行的作業。伺服器應監聽 ZX_CHANNEL_PEER_CLOSED 並取消交易,以免浪費資源。
如果作業沒有專屬管道,也有類似的用途。舉例來說,fuchsia.net.http.Loader 通訊協定具有啟動 HTTP 要求的 Fetch 方法。HTTP 交易完成後,伺服器會以 HTTP 回應回覆要求,這可能需要相當長的時間。用戶端沒有明顯的方法可以取消要求,除非關閉整個 Loader 通訊協定,但這可能會取消許多其他未完成的要求。
eventpair 取消模式可解決這個問題,方法是讓用戶端將 zx::eventpair 中的其中一個糾纏事件做為方法的參數。接著,伺服器會監聽 ZX_EVENTPAIR_PEER_CLOSED,並在該信號生效時取消作業。使用 zx::eventpair 比使用 zx::event 或其他信號更好,因為 zx::eventpair 方法會隱含處理用戶端當機或其他終止情況。當用戶端保留的糾纏事件遭到刪除時,核心會產生 ZX_EVENTPAIR_PEER_CLOSED。
空白通訊協定
有時空白通訊協定也能提供價值。舉例來說,建立物件的方法也可能接收 server_end:FooController 參數。呼叫端會提供這個空白通訊協定的實作:
protocol FooController {};
FooController 不含任何控制所建立物件的方法,但伺服器可以使用通訊協定上的 ZX_CHANNEL_PEER_CLOSED 信號,觸發物件的刪除作業。日後,這個通訊協定可能會擴充,加入控制所建立物件的方法。
控管設定類資料
伺服器通常會公開用戶端可修改的設定。建議使用 table 來表示這類設定。舉例來說,fuchsia.accessibility 程式庫定義了:
type Settings = table {
1: magnification_enabled bool;
2: magnification_zoom_factor float32;
3: screen_reader_enabled bool;
4: color_inversion_enabled bool;
5: color_correction ColorCorrection;
6: color_adjustment_matrix array<float32, 9>;
};
(為方便閱讀,已省略註解)。
您可以透過多種方式,讓用戶端能夠變更這些設定。
部分更新方法會公開採用部分設定值的 Update 方法,且「僅」在部分值中存在時變更欄位。
protocol TheManagerOfSomeSorts {
/// Description how the update modifies the behavior.
///
/// Only fields present in the settings value will be changed.
Update(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
replace 方法會公開採用完整設定值的 Replace 方法,並將設定變更為新提供的值。
protocol TheManagerOfOtherSorts {
/// Description how the override modifies the behavior.
///
/// This replaces the setting.
Replace(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
應避免的事項:
請避免在部分更新或取代方法中使用動詞
Set或Override,因為提供的語意會模稜兩可。避免使用個別方法更新設定欄位,例如
SetMagnificationEnabled。這類個別方法較難維護,且呼叫端很少需要更新單一值。請避免移除含有魔術值的設定,例如
-1。如要移除設定,請移除該設定欄位。
參照聯集變體和資料表欄位
參照類型欄位通常很有用,例如參照資料表的一或多個欄位,或參照特定聯集變體。
假設某個 API 以包含多個欄位的 table 提供中繼資料。如果這類中繼資料可能變得相當龐大,通常可以採用一種機制,讓接收者向傳送者指出要讀取這類中繼資料中的哪些欄位,避免傳送接收者不會考慮的多餘欄位。在這種情況下,如果有一個平行的 bits,其成員與 table 的欄位一對一對應,就能為建構 API 奠定穩固的基礎:
type MetadataFields = flexible bits {
VERSION = 0b1;
COUNT = 0b10;
};
type Metadata = table {
1: version string;
2: count uint32;
// ...
};
protocol MetadataGetter {
Get(struct {
fields MetadataFields;
}) -> (struct {
metadata Metadata;
});
};
現在,請考慮指令聯集。在複雜的情況下,伺服器可能需要描述其支援的指令。在這種情況下,如果有一個平行的 enum,其成員與 union 的變體一對一對應,就能為建構 API 奠定穩固基礎:
type MyCommandVariant = strict enum {
POKE = 1;
PROD = 2;
// ...
};
protocol HighVolumeSinkContinued {
SupportedCommands() -> (struct {
supported_commands vector<MyCommandVariant>;
});
};
請注意,雖然使用 bits 值來代表指令集可能很吸引人,但這會導致後續更難做出選擇。如果您的 API 演變到需要參照特定指令,則使用 enum 會更自然。如果您一開始使用 bits 值,現在就必須在兩個不好的選項中擇一:
導入
enum,這表示現在有兩種參照欄位的方式,且用戶端程式碼可能會有轉換問題 (從一種表示法轉換為另一種表示法);或繼續使用
bits,但限制在任何指定時間只能設定一個位元,現在要對應回設定的特定位元相當繁瑣。
table摘要:
bits的名稱應為table的名稱加上後置字串Fields(複數)。每個成員值應為序數索引處的位元,即1 << (ordinal - 1)。與
union的建議類似,您需要比對bits和table之間的彈性,也就是說,由於 FIDL 目前僅支援彈性資料表,因此bits必須是flexible。
針對 union:
為
enum命名,並以union的名稱加上後置字串Variant(單數) 列出所有變化版本。每個成員值都應是其描述的變體的序數。union和enum的彈性必須一致,也就是說,如果union為strict,則enum也必須為strict。
反面模式
本節說明幾種反模式,也就是通常會帶來負面價值的設計模式。學會辨識這些模式,是避免誤用的第一步。
推送設定:盡可能避免
Fuchsia 平台通常偏好提取語意。元件的語意提供了一個重要範例;系統會假設功能是從元件中提取,因此元件啟動可以延遲,而元件關閉順序則可從能力路徑的有向圖推斷。
使用 FIDL 通訊協定將設定從一個元件推送至另一個元件的設計,因其顯而易見的簡單性而具有吸引力。當元件 A 將政策推送至元件 B,以執行 B 的商業邏輯時,就會發生這種情況。這會導致平台誤解依附元件關係:A 不會自動啟動來支援 B 的函式,A 可能會在 B 完成商業邏輯執行前關閉。這會導致需要使用弱依附元件和虛擬反向依附元件的解決方法,才能產生所需行為;如果政策是提取而非推送,這一切都會更簡單。
盡可能採用提取政策而非推送政策的設計。
用戶端程式庫:請謹慎使用
理想情況下,用戶端會使用 FIDL 編譯器產生的語言專屬用戶端程式庫,與 FIDL 中定義的通訊協定互動。雖然這種方法可讓 Fuchsia 為大量目標語言提供高品質支援,但有時通訊協定過於低階,無法直接編寫程式。在這種情況下,建議您提供手寫的用戶端程式庫,該程式庫會與相同的基礎通訊協定介面,但更容易正確使用。
舉例來說,fuchsia.io 有一個用戶端程式庫 libfdio.so,可為通訊協定提供類似 POSIX 的前端。如果用戶端預期使用 POSIX 樣式的 open/close/read/write 介面,可以連結至 libfdio.so,並使用 fuchsia.io 通訊協定,只需進行極少修改。這個用戶端程式庫之所以有價值,是因為它能調整現有的程式庫介面和基礎 FIDL 通訊協定。
另一種能提供正面價值的用戶端程式庫是架構。框架是大型用戶端程式庫,可為應用程式的大部分內容提供結構。一般來說,框架會針對各種通訊協定提供大量抽象化。舉例來說,Flutter 是一種架構,可視為 fuchsia.ui 通訊協定的廣泛用戶端程式庫。
無論通訊協定是否有相關聯的用戶端程式庫,都應完整記錄 FIDL 通訊協定。獨立的軟體工程師團隊應能直接根據定義理解並正確使用通訊協定,而不需對用戶端程式庫進行反向工程。如果通訊協定有用戶端程式庫,則應清楚記錄通訊協定中低階且細微的部分,以便您建立用戶端程式庫。
用戶端程式庫的主要難題在於,必須為每種譯文語言維護程式庫,這往往意味著較不熱門的語言會缺少用戶端程式庫 (或品質較低)。用戶端程式庫也傾向於僵化基礎通訊協定,因為它們會導致每個用戶端以完全相同的方式與伺服器互動。伺服器會預期這種互動模式,如果用戶端偏離用戶端程式庫使用的模式,伺服器就無法正常運作。
如要在 Fuchsia SDK 中加入用戶端程式庫,我們應提供至少兩種語言的程式庫實作。
服務中樞:請謹慎使用
「服務中樞」是一種 Discoverable 通訊協定,可讓您探索多個其他通訊協定,通常具有明確名稱:
// BAD
@discoverable
protocol ServiceHub {
GetFoo(resource struct {
foo server_end:Foo;
});
GetBar(resource struct {
bar server_end:Bar;
});
GetBaz(resource struct {
baz server_end:Baz;
});
GetQux(resource struct {
qux server_end:Qux;
});
};
特別是如果無狀態,ServiceHub 通訊協定不會提供太多價值,而只是讓個別通訊協定服務直接可供偵測:
@discoverable
protocol Foo {};
@discoverable
protocol Bar {};
@discoverable
protocol Baz {};
@discoverable
protocol Qux {};
無論是哪種方式,用戶端都能與列舉的服務建立連線。在後者的情況下,用戶端可以透過整個系統用於探索服務的一般機制,探索相同的服務。使用一般機制可讓核心平台對探索套用適當政策。
不過,服務中樞在某些情況下仍有其用處。舉例來說,如果通訊協定是有狀態的,或透過比一般服務探索更複雜的程序取得,則通訊協定可透過將狀態轉移至取得的服務來提供價值。另一個例子是,如果取得服務的方法需要額外參數,則通訊協定在連線至服務時,可將這些參數納入考量,進而提供價值。
過度物件導向設計:否
有些程式庫會為通訊協定中的每個邏輯物件建立獨立的通訊協定執行個體,但這種做法有許多缺點:
不同通訊協定執行個體之間的訊息順序未定義。透過單一通訊協定傳送的訊息會以 FIFO 順序 (在每個方向) 處理,但透過不同管道傳送的訊息會發生競爭。當用戶端與伺服器之間的互動分散在多個管道時,如果訊息意外重新排序,就更有可能發生錯誤。
每個通訊協定執行個體都會耗用核心資源、等待佇列和排程。雖然 Fuchsia 的設計可擴充至大量管道,但整個系統的成本會增加,而且建立大量物件來模擬系統中的每個邏輯物件,會對系統造成沉重負擔。
錯誤處理和拆除作業會變得更加複雜,因為錯誤和拆除狀態的數量會隨著互動中涉及的通訊協定執行個體數量呈指數級成長。使用單一通訊協定執行個體時,用戶端和伺服器都可以關閉通訊協定,乾淨俐落地結束互動。如果有多個通訊協定執行個體,互動可能會進入部分關閉狀態,或雙方對關閉狀態的看法不一致。
- 跨通訊協定邊界的協調作業比單一通訊協定內更複雜,因為多個通訊協定需要允許不同用戶端使用不同通訊協定的可能性,而這些用戶端可能不完全信任彼此。
不過,在某些情況下,將功能拆分為多個通訊協定會更合適:
提供獨立的通訊協定有助於提升安全性,因為部分用戶端可能只能存取其中一個通訊協定,因此與伺服器的互動會受到限制。
此外,您也可以更輕鬆地從不同的執行緒使用不同的通訊協定。舉例來說,一個通訊協定可能繫結至一個執行緒,而另一個通訊協定可能繫結至另一個執行緒。
用戶端和伺服器會為通訊協定中的每個方法支付 (少量) 費用。如果一次只需要幾個較小的通訊協定,那麼使用一個包含所有可能方法的巨型通訊協定,效率可能會低於使用多個較小的通訊協定。
有時,伺服器保留的狀態會沿著方法邊界清楚地分解。在這種情況下,請考慮將通訊協定拆分為較小的通訊協定,並沿用相同的邊界,以便提供不同的通訊協定來與不同的狀態互動。
如要避免過度物件導向,建議使用用戶端指派的 ID,在通訊協定中建立邏輯物件模型。這種模式可讓用戶端透過單一通訊協定,與可能龐大的邏輯物件組合互動。
使用魔術值指定缺席:否
我們通常會指示伺服器設定某些狀態,但也允許移除狀態。以下使用魔術值指示移除:
// BAD
protocol View3 {
ReplaceHitRegion(struct {
id uint64;
// Set to { -1, -1 } to remove.
size fuchsia.math.Rect;
});
ReplaceName(struct {
// Set to "" to remove.
name string;
});
};
不過,FIDL 為許多資料類型提供選用性。使用選用性可產生更慣用的介面:
protocol View4 {
ReplaceHitRegion(struct {
id uint64;
size box<fuchsia.math.Rect>;
});
ReplaceName(struct {
name string:optional;
});
};
-
雖然 FIDL 類型系統在 ABI 方面是結構化類型系統,也就是名稱不重要,只有類型結構重要,但 FIDL 類型系統在 API 方面具有具名類型語意。 ↩