FIDL API 评分准则

一般建议

本部分介绍了在 Fuchsia 接口定义语言中定义协议的技术、最佳实践和一般建议。

另请参阅 FIDL 样式指南。

协议而非对象

FIDL 是一种用于定义进程间通信协议的语言。虽然语法类似于面向对象的接口定义,但设计考虑因素更类似于网络协议,而不是对象系统。例如,要设计高质量的协议,您需要考虑带宽、延迟时间和流量控制。您还应考虑到,协议不仅仅是操作的逻辑分组:协议还会对请求施加 FIFO 顺序,将协议拆分为两个较小的协议意味着,在两个不同协议上发出的请求可以相互重新排序。

重点关注

设计 FIDL 协议的一个良好起点是设计协议将使用的数据结构。例如,关于网络的 FIDL 协议可能包含各种类型 IP 地址的数据结构,而关于图形的 FIDL 协议可能包含各种几何概念的数据结构。您应该能够查看类型名称,并对协议操作的概念以及这些概念的结构有一些直观的了解。

语言中立

有许多不同语言的 FIDL 后端。您应避免针对任何特定目标语言过度特化 FIDL 定义。随着时间的推移,您的 FIDL 协议可能会被许多不同的语言使用,甚至可能包括目前尚不支持的一些语言。FIDL 是将系统连接在一起的粘合剂,可让 Fuchsia 支持各种语言和运行时。如果您过于专注于自己喜欢的语言,就会削弱这一核心价值主张。

序数

协议包含多种方法。系统会自动为每个方法分配一个唯一的 64 位标识符,称为序数。服务器使用序数值来确定应分派哪个协议方法。

编译器通过对库、协议和方法名称进行哈希处理来确定序数值。在极少数情况下,同一协议中的序数可能会发生冲突。如果发生这种情况,您可以使用 Selector 属性更改编译器用于哈希处理的方法的名称。以下示例将使用方法名称“C”而不是方法名称“B”来计算哈希值:

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

如果开发者希望更改方法的名称,也可以使用选择器来保持与传输格式的向后兼容性。

诊断

有时,需要公开对调试或诊断程序有用的信息。这些数据可以采用统计信息和指标(例如错误数量、调用次数、大小等)、对开发有用的信息、组件的健康状态或类似信息的形式。

在测试协议或生产协议的调试方法中公开这些信息可能很有吸引力。不过,Fuchsia 提供了一种单独的机制来公开此类信息:Inspect,在决定如何公开此类数据时,应考虑使用该机制。如果需要公开有关程序的诊断信息,而这些信息有助于在测试中进行调试、供开发者工具使用,或通过崩溃报告或指标在现场检索,则应使用 Inspect 而不是 FIDL 方法/协议,前提是没有其他程序使用这些信息来做出运行时决策。

当其他程序需要根据诊断信息做出运行时决策时,应使用 FIDL。Inspect 绝不能用于程序之间的通信,它是一个尽力而为的系统,在生产环境中运行时,不得依赖它来做出决策或改变行为。

用于决定是使用 Inspect 还是 FIDL 的启发式方法可能如下:

  1. 生产环境中的其他程序是否会使用这些数据?

    • 是:使用 FIDL。
  2. 这些数据是否会用于崩溃报告或指标?

    • 是:使用“检查”功能。
  3. 这些数据是否由测试或开发者工具使用?它是否有可能在生产环境中使用?

    • 是:使用 FIDL。
    • 否:使用其中一种。

库结构

将 FIDL 声明分组到 FIDL 库中有两个具体目标:

  • 帮助 FIDL 开发者(使用 FIDL 库的开发者)浏览 API 接口。
  • 提供结构,以便在 FIDL 库中对 FIDL 声明进行分层范围限定。

请仔细考虑如何将类型和协议定义划分到库中。如何将这些定义分解为库对这些定义的使用者有很大影响,因为 FIDL 库是类型和协议的依赖项和分发单元。

FIDL 编译器要求库之间的依赖关系图为 DAG,这意味着您无法跨库边界创建循环依赖关系。不过,您可以在库中创建(一些)循环依赖项。

如需决定是否将库分解为较小的库,请考虑以下问题:

  • 库的客户是否可以细分为不同的角色,这些角色可能希望使用库中的部分功能或声明?如果是,请考虑将该库拆分为针对每个角色的单独库。

  • 该库是否对应于具有普遍理解结构的行业概念?如果是,请考虑将库的结构调整为符合业界标准结构。例如,蓝牙分为 fuchsia.bluetooth.le 和 fuchsia.bluetooth.gatt,以符合业界对这些概念的普遍理解。同样,fuchsia.net.http 对应于业界标准的 HTTP 网络协议。

  • 是否有许多其他库依赖于该库?如果是,请检查这些传入依赖项是否确实需要依赖于整个库,或者是否存在一组“核心”定义,可以从库中提取出来以接收大部分传入依赖项。

理想情况下,我们会为整个 Fuchsia 生成一个全局最优的 FIDL 库结构。不过,康威定律指出,“设计系统的组织 […] 必然会产生与这些组织的通信结构如出一辙的设计。”我们应该花适量的时间来对抗康威定律。

访问权限控制采用协议粒度

在决定要在哪个库中定义协议时,请勿考虑访问权限控制。通常,访问权限控制以协议粒度表示。定义协议的库与访问权限控制无关,不能用于确定是否可以访问该协议。

例如,某个进程可能会访问 fuchsia.logger.LogSink,或者某个进程会获得 fuchsia.media.StreamSource 协议的客户端。不过,FIDL 并非旨在用于表达对 fuchsia.logger 库的访问权限,也无法用于阻止对 fuchsia.ldsvc 库的访问。

fuchsia 命名空间

在平台源代码树中定义的 FIDL 库(即在 fuchsia.googlesource.com 中定义的库)必须位于 fuchsia 顶级命名空间(例如 fuchsia.ui)中,除非满足以下条件之一:

  • 该库定义了 FIDL 语言本身或其一致性测试套件的部分内容,在这种情况下,顶级命名空间必须为 fidl。
  • 该库仅用于内部测试,不包含在 SDK 或正式 build 中,在这种情况下,顶级命名空间必须为 test。

强烈建议顶级命名空间 fuchsia 命名空间中的 FIDL 库不超过四个组件,即 fuchsia.<api-namespace>、fuchsia.<api-namespace>.<name> 或 fuchsia.<api-namespace>.<name>.<subname>。选择适当的 api-namespace,可能需要 API 委员会成员的帮助。

例如,在平台源代码树中定义的 FIDL 库(用于向应用公开硬件功能)必须位于 fuchsia.hardware 命名空间中。例如,用于公开以太网设备的协议可能名为 fuchsia.hardware.ethernet.Device。基于这些 FIDL 协议构建的高级功能不属于 fuchsia.hardware 命名空间。例如,网络协议更适合放在 fuchsia.net 下,而不是 fuchsia.hardware 下。

避免嵌套过深

最好使用包含三个组成部分的库名称(例如 fuchsia.hardware.network),并避免使用包含四个以上组成部分的库名称(例如 fuchsia.apps.foo.bar.baz)。如果您使用四个以上的组成部分,则应有特定的理由。

库依赖项

最好从名称更具体的库引入对名称不太具体的库的依赖项。例如,fuchsia.foo.bar 可能依赖于 fuchsia.foo,但 fuchsia.foo 不应依赖于 fuchsia.foo.bar。这种模式更利于扩展,因为随着时间的推移,我们可以添加更多具有更具体名称的库,但具有不太具体名称的库数量有限。

导入库的可见性

为了进一步实现将 FIDL 声明分组到 FIDL 库中的第二个目标,我们希望改进 FIDL,以提供可见性规则,从而改变导入库(“子库”)是否可以使用元素,例如 public 或 private 修饰符。

internal 库组件名称旨在进行特殊处理,并指示可见性规则的本地限制。例如,fuchsia.net.dhcp.internal.foo 库中的公共声明可能仅对其父级 fuchsia.net.dhcp 或其同级(例如 fuchsia.net.dhcp.internal.bar)可见。

使用多字词库组件

虽然允许使用包含多个字词的组件的库名称(例如 fuchsia.modular.storymodel),但应尽量避免使用。如果库名称违反了嵌套规则,或者从层次结构角度考虑库的放置位置时,两个字词都不应优先于另一个字词,则库作者可以采用将多个字词联接在一起的方式。

版本字符串

如果库需要版本控制,则应添加单个版本号作为后缀,例如 fuchsia.io2 或 fuchsia.something.something4.。版本号不应包含多个部分,例如 fuchsia.io2.1 是不可接受的,而应为 fuchsia.io3。任何库组件都可以进行版本控制,但强烈建议不要使用多个版本控制组件,例如 fuchsia.hardware.cpu2.ctrl,而不是 fuchsia.hardware.cpu2.ctrl4。

版本号应仅表示库的较新版本,而不是实质上不同的网域。举个反例,fuchsia.input 库用于较低级别的设备处理,而 fuchsia.ui.input{2,3} 用于与 Scenic 以及渲染界面的软件组件交互的输入。如果仅关注版本控制,则使用 fuchsia.ui.scenic.input 和 fuchsia.ui.scenic.input2 会更清楚,以便与 fuchsia.input 服务的其他网域区分开来。

类型

如“一般建议”中所述,您应特别注意协议定义中使用的类型。

保持一致

对同一概念使用一致的类型。例如,在整个库中,针对特定概念始终如一地使用 uint32 或 int32。如果您为某个概念创建了 struct,请始终使用该结构体来表示该概念。

理想情况下,类型也应在库边界之间保持一致。 检查相关库中是否有类似的概念,并与这些库保持一致。如果多个库之间共享许多概念,请考虑将这些概念的类型定义重构到一个通用库中。例如,fuchsia.mem 和 fuchsia.math 分别包含许多用于表示内存和数学概念的常用类型。

首选语义类型

创建结构体来命名常用概念,即使这些概念可以使用基元表示。例如,IPv4 地址是网络库中的一个重要概念,即使数据可以使用基元表示,也应使用结构体进行命名:

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

在对性能要求较高的目标语言中,结构体以内嵌方式表示,这降低了使用结构体来命名重要概念的成本。

zx.Time 具有明确定义的时间基准

zx.Time 类型会单调测量自设备专用时基以来的纳秒数。zx.Time 的使用可以假定此时间基准,无需明确说明。

谨慎使用匿名类型

匿名类型非常适合用来更流畅地描述 API。特别是,如果您事先知道命名类型的子元素与该命名类型固有地相关联,并且在包含该子元素的命名容器的上下文之外使用时没有用处或意义,那么匿名类型非常适合这种情况。

例如,考虑一个联合变体,它将一些内容聚合在一起。 联合体变体极少单独使用,也就是说,我们事先知道联合体变体仅在其特定使用场景中才有意义。因此,建议对联合体变体使用匿名类型。

理想情况下,类型应与 API 的关键概念一一对应,并且不应有两个类型具有相同的定义。不过,并非总是能同时实现这两点,尤其是在类型命名(引入了不同的概念1)具有超出其作为 API Surface 元素的用途的意义时。例如,假设命名标识符 type EntityId = struct { id uint64; }; 和 type OtherEntityId = struct { id uint64; }; 表示不同的概念,但除了名称之外,它们具有相同的类型定义。

使用匿名类型会创建多个类型,这些类型彼此不兼容。因此,如果使用多个匿名类型来表示同一概念,会导致 API 过于复杂,并阻碍大多数目标语言中的通用处理。

