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 提供另一種機制,可用於提供這類資訊:檢查,應將其納入考量,以便針對如何公開這類資料做出最佳決策。如需公開有關程式的診斷資訊,以便在測試中進行偵錯、由開發工具使用,或透過當地回報或指標擷取,但沒有其他程式使用該資訊做出執行階段決策,則應使用檢查功能,而非 FIDL 方法/通訊協定。

當需要根據其他程式的診斷資訊做出執行階段決策時,應使用 FIDL。請勿將 Inspect 用於程式之間的通訊,這是一種盡力系統,請勿依賴它在正式版的執行階段做出決策或變更行為。

判斷是否要使用 Inspect 或 FIDL 的啟發式可能為:

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

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

    • 是:請使用「檢查」功能。
  3. 資料是用於測試還是開發人員工具?是否有可能用於實際工作環境?

    • 是:使用 FIDL。
    • 否:請使用任一項。

程式庫結構

將 FIDL 宣告分組成 FIDL 程式庫有兩個具體目標:

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

請仔細考量如何將類型和通訊協定定義劃分為程式庫。您如何將這些定義分解為程式庫,會對這些定義的使用者造成重大影響,因為 FIDL 程式庫是類型和通訊協定的依附元件和發行單位。

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

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

  • 程式庫的客戶是否會分為不同的角色,而這些角色會想使用程式庫中的部分功能或宣告?如果是這樣,請考慮將程式庫分割成各自鎖定各個角色的程式庫。

  • 程式庫是否對應到具有一般可理解結構的產業概念?如果是這樣,建議您將程式庫的結構調整為符合業界標準的結構。舉例來說,Bluetooth 會分為 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),除非下列其中一項條件為 true:

  • 此程式庫會定義 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.input2 會更清楚,可與 fuchsia.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 連線,將 FIDL 用戶端連線至對應伺服器的其他 (非 @discoverable) 方式!

通訊協定端點是一般 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 lazy_static::lazy_static;
use regex::Regex;

use fidl_examples_keyvaluestore_additerator::{
    Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
    StoreRequestStream, WriteError,
};
use fuchsia_async as fasync;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::ops::Bound::*;
use std::sync::{Arc, Mutex};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        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().unwrap().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().unwrap();
                            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().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (Wire)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

字串編碼、字串內容和長度邊界

FIDL string 會以 UTF-8 編碼,這是一種可變寬度編碼,每個 Unicode 碼點會使用 1、2、3 或 4 個位元組。

繫結會為字串強制執行有效的 UTF-8,因此字串不適合用於任意二進制資料。請參閱「我應該使用 string 還是 vector<byte>」。

由於長度上限宣告的目的是在 FIDL 訊息的總位元組大小上提供可輕鬆計算的上限,因此 string 上限會指定欄位中的最大位元組數。為確保安全,您通常會想預算 (4 bytes · code points in string)。(如果您確定文字只使用單字節 ASCII 範圍內的程式碼點,例如電話號碼或信用卡號碼,則每個程式碼點 1 個位元組就足夠了)。

字串中有多少個代碼點?這個問題的答案可能很複雜,尤其是使用者產生的字串內容,因為 Unicode 程式碼點與使用者認為的「字元」之間不一定一對一對應。

例如,字串

會以單一使用者感知的「字元」呈現,但實際上包含兩個程式碼點:

1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)

在 Unicode 術語中,這種使用者感知的「字元」稱為字母叢集

單一字元簇可包含任意多個代碼點。請參考以下較長的範例:

á🇨🇦b👮🏽‍♀️

如果系統和字型支援此功能,您應該會在畫面上看到四個字元簇

1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone

這四個表音符號叢集會編碼為 十個程式碼點

 1. LATIN SMALL LETTER A (U+0061)
 2. COMBINING ACUTE ACCENT (U+0301)
 3. REGIONAL INDICATOR SYMBOL LETTER C (U+1F1E8)
 4. REGIONAL INDICATOR SYMBOL LETTER A (U+1F1E6)
 5. LATIN SMALL LETTER B (U+0062)
 6. POLICE OFFICER (U+1F46E)
 7. EMOJI MODIFIER FITZPATRICK TYPE-4 (U+1F3FD)
 8. ZERO WIDTH JOINER (U+200D)
 9. FEMALE SIGN (U+2640)
10. VARIATION SELECTOR-16 (U+FE0F)

在 UTF-8 中,這個字串會佔用 28 個位元組

從這個範例可知,如果應用程式的 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 = strict 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;
    });
};

不過,現在已棄用此模式,改用錯誤語法在信封中內嵌小值帶來的效能優勢已淘汰,現在則普遍採用對聯集的低階支援。

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

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

安全性注意事項:同樣地,向用戶端回報堆疊追蹤或例外訊息,可能會不經意洩漏特權資訊。

本地化字串和錯誤訊息

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

如果所有訊息都很簡單且未指定參數,請使用 enum 來回報錯誤和一般 UI 字串。如要傳送更詳細的訊息,並附上名稱、數字和位置等參數,請使用 tablexunion,並將參數做為字串或數字欄位傳遞。

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

不過,這種簡單做法有一些嚴重的缺點:

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

除非您要建構與單一 UI 實作緊密結合的專門服務,否則不應在 FIDL 服務中公開可供使用者查看的 UI 字串。

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

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

  • 是否有實質的封裝邊界?如果一組參數具有某種超出此方法的凝聚力,因此以單一單位傳遞會比較合理,您可能就會想將這些參數封裝在結構體中。(希望您在開始設計通訊協定時,已依照上述「一般建議」來識別這些緊密相關的群組,並在早期著重於這些類型)。

  • 除了呼叫的方法之外,結構體是否對其他任何內容都很有用?如果不是,請考慮個別傳遞參數。

  • 您是否在許多方法中重複使用相同的參數組合?如果是這樣,請考慮將這些參數分組為一或多個結構。您也可以考慮這些參數是否代表協定中的某些重要概念,因此重複出現。

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

  • 是否有同時一律為空值或非空值的參數群組?如果是這樣,請考慮將這些參數分組為可為空的結構體,以便在通訊協定中強制執行不變量。舉例來說,上述定義的 FrobinateResult 結構體包含的值,在 errorMyError.OK 的同時,一律為空值。

我該使用 string 還是 vector<byte>

在 FIDL 中,string 資料必須是有效的 UTF-8,也就是說字串可以代表 Unicode 程式碼點序列,但無法代表任意二進位資料。相反地,vector<byte>array<byte, N> 可代表任意二進位資料,且不會暗示萬國碼。

使用 string 處理文字資料:

  • 請使用 string 代表套件名稱,因為套件名稱必須是有效的 UTF-8 字串 (含特定排除字元)。

  • 請使用 string 代表套件中的檔案名稱,因為套件中的檔案名稱必須是有效的 UTF-8 字串 (某些字元除外)。

  • 媒體編碼器名稱會從有效 UTF-8 字串的固定字彙中選取,因此請使用 string 代表媒體編碼器名稱。

  • 請使用 string 代表 HTTP 方法,因為 HTTP 方法由固定的字元組成,這些字元一律為有效的 UTF-8。

針對小型非文字資料,請使用 vector<byte>array<byte, N>

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

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

  • 請將 array<byte, 16> 用於 UUID,因為 UUID 幾乎是任意的二進位資料。

使用共用記憶體基本元素處理 blob:

  • 當完全緩衝資料時,請使用 zx.Handle:VMO 處理圖片和 (大型) protobuf。
  • 請將 zx.Handle:SOCKET 用於音訊和影片串流,因為資料可能會隨時間到達,或者在資料尚未完全寫入或可用時,處理資料會比較合理。

