FIDL API 評分量表

一般建議

本節介紹在 Fuchsia 介面定義語言中定義通訊協定的技巧、最佳做法和一般建議。

另請參閱 FIDL 樣式指南

通訊協定 (而非物件)

FIDL 是一種語言,用於定義跨程序通訊協定。雖然語法類似於物件導向介面的定義,但設計考量更接近網路通訊協定,而非物件系統。舉例來說,如要設計高品質的通訊協定,您必須考量頻寬、延遲和流量控管。此外,您也應考量通訊協定不只是作業的邏輯分組,通訊協定也會對要求強制執行 FIFO 排序,將通訊協定分成兩個較小的通訊協定,表示在兩個不同通訊協定上提出的要求可以彼此重新排序。

著重於類型

設計 FIDL 通訊協定的良好起點,是設計通訊協定將使用的資料結構。舉例來說,有關網路的 FIDL 通訊協定可能包含各種 IP 位址的資料結構,而有關圖像的 FIDL 通訊協定可能包含各種幾何概念的資料結構。您應該可以查看型別名稱,並直覺瞭解通訊協定所操控的概念,以及這些概念的結構。

語言中立性

許多不同語言都有 FIDL 後端。請避免針對任何特定目標語言過度專門化 FIDL 定義。隨著時間推移,您的 FIDL 通訊協定可能會由許多不同語言使用,甚至可能包括目前不支援的語言。FIDL 是將系統連結在一起的黏著劑,可讓 Fuchsia 支援多種語言和執行階段。如果過度專精於自己喜歡的語言,就會削弱核心價值主張。

序數

通訊協定包含多種方法。系統會自動為每個方法指派專屬的 64 位元 ID,稱為序數。伺服器會使用序數值判斷應調度哪個通訊協定方法。

編譯器會對程式庫、通訊協定和方法名稱進行雜湊處理,藉此判斷序數值。在極少數情況下,相同通訊協定中的序數可能會發生衝突。如果發生這種情況,您可以使用 Selector 屬性,變更編譯器用於雜湊處理的方法名稱。在下列範例中,系統會使用方法名稱「C」而非「B」計算雜湊:

protocol A {
    @selector("C")
    B(struct {
        s string;
        b bool;
    });
};

如果開發人員想變更方法名稱,也可以使用選取器來維持與線路格式的回溯相容性。

診斷

有時需要公開資訊,以利程式的偵錯或診斷。這類資料可以是統計資料和指標 (例如錯誤數、呼叫次數、大小等)、有助於開發的資訊、元件的健康狀態等。

在正式版通訊協定中,您可能會想在測試通訊協定或偵錯方法中公開這項資訊,不過,Fuchsia 提供獨立的機制來公開這類資訊:檢查,您應將此納入考量,以便決定公開這類資料的最佳方式。如果需要公開程式的診斷資訊,以利在測試中進行偵錯、供開發人員工具使用,或透過當機報告或指標在現場擷取,只要沒有其他程式使用該資訊做出執行階段決策,就應使用 Inspect,而非 FIDL 方法/通訊協定。

如果其他程式會根據診斷資訊做出執行階段決策,就應使用 FIDL。Inspect 絕不能用於程式間的通訊,這項系統盡可能提供最佳服務,但不能用於在正式環境中做出決策或變更執行階段行為。

判斷是否要使用 Inspect 或 FIDL 的啟發式方法可能如下:

  1. 其他程式是否在正式版中使用這些資料?

    • 是:使用 FIDL。
  2. 這些資料是否會用於當機報告或指標?

    • 是:使用「檢查」。
  3. 測試或開發人員工具是否會使用資料?是否有機會在正式環境中使用?

    • 是:使用 FIDL。
    • 否:請使用其中一個。

程式庫結構

將 FIDL 宣告分組到 FIDL 程式庫有兩個特定目標:

  • 協助 FIDL 開發人員 (使用 FIDL 程式庫者) 瀏覽 API 介面。
  • 在 FIDL 程式庫中,為階層式範圍的 FIDL 宣告提供結構。

請仔細考慮如何將型別和通訊協定定義劃分為程式庫。您如何將這些定義分解為程式庫,對這些定義的消費者有很大的影響,因為 FIDL 程式庫是型別和通訊協定的依附元件和發布單元。

FIDL 編譯器要求程式庫之間的依附元件圖為 DAG,也就是說,您無法在程式庫界線之間建立循環依附元件。不過,您可以在程式庫中建立 (部分) 循環依附元件。

如要決定是否將程式庫分解為較小的程式庫,請考量下列問題:

  • 程式庫的客戶是否會分成不同角色,並想使用程式庫中的部分功能或宣告?如果是,請考慮將程式庫分成多個程式庫,分別針對每個角色。

  • 這個程式庫是否對應到一般人都能理解結構的產業概念?如果是,建議您按照業界標準架構整理程式庫。舉例來說,藍牙會整理成 fuchsia.bluetooth.lefuchsia.bluetooth.gatt,與業界普遍瞭解這些概念的方式一致。同樣地,fuchsia.net.http 對應於業界標準的 HTTP 網路通訊協定。

  • 許多其他程式庫是否依附於該程式庫?如果是,請檢查這些傳入的依附元件是否真的需要依附整個程式庫,或是是否有可從程式庫中分解出來的「核心」定義集,以接收大量傳入的依附元件。