因此,在使用匿名类型时,您必须避免使用多个匿名类型来表示同一概念。例如,如果 API 的演变可能会导致多个匿名类型表示同一概念,则不得使用匿名类型。

考虑使用虚拟内存对象 (VMO)

虚拟内存对象 (VMO) 是一个内核对象,表示虚拟内存的连续区域以及逻辑大小。使用此类型在 FIDL 消息中传输内存,并使用 ZX_PROP_VMO_CONTENT_SIZE 属性跟踪对象中包含的数据量。

指定向量和字符串的边界

所有 vector 和 string 声明都应指定长度边界。 声明通常分为以下两类:

  • 数据本身存在限制。例如,包含文件系统名称组件的字符串不得超过 fuchsia.io.MAX_NAME_LENGTH。
  • 除了“尽可能多”之外,没有其他限制。在这些情况下,您应使用内置常量 MAX。

每当您使用 MAX 时,请考虑消息的接收者是否真的想要处理任意长度的序列,或者极长的序列是否代表滥用。

请注意,通过 zx::channel 发送时,所有声明都会隐式受限于最大消息长度。如果确实存在任意长序列的使用场景,那么仅使用 MAX 可能无法解决这些使用场景,因为尝试提供极长序列的客户端可能会达到消息长度上限。

如需处理具有任意大序列的用例,请考虑使用下文讨论的分页模式之一将序列拆分为多条消息,或考虑将数据移出消息本身,例如移入 VMO。

FIDL 配方:大小限制

FIDL 向量和字符串可能带有大小限制,用于指定该类型可以包含的成员数量上限。对于向量,这指的是向量中存储的元素数量;对于字符串,这指的是字符串包含的字节数。

强烈建议使用大小限制,因为这会为原本无限大的类型设置上限。

键值对存储区的一个有用操作是按顺序迭代:即,给定一个键,按顺序返回出现在该键后面的元素列表(通常分页)。

推理

在 FIDL 中,最好使用迭代器来完成此操作,迭代器通常作为单独的协议实现,可以在该协议上进行迭代。使用单独的协议(因此使用单独的通道)有很多好处,包括将迭代拉取请求与通过主协议完成的其他操作去交错。

协议 P 的通道连接的客户端和服务器端可以分别表示为 FIDL 数据类型,即 client_end:P 和 server_end:P。这些类型统称为协议端,代表将 FIDL 客户端连接到其对应服务器的另一种(非 @discoverable)方式:通过现有 FIDL 连接!

协议端是通用 FIDL 概念(资源类型)的特定实例。资源类型旨在包含 FIDL 句柄,因此需要对该类型的使用方式施加额外的限制。该类型必须始终是唯一的,因为底层资源由其他一些功能管理器(通常是 Zircon 内核)进行中介。在不涉及管理器的情况下,通过简单的内存中复制来复制此类资源是不可能的。为防止此类重复,FIDL 中的所有资源类型始终只能移动。

最后,Iterator 协议本身的 Get() 方法会利用返回载荷的大小限制。这会限制单次拉取中可能传输的数据量,从而在一定程度上控制资源使用。它还创建了一个自然的分页边界:服务器无需一次性转储所有结果,而只需一次准备一小批结果。

实现

FIDL、CML 和领域接口定义如下:

FIDL

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

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

客户端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
        {
            config: "fuchsia.examples.keyvaluestore.additerator.WriteItems",
            key: "write_items",
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
            availability: "transitional",
            default: [],
        },

        // A key to iterate from, after all items in `write_items` have been written.
        {
            config: "fuchsia.examples.keyvaluestore.additerator.IterateFrom",
            key: "iterate_from",
            type: "string",
            max_size: 64,
            availability: "transitional",
            default: "",
        },

    ],
}

服务器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

领域

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            dictionary: "diagnostics",
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

然后,您可以使用任何受支持的语言编写客户端和服务器实现:

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

use fidl::endpoints::create_proxy;
use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker};
use futures::join;

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

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>();

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

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

服务器

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

use anyhow::{Context as _, Error};
use fuchsia_component::server::ServiceFs;
use futures::prelude::*;
use regex::Regex;
use std::sync::LazyLock;

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

static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile")
});

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

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

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

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

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

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

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

    Ok(())
}

C++(自然)

客户端

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

字符串编码、字符串内容和长度边界

FIDL string 采用 UTF-8 编码,这是一种可变宽度编码,每个 Unicode 码位使用 1、2、3 或 4 个字节。

绑定会强制对字符串使用有效的 UTF-8,因此字符串不适用于任意二进制数据。请参阅我应该使用 string 还是 vector<byte>?。

由于长度边界声明的目的是为 FIDL 消息的总字节大小提供一个易于计算的上限,因此 string 边界指定了字段中的最大字节数。为安全起见,您通常需要预算 (4 bytes · code points in string)。(如果您确定文本仅使用单字节 ASCII 范围内的码位,例如电话号码或卡号,则每个码位 1 个字节就足够了。)

字符串中有多少个码位?这个问题可能很难回答,尤其是对于用户生成的字符串内容,因为 Unicode 码位与用户可能认为的“字符”之间不一定存在一对一的对应关系。

例如,字符串

á

呈现为单个用户感知的“字符”,但实际上由两个码位组成:

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

在 Unicode 术语中,这种用户感知的“字符”称为字形群集。

单个字形集群可以由任意数量的码位组成。请参考以下较长的示例:

á🇨🇦b👮🏽‍♀️

如果您的系统和字体支持,您应该会在上方看到四个字位簇:

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

这四个字形集群编码为十个码位:

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

在 UTF-8 中,此字符串占用 28 个字节。

从这个示例中可以清楚地看出,如果应用的界面显示一个文本输入框,允许输入 N 个任意字形集群(用户认为的“字符”),并且您计划通过 FIDL 传输这些用户输入的字符串,则必须在 FIDL string 字段中为 4·N 预留数倍的预算。

这个倍数应该是多少?这取决于您的数据。如果您处理的是相当受限的用例(例如人名、邮政地址、信用卡号),则可以假设每个字形集群有 1-2 个码位。如果您要构建一个大量使用表情符号的聊天客户端,那么每个字形聚簇 4-5 个码位可能更安全。在任何情况下,您的输入验证界面都应显示清晰的视觉反馈,以便用户在空间不足时不会感到意外。

整数类型

选择适合您的用例的整数类型,并保持使用方式的一致性。如果您的值最好被视为一个数据字节,请使用 byte。如果负值没有意义,请使用无符号类型。根据经验,如果您不确定,请对少量数据使用 32 位值,对大量数据使用 64 位值。

如果可能存在更多状态,请避免使用布尔值

添加布尔值字段时,如果该字段将来可能会扩展为表示其他状态,请考虑改用枚举。例如,布尔值 is_gif 字段可能更适合表示为

type FileType = strict enum {
    UNKNOWN = 0;
    GIF = 1;
};

然后,可以根据需要使用 JPEG = 2 扩展该枚举。

我应该如何表示错误?

为您的用例选择适当的错误类型,并确保报告错误的方式保持一致。

使用 error 语法清楚地记录和传达可能的错误返回,并利用量身定制的目标语言绑定。

(不推荐使用具有错误枚举模式的可选值。)

使用错误语法

方法可以采用可选的 error <type> 说明符来指示它们会返回值,或者出错并生成 <type>。示例如下:

// Only erroneous statuses are listed
type MyErrorCode = flexible enum {
    MISSING_FOO = 1; // avoid using 0
    NO_BAR = 2;
};

protocol Frobinator {
    Frobinate() -> (struct {
        value SuccessValue;
    }) error MyErrorCode;
};

使用此模式时,您可以使用 int32、uint32 或其枚举来表示返回的错误类型。在大多数情况下,返回枚举是首选方法。

最好在协议的所有方法中使用单一错误类型。

首选特定于网域的枚举

在定义和控制网域时,请使用专门构建的枚举错误类型。例如,如果协议是专门构建的,并且传达错误的语义是唯一的设计限制,则定义枚举。如枚举部分所述,最好避免使用值 0。

在某些情况下,可能适合先使用空的灵活枚举:

type MyEmptyErrorCode = flexible enum {};

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

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

灵活枚举具有默认未知成员。因此,空灵活枚举是一个类型化占位符,可提供可演变性。使用此模式时,建议定义一个独立类型,以便由协议(或库)中的多个方法重复使用,而不是使用匿名枚举。使用匿名枚举会创建多个彼此不兼容的类型,这会导致 API 过于复杂,并会阻止大多数目标语言中的错误通用处理。

如果您遵循明确定义的规范(例如 HTTP 错误代码),并且枚举旨在以符合人体工程学的方式表示规范规定的原始值,请使用特定于网域的枚举错误类型。

具体而言,对于与内核对象或 IO 相关的错误,请使用 zx.Status 类型。例如,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 或枚举值之外,还可能包含错误的字符串说明。不过,包含字符串会带来一些问题。例如,客户端可能会尝试解析字符串以了解发生了什么,这意味着字符串的确切格式会成为协议的一部分,当字符串本地化时,这尤其会带来问题。

安全说明:同样,向客户端报告堆栈轨迹或异常消息可能会无意中泄露特权信息。

本地化字符串和出错提示

如果您要构建的服务充当界面的后端,请使用结构化、类型化的消息,并将渲染留给界面层。

如果您的所有消息都很简单且不含形参,请使用 enum 报告错误和显示常规界面字符串。对于更详细的消息(包含名称、数字和位置等参数),请使用 table 或 xunion,并将参数作为字符串或数字字段传递。

您可能希望在服务中生成消息(英语),并以字符串的形式提供给界面,界面只需接收字符串并弹出通知或错误对话框。

不过,这种更简单的方法存在一些严重缺点:

  • 您的服务是否知道界面中使用的是哪种语言区域(语言和地区)?您必须在每个请求中传递语言区域(请参阅示例),或跟踪每个已连接客户端的状态,以便以正确的语言提供消息。
  • 您的服务的开发环境是否对本地化有良好的支持?如果您使用 C++ 编写代码,则可以轻松访问 ICU 库和 MessageFormat,但如果您使用 Rust,则库支持目前会受到更多限制。
  • 您的任何出错提示是否需要包含界面已知但服务未知的参数?
  • 您的服务是否仅服务于单个界面实现?服务是否知道界面有多少空间可用于显示消息?
  • 错误是否仅以文本形式显示?您可能还需要针对特定错误的提醒图标、音效或文字转语音提示。
  • 用户能否在界面仍在运行时更改显示语言区域?如果发生这种情况,预本地化的字符串可能很难更新到新的语言区域,尤其是当它们是某些非幂等操作的结果时。

除非您要构建与单一界面实现紧密耦合的高度专业化服务,否则您可能不应在 FIDL 服务中公开用户可见的界面字符串。

我是否应该定义一个结构体来封装方法参数(或响应)?

每当您定义方法时,都需要决定是单独传递参数,还是将参数封装在结构体中。做出最佳选择需要权衡多个因素。请考虑以下问题,以帮助您做出决策:

  • 是否存在有意义的封装边界?如果一组参数由于在此方法之外具有某种内聚性而适合作为一个单元传递,您可能需要将这些参数封装在结构体中。(希望您在开始设计协议时已经确定了这些内聚组,因为您遵循了上述“一般建议”,并在早期就专注于类型。)

  • 除了被调用的方法之外,该结构体是否还有其他用途?如果不是,请考虑单独传递参数。

  • 您是否在许多方法中重复使用相同的参数组?如果是,请考虑将这些参数分组到一个或多个结构体中。您可能还需要考虑重复是否表明这些参数具有凝聚力,因为它们代表了协议中的一些重要概念。

  • 是否有大量参数是可选的,或者通常会给定默认值?如果是,请考虑使用结构体来减少调用方的样板代码。

  • 是否存在始终同时为 null 或非 null 的参数组?如果是,请考虑将这些参数分组到一个可为 null 的结构体中,以便在协议本身中强制执行该不变量。例如,当 error 不是 MyError.OK 时,上面定义的 FrobinateResult 结构体包含的值始终同时为 null。

