一般建议
本部分介绍了有关在 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 而非 FIDL 方法/协议,前提是没有其他程序使用该信息来做出运行时决策,并且该信息对测试中的调试、开发工具的使用或在现场通过崩溃报告或指标检索非常有用。
当运行时决策将根据其他程序的诊断信息做出时,应使用 FIDL。Inspect 绝不能用于程序之间的通信,它是一种尽力而为的系统,不得依赖于它在正式版中运行期间做出决策或更改行为。
用于确定是使用 Inspect 还是 FIDL 的启发词语可能是:
数据是否会被正式版中的其他程序使用?
- 是:使用 FIDL。
这些数据是崩溃报告中使用的数据,还是指标中使用的数据?
- 是:使用“检查”。
数据是否会被测试或开发者工具使用?它有机会用于生产环境吗?
- 是:使用 FIDL。
- 否:请使用任一项。
库结构
将 FIDL 声明分组到 FIDL 库有两个具体目标:
- 帮助 FIDL 开发者(使用 FIDL 库的开发者)浏览 API Surface。
- 提供结构,以便在 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}
用于与场景和渲染界面的软件组件交互的输入。如果仅关注版本控制,则可以使用 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 接口元素之外还有其他意义的情况下。例如,假设命名标识符 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 句柄,因此需要对此类型的使用方式施加额外限制。该类型始终必须是唯一的,因为底层资源由某些其他 capability 管理器(通常是 Zircon 内核)中介。在不涉及管理器的情况下,通过简单的内存复制来复制此类资源是不可能的。为防止出现此类重复,FIDL 中的所有资源类型始终都是只移动的。
最后,Iterator
协议本身的 Get()
方法会对返回载荷使用大小限制。这会限制单次拉取中可能传输的数据量,从而实现一定程度的资源使用控制。它还会创建自然的分页边界:服务器只需一次准备小批次数据,而不是一次性转储所有结果。
实现
FIDL、CML 和 Realm 接口定义如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], config: { write_items: { type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, }, // A key to iterate from, after all items in `write_items` have been written. iterate_from: { type: "string", max_size: 64, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
领域
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
然后,您可以使用任何受支持的语言编写客户端和服务器实现:
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 lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, Mutex}; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++ (Wire)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
字符串编码、字符串内容和长度边界
FIDL string
采用 UTF-8 编码,这是一种可变宽度编码,每个 Unicode 码点使用 1、2、3 或 4 个字节。
绑定会强制为字符串使用有效的 UTF-8,因此字符串不适用于任意二进制数据。请参阅我应该使用 string
还是 vector<byte>
?。
由于长度边界声明的目的是为 FIDL 消息的总字节大小提供一个易于计算的上限,因此 string
边界会指定字段中的最大字节数。为安全起见,您通常应预留 (4 bytes · code points in
string)
的预算。(如果您确信文本仅使用单字节 ASCII 范围内的码位(例如电话号码或信用卡号码),则每个码位 1 个字节就足够了。)
字符串中有多少个代码点?这个问题的答案可能很复杂,尤其是对于用户生成的字符串内容,因为 Unicode 代码点与用户可能认为的“字符”之间不一定是一对一对应的关系。
例如,字符串
á
会呈现为用户感知的单个“字符”,但实际上由两个代码点组成:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
在 Unicode 术语中,这种用户感知的“字符”称为字形簇。
单个字符集群可以包含任意多个代码点。请参考以下较长的示例:
á🇨🇦b👮🏽♀️
如果您的系统和字体支持,您应该会在上面看到四个字符图形集群:
1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone
这四个字符簇的编码方式为 10 个代码点:
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 = strict 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 的结构体中,以便在协议本身中强制执行该不变性。例如,上面定义的
FrobinateResult
结构体在error
不为MyError.OK
时,包含始终为 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 地址使用
array<byte, 6>
,因为 MAC 地址是二进制数据。请为 UUID 使用
array<byte, 16>
,因为 UUID 是(几乎!)任意二进制数据。
对 blob 使用共享内存基元:
- 当完全缓冲数据有意义时,请对图片和(大型)Protobuf 使用
zx.Handle:VMO
。 - 请针对音频和视频流使用
zx.Handle:SOCKET
,因为数据可能会随着时间的推移而到达,或者在数据完全写入或可用之前处理数据是有意义的。
我应该使用 vector
还是 array
?
vector
是一个长度可变的序列,以线格格式离线表示。array
是线格格式中以内嵌方式表示的固定长度序列。
对于长度可变的数据,请使用 vector
:
- 请使用
vector
作为日志消息中的标记,因为日志消息可以包含 0 到 5 个标记。
使用 array
处理固定长度数据:
- 请使用
array
表示 MAC 地址,因为 MAC 地址的长度始终为 6 个字节。
我应该使用 struct
还是 table
?
结构体和表都表示具有多个命名字段的对象。不同之处在于,结构体在线格格式中具有固定的布局,这意味着,如果不破坏二进制兼容性,则不能对其进行修改。相比之下,表在线格格式中具有灵活的布局,这意味着可以随着时间的推移向表中添加字段,而不会破坏二进制兼容性。
请针对性能至关重要的协议元素或未来不太可能发生变化的协议元素使用结构体。例如,使用结构体来表示 MAC 地址,因为 MAC 地址的结构将来不太可能发生变化。
对于未来可能会发生变化的协议元素,请使用表格。例如,使用表格来表示摄像头设备的元数据信息,因为元数据中的字段可能会随时间推移而演变。
如何表示常量?
您可以通过以下三种方式表示常量,具体取决于您所拥有的常量类型:
- 使用
const
表示特殊值,例如 PI 或 MAX_NAME_LEN。 - 如果值是集合的元素(例如媒体播放器的重复模式:OFF、SINGLE_TRACK 或 ALL_TRACKS),请使用
enum
。 - 使用
bits
表示用于组成一组标志的常量,例如接口的功能:WLAN、SYNTH 和 LOOPBACK。
const
如果您希望以符号形式使用某个值,而不是每次都输入该值,请使用 const
。典型示例是 PI,它通常编码为 const
,因为这样您每次使用此值时就不必输入 3.141592653589
,非常方便。
或者,如果值可能会发生变化,但在整个过程中需要始终保持一致,您也可以使用 const
。给定字段中可提供的字符数上限就是一个很好的示例(例如MAX_NAME_LEN)中减去相应时间戳。通过使用 const
,您可以集中定义该数字,从而避免在整个代码中出现不同的值。
选择 const
的另一个原因是,您既可以使用它来约束消息,也可以在后续的代码中使用它。例如:
const MAX_BATCH_SIZE int32 = 128;
protocol Sender {
Emit(struct {
batch vector<uint8>:MAX_BATCH_SIZE;
});
};
然后,您可以在代码中使用常量 MAX_BATCH_SIZE
来组装批处理。
枚举
如果枚举值集由 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
定义了三个值:值为 1
的 RUNNING
、值为 2
的 STOPPING
和值为 3
的 STOPPED
。
在以下两种情况下,使用值 0
是适当的:
- 枚举具有自然的默认、初始或未知状态;
- 该枚举定义了包含错误枚举的可选值模式中使用的错误代码。
位
如果您的协议包含位字段,请使用 bits
值表示其值(如需了解详情,请参阅 RFC-0025
:“位标志”)。
例如:
type InfoFeaturesHex = strict bits : uint32 {
WLAN = 0x00000001; // If present, this device represents WLAN hardware
SYNTH = 0x00000002; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0x00000004; // If present, this device receives all messages it sends
};
这表示 InfoFeatures
位字段由无符号 32 位整数支持,然后继续定义所使用的三位。
您还可以使用 0b
表示法以二进制(而非十六进制)表示值:
type InfoFeaturesBits = strict bits : uint32 {
WLAN = 0b00000001; // If present, this device represents WLAN hardware
SYNTH = 0b00000010; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0b00000100; // If present, this device receives all messages it sends
};
这与上一个示例相同。
我是否应使用 resource
?
FIDL 编译器会强制要求已包含 resource
的任何类型都进行标记。
如果 flexible
类型不包含资源,但将来可能会包含,则应预先添加 resource
修饰符,以免日后难以过渡。这种情况很少见:经验表明,大多数消息不包含资源,并且在协议中传递资源需要谨慎和提前规划。
我应该对类型使用 strict
还是 flexible
?
将类型标记为 flexible
后,便可以处理当前 FIDL 架构中未知的数据,对于日后可能会添加或移除成员的类型(例如配置、元数据或错误),建议采用此方法。对于现有类型,始终可以在 strict
和 flexible
之间进行软过渡。
在类型允许的情况下,始终指定此修饰符会更美观。Fuchsia 项目通过 lint 检查强制执行此样式。
使用 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() -> ();
};
这里的所有方法都是 flexible
,EnablePIIMode
除外;请考虑如果服务器不识别其中任何一种方法,会发生什么情况:
AddRecord
:服务器只是未能将数据添加到日志输出中。发送应用的行为正常,但其日志记录的用处会降低。这不太方便,但很安全。EnablePIIMode
:服务器未能启用个人身份信息模式,这意味着它可能未能采取安全预防措施并泄露个人身份信息。这是一个严重问题,因此,如果服务器不识别此方法,最好关闭该渠道。DisablePIIMode
:服务器对不需要记录个人身份信息的消息采取了不必要的安全预防措施。这可能会给尝试读取日志的用户带来不便,但对系统来说是安全的。Flush
:服务器未能按请求刷新记录,这可能会带来不便,但仍然是安全的。
若要使此协议具有完全的灵活性,另一种设计方法是将 EnablePIIMode
设为双向方法 (flexible EnablePIIMode() -> ();
),以便客户端能够确定服务器是否不支持该方法。请注意,这如何为客户端带来额外的灵活性;采用这种设置后,客户端可以选择通过关闭连接或仅选择不记录个人身份信息来响应服务器不识别 EnablePIIMode
,而使用 strict
时,协议始终会自动关闭。不过,这确实会中断前馈流。
请注意,审核严格程度取决于发件人。假设您在版本 1 中使用了某个方法 strict A();
,然后在版本 2 中将其更改为 flexible A();
,最后在版本 3 中将其删除。如果版本 1 构建的客户端尝试在版本 3
构建的服务器上调用 A()
,该方法将被视为严格方法,因为版本 1 的客户端认为该方法是严格方法,而版本 3 的服务器会相信客户端,因为它根本不认识该方法。
始终指定此修饰符会使代码看起来更简洁。Fuchsia 项目通过 lint 检查强制执行此样式。
我应该使用 open
、ajar
还是 closed
?
将协议标记为 open
后,当不同组件可能在不同版本中构建时,便可更轻松地处理移除方法或事件的问题,因为每个组件对存在的方法和事件都有不同的看法。由于通常希望协议具有灵活性,因此建议为协议选择 open
,除非有理由选择更封闭的协议。
决定使用 ajar
还是 closed
应基于对协议演变的预期约束条件。使用 closed
或 ajar
不会阻止协议演变,但会要求延长发布周期,在该周期内,方法和事件存在但不使用,以确保所有客户端和服务器都同意存在哪些方法。使用 flexible
的灵活性适用于添加和移除方法和事件,具体取决于先更新的是客户端还是服务器。
对于使用前馈数据流但预计其演变仅限于单向方法的协议,ajar
可能很有用。例如,这适用于表示事务的分离协议,其中唯一的双向方法是提交操作,该操作必须严格执行,而对事务的其他操作可能会发生变化。
ajar protocol BazTransaction {
flexible Add(resource struct {
bars vector<client_end:Bar>;
});
strict Commit() -> (struct {
args Args;
});
};
open protocol Foo3 {
flexible StartBazTransaction(resource struct {
transaction server_end:BazTransaction;
});
};
closed
适用于任何未知方法都属于严重问题的关键协议,这类问题应导致关闭通道,而不是继续处于可能不良的状态。对于极不可能发生变化的协议,或者至少任何更改都可能涉及极长的发布周期,因此在发布周期中预计会发生更改 strict
方法的额外费用,也可以合理地使用此方法。
协议请求流水线
最优秀且最广泛使用的设计模式之一是协议请求流水线。客户端不会返回支持某个协议的通道,而是会发送该通道并请求服务器将该协议的实现绑定到该通道:
// GOOD:
protocol GoodFoo {
GetBar(resource struct {
name string;
bar server_end:Bar;
});
};
// BAD:
protocol BadFoo {
GetBar(struct {
name string;
}) -> (resource struct {
bar client_end:Bar;
});
};
这种模式非常有用,因为客户端无需等待往返时间即可开始使用 Bar
协议。而是可以立即将消息加入 Bar
队列。这些消息将由内核缓冲,并在 Bar
实现绑定到协议请求后最终进行处理。相比之下,如果服务器返回 Bar
协议的实例,客户端需要等待整个往返过程,然后才能为 Bar
加入消息队列。
如果请求可能会失败,不妨考虑通过描述操作是否成功的回复来扩展此模式:
protocol CodecProvider {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec;
}) -> (struct {
succeed bool;
});
};
为了处理失败情况,客户端会等待回复,并在请求失败时执行一些其他操作。另一种方法是让协议包含服务器在协议开始时发送的事件:
protocol Codec2 {
-> OnReady();
};
protocol CodecProvider2 {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec2;
});
};
为了处理失败情况,客户端会等待 OnReady
事件,如果 Codec2
通道在事件到达之前关闭,则执行其他操作。
但是,如果请求可能成功,则发送任何一种成功信号都可能会有害,因为信号可让客户端区分通常应以相同方式处理的不同失败模式。例如,客户端应将在建立连接后立即失败的服务视为无法访问的服务。在这两种情况下,服务都不可用,客户端应生成错误或另寻他法来完成任务。
流控制
FIDL 消息由内核缓冲。如果一个端点生成的消息比另一个端点消耗的消息多,这些消息会在内核中累积,占用内存,使系统更难恢复。相反,设计良好的协议应节流消息生成,以匹配这些消息的使用速率,这项属性称为流控制。
流控制是一个广泛而复杂的主题,其中包含许多有效的设计模式。本部分介绍了一些较为常见的流控制模式,但并非详尽无遗。这些模式按偏好度降序排列。如果其中一种模式适用于特定用例,则应使用该模式。但如果没有,协议可以自由使用下方未列出的替代流量控制机制。
首选拉取
如果不进行精心设计,服务器将数据推送到客户端的协议通常具有较差的流控制。若要提供更好的流控制,一种方法是让客户端从服务器拉取一个或多个范围。拉取模型具有内置的流量控制,因为客户端会自然地限制服务器生成数据的速率,并避免被服务器推送的消息淹没。
使用挂起的 get 延迟响应
实现基于拉取的协议的一种简单方法是使用挂起 get 模式在服务器上“停车回调”:
protocol FooProvider {
WatchFoo(struct {
args Args;
}) -> (resource struct {
foo client_end:Foo;
});
};
在此模式中,客户端会发送 WatchFoo
消息,但服务器不会回复,除非它有新信息要发送给客户端。客户端会使用 foo
,并立即发送另一个挂起的 get 请求。客户端和服务器每个数据项执行一项工作,这意味着两者不会出现先后之分。
当传输的数据项集大小受限且服务器端状态简单时,挂起 get 模式非常适用,但在客户端和服务器需要同步其工作的情况下,这种模式效果不佳。
例如,服务器可能会针对某些可变状态 foo
实现挂起 get 模式,为每个客户端使用一个“脏”位。它会将此位初始化为 true,在每次 WatchFoo
响应时清除此位,并在每次 foo
更改时设置此位。只有在脏位设置后,服务器才会响应 WatchFoo
消息。
使用确认来节流推送
在使用推送的协议中提供流控制的方法之一是确认模式,其中调用方提供调用方用于流控制的确认响应。例如,请考虑以下通用监听器协议:
protocol Listener {
OnBar(struct {
args Args;
}) -> ();
};
监听器应在收到 OnBar
消息后立即发送空响应消息。响应不会向调用方传达任何数据。而是让调用方观察调用对象使用消息的速率。调用方应节流其生成消息的速率,以匹配被调用方使用消息的速率。例如,调用方可以安排只有 1 条(或固定数量)消息处于传输中(即等待确认)。
FIDL 方案:确认模式
确认模式是一种简单的流控制方法,适用于原本是单向调用的其他方法。系统会将该方法转换为无响应的双向调用,而不是将其保留为一项单向调用,这在通常情况下称为 ack。确认的唯一目的是告知发件人已收到邮件,以便发件人据此决定后续操作。
此确认的代价是增加了频道上的杂音。如果客户端在等待确认后再继续进行下一次调用,这种模式也可能会导致性能下降。
来回发送不计量的单向调用会产生简单的设计,但存在潜在的陷阱:如果服务器处理更新的速度比客户端发送更新的速度慢得多,该怎么办?例如,客户端可能会从某个文本文件加载由数千行组成的绘图,并尝试按顺序发送所有这些行。如何向客户端施加回压,以防止服务器因这波更新而过载?
通过使用确认模式并将单向调用 AddLine(...);
转换为双向调用 AddLine(...) -> ();
,我们可以向客户端提供反馈。这样,客户端就可以酌情节流其输出。在此示例中,我们只会让客户端等待 ACK,然后再发送其正在等待的下一条消息,不过更复杂的设计可能会以乐观的方式发送消息,并且仅在收到异步 ACK 的频率低于预期时才会进行节流。
首先,我们需要定义接口定义和测试框架。FIDL、CML 和 Realm 接口定义设置了一个任意实现都可以使用的框架:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.addlinemetered; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add a line to the canvas. /// /// This method can be considered an improvement over the one-way case from a flow control /// perspective, as it is now much more difficult for a well-behaved client to "get ahead" of /// the server and overwhelm. This is because the client now waits for each request to be acked /// by the server before proceeding. This change represents a trade-off: we get much greater /// synchronization of message flow between the client and the server, at the cost of worse /// performance at the limit due to the extra wait imposed by each ack. flexible AddLine(struct { line Line; }) -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.addlinemetered.Instance" }, ], config: { // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "WAIT". The former // calls `AddLine(...)`, while the latter pauses execution until the next `->OnDrawn(...)` // event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; WaitEnum}` // here. script: { type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.addlinemetered.Instance" }, ], expose: [ { protocol: "examples.canvas.addlinemetered.Instance", from: "self", }, ], }
领域
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.addlinemetered.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
然后,您可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{format_err, Context as _, Error}; use config::Config; use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); for action in config.script.into_iter() { // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } continue; } // If the action is not a "WAIT", we need to draw a line instead. Parse the string input, // making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let line = [from, to]; // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. println!("AddLine request sent: {:?}", line); // By awaiting on the reply, we prevent the client from sending another request before the // server is ready to handle, thereby syncing the flow rate between the two parties over // this method. instance.add_line(&line).await.context("Error sending request")?; println!("AddLine response received"); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use futures::future::join; use futures::prelude::*; use std::sync::{Arc, Mutex}; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, bounding_box: BoundingBox, } impl CanvasState { /// Handler for the `AddLine` method. fn add_line(&mut self, line: [Point; 2]) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut self.bounding_box; for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. self.changed = true } } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change tracker. state.changed = false } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLine { line, responder } => { println!("AddLine request received: {:?}", line); state_ref.lock().unwrap().add_line(line); // Because this is now a two-way method, we must use the generated `responder` // to send an in this case empty reply back to the client. This is the mechanic // which syncs the flow rate between the client and server on this method, // thereby preventing the client from "flooding" the server with unacknowledged // work. responder.send().context("Error responding")?; println!("AddLine response sent"); } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This await does not complete, and thus the function does not return, unless the server errors // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first // wait for the updater task to realize the connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override { auto top_left = event.top_left(); auto bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::Point(x, y); } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.is_ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.error_value().FormatDescription(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override { auto points = request.line(); FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request.line()) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override { auto top_left = event->top_left; auto bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine( const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override { auto points = request->line; FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request->line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. weak->state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <examples/fidl/new/canvas/add_line_metered/hlcpp/client/config.h> #include "lib/fpromise/result.h" // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::addlinemetered::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples::canvas::addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::addlinemetered::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop]( ::examples::canvas::addlinemetered::Point top_left, ::examples::canvas::addlinemetered::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; instance_proxy->AddLine(line, [&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do to // recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples::canvas::addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::addlinemetered::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) : binding_(fidl::Binding<examples::canvas::addlinemetered::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(::std::array<::examples::canvas::addlinemetered::Point, 2> line, AddLineCallback callback) override { FX_LOGS(INFO) << "AddLine request received: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |callback| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. callback(fpromise::ok()); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::Binding<examples::canvas::addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached to // the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::addlinemetered::Instance>( [dispatcher](fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
使用事件推送边界数据
在 FIDL 中,服务器可以向客户端发送名为事件的非请求消息。使用事件的协议需要特别注意流控制,因为事件机制本身不提供任何流控制。
事件的一个很好的用例是,在渠道生命周期内最多发送一个事件实例。在此模式中,协议不需要对事件进行任何流控制:
protocol DeathWish {
-> OnFatalError(struct {
error_code zx.Status;
});
};
事件的另一个很好的用例是,客户端请求服务器生成事件,并且服务器生成的事件总数是有限的。此模式是挂起 get 模式的更复杂版本,其中服务器可以对“get”请求做出有限次数的响应(而不仅仅是一次):
protocol NetworkScanner {
ScanForNetworks();
-> OnNetworkDiscovered(struct {
network string;
});
-> OnScanFinished();
};
使用确认来节流事件
如果事先不知道事件数量的上限,不妨考虑让客户端通过发送消息来确认事件。此模式是确认模式的一种更不方便的变体,其中客户端和服务器的角色会切换。与其他模式一样,服务器应节流事件生成,以匹配客户端使用事件的速率:
protocol View1 {
-> OnInputEvent(struct {
event InputEvent;
});
NotifyInputEventHandled();
};
与常规确认模式相比,此模式的一个优势是,客户端可以更轻松地使用单个消息确认多个事件,因为确认与要确认的事件解除关联。这种模式通过减少确认消息的数量,可更高效地进行批量处理,并且非常适合按顺序处理多种事件类型:
protocol View2 {
-> OnInputEvent(struct {
event InputEvent;
seq uint64;
});
-> OnFocusChangedEvent(struct {
event FocusChangedEvent;
seq uint64;
});
NotifyEventsHandled(struct {
last_seq uint64;
});
};
与使用确认的节流推送不同,此模式不会使用 FIDL 语法表达请求与响应之间的关系,因此容易被滥用。只有当客户端正确实现通知消息的发送时,流控制才会起作用。
FIDL 食谱:节流事件模式
事件是从服务器发起的 FIDL 调用。由于这些调用没有内置的客户端响应,因此不受流量控制:服务器可能会将大量此类调用加入队列,并使客户端发生洪泛。解决此问题的一种方法是节流事件模式。此模式涉及添加一个由客户端调用的 FIDL 方法,以用作一个或多个事件同步到的确认点。
在收到来自客户端的下一个确认调用之前,服务器应避免发送更多受限事件(此处的确切语义因实现协议而异)。同样,如果服务器在客户端确认事件之前发送的节流事件超出允许数量,客户端应关闭连接。这些限制并未内置于 FIDL 运行时,需要客户端/服务器实现者进行一些手动实现,以确保正确的行为。
提高 Instance
协议性能的一种方法是允许批量处理线条:我们每次有要添加到画布的新线条时,不必发送单个 AddLine(...);
,等待回复,然后再为下一条线条重复此操作,而是可以将多条线条批量处理到对新 AddLines(...);
调用的单次调用中。客户端现在可以决定如何最好地对要绘制的大量线条进行细分。
如果简单地实现,我们会发现服务器和客户端完全不同步:客户端可能会向服务器发送无限数量的 AddLines(...);
调用,而服务器也可能会向客户端发送超出其处理能力的 -> OnDrawn(...);
事件。解决这两个问题的方法是添加一个简单的 Ready() -> ();
方法以进行同步。每当客户端准备好接收下一个绘制更新时,都会调用此方法,服务器的响应会指明客户端可以继续发出更多请求。
现在,我们在两个方向上都实现了流量控制。该协议现在实现了前馈模式,允许在某些同步“提交”调用触发服务器上的实际工作之前进行许多不受控制的调用。这样可以防止客户端的工作量过多而使服务器过载。同样,服务器也不再允许发送无限的 -> OnDrawn(...);
事件:每个事件都必须遵循来自客户端的信号(即 Ready() -> ();
调用),该信号表示客户端已准备好执行更多工作。这称为“节流事件模式”。
具体实现必须手动应用其中的一些规则:如果客户端收到未通过 Ready() -> ();
方法请求的 -> OnDrawn(...);
事件,则必须关闭连接。
FIDL、CML 和 Realm 接口定义如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.canvas.clientrequesteddraw; /// A point in 2D space. type Point = struct { x int64; y int64; }; /// A line in 2D space. alias Line = array<Point, 2>; /// A bounding box in 2D space. This is the result of "drawing" operations on our canvas, and what /// the server reports back to the client. These bounds are sufficient to contain all of the /// lines (inclusive) on a canvas at a given time. type BoundingBox = struct { top_left Point; bottom_right Point; }; /// Manages a single instance of a canvas. Each session of this protocol is responsible for a new /// canvas. @discoverable open protocol Instance { /// Add multiple lines to the canvas. We are able to reduce protocol chatter and the number of /// requests needed by batching instead of calling the simpler `AddLine(...)` one line at a /// time. flexible AddLines(struct { lines vector<Line>; }); /// Rather than the server randomly performing draws, or trying to guess when to do so, the /// client must explicitly ask for them. This creates a bit of extra chatter with the additional /// method invocation, but allows much greater client-side control of when the canvas is "ready" /// for a view update, thereby eliminating unnecessary draws. /// /// This method also has the benefit of "throttling" the `-> OnDrawn(...)` event - rather than /// allowing a potentially unlimited flood of `-> OnDrawn(...)` calls, we now have the runtime /// enforced semantic that each `-> OnDrawn(...)` call must follow a unique `Ready() -> ()` call /// from the client. An unprompted `-> OnDrawn(...)` is invalid, and should cause the channel to /// immediately close. flexible Ready() -> (); /// Update the client with the latest drawing state. The server makes no guarantees about how /// often this event occurs - it could occur multiple times per board state, for example. flexible -> OnDrawn(BoundingBox); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, ], config: { // A script for the client to follow. Entries in the script may take one of two forms: a // pair of signed-integer coordinates like "-2,15:4,5", or the string "READY". The former // builds a local vector sent via a single `AddLines(...)` call, while the latter sends a // `Ready() -> ()` call pauses execution until the next `->OnDrawn(...)` event is received. // // TODO(https://fxbug.dev/42178362): It would absolve individual language implementations of a great // deal of string parsing if we were able to use a vector of `union { Point; Ready}` here. script: { type: "vector", max_count: 100, element: { type: "string", max_size: 64, }, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.canvas.clientrequesteddraw.Instance" }, ], expose: [ { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "self", }, ], }
领域
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.canvas.clientrequesteddraw.Instance", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
然后,您可以使用任何受支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{format_err, Context as _, Error}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); let mut batched_lines = Vec::<[Point; 2]>::new(); for action in config.script.into_iter() { // If the next action in the script is to "PUSH", send a batch of lines to the server. if action == "PUSH" { instance.add_lines(&batched_lines).context("Could not send lines")?; println!("AddLines request sent"); batched_lines.clear(); continue; } // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. println!("Ready request sent"); instance.ready().await.context("Could not send ready call")?; println!("Ready success"); continue; } // Add a line to the next batch. Parse the string input, making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let mut line: [Point; 2] = [from, to]; // Batch a line for drawing to the canvas using the two points provided. println!("AddLines batching line: {:?}", &mut line); batched_lines.push(line); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{anyhow, Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use futures::future::join; use futures::prelude::*; use std::sync::{Arc, Mutex}; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, // Tracks whether or not the client has declared itself ready to receive more updated. ready: bool, bounding_box: BoundingBox, } /// Handler for the `AddLines` method. fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut state.bounding_box; for line in lines { println!("AddLines printing line: {:?}", line); for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. state.changed = true } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, ready: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second, but only if the client has declared that it // is ready to receive one. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed || !state.ready { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change and ready trackers. state.ready = false; state.changed = false; } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLines { lines, .. } => { println!("AddLines request received"); add_lines(&mut state_ref.lock().unwrap(), lines); } InstanceRequest::Ready { responder, .. } => { println!("Ready request received"); // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` // event; if two "consecutive" `Ready() -> ();` calls are received, this // interaction has entered an invalid state, and should be aborted immediately. let mut state = state_ref.lock().unwrap(); if state.ready == true { return Err(anyhow!("Invalid back-to-back `Ready` requests received")); } state.ready = true; responder.send().context("Error responding")?; } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This line will only be reached if the server errors out. The stream will await indefinitely, // thereby creating a long-lived server. Here, we first wait for the updater task to realize the // connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override { ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left(); ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::Point(x, y); } using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fit::result<fidl::Error> result = client->AddLines(batched_lines); if (!result.is_ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) { // Check if the FIDL call succeeded or not. if (result.is_ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request.lines()) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override { ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left; ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y}; } using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines)); if (!status.ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>& result) { // Check if the FIDL call succeeded or not. if (result.ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request->lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; weak->state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h> // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::clientrequesteddraw::Point{.x = x, .y = y}; } using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::clientrequesteddraw::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop](::examples::canvas::clientrequesteddraw::Point top_left, ::examples::canvas::clientrequesteddraw::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { instance_proxy->AddLines(batched_lines); batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are ready // for us. FX_LOGS(INFO) << "Ready request sent"; instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do // to recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "Ready success"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples::canvas::clientrequesteddraw::BoundingBox bounding_box; }; using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLines(std::vector<Line> lines) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next // |OnDrawn| event. state_.changed = true; } void Ready(ReadyCallback callback) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if // two "consecutive" `Ready() -> ();` calls are received, this interaction has entered an // invalid state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; callback(fpromise::ok()); } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something // has changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client // has not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::Binding<examples::canvas::clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from // the client. The following initializes the loop, and obtains the dispatcher, which will be // used when binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>( [dispatcher]( fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
前馈数据流
某些协议具有前馈数据流,这种数据流主要在一个方向(通常是从客户端到服务器)流动,从而避免了往返延迟。该协议仅在必要时同步这两个端点。由于执行给定任务所需的总上下文切换次数更少,因此前馈数据流还会提高吞吐量。
前馈数据流的关键在于,客户端无需等待先前方法调用的结果,即可发送后续消息。例如,协议请求流水线功能让客户端无需等待服务器使用协议进行回复,即可使用该协议。同样,客户端分配的标识符(见下文)可让客户端无需等待服务器为服务器持有的状态分配标识符。
通常,反馈协议会涉及客户端提交一系列单向方法调用,而无需等待服务器的响应。提交这些消息后,客户端会通过调用具有回复的方法(例如 Commit
或 Flush
)来明确与服务器同步。回复可能是一个空消息,也可能包含有关提交的序列是否成功的信息。在更复杂的协议中,单向消息表示为命令对象的联合,而不是单个方法调用;请参阅下文中的命令联合模式。
使用前馈数据流的协议非常适合与乐观错误处理策略搭配使用。不要让服务器使用状态值回复每个方法,因为这会促使客户端等待每个消息之间的往返时间;而是仅在方法因客户端无法控制的原因而可能失败时,才包含状态响应。如果客户端发送的消息是客户端本应知道无效的消息(例如,引用无效的客户端分配的标识符),请通过关闭连接来发出错误信号。如果客户端发送的消息无效,但客户端无法得知,则可以提供表示成功或失败的响应(这需要客户端进行同步),或者记住错误并忽略后续的依赖请求,直到客户端同步并以某种方式从错误中恢复。
示例:
protocol Canvas {
Flush() -> (struct {
code zx.Status;
});
Clear();
UploadImage(struct {
image_id uint32;
image Image;
});
PaintImage(struct {
image_id uint32;
x float32;
y float32;
});
DiscardImage(struct {
image_id uint32;
});
PaintSmileyFace(struct {
x float32;
y float32;
});
PaintMoustache(struct {
x float32;
y float32;
});
};
FIDL 方案:大小限制
FIDL 矢量和字符串可以带有大小约束条件,该约束条件用于指定该类型可以包含的成员数量上限。对于向量,这表示向量中存储的元素数;对于字符串,这表示字符串包含的字节数。
强烈建议使用大小约束条件,因为它会为原本无限大的类型设置上限。
有序迭代是键值对存储区的一个实用操作:即,给定键时,按顺序返回其后面显示的元素的列表(通常分页)。
推理
在 FIDL 中,最好使用迭代器来实现此操作,迭代器通常实现为可进行此迭代的单独协议。使用单独的协议(因此是单独的通道)有许多好处,包括将迭代拉取请求与通过主要协议执行的其他操作解交错。
协议 P
的通道连接的客户端和服务器端可以分别表示为 FIDL 数据类型 client_end:P
和 server_end:P
。这些类型统称为协议端,表示将 FIDL 客户端连接到其相应服务器的另一种(非 @discoverable
)方式:通过现有 FIDL 连接!
协议端点是一般 FIDL 概念(资源类型)的具体实例。资源类型旨在包含 FIDL 句柄,因此需要对此类型的使用方式施加额外限制。该类型始终必须是唯一的,因为底层资源由某些其他 capability 管理器(通常是 Zircon 内核)中介。在不涉及管理器的情况下,通过简单的内存复制来复制此类资源是不可能的。为防止出现此类重复,FIDL 中的所有资源类型始终都是只移动的。
最后,Iterator
协议本身的 Get()
方法会对返回载荷使用大小限制。这会限制单次拉取中可能传输的数据量,从而实现一定程度的资源使用控制。它还会创建自然的分页边界:服务器只需一次准备小批次数据,而不是一次性转储所有结果。
实现
FIDL、CML 和 Realm 接口定义如下:
FIDL
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. library examples.keyvaluestore.additerator; /// An item in the store. The key must match the regex `^[A-z][A-z0-9_\.\/]{2,62}[A-z0-9]$`. That /// is, it must start with a letter, end with a letter or number, contain only letters, numbers, /// periods, and slashes, and be between 4 and 64 characters long. type Item = struct { key string:128; value vector<byte>:64000; }; /// An enumeration of things that may go wrong when trying to write a value to our store. type WriteError = flexible enum { UNKNOWN = 0; INVALID_KEY = 1; INVALID_VALUE = 2; ALREADY_EXISTS = 3; }; /// An enumeration of things that may go wrong when trying to create an iterator. type IterateConnectionError = flexible enum { /// The starting key was not found. UNKNOWN_START_AT = 1; }; /// A key-value store which supports insertion and iteration. @discoverable open protocol Store { /// Writes an item to the store. flexible WriteItem(struct { attempt Item; }) -> () error WriteError; /// Iterates over the items in the store, using lexicographic ordering over the keys. /// /// The [`iterator`] is [pipelined][pipelining] to the server, such that the client can /// immediately send requests over the new connection. /// /// [pipelining]: https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#request-pipelining flexible Iterate(resource struct { /// If present, requests to start the iteration at this item. starting_at string:<128, optional>; /// The [`Iterator`] server endpoint. The client creates both ends of the channel and /// retains the `client_end` locally to use for pulling iteration pages, while sending the /// `server_end` off to be fulfilled by the server. iterator server_end:Iterator; }) -> () error IterateConnectionError; }; /// An iterator for the key-value store. Note that this protocol makes no guarantee of atomicity - /// the values may change between pulls from the iterator. Unlike the `Store` protocol above, this /// protocol is not `@discoverable`: it is not independently published by the component that /// implements it, but rather must have one of its two protocol ends transmitted over an existing /// FIDL connection. /// /// As is often the case with iterators, the client indicates that they are done with an instance of /// the iterator by simply closing their end of the connection. /// /// Since the iterator is associated only with the Iterate method, it is declared as closed rather /// than open. This is because changes to how iteration works are more likely to require replacing /// the Iterate method completely (which is fine because that method is flexible) rather than /// evolving the Iterator protocol. closed protocol Iterator { /// Gets the next batch of keys. /// /// The client pulls keys rather than having the server proactively push them, to implement /// [flow control][flow-control] over the messages. /// /// [flow-control]: /// https://fuchsia.dev/fuchsia-src/development/api/fidl?hl=en#prefer_pull_to_push strict Get() -> (struct { /// A list of keys. If the iterator has reached the end of iteration, the list will be /// empty. The client is expected to then close the connection. entries vector<string:128>:10; }); };
CML
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/client_bin", }, use: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], config: { write_items: { type: "vector", max_count: 16, element: { type: "string", max_size: 64, }, }, // A key to iterate from, after all items in `write_items` have been written. iterate_from: { type: "string", max_size: 64, }, }, }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { include: [ "syslog/client.shard.cml" ], program: { runner: "elf", binary: "bin/server_bin", }, capabilities: [ { protocol: "examples.keyvaluestore.additerator.Store" }, ], expose: [ { protocol: "examples.keyvaluestore.additerator.Store", from: "self", }, ], }
领域
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. { children: [ { name: "client", url: "#meta/client.cm", }, { name: "server", url: "#meta/server.cm", }, ], offer: [ // Route the protocol under test from the server to the client. { protocol: "examples.keyvaluestore.additerator.Store", from: "#server", to: "#client", }, // Route diagnostics support to all children. { dictionary: "diagnostics", from: "parent", to: [ "#client", "#server", ], }, ], }
然后,您可以使用任何受支持的语言编写客户端和服务器实现:
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 lazy_static::lazy_static; use regex::Regex; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use std::collections::btree_map::Entry; use std::collections::BTreeMap; use std::ops::Bound::*; use std::sync::{Arc, Mutex}; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++ (Wire)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
从设计上保障用户隐私
协议中的客户端和服务器通常有权访问不同的敏感数据集。通过该协议意外泄露过多的数据可能会导致隐私或安全问题。
设计协议时,请特别注意协议中具有以下特征的字段:
- 包含姓名、电子邮件地址或付款详情等个人身份信息。
- 由用户提供,因此可能包含个人信息。示例包括设备名称和评论字段。
- 充当可跨供应商、用户、设备或重置相关联的唯一标识符。例如,序列号、MAC 地址、IP 地址和全球账号 ID。
我们会对此类字段进行全面审核,并且包含此类字段的协议的可用性可能会受到限制。确保您的协议不包含超出所需的信息。
如果某个 API 用例需要个人数据或可关联的数据,而其他用例不需要,请考虑使用两种不同的协议,以便单独控制对敏感度更高的用例的访问。
下面两个假设示例说明了 API 设计选择会导致隐私泄露:
示例 1 - 外围设备控制 API 中的序列号
假设有一个外围设备控制 API,其中包含 USB 外围设备的序列号。序列号不包含个人数据,但是一种非常稳定且易于关联的标识符。在此 API 中包含序列号会导致许多隐私问题:
- 任何有权访问该 API 的客户端都可以关联使用同一 Fuchsia 设备的不同账号。
- 有权访问该 API 的任何客户端都可以关联账号中的不同角色。
- 不同的软件供应商可能会串通起来,了解他们的软件是否由同一用户使用或在同一设备上使用。
- 如果外围设备在设备之间移动,则任何有权访问该 API 的客户端都可以将外围设备共享的一组设备与用户相关联。
- 如果外围设备被售出,则有权访问该 API 的客户端可以关联外围设备的新旧所有者。
- 有些制造商会在序列号中编码信息。这可能会让有权访问该 API 的客户端推断出用户购买外围设备的地点或时间。
在此示例中,序列号的意图是允许客户端检测何时重新连接了同一 USB 外围设备。实现此 intent 确实需要稳定的标识符,但不需要全局标识符。不同的客户端不需要接收相同的标识符,同一客户端在不同的 Fuchsia 设备上不需要接收相同的标识符,并且标识符在出厂重置事件中也不需要保持不变。
在此示例中,一个不错的替代方案是发送一个仅保证在单个设备上针对单个客户端是稳定的标识符。此标识符可能是外围设备的序列号、Fuchsia 设备标识符和连接标识符的哈希值。
示例 2 - 设备设置 API 中的设备名称
考虑一个设备设置 API,其中包含用于协助设置设备的手机型号。在大多数情况下,手机的型号字符串由原始设备制造商 (OEM) 设置,但某些手机会将用户提供的设备名称报告为其型号。这导致许多模型字符串包含用户的真实姓名或假名。因此,此 API 可能会将用户跨身份或跨设备关联起来。罕见的模型字符串或预发布模型字符串可能会泄露敏感信息,即使这些信息并非由用户提供也是如此。
在某些情况下,使用模型字符串但限制哪些客户端可以访问 API 可能是合适的做法。或者,API 也可以使用从不受用户控制的字段,例如制造商字符串。另一种方法是通过将型号字符串与热门手机型号的许可名单进行比较,并将罕见的型号字符串替换为通用字符串来对型号字符串进行排错。
客户端分配的标识符
通常,协议会允许客户端操控服务器持有的多种状态。在设计对象系统时,解决此问题的典型方法是为服务器持有的每一段一致状态创建单独的对象。不过,在设计协议时,为每个状态使用单独的对象有几个缺点。
为每个逻辑对象创建单独的协议实例会消耗内核资源,因为每个实例都需要单独的通道对象。每个实例都维护一个单独的消息 FIFO 队列。为每个逻辑对象使用单独的实例意味着,发送到不同对象的消息可以彼此重新排序,从而导致客户端和服务器之间出现无序交互。
客户端分配的标识符模式通过让客户端向服务器保留的对象分配 uint32
或 uint64
标识符来避免这些问题。客户端和服务器之间交换的所有消息都通过单个协议实例传送,这为整个互动提供了一致的 FIFO 排序。
让客户端(而非服务器)分配标识符有助于实现前馈数据流,因为客户端可以向对象分配标识符,然后立即对该对象进行操作,而无需等待服务器回复对象的标识符。在此模式中,标识符仅在当前连接的范围内有效,通常,零标识符会被保留为哨兵。安全注意事项:客户端不应将其地址空间中的地址用作标识符,因为这些地址可能会泄露其地址空间的布局。
客户端分配的标识符模式有一些缺点。例如,客户端的编写难度更高,因为客户端需要管理自己的标识符。开发者通常希望创建一个客户端库,为服务提供面向对象的外观,以隐藏管理标识符的复杂性,这本身就是一个反模式(请参阅下文中的客户端库)。
如果您想使用内核的对象功能系统来保护对该对象的访问,则强烈建议您创建单独的协议实例来表示对象,而不是使用客户端分配的标识符。例如,如果您希望客户端能够与某个对象交互,但不希望客户端能够与其他对象交互,那么创建单独的协议实例意味着您可以将底层通道用作控制对该对象的访问权限的 capability。
命令联合
在使用前馈数据流的协议中,客户端通常会先向服务器发送许多单向消息,然后再发送双向同步消息。如果协议涉及的消息量特别大,发送消息的开销可能会变得明显。在这些情况下,不妨考虑使用命令联合模式将多个命令批量处理到单个消息中。
在此模式中,客户端会发送命令的 vector
,而不是为每个命令发送单独的消息。该矢量包含所有可能命令的并集,并且服务器除了使用方法序数之外,还使用并集标记作为命令调度的选择器:
type PokeCmd = struct {
x int32;
y int32;
};
type ProdCmd = struct {
message string:64;
};
type MyCommand = strict union {
1: poke PokeCmd;
2: prod ProdCmd;
};
protocol HighVolumeSink {
Enqueue(struct {
commands vector<MyCommand>;
});
Commit() -> (struct {
result MyStatus;
});
};
通常,客户端会在其地址空间中本地缓冲命令,并将其批量发送到服务器。客户端应在达到字节和句柄的通道容量限制之前将批处理刷新到服务器。
对于消息量更高的协议,请考虑在数据平面的 zx.Handle:VMO
中使用环形缓冲区,并在控制平面中使用关联的 zx.Handle:FIFO
。此类协议会给客户端和服务器带来更高的实现负担,但当您需要最大限度地提升性能时,它们非常适用。例如,块设备协议使用此方法来优化性能。
分页
FIDL 消息通常通过通道发送,通道具有消息大小上限。在许多情况下,最大消息大小足以传输合理数量的数据,但有些使用情形需要传输大量(甚至无限量)的数据。传输大量或无限量信息的一种方法是使用分页模式。
分页写入
对服务器进行分页写入的一种简单方法是让客户端以多条消息发送数据,然后使用“完成”方法让服务器处理发送的数据:
protocol Foo1 {
AddBars(resource struct {
bars vector<client_end:Bar>;
});
UseTheBars() -> (struct {
args Args;
});
};
例如,fuchsia.process.Launcher
使用此模式让客户端发送任意数量的环境变量。
此模式的更复杂版本会创建一个表示事务的协议,通常称为分离协议:
protocol BarTransaction {
Add(resource struct {
bars vector<client_end:Bar>;
});
Commit() -> (struct {
args Args;
});
};
protocol Foo2 {
StartBarTransaction(resource struct {
transaction server_end:BarTransaction;
});
};
当客户端可能并发执行许多操作,并且将写入拆分为单独的消息会导致原子性丢失时,此方法非常有用。请注意,BarTransaction
不需要 Abort
方法。更好的交易中止方法是客户端关闭 BarTransaction
协议。
分页读取
对服务器进行分页读取的一种简单方法是让服务器使用事件向单个请求发送多个响应:
protocol EventBasedGetter {
GetBars();
-> OnBars(resource struct {
bars vector<client_end:Bar>;
});
-> OnBarsDone();
};
根据特定于领域的语义,此模式可能还需要第二个事件,用于在服务器发送完数据时发出信号。这种方法适用于简单的情况,但存在许多扩缩问题。例如,该协议缺少流控制,如果客户端不再需要其他数据,则无法停止服务器(除非关闭整个协议)。
更可靠的方法是使用分离协议创建迭代器:
protocol BarIterator {
GetNext() -> (resource struct {
bars vector<client_end:Bar>;
});
};
protocol ChannelBasedGetter {
GetBars(resource struct {
iterator server_end:BarIterator;
});
};
调用 GetBars
后,客户端会使用协议请求流水线立即将第一个 GetNext
调用加入队列。之后,客户端会反复调用 GetNext
以从服务器读取其他数据,并限制未处理的 GetNext
消息数量以提供流控制。请注意,迭代器不需要“完成”响应,因为服务器可以使用空矢量进行回复,然后在完成时关闭迭代器。
分页读取的另一种方法是使用令牌。在此方法中,服务器会以不透明令牌的形式将迭代器状态存储在客户端上,并且客户端会在每次进行部分读取时将令牌返回给服务器:
type Token = struct {
opaque array<uint8, 16>;
};
protocol TokenBasedGetter {
/// If token is null, fetch the first N entries. If token is not null,
/// return the N items starting at token. Returns as many entries as it can
/// in results and populates next_token if more entries are available.
GetEntries(struct {
token box<Token>;
}) -> (struct {
entries vector<Entry>;
next_token box<Token>;
});
};
如果服务器可以将其所有分页状态托管给客户端,因此不再需要维护分页状态,这种模式尤为有吸引力。服务器应记录客户端能否保留令牌并在协议实例中重复使用该令牌。安全注意事项:无论是哪种情况,服务器都必须验证客户端提供的令牌,以确保客户端的访问权限仅限于其自己的分页结果,而不包括面向其他客户端的结果。
事件对相关性
使用客户端分配的标识符时,客户端使用仅在其与服务器的连接上下文中才有意义的标识符来识别服务器持有的对象。不过,某些用例需要在客户端之间关联对象。例如,在 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
属性。
取消事件对
使用分离式协议事务时,客户端可以通过关闭协议的客户端端来取消长时间运行的操作。服务器应监听 ZX_CHANNEL_PEER_CLOSED
并中止事务,以免浪费资源。
对于没有专用渠道的操作,也有类似的用例。例如,fuchsia.net.http.Loader
协议有一个用于发起 HTTP 请求的 Fetch
方法。HTTP 事务完成后,服务器会使用 HTTP 响应回复请求,这可能需要很长时间。除了关闭整个 Loader
协议(这可能会取消许多其他待处理请求)之外,客户端没有明显的方法来取消请求。
eventpair 取消模式通过让客户端将 zx::eventpair
中的某个纠缠事件作为方法的参数包含在内,从而解决了这个问题。然后,服务器会监听 ZX_EVENTPAIR_PEER_CLOSED
,并在该信号被断言时取消操作。使用 zx::eventpair
比使用 zx::event
或其他信号更好,因为 zx::eventpair
方法会隐式处理客户端崩溃或以其他方式拆解的情况。当客户端保留的纠缠事件被销毁时,内核会生成 ZX_EVENTPAIR_PEER_CLOSED
。
空协议
有时,空协议也能提供价值。例如,用于创建对象的方法可能还会接收 server_end:FooController
参数。调用方提供此空协议的实现:
protocol FooController {};
FooController
不包含任何用于控制创建的对象的方法,但服务器可以使用协议上的 ZX_CHANNEL_PEER_CLOSED
信号触发对象的销毁。未来,该协议可能会通过用于控制所创建对象的方法进行扩展。
控制设置类数据
通常,服务器会公开客户端可以修改的设置。建议使用 table
表示此类设置。例如,fuchsia.accessibility
库定义了:
type Settings = table {
1: magnification_enabled bool;
2: magnification_zoom_factor float32;
3: screen_reader_enabled bool;
4: color_inversion_enabled bool;
5: color_correction ColorCorrection;
6: color_adjustment_matrix array<float32, 9>;
};
(为了方便阅读,省略了注释。)
您可以通过多种方式让客户更改这些设置。
部分更新方法会公开一个接受部分设置值的 Update
方法,并且仅在部分值中存在字段时更改字段。
protocol TheManagerOfSomeSorts {
/// Description how the update modifies the behavior.
///
/// Only fields present in the settings value will be changed.
Update(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
replace 方法会公开一个接受完整设置值的 Replace
方法,并将设置更改为新提供的设置。
protocol TheManagerOfOtherSorts {
/// Description how the override modifies the behavior.
///
/// This replaces the setting.
Replace(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
需要避免的事项:
避免对部分更新或替换方法使用动词
Set
或Override
,因为所提供的语义会模糊不清。避免使用单独的方法更新设置的字段,例如
SetMagnificationEnabled
。这种单独的方法更难以维护,并且调用方很少希望更新单个值。避免移除具有魔法值(例如
-1
)的设置。而是通过缺少相应设置字段来移除设置。
引用联合变体和表字段
引用类型的字段通常很有用,例如引用表格的一个或多个字段,或引用特定的联合变体。
假设有一个 API,它将元数据作为包含多个字段的 table
提供。如果此元数据可能会变得非常大,通常有必要提供一种机制,让接收方向发件人指明将读取此元数据中的哪些字段,从而避免发送接收方不会考虑的多余字段。在这种情况下,拥有一个成员与 table
的字段一对一匹配的并行 bits
可以成为构建 API 的坚实基础:
type MetadataFields = flexible bits {
VERSION = 0b1;
COUNT = 0b10;
};
type Metadata = table {
1: version string;
2: count uint32;
// ...
};
protocol MetadataGetter {
Get(struct {
fields MetadataFields;
}) -> (struct {
metadata Metadata;
});
};
现在,我们来看看命令联合。在复杂场景中,服务器可能希望能够描述它支持的命令。在这种情况下,拥有一个成员与 union
的变体一对一匹配的并行 enum
可以成为构建 API 的坚实基础:
type MyCommandVariant = strict enum {
POKE = 1;
PROD = 2;
// ...
};
protocol HighVolumeSinkContinued {
SupportedCommands() -> (struct {
supported_commands vector<MyCommandVariant>;
});
};
请注意,虽然使用 bits
值来表示一组命令可能很诱人,但这会导致日后做出一些更困难的选择。如果您的 API 演变到需要引用特定命令,则使用 enum
会很自然。如果您一开始就使用了 bits
值,那么现在您面临着以下两个糟糕的选择之一:
引入
enum
,这意味着现在有两种引用字段的方式,并且客户端代码中可能会出现转换问题(从一种表示法转换为另一种表示法);或者继续使用
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
协议的广泛客户端库。
无论该协议是否具有关联的客户端库,都应对其进行完整的文档编写。一个独立的软件工程师团队应该能够根据协议定义直接理解和正确使用该协议,而无需对客户端库进行逆向工程。如果协议有客户端库,则应明确记录协议中足以促使您创建客户端库的低级别和细微方面。
客户端库的主要难点在于,需要为每种目标语言维护客户端库,这往往意味着不太流行的语言没有客户端库(或客户端库质量较低)。客户端库还会导致底层协议固化,因为它们会导致每个客户端以完全相同的方式与服务器交互。服务器会逐渐期待这种确切的互动模式,如果客户端偏离客户端库使用的模式,服务器将无法正常运行。
为了将客户端库添加到 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;
});
};
-
虽然在 ABI 方面,FIDL 类型系统是一种结构化类型系统(即名称没有任何意义,只有类型的结构才重要),但在 API 方面,FIDL 类型系统具有命名类型语义。 ↩