理想上,我們會為整個 Fuchsia 產生 FIDL 程式庫結構,這是全域最佳化。不過,康威定律指出,「設計系統的機構 [...] 會受到限制,只能產生與這些機構的通訊結構相同的設計」。我們應該花適當的時間與康威定律抗衡。

存取權控管機制會以通訊協定為單位

決定要在哪個程式庫中定義通訊協定時,請勿考量存取控制。一般來說,存取權控管是以通訊協定精細度表示。定義通訊協定的程式庫與存取控制無關,無法用來判斷是否可存取。

舉例來說,程序可能會存取 fuchsia.logger.LogSink,或是程序會取得 fuchsia.media.StreamSource 通訊協定的用戶端。不過,FIDL 的設計目的並非用於表示 fuchsia.logger 程式庫的存取權,也無法用於防止存取 fuchsia.ldsvc 程式庫。

fuchsia 命名空間

在平台來源樹狀結構中定義的 FIDL 程式庫 (即在 fuchsia.googlesource.com 中定義),必須位於 fuchsia 頂層命名空間 (例如 fuchsia.ui),除非符合下列其中一項條件:

  • 這個程式庫定義 FIDL 語言本身或其一致性測試套件的部分內容,在這種情況下,頂層命名空間必須是 fidl
  • 這個程式庫僅用於內部測試,不會納入 SDK 或正式版建構作業,在這種情況下,頂層命名空間必須是 test

強烈建議頂層命名空間 fuchsia 中的 FIDL 程式庫最多只有四個元件,也就是 fuchsia.<api-namespace>fuchsia.<api-namespace>.<name>fuchsia.<api-namespace>.<name>.<subname>。選擇適當的 api-namespace,可能需要 API 委員會成員的協助。

舉例來說,為向應用程式公開硬體功能,在平台來源樹狀結構中定義的 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,提供可變更元素是否可由匯入程式庫 (「子項程式庫」) 使用的可見度規則,例如 publicprivate 修飾符。

internal 程式庫元件名稱應特別處理,並指出可見度規則的本機限制。舉例來說,fuchsia.net.dhcp.internal.foo 程式庫中的公開宣告可能只對其父項 fuchsia.net.dhcp 或同層級 (例如 fuchsia.net.dhcp.internal.bar) 可見。

使用多字詞程式庫元件

雖然允許使用含有連接多個單字元件的程式庫名稱 (例如 fuchsia.modular.storymodel),但應盡量避免使用。如果程式庫名稱會違反巢狀結構規則,或者在以階層方式思考程式庫的放置位置時,兩個字都不應優先於另一個字,程式庫作者可以將多個字詞合併在一起。

版本字串

如果程式庫需要版本控管,應加上單一版本號碼後置字串,例如 fuchsia.io2fuchsia.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.inputfuchsia.ui.scenic.input2fuchsia.input 服務的其他網域區分開來。

類型

如「一般建議」一節所述,您應特別注意在通訊協定定義中使用的型別。

保持一致

針對相同概念使用一致的型別。舉例來說,在整個程式庫中,針對特定概念使用 uint32int32。如果您為概念建立 struct,請一律使用該結構體來表示概念。

理想情況下,類型也應在程式庫界線之間保持一致。 請檢查相關程式庫是否有類似概念,並與這些程式庫保持一致。如果程式庫之間共用許多概念,請考慮將這些概念的型別定義納入通用程式庫。舉例來說,fuchsia.memfuchsia.math 分別包含許多常用型別,可用於表示記憶體和數學概念。

偏好語意類型

建立結構體來命名常用概念,即使這些概念可以使用基本型別表示也沒關係。舉例來說,IPv4 位址是網路程式庫中的重要概念,即使資料可使用基本型別表示,也應使用結構體命名:

type Ipv4Address = struct {
    octets array<uint8, 4>;
};

在效能至關重要的目標語言中,結構體會以行內形式表示,這可降低使用結構體命名重要概念的成本。

zx.Time」有明確定義的時間碼

zx.Time 類型會單調遞增地測量自裝置專屬時間基準起經過的奈秒數。zx.Time 的用途可以假設這個時間基準,不需要明確說明。

審慎使用匿名型別

匿名型別非常實用,可更流暢地說明 API。具體來說,如果您事先知道具名型別的子元素本質上與該具名型別相關聯,且在包含具名容器的環境外使用時,不會有任何用處或意義,就非常適合使用匿名型別。

舉例來說,聯集變體會將幾項事物匯總在一起。 聯集變體極少會單獨使用,也就是說,我們事先知道聯集變體只有在特定用途的脈絡中才有意義。因此,使用匿名型別做為聯集變體是適當且建議的做法。

