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 還是 FIDL 的做法如下:

  1. 其他實際工作環境中是否有其他程式使用這些資料?

    • 是:使用 FIDL。
  2. 當機報告會使用資料還是在指標中使用?

    • 是:使用「檢查」。
  3. 測試或開發人員工具會使用資料嗎?這些作品是否會用於實際工作環境?

    • 是:使用 FIDL。
    • 否:請擇一使用。

程式庫結構

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

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

請審慎考慮將類型和通訊協定定義分為程式庫的方式。將這些定義分解為程式庫會大幅影響這些定義的使用者,因為 FIDL 程式庫是類型和通訊協定的依附元件和發布單位。

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

如要判斷是否要將程式庫分解為較小的程式庫,請考慮下列問題:

  • 程式庫的客戶是否會細分為需要在程式庫中使用部分功能或宣告的獨立角色?如果有,請考慮將程式庫拆分為多個指定各角色的獨立程式庫。

  • 這個程式庫是否對應到一個具有一般結構結構的產業概念?如果可以,請考慮建構程式庫以符合業界標準結構。舉例來說,藍牙會歸入 fuchsia.bluetooth.lefuchsia.bluetooth.gatt,以便配合業界普遍理解這些概念。同樣地,fuchsia.net.http 也能對應至業界標準的 HTTP 網路通訊協定。

  • 還有許多其他程式庫依附此程式庫?如果是的話,請確認這些傳入的依附元件是否確實需要依附整個程式庫,還是是否有可因程式庫外分出的「核心」定義組合,才能接收大量傳入的依附元件。

在理想情況下,我們會為 Fuchsia 產生一個全球最佳 FIDL 程式庫結構。然而,Conway 的法律指出「設計系統 [...] 的機構必須設計為這些機構的通訊結構副本。」我們應花中等的時間打擊康威法律。

存取控制是以通訊協定精細程度為準

決定要定義通訊協定的程式庫時,請勿將存取權控管因素納入考量。一般而言,存取權控管是以通訊協定的精細程度表示。定義通訊協定的程式庫沒有存取權控管,也無法用於判斷是否可存取。

例如,程序可能會存取 fuchsia.logger.LogSink,或是由 fuchsia.media.StreamSource 通訊協定的用戶端指定程序。不過,FIDL 並未設計,無法用來明確授予 fuchsia.logger 程式庫的存取權,或禁止存取 fuchsia.ldsvc 程式庫。

fuchsia 命名空間

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

  • 程式庫定義了 FIDL 語言本身的部分或其一致性測試套件,在這種情況下,頂層命名空間必須為 fidl
  • 這個程式庫僅供內部測試使用,並未包含在 SDK 或正式版版本中,在這種情況下,頂層命名空間必須為 test

我們強烈建議在頂層命名空間「fuchsia」命名空間中的 FIDL 程式庫提供不超過四個元件,即 fuchsia.<api-namespace>fuchsia.<api-namespace>.<name>fuchsia.<api-namespace>.<name>.<subname>。選擇適當的 api-namespace,可能是由 API 委員會成員的說明。

舉例來說,平台來源樹狀結構中定義的 FIDL 程式庫,目的是向應用程式公開硬體功能。這些程式庫必須位於 fuchsia.hardware 命名空間中。舉例來說,公開乙太網路裝置的通訊協定可以命名為 fuchsia.hardware.ethernet.Device。建構於這些 FIDL 通訊協定的高階功能不在 fuchsia.hardware 命名空間中。例如,相較於 fuchsia.hardware,網路通訊協定更適合使用 fuchsia.net

避免過於深層巢狀結構

請優先使用內含三個元件的程式庫名稱 (例如 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} 則用於與視覺呈現互動,以及與算繪 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 的重要概念,而且兩個類型的定義不能相同。但是,不一定能夠同時進行這兩種事件,尤其是在類型命名的情況下 (會對其做為 API 介面元素使用不同概念1) 更具有意義。假設執行個體已命名的 ID type EntityId = struct { id uint64; };type OtherEntityId = struct { id uint64; }; 代表不同的概念,但除了名稱以外,兩者的類型定義仍相同。

使用匿名類型會建立多種類型,全都彼此不相容。因此,如果使用多個匿名類型代表同一概念,將導致 API 過於複雜,並預防大多數目標語言的一般處理。

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

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

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

指定向量和字串的界限

所有 vectorstring 宣告都必須指定長度限制。聲明通常分為以下兩種類型:

  • 資料本身設有限制。例如,包含檔案系統名稱元件的字串不得超過 fuchsia.io.MAX_FILENAME
  • 沒有「越多越好」的限制。在這種情況下,建議您使用內建常數 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() 方法會在回傳酬載中使用「大小限制」。這會限制在單一提取作業中傳輸的資料量,因此能進行資源使用限制的某些評估作業。此函式也會建立自然的分頁界線:伺服器只需要一次準備小型批次,而不是一次所有結果的大量傾印。

實作

fx set core.x64 --with=//examples/fidl/new:tests && fx test key_value_store_add_iterator

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

行銷長

用戶端

// 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.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

這樣就能以任何支援的語言編寫用戶端和伺服器實作:

Rust

用戶端

// 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},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    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},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::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++ (有線)

用戶端

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

伺服器

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

HTTP 即時串流 (HLP)

用戶端

// 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,因此字串不適用於任意二進位資料。請參閱「我該使用字串或向量?」一節。

由於長度繫結宣告的目的在於提供可輕鬆計算的 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👮🏽‍♀️

如果您的系統和字型支援,那麼上方會顯示這 4 個圖形叢集

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 或列舉項目來代表傳回的錯誤類型。在大多數情況下,建議做法是傳回列舉。