我应该使用 string 还是 vector<byte>?

在 FIDL 中,string 数据必须是有效的 UTF-8,这意味着字符串可以表示 Unicode 码位序列,但不能表示任意二进制数据。相比之下,vector<byte> 或 array<byte, N> 可以表示任意二进制数据,并不意味着 Unicode。

对文本数据使用 string:

  • 使用 string 表示软件包名称,因为软件包名称必须是有效的 UTF-8 字符串(不包含某些排除的字符)。

  • 使用 string 表示软件包中的文件名,因为软件包中的文件名必须是有效的 UTF-8 字符串(不包含某些排除的字符)。

  • 使用 string 表示媒体编解码器名称,因为媒体编解码器名称是从有效 UTF-8 字符串的固定词汇表中选择的。

  • 使用 string 表示 HTTP 方法,因为 HTTP 方法由固定选择的字符组成,这些字符始终是有效的 UTF-8。

对于小型非文本数据,请使用 vector<byte> 或 array<byte, N>:

  • 对 HTTP 标头字段使用 vector<byte>,因为 HTTP 标头字段不指定编码,因此不一定能以 UTF-8 表示。

  • 由于 MAC 地址是二进制数据,因此请对 MAC 地址使用 array<byte, 6>。

  • 对 UUID 使用 array<byte, 16>,因为 UUID 是(几乎!)任意二进制数据。

对 blob 使用共享内存基元:

  • 如果需要完全缓冲数据,请对图片和(大型)protobuf 使用 zx.Handle:VMO。
  • 对于音频和视频流,请使用 zx.Handle:SOCKET,因为数据可能会随着时间的推移而到达,或者在完全写入或可用之前处理数据是有意义的。

我应该使用 vector 还是 array?

vector 是一个可变长度序列,在传输格式中以非内嵌方式表示。array 是一个固定长度的序列,在线格式中以内嵌方式表示。

对于长度可变的数据,请使用 vector:

  • 对日志消息中的标记使用 vector,因为日志消息可以有 0 到 5 个标记。

对固定长度的数据使用 array:

  • 对 MAC 地址使用 array,因为 MAC 地址始终为 6 个字节。

我应该使用 struct 还是 table?

结构体和表都表示具有多个命名字段的对象。不同之处在于,结构体在传输格式中具有固定布局,这意味着如果不破坏二进制兼容性,就无法修改结构体。相比之下,表在传输格式中具有灵活的布局,这意味着可以随着时间的推移向表中添加字段,而不会破坏二进制兼容性。

对于性能关键型协议元素或未来极不可能发生变化的协议元素,请使用结构体。例如,使用结构体来表示 MAC 地址,因为 MAC 地址的结构在未来不太可能发生变化。

对于未来可能会发生变化的协议元素,请使用表格。例如,使用表来表示有关摄像头设备的元数据信息,因为元数据中的字段可能会随时间而变化。

我应该如何表示常量?

您可以通过三种方式来表示常量,具体取决于您拥有的常量类型:

  1. 对特殊值使用 const,例如 PI 或 MAX_NAME_LEN。
  2. 当值是集合的元素时,请使用 enum,例如媒体播放器的重复模式:OFF、SINGLE_TRACK 或 ALL_TRACKS。
  3. 对于构成一组标志的常量,例如接口的功能:WLAN、SYNTH 和 LOOPBACK,请使用 bits。

const

如果您希望以符号形式使用某个值,而不是每次都输入该值,请使用 const。典型的例子是 PI - 它通常被编码为 const,因为这样可以避免每次想要使用此值时都必须输入 3.141592653589。

或者,如果值可能会发生变化,但需要在整个过程中保持一致,您可以使用 const。一个很好的例子是给定字段中可以提供的最大字符数(例如 MAX_NAME_LEN)。通过使用 const,您可以集中定义该数字,从而避免在整个代码中出现不同的值。

选择 const 的另一个原因是,您既可以使用它来限制消息,也可以在代码中使用它。例如:

const MAX_BATCH_SIZE int32 = 128;

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

然后,您可以在代码中使用常量 MAX_BATCH_SIZE 来组合批次。

枚举

如果枚举值集受限且由 Fuchsia 项目控制,请使用枚举。例如,Fuchsia 项目定义了指针事件输入模型,因此控制由 PointerEventPhase 枚举的值。

在某些情况下,即使 Fuchsia 项目本身不控制枚举值集,您也应该使用枚举,前提是我们可以合理地预期,想要注册新值的人会向 Fuchsia 源代码树提交补丁来注册其值。例如,纹理格式需要由 Fuchsia 图形驱动程序理解,这意味着即使纹理格式集由图形硬件供应商控制,开发这些驱动程序的开发者也可以添加新的纹理格式。反例:请勿使用枚举来表示 HTTP 方法,因为我们无法合理地期望使用新颖 HTTP 方法的人员向平台源代码树提交补丁。

对于先验无界集,如果您预见到需要动态扩展该集,则 string 可能是更合适的选择。例如,使用 string 表示媒体编解码器名称,因为中间方可能能够对新的媒体编解码器名称执行一些合理的操作。

如果枚举值集由外部实体控制,请使用整数(大小合适)或 string。例如,使用(某种大小的)整数来表示 USB HID 标识符,因为 USB HID 标识符集由行业联盟控制。同样,使用 string 表示 MIME 类型,因为 MIME 类型由 IANA 注册表控制(至少在理论上是这样)。

我们建议开发者尽可能避免使用 0 作为枚举值。 由于许多目标语言都使用 0 作为整数的默认值,因此很难区分 0 值是故意设置的,还是因为它是默认值而设置的。例如,fuchsia.module.StoryState 定义了三个值:RUNNING(值为 1)、STOPPING(值为 2)和 STOPPED(值为 3)。

在以下两种情况下,适合使用值 0:

位

如果您的协议具有位字段,请使用 bits 值表示其值(如需了解详情,请参阅 RFC-0025:“位标志”)。

例如:

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

这表示 InfoFeatures 位字段由无符号 32 位整数支持,然后继续定义所使用的三个位。

您还可以使用 0b 表示法以二进制(而非十六进制)形式表示值:

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

这与上一个示例相同。

我应该使用 resource 吗?

FIDL 编译器会强制要求将已包含 resource 的任何类型标记为。

如果 flexible 类型不包含资源,但将来可能会包含,则应预先添加 resource 修饰符,以避免日后难以转换。这种情况很少见:经验表明,大多数消息不包含资源,并且在协议中传递资源需要谨慎和提前规划。

我应该在类型中使用 strict 还是 flexible?

将类型标记为 flexible 可以处理当前 FIDL 架构未知的数据,但需要用户考虑这种可能性。具体而言,用户无法对灵活的 bits、enum 或 union 执行详尽的模式匹配。strict 类型通常更易于使用和推理,但它们不允许未来扩展。

在 strict 和 flexible 之间进行选择时,请思考以下问题:

  • 我以后是否可能需要向此类型添加成员?如果是,请使用 flexible。
  • 在极少数情况下,如果我最终确实需要在未来向此类型添加成员,那么最好是引入新类型,而不是向此类型添加成员吗?如果是,请使用 strict。

类型可以从 strict 转换为 flexible,但这是一个缓慢的过程 - 在该类型为 strict 的所有 API 级别都已停用之前,无法向该类型添加新成员。如需开始此转换,请指明自 NEXT 起,类型不再是 strict,而是 flexible:

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

如果类型允许,始终指定此修饰符是一种时尚的做法。Fuchsia 项目通过 linter 检查强制执行此样式。

使用 strict 或 flexible 不会对性能产生任何重大影响。

处理权利

本部分介绍了在 FIDL 中为句柄分配权限限制的最佳实践。

如需详细了解如何在绑定中使用权限,请参阅 FIDL 绑定规范或 RFC-0028。

如需了解 zircon 权限定义,请参阅内核权限。FIDL 使用 rights.fidl 来解析权限限制。

始终指定标识名的权限

所有句柄都应指定权限,以便明确预期用途。此要求会强制您预先决定要传递哪些权限,而不是根据观察到的行为来决定。拥有明确的权限也有助于提高 API 接口的可审核性。

使用接收者所需的最低权限

在确定要提供哪些权限时,请尽量提供最少权限,即实现所需功能所需的最低权限。例如,如果已知只需要 zx.Rights.READ 和 zx.Rights.WRITE,则应仅指定这两项权限。

请勿根据推测的需求添加权限。如果将来需要添加权限,可以从来源开始,将其添加到调用路径中的每个位置,直到最终使用点。

谨慎使用 zx.Rights.SAME_RIGHTS

zx.Rights.SAME_RIGHTS 非常适合用于转发具有未知权限的句柄的协议,但在大多数情况下,应改用一组特定权限。这样做的部分原因是,zx.Rights.SAME_RIGHTS 会告知绑定跳过权限检查,因此它会停用句柄权限可能提供的安全保护。此外,zx.Rights.SAME_RIGHTS 会使权限集变为动态,这意味着进程可能会收到比实际需要更少或更多的权限。

值得注意的是,zx.Rights.SAME_RIGHTS 与为类型设置的默认权限(例如 zx.DEFAULT_CHANNEL_RIGHTS)不同。前者会跳过权限检查,而后者要求给定对象类型的所有常规权限都存在。

良好的设计模式

本部分介绍了在许多 FIDL 协议中反复出现的几种良好设计模式。

我应该在方法和事件中使用 strict 还是 flexible?

将方法或事件标记为 flexible 后,当不同组件可能基于不同版本构建时,可以更轻松地处理方法或事件的移除,例如,某些组件认为方法或事件存在,而与其通信的其他组件则认为不存在。由于通常需要灵活地演进协议,因此建议为方法和事件选择 flexible,除非有充分的理由选择 strict。

将方法设为 flexible 不会给单向方法或事件带来任何开销。对于双向方法,选择 flexible 会给消息增加少量开销(16 字节或更少),并且可能会给消息解码增加少量额外时间。总体而言,使双向方法具有灵活性的成本应该足够小,在几乎所有用例中都不需要考虑。

只有当方法和事件对协议的正确行为至关重要,以至于接收端缺少该方法或事件会严重到应中止两端之间的所有通信并关闭连接时,才应将其设为 strict。

在设计前馈数据流时,这尤其有用。请考虑使用此日志记录器协议,该协议支持一种安全处理包含个人身份信息 (PII) 的日志的模式。它使用前馈模式添加记录,以便客户端可以依序发起多项操作,而无需等待往返时间,只需在最后刷新待处理的操作即可。

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