理想情況下,型別應與 API 的重要概念一一對應,且不得有兩個型別的定義相同。不過,要同時達成這兩項目標並不容易,特別是當型別的命名 (會引入不同的概念 1) 除了做為 API 介面元素之外,還有其他意義時。舉例來說,假設有名為 type EntityId = struct { id uint64; };type OtherEntityId = struct { id uint64; }; 的 ID,代表不同概念,但除了名稱以外,兩者具有相同的型別定義。

使用匿名型別會建立多個型別,且彼此不相容。因此,如果使用多個匿名型別來表示相同概念,就會導致 API 過於複雜,且無法在大多數目標語言中進行泛型處理。

因此,使用匿名型別時,請務必避免多個匿名型別代表相同概念。舉例來說,如果 API 的演進可能會導致多個匿名型別代表相同概念,您就不得使用匿名型別。

考慮使用虛擬記憶體物件 (VMO)

虛擬記憶體物件 (VMO) 是核心物件,代表連續的虛擬記憶體區域和邏輯大小。使用這個型別在 FIDL 訊息中傳輸記憶體,並使用 ZX_PROP_VMO_CONTENT_SIZE 屬性追蹤物件中包含的資料量。

指定向量和字串的界限

所有 vectorstring 宣告都應指定長度界線。 聲明通常可分為兩類:

  • 資料本身有固有的限制。舉例來說,包含檔案系統名稱元件的字串長度不得超過 fuchsia.io.MAX_NAME_LENGTH
  • 除了「盡可能」之外,沒有其他限制。在這些情況下,您應使用內建常數 MAX

使用 MAX 時,請考慮訊息接收者是否真的想處理任意長度的序列,或是極長的序列是否代表濫用行為。

請注意,透過 zx::channel 傳送所有聲明時,系統會隱含地以訊息長度上限為界。如果真的有任意長度序列的用途,單純使用 MAX 可能無法滿足這些用途,因為嘗試提供極長序列的用戶端可能會達到訊息長度上限。

如要處理任意長度的序列,請考慮使用下列其中一種分頁模式,將序列分成多則訊息,或是將資料移出訊息本身 (例如移至 VMO)。

FIDL 配方:大小限制

FIDL 向量和字串可能會有大小限制,指定類型可包含的成員數量上限。如果是向量,這指的是向量中儲存的元素數量;如果是字串,則是指字串包含的位元組數量

強烈建議使用大小限制,因為這會為原本無限制的大型型別設定上限。

鍵/值儲存空間的實用作業是依序疊代,也就是在指定鍵時,依序傳回該鍵之後的元素清單 (通常會分頁)。

推理

在 FIDL 中,最好使用迭代器完成這項作業,迭代器通常會實作為獨立通訊協定,可透過該通訊協定進行迭代。使用獨立的通訊協定 (因此也是獨立的管道) 有許多優點,包括從透過主要通訊協定執行的其他作業中,取消交錯疊代提取要求。

通訊協定 P 的管道連線用戶端和伺服器端可分別以 FIDL 資料類型表示,即 client_end:Pserver_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: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// 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",
        },
    ],
}

運作範圍

// 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(&current_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++ (Natural)

用戶端

// 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 stringUTF-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 個位元組

從這個範例中,您應該可以清楚瞭解,如果應用程式的 UI 顯示文字輸入框,允許 N 個任意字素叢集 (使用者認為的「字元」),且您打算透過 FIDL 傳輸使用者輸入的字串,則必須在 FIDL string 欄位中預留 一些倍數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;
};

使用這個模式時,您可以選擇使用 int32uint32 或列舉,代表傳回的錯誤類型。在大多數情況下,建議採用列舉

建議在通訊協定的所有方法中,使用單一錯誤類型。

偏好網域專屬列舉

定義及控管網域時,請使用專為此用途建構的列舉錯誤型別。舉例來說,如果通訊協定是專為特定用途而建構,且傳達錯誤語意是唯一設計限制,請定義列舉。如「列舉」一節所述,最好避免使用 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 錯誤代碼),且列舉是代表規格所規定原始值的便利方式,請使用網域專屬的列舉錯誤型別。

特別是與核心物件或 I/O 相關的錯誤,請使用 zx.Status 型別。舉例來說,fuchsia.process 使用 zx.Status,因為這個程式庫主要用於操控核心物件。以另一個例子來說,fuchsia.io 大量使用 zx.Status,因為這個程式庫與 I/O 有關。

使用含有錯誤列舉的選用值

過去定義方法時,若有兩個傳回值 (一個是選用值,另一個是錯誤碼),效能會稍微提升。例如:

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;
    });
};

不過,這個模式現在已遭淘汰,取而代之的是 error 語法在封裝中內嵌小值的做法已取代這個模式的效能優勢,且現在普遍支援低階聯集。

避免在錯誤中顯示訊息和說明