該使用 vector 還是 array

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

使用 vector 處理變長資料:

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

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

  • 請使用 array 表示 MAC 位址,因為 MAC 位址的長度一律為六個位元組。

我該使用 struct 還是 table

結構體和資料表都代表具有多個命名欄位的物件。差異在於結構體在傳輸格式中具有固定的版面配置,也就是說,如果要修改結構體,就必須破壞二進位相容性。相較之下,資料表在線格格式中具有彈性的版面配置,這表示欄位「可以」隨時間新增至資料表,而不會破壞二進位相容性。

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

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

我該如何表示常數?

您可以透過三種方式表示常數,具體取決於您擁有的常數類型:

  1. 使用 const 處理特殊值,例如 PIMAX_NAME_LEN
  2. 如果值是集合的元素,例如媒體播放器的重複模式:OFFSINGLE_TRACKALL_TRACKS,請使用 enum
  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 來組合批次。

列舉

如果列舉值組合受到 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 定義了三個值:值為 1RUNNING、值為 2STOPPING,以及值為 3STOPPED

有兩種情況適合使用 0 值:

位元

如果通訊協定含有位元欄位,請使用 bits 值表示其值 (詳情請參閱 RFC-0025:"位元旗標")。

例如:

type InfoFeaturesHex = strict bits : uint32 {
    WLAN = 0x00000001; // If present, this device represents WLAN hardware
    SYNTH = 0x00000002; // If present, this device is synthetic (not backed by h/w)
    LOOPBACK = 0x00000004; // If present, this device receives all messages it sends
};

這表示 InfoFeatures 位元欄位是由未經編號的 32 位元整數支援,然後繼續定義所使用的三個位元。

您也可以使用 0b 符號,以二進位 (而非十六進位) 表示值:

type InfoFeaturesBits = strict bits : uint32 {
    WLAN = 0b00000001; // If present, this device represents WLAN hardware
    SYNTH = 0b00000010; // If present, this device is synthetic (not backed by h/w)
    LOOPBACK = 0b00000100; // If present, this device receives all messages it sends
};

這與前一個範例相同。

我該使用 resource 嗎?

FIDL 編譯器會強制執行任何已包含 resource 的類型,並標示為此類型。

如果 flexible 類型不含資源,但日後可能會包含資源,則應預先新增 resource 修飾符,以免日後難以進行轉換。這種情況很少發生:根據經驗,大多數訊息都不會包含資源,因此在通訊協定中傳遞資源時,必須小心謹慎,並事先做好規劃。

我應該在類型上使用 strict 還是 flexible

將類型標示為 flexible 可處理目前 FIDL 結構定義未知的資料,建議用於日後可能會新增或移除成員的類型 (例如設定、中繼資料或錯誤)。對於現有型別,您一律可以在 strictflexible 之間進行軟轉換

在類型允許的情況下,一律指定此修飾符是時尚的做法。Fuchsia 專案會透過 Linter 檢查強制執行此樣式。

使用 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 通訊協定中重複出現的幾種良好設計模式。

我應該在方法和事件中使用 strict 還是 flexible

將方法或事件標示為 flexible 後,當不同元件可能在不同版本中建構時,您就能更輕鬆地處理移除方法或事件的作業,例如,某些元件認為方法或事件存在,但與其通訊的其他元件則不存在。由於一般來說,我們都希望通訊協定具有彈性,因此建議您為方法和事件選擇 flexible,除非有充分理由選擇 strict

將方法設為 flexible 不會對單向方法或事件造成額外負擔。對於雙向方法,選擇 flexible 會為訊息增加少許額外負擔 (16 個位元組或更少),並可能增加一些訊息解碼時間。整體而言,讓雙向方法具備彈性所需的成本應該很低,幾乎不會影響所有用途。

只有在方法和事件對通訊協定的正確行為至關重要,且接收端缺少該方法或事件的情況嚴重到兩端之間的所有通訊都應中止並關閉連線時,才應將方法和事件設為 strict

這對於前饋資料流的設計特別實用。請考慮使用這個記錄器通訊協定,因為它支援可安全處理含有個人識別資訊 (PII) 的記錄模式。它會使用前饋模式新增記錄,讓用戶端能夠依序啟動多項作業,不必等待往返時間,並在結束時清除待處理的作業。

open protocol Logger {
    flexible AddRecord(struct {
        message string;
    });
    strict EnablePIIMode();
    flexible DisablePIIMode();
    flexible Flush() -> ();
};

除了 EnablePIIMode 以外,這裡的所有方法都是 flexible。請考慮如果伺服器無法辨識任何一種方法,會發生什麼情況:

  • AddRecord:伺服器無法將資料新增至記錄輸出內容。傳送應用程式會正常運作,但其記錄的記錄檔就沒那麼實用了。雖然不方便,但很安全。
  • EnablePIIMode:伺服器無法啟用 PII 模式,表示可能無法採取安全防護措施,並洩漏 PII。這是嚴重的問題,因此如果伺服器無法辨識這個方法,建議關閉管道。
  • DisablePIIMode:伺服器會針對不需要記錄 PII 的訊息採取不必要的安全防護措施。這對嘗試讀取記錄檔的使用者來說可能不方便,但對系統來說是安全的。
  • Flush:伺服器無法依要求清除記錄,這可能會造成不便,但仍是安全的。

如要讓這個通訊協定具備完全彈性,另一種設計方式是將 EnablePIIMode 設為雙向方法 (flexible EnablePIIMode() -> ();),讓用戶端能夠找出伺服器是否沒有該方法。請注意,這會為用戶端帶來額外的彈性。在這種設定下,用戶端可以選擇是否要回應伺服器未偵測到 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;
    });
};

為了處理失敗情況,如果 Codec2 管道在事件到達前關閉,用戶端會等待 OnReady 事件,並採取其他動作。

不過,如果要求可能會成功,則任何一種成功信號都可能造成傷害,因為信號可讓用戶端區分不同失敗模式,而這些模式通常應以相同方式處理。舉例來說,如果服務在建立連線後立即失敗,用戶端應以與無法連線的服務相同方式處理。在上述兩種情況下,服務都無法使用,用戶端應產生錯誤,或找出其他方式來完成任務。

流量控制

FIDL 訊息會由核心緩衝。如果某個端點產生的訊息數量超過另一個端點所消耗的訊息數量,這些訊息就會在核心中累積,占用記憶體,導致系統更難復原。相反地,設計良好的通訊協定應限制訊息產生頻率,以符合訊息使用率,這項屬性稱為「流量控制」

流程控制是一個廣泛且複雜的主題,且有許多有效的設計模式。本節將討論一些較常見的流程控制模式,但不包含所有模式。模式會依偏好程度遞減排序。如果這些模式其中之一適用於特定用途,就應使用該模式。但如果沒有,協定可以自由使用下方未列出的其他流程控制機制。

偏好使用 pull 來推送

如果沒有仔細設計,伺服器將資料推送至用戶端的通訊協定通常會出現流量控制不佳的問題。提供更完善的流程控管方式之一,就是讓用戶端從伺服器中拉取一個或一組資料。由於用戶端會自然限制伺服器產生資料的速度,並避免遭到伺服器推送的訊息淹沒,因此提取模型具有內建的流程控制。

使用掛起的 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::{format_err, Context as _, Error};
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 futures::future::join;
use futures::prelude::*;
use std::sync::{Arc, Mutex};