建議您將單一錯誤類型用於通訊協定的所有方法。

偏好特定網域列舉

定義及控制網域時,請使用基於特定目的建立的列舉錯誤類型。例如,在通訊協定建構時定義列舉,而傳達錯誤的語意是唯一的設計限制。如「enum」一節所述,最好避免使用 0 值。

在某些情況下,適合開始使用空白的彈性列舉:

type MyEmptyErrorCode = flexible enum {};

protocol Frobinator2 {
    Frobinate() -> (struct {
        value SuccessValue;
    }) error MyEmptyErrorCode;

    BadFrobinate() -> (struct {
        value SuccessValue;
    }) error flexible enum {}; // avoid anonymous enum
};

彈性列舉有預設未知成員。因此,空白彈性列舉是一種型別預留位置,提供可調整的預設用途。使用這個模式時,建議您定義獨立類型,讓通訊協定 (或程式庫) 中的多個方法重複使用,而不是使用匿名列舉。使用匿名列舉會建立多種類型,全都彼此不相容,導致 API 過於複雜,並預防大多數目標語言的錯誤處理錯誤。

採用明確定義的規格 (例如 HTTP 錯誤代碼) 時,請使用網域專屬的列舉錯誤類型,而列舉的用意是代表規格指出的原始值。

請特別注意,使用 zx.Status 類型處理與核心物件或 I/O 相關的錯誤。舉例來說,fuchsia.process 使用 zx.Status,因為程式庫主要在操控核心物件。再舉一個例子,fuchsia.io 會廣泛使用 zx.Status,因為程式庫與 IO 相關。

搭配錯誤列舉使用選用值

過去,定義一個方法時,傳回兩個傳回值、選用值和錯誤代碼,在效能方面會有些微利。請看以下範例:

type MyStatusCode = strict enum {
    OK = 0; // The success value should be 0,
    MISSING_FOO = 1; // with erroneous status next.
    NO_BAR = 2;
};

protocol Frobinator3 {
    Frobinate() -> (struct {
        value box<SuccessValue>;
        err MyStatusCode;
    });
};

不過,這個模式現已淘汰,並改用錯誤語法:這種模式的效能優勢已藉由在信封內內嵌小值取代,而現在則普遍使用低階支援。

避免出現錯誤的訊息和說明

在某些罕見的情況下,如果可能的錯誤狀況範圍很大,且對用戶端有實用的描述性錯誤訊息,通訊協定就可能包含錯誤字串說明以及 status 或列舉值。但包含字串邀請問題。舉例來說,用戶端可能會嘗試剖析字串來瞭解情況,代表字串的確切格式將成為通訊協定的一部分,而當字串本地化時,特別會發生問題。

安全性附註:同樣地,將堆疊追蹤或例外狀況訊息回報給用戶端可能會無意中外洩的權限資訊。

本地化字串和錯誤訊息

如果您要建構做為 UI 後端的服務,請使用結構化、輸入的訊息,並將轉譯留在 UI 層。

如果所有訊息都單純且未經參數化,請使用 enum 製作錯誤報告和一般 UI 字串。如需更詳細的訊息,包括名稱、數字和位置等參數,請使用 tablexunion,並將參數做為字串或數值欄位傳遞。

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

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

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

除非您正在建構高度專業的服務,且服務與單一 UI 實作緊密結合,否則應該不會在 FIDL 服務中公開使用者可見的 UI 字串。

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

定義方法時,您必須決定要逐一傳送參數,還是要將參數封裝在結構中。做出最佳選擇時,需要在多項因素之間取得平衡。進行決策時,請思考以下問題:

  • 有重要的封裝界線嗎?如果一組參數因為與這個方法以外的交錯而受到妥善傳遞,建議您將這些參數封裝在結構中。(希望您在開始設計通訊協定時,已找到這些一致群組,因為您之前已按照上述的「一般建議」操作,並盡早著重於類型)。

  • 除了呼叫的方法以外,結構是否有幫助?否則,請考慮分別傳送參數。

  • 您是否會在許多方法中重複使用同一組參數?如果有,請考慮將這些參數分組為一或多個結構。此外,您可能也會想知道這些參數是否具有連貫性,因為這些參數代表通訊協定中的重要概念。

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

  • 是否有多組參數同時為空值或非空值?如果有,請考慮將這些參數組合成可為空值的結構,以強制執行通訊協定本身中的不變性。舉例來說,上述定義的 FrobinateResult 結構包含 error 不是 MyError.OK 時,一律為空值的值。

我該使用 string 還是 vector<byte>

在 FIDL 中,string 資料必須是有效的 UTF-8,這表示字串可以代表萬國碼 (Unicode) 碼點的順序,但無法代表任意二進位資料。相較之下,vector<byte>array<byte, N> 可以代表任意二進位資料,且不代表萬國碼 (Unicode)。

使用 string 處理文字資料:

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

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

  • 請使用 string 來代表媒體轉碼器名稱,因為媒體轉碼器名稱是從有效 UTF-8 字串的固定詞彙中選取。

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

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

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

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

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

針對 blob 使用共用記憶體基元:

  • 若是圖片和 (大型) protobufs,請使用 zx.Handle:VMO 來完全緩衝資料。
  • 請將 zx.Handle:SOCKET 用於音訊和視訊串流,因為資料可能會隨時間送達,或者在完全寫入或提供前處理資料時才合理。

我該使用 vector 還是 array

vector 是可變長度序列,以無線格式表示。array 是固定長度的序列,以傳輸格式表示。