除 EnablePIIMode 之外,此处的所有方法都是 flexible;请考虑如果服务器无法识别其中任何一种方法,会发生什么情况:

  • AddRecord:服务器只是无法将数据添加到日志输出中。发送应用会正常运行,但其日志记录的实用性会降低。这虽然不方便,但很安全。
  • EnablePIIMode:服务器未能启用 PII 模式,这意味着它可能无法采取安全预防措施并泄露 PII。这是一个严重的问题,因此如果服务器无法识别此方法,最好关闭通道。
  • DisablePIIMode:服务器会针对不需要 PII 日志记录的消息采取不必要的安全预防措施。这可能会给尝试读取日志的用户带来不便,但对系统来说是安全的。
  • Flush:服务器未能按请求刷新记录,这可能会带来不便,但仍然安全。

另一种设计此协议以实现完全灵活性的方法是将 EnablePIIMode 设为双向方法 (flexible EnablePIIMode() -> ();),以便客户端可以了解服务器是否没有该方法。请注意,这为客户端提供了额外的灵活性;通过此设置,客户端可以选择是通过关闭连接还是选择不记录 PII 来响应服务器无法识别 EnablePIIMode 的情况,而使用 strict 时,协议始终会自动关闭。不过,这会中断前馈流程。

请注意,严格程度取决于发件人。假设您在版本 1 中有一个方法 strict A();,然后在版本 2 中将其更改为 flexible A();,最后在版本 3 中将其删除。如果基于版本 1 构建的客户端尝试在基于版本 3 构建的服务器上调用 A(),该方法将被视为严格方法,因为版本 1 的客户端认为该方法是严格方法,而版本 3 的服务器完全不认识该方法,因此会听信客户端。

始终指定此修饰符是一种时尚。Fuchsia 项目通过 linter 检查强制执行此样式。

我应该使用 open、ajar 还是 closed?

将协议标记为 open 后,当不同组件可能是在不同版本上构建时,可以更轻松地处理移除方法或事件,这样每个组件对存在哪些方法和事件都有不同的视图。由于通常需要灵活地演进协议,因此建议为协议选择 open,除非有理由选择更封闭的协议。

决定使用 ajar 还是 closed 应基于对协议演变的预期限制。使用 closed 或 ajar 不会阻止协议演进,但确实需要更长的发布期,在此期间,方法和事件存在但未被使用,以确保所有客户端和服务器都同意存在哪些方法。使用 flexible 的灵活性适用于添加和移除方法和事件,具体取决于客户端还是服务器先更新。

ajar 可能对使用前馈数据流但预计其演变仅限于单向方法的协议有用。例如,这可能适用于表示事务的分离协议,其中唯一的双向方法是必须严格的提交操作,而事务上的其他操作可能会演变。

ajar protocol BazTransaction {
    flexible Add(resource struct {
        bars vector<client_end:Bar>;
    });
    strict Commit() -> (struct {
        args Args;
    });
};

open protocol Foo3 {
    flexible StartBazTransaction(resource struct {
        transaction server_end:BazTransaction;
    });
};

closed 适用于关键协议,在这些协议中,任何未知方法都是严重问题,应导致关闭通道,而不是继续处于潜在的不良状态。对于极不可能发生更改的协议,或者至少任何更改都可能涉及极长发布周期的协议,使用它也是合理的,这样在发布周期中就已经预料到更改 strict 方法所涉及的额外费用。

协议请求流水线

协议请求流水线是最好且最广泛使用的设计模式之一。客户端不会返回支持协议的通道,而是发送通道并请求服务器将协议的实现绑定到该通道:

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

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

此模式非常有用,因为客户端无需等待往返即可开始使用 Bar 协议。相反,客户端可以立即将消息加入 Bar 的队列。这些消息将由内核进行缓冲,并在 Bar 的实现绑定到协议请求后最终得到处理。相反,如果服务器返回 Bar 协议的实例,则客户端需要等待整个往返过程,然后才能将消息加入 Bar 的队列。

如果请求可能失败,请考虑使用描述操作是否成功的回复来扩展此模式:

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

为了处理失败情况,客户端会等待回复,并在请求失败时采取其他一些操作。另一种方法是让协议具有服务器在协议开始时发送的事件:

protocol Codec2 {
    -> OnReady();
};

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

为了处理失败情况,客户端会等待 OnReady 事件,如果 Codec2 通道在事件到达之前关闭,则会执行其他操作。

不过,如果请求很可能会成功,那么这两种成功信号都可能有害,因为信号允许客户端区分不同的失败模式,而这些模式通常应以相同的方式处理。例如,客户端应将建立连接后立即失败的服务与一开始就无法访问的服务视为相同。在这两种情况下,服务都不可用,客户端应生成错误或找到其他方法来完成其任务。

流控制

FIDL 消息由内核进行缓冲。如果一个端点生成的消息多于另一个端点使用的消息,则消息将在内核中累积,从而占用内存并使系统更难恢复。相反,设计良好的协议应限制消息的生成速度,使其与这些消息的使用速率相匹配,此属性称为流控制。

流控制是一个广泛而复杂的主题,有许多有效的设计模式。本部分讨论了一些较为常用的流控制模式,但并未详尽列出。这些模式按偏好降序列出。如果其中一种模式非常适合特定用例,则应使用该模式。但如果不是,协议可以自由使用下面未列出的替代流控制机制。

首选拉取而非推送

如果设计不当,服务器向客户端推送数据的协议通常会出现流量控制不佳的情况。提供更好的流量控制的一种方法是让客户端从服务器拉取一个或一系列数据。拉取模型具有内置的流控制,因为客户端会自然地限制服务器生成数据的速率,并避免被服务器推送的消息淹没。

使用挂起获取延迟响应

实现基于拉取的协议的一种简单方法是使用挂起获取模式在服务器上“停放回调”:

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

在此模式中,客户端会发送 WatchFoo 消息,但服务器在有新信息要发送给客户端之前不会回复。客户端使用 foo 并立即发送另一个挂起 get。客户端和服务器分别对每个数据项执行一个工作单元,这意味着两者都不会领先于对方。

当传输的数据项集大小有限且服务器端状态简单时,挂起获取模式非常有效,但在客户端和服务器需要同步其工作的情况下,该模式效果不佳。

例如,服务器可能会使用每个客户端的“脏”位为某些可变状态 foo 实现挂起获取模式。它会将此位初始化为 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);
};

CML

客户端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.addlinemetered.Instance" },

        // A script for the client to follow. Entries in the script may take one of two forms: a
        // pair of signed-integer coordinates like "-2,15:4,5", or the string "WAIT". The former
        // calls `AddLine(...)`, while the latter pauses execution until the next `->OnDrawn(...)`
        // event is received.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use a vector of `union { Point; WaitEnum}`
        //   here.
        {
            config: "fuchsia.examples.canvas.addlinemetered.Script",
            key: "script",
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
            availability: "transitional",
            default: [],
        },
    ],
}

服务器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.canvas.addlinemetered.Instance" },
    ],
    expose: [
        {
            protocol: "examples.canvas.addlinemetered.Instance",
            from: "self",
        },
    ],
}

领域

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.canvas.addlinemetered.Instance",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            dictionary: "diagnostics",
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

然后,您可以使用任何受支持的语言编写客户端和服务器实现:

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, format_err};
use config::Config;
use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point};
use fuchsia_component::client::connect_to_protocol;
use futures::TryStreamExt;
use std::{thread, time};

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

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send Instance requests
    // across the channel.
    let instance = connect_to_protocol::<InstanceMarker>()?;
    println!("Outgoing connection enabled");

    for action in config.script.into_iter() {
        // If the next action in the script is to "WAIT", block until an OnDrawn event is received
        // from the server.
        if action == "WAIT" {
            let mut event_stream = instance.take_event_stream();
            loop {
                match event_stream
                    .try_next()
                    .await
                    .context("Error getting event response from proxy")?
                    .ok_or_else(|| format_err!("Proxy sent no events"))?
                {
                    InstanceEvent::OnDrawn { top_left, bottom_right } => {
                        println!(
                            "OnDrawn event received: top_left: {:?}, bottom_right: {:?}",
                            top_left, bottom_right
                        );
                        break;
                    }
                    InstanceEvent::_UnknownEvent { ordinal, .. } => {
                        println!("Received an unknown event with ordinal {ordinal}");
                    }
                }
            }
            continue;
        }

        // If the action is not a "WAIT", we need to draw a line instead. Parse the string input,
        // making two points out of it.
        let mut points = action
            .split(":")
            .map(|point| {
                let integers = point
                    .split(",")
                    .map(|integer| integer.parse::<i64>().unwrap())
                    .collect::<Vec<i64>>();
                Point { x: integers[0], y: integers[1] }
            })
            .collect::<Vec<Point>>();

        // Assemble a line from the two points.
        let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?;
        let line = [from, to];

        // Draw a line to the canvas by calling the server, using the two points we just parsed
        // above as arguments.
        println!("AddLine request sent: {:?}", line);

        // By awaiting on the reply, we prevent the client from sending another request before the
        // server is ready to handle, thereby syncing the flow rate between the two parties over
        // this method.
        instance.add_line(&line).await.context("Error sending request")?;
        println!("AddLine response received");
    }

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

服务器

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

use anyhow::{Context as _, Error};
use fidl::endpoints::RequestStream as _;
use fidl_examples_canvas_addlinemetered::{
    BoundingBox, InstanceRequest, InstanceRequestStream, Point,
};
use fuchsia_async::{MonotonicInstant, Timer};
use fuchsia_component::server::ServiceFs;

use fuchsia_sync::Mutex;
use futures::future::join;
use futures::prelude::*;
use std::sync::Arc;

// A struct that stores the two things we care about for this example: the bounding box the lines
// that have been added thus far, and bit to track whether or not there have been changes since the
// last `OnDrawn` event.
#[derive(Debug)]
struct CanvasState {
    // Tracks whether there has been a change since the last send, to prevent redundant updates.
    changed: bool,
    bounding_box: BoundingBox,
}

impl CanvasState {
    /// Handler for the `AddLine` method.
    fn add_line(&mut self, line: [Point; 2]) {
        // Update the bounding box to account for the new lines we've just "added" to the canvas.
        let bounds = &mut self.bounding_box;
        for point in line {
            if point.x < bounds.top_left.x {
                bounds.top_left.x = point.x;
            }
            if point.y > bounds.top_left.y {
                bounds.top_left.y = point.y;
            }
            if point.x > bounds.bottom_right.x {
                bounds.bottom_right.x = point.x;
            }
            if point.y < bounds.bottom_right.y {
                bounds.bottom_right.y = point.y;
            }
        }

        // Mark the state as "dirty", so that an update is sent back to the client on the next tick.
        self.changed = true
    }
}