在某些特殊情況下,如果可能發生的錯誤情況範圍很大,且描述性錯誤訊息可能對用戶端有幫助,則通訊協定除了 status 或列舉值之外,也可能包含錯誤的字串說明。不過,包含字串會造成困難。舉例來說,用戶端可能會嘗試剖析字串,瞭解發生了什麼事,這表示字串的確切格式會成為通訊協定的一部分,而當字串本地化時,這會特別造成問題。

安全注意事項: 同樣地,向用戶端回報堆疊追蹤或例外狀況訊息,可能會無意間洩漏私密資訊。

本地化字串和錯誤訊息

如果您要建構做為 UI 後端的服務,請使用結構化型別訊息,並將轉譯作業留給 UI 層。

如果所有訊息都很簡單且未經過參數化,請使用 enums 進行錯誤回報和一般 UI 字串。如要傳送更詳細的訊息,並加入名稱、號碼和位置等參數,請使用 tables 或 xunions,並將參數做為字串或數字欄位傳遞。

您可能會想在服務中產生訊息 (英文),然後以字串形式提供給 UI,因為 UI 只會收到字串,並彈出通知或錯誤對話方塊。

不過,這種較簡單的做法有幾個嚴重缺點:

  • 您的服務是否知道 UI 中使用的語言和地區?您必須在每個要求中傳遞語言代碼 (請參閱範例),或是追蹤每個已連線用戶端的狀態,才能以正確的語言提供訊息。
  • 服務的開發環境是否支援本地化?如果您使用 C++ 編寫程式,可以輕鬆存取 ICU 程式庫和 MessageFormat,但如果您使用 Rust,目前程式庫支援的範圍就相當有限。
  • 您的錯誤訊息是否需要包含 UI 已知但服務未知的參數?
  • 您的服務是否只提供單一 UI 實作項目?服務是否知道 UI 有多少空間可顯示訊息?
  • 錯誤只會以文字形式顯示嗎?您可能還需要錯誤專屬的警示圖示、音效或文字轉語音提示。
  • 使用者能否在 UI 仍在執行時變更顯示語言代碼?如果發生這種情況,可能難以將預先本地化的字串更新為新語言代碼,尤其是當這些字串是某些非等冪運算的結果時。

除非您要建構與單一 UI 實作項目緊密耦合的高度專業化服務,否則可能不應在 FIDL 服務中公開使用者可見的 UI 字串。

我是否應定義結構體來封裝方法參數 (或回應)?

定義方法時,您需要決定要個別傳遞參數,還是將參數封裝在結構體中。做出最佳選擇時,需要權衡多項因素。請參考下列問題,協助您做出決策:

  • 是否有有意義的封裝界線?如果一組參數適合以單元形式傳遞,因為這些參數除了這個方法外,還具有某種凝聚力,您可能想將這些參數封裝在結構體中。(希望您在開始設計通訊協定時,就已根據上述「一般建議」找出這些連貫的群組,並及早著重於類型。)

  • 除了呼叫的方法之外,這個結構體是否還有其他用途?如果不是,請考慮分別傳遞參數。

  • 您是否在許多方法中重複使用相同的參數群組?如果是,請考慮將這些參數分組為一或多個結構。您也可以考慮重複是否表示這些參數具有凝聚力,因為它們代表通訊協定中的某些重要概念。

  • 是否有大量選用參數,或通常會提供預設值的參數?如果是,請考慮使用結構體,減少呼叫端的樣板。

  • 是否有參數群組一律同時為空值或非空值?如果是這樣,請考慮將這些參數分組到可為空值的結構體中,以在通訊協定本身中強制執行不變量。舉例來說,上述定義的 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>

  • 請使用 vector<byte> 做為 HTTP 標頭欄位,因為 HTTP 標頭欄位不會指定編碼,因此不一定能以 UTF-8 表示。

  • 請使用 array<byte, 6> 做為 MAC 位址,因為 MAC 位址是二進位資料。

  • UUID 應使用 array<byte, 16>,因為 UUID 是 (幾乎!) 任意二進位資料。

針對 Blob 使用共用記憶體基本類型:

  • 如果完全緩衝處理資料有意義,請對圖片和 (大型) protobuf 使用 zx.Handle:VMO
  • 音訊和影片串流請使用 zx.Handle:SOCKET,因為資料可能會隨時間傳送,或是在完全寫入或可用之前處理資料。

我應該使用 vector 還是 array

vector 是變數長度的序列,以線路格式的行外表示。array 是固定長度的序列,以線路格式內嵌表示。

使用 vector 代表長度不定的資料:

  • 記錄訊息中的標記請使用 vector,因為記錄訊息可以有零到五個標記。

使用 array 處理固定長度的資料:

  • MAC 位址一律為六個位元組,因此請使用 array

我應該使用 struct 還是 table