// 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().unwrap();
            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().unwrap().add_line(line);

                    // Because this is now a two-way method, we must use the generated `responder`
                    // to send an in this case empty reply back to the client. This is the mechanic
                    // which syncs the flow rate between the client and server on this method,
                    // thereby preventing the client from "flooding" the server with unacknowledged
                    // work.
                    responder.send().context("Error responding")?;
                    println!("AddLine response sent");
                } //
                InstanceRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        });

    // This await does not complete, and thus the function does not return, unless the server errors
    // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first
    // wait for the updater task to realize the connection has died, then bubble up the error.
    join(request_handler, update_sender()).await.0
}

// A helper enum that allows us to treat a `Instance` service instance as a value.
enum IncomingService {
    Instance(InstanceRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Instance` protocol - this will allow the client to see
    // the server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Instance);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override {
    auto top_left = event.top_left();
    auto bottom_right = event.bottom_right();
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x()
                  << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x()
                  << ", y: " << bottom_right.y() << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

  explicit EventHandler(async::Loop& loop) : loop_(loop) {}

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_addlinemetered::Point(x, y);
}

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a
  // |zx::result| and it must be checked for errors.
  zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.script()) {
    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();
      continue;
    }

    // Draw a line to the canvas by calling the server, using the two points we just parsed
    // above as arguments.
    auto line = ParseLine(action);
    FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y()
                  << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]";

    client->AddLine(line).ThenExactlyOnce(
        [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) {
          // Check if the FIDL call succeeded or not.
          if (!result.is_ok()) {
            // Check that our two-way call succeeded, and handle the error appropriately. In the
            // case of this example, there is nothing we can do to recover here, except to log an
            // error and exit the program.
            FX_LOGS(ERROR) << "Could not send AddLine request: "
                           << result.error_value().FormatDescription();
          }
          FX_LOGS(INFO) << "AddLine response received";

          // Quit the loop, thereby handing control back to the outer loop of actions being iterated
          // over.
          loop.Quit();
        });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  examples_canvas_addlinemetered::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](InstanceImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override {
    auto points = request.line();
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x()
                  << ", y: " << points[1].y() << " }, Point { x: " << points[0].x()
                  << ", y: " << points[0].y() << " }]";

    // Update the bounding box to account for the new line we've just "added" to the canvas.
    auto& bounds = state_.bounding_box;
    for (const auto& point : request.line()) {
      if (point.x() < bounds.top_left().x()) {
        bounds.top_left().x() = point.x();
      }
      if (point.y() > bounds.top_left().y()) {
        bounds.top_left().y() = point.y();
      }
      if (point.x() > bounds.bottom_right().x()) {
        bounds.bottom_right().x() = point.x();
      }
      if (point.y() < bounds.bottom_right().y()) {
        bounds.bottom_right().y() = point.y();
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;

    // Because this is now a two-way method, we must use the generated |completer| to send an in
    // this case empty reply back to the client. This is the mechanic which syncs the flow rate
    // between the client and server on this method, thereby preventing the client from "flooding"
    // the server with unacknowledged work.
    completer.Reply();
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one.
          if (!weak->state_.changed) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box);
          if (!result.is_ok()) {
            return;
          }

          auto top_left = state_.bounding_box.top_left();
          auto bottom_right = state_.bounding_box.bottom_right();
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x()
                        << ", y: " << top_left.y()
                        << " }, bottom_right: Point { x: " << bottom_right.x()
                        << ", y: " << bottom_right.y() << " }";

          // Reset the change tracker.
          state_.changed = false;
        },
        after);
  }

  fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (Wire)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override {
    auto top_left = event->top_left;
    auto bottom_right = event->bottom_right;
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

  explicit EventHandler(async::Loop& loop) : loop_(loop) {}

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y};
}

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine(
    const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a
  // |zx::result| and it must be checked for errors.
  zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  for (const auto& action : conf.script()) {
    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();
      continue;
    }

    // Draw a line to the canvas by calling the server, using the two points we just parsed
    // above as arguments.
    auto line = ParseLine(action);
    FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";

    client->AddLine(line).ThenExactlyOnce(
        [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) {
          // Check if the FIDL call succeeded or not.
          if (!result.ok()) {
            // Check that our two-way call succeeded, and handle the error appropriately. In the
            // case of this example, there is nothing we can do to recover here, except to log an
            // error and exit the program.
            FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string();
          }
          FX_LOGS(INFO) << "AddLine response received";

          // Quit the loop, thereby handing control back to the outer loop of actions being iterated
          // over.
          loop.Quit();
        });

    // Run the loop until the callback is resolved, at which point we can continue from here.
    loop.Run();
    loop.ResetQuit();
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.addlinemetered/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  examples_canvas_addlinemetered::wire::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end)
      : binding_(fidl::BindServer(
            dispatcher, std::move(server_end), this,
            [this](InstanceImpl* impl, fidl::UnbindInfo info,
                   fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
              if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
                FX_LOGS(ERROR) << "Shutdown unexpectedly";
              }
              delete this;
            })),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override {
    auto points = request->line;
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x
                  << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                  << ", y: " << points[0].y << " }]";

    // Update the bounding box to account for the new line we've just "added" to the canvas.
    auto& bounds = state_.bounding_box;
    for (const auto& point : request->line) {
      if (point.x < bounds.top_left.x) {
        bounds.top_left.x = point.x;
      }
      if (point.y > bounds.top_left.y) {
        bounds.top_left.y = point.y;
      }
      if (point.x > bounds.bottom_right.x) {
        bounds.bottom_right.x = point.x;
      }
      if (point.y < bounds.bottom_right.y) {
        bounds.bottom_right.y = point.y;
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;

    // Because this is now a two-way method, we must use the generated |completer| to send an in
    // this case empty reply back to the client. This is the mechanic which syncs the flow rate
    // between the client and server on this method, thereby preventing the client from "flooding"
    // the server with unacknowledged work.
    completer.Reply();
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one.
          if (!weak->state_.changed) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = weak->state_.bounding_box.top_left;
          auto bottom_right = weak->state_.bounding_box.bottom_right;
          fidl::Status status =
              fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right);
          if (!status.ok()) {
            return;
          }
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change tracker.
          weak->state_.changed = false;
        },
        after);
  }

  fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// 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 <lib/async-loop/cpp/loop.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/canvas/addlinemetered/cpp/fidl.h>
#include <examples/fidl/new/canvas/add_line_metered/hlcpp/client/config.h>

#include "lib/fpromise/result.h"

// 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 = 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.
::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.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace, then create an asynchronous client
  // using the newly-established connection.
  examples::canvas::addlinemetered::InstancePtr instance_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(instance_proxy.NewRequest(dispatcher));
  FX_LOGS(INFO) << "Outgoing connection enabled";

  instance_proxy.set_error_handler([&loop](zx_status_t status) {
    FX_LOGS(ERROR) << "Shutdown unexpectedly";
    loop.Quit();
  });

  // Provide a lambda to handle incoming |OnDrawn| events asynchronously.
  instance_proxy.events().OnDrawn = [&loop](
                                        ::examples::canvas::addlinemetered::Point top_left,
                                        ::examples::canvas::addlinemetered::Point 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();
  };

  instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal;
  };

  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 << " }]";

    instance_proxy->AddLine(line, [&](fpromise::result<void, fidl::FrameworkErr> result) {
      if (result.is_error()) {
        // Check that our flexible two-way call was known to the server and handle the case of an
        // unknown method 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) << "Server does not implement AddLine";
      }
      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 <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/canvas/addlinemetered/cpp/fidl.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 examples::canvas::addlinemetered::Instance {
 public:
  // Bind this implementation to an |InterfaceRequest|.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request)
      : binding_(fidl::Binding<examples::canvas::addlinemetered::Instance>(this)),
        weak_factory_(this) {
    binding_.Bind(std::move(request), dispatcher);

    // Gracefully handle abrupt shutdowns.
    binding_.set_error_handler([this](zx_status_t status) mutable {
      if (status != ZX_ERR_PEER_CLOSED) {
        FX_LOGS(ERROR) << "Shutdown unexpectedly";
      }
      delete this;
    });

    // Start the update timer on startup. Our server sends one update per second.
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLine(::std::array<::examples::canvas::addlinemetered::Point, 2> line,
               AddLineCallback callback) override {
    FX_LOGS(INFO) << "AddLine request received: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[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 : 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 |callback| 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.
    callback(fpromise::ok());
    FX_LOGS(INFO) << "AddLine response sent";
  }

  void handle_unknown_method(uint64_t ordinal, bool method_has_response) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << 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 = state_.bounding_box.top_left;
          auto bottom_right = state_.bounding_box.bottom_right;
          binding_.events().OnDrawn(top_left, 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::Binding<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.
  //
  // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached to
  // the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  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.
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Register a handler for components trying to connect to
  // |examples.canvas.addlinemetered.Instance|.
  context->outgoing()->AddPublicService(
      fidl::InterfaceRequestHandler<examples::canvas::addlinemetered::Instance>(
          [dispatcher](fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) {
            // Create an instance of our |InstanceImpl| that destroys itself when the connection
            // closes.
            new InstanceImpl(dispatcher, std::move(request));
          }));

  // 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() -> (); 方法來進行同步處理。每當用戶端準備接收下一個繪圖更新時,就會呼叫這個方法,並透過伺服器的回應指出用戶端可以繼續執行更多要求。

我們現在在兩個方向都設有一些資料流控制。這項通訊協定現在實作了前饋模式,允許在某些同步「commit」呼叫觸發伺服器上的實際工作之前,進行許多未受控的呼叫。這可避免用戶端因工作量過大而癱瘓伺服器。同樣地,伺服器也不再允許傳送無限的 -> OnDrawn(...); 事件:每個事件都必須遵循來自用戶端的信號 (Ready() -> (); 呼叫),表示該事件已準備好執行更多工作。這就是所謂的「節流事件模式」

具體實作必須手動套用部分規則:如果用戶端收到未透過 Ready() -> (); 方法要求的 -> OnDrawn(...); 事件,就必須關閉連線。

FIDL、CML 和領域介面定義如下:

FIDL

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
library examples.canvas.clientrequesteddraw;

/// A point in 2D space.
type Point = struct {
    x int64;
    y int64;
};

/// A line in 2D space.
alias Line = array<Point, 2>;

/// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what
/// the server reports back to the client. These bounds are sufficient to contain all of the
/// lines (inclusive) on a canvas at a given time.
type BoundingBox = struct {
    top_left Point;
    bottom_right Point;
};

/// Manages a single instance of a canvas. Each session of this protocol is responsible for a new
/// canvas.
@discoverable
open protocol Instance {
    /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of
    /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a
    /// time.
    flexible AddLines(struct {
        lines vector<Line>;
    });

    /// Rather than the server randomly performing draws, or trying to guess when to do so, the
    /// client must explicitly ask for them. This creates a bit of extra chatter with the additional
    /// method invocation, but allows much greater client-side control of when the canvas is "ready"
    /// for a view update, thereby eliminating unnecessary draws.
    ///
    /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than
    /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime
    /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call
    /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to
    /// immediately close.
    flexible Ready() -> ();

    /// Update the client with the latest drawing state. The server makes no guarantees about how
    /// often this event occurs - it could occur multiple times per board state, for example.
    flexible -> OnDrawn(BoundingBox);
};

CML

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    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 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::{format_err, Context as _, Error};
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::{anyhow, Context as _, Error};
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 futures::future::join;
use futures::prelude::*;
use std::sync::{Arc, Mutex};

// 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().unwrap();
            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().unwrap(), 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().unwrap();
                    if state.ready == true {
                        return Err(anyhow!("Invalid back-to-back `Ready` requests received"));
                    }

                    state.ready = true;
                    responder.send().context("Error responding")?;
                } //
                InstanceRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        });

    // This line will only be reached if the server errors out. The stream will await indefinitely,
    // thereby creating a long-lived server. Here, we first wait for the updater task to realize the
    // connection has died, then bubble up the error.
    join(request_handler, update_sender()).await.0
}

// A helper enum that allows us to treat a `Instance` service instance as a value.
enum IncomingService {
    Instance(InstanceRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Instance` protocol - this will allow the client to see
    // the server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Instance);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override {
    ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left();
    ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right();
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x()
                  << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x()
                  << ", y: " << bottom_right.y() << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

  explicit EventHandler(async::Loop& loop) : loop_(loop) {}

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::Point(x, y);
}

using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a
  // |zx::result| and it must be checked for errors.
  zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::Client client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fit::result<fidl::Error> result = client->AddLines(batched_lines);
      if (!result.is_ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) {
            // Check if the FIDL call succeeded or not.
            if (result.is_ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y()
                  << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request.lines()) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x()
                    << ", y: " << points[1].y() << " }, Point { x: " << points[0].x()
                    << ", y: " << points[0].y() << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x() < bounds.top_left().x()) {
          bounds.top_left().x() = point.x();
        }
        if (point.y() > bounds.top_left().y()) {
          bounds.top_left().y() = point.y();
        }
        if (point.x() > bounds.bottom_right().x()) {
          bounds.bottom_right().x() = point.x();
        }
        if (point.y() < bounds.bottom_right().y()) {
          bounds.bottom_right().y() = point.y();
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box);
          if (!result.is_ok()) {
            return;
          }