/// Creates a new instance of the server, paired to a single client across a zircon channel.
async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> {
    // Create a new in-memory state store for the state of the canvas. The store will live for the
    // lifetime of the connection between the server and this particular client.
    let state = Arc::new(Mutex::new(CanvasState {
        changed: true,
        bounding_box: BoundingBox {
            top_left: Point { x: 0, y: 0 },
            bottom_right: Point { x: 0, y: 0 },
        },
    }));

    // Take ownership of the control_handle from the stream, which will allow us to push events from
    // a different async task.
    let control_handle = stream.control_handle();

    // A separate watcher task periodically "draws" the canvas, and notifies the client of the new
    // state. We'll need a cloned reference to the canvas state to be accessible from the new
    // task.
    let state_ref = state.clone();
    let update_sender = || async move {
        loop {
            // Our server sends one update per second.
            Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await;
            let mut state = state_ref.lock();
            if !state.changed {
                continue;
            }

            // After acquiring the lock, this is where we would draw the actual lines. Since this is
            // just an example, we'll avoid doing the actual rendering, and simply send the bounding
            // box to the client instead.
            let bounds = state.bounding_box;
            match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) {
                Ok(_) => println!(
                    "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}",
                    bounds.top_left, bounds.bottom_right
                ),
                Err(_) => return,
            }

            // Reset the change tracker.
            state.changed = false
        }
    };

    // Handle requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    let state_ref = &state;
    let request_handler =
        stream.map(|result| result.context("failed request")).try_for_each(|request| async move {
            // Match based on the method being invoked.
            match request {
                InstanceRequest::AddLine { line, responder } => {
                    println!("AddLine request received: {:?}", line);
                    state_ref.lock().add_line(line);

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

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

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

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

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

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

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

    Ok(())
}

C++(自然)

客户端

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

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

#include <charconv>

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

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

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

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

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

 private:
  async::Loop& loop_;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

服务器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

C++(有线)

客户端

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

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

#include <charconv>

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

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

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

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

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

 private:
  async::Loop& loop_;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

服务器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

使用事件推送有界限的数据

在 FIDL 中,服务器可以向客户端发送主动发送的消息,称为“事件”。 使用事件的协议需要特别注意流量控制,因为事件机制本身不提供任何流量控制。

事件的一个良好应用场景是,在通道的生命周期内,最多发送一个事件实例。在此模式中,协议不需要对事件进行任何流控制:

protocol DeathWish {
    -> OnFatalError(struct {
        error_code zx.Result;
    });
};

事件的另一个适用场景是,客户端请求服务器生成事件,并且服务器生成的事件总数有限。此模式是挂起 get 模式的更复杂版本,其中服务器可以响应“get”请求有限次数(而不仅仅是一次):

protocol NetworkScanner {
    ScanForNetworks();
    -> OnNetworkDiscovered(struct {
        network string;
    });
    -> OnScanFinished();
};

使用确认来限制事件

如果事先不知道事件数量的界限,请考虑让客户端通过发送消息来确认事件。此模式是确认模式的一种更笨拙的版本,其中客户端和服务器的角色互换。与另一种模式一样,服务器应限制事件生成速率,以匹配客户端使用事件的速率:

protocol View1 {
    -> OnInputEvent(struct {
        event InputEvent;
    });
    NotifyInputEventHandled();
};

与常规确认模式相比,此模式的一个优势是,客户端可以更轻松地通过一条消息确认多个事件,因为确认与要确认的事件是分离的。此模式通过减少确认消息的数量来实现更高效的批处理,并且非常适合按顺序处理多种事件类型:

protocol View2 {
    -> OnInputEvent(struct {
        event InputEvent;
        seq uint64;
    });
    -> OnFocusChangedEvent(struct {
        event FocusChangedEvent;
        seq uint64;
    });
    NotifyEventsHandled(struct {
        last_seq uint64;
    });
};

与使用确认的节流推送不同,此模式不会在 FIDL 语法中表达请求与响应之间的关系,因此很容易被误用。只有当客户端正确实现通知消息的发送时,流量控制才会起作用。

FIDL 配方:限制事件模式

事件是从服务器发起的 FIDL 调用。由于这些调用没有内置的客户端响应,因此不受流量控制:服务器可能会将大量此类调用排入队列,并使客户端过载。此问题的一种解决方案是限制事件模式。此模式涉及添加一个由客户端调用的 FIDL 方法,作为要同步到的一个或多个事件的确认点。

在收到来自客户端的下一个确认调用之前,服务器应避免发送更多受限事件(此处的具体语义取决于实现协议)。同样,如果服务器在客户端确认之前发送的受限事件数量超过允许的数量,客户端应关闭连接。这些限制并未内置于 FIDL 运行时中,需要客户端/服务器实现者进行一些手动实现,以确保行为正确。

提高 Instance 协议性能的一种方法是允许批量处理线条:我们不必在每次有新线条要添加到画布时都发送一个 AddLine(...);,等待回复,然后再对下一条线条执行相同操作,而是可以将多条线条批量处理到对新 AddLines(...); 调用的单次调用中。现在,客户端可以决定如何以最佳方式对要绘制的大量线条进行分段。

如果只是简单地实现,我们会发现服务器和客户端完全不同步:客户端可能会向服务器发送大量无限制的 AddLines(...); 调用,而服务器也可能会向客户端发送超出其处理能力的更多 -> OnDrawn(...); 事件。这两个问题的解决方案是添加一个简单的 Ready() -> (); 方法,以便进行同步。每当客户端准备好接收下一个绘制更新时,就会调用此方法,服务器的响应会指示客户端可以继续发出更多请求。

现在,我们已经实现了双向流量控制。该协议现在实现了前馈模式,允许在某些同步“提交”调用触发服务器上的实际工作之前进行许多不受控制的调用。这样可以防止客户端因工作量过大而导致服务器过载。同样,服务器也不再允许发送无限制的 -> OnDrawn(...); 事件:每个事件都必须遵循来自客户端的信号(Ready() -> (); 调用),该信号表示客户端已准备好执行更多工作。这称为限制事件模式。

具体实现必须手动应用其中一些规则:如果客户端收到未通过 Ready() -> (); 方法请求的 -> OnDrawn(...); 事件,则必须关闭连接。

FIDL、CML 和领域接口定义如下:

FIDL

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

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

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

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

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

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

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

CML

客户端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },

        // A script for the client to follow. Entries in the script may take one of two forms: a
        // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former
        // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a
        // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received.
        //
        // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great
        //   deal of string parsing if we were able to use a vector of `union { Point; Ready}` here.
        {
            config: "fuchsia.examples.canvas.clientrequesteddraw.Script",
            key: "script",
            type: "vector",
            max_count: 100,
            element: {
                type: "string",
                max_size: 64,
            },
            availability: "transitional",
            default: [],
        },

    ],
}

服务器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.canvas.clientrequesteddraw.Instance" },
    ],
    expose: [
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "self",
        },
    ],
}

领域

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.canvas.clientrequesteddraw.Instance",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            dictionary: "diagnostics",
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

然后,您可以使用任何受支持的语言编写客户端和服务器实现:

Rust

客户端

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

use anyhow::{Context as _, Error, format_err};
use config::Config;
use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point};
use fuchsia_component::client::connect_to_protocol;
use futures::TryStreamExt;
use std::{thread, time};

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

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send Instance requests
    // across the channel.
    let instance = connect_to_protocol::<InstanceMarker>()?;
    println!("Outgoing connection enabled");

    let mut batched_lines = Vec::<[Point; 2]>::new();
    for action in config.script.into_iter() {
        // If the next action in the script is to "PUSH", send a batch of lines to the server.
        if action == "PUSH" {
            instance.add_lines(&batched_lines).context("Could not send lines")?;
            println!("AddLines request sent");
            batched_lines.clear();
            continue;
        }

        // If the next action in the script is to "WAIT", block until an OnDrawn event is received
        // from the server.
        if action == "WAIT" {
            let mut event_stream = instance.take_event_stream();
            loop {
                match event_stream
                    .try_next()
                    .await
                    .context("Error getting event response from proxy")?
                    .ok_or_else(|| format_err!("Proxy sent no events"))?
                {
                    InstanceEvent::OnDrawn { top_left, bottom_right } => {
                        println!(
                            "OnDrawn event received: top_left: {:?}, bottom_right: {:?}",
                            top_left, bottom_right
                        );
                        break;
                    }
                    InstanceEvent::_UnknownEvent { ordinal, .. } => {
                        println!("Received an unknown event with ordinal {ordinal}");
                    }
                }
            }

            // Now, inform the server that we are ready to receive more updates whenever they are
            // ready for us.
            println!("Ready request sent");
            instance.ready().await.context("Could not send ready call")?;
            println!("Ready success");
            continue;
        }

        // Add a line to the next batch. Parse the string input, making two points out of it.
        let mut points = action
            .split(":")
            .map(|point| {
                let integers = point
                    .split(",")
                    .map(|integer| integer.parse::<i64>().unwrap())
                    .collect::<Vec<i64>>();
                Point { x: integers[0], y: integers[1] }
            })
            .collect::<Vec<Point>>();

        // Assemble a line from the two points.
        let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?;
        let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?;
        let line: [Point; 2] = [from, to];

        // Batch a line for drawing to the canvas using the two points provided.
        println!("AddLines batching line: {:?}", line);
        batched_lines.push(line);
    }

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

服务器

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

use anyhow::{Context as _, Error, anyhow};
use fidl::endpoints::RequestStream as _;
use fidl_examples_canvas_clientrequesteddraw::{
    BoundingBox, InstanceRequest, InstanceRequestStream, Point,
};
use fuchsia_async::{MonotonicInstant, Timer};
use fuchsia_component::server::ServiceFs;
use fuchsia_sync::Mutex;
use futures::future::join;
use futures::prelude::*;
use std::sync::Arc;

// A struct that stores the two things we care about for this example: the bounding box the lines
// that have been added thus far, and bit to track whether or not there have been changes since the
// last `OnDrawn` event.
#[derive(Debug)]
struct CanvasState {
    // Tracks whether there has been a change since the last send, to prevent redundant updates.
    changed: bool,
    // Tracks whether or not the client has declared itself ready to receive more updated.
    ready: bool,
    bounding_box: BoundingBox,
}