使用 vector 處理長度不定的資料:

  • 請將 vector 用於記錄訊息中的標記,因為記錄訊息可以有 0 到 5 個標記。

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

  • 使用 array 做為 MAC 位址,因為 MAC 位址一律為六位元組長。

我該使用 struct 還是 table

結構體和表格都代表含有多個已命名欄位的物件。差別在於結構體具有固定的傳輸格式版面配置,也就是說,在不破壞二進位檔相容性的情況下,結構無法修改。相較之下,資料表採用傳輸格式的彈性版面配置,代表您可以隨著時間將欄位新增至資料表,而不會破壞二進位檔相容性。

針對攸關效能的通訊協定元素,或是未來不太可能變更的通訊協定元素使用結構。舉例來說,您可以使用結構表示 MAC 位址,因為 MAC 位址的結構在日後不太可能發生變更。

請將表格用於日後可能變動的通訊協定元素。例如,使用資料表來代表相機裝置的中繼資料資訊,因為中繼資料中的欄位可能會隨時間演變。

如何表示常數?

視您擁有的常數口味而定,有三種方式表示常數:

  1. 使用 const 做為特殊值,例如 PIMAX_NAME_LEN
  2. 當這些值是集合的元素,例如媒體播放器的重複模式時,請使用 enumOFFSINGLE_TRACKALL_TRACKS
  3. bits 用於構成一組標記的常數,例如介面的功能:WLANSYNTHLOOPBACK

const

如果您希望在值上以符號方式使用值,請使用 const,而不要每次都輸入值。經典範例為 PI,通常編碼為 const,因為您不必在每次要使用這個值時輸入 3.141592653589

或者,當值可能會改變時,也可以使用 const,但在其他情況下必須一致地使用。在指定欄位中可提供的字元數上限是一個很好的範例 (例如MAX_NAME_LEN)。使用 const 即可集中管理該數字的定義,避免在整個程式碼中產生不同的值。

另一種選擇 const 的理由,是您可以用來限制訊息,之後再用於程式碼中。例如:

const MAX_BATCH_SIZE int32 = 128;

protocol Sender {
    Emit(struct {
        batch vector<uint8>:MAX_BATCH_SIZE;
    });
};

然後,您就可以在程式碼中使用常數 MAX_BATCH_SIZE 組合批次。

列舉

如果列舉值的組合受到 Fuuchsia 專案約束及控管,請使用列舉。舉例來說,Fchsia 專案會定義指標事件輸入模型,因此控管 PointerEventPhase 列舉的值。

在某些情況下,即使 Fuchsia 專案本身無法控制列舉值組合,只要我們合理預期想註冊新值的人員將提交修補程式至 Fuchsia 來源樹狀結構,即可登錄其值。例如,紋理格式必須由 Fuchsia 圖形驅動程式瞭解,這表示在使用這些驅動程式的開發人員可以新增新的紋理格式,即使一組紋理格式是由圖形硬體供應商控制也一樣。做為反例,請不要使用列舉來代表 HTTP 方法,因為我們無法合理預期使用者會使用新的 HTTP 方法將修補程式提交至 Platform Source Tree。

對於「之前」的不邊界集合,如果您預期想要動態擴充組合,string 可能會是更適當的選擇。舉例來說,使用 string 代表媒體轉碼器名稱,因為中介商或許能使用新的媒體轉碼器名稱做到合理一些。

如果一組列舉值由外部實體控制,請使用適當大小的整數或 string。例如,使用特定大小的整數 (特定大小) 代表 USB HID ID,因為 USB HID ID 組合由產業聯盟控管。同樣地,請使用 string 代表 MIME 類型,因為 MIME 類型是由 IANA 註冊資料庫控管 (至少在理論上)。

我們建議開發人員盡可能避免使用 0 做為列舉值。由於許多譯文語言都使用 0 做為整數的預設值,因此很難區分 0 值是刻意設定還是因為預設值而設定。舉例來說,fuchsia.module.StoryState 定義了三個值:RUNNING 值為 1STOPPING 值為 2,而 STOPPED 值為 3

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

位元

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

例如:

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

這表示 InfoFeatures 位元欄位是由未簽署的 32 位元整數支援,然後定義使用的三個位元。

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

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

這與上一個範例相同。

我該使用 resource 嗎?

FIDL 編譯器會強制所有已包含 resource 的類型都加上此標記。

如果 flexible 類型不含資源,但未來很有可能會發生,則應預先新增 resource 修飾符,避免日後發生難以轉換的情形。這種情況很少見:體驗已顯示,大多數訊息都不會包含資源,而且在通訊協定中傳遞資源需要照護和預先規劃。

我該針對類型使用 strictflexible 嗎?

將類型標示為 flexible 可讓您處理目前 FIDL 結構定義不明的資料,且建議用於日後新增或移除成員的類型 (例如設定、中繼資料或錯誤)。您隨時可以針對現有類型,在 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 通訊協定中重現的幾種良好設計模式。

我該針對方法和事件使用 strictflexible 嗎?

如果將方法或事件標示為 flexible,當不同的元件可能已在不同版本建構時 (例如部分元件認為該方法或事件存在,而其他元件進行通訊),則可更輕鬆地處理該方法或事件。由於更新式通訊協定的靈活性通常較為理想,因此除非有適當理由,否則建議您為方法和事件選擇 flexiblestrict

為方法建立 flexible 並不會造成單向方法或事件的負擔。以雙向方法來說,選擇 flexible 會為訊息增加極少的負擔 (16 個位元組或更少),且可能多一點時間解碼。整體而言,彈性雙向方法的成本應較小,無法成為幾乎所有用途時的考慮因素。