          auto top_left = state_.bounding_box.top_left();
          auto bottom_right = state_.bounding_box.bottom_right();
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x()
                        << ", y: " << top_left.y()
                        << " }, bottom_right: Point { x: " << bottom_right.x()
                        << ", y: " << bottom_right.y() << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

C++ (Wire)

用戶端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/component/incoming/cpp/protocol.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h>

// The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming
// events asynchronously.
class EventHandler
    : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Handler for |OnDrawn| events sent from the server.
  void OnDrawn(
      fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override {
    ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left;
    ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right;
    FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x
                  << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x
                  << ", y: " << bottom_right.y << " }";
    loop_.Quit();
  }

  void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; }

  void handle_unknown_event(
      fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal;
  }

  explicit EventHandler(async::Loop& loop) : loop_(loop) {}

 private:
  async::Loop& loop_;
};

// A helper function that takes a coordinate in string form, like "123,-456", and parses it into a
// a struct of the form |{ in64 x; int64 y; }|.
::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) {
  int64_t x = 0;
  int64_t y = 0;
  size_t index = input.find(',');
  if (index != std::string::npos) {
    std::from_chars(input.data(), input.data() + index, x);
    std::from_chars(input.data() + index + 1, input.data() + input.length(), y);
  }
  return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y};
}

using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>;

// A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it
// into an array of 2 |Point| structs.
Line ParseLine(const std::string& action) {
  auto input = std::string_view(action);
  size_t index = input.find(':');
  if (index != std::string::npos) {
    return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))};
  }
  return {};
}