結構體和資料表都代表具有多個具名字段的物件。不同之處在於,結構體在傳輸格式中具有固定版面配置,這表示「無法」修改結構體,否則會破壞二進位檔的相容性。相較之下,表格在線路格式中具有彈性版面配置,這表示欄位可以隨時間新增至表格,而不會破壞二進位檔相容性。

對於效能至關重要的通訊協定元素,或是未來不太可能變更的通訊協定元素,請使用結構體。舉例來說,您可以使用結構體代表 MAC 位址,因為 MAC 位址的結構在未來不太可能變更。

如果通訊協定元素日後可能會變更,請使用表格。舉例來說,您可以使用表格表示攝影機裝置的中繼資料資訊,因為中繼資料中的欄位可能會隨時間演變。

如何表示常數?

視常數的類型而定,有三種方式可表示常數:

  1. 針對特殊值 (例如 PIMAX_NAME_LEN) 使用 const
  2. 當值為一組元素時,請使用 enum,例如媒體播放器的重複模式:OFFSINGLE_TRACKALL_TRACKS
  3. 使用 bits 代表形成一組旗標的常數,例如介面的功能:WLANSYNTHLOOPBACK

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 修飾符,以免日後難以轉換。這種情況很少發生:經驗顯示,大多數訊息都不含資源,而且在通訊協定中傳遞資源需要謹慎處理和事先規劃。

我應該在型別上使用 strictflexible 嗎?

將型別標示為 flexible 後,即可處理目前 FIDL 結構定義不明的資料,但使用者必須考量這種可能性。具體來說,使用者無法對彈性位元、列舉或聯集執行詳盡的模式比對。strict 類型通常較容易使用和推論,但不允許日後擴充。

strictflexible 之間做選擇時,請思考下列問題:

  • 我是否有可能在日後需要為這類別新增成員?如果是,請使用 flexible
  • 萬一我真的需要為這類項目新增成員,是否最好直接導入新類型,而不是為這類項目新增成員?如果是,請使用 strict

型別可以從 strict 轉換為 flexible,但這個過程很緩慢,必須等到該型別的所有 API 級別都已淘汰,才能將新成員加入型別。strict如要開始這項轉換,請指出自 NEXT 起,類型不再是 strict,而是 flexible

type Color2 = strict(removed=NEXT) flexible(added=NEXT) enum {
    RED = 1;
};

如果類型允許,建議您一律指定這個修飾符,這樣會更具風格。Fuchsia 專案會透過 Lint 檢查強制執行此樣式。

使用 strictflexible 不會對效能造成顯著影響。

處理權限

本節說明在 FIDL 中,為控制代碼指派權限限制的最佳做法。

如要進一步瞭解繫結中如何使用權限,請參閱 FIDL 繫結規格RFC-0028

如需 zircon 權利定義,請參閱核心權利。 FIDL 會使用 rights.fidl 解決版權限制。

一律指定帳號代碼的權利

所有控制碼都應指定權利,明確說明預期用途。這項規定會強制您預先決定要傳遞哪些權利,而不是根據觀察到的行為來決定。擁有明確的權利也有助於稽核 API 介面。

授予收件者最低權限

決定要提供哪些權利時,請盡量減少,也就是只提供達成所需功能的最少權利。舉例來說,如果只會用到 zx.Rights.READzx.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 通訊協定中。

我應該在方法和事件中使用 strictflexible 嗎?

將方法或事件標示為 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() -> ();),讓用戶端瞭解伺服器是否沒有該方法。請注意,這樣做可為用戶端提供額外的彈性;透過這項設定,用戶端可選擇是否要回應伺服器未辨識 EnablePIIMode 的情況,方法是關閉連線或選擇不記錄 PII,而使用 strict 時,通訊協定一律會自動關閉。但這會中斷前饋流程。

請注意,嚴格程度取決於寄件者。假設您在版本 1 中有某個方法 strict A();,然後在版本 2 中將其變更為 flexible A();,最後在版本 3 中刪除。如果以版本 1 建構的用戶端嘗試在以版本 3 建構的伺服器上呼叫 A(),系統會將該方法視為嚴格,因為版本 1 的用戶端認為該方法是嚴格的,而版本 3 的伺服器會採信用戶端的說法,因為伺服器完全無法辨識該方法。

建議您一律指定這個修飾符,因為這樣做很時尚。Fuchsia 專案會透過 Linter 檢查強制執行此樣式。

該使用 openajar 還是 closed

將通訊協定標示為 open,可讓您更輕鬆地處理方法或事件的移除作業,因為不同元件可能是在不同版本中建構,因此每個元件對現有的方法和事件都有不同的看法。由於一般而言,我們希望通訊協定能靈活演進,因此建議選擇 open 做為通訊協定,除非有理由選擇較封閉的通訊協定。

決定使用 ajarclosed 時,應考量通訊協定演進的預期限制。使用 closedajar 不會阻止通訊協定演進,但會要求較長的推出期,在此期間,方法和事件存在但未使用,以確保所有用戶端和伺服器都同意存在哪些方法。視用戶端或伺服器先更新而定,使用 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 模式運作良好,但如果用戶端和伺服器需要同步處理工作,這個模式就不適用。