只有在方法和事件對通訊協定的正確行為極為重要時,才應將其設為 strict,因為接收端缺少該方法或事件,這樣就足以導致兩個末端之間的所有通訊都應取消,並關閉連線。

在設計動態饋給前 Dataflow 時,這項功能特別實用。請考慮這個記錄器通訊協定,這個通訊協定支援透過安全處理含有個人識別資訊 (PII) 的記錄。此方法使用前饋模式新增記錄,讓用戶端不必等待往返時間,就能依序啟動多項作業,並在最後清除待處理的作業。

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

這裡的所有方法都是 flexible,但 EnablePIIMode 除外;如果伺服器無法辨識任何方法,請考慮會發生什麼事:

  • 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 對使用動態饋給轉送 Dataflow 的通訊協定可能很實用,但這種通訊協定只會受到單向方法的演進。舉例來說,這可能適用於代表交易的「拆解通訊協定」,其中唯一雙向方法屬於修訂作業,其必須嚴格,交易上的其他作業可能會有所變更。

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 適用於重要的通訊協定,其中任何不明方法都是嚴重問題,這會導致關閉管道,而不是繼續處於可能不良狀態。另外,如果通訊協定不太可能變更,或至少在任何變更可能涉及非常長時間的發布週期時,就很適合使用這個 API,因此在發布週期中已預期會發生變更 strict 方法的額外成本。

通訊協定要求管道

最佳且最廣泛使用的設計模式之一是通訊協定要求管道。用戶端不會傳回支援通訊協定的管道,而是傳送管道並要求伺服器,將通訊協定的實作繫結至該管道:

// GOOD:
protocol GoodFoo {
    GetBar(resource struct {
        name string;
        bar server_end:Bar;
    });
};

// BAD:
protocol BadFoo {
    GetBar(struct {
        name string;
    }) -> (resource struct {
        bar client_end:Bar;
    });
};

這個模式很實用,因為用戶端在開始使用 Bar 通訊協定之前,不需要等待往返行程。用戶端可以改將 Bar 的訊息排入佇列。這些訊息將由核心緩衝,並在 Bar 的實作繫結至通訊協定要求後完成處理。相反地,如果伺服器傳回 Bar 通訊協定的執行個體,則用戶端必須等待整個來回行程,才能將 Bar 的訊息排入佇列。

如果要求可能會失敗,請考慮擴充此模式,並以描述作業是否成功的回覆來說明:

protocol CodecProvider {
    TryToCreateCodec(resource struct {
        params CodecParams;
        codec server_end:Codec;
    }) -> (struct {
        succeed bool;
    });
};

為了處理失敗情況,用戶端會等待回覆,並在要求失敗時採取其他動作。另一個方法是讓通訊協定在伺服器開始時傳送事件:

protocol Codec2 {
    -> OnReady();
};

protocol CodecProvider2 {
    TryToCreateCodec(resource struct {
        params CodecParams;
        codec server_end:Codec2;
    });
};

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

不過,如果要求可能會成功,擁有任一類型的成功信號可能會有害,因為信號可讓用戶端區分不同的失敗模式,這些模式通常應以相同方式處理。例如,用戶端應處理在建立連線後立即失敗的服務,處理方式與一開始無法存取的服務相同。在這兩種情況下,服務都無法使用,用戶端則應產生錯誤或以其他方式完成工作。

流量控制

FIDL 訊息會由核心進行緩衝。如果其中一個端點產生的訊息比其他端點耗用的數量多,訊息就會在核心內累積,進而佔用記憶體,導致系統更難復原。而設計完善的通訊協定應調節訊息的產生作業,以便符合這些訊息的耗用速率。這個屬性稱為「流量控制」

流程控制是一種廣泛、複雜的主題,其中有許多有效的設計模式。本節會探討一些較熱門的流程控制模式,但並非詳盡無遺。模式會按照偏好遞減排序。如果其中一個模式適合特定用途,則應使用模式。但如果不是,則通訊協定可免費使用未列於下方的替代流量控制機制。

偏好提取來推送

如果沒有嚴謹的設計,則伺服器將資料推送至用戶端的通訊協定通常無法控制流量。要有效控制流量的一種方法是讓用戶端從伺服器提取一或多個範圍。提取模型已內建流量控管機制,因為用戶端自然會限制伺服器產生資料的頻率,並避免從伺服器推送的訊息不堪負荷。

使用 hanging 延遲回應

實作提取式通訊協定的簡單方法,是使用停止運作取得模式與伺服器「儲存回呼」:

protocol FooProvider {
    WatchFoo(struct {
        args Args;
    }) -> (resource struct {
        foo client_end:Foo;
    });
};

在這個模式中,用戶端會傳送 WatchFoo 訊息,但伺服器必須等到有新資訊可傳送至用戶端後才會回覆。用戶端會使用 foo,並立即傳送另一個等待取得。用戶端和伺服器每個資料項目都會執行一個工作單位,這表示兩者均優先。

當傳輸的資料項目組合大小受限,且伺服器端狀態很簡單,但在用戶端和伺服器需要同步工作時,停止運作模式就能發揮效用。

例如,伺服器可能會針對某些可變動狀態 foo 使用「dirty」位元為每個用戶端實作等待取得模式。此函式會將這個位元初始化為 true,在每個 WatchFoo 回應中清除,然後在每次 foo 變更時設定該位元。只有在設定無效位元時,伺服器才會回應 WatchFoo 訊息。

使用確認來限制推送

在使用推送的通訊協定中提供流量控制的方法之一是「確認模式」,其中呼叫者會提供呼叫端用於流量控制的確認回應。例如,請考慮以下一般事件監聽器通訊協定:

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