/// Handler for the `AddLines` method.
fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) {
    // Update the bounding box to account for the new lines we've just "added" to the canvas.
    let bounds = &mut state.bounding_box;
    for line in lines {
        println!("AddLines printing line: {:?}", line);
        for point in line {
            if point.x < bounds.top_left.x {
                bounds.top_left.x = point.x;
            }
            if point.y > bounds.top_left.y {
                bounds.top_left.y = point.y;
            }
            if point.x > bounds.bottom_right.x {
                bounds.bottom_right.x = point.x;
            }
            if point.y < bounds.bottom_right.y {
                bounds.bottom_right.y = point.y;
            }
        }
    }

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

/// Creates a new instance of the server, paired to a single client across a zircon channel.
async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> {
    // Create a new in-memory state store for the state of the canvas. The store will live for the
    // lifetime of the connection between the server and this particular client.
    let state = Arc::new(Mutex::new(CanvasState {
        changed: true,
        ready: true,
        bounding_box: BoundingBox {
            top_left: Point { x: 0, y: 0 },
            bottom_right: Point { x: 0, y: 0 },
        },
    }));

    // Take ownership of the control_handle from the stream, which will allow us to push events from
    // a different async task.
    let control_handle = stream.control_handle();

    // A separate watcher task periodically "draws" the canvas, and notifies the client of the new
    // state. We'll need a cloned reference to the canvas state to be accessible from the new
    // task.
    let state_ref = state.clone();
    let update_sender = || async move {
        loop {
            // Our server sends one update per second, but only if the client has declared that it
            // is ready to receive one.
            Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await;
            let mut state = state_ref.lock();
            if !state.changed || !state.ready {
                continue;
            }

            // After acquiring the lock, this is where we would draw the actual lines. Since this is
            // just an example, we'll avoid doing the actual rendering, and simply send the bounding
            // box to the client instead.
            let bounds = state.bounding_box;
            match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) {
                Ok(_) => println!(
                    "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}",
                    bounds.top_left, bounds.bottom_right
                ),
                Err(_) => return,
            }

            // Reset the change and ready trackers.
            state.ready = false;
            state.changed = false;
        }
    };

    // Handle requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    let state_ref = &state;
    let request_handler =
        stream.map(|result| result.context("failed request")).try_for_each(|request| async move {
            // Match based on the method being invoked.
            match request {
                InstanceRequest::AddLines { lines, .. } => {
                    println!("AddLines request received");
                    add_lines(&mut state_ref.lock(), lines);
                }
                InstanceRequest::Ready { responder, .. } => {
                    println!("Ready request received");
                    // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();`
                    // event; if two "consecutive" `Ready() -> ();` calls are received, this
                    // interaction has entered an invalid state, and should be aborted immediately.
                    let mut state = state_ref.lock();
                    if state.ready == true {
                        return Err(anyhow!("Invalid back-to-back `Ready` requests received"));
                    }

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

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

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

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

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

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

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

    Ok(())
}

C++(自然)

客户端

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

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

#include <charconv>

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

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

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

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

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

 private:
  async::Loop& loop_;
};

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

      continue;
    }

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

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

服务器

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

C++(有线)

客户端

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

前馈数据流

某些协议具有前馈数据流,通过使数据主要沿一个方向(通常是从客户端到服务器)流动来避免往返延迟。该协议仅在必要时同步两个端点。前馈数据流还会提高吞吐量,因为执行给定任务所需的总上下文切换次数更少。

前馈数据流的关键在于,客户端无需等待先前方法调用的结果,即可发送后续消息。例如,协议请求流水线化可让客户端无需等待服务器回复协议,即可使用该协议。同样,客户端分配的标识符(见下文)使客户端无需等待服务器为服务器持有的状态分配标识符。

通常,前馈协议会涉及客户端提交一系列单向方法调用,而无需等待服务器的响应。提交这些消息后,客户端通过调用具有回复的方法(例如 Commit 或 Flush)来与服务器显式同步。回复可能是空消息,也可能包含有关提交的序列是否成功的信息。在更复杂的协议中,单向消息表示为命令对象的联合,而不是单个方法调用;请参阅下面的命令联合模式。

使用前馈数据流的协议非常适合乐观错误处理策略。不要让服务器使用状态值回复每个方法,这会促使客户端等待每条消息之间的往返,而应仅在方法可能因不受客户端控制的原因而失败时才包含状态回复。如果客户端发送了其本应知道无效的消息(例如,引用了无效的客户端分配标识符),则通过关闭连接来发出错误信号。如果客户端发送了一条它无法知道无效的消息,则要么提供一个表示成功或失败的响应(这需要客户端进行同步),要么记住错误并忽略后续依赖请求,直到客户端以某种方式同步并从错误中恢复。

示例:

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

FIDL 配方:大小限制

FIDL 向量和字符串可能带有大小限制,用于指定该类型可以包含的成员数量上限。对于向量,这指的是向量中存储的元素数量;对于字符串,这指的是字符串包含的字节数。

强烈建议使用大小限制,因为这会为原本无限大的类型设置上限。

键值对存储区的一个有用操作是按顺序迭代:即,给定一个键,按顺序返回出现在该键后面的元素列表(通常分页)。

推理

在 FIDL 中,最好使用迭代器来完成此操作,迭代器通常作为单独的协议实现,可以在该协议上进行迭代。使用单独的协议(因此使用单独的通道)有很多好处,包括将迭代拉取请求与通过主协议完成的其他操作去交错。

协议 P 的通道连接的客户端和服务器端可以分别表示为 FIDL 数据类型,即 client_end:P 和 server_end:P。这些类型统称为协议端,代表将 FIDL 客户端连接到其对应服务器的另一种(非 @discoverable)方式:通过现有 FIDL 连接!

协议端是通用 FIDL 概念(资源类型)的特定实例。资源类型旨在包含 FIDL 句柄,因此需要对该类型的使用方式施加额外的限制。该类型必须始终是唯一的,因为底层资源由其他一些功能管理器(通常是 Zircon 内核)进行中介。在不涉及管理器的情况下,通过简单的内存中复制来复制此类资源是不可能的。为防止此类重复,FIDL 中的所有资源类型始终只能移动。

最后,Iterator 协议本身的 Get() 方法会利用返回载荷的大小限制。这会限制单次拉取中可能传输的数据量,从而在一定程度上控制资源使用。它还创建了一个自然的分页边界:服务器无需一次性转储所有结果,而只需一次准备一小批结果。

实现

FIDL、CML 和领域接口定义如下:

FIDL

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

/// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That
/// is, it must start with a letter, end with a letter or number, contain only letters, numbers,
/// periods, and slashes, and be between 4 and 64 characters long.
type Item = struct {
    key string:128;
    value vector<byte>:64000;
};

/// An enumeration of things that may go wrong when trying to write a value to our store.
type WriteError = flexible enum {
    UNKNOWN = 0;
    INVALID_KEY = 1;
    INVALID_VALUE = 2;
    ALREADY_EXISTS = 3;
};

/// An enumeration of things that may go wrong when trying to create an iterator.
type IterateConnectionError = flexible enum {
    /// The starting key was not found.
    UNKNOWN_START_AT = 1;
};

/// A key-value store which supports insertion and iteration.
@discoverable
open protocol Store {
    /// Writes an item to the store.
    flexible WriteItem(struct {
        attempt Item;
    }) -> () error WriteError;

    /// Iterates over the items in the store, using lexicographic ordering over the keys.
    ///
    /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can
    /// immediately send requests over the new connection.
    ///
    /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining
    flexible Iterate(resource struct {
        /// If present, requests to start the iteration at this item.
        starting_at string:<128, optional>;

        /// The [`Iterator`] server endpoint. The client creates both ends of the channel and
        /// retains the `client_end` locally to use for pulling iteration pages, while sending the
        /// `server_end` off to be fulfilled by the server.
        iterator server_end:Iterator;
    }) -> () error IterateConnectionError;
};

/// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity -
/// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this
/// protocol is not `@discoverable`: it is not independently published by the component that
/// implements it, but rather must have one of its two protocol ends transmitted over an existing
/// FIDL connection.
///
/// As is often the case with iterators, the client indicates that they are done with an instance of
/// the iterator by simply closing their end of the connection.
///
/// Since the iterator is associated only with the Iterate method, it is declared as closed rather
/// than open. This is because changes to how iteration works are more likely to require replacing
/// the Iterate method completely (which is fine because that method is flexible) rather than
/// evolving the Iterator protocol.
closed protocol Iterator {
    /// Gets the next batch of keys.
    ///
    /// The client pulls keys rather than having the server proactively push them, to implement
    /// [flow control][flow-control] over the messages.
    ///
    /// [flow-control]:
    ///     https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push
    strict Get() -> (struct {
        /// A list of keys. If the iterator has reached the end of iteration, the list will be
        /// empty. The client is expected to then close the connection.
        entries vector<string:128>:10;
    });
};

CML

客户端

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/client_bin",
    },
    use: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
        {
            config: "fuchsia.examples.keyvaluestore.additerator.WriteItems",
            key: "write_items",
            type: "vector",
            max_count: 16,
            element: {
                type: "string",
                max_size: 64,
            },
            availability: "transitional",
            default: [],
        },

        // A key to iterate from, after all items in `write_items` have been written.
        {
            config: "fuchsia.examples.keyvaluestore.additerator.IterateFrom",
            key: "iterate_from",
            type: "string",
            max_size: 64,
            availability: "transitional",
            default: "",
        },

    ],
}

服务器

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    include: [ "syslog/client.shard.cml" ],
    program: {
        runner: "elf",
        binary: "bin/server_bin",
    },
    capabilities: [
        { protocol: "examples.keyvaluestore.additerator.Store" },
    ],
    expose: [
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "self",
        },
    ],
}

领域

// Copyright 2022 The Fuchsia Authors. All rights reserved.
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.
{
    children: [
        {
            name: "client",
            url: "#meta/client.cm",
        },
        {
            name: "server",
            url: "#meta/server.cm",
        },
    ],
    offer: [
        // Route the protocol under test from the server to the client.
        {
            protocol: "examples.keyvaluestore.additerator.Store",
            from: "#server",
            to: "#client",
        },

        // Route diagnostics support to all children.
        {
            dictionary: "diagnostics",
            from: "parent",
            to: [
                "#client",
                "#server",
            ],
        },
    ],
}

然后,您可以使用任何受支持的语言编写客户端和服务器实现:

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

use fidl::endpoints::create_proxy;
use fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker};
use futures::join;

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

    // Load the structured config values passed to this component at startup.
    let config = Config::take_from_startup_handle();

    // Use the Component Framework runtime to connect to the newly spun up server component. We wrap
    // our retained client end in a proxy object that lets us asynchronously send `Store` requests
    // across the channel.
    let store = connect_to_protocol::<StoreMarker>()?;
    println!("Outgoing connection enabled");

    // This client's structured config has one parameter, a vector of strings. Each string is the
    // path to a resource file whose filename is a key and whose contents are a value. We iterate
    // over them and try to write each key-value pair to the remote store.
    for key in config.write_items.into_iter() {
        let path = format!("/pkg/data/{}.txt", key);
        let value = std::fs::read_to_string(path.clone())
            .with_context(|| format!("Failed to load {path}"))?;
        match store.write_item(&Item { key: key, value: value.into_bytes() }).await? {
            Ok(_) => println!("WriteItem Success"),
            Err(err) => println!("WriteItem Error: {}", err.into_primitive()),
        }
    }

    if !config.iterate_from.is_empty() {
        // This helper creates a channel, and returns two protocol ends: the `client_end` is already
        // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is
        // unbound and ready to be sent over the wire.
        let (iterator, server_end) = create_proxy::<IteratorMarker>();

        // There is no need to wait for the iterator to connect before sending the first `Get()`
        // request - since we already hold the `client_end` of the connection, we can start queuing
        // requests on it immediately.
        let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end);
        let first_get = iterator.get();

        // Wait until both the connection and the first request resolve - an error in either case
        // triggers an immediate resolution of the combined future.
        let (connection, first_page) = join!(connect_to_iterator, first_get);

        // Handle any connection error. If this has occurred, it is impossible for the first `Get()`
        // call to have resolved successfully, so check this error first.
        if let Err(err) = connection.context("Could not connect to Iterator")? {
            println!("Iterator Connection Error: {}", err.into_primitive());
        } else {
            println!("Iterator Connection Success");

            // Consecutively repeat the `Get()` request if the previous response was not empty.
            let mut entries = first_page.context("Could not get page from Iterator")?;
            while !&entries.is_empty() {
                for entry in entries.iter() {
                    println!("Iterator Entry: {}", entry);
                }
                entries = iterator.get().await.context("Could not get page from Iterator")?;
            }
        }
    }

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

服务器

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

use anyhow::{Context as _, Error};
use fuchsia_component::server::ServiceFs;
use futures::prelude::*;
use regex::Regex;
use std::sync::LazyLock;

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

static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| {
    Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile")
});

/// Handler for the `WriteItem` method.
fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> {
    // Validate the key.
    if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) {
        println!("Write error: INVALID_KEY, For key: {}", attempt.key);
        return Err(WriteError::InvalidKey);
    }

    // Validate the value.
    if attempt.value.is_empty() {
        println!("Write error: INVALID_VALUE, For key: {}", attempt.key);
        return Err(WriteError::InvalidValue);
    }

    // Write to the store, validating that the key did not already exist.
    match store.entry(attempt.key) {
        Entry::Occupied(entry) => {
            println!("Write error: ALREADY_EXISTS, For key: {}", entry.key());
            Err(WriteError::AlreadyExists)
        }
        Entry::Vacant(entry) => {
            println!("Wrote value at key: {}", entry.key());
            entry.insert(attempt.value);
            Ok(())
        }
    }
}