舉例來說,伺服器可能會針對某些可變動的狀態 foo,為每個用戶端實作暫止的 GET 模式,並使用「髒」位元。這個位元會初始化為 true,在每個 WatchFoo 回應中清除,並在每次 foo 變更時設定。只有在設定髒位元時,伺服器才會回應 WatchFoo 訊息。

使用確認訊息限制推送

在採用推送的通訊協定中提供流量控管機制時,其中一種方法是使用確認模式,也就是呼叫端提供確認回應,供呼叫端用於流量控管。舉例來說,假設有下列一般接聽程式通訊協定:

protocol Listener {
    OnBar(struct {
        args Args;
    }) -> ();
};

預期接聽程式會在收到 OnBar 訊息後,立即傳送空白的回應訊息。回應不會將任何資料傳達給呼叫端。而是讓來電者觀察接聽者消耗訊息的速率。呼叫端應限制產生訊息的速率,使其與被呼叫端取用訊息的速率相符。舉例來說,呼叫端可能會安排只有一則 (或固定數量的) 訊息處於傳輸中 (即等待確認)。

FIDL 方案:確認模式

確認模式是簡單的流程控制方法,適用於原本是單向呼叫的方法。方法不會保留為單向呼叫,而是改為雙向呼叫,但沒有回應,俗稱「ack」。確認訊息存在的唯一理由是通知傳送者訊息已送達,傳送者可據此決定後續做法。

這項確認的費用會增加管道的閒聊。如果用戶端等待確認訊息,才繼續下一個呼叫,這種模式也可能導致效能降低。

來回傳送未計量的單向呼叫可產生簡單的設計,但可能會有潛在的陷阱:如果伺服器處理更新的速度遠慢於用戶端傳送更新的速度,該怎麼辦?舉例來說,用戶端可能會從某個文字檔載入由數千行組成的繪圖,並嘗試依序傳送所有這些行。如何對用戶端施加背壓,防止伺服器因這波更新而負荷過重?

使用確認模式並將單向呼叫 AddLine(...); 設為雙向 AddLine(...) -> ();,即可向用戶端提供意見回饋。這樣一來,用戶端就能視情況節流輸出內容。在這個範例中,我們只會讓用戶端等待 ACK,然後傳送下一個等待中的訊息,但更複雜的設計可能會樂觀地傳送訊息,只有在收到的非同步 ACK 比預期少時,才會節流。

首先,我們需要定義介面定義和測試架構。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" },
    ],
    config: {
        // 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.
        script: {
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
        },
    },
}

伺服器

// 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",
        },
    ],
}

運作範圍

// 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++ (Natural)

用戶端

// 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.Status;
    });
};

另一個適合使用事件的案例是,當用戶端要求伺服器產生事件,且伺服器產生的事件總數有上限時。這個模式是懸掛式 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() -> (); 方法,以利同步處理。每當用戶端準備好接收下一個繪圖更新時,就會呼叫這個方法,伺服器的回應會指出用戶端可以繼續提出更多要求。

我們現在已在雙向進行流量控管。這項通訊協定現在會實作前饋模式,允許許多不受控的呼叫,直到某些同步「提交」呼叫觸發伺服器上的實際工作為止。這樣可避免用戶端工作量過大,導致伺服器超載。同樣地,伺服器也不再允許傳送無界限的 -> 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" },
    ],
    config: {
        // 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.
        script: {
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
        },

    },
}

伺服器

// 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",
        },
    ],
}

運作範圍

// 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 mut line: [Point; 2] = [from, to];

        // Batch a line for drawing to the canvas using the two points provided.
        println!("AddLines batching line: {:?}", &mut 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++ (Natural)

用戶端

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

通常,前饋通訊協定會涉及用戶端提交一連串單向方法呼叫,而不等待伺服器的回應。提交這些訊息後,用戶端會呼叫 CommitFlush 等具有回覆的方法,與伺服器明確同步。回覆內容可能是空白訊息,也可能包含提交的序號是否成功等資訊。在更複雜的通訊協定中,單向訊息會以指令物件的聯集表示,而非個別方法呼叫;請參閱下方的指令聯集模式

使用前饋資料流的通訊協定很適合採用最佳化錯誤處理策略。伺服器不必為每個方法回覆狀態值,以免鼓勵用戶端等待每則訊息之間的往返行程,而是只有在方法可能因用戶端無法控制的原因而失敗時,才加入狀態回覆。如果用戶端傳送的訊息應為無效 (例如參照無效的用戶端指派 ID),請關閉連線來發出錯誤信號。如果用戶端傳送的訊息無效,但用戶端可能不知道,請提供表示成功或失敗的回應 (這需要用戶端同步處理),或記住錯誤並忽略後續的相依要求,直到用戶端同步處理並以某種方式從錯誤中復原為止。

範例:

protocol Canvas {
    Flush() -> (struct {
        code zx.Status;
    });
    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:Pserver_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: {
        write_items: {
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
        },

        // A key to iterate from, after all items in `write_items` have been written.
        iterate_from: {
            type: "string",
            max_size: 64,
        },

    },
}

伺服器

// 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",
        },
    ],
}