事件監聽器在收到 OnBar 訊息後,會立即傳送空白的回應訊息。回應不會向呼叫端傳送任何資料。相反地,回應會讓呼叫端觀察呼叫端使用訊息的速率。呼叫端應限制呼叫端產生的訊息頻率,以符合受呼叫端的接收速率。例如,呼叫端可能只安排一段 (或固定數量) 的訊息,也就是等待確認訊息。

FIDL 方案:確認模式

確認模式是一種簡單的資料流控制方法,適用於其中一種方式呼叫的方法。該方法並非僅將方法保留為單向呼叫,而是會轉而轉換為具有缺少回應的雙向呼叫,亦即口語稱為「堆疊」ack。之所以存在,唯一的原因是要通知寄件人已收到訊息,而傳送方可以使用該訊息決定如何繼續。

此確認費用的費用會由聊天室新增。如果用戶端在進行下一個呼叫前等待確認,此模式也可能會導致效能降低。

以非計量付費的方式呼叫訊息會產生簡單的設計,但可能會有錯誤:如果伺服器處理更新的速度比用戶端傳送更新慢許多,該怎麼辦?舉例來說,用戶端可能會從某些文字檔案載入內含數千行的繪圖,然後嘗試依序傳送。要如何向用戶端套用返回壓力,避免伺服器因這次的更新而感到不堪負荷?

只要使用確認模式,並將 AddLine(...); 以雙向方式呼叫 AddLine(...) -> ();,就能向用戶端提供意見回饋。如此一來,用戶端就能視情況限制其輸出內容。在此範例中,我們會要求用戶端在傳送下一個訊息之前等待確認,但較複雜的設計可能會讓訊息以最佳方式傳送訊息,並且只在收到比預期低的非同步呼叫時進行節流。

首先,我們需要定義介面定義和測試控管。FIDL、CML 和領域介面定義會設定一個鷹架,供任意實作項目使用:

FIDL

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

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

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

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