/// Handler for the `Iterate` method, which deals with validating that the requested start position
/// exists, and then sets up the asynchronous side channel for the actual iteration to occur over.
fn iterate(
    store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>,
    starting_at: Option<String>,
    stream: IteratorRequestStream,
) -> Result<(), IterateConnectionError> {
    // Validate that the starting key, if supplied, actually exists.
    if let Some(start_key) = starting_at.clone() {
        if !store.lock().contains_key(&start_key) {
            return Err(IterateConnectionError::UnknownStartAt);
        }
    }

    // Spawn a detached task. This allows the method call to return while the iteration continues in
    // a separate, unawaited task.
    fasync::Task::spawn(async move {
        // Serve the iteration requests. Note that access to the underlying store is behind a
        // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift,
        // change, or disappear entirely between `Get()` requests.
        stream
            .map(|result| result.context("failed request"))
            .try_fold(
                match starting_at {
                    Some(start_key) => Included(start_key),
                    None => Unbounded,
                },
                |mut lower_bound, request| async {
                    match request {
                        IteratorRequest::Get { responder } => {
                            println!("Iterator page request received");

                            // The `page_size` should be kept in sync with the size constraint on
                            // the iterator's response, as defined in the FIDL protocol.
                            static PAGE_SIZE: usize = 10;

                            // An iterator, beginning at `lower_bound` and tracking the pagination's
                            // progress through iteration as each page is pulled by a client-sent
                            // `Get()` request.
                            let held_store = store.lock();
                            let mut entries = held_store.range((lower_bound.clone(), Unbounded));
                            let mut current_page = vec![];
                            for _ in 0..PAGE_SIZE {
                                match entries.next() {
                                    Some(entry) => {
                                        current_page.push(entry.0.clone());
                                    }
                                    None => break,
                                }
                            }

                            // Update the `lower_bound` - either inclusive of the next item in the
                            // iteration, or exclusive of the last seen item if the iteration has
                            // finished. This `lower_bound` will be passed to the next request
                            // handler as its starting point.
                            lower_bound = match entries.next() {
                                Some(next) => Included(next.0.clone()),
                                None => match current_page.last() {
                                    Some(tail) => Excluded(tail.clone()),
                                    None => lower_bound,
                                },
                            };

                            // Send the page. At the end of this scope, the `held_store` lock gets
                            // dropped, and therefore released.
                            responder.send(&current_page).context("error sending reply")?;
                            println!("Iterator page sent");
                        }
                    }
                    Ok(lower_bound)
                },
            )
            .await
            .ok();
    })
    .detach();

    Ok(())
}

/// Creates a new instance of the server. Each server has its own bespoke, per-connection instance
/// of the key-value store.
async fn run_server(stream: StoreRequestStream) -> Result<(), Error> {
    // Create a new in-memory key-value store. The store will live for the lifetime of the
    // connection between the server and this particular client.
    //
    // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`.
    // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The
    // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main
    // task which handles communication over the protocol, and one additional task per iterator
    // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between
    // these racing tasks.
    let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new()));

    // Serve all requests on the protocol sequentially - a new request is not handled until its
    // predecessor has been processed.
    stream
        .map(|result| result.context("failed request"))
        .try_for_each(|request| async {
            // Match based on the method being invoked.
            match request {
                StoreRequest::WriteItem { attempt, responder } => {
                    println!("WriteItem request received");

                    // The `responder` parameter is a special struct that manages the outgoing reply
                    // to this method call. Calling `send` on the responder exactly once will send
                    // the reply.
                    responder
                        .send(write_item(&mut store.clone().lock(), attempt))
                        .context("error sending reply")?;
                    println!("WriteItem response sent");
                }
                StoreRequest::Iterate { starting_at, iterator, responder } => {
                    println!("Iterate request received");

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

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

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

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

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

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

    Ok(())
}

C++(自然)

客户端

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

从设计上保障用户隐私

协议中的客户端和服务器通常可以访问不同的敏感数据集。如果通过协议无意中泄露了不必要的数据,可能会导致隐私或安全问题。

在设计协议时,请特别注意协议中具有以下特征的字段:

  • 包含个人身份信息,例如姓名、邮箱或支付信息。
  • 由用户提供,因此可能包含个人信息。例如设备名称和评论字段。
  • 充当唯一标识符,可跨供应商、用户、设备或重置操作进行关联。例如序列号、MAC 地址、IP 地址和全局账号 ID。

我们会对这些类型的字段进行全面审核,并可能会限制包含这些字段的协议的可用性。确保您的协议不包含超出所需的信息。

如果 API 的某个用例需要个人数据或可关联数据,而其他用例不需要,请考虑使用两种不同的协议,以便单独控制对更敏感用例的访问。

请考虑以下两个假设示例,它们说明了由 API 设计选择引起的隐私权侵犯:

示例 1 - 外围设备控制 API 中的序列号

假设有一个外围设备控制 API,其中包含 USB 外围设备的序列号。序列号不包含个人数据,但它是一个非常稳定的标识符,很容易关联。在此 API 中包含序列号会导致许多隐私问题:

  • 任何有权访问该 API 的客户端都可以关联使用同一 Fuchsia 设备的不同账号。
  • 任何有权访问该 API 的客户端都可以关联账号中的不同角色。
  • 不同的软件供应商可能会串通起来,了解他们是否被同一用户或在同一设备上使用。
  • 如果外围设备在设备之间移动,则任何有权访问该 API 的客户端都可以关联共享该外围设备的一组设备和用户。
  • 如果外围设备被出售,有权访问该 API 的客户端可以将外围设备的新旧所有者关联起来。
  • 有些制造商会在序列号中编码信息。这可能会让有权访问该 API 的客户端推断出用户购买外围设备的时间或地点。

在此示例中,序列号的用途是让客户端能够检测到同一 USB 外围设备何时重新连接。实现此目的确实需要一个稳定的标识符,但不需要全局标识符。不同的客户端不需要接收相同的标识符,同一客户端不需要在不同的 Fuchsia 设备上接收相同的标识符,并且标识符不需要在恢复出厂设置事件之间保持不变。

在此示例中,一个不错的替代方案是发送一个仅保证在单个设备上的单个客户端中保持稳定的标识符。此标识符可能是外围设备序列号、Fuchsia 设备标识符和连接名字对象的哈希值。

示例 2 - 设备设置 API 中的设备名称

假设有一个设备设置 API,其中包含用于协助设置设备的手机型号。在大多数情况下,手机的型号字符串由 OEM 设置,但有些手机会将用户提供的设备名称报告为其型号。这导致许多模型字符串包含用户的真实姓名或假名。因此,此 API 可能会跨身份或跨设备关联用户。即使不是由用户提供,罕见或预发布型号字符串也可能会泄露敏感信息。

在某些情况下,使用模型字符串但限制哪些客户端可以访问 API 可能是合适的。或者,API 可以使用用户无法控制的字段,例如制造商字符串。另一种方法是,通过将模型字符串与热门手机型号的许可清单进行比较,并用通用字符串替换罕见的模型字符串,来清理模型字符串。

客户端分配的标识符

通常,协议会允许客户端操纵服务器持有的多条状态。在设计对象系统时,解决此问题的典型方法是为服务器持有的每个连贯状态片段创建单独的对象。不过,在设计协议时,为每个状态使用单独的对象有几个缺点。

为每个逻辑对象创建单独的协议实例会消耗内核资源,因为每个实例都需要一个单独的通道对象。 每个实例都会维护一个单独的消息 FIFO 队列。为每个逻辑对象使用单独的实例意味着,发送到不同对象的消息可以相互重新排序,从而导致客户端和服务器之间的交互顺序混乱。

客户端分配的标识符模式通过让客户端将 uint32 或 uint64 标识符分配给服务器保留的对象来避免这些问题。客户端和服务器之间交换的所有消息都通过单个协议实例进行汇集,该实例为整个交互提供一致的 FIFO 排序。

让客户端(而不是服务器)分配标识符可实现前馈数据流,因为客户端可以为对象分配标识符,然后立即对该对象执行操作,而无需等待服务器回复对象的标识符。在此模式中,标识符仅在当前连接的范围内有效,并且通常将零标识符保留为标记。安全说明:客户端不应使用其地址空间中的地址作为其标识符,因为这些地址可能会泄露其地址空间的布局。

客户端分配的标识符模式有一些缺点。例如,客户端更难编写,因为客户端需要管理自己的标识符。开发者通常希望创建一个客户端库,为服务提供面向对象的 facade,以隐藏管理标识符的复杂性,这本身就是一种反模式(请参阅下面的客户端库)。

如果您想使用内核的对象功能系统来保护对该对象的访问,则强烈建议您创建一个单独的协议实例来表示对象,而不是使用客户端分配的标识符。例如,如果您希望客户端能够与某个对象交互,但不希望客户端能够与其他对象交互,则创建单独的协议实例意味着您可以将底层通道用作控制对该对象的访问权限的功能。

命令联合

在采用前馈数据流的协议中,客户端通常会先向服务器发送多条单向消息,然后再发送双向同步消息。如果协议涉及的消息量特别大,则发送消息的开销可能会变得很明显。在这些情况下,请考虑使用命令联合模式将多个命令批量处理为单个消息。

在此模式下,客户端会发送一个命令 vector,而不是为每个命令发送一条单独的消息。该矢量包含所有可能命令的联合,除了使用方法序数之外,服务器还使用联合标记作为命令分派的选择器:

type PokeCmd = struct {
    x int32;
    y int32;
};

type ProdCmd = struct {
    message string:64;
};

type MyCommand = strict union {
    1: poke PokeCmd;
    2: prod ProdCmd;
};

protocol HighVolumeSink {
    Enqueue(struct {
        commands vector<MyCommand>;
    });
    Commit() -> (struct {
        result MyStatus;
    });
};

通常,客户端会在其地址空间中本地缓冲命令,并批量发送到服务器。客户端应在达到通道容量限制(以字节和句柄为单位)之前将批次刷新到服务器。

对于消息量更高的协议,请考虑在数据平面的 zx.Handle:VMO 中使用环形缓冲区,并在控制平面的关联 zx.Handle:FIFO 中使用环形缓冲区。此类协议会给客户端和服务器带来更高的实现负担,但当您需要最高性能时,此类协议非常合适。例如,块存储设备协议使用此方法来优化性能。

分页

FIDL 消息通常通过通道发送,通道有最大消息大小限制。在许多情况下,最大消息大小足以传输合理数量的数据,但也有一些使用场景需要传输大量(甚至无限量)数据。传输大量或无限量信息的一种方法是使用分页模式。

分页写入

对写入服务器的数据进行分页的一种简单方法是让客户端在多条消息中发送数据,然后使用“finalize”方法让服务器处理发送的数据:

protocol Foo1 {
    AddBars(resource struct {
        bars vector<client_end:Bar>;
    });
    UseTheBars() -> (struct {
        args Args;
    });
};

例如,fuchsia.process.Launcher 使用此模式来让客户端发送任意数量的环境变量。

此模式的更复杂版本会创建一个表示事务的协议,通常称为拆分协议:

protocol BarTransaction {
    Add(resource struct {
        bars vector<client_end:Bar>;
    });
    Commit() -> (struct {
        args Args;
    });
};

protocol Foo2 {
    StartBarTransaction(resource struct {
        transaction server_end:BarTransaction;
    });
};

当客户端可能同时执行多项操作,并且将写入操作拆分为单独的消息会失去原子性时,此方法非常有用。请注意,BarTransaction 不需要 Abort 方法。更好的中止事务的方法是让客户端关闭 BarTransaction 协议。

分页读取

对来自服务器的读取操作进行分页的一种简单方法是,让服务器使用事件向单个请求发送多个响应:

protocol EventBasedGetter {
    GetBars();
    -> OnBars(resource struct {
        bars vector<client_end:Bar>;
    });
    -> OnBarsDone();
};

根据特定于网域的语义,此模式可能还需要第二个事件,用于指示服务器何时完成数据发送。这种方法适用于简单情况,但存在许多扩缩问题。例如,该协议缺乏流量控制,如果客户端不再需要额外的数据,则无法停止服务器(除非关闭整个协议)。

更可靠的方法是使用 tear-off 协议创建迭代器:

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

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

调用 GetBars 后,客户端会使用协议请求流水线立即将第一个 GetNext 调用加入队列。此后,客户端会重复调用 GetNext 以从服务器读取其他数据,从而限制未完成的 GetNext 消息的数量,以提供流量控制。请注意,迭代器不需要“完成”响应,因为服务器可以回复空向量,然后在完成后关闭迭代器。

另一种分页读取方法是使用令牌。在此方法中,服务器以不透明令牌的形式将迭代器状态存储在客户端上,客户端在每次部分读取时将令牌返回给服务器:

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

当服务器可以将其所有分页状态托管给客户端,从而不再需要维护分页状态时,此模式尤其具有吸引力。服务器应记录客户端是否可以持久保存令牌并在协议实例之间重复使用。安全说明:在任一情况下,服务器都必须验证客户端提供的令牌,以确保客户端的访问权限仅限于其自己的分页结果,而不包含针对其他客户端的结果。

事件对关联

使用客户端分配的标识符时,客户端会使用仅在其与服务器的连接上下文中有意义的标识符来标识服务器持有的对象。不过,某些用例需要关联不同客户端中的对象。例如,在 fuchsia.ui.scenic 中,客户端主要使用客户端分配的标识符与场景图中的节点进行交互。不过,从其他进程导入节点需要跨进程边界关联对该节点的引用。

事件对关联模式通过使用前馈数据流来解决此问题,它依靠内核来提供必要的安全性。首先,希望导出对象的客户端会创建一个 zx::eventpair,并将其中一个纠缠事件以及客户端分配的对象标识符发送到服务器。然后,客户端将另一个纠缠事件发送到另一个客户端,后者使用其自己为现在共享的对象分配的客户端标识符将该事件转发到服务器:

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

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

为了关联这些对象,服务器会使用 ZX_INFO_HANDLE_BASIC 调用 zx_object_get_info,并匹配来自关联事件对象的 koid 和 related_koid 属性。

Eventpair cancellation

使用拆分协议事务时,客户端可以通过关闭协议的客户端端来取消长时间运行的操作。服务器应监听 ZX_CHANNEL_PEER_CLOSED 并中止事务,以避免浪费资源。

对于没有专用渠道的操作,也有类似的用例。 例如,fuchsia.net.http.Loader 协议具有一个用于发起 HTTP 请求的 Fetch 方法。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;
    });
};