運作範圍

// 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(&current_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++ (Natural)

用戶端

// 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 周邊裝置的序號。序號不含個人資料,但屬於非常穩定的識別碼,容易建立關聯。在 API 中加入序號會導致許多隱私權問題:

  • 任何有權存取 API 的用戶端,都能使用同一部 Fuchsia 裝置關聯不同帳戶。
  • 任何可存取 API 的用戶端,都能關聯帳戶中的不同角色。
  • 不同軟體供應商可能會串通,瞭解他們是否由同一位使用者或在同一部裝置上使用。
  • 如果周邊裝置在不同裝置之間移動,任何有權存取 API 的用戶端都可能將周邊裝置共用的裝置和使用者集建立關聯。
  • 如果周邊裝置已售出,有權存取 API 的用戶端可能會將周邊裝置的新舊擁有者建立關聯。
  • 部分製造商會在序號中編碼資訊。這可能會讓有權存取 API 的用戶端推斷使用者購買周邊裝置的地點或時間。

在本例中,序號的用途是讓用戶端偵測到重新連線的 USB 周邊裝置是否相同。如要達成此意圖,必須使用穩定 ID,但不需要全域 ID。不同用戶端不必收到相同 ID,同一用戶端也不必在不同 Fuchsia 裝置上收到相同 ID,且 ID 不必在恢復原廠設定事件中保持不變。

在本例中,較好的替代做法是傳送 ID,但要確保 ID 只會針對單一裝置上的單一用戶端保持穩定。這個 ID 可能是週邊裝置序號的雜湊值、Fuchsia 裝置 ID,以及連線的路徑名稱。

範例 2 - 裝置設定 API 中的裝置名稱

假設裝置設定 API 包含用來輔助設定裝置的手機型號。在大多數情況下,手機的型號字串是由原始設備製造商 (OEM) 設定,但有些手機會將使用者提供的裝置名稱回報為型號。因此許多模型字串都含有使用者的真實姓名或假名。因此,這項 API 可能會將使用者與不同身分或裝置建立關聯。即使使用者未提供,罕見或預先發布的模型字串仍可能揭露私密資訊。

在某些情況下,您可能適合使用模型字串,但限制哪些用戶端可以存取 API。或者,API 可以使用從不受使用者控制的欄位,例如製造商字串。另一個替代方案是比較模型字串與熱門手機型號的允許清單,並以一般字串取代罕見模型字串,藉此清除模型字串。

用戶端指派的 ID

通訊協定通常會讓用戶端操控伺服器持有的多個狀態。設計物件系統時,解決這個問題的常見做法是為伺服器持有的每個連貫狀態建立個別物件。不過,設計通訊協定時,為每個狀態片段使用個別物件有幾個缺點。

為每個邏輯物件建立個別的通訊協定執行個體會耗用核心資源,因為每個執行個體都需要個別的管道物件。每個執行個體都會維護獨立的訊息先進先出佇列。為每個邏輯物件使用個別例項,表示傳送至不同物件的訊息可以彼此重新排序,導致用戶端與伺服器之間的互動順序錯誤。

用戶端指派的 ID 模式可讓用戶端將 uint32uint64 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();
};

視網域專屬語意而定,這個模式可能也需要第二個事件,用來表示伺服器已完成傳送資料。這種做法適用於簡單的情況,但會產生許多擴充問題。舉例來說,這個通訊協定缺少流量控制機制,如果用戶端不再需要額外資料,也無法停止伺服器 (除非關閉整個通訊協定)。

更穩固的方法是使用撕除通訊協定建立疊代器:

protocol BarIterator {
    GetNext() -> (resource struct {
        bars vector<client_end:Bar>;
    });
};

protocol ChannelBasedGetter {
    GetBars(resource struct {
        iterator server_end:BarIterator;
    });
};

呼叫 GetBars 後,用戶端會使用通訊協定要求管道化,立即將第一個 GetNext 呼叫加入佇列。之後,用戶端會重複呼叫 GetNext,從伺服器讀取額外資料,並限制未處理的 GetNext 訊息數量,以提供流量控制。請注意,疊代器不需要「完成」回應,因為伺服器可以回覆空向量,然後在完成時關閉疊代器。

另一種讀取分頁方式是使用權杖。採用這種方法時,伺服器會以不透明權杖的形式,將疊代器狀態儲存在用戶端,而用戶端會在每次部分讀取時,將權杖傳回伺服器:

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,並比對糾纏事件物件中的 koidrelated_koid 屬性。

取消 Eventpair

使用撕除通訊協定交易時,用戶端可以關閉通訊協定的用戶端,藉此取消長時間執行的作業。伺服器應監聽 ZX_CHANNEL_PEER_CLOSED,並中止交易,以免浪費資源。