int main(int argc, const char** argv) {
  FX_LOGS(INFO) << "Started";

  // Retrieve component configuration.
  auto conf = config::Config::TakeFromStartupHandle();

  // Start up an async loop and dispatcher.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a
  // |zx::result| and it must be checked for errors.
  zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>();
  if (!client_end.is_ok()) {
    FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: "
                   << client_end.status_string();
    return -1;
  }

  // Create an instance of the event handler.
  EventHandler event_handler(loop);

  // Create an asynchronous client using the newly-established connection.
  fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler);
  FX_LOGS(INFO) << "Outgoing connection enabled";

  std::vector<Line> batched_lines;
  for (const auto& action : conf.script()) {
    // If the next action in the script is to "PUSH", send a batch of lines to the server.
    if (action == "PUSH") {
      fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines));
      if (!status.ok()) {
        // Check that our one-way call was enqueued successfully, and handle the error
        // appropriately. In the case of this example, there is nothing we can do to recover here,
        // except to log an error and exit the program.
        FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error();
        return -1;
      }

      batched_lines.clear();
      FX_LOGS(INFO) << "AddLines request sent";
      continue;
    }

    // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received
    // from the server.
    if (action == "WAIT") {
      loop.Run();
      loop.ResetQuit();

      // Now, inform the server that we are ready to receive more updates whenever they are
      // ready for us.
      FX_LOGS(INFO) << "Ready request sent";
      client->Ready().ThenExactlyOnce(
          [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>&
                  result) {
            // Check if the FIDL call succeeded or not.
            if (result.ok()) {
              FX_LOGS(INFO) << "Ready success";
            } else {
              FX_LOGS(ERROR) << "Could not send Ready request: " << result.error();
            }

            // Quit the loop, thereby handing control back to the outer loop of actions being
            // iterated over.
            loop.Quit();
          });

      // Run the loop until the callback is resolved, at which point we can continue from here.
      loop.Run();
      loop.ResetQuit();

      continue;
    }

    // Batch a line for drawing to the canvas using the two points provided.
    Line line = ParseLine(action);
    batched_lines.push_back(line);
    FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y
                  << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]";
  }

  // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the
  // referenced bug has been resolved, we can remove the sleep.
  sleep(2);
  return 0;
}

伺服器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

#include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/async/cpp/task.h>
#include <lib/component/outgoing/cpp/outgoing_directory.h>
#include <lib/fidl/cpp/wire/channel.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <src/lib/fxl/macros.h>
#include <src/lib/fxl/memory/weak_ptr.h>

// A struct that stores the two things we care about for this example: the set of lines, and the
// bounding box that contains them.
struct CanvasState {
  // Tracks whether there has been a change since the last send, to prevent redundant updates.
  bool changed = true;
  // Tracks whether or not the client has declared itself ready to receive more updated.
  bool ready = true;
  examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box;
};

// An implementation of the |Instance| protocol.
class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> {
 public:
  // Bind this implementation to a channel.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end)
      : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)),
        weak_factory_(this) {
    // Start the update timer on startup. Our server sends one update per second
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void OnFidlClosed(fidl::UnbindInfo info) {
    if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) {
      FX_LOGS(ERROR) << "Shutdown unexpectedly";
    }
    delete this;
  }

  void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : request->lines) {
      FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x
                    << ", y: " << points[1].y << " }, Point { x: " << points[0].x
                    << ", y: " << points[0].y << " }]";

      // Update the bounding box to account for the new line we've just "added" to the canvas.
      auto& bounds = state_.bounding_box;
      for (const auto& point : points) {
        if (point.x < bounds.top_left.x) {
          bounds.top_left.x = point.x;
        }
        if (point.y > bounds.top_left.y) {
          bounds.top_left.y = point.y;
        }
        if (point.x > bounds.bottom_right.x) {
          bounds.bottom_right.x = point.x;
        }
        if (point.y < bounds.bottom_right.y) {
          bounds.bottom_right.y = point.y;
        }
      }
    }

    // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn|
    // event.
    state_.changed = true;
  }

  void Ready(ReadyCompleter::Sync& completer) override {
    FX_LOGS(INFO) << "Ready request received";

    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two
    // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid
    // state, and should be aborted immediately.
    if (state_.ready == true) {
      FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received";
    }

    state_.ready = true;
    completer.Reply();
  }

  void handle_unknown_method(
      fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata,
      fidl::UnknownMethodCompleter::Sync& completer) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal;
  }

 private:
  // Each scheduled update waits for the allotted amount of time, sends an update if something has
  // changed, and schedules the next update.
  void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) {
    async::PostDelayedTask(
        dispatcher,
        [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] {
          // Halt execution if the binding has been deallocated already.
          if (!weak) {
            return;
          }

          // Schedule the next update if the binding still exists.
          weak->ScheduleOnDrawnEvent(dispatcher, after);

          // No need to send an update if nothing has changed since the last one, or the client has
          // not yet informed us that it is ready for more updates.
          if (!weak->state_.changed || !weak->state_.ready) {
            return;
          }

          // This is where we would draw the actual lines. Since this is just an example, we'll
          // avoid doing the actual rendering, and simply send the bounding box to the client
          // instead.
          auto top_left = weak->state_.bounding_box.top_left;
          auto bottom_right = weak->state_.bounding_box.bottom_right;
          fidl::Status status =
              fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right);
          if (!status.ok()) {
            return;
          }
          FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x
                        << ", y: " << top_left.y
                        << " }, bottom_right: Point { x: " << bottom_right.x
                        << ", y: " << bottom_right.y << " }";

          // Reset the change and ready trackers.
          state_.ready = false;
          weak->state_.changed = false;
        },
        after);
  }

  fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_;
  CanvasState state_ = CanvasState{};

  // Generates weak references to this object, which are appropriate to pass into asynchronous
  // callbacks that need to access this object. The references are automatically invalidated
  // if this object is destroyed.
  fxl::WeakPtrFactory<InstanceImpl> weak_factory_;
};

int main(int argc, char** argv) {
  FX_LOGS(INFO) << "Started";

  // The event loop is used to asynchronously listen for incoming connections and requests from the
  // client. The following initializes the loop, and obtains the dispatcher, which will be used when
  // binding the server implementation to a channel.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Create an |OutgoingDirectory| instance.
  //
  // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This
  // directory is where the outgoing FIDL protocols are installed so that they can be provided to
  // other components.
  component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher);

  // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle.
  // The startup handle is a handle provided to every component by the system, so that they can
  // serve capabilities (e.g. FIDL protocols) to other components.
  zx::result result = outgoing.ServeFromStartupInfo();
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string();
    return -1;
  }

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>(
      [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) {
        // Create an instance of our InstanceImpl that destroys itself when the connection closes.
        new InstanceImpl(dispatcher, std::move(server_end));
      });
  if (result.is_error()) {
    FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string();
    return -1;
  }

  // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up.
  FX_LOGS(INFO) << "Listening for incoming connections";
  loop.Run();
  return 0;
}

HLCPP

用戶端

// 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 <lib/async-loop/cpp/loop.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <charconv>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.h>
#include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h>