/// Manages a single instance of a canvas. Each session of this protocol is responsible for a new
/// canvas.
@discoverable
open protocol Instance {
    /// Add a line to the canvas.
    ///
    /// This method can be considered an improvement over the one-way case from a flow control
    /// perspective, as it is now much more difficult for a well-behaved client to "get ahead" of
    /// the server and overwhelm. This is because the client now waits for each request to be acked
    /// by the server before proceeding. This change represents a trade-off: we get much greater
    /// synchronization of message flow between the client and the server, at the cost of worse
    /// performance at the limit due to the extra wait imposed by each ack.
    flexible AddLine(struct {
        line Line;
    }) -> ();

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

行銷長

用戶端

// 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.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

這樣就能以任何支援的語言編寫用戶端和伺服器實作:

Rust

用戶端

// 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},
    config::Config,
    fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point},
    fuchsia_component::client::connect_to_protocol,
    futures::TryStreamExt,
    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(format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or(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},
    fidl::endpoints::RequestStream as _,
    fidl_examples_canvas_addlinemetered::{
        BoundingBox, InstanceRequest, InstanceRequestStream, Point,
    },
    fuchsia_async::{Time, Timer},
    fuchsia_component::server::ServiceFs,
    fuchsia_zircon::{self as zx},
    futures::future::join,
    futures::prelude::*,
    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(Time::after(zx::Duration::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++ (有線)

用戶端

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

HTTP 即時串流 (HLP)

用戶端

// 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」要求限定的次數 (而非只會回應一次):

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 通訊協定效能的其中一種方法是允許行批次:與其每次將新行新增到畫布,等待回覆,然後再執行下一行,我們就可以改為批次處理多行程式碼,並叫用新的 AddLines(...); 呼叫。AddLine(...);用戶端現在可以決定如何妥善區隔大量線條集合。

單純實作,我們會發現自己在伺服器與用戶端完全未同步的情況下:用戶端可以透過未繫結的 AddLines(...); 呼叫大量流向伺服器,而伺服器同樣也會發出更多 -> OnDrawn(...); 事件,導致用戶端發出超出可處理的 -> OnDrawn(...); 事件。這兩個問題的解決方法就是新增用於同步處理的簡易 Ready() -> (); 方法。每當用戶端準備好接收下一個繪圖更新時,用戶端就會呼叫此方法,而伺服器的回應表示用戶端可以處理更多要求。

我們現在提供兩個方向的流程控制。通訊協定現在會實作動態饋給正向模式,在部分同步處理「修訂」呼叫之前,會允許許多未控制的呼叫觸發伺服器上的實際工作。以免用戶端對伺服器的工作負荷過重。同樣地,伺服器也無法再傳送未受限的 -> OnDrawn(...); 事件:每個事件都必須遵循來自用戶端的信號 Ready() -> (); 呼叫,表示已準備好執行更多工作。這稱為經過調節的事件模式

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

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

FIDL

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

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

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

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

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

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

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

行銷長

用戶端

// 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.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

這樣就能以任何支援的語言編寫用戶端和伺服器實作:

Rust

用戶端

// 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},
    config::Config,
    fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point},
    fuchsia_component::client::connect_to_protocol,
    futures::TryStreamExt,
    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(format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or(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},
    fidl::endpoints::RequestStream as _,
    fidl_examples_canvas_clientrequesteddraw::{
        BoundingBox, InstanceRequest, InstanceRequestStream, Point,
    },
    fuchsia_async::{Time, Timer},
    fuchsia_component::server::ServiceFs,
    fuchsia_zircon::{self as zx},
    futures::future::join,
    futures::prelude::*,
    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(Time::after(zx::Duration::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++ (有線)

用戶端

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

HTTP 即時串流 (HLP)

用戶端

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

以動態消息為主的 Dataflow

部分通訊協定支援動態饋給轉送 Dataflow,資料主要朝一個方向 (通常是從用戶端到伺服器) 流動,可以避免往返延遲時間。通訊協定只會在必要時同步處理兩個端點。以動態饋給轉送的 Dataflow 也會增加處理量,因為執行特定工作所需的內容切換總數變少。

預先處理 Dataflow 的關鍵,在於客戶不需要等待先前方法呼叫的結果,再傳送後續訊息。舉例來說,透過通訊協定要求管道,用戶端無需等待伺服器透過通訊協定回覆,用戶端就能使用通訊協定。同樣地,用戶端指派的 ID (請參閱下文) 不需要等待伺服器,針對伺服器保存的狀態指派 ID。

一般而言,動態饋給轉送通訊協定通常會讓用戶端提交單向方法呼叫序列,不必等待伺服器的回應。提交這些訊息後,用戶端會呼叫具有回覆的方法 (例如 CommitFlush) 來明確與伺服器同步。回覆可能是空白訊息,或可能包含提交的序列是否成功的相關資訊。在更複雜的通訊協定中,單向訊息會表示為指令物件的聯集,而非個別方法呼叫;請參閱下方的指令聯集模式

使用前饋類 Dataflow 的通訊協定,可與錯誤處理策略的最佳化策略搭配使用。與其讓伺服器透過狀態值為每個方法回覆每個方法,以鼓勵用戶端在每則訊息之間等待來回航班,只有在因無法由用戶端控制的原因而方法失敗時,才加入狀態回覆。如果用戶端傳送的訊息應已知無效 (例如參照的用戶端指派的 ID 無效),請關閉連線以指出發生錯誤。如果用戶端傳送無法已知的訊息無效,則提供回應表示成功或失敗 (需要用戶端同步),或記住錯誤並忽略後續相依要求,直到用戶端同步處理錯誤並某種原因復原為止。

例子:

protocol Canvas {
    Flush() -> (struct {
        code zx.Status;
    });
    Clear();
    UploadImage(struct {
        image_id uint32;
        image Image;
    });
    PaintImage(struct {
        image_id uint32;
        x float32;
        y float32;
    });
    DiscardImage(struct {
        image_id uint32;
    });
    PaintSmileyFace(struct {
        x float32;
        y float32;
    });
    PaintMoustache(struct {
        x float32;
        y float32;
    });
};

FIDL 方案:大小限制

FIDL 向量和字串可以有大小限制,用於指定該類型可包含的成員數目限制。以向量的情況來說,這是指儲存在向量中的元素數量,而對於字串,則是指字串包含的「位元組數量」

強烈建議您使用大小限制,因為這個限制會針對沒有限制的大型類型設定上限。

鍵/值儲存庫的一項實用做法,是按照順序疊代:也就是說,當指定鍵時,會傳回後方依順序顯示的 (通常是分頁) 元素清單。

原因

在 FIDL 中,最佳做法是使用疊代器,而疊代器通常會實作為獨立通訊協定,用於進行疊代。使用獨立的通訊協定 (因此獨立的管道) 有許多好處,包括將疊代從主要通訊協定中完成的其他作業中分離疊代提取要求。

通訊協定 P 管道連線的用戶端和伺服器端,分別能以 FIDL 資料類型表示 (分別以 client_end:Pserver_end:P 表示)。這些類型統稱為「通訊協定結尾」,代表透過現有的 FIDL 連線將 FIDL 用戶端連線至對應伺服器的其他 (非 @discoverable) 方法。

通訊協定結尾是一般 FIDL 概念的特定例項:資源類型。資源類型用來包含 FIDL 控制代碼,必須對類型的使用方式設有額外限制。類型必須一律使用專用,因為基礎資源是由其他能力管理員 (通常是 Zircon 核心) 居中調解。如果沒有在管理程式的情況下,透過簡單的記憶體內副本複製此類資源,是不可能的。為避免發生這類重複情形,FIDL 中的所有資源類型一律為移動項目。

最後,Iterator 通訊協定本身的 Get() 方法會在回傳酬載中使用「大小限制」。這會限制在單一提取作業中傳輸的資料量,因此能進行資源使用限制的某些評估作業。此函式也會建立自然的分頁界線:伺服器只需要一次準備小型批次,而不是一次所有結果的大量傾印。

實作

fx set core.x64 --with=//examples/fidl/new:tests && fx test key_value_store_add_iterator

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

行銷長

用戶端

// 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.
        {
            protocol: [
                "fuchsia.inspect.InspectSink",
                "fuchsia.logger.LogSink",
            ],
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

這樣就能以任何支援的語言編寫用戶端和伺服器實作:

Rust

用戶端

// 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},
    config::Config,
    fuchsia_component::client::connect_to_protocol,
    std::{thread, time},
};

use {
    fidl::endpoints::create_proxy,
    fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker},
    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},
    fuchsia_component::server::ServiceFs,
    futures::prelude::*,
    lazy_static::lazy_static,
    regex::Regex,
};

use {
    fidl_examples_keyvaluestore_additerator::{
        Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest,
        StoreRequestStream, WriteError,
    },
    fuchsia_async as fasync,
    std::collections::btree_map::Entry,
    std::collections::BTreeMap,
    std::ops::Bound::*,
    std::sync::Arc,
    std::sync::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++ (有線)

用戶端

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

伺服器

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

HTTP 即時串流 (HLP)

用戶端

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

伺服器

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

隱私保護設計

通訊協定中的用戶端和伺服器經常存取不同的機密資料組合。隱私權或安全性問題可能是因為在通訊協定中不小心洩漏了過多資料。

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

  • 包含姓名、電子郵件地址或付款資料等個人識別資訊。
  • 由使用者提供,可能含有個人資訊。範例包括裝置名稱和註解欄位。
  • 可做為專屬 ID,與供應商、使用者、裝置或重設項目建立關聯。例如序號、MAC 位址、IP 位址和全域帳戶 ID。

我們會徹底審查這些類型欄位,而且包含這些欄位的通訊協定可能會受到限制。請確保通訊協定不含任何需要的資訊。

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

請參考兩個假設的示例,說明 API 設計選擇造成的隱私權違規情形:

範例 1 -週邊裝置控制 API 中的序號

假設有一個週邊裝置控制 API,當中包含 USB 週邊裝置序號。序號不含個人資料,但是非常穩定的 ID,可以輕鬆建立關聯。在此 API 中加入序號會導致許多隱私權疑慮:

  • 凡是可存取 API 的用戶端,都能與使用相同 Fuchsia 裝置的不同帳戶建立關聯。
  • 凡是可存取 API 的用戶端,都能找出帳戶中的不同人物角色。
  • 不同的軟體供應商可能會無法察覺,瞭解他們是在同一部使用者還是相同的裝置上使用。
  • 如果您在裝置之間移動週邊裝置,可存取 API 的用戶端都可以為一組裝置與使用者共用該週邊裝置的使用者建立關聯。
  • 如果週邊裝置售出,可存取 API 的用戶端會將週邊裝置的新擁有者和新擁有者建立關聯。
  • 部分製造商會將資訊編碼成序號。這可能會讓具有 API 存取權的用戶端在使用者購買週邊裝置的位置或時機產生。

在此範例中,序號的用途是讓用戶端偵測何時重新連線相同的 USB 週邊裝置。符合這項意圖需要穩定的 ID,但不需要全域 ID。不同的用戶端不需要接收相同的 ID,同一個用戶端不需要在不同 Fuchsia 裝置上接收相同的 ID,且可在恢復原廠設定事件時保持 ID 不變。

在這個範例中,建議您傳送僅保證在單一裝置上穩定運作的 ID。此 ID 可以是周邊裝置序號、 Fuchsia 裝置 ID 和連線路徑名稱的雜湊。

範例 2 - Device setup API 中的裝置名稱

考慮採用裝置設定 API,當中包含用於設定裝置的手機型號。在大多數情況下,手機的型號字串是由原始設備製造商 (OEM) 設定,但部分手機會將使用者提供的裝置名稱回報為型號。導致許多模型字串包含使用者真實姓名或匿名化。因此,這項 API 風險會在跨身分或跨裝置建立關聯。罕見或預先發布版的模型字串可能會揭露機密資訊,即使使用者未提供該資訊也一樣。

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

客戶指派的 ID

通訊協定通常會允許用戶端操控伺服器保存的多個狀態部分。設計物件系統時,這個問題的一般做法是為伺服器持有的每個連貫狀態建立個別的物件。但是,設計通訊協定時,針對每個狀態使用獨立物件,有幾種缺點。

為每個邏輯物件建立不同的通訊協定執行個體都會耗用核心資源,因為每個執行個體都需要單獨的管道物件。每個執行個體都會單獨維護一個獨立的 FIFO 訊息佇列。針對每個邏輯物件使用單獨的執行個體,代表傳送至不同物件的訊息可相互重新排序,導致用戶端和伺服器之間出現順序外的互動。

用戶端指派的 ID 模式會要求用戶端將 uint32uint64 ID 指派給伺服器保留的物件,藉此避免這些問題。所有在用戶端和伺服器之間交換的訊息都會透過單一通訊協定執行個體進行漏斗,為整個互動提供一致的 FIFO 排序。

由用戶端 (而非伺服器) 指派 ID 可讓您使用前饋送流,因為用戶端可以將 ID 指派給物件,然後立即對該物件執行作業,不必等待伺服器回覆物件 ID。在這個模式中,ID 只能在目前連線的範圍內有效,且零 ID 通常以 sentinel 保留。安全性注意事項:用戶端不應將位址空間中的位址當做 ID,因為這些位址可能會導致位址空間的版面配置外洩。

用戶端指派的 ID 模式有一些缺點。舉例來說,用戶端更難編寫,因為客戶需要管理自己的 ID。開發人員通常想要建立用戶端程式庫,藉此提供服務的物件導向外觀來隱藏管理 ID 的複雜度,而 ID 本身為反模式 (請參閱下方的用戶端程式庫)。

如果您希望使用核心的物件能力系統來保護對該物件的存取權,您應建立個別的通訊協定執行個體來代表物件,而非使用用戶端指派的 ID。舉例來說,如果您希望用戶端能夠與物件互動,但不希望用戶端與其他物件互動,您可以建立個別的通訊協定執行個體,並使用基礎管道當做控制物件存取權的能力。

指揮會

在使用動態饋給轉送 Dataflow 的通訊協定中,用戶端通常會在傳送雙向同步處理訊息之前,傳送許多單向訊息至伺服器。如果通訊協定涉及大量訊息,傳送訊息的負擔可能會變得明顯。在這類情況下,請考慮使用指令聯集模式,將多個指令批次處理為單一訊息。

在這個模式中,用戶端會傳送 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 訊息通常會透過訊息大小上限的管道傳送。在許多情況下,訊息大小上限足以傳輸合理資料量,不過在傳送大量 (甚至是不受限) 資料量的情況下也會使用這個限制。如要傳輸大量或無限量的資訊,其中一種方式是使用分頁模式

分頁寫入

為伺服器將資料分頁寫入簡單做法,是讓用戶端在多個訊息中傳送資料,然後先使用「完成」方法,讓伺服器處理傳送的資料:

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

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

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

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

當伺服器可以將所有分頁狀態提報給用戶端,因此完全不再需要維持分頁狀態時,這個模式就特別有吸引力。伺服器應記錄用戶端是否能夠保留權杖,並在通訊協定的執行個體之間重複使用該權杖。安全性注意事項:不論是哪一種情況,伺服器都必須驗證用戶端提供的權杖,確保用戶端的存取權僅限於自己的分頁結果,且不含用於其他用戶端的結果。

事件配對相關

使用用戶端指派的 ID 時,用戶端會使用僅於本身與伺服器連線期間有意義的 ID,來識別伺服器保留的物件。不過,某些用途需要為每個用戶端建立物件關聯。舉例來說,在 fuchsia.ui.scenic 中,用戶端會使用用戶端指派的 ID 與場景圖中的節點互動。不過,從其他程序匯入節點時,需要跨程序邊界與該節點的參照建立關聯。

事件配對相關模式藉由仰賴核心來提供必要的安全性,使用以動態饋給轉送的 Dataflow 解決這個問題。首先,希望匯出物件的用戶端會建立 zx::eventpair,並將其中一個接角事件連同該物件的用戶端指派 ID 傳送至伺服器。接著,用戶端會將其他接角的事件傳送至其他用戶端,使用針對現在共用物件的用戶端指派 ID 將事件轉送至伺服器:

protocol Exporter {
    ExportThing(resource struct {
        client_assigned_id uint32;
        export_token zx.Handle:EVENTPAIR;
    });
};

protocol Importer {
    ImportThing(resource struct {
        some_other_client_assigned_id uint32;
        import_token zx.Handle:EVENTPAIR;
    });
};

為了建立物件關聯,伺服器會以 ZX_INFO_HANDLE_BASIC 呼叫 zx_object_get_info,並比對關注事件物件的 koidrelated_koid 屬性。

取消活動配對

使用解除通訊協定交易時,用戶端可以關閉通訊協定的用戶端,藉此取消長時間執行的作業。伺服器應監聽 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 可提供中繼資料做為具有多個欄位的 table,如果這個中繼資料可能會隨時間增加,通常是很實用的做法,可讓接收者向傳送者表示會讀取這份中繼資料中的哪些欄位,這樣可避免傳送收件者不會考慮的多餘欄位。在這種情況下,平行的 bits 的成員與 table 欄位的一對一相符,是建構 API 的強大基礎:

type MetadataFields = flexible bits {
    VERSION = 0b1;
    COUNT = 0b10;
};

type Metadata = table {
    1: version string;
    2: count uint32;
    // ...
};

protocol MetadataGetter {
    Get(struct {
        fields MetadataFields;
    }) -> (struct {
        metadata Metadata;
    });
};

現在,請考慮使用 command union。在複雜的情境中,伺服器可能會想能力描述其支援的指令。在這種情況下,具有平行的 enum,並讓成員將一對一與 union 的變化版本配對,可能是建構 API 的強大基礎:

type MyCommandVariant = strict enum {
    POKE = 1;
    PROD = 2;
    // ...
};

protocol HighVolumeSinkContinued {
    SupportedCommands() -> (struct {
        supported_commands vector<MyCommandVariant>;
    });
};

請注意,雖然您可能會想使用 bits 值來代表指令組合,但這會造成一些更困難的選擇,但如果您的 API 不斷演進,您需要參照特定指令,那麼採用 enum 即可。如果您開始使用 bits 值,現在會發生下列兩種錯誤選擇之一:

  1. 加入 enum,表示現在有兩種方法可以參照欄位,也可能在用戶端程式碼中產生轉換問題 (以便從一個表示法跳至另一個表示法);或

  2. 繼續使用 bits,但每次只能設定一個位元,現在返回至設定的特定位元較麻煩。

簡單來說,對 table 來說:

  • 使用 table 的名稱以及後置字串 Fields (複數) 為 bits 命名。每個成員值都應為序數索引的位元,也就是 1 << (ordinal - 1)

  • union 的建議類似,您想要符合 bitstable 之間的彈性,也就是因為 FIDL 目前僅支援彈性資料表,bits 必須是 flexible

針對 union

  • enum 命名列出 union 名稱的所有變化版本,以及後置字串 Variant (單數)。每個成員值都應為其描述的變化版本序數。

  • 請比對 unionenum 之間的彈性,例如,如果 unionstrict,則 enum 也必須為 strict

反面模式

本節說明幾種反面模式:通常提供負值的設計模式。學會辨識這些模式是避免以錯誤方式使用的第一步。

已推送設定:盡量避免

Fuchsia 平台一般偏好提取語意。「元件的語意」提供一個重要範例;系統假設功能是從元件中提取,則允許元件啟動延遲和元件關閉順序,以便透過能力路徑的引導圖推論出來。

在設計中,使用 FIDL 通訊協定將設定從一個元件推送至另一個元件,由於簡單明瞭,因此會引起使用者的興趣。如果元件 A 將政策推送至 B 元件 B 的商業邏輯,就會發生這種情況。這會導致平台誤解依附元件關係:不會自動開始支援 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. 雖然 FIDL 類型系統是屬於 ABI 的結構型類型系統 (亦即名稱沒有意義,只有型別的結構),但 FIDL 類型系統在 API 方面有已命名的類型語意。