如果作業沒有專屬管道,也有類似的用途。 舉例來說,fuchsia.net.http.Loader 通訊協定有 Fetch 方法,可啟動 HTTP 要求。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 方法,該方法會採用完整的設定值,並將設定變更為新提供的值。

protocol TheManagerOfOtherSorts {
    /// Description how the override modifies the behavior.
    ///
    /// This replaces the setting.
    Replace(struct {
        settings Settings;
    }) -> (struct {
        args Args;
    });
};

應避免的事項:

  • 無論是部分更新或取代方法,都請避免使用 SetOverride 動詞,因為提供的語意會模稜兩可。

  • 請避免使用個別方法更新設定的欄位,例如 SetMagnificationEnabled。維護這類個別方法較為麻煩,而且呼叫端很少會想更新單一值。

  • 請避免使用 -1 等魔法值移除設定。請改為移除該設定欄位,藉此移除設定。

參照聯集變體和資料表欄位

參照型別的欄位通常很有用,例如參照表格的一或多個欄位,或是參照特定聯集變體。

假設某個 API 提供中繼資料做為具有許多欄位的 table。如果這項中繼資料可能會變得相當龐大,通常建議提供機制,讓收件者向寄件者指出要讀取的中繼資料欄位,避免傳送收件者不會考慮的多餘欄位。在這種情況下,如果有一個與 table 欄位成員一一對應的平行 bits,就能為建構 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 值,現在面臨以下兩個糟糕的選擇:

  1. 導入 enum,這表示現在有兩種方式可參照欄位,且用戶端程式碼中可能會有轉換問題 (從一種表示法轉換為另一種);或

  2. 繼續使用 bits,但一次只能設定一個位元,現在要回溯對應至哪個特定位元很麻煩。

總結來說,table的摘要如下:

  • table 的名稱加上後置字元 Fields (複數) 為 bits 命名。每個成員值都應是序數索引處的位元,即 1 << (ordinal - 1)

  • union 的建議類似,您希望 bitstable 之間的彈性相符,也就是說,由於 FIDL 目前只支援彈性表格,因此 bits 必須是 flexible

針對 union

  • enum 刊登商品命名為 union 的名稱,並加上後綴 Variant (單數)。每個成員值都應是所描述變體的序數。

  • unionenum 之間的彈性必須相符,也就是說,如果 unionstrict,則 enum 也必須是 strict

反模式

本節將說明幾種反模式,也就是通常會產生負面價值的設計模式。學會辨識這些模式是第一步,有助於避免以錯誤方式使用。

推送的設定:盡量避免

Fuchsia 平台通常偏好提取語意。元件的語意提供重要範例;功能會從元件中提取,因此元件啟動作業會延遲,而元件關閉順序則會從能力路徑的有向圖推斷。

使用 FIDL 通訊協定將設定從一個元件推送至另一個元件的設計,看似簡單,因此很吸引人。如果元件 A 將政策推送至元件 B,以供 B 的業務邏輯使用,就會發生這種情況。這會導致平台誤解依附元件關係:系統不會自動啟動 A 來支援 B 的函式,而且可能會在 B 完成執行商業邏輯前關閉 A。這會導致需要使用弱依附元件和虛擬反向依附元件的解決方法,才能產生所需行為;如果採用提取而非推送政策,這些做法都會變得更簡單。

盡可能採用從伺服器擷取政策的設計,而非推送政策。

用戶端程式庫:請謹慎使用

理想情況下,用戶端會使用 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 {};

無論採用哪種方式,用戶端都能與列舉的服務建立連線。在後者情況下,用戶端可透過系統中用於探索服務的正常機制,探索相同的服務。使用一般機制可讓核心平台對探索套用適當的政策。

不過,在某些情況下,服務中心可能很有用。舉例來說,如果通訊協定是有狀態的,或是透過比一般服務探索更複雜的程序取得,則通訊協定可將狀態轉移至取得的服務,進而提供價值。再舉一例,如果取得服務的方法需要額外參數,則通訊協定在連線至服務時,可將這些參數納入考量,進而提供價值。

過度物件導向的設計:否

部分程式庫會為通訊協定中的每個邏輯物件建立個別的通訊協定執行個體,但這種做法有許多缺點:

  • 不同通訊協定執行個體之間的訊息順序未定義。 透過單一通訊協定傳送的訊息會依先進先出順序處理 (每個方向),但透過不同管道傳送的訊息會競爭。如果用戶端與伺服器之間的互動分散在多個管道,訊息意外重新排序時,就可能發生更多錯誤。

  • 每個通訊協定執行個體都會產生核心資源、等待佇列和排程方面的成本。雖然 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;
    });
};

  1. 就 ABI 而言,FIDL 型別系統是結構型別系統,也就是說,名稱無關緊要,只有型別的結構才重要;但就 API 而言,FIDL 型別系統具有已命名型別語意。