// 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 = x, .y = 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.
  async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
  async_dispatcher_t* dispatcher = loop.dispatcher();

  // Connect to the protocol inside the component's namespace, then create an asynchronous client
  // using the newly-established connection.
  examples::canvas::clientrequesteddraw::InstancePtr instance_proxy;
  auto context = sys::ComponentContext::Create();
  context->svc()->Connect(instance_proxy.NewRequest(dispatcher));
  FX_LOGS(INFO) << "Outgoing connection enabled";

  instance_proxy.set_error_handler([&loop](zx_status_t status) {
    FX_LOGS(ERROR) << "Shutdown unexpectedly";
    loop.Quit();
  });

  // Provide a lambda to handle incoming |OnDrawn| events asynchronously.
  instance_proxy.events().OnDrawn =
      [&loop](::examples::canvas::clientrequesteddraw::Point top_left,
              ::examples::canvas::clientrequesteddraw::Point 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();
      };

  instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) {
    FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal;
  };

  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") {
      instance_proxy->AddLines(batched_lines);
      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";
      instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) {
        if (result.is_error()) {
          // Check that our flexible two-way call was known to the server and handle the case of an
          // unknown method 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) << "Server does not implement AddLine";
        }

        FX_LOGS(INFO) << "Ready success";

        // 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 <lib/async-loop/cpp/loop.h>
#include <lib/async-loop/default.h>
#include <lib/async/cpp/task.h>
#include <lib/fidl/cpp/binding.h>
#include <lib/sys/cpp/component_context.h>
#include <lib/syslog/cpp/macros.h>
#include <unistd.h>

#include <examples/canvas/clientrequesteddraw/cpp/fidl.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;
};

using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>;

// An implementation of the |Instance| protocol.
class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance {
 public:
  // Bind this implementation to an |InterfaceRequest|.
  InstanceImpl(async_dispatcher_t* dispatcher,
               fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request)
      : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)),
        weak_factory_(this) {
    binding_.Bind(std::move(request), dispatcher);

    // Gracefully handle abrupt shutdowns.
    binding_.set_error_handler([this](zx_status_t status) mutable {
      if (status != ZX_ERR_PEER_CLOSED) {
        FX_LOGS(ERROR) << "Shutdown unexpectedly";
      }
      delete this;
    });

    // Start the update timer on startup. Our server sends one update per second.
    ScheduleOnDrawnEvent(dispatcher, zx::sec(1));
  }

  void AddLines(std::vector<Line> lines) override {
    FX_LOGS(INFO) << "AddLines request received";
    for (const auto& points : 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(ReadyCallback callback) 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;
    callback(fpromise::ok());
  }

  void handle_unknown_method(uint64_t ordinal, bool method_has_response) override {
    FX_LOGS(WARNING) << "Received an unknown method with ordinal " << 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 = state_.bounding_box.top_left;
          auto bottom_right = state_.bounding_box.bottom_right;
          binding_.events().OnDrawn(top_left, 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::Binding<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.
  //
  // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached
  // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration.
  async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread);
  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.
  auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory();

  // Register a handler for components trying to connect to
  // |examples.canvas.clientrequesteddraw.Instance|.
  context->outgoing()->AddPublicService(
      fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>(
          [dispatcher](
              fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) {
            // Create an instance of our |InstanceImpl| that destroys itself when the connection
            // closes.
            new InstanceImpl(dispatcher, std::move(request));
          }));

  // 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),明確與伺服器同步處理。回覆內容可能為空白訊息,也可能包含提交序列是否成功的相關資訊。在更複雜的通訊協定中,單向訊息會以指令物件的聯集表示,而非個別方法呼叫;請參閱下方的指令聯集模式

使用前饋資料流程的通訊協定,可搭配樂觀錯誤處理策略順利運作。請不要讓伺服器以狀態值回覆每個方法,因為這會促使用戶端等待每則訊息之間的來回傳輸,請改為只在方法因非用戶端可控的因素而失敗時,才加入狀態回應。如果用戶端傳送的訊息是用戶端應知為無效的訊息 (例如參照無效的用戶端指派識別碼),請關閉連線以傳送錯誤信號。如果用戶端傳送的訊息無效,但用戶端無法得知這點,請提供回應,以便傳送成功或失敗信號 (這需要用戶端同步處理),或者記住錯誤,並忽略後續的依附要求,直到用戶端同步處理並以某種方式復原錯誤為止。

範例:

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 連線,將 FIDL 用戶端連線至對應伺服器的其他 (非 @discoverable) 方式!

通訊協定端點是一般 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 lazy_static::lazy_static;
use regex::Regex;

use fidl_examples_keyvaluestore_additerator::{
    Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
    StoreRequestStream, WriteError,
};
use fuchsia_async as fasync;
use std::collections::btree_map::Entry;
use std::collections::BTreeMap;
use std::ops::Bound::*;
use std::sync::{Arc, Mutex};

lazy_static! {
    static ref KEY_VALIDATION_REGEX: Regex =
        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().unwrap().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().unwrap();
                            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().unwrap(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

                    // The `iterate` handler does a quick check to see that the request is valid,
                    // then spins up a separate worker task to serve the newly minted `Iterator`
                    // protocol instance, allowing this call to return immediately and continue the
                    // request stream with other work.
                    responder
                        .send(iterate(store.clone(), starting_at, iterator.into_stream()))
                        .context("error sending reply")?;
                    println!("Iterate response sent");
                } //
                StoreRequest::_UnknownMethod { ordinal, .. } => {
                    println!("Received an unknown method with ordinal {ordinal}");
                }
            }
            Ok(())
        })
        .await
}

// A helper enum that allows us to treat a `Store` service instance as a value.
enum IncomingService {
    Store(StoreRequestStream),
}

#[fuchsia::main]
async fn main() -> Result<(), Error> {
    println!("Started");

    // Add a discoverable instance of our `Store` protocol - this will allow the client to see the
    // server and connect to it.
    let mut fs = ServiceFs::new_local();
    fs.dir("svc").add_fidl_service(IncomingService::Store);
    fs.take_and_serve_directory_handle()?;
    println!("Listening for incoming connections");

    // The maximum number of concurrent clients that may be served by this process.
    const MAX_CONCURRENT: usize = 10;

    // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit.
    fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| {
        run_server(stream).unwrap_or_else(|e| println!("{:?}", e))
    })
    .await;

    Ok(())
}

C++ (自然)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.

C++ (Wire)

用戶端

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

伺服器

// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.

HLCPP

用戶端

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

伺服器

// TODO(https://fxbug.dev/42060656): HLCPP implementation.

隱私保護設計

通訊協定中的用戶端和伺服器通常會存取不同的敏感資料組合。隱私權或安全性問題可能會因為在通訊協定中不經意洩漏過多資料而發生。

設計通訊協定時,請特別留意通訊協定中的欄位是否:

  • 包含姓名、電子郵件地址或付款詳細資料等個人識別資訊。
  • 由使用者提供,因此可能含有個人資訊。例如裝置名稱和註解欄位。
  • 做為可在供應商、使用者、裝置或重設作業之間建立關聯的獨特 ID。例如序號、MAC 位址、IP 位址和全球帳戶 ID。

這類欄位會經過徹底審查,且包含這類欄位的通訊協定可能會受到限制。請確認您的通訊協定不含多餘的資訊。

如果 API 的用途需要個人或可連結的資料,而其他用途則不需要,建議您使用兩種不同的通訊協定,以便分別控管較敏感用途的存取權。

請考慮以下兩個假想的例子,說明 API 設計選項可能造成的隱私權侵害情形:

範例 1:周邊控制 API 中的序號