需要避免的事项:

  • 避免对部分更新或替换方法使用动词 Set 或 Override,因为所提供的语义会不明确。

  • 避免使用单独的方法来更新设置的字段,例如 SetMagnificationEnabled。此类单独的方法维护起来更繁琐,并且调用方很少希望更新单个值。

  • 避免移除具有魔术值(例如 -1)的设置。相反,您可以通过不提供该设置字段来移除设置。

引用联合体变体和表字段

引用类型的字段通常很有用,例如引用表的一个或多个字段,或引用特定的联合变体。

假设有一个 API,它以包含许多字段的 table 形式提供元数据。如果此元数据可能会变得非常大,那么通常最好有一种机制,让接收方可以向发送方指示将读取此元数据中的哪些字段,从而避免发送接收方不会考虑的多余字段。在这种情况下,如果有一个并行的 bits,其成员与 table 的字段一一对应,则可以为构建 API 奠定坚实的基础:

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

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

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

现在,考虑命令并集。在复杂场景中,服务器可能需要能够描述其支持的命令。在这种情况下,如果有一个并行的 enum,其成员与 union 的变体一一对应,则可以为构建 API 奠定坚实的基础:

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

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

请注意,虽然使用 bits 值来表示命令集可能很有吸引力,但这会导致后续出现一些更困难的选择。如果您的 API 演变为需要引用特定命令,那么使用 enum 会很自然。如果您从 bits 值开始,现在您将面临两个糟糕的选择:

  1. 引入 enum,这意味着现在有两种方法可以引用字段,并且客户端代码中可能存在转换问题(从一种表示法转换为另一种表示法);或者

  2. 继续使用 bits,但限制为在任何给定时间只能设置一个位,现在映射回设置的具体位非常繁琐。

简而言之,对于 table:

  • 使用 table 的名称以及后缀 Fields(复数)来命名 bits。每个成员值都应是序数索引处的位,即 1 << (ordinal - 1)。

  • 与针对 union 的建议类似,您需要匹配 bits 和 table 之间的灵活性,即由于 FIDL 目前仅支持灵活表,因此 bits 必须为 flexible。

对于 union:

  • 为列出所有变体的 enum 命名,方法是使用 union 的名称加上后缀 Variant(单数)。每个成员值都应是其描述的变体的序数。

  • union 和 enum 的灵活性必须一致,即如果 union 为 strict,则 enum 也必须为 strict。

反模式

本部分介绍了几个反模式:通常会提供负值的模式。学会识别这些模式是避免以错误方式使用它们的第一步。

推送设置:尽可能避免

Fuchsia 平台通常更倾向于拉取语义。组件的语义提供了一个重要示例;系统会假定从组件中拉取功能,从而允许组件延迟启动,并从功能路由的有向图中推断组件关停顺序。

使用 FIDL 协议将配置从一个组件推送到另一个组件的设计因其明显的简单性而极具吸引力。当组件 A 将政策推送到组件 B 以实现 B 的业务逻辑时,就会出现这种情况。这会导致平台误解依赖关系:A 不会自动启动以支持 B 的功能,A 可能会在 B 完成其业务逻辑执行之前关闭。这反过来又会导致涉及弱依赖项以及虚假反向依赖项的权宜解决方法,以产生所需的行为;当拉取而不是推送政策时,所有这些都会更简单。

尽可能采用拉取而非推送政策的设计。

客户端库:谨慎使用

理想情况下,客户端使用由 FIDL 编译器生成的特定语言客户端库,与 FIDL 中定义的协议进行交互。 虽然这种方法使 Fuchsia 能够为大量目标语言提供高质量支持,但有时协议级别太低,无法直接编程。在这种情况下,最好提供一个手写的客户端库,该库可连接到相同的底层协议,但更易于正确使用。

例如,fuchsia.io 有一个客户端库 libfdio.so,它为协议提供类似 POSIX 的前端。需要 POSIX 样式的 open/close/read/write 接口的客户端可以链接到 libfdio.so,并使用 fuchsia.io 协议,只需进行极少的修改。此客户端库之所以有价值,是因为它在现有库接口和底层 FIDL 协议之间进行适配。

另一种能带来积极价值的客户端库是框架。框架是一个广泛的客户端库,可为应用的大部分内容提供结构。通常,框架会针对各种协议提供大量抽象。例如,Flutter 是一个框架,可以被视为 fuchsia.ui 协议的广泛客户端库。

无论协议是否有关联的客户端库,都应完整记录 FIDL 协议。一个独立的软件工程师团队应该能够直接根据协议的定义理解并正确使用协议,而无需对客户端库进行逆向工程。如果协议有客户端库,则应清楚地记录协议中那些底层且微妙、足以促使您创建客户端库的方面。

客户端库的主要问题在于,需要针对每种目标语言进行维护,这往往意味着不太常用的语言会缺少客户端库(或质量较低)。客户端库也往往会使底层协议僵化,因为它们会导致每个客户端以完全相同的方式与服务器交互。服务器会逐渐期望这种确切的交互模式,当客户端偏离客户端库使用的模式时,服务器将无法正常工作。

为了在 Fuchsia SDK 中包含客户端库,我们应至少提供两种语言的库实现。

服务中心:谨慎使用

服务中心是一种 Discoverable 协议,可让您发现许多其他协议,这些协议通常具有明确的名称:

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

特别是如果无状态,ServiceHub 协议不会提供太多价值,而只是让各个协议服务直接可被发现:

@discoverable
protocol Foo {};

@discoverable
protocol Bar {};

@discoverable
protocol Baz {};

@discoverable
protocol Qux {};

无论采用哪种方式,客户端都可以与枚举的服务建立连接。 在后一种情况下,客户端可以通过整个系统用于发现服务的常规机制来发现相同的服务。使用常规机制可让核心平台对发现应用适当的政策。

不过,在某些情况下,服务中心可能会很有用。例如,如果协议是有状态的,或者通过比正常服务发现更复杂的过程获得,则该协议可以通过将状态转移到获得的服务来提供价值。再举一个例子,如果获取服务的方法需要额外的参数,那么协议可以通过在连接到服务时考虑这些参数来提供价值。

过度面向对象的设计:否

某些库会为协议中的每个逻辑对象创建单独的协议实例,但这种方法有许多缺点:

  • 不同协议实例之间的消息顺序未定义。 通过单个协议发送的消息会按 FIFO 顺序(在每个方向上)进行处理,但通过不同通道发送的消息会发生竞态。当客户端和服务器之间的交互分布在多个通道中时,如果消息意外重新排序,则出现 bug 的可能性更大。

  • 每个协议实例都会在内核资源、等待队列和调度方面产生费用。虽然 Fuchsia 旨在扩缩到大量通道,但整个系统的成本会增加,并且创建大量对象来对系统中的每个逻辑对象进行建模会给系统带来沉重的负担。

  • 错误处理和拆解要复杂得多,因为错误和拆解状态的数量会随着交互中涉及的协议实例数量呈指数级增长。使用单个协议实例时,客户端和服务器都可以通过关闭协议来彻底关闭互动。如果存在多个协议实例,互动可能会进入部分关闭的状态,或者双方对关闭状态的看法不一致。

    • 跨协议边界的协调比在单个协议内协调更复杂,因为多个协议需要考虑到不同客户端会使用不同协议的可能性,而这些客户端可能并不完全信任彼此。

不过,在某些应用场景中,需要将功能拆分到多个协议中:

  • 提供单独的协议可能有利于提高安全性,因为某些客户端可能只能访问其中一个协议,从而限制了它们与服务器的交互。

  • 此外,还可以更轻松地从不同的线程使用不同的协议。例如,一个协议可能绑定到一个线程,而另一个协议可能绑定到另一个线程。

  • 客户端和服务器需要为协议中的每个方法支付(少量)费用。 如果一次只需要几个较小的协议,那么一个包含所有可能方法的巨型协议可能不如多个较小的协议高效。

  • 有时,服务器持有的状态会沿着方法边界清晰地分解。在这些情况下,请考虑沿这些相同的边界将协议分解为更小的协议,以便为与单独状态交互提供单独的协议。

避免过度面向对象的一种好方法是使用客户端分配的标识符对协议中的逻辑对象进行建模。该模式允许客户端通过单个协议与可能很大的逻辑对象集进行交互。

使用魔法值指定缺失:否

我们通常希望指示服务器设置某种状态,但也允许移除状态。以下示例使用魔术值来指示移除:

// 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 方面具有命名类型语义。 ↩