請考慮使用包含 USB 外接裝置序號的外接裝置控制 API。序號不含個人資料,但它是一種非常穩定的 ID,可輕鬆進行關聯。在這個 API 中加入序號會導致許多隱私權疑慮:

  • 任何可存取 API 的用戶端,都可以將使用相同 Fuchsia 裝置的不同帳戶關聯起來。
  • 任何可存取 API 的用戶端,都可以將帳戶中的不同人物角色建立關聯。
  • 不同軟體供應商可能會勾結,瞭解是否有使用者在同一裝置上使用這些軟體。
  • 如果周邊裝置在裝置之間移動,任何可存取 API 的用戶端都能將周邊裝置與共用該裝置的使用者建立關聯。
  • 如果周邊裝置已售出,可存取 API 的用戶端就能將周邊裝置的舊擁有者與新擁有者連結起來。
  • 部分製造商會在序號中編碼,這可能讓能存取 API 的用戶端推斷使用者購買周邊的時間或地點。

在本例中,序號的意圖是讓用戶端偵測相同 USB 外接裝置何時重新連線。為了符合這項意圖,您必須使用穩定的 ID,但不必使用全域 ID。不同的用戶端不需要收到相同的 ID,同一個用戶端也不需要在不同的 Fuchsia 裝置上收到相同的 ID,而且 ID 也不需要在出廠重設事件中保持不變。

在這個範例中,您可以傳送只保證在單一裝置上為單一用戶端提供穩定性的 ID,這也是不錯的替代做法。這個 ID 可能是周邊裝置序號、Fuchsia 裝置 ID 和連線路徑名稱的雜湊值。

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

請考慮使用裝置設定 API,其中包含用於協助設定裝置的手機型號。在大多數情況下,手機的型號字串是由原始設備製造商 (OEM) 設定,但某些手機會將使用者提供的裝置名稱回報為型號。這會導致許多模型字串包含使用者的真實姓名或化名。因此,這個 API 可能會在不同身分或裝置之間建立使用者關聯。即使使用者未提供私密資訊,罕見或未發布版本的模型字串仍可能會揭露私密資訊。

在某些情況下,使用模型字串可能比較適當,但請限制哪些用戶端可以存取 API。或者,API 可以使用從未由使用者控制的欄位,例如製造商字串。另一個替代方案是將型號字串與熱門手機型號的許可清單進行比對,並將罕見的型號字串替換為一般字串。

由用戶端指派的 ID

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

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

用戶端指派的 ID 模式可避免這些問題,因為用戶端會將 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 會使用這個模式,讓用戶端傳送任意數量的環境變數。

此模式的更精細版本會建立代表交易的通訊協定,通常稱為「分離式通訊協定」:

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 訊息的數量,以提供流程控管。請注意,疊代器不需要要求「done」回應,因為伺服器可以傳回空白向量,然後在完成時關閉疊代器。

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

type Token = struct {
    opaque array<uint8, 16>;
};

protocol TokenBasedGetter {
    /// If token is null, fetch the first N entries. If token is not null,
    /// return the N items starting at token. Returns as many entries as it can
    /// in results and populates next_token if more entries are available.
    GetEntries(struct {
        token box<Token>;
    }) -> (struct {
        entries vector<Entry>;
        next_token box<Token>;
    });
};

當伺服器可以將所有分頁狀態託管給用戶端,因此不再需要維護分頁狀態時,這個模式就特別實用。伺服器應記錄用戶端是否可以保留權杖,並在通訊協定例項中重複使用。安全性注意事項:無論是哪種情況,伺服器都必須驗證用戶端提供的權杖,確保用戶端的存取權僅限於其分頁結果,且不包含其他用戶端的結果。

事件組關聯

使用用戶端指派的 ID 時,用戶端會使用 ID 來識別伺服器所持有的物件,這些 ID 僅在用戶端與伺服器的連線內容中才有意義。不過,某些用途需要在不同用戶端之間建立關聯物件。例如,在 fuchsia.ui.scenic 中,用戶端大多會使用用戶端指派的 ID 與場景圖中的節點互動。不過,從其他程序匯入節點時,需要在跨程序邊界中將參照與該節點建立關聯。

事件組合相關模式會使用前饋資料流,並仰賴核心提供必要的安全性,解決這個問題。首先,想要匯出物件的用戶端會建立 zx::eventpair,並將其中一個糾結事件連同用戶端指派的物件 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 屬性。

取消事件配對

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

對於沒有專屬管道的作業,也有類似的用途。舉例來說,fuchsia.net.http.Loader 通訊協定有 Fetch 方法,可用來啟動 HTTP 要求。一旦 HTTP 交易完成,伺服器就會以 HTTP 回應回覆要求,這可能需要相當長的時間。用戶端沒有明顯的方法可以取消要求,除非關閉整個 Loader 通訊協定,但這可能會取消許多其他未完成的要求。

事件對取消模式可解決這個問題,方法是讓用戶端將 zx::eventpair 中其中一個糾結事件納入方法的參數。接著,伺服器會監聽 ZX_EVENTPAIR_PEER_CLOSED,並在該信號確認時取消作業。使用 zx::eventpair 比使用 zx::event 或其他信號更佳,因為 zx::eventpair 方法會隱含處理用戶端當機或其他解構情況。當客戶端保留的糾結事件遭到刪除時,核心會產生 ZX_EVENTPAIR_PEER_CLOSED

空白通訊協定

有時空白的通訊協定也能提供價值。舉例來說,建立物件的函式也可能會收到 server_end:FooController 參數。呼叫端會提供此空白通訊協定的實作方式:

protocol FooController {};

FooController 不含任何用於控制已建立物件的方法,但伺服器可使用通訊協定上的 ZX_CHANNEL_PEER_CLOSED 信號,觸發物件的銷毀作業。日後,這個通訊協定可能會透過控制已建立物件的方法擴充。

控管設定類資料

通常,伺服器會公開用戶端可修改的設定。建議使用 table 來表示這類設定。例如,fuchsia.accessibility 程式庫定義了:

type Settings = table {
    1: magnification_enabled bool;
    2: magnification_zoom_factor float32;
    3: screen_reader_enabled bool;
    4: color_inversion_enabled bool;
    5: color_correction ColorCorrection;
    6: color_adjustment_matrix array<float32, 9>;
};

(為了方便閱讀,我們省略了註解)。

您可以透過多種方式讓客戶變更這些設定。

部分更新方法會公開 Update 方法,該方法會採用部分設定值,並只在部分值中存在欄位時變更欄位。

protocol TheManagerOfSomeSorts {
    /// Description how the update modifies the behavior.
    ///
    /// Only fields present in the settings value will be changed.
    Update(struct {
        settings Settings;
    }) -> (struct {
        args Args;
    });
};

replace 方法會公開 Replace 方法,該方法會採用完整的設定值,並將設定變更為新提供的設定。

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

應避免的事項:

  • 請勿在部分更新或取代方法中使用動詞 SetOverride,因為這會造成語意模糊。

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

  • 請勿移除含有魔術值 (例如 -1) 的設定。而是不顯示該設定欄位來移除設定。

參照聯集變數和表格欄位

參照類型欄位通常很有用,例如參照資料表的一或多個欄位,或是參照特定的聯集變化版本。

請考慮一個 API,該 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;
    });
};

接著,請考慮指令聯集。在複雜的情況下,伺服器可能需要說明其支援的指令。在這種情況下,擁有一個成員與 union 變化版本一對一相符的平行 enum,可以成為建立 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

  • 請使用 union 名稱和後綴 Variant (單數) 命名 enum,列出所有變化版本。每個成員值應為所描述變數的序數。

  • unionenum 之間的彈性必須一致,也就是說,如果 unionstrictenum 也必須是 strict

反面模式

本節將說明幾種反模式:經常提供負面價值的設計模式。學習辨識這些模式,是避免錯誤使用這些模式的第一步。

推送設定:盡量避免

Fuchsia 平台通常偏好提取語意。元件語意提供了一個重要例子:假設功能會從元件中提取,讓元件啟動作業以延遲方式進行,並從能力路徑的導向圖中推斷元件關閉順序。

使用 FIDL 通訊協定將設定從一個元件推送至另一個元件的設計,看似簡單,因此很誘人。當元件 A 將政策推送至元件 B 的業務邏輯時,就會發生這種情況。這會導致平台誤解依附元件關係:A 並未自動啟動以支援 B 的功能,因此 A 可能會在 B 執行完商業邏輯前關閉。這會導致使用弱依附元件和幻影反向依附元件來產生所需行為的因應措施;如果是拉取政策而非推送政策,則這一切都會變得更簡單。

盡可能採用可拉取政策而非推送政策的設計。

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

理想情況下,用戶端會使用由 FIDL 編譯器產生的語言專屬用戶端程式庫,與 FIDL 中定義的通訊協定介接。雖然這種做法可讓 Fuchsia 為大量目標語言提供高品質支援,但有時通訊協定太低階,無法直接編程。在這種情況下,建議您提供手寫用戶端程式庫,讓其與相同的基礎通訊協定介接,但更容易正確使用。

舉例來說,fuchsia.io 有一個用戶端程式庫 libfdio.so,可為通訊協定提供類似 POSIX 的前端。預期 POSIX 風格 open/close/read/write 介面的用戶端,可以連結至 libfdio.so,並以最少的修改來使用 fuchsia.io 通訊協定。這個用戶端程式庫提供的價值在於,程式庫會在現有程式庫介面和基礎 FIDL 通訊協定之間進行調整。

另一種提供正面價值的用戶端程式庫是架構。架構是廣泛的用戶端程式庫,可為應用程式的大部分提供結構。一般來說,架構會針對多種不同的通訊協定提供大量的抽象化。舉例來說,Flutter 是一個架構,可視為 fuchsia.ui 通訊協定的擴充用戶端程式庫。

無論通訊協定是否有相關聯的用戶端程式庫,都應完整記錄 FIDL 通訊協定。獨立的軟體工程師團隊應能夠直接瞭解並正確使用該通訊協定,而無須逆向工程用戶端程式庫。如果通訊協定有用戶端程式庫,則應清楚記錄通訊協定的低階和精細層面,以便您建立用戶端程式庫。

用戶端程式庫的主要難處在於,需要為每種目標語言維護用戶端程式庫,這往往意味著,用戶端程式庫在較不常見的語言中缺少 (或品質較低)。用戶端程式庫也傾向於將基礎通訊協定固定化,因為這會導致每個用戶端都以完全相同的方式與伺服器互動。伺服器會逐漸預期這個確切的互動模式,如果用戶端偏離用戶端程式庫使用的模式,就會無法正常運作。

為了在 Fuchsia SDK 中納入用戶端程式庫,我們應至少以兩種語言提供程式庫的實作項目。

服務中心:請謹慎使用

服務中樞是一種 Discoverable 通訊協定,可讓您探索多種其他通訊協定,通常會使用明確的名稱:

// BAD
@discoverable
protocol ServiceHub {
    GetFoo(resource struct {
        foo server_end:Foo;
    });
    GetBar(resource struct {
        bar server_end:Bar;
    });
    GetBaz(resource struct {
        baz server_end:Baz;
    });
    GetQux(resource struct {
        qux server_end:Qux;
    });
};

特別是在無狀態的情況下,ServiceHub 通訊協定提供的價值並沒有比直接讓個別通訊協定服務可供探索更高:

@discoverable
protocol Foo {};

@discoverable
protocol Bar {};

@discoverable
protocol Baz {};

@discoverable
protocol Qux {};

無論如何,用戶端都能與列舉的服務建立連線。在後一種情況下,用戶端可以透過整個系統用於探索服務的一般機制,探索相同的服務。使用一般機制可讓核心平台將適當政策套用至探索功能。

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

過度以物件為導向的設計:否

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

  • 不同通訊協定例項之間的訊息排序未定義。透過單一通訊協定傳送的訊息會以 FIFO 順序 (在每個方向) 處理,但透過不同管道傳送的訊息會競爭。如果用戶端和伺服器之間的互動分散在多個管道,在訊息發生非預期重新排序的情況時,發生錯誤的可能性就會更高。

  • 每個通訊協定例項都會產生成本,包括核心資源、等待佇列和排程。雖然 Fuchsia 的設計可擴充至大量管道,但成本會累積在整個系統中,而且建立大量物件來模擬系統中的每個邏輯物件,會對系統造成沉重負擔。

  • 錯誤處理和拆卸作業更加複雜,因為錯誤和拆卸狀態的數量會隨著互動中涉及的通訊協定例項數量呈指數增長。使用單一通訊協定例項時,用戶端和伺服器都能透過關閉通訊協定,乾淨地關閉互動。在多個通訊協定例項中,互動可能會進入互動部分關閉的狀態,或是雙方對關閉狀態的觀點不一致。

    • 跨通訊協定邊界進行協調的複雜度高於單一通訊協定,因為多個通訊協定需要允許不同用戶端使用不同通訊協定的可能性,而這些用戶端可能不會完全互信。

不過,有些用途需要將功能分割成多個通訊協定:

  • 提供個別通訊協定有助於提升安全性,因為某些用戶端可能只能存取其中一種通訊協定,因此與伺服器的互動會受到限制。

  • 您也可以更輕鬆地從個別執行緒使用個別通訊協定。舉例來說,一個通訊協定可能會繫結至一個執行緒,而另一個通訊協定則可能會繫結至另一個執行緒。

  • 用戶端和伺服器會為通訊協定中的每個方法支付 (少量) 費用。如果每次只需要少數幾個較小的通訊協定,那麼,與其使用包含所有可能方法的單一巨型通訊協定,不如使用多個較小的通訊協定,因為前者效率較低。

  • 有時,伺服器因素會沿著方法邊界保持清晰狀態。在這種情況下,請考慮將通訊協定分割成沿著相同邊界劃分的較小通訊協定,以便提供可與個別狀態互動的個別通訊協定。

避免過度以物件為導向的好方法,就是使用由用戶端指派的 ID,在通訊協定中模擬邏輯物件。這個模式可讓用戶端透過單一通訊協定與可能大量的邏輯物件互動。

使用魔術值指定缺少值:否

我們通常會指示伺服器設定某些狀態,但也允許移除狀態。以下使用魔術值指示移除作業:

// BAD
protocol View3 {
    ReplaceHitRegion(struct {
        id uint64;
        // Set to { -1, -1 } to remove.
        size fuchsia.math.Rect;
    });
    ReplaceName(struct {
        // Set to "" to remove.
        name string;
    });
};

不過,FIDL 提供許多資料類型的選用項目。使用選用性可產生更符合慣用法的介面:

protocol View4 {
    ReplaceHitRegion(struct {
        id uint64;
        size box<fuchsia.math.Rect>;
    });
    ReplaceName(struct {
        name string:optional;
    });
};

  1. 雖然在 ABI 方面,FIDL 類型系統是結構型類型系統 (也就是說名稱沒有影響力,只有類型結構才重要),但在 API 方面,FIDL 類型系統則有已命名的類型語意。