一般建议
本部分介绍有关在 Fuchsia 接口定义语言中定义协议的技术、最佳实践和一般建议。
另请参阅 FIDL 样式指南。
协议不是对象
FIDL 是一种用于定义进程间通信协议的语言。虽然该语法类似于面向对象接口的定义,但设计考虑因素更类似于网络协议,而不是对象系统。例如,要设计高质量的协议,您需要考虑带宽、延迟和流控制。您还应考虑到,协议不仅仅是操作的逻辑分组:协议还会对请求施加 FIFO 排序,将协议拆分为两个较小的协议意味着,在两个不同协议上发出的请求可以相互重新排序。
重点关注类型
设计 FIDL 协议的一个不错的起点是设计协议将使用的数据结构。例如,有关网络组建和管理的 FIDL 协议可能会包含各种类型的 IP 地址的数据结构,而有关图形的 FIDL 协议可能会包含各种几何图形概念的数据结构。您应该能够查看类型名称,并对协议所操纵的概念以及这些概念的结构方式有一些直观的了解。
语言中立性
有许多不同语言的 FIDL 后端。您应避免针对任何特定目标语言过度专门化 FIDL 定义。随着时间的推移,您的 FIDL 协议可能会被许多不同的语言使用,甚至可能被一些目前尚不支持的语言使用。FIDL 是将系统粘合在一起的粘合剂,可让 Fuchsia 支持各种语言和运行时。如果您过度专注于自己喜欢的语言,就会削弱这一核心价值主张。
序数
协议包含许多方法。系统会自动为每个方法分配一个唯一的 64 位标识符,称为序号。服务器使用序号值来确定应调度哪个协议方法。
编译器通过对库、协议和方法名称进行哈希处理来确定序号值。在极少数情况下,同一协议中的序号可能会发生冲突。如果发生这种情况,您可以使用 Selector 属性来更改编译器用于哈希处理的方法的名称。以下示例将使用方法名称“C”而非方法名称“B”来计算哈希:
protocol A {
@selector("C")
B(struct {
s string;
b bool;
});
};
在开发者希望更改方法名称的情况下,选择器还可用于保持与有线格式的向后兼容性。
诊断
有时,我们需要公开一些有助于调试或诊断程序的信息。这些数据可以采用统计数据和指标(例如错误数、调用次数、大小等)的形式,也可以是对开发、组件健康状态或其他类似方面有用的信息。
在生产协议中,很容易在测试协议或调试方法中公开此信息。不过,Fuchsia 提供了一种单独的机制来公开此类信息:Inspect,在决定如何公开此类数据时应考虑此机制。如果需要公开有关程序的诊断信息(这些信息可用于在测试中进行调试、由开发工具使用或在现场通过崩溃报告或指标检索),则应使用 Inspect 而不是 FIDL 方法/协议,前提是其他程序不会使用该信息来做出运行时决策。
当其他程序将根据诊断信息做出运行时决策时,应使用 FIDL。检查绝不能用于程序之间的通信,它是一个尽力而为的系统,绝不能依赖它在生产环境中的运行时做出决策或改变行为。
用于决定是使用 Inspect 还是 FIDL 的启发法可以是:
数据是否被其他程序用于生产?
- 可以:使用 FIDL。
数据是否用于崩溃报告或指标?
- 可以:使用“检查”。
数据是否供测试或开发者工具使用?是否有可能在生产环境中使用?
- 可以:使用 FIDL。
- 否:使用任一选项。
库结构
将 FIDL 声明分组到 FIDL 库中有两个具体目标:
- 帮助 FIDL 开发者(使用 FIDL 库的开发者)浏览 API 表面。
- 在 FIDL 库中为分层范围的 FIDL 声明提供结构。
请仔细考虑如何将类型和协议定义划分为库。您将这些定义分解为库的方式会对这些定义的使用者产生很大影响,因为 FIDL 库是类型和协议的依赖项和分发单元。
FIDL 编译器要求库之间的依赖关系图为 DAG,这意味着您无法跨库边界创建循环依赖关系。不过,您可以在库中创建(某些)循环依赖项。
如需决定是否将库分解为更小的库,请考虑以下问题:
库的客户是否会分为不同的角色,这些角色可能只想使用库中的部分功能或声明?如果确实如此,请考虑将该库拆分为针对每个角色的单独库。
该库是否对应于具有公认结构的行业概念?如果是,请考虑按照行业标准结构来构建库。例如,蓝牙分为
fuchsia.bluetooth.le和fuchsia.bluetooth.gatt,以符合行业内对这些概念的普遍理解。同样,fuchsia.net.http对应于行业标准的 HTTP 网络协议。是否有许多其他库依赖于该库?如果是,请检查这些传入的依赖项是否真的需要依赖整个库,或者是否有一组“核心”定义可以从库中提取出来,以接收大部分传入的依赖项。
理想情况下,我们会为整个 Fuchsia 生成一个全局最优的 FIDL 库结构。不过,康威定律指出,“设计系统 [...] 的组织受到限制,完成的设计往往是这些组织内部沟通结构的复制”。我们应该花费适量的时间来对抗康威定律。
访问权限控制以协议为粒度
在决定在哪个库中定义协议时,请勿考虑访问权限控制因素。一般来说,访问权限控制以协议粒度表示。定义协议的库与访问权限控制无关,不能用于确定是否可以访问该协议。
例如,某个进程可能访问 fuchsia.logger.LogSink,或者某个进程被赋予 fuchsia.media.StreamSource 协议的客户端。不过,FIDL 并非旨在用于表达对 fuchsia.logger 库的访问权限,也无法用于阻止对 fuchsia.ldsvc 库的访问。
fuchsia 命名空间
在平台源代码树(即在 fuchsia.googlesource.com 中)定义的 FIDL 库必须位于 fuchsia 顶级命名空间中(例如,fuchsia.ui),除非满足以下任一条件:
- 库定义 FIDL 语言本身或其一致性测试套件的部分内容,在这种情况下,顶级命名空间必须为
fidl。 - 该库仅用于内部测试,不包含在 SDK 或正式版 build 中,在这种情况下,顶级命名空间必须为
test。
强烈建议顶级命名空间 fuchsia 中的 FIDL 库不超过四个组成部分,即 fuchsia.<api-namespace>、fuchsia.<api-namespace>.<name> 或 fuchsia.<api-namespace>.<name>.<subname>。选择合适的 api-namespace,可能需要借助 API 委员会成员的帮助。
例如,平台源代码树中定义的 FIDL 库(旨在向应用公开硬件功能)必须位于 fuchsia.hardware 命名空间中。例如,用于公开以太网设备的协议可能命名为 fuchsia.hardware.ethernet.Device。基于这些 FIDL 协议构建的更高级别功能不属于 fuchsia.hardware 命名空间。例如,网络协议更适合放在 fuchsia.net 下,而不是 fuchsia.hardware 下。
避免嵌套过深
最好使用包含三个组成部分的库名称(例如 fuchsia.hardware.network),并避免使用包含四个以上组成部分的库名称(例如 fuchsia.apps.foo.bar.baz)。如果您使用四个以上组成部分,则应有充分的理由。
库依赖项
最好从名称更具体的库向名称不太具体的库引入依赖项。例如,fuchsia.foo.bar 可能依赖于 fuchsia.foo,但 fuchsia.foo 不应依赖于 fuchsia.foo.bar。这种模式更适合扩展,因为随着时间的推移,我们可以添加更多名称更具体的库,但名称不太具体的库数量有限。
导入库的可见性
为了进一步实现将 FIDL 声明分组到 FIDL 库中的第二个目标,我们希望改进 FIDL,以提供可见性规则,从而改变元素是否可被导入的库(“子库”)使用,例如 public 或 private 修饰符。
internal 库组件名称旨在进行特殊处理,表示对可见性规则的本地限制。例如,fuchsia.net.dhcp.internal.foo 库中的公开声明可能仅对其父级 fuchsia.net.dhcp 或同级(例如 fuchsia.net.dhcp.internal.bar)可见。
使用包含多个字词的库组件
虽然允许使用包含连接多个字词的组件(例如 fuchsia.modular.storymodel)的库名称,但应仅在特殊情况下使用。如果库名称会违反嵌套规则,或者在以分层方式考虑库的放置位置时,两个字词都不应优先于另一个字词,库作者可以考虑将多个字词连接在一起。
版本字符串
如果库需要进行版本控制,则应添加单个版本号后缀,例如 fuchsia.io2 或 fuchsia.something.something4.。版本号不应包含多个部分,例如 fuchsia.io2.1 是不可接受的,而应为 fuchsia.io3。任何库组件都可以进行版本控制,但强烈建议不要有多个版本控制的组件,例如 fuchsia.hardware.cpu2.ctrl,而不是 fuchsia.hardware.cpu2.ctrl4。
版本号应仅表示库的更新版本,而不是实质上不同的网域。举个反例,fuchsia.input 库用于较低级别的设备处理,而 fuchsia.ui.input{2,3} 用于与 Scenic 和渲染界面的软件组件交互的输入。如果仅关注版本控制,则使用 fuchsia.ui.scenic.input 和 fuchsia.ui.scenic.input2 会更清晰,以便与 fuchsia.input 服务的其他网域区分开来。
类型
如“一般建议”中所述,您应特别注意在协议定义中使用的类型。
保持一致
针对同一概念使用一致的类型。例如,在整个库中,始终使用 uint32 或 int32 来表示某个特定概念。如果您为某个概念创建了 struct,请始终使用该结构体来表示该概念。
理想情况下,类型也应在库边界之间保持一致。检查相关库中是否有类似的概念,并与这些库保持一致。如果库之间共享了许多概念,请考虑将这些概念的类型定义分解为通用库。例如,fuchsia.mem 和 fuchsia.math 分别包含许多用于表示内存和数学概念的常用类型。
首选语义类型
创建用于命名常用概念的结构,即使这些概念可以使用原语表示也是如此。例如,IPv4 地址是网络库中的一个重要概念,即使可以使用原语表示数据,也应使用结构体来命名:
type Ipv4Address = struct {
octets array<uint8, 4>;
};
在对性能要求较高的目标语言中,结构体以内嵌方式表示,这降低了使用结构体来命名重要概念的成本。
zx.Time 具有明确定义的时间基准
zx.Time 类型单调地测量自特定于设备的时基以来的纳秒数。使用 zx.Time 时可以假设使用此时间基准,无需明确说明。
谨慎使用匿名类型
匿名类型对于更流畅地描述 API 非常有用。特别是,如果您先验地知道,命名类型的子元素本质上与该命名类型相关联,并且在包含命名容器的上下文之外使用时没有用处或意义,那么匿名类型非常适合这种情况。
例如,假设有一个联合变体,它将一些事物聚合在一起。联合变体很少单独使用,也就是说,我们先验地知道,联合变体只有在其特定使用情境中才有意义。因此,使用匿名类型作为联合变体是合适且推荐的做法。
理想情况下,类型应与 API 的关键概念一一对应,并且不应有两个类型具有相同的定义。不过,同时实现这两点并非总是可行,尤其是在类型命名(引入了不同的概念1)的意义不仅限于用作 API 表面元素的情况下。例如,假设有名为 type EntityId = struct { id
uint64; }; 和 type OtherEntityId = struct { id uint64; }; 的标识符,它们表示不同的概念,但除了名称之外,具有相同的类型定义。
使用匿名类型会创建多个类型,这些类型彼此不兼容。因此,如果使用多个匿名类型来表示同一概念,会导致 API 过度复杂,并阻止大多数目标语言中的通用处理。
因此,在使用匿名类型时,您必须避免使用多个表示同一概念的匿名类型。例如,如果 API 的演变可能会导致多个匿名类型表示同一概念,您就不得使用匿名类型。
考虑使用虚拟内存对象 (VMO)
虚拟内存对象 (VMO) 是一种内核对象,表示一段连续的虚拟内存以及一个逻辑大小。使用此类型在 FIDL 消息中传输内存,并使用 ZX_PROP_VMO_CONTENT_SIZE 属性跟踪对象中包含的数据量。
为向量和字符串指定边界
所有 vector 和 string 声明都应指定长度界限。声明通常分为以下两类:
- 数据本身存在限制。例如,包含文件系统名称组成部分的字符串不得超过
fuchsia.io.MAX_NAME_LENGTH个字符。 - 除了“尽可能多”之外,没有其他限制。在这些情况下,您应使用内置常量
MAX。
每当您使用 MAX 时,请考虑消息接收者是否真的想处理任意长度的序列,或者极长的序列是否表示滥用。
请注意,通过 zx::channel 发送的所有声明都会受到最大消息长度的隐式限制。如果确实存在任意长度序列的使用情形,那么仅使用 MAX 可能无法解决这些使用情形,因为尝试提供极长序列的客户端可能会达到消息长度上限。
如需处理任意长度的序列,请考虑使用下文讨论的某种分页模式将序列拆分为多条消息,或者考虑将数据移出消息本身(例如移入 VMO)。
FIDL 配方:大小限制
FIDL 向量和字符串可能带有大小限制,用于指定相应类型可包含的成员数量上限。对于向量,这指的是向量中存储的元素数量;对于字符串,这指的是字符串包含的字节数。
强烈建议使用大小限制,因为它可以为原本不受限制的大型类型设置上限。
对于键值对存储区,一个有用的操作是按顺序迭代:也就是说,给定一个键时,按顺序返回出现在该键之后的元素(通常是分页)列表。
推理
在 FIDL 中,最好使用迭代器来完成此操作,迭代器通常实现为可进行此迭代的单独协议。使用单独的协议(因此也使用单独的渠道)有很多好处,包括将迭代拉取请求与通过主协议完成的其他操作分离。
协议 P 的通道连接的客户端和服务器端可以分别表示为 FIDL 数据类型 client_end:P 和 server_end:P。这些类型统称为协议端,表示将 FIDL 客户端连接到其相应服务器的另一种(非 @discoverable)方式:通过现有的 FIDL 连接!
协议端点是 FIDL 一般概念(即资源类型)的特定实例。资源类型旨在包含 FIDL 句柄,因此必须对该类型的使用方式施加额外的限制。该类型必须始终是唯一的,因为底层资源由其他功能管理器(通常是 Zircon 内核)进行中介。如果不涉及管理器,则无法通过简单的内存中复制来复制此类资源。为防止出现此类重复,FIDL 中的所有资源类型始终只能移动。
最后,Iterator 协议的 Get() 方法本身会使用返回载荷的大小限制。这样可以限制单次拉取中可传输的数据量,从而实现一定程度的资源使用控制。它还创建了一个自然的分页边界:服务器只需一次准备少量批次,而不是一次性转储所有结果。
实现
FIDL、CML 和 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 regex::Regex; use std::sync::LazyLock; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use fuchsia_sync::Mutex; use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::ops::Bound::*; use std::sync::Arc; static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile") }); /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤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(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.C++(有线)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
字符串编码、字符串内容和长度界限
FIDL string 采用 UTF-8 编码,这是一种变宽编码,每个 Unicode 码位使用 1、2、3 或 4 个字节。
绑定会强制要求字符串采用有效的 UTF-8 编码,因此字符串不适合任意二进制数据。请参阅我应该使用 string 还是 vector<byte>?。
由于长度界限声明的目的是为 FIDL 消息的总字节大小提供一个易于计算的上限,因此 string 界限指定了字段中的最大字节数。为安全起见,您通常需要预留 (4 bytes · code points in
string) 的预算。(如果您确定文本仅使用单字节 ASCII 范围内的码位,例如电话号码或信用卡号,则每个码位 1 个字节就足够了。)
字符串中有多少个代码点?这个问题可能很难回答,尤其是对于用户生成的字符串内容,因为 Unicode 码位与用户可能认为的“字符”之间不一定存在一对一的对应关系。
例如,字符串
á
呈现为单个用户感知到的“字符”,但实际上由两个代码点组成:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
在 Unicode 术语中,这种用户感知到的“字符”称为“字形簇”。
单个字形簇可以包含任意数量的码点。请看以下较长的示例:
á🇨🇦b👮🏽♀️
如果您的系统和字体支持,您应该会在上方看到四个字形集群:
1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone
这四个字形簇编码为 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 = flexible enum {
MISSING_FOO = 1; // avoid using 0
NO_BAR = 2;
};
protocol Frobinator {
Frobinate() -> (struct {
value SuccessValue;
}) error MyErrorCode;
};
使用此模式时,您可以使用 int32、uint32 或其枚举来表示返回的错误类型。在大多数情况下,返回枚举是首选方法。
最好在协议的所有方法中都使用单一错误类型。
首选特定于网域的枚举
在定义和控制网域时,使用专门构建的枚举错误类型。例如,当协议是专门构建的,并且传达错误语义是唯一的设计限制时,请定义枚举。如枚举部分所述,最好避免使用值 0。
在某些情况下,可以先使用空的灵活枚举:
type MyEmptyErrorCode = flexible enum {};
protocol Frobinator2 {
Frobinate() -> (struct {
value SuccessValue;
}) error MyEmptyErrorCode;
BadFrobinate() -> (struct {
value SuccessValue;
}) error flexible enum {}; // avoid anonymous enum
};
灵活的枚举具有默认的未知成员。因此,空灵活枚举是一个类型化占位符,可为可演化性提供支持。使用此模式时,建议定义一个独立类型,以供协议(或库)中的多个方法重复使用,而不是使用匿名枚举。使用匿名枚举会创建多种类型,这些类型彼此不兼容,从而导致 API 过度复杂,并阻止在大多数目标语言中对错误进行通用处理。
如果您遵循的是明确定义的规范(例如 HTTP 错误代码),并且枚举旨在以符合人体工程学的方式表示规范规定的原始值,请使用特定于网域的枚举错误类型。
特别是,对于与内核对象或 IO 相关的错误,请使用 zx.Status 类型。例如,fuchsia.process 使用 zx.Status,因为该库主要用于操作内核对象。再举一个例子,fuchsia.io 大量使用 zx.Status,因为该库与 IO 有关。
将可选值与错误枚举搭配使用
过去,定义具有两个返回值(一个可选值和一个错误代码)的方法在性能方面略有优势。例如,请参阅:
type MyStatusCode = strict enum {
OK = 0; // The success value should be 0,
MISSING_FOO = 1; // with erroneous status next.
NO_BAR = 2;
};
protocol Frobinator3 {
Frobinate() -> (struct {
value box<SuccessValue>;
err MyStatusCode;
});
};
不过,此模式现已弃用,取而代之的是错误语法:通过在信封中内嵌小值,此模式曾经存在的性能优势已过时,并且现在对联合的底层支持非常普遍。
避免在错误中显示消息和说明
在某些特殊情况下,如果可能的错误情况范围很大,并且描述性错误消息可能对客户端有用,那么除了 status 或枚举值之外,协议还可以包含错误的字符串说明。不过,包含字符串会带来一些困难。例如,客户端可能会尝试解析字符串以了解发生了什么情况,这意味着字符串的确切格式会成为协议的一部分,当字符串是本地化时,这尤其成问题。
安全注意事项:同样,向客户端报告堆栈轨迹或异常消息可能会无意中泄露机密信息。
本地化字符串和错误消息
如果您要构建充当界面后端的服务,请使用结构化类型化消息,并将渲染留给界面层。
如果您的所有消息都简单且未进行参数化,请使用 enums 来表示错误报告和常规界面字符串。如需更详细的消息(包含名称、数字和位置等参数),请使用 tables 或 xunions,并将参数作为字符串或数字字段传递。
您可能很想在服务中生成消息(以英文显示),并以字符串的形式将其提供给界面,这样界面只需接收一个字符串,然后弹出一个通知或错误对话框。
不过,这种更简单的方法存在一些严重缺点:
- 您的服务是否知道界面中正在使用哪种语言区域(语言和地区)?您必须在每个请求中传递语言区域设置(请参阅示例),或者跟踪每个已连接客户端的状态,才能以正确的语言提供消息。
- 您的服务开发环境是否对本地化提供良好的支持?如果您使用 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 表示固定长度的数据:
- 对于 MAC 地址,请使用
array,因为 MAC 地址始终为 6 个字节。
我应该使用 struct 还是 table?
结构体和表都表示具有多个命名字段的对象。不同之处在于,结构体在有线格式中具有固定的布局,这意味着在不破坏二进制兼容性的情况下,无法修改结构体。相比之下,表格在有线格式中具有灵活的布局,这意味着可以随时向表格中添加字段,而不会破坏二进制兼容性。
对于对性能要求很高的协议元素或未来极不可能发生变化的协议元素,请使用结构体。例如,使用结构体表示 MAC 地址,因为 MAC 地址的结构在未来发生变化的可能性非常小。
对于未来可能会发生变化的协议元素,请使用表格。例如,使用表格来表示相机设备的元数据信息,因为元数据中的字段可能会随时间推移而发生变化。
如何表示常量?
根据常量类型的不同,您可以通过以下三种方式表示常量:
- 使用
const表示特殊值,例如 PI 或 MAX_NAME_LEN。 - 当值是集合的元素时,请使用
enum,例如媒体播放器的重复模式:OFF、SINGLE_TRACK 或 ALL_TRACKS。 - 对于构成一组标志的常量,请使用
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 类型通常更易于使用和推理,但它们不允许进行未来的扩展。
在 strict 和 flexible 之间进行选择时,请问问自己以下问题:
- 我将来是否可能需要向这种类型的群组添加成员?如果需要,请使用
flexible。 - 万一我将来确实需要向这种类型添加成员,是直接引入新类型更好,还是向这种类型添加成员更好?如果系统这样要求,请使用
strict。
类型可以从 strict 转换为 flexible,但这是一个缓慢的过程 - 在所有将该类型标记为 strict 的 API 级别都已弃用之前,无法向该类型添加新成员。若要开始此过渡,请指明自 NEXT 起,类型不再是 strict,而是 flexible:
type Color2 = strict(removed=NEXT) flexible(added=NEXT) enum {
RED = 1;
};
如果类型允许,最好始终指定此修饰符,这样会显得更规范。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() -> ();
};
除了 EnablePIIMode 之外,这里的所有方法都是 flexible;请考虑如果服务器无法识别任何一种方法会发生什么情况:
AddRecord:服务器只是无法将数据添加到日志输出中。发送应用的行为正常,但其日志记录的实用性会降低。此操作不太方便,但很安全。EnablePIIMode:服务器未能启用 PII 模式,这意味着它可能未能采取安全预防措施并泄露 PII。这是一个严重的问题,因此如果服务器无法识别此方法,最好关闭通道。DisablePIIMode:服务器针对不需要记录个人身份信息的消息采取了不必要的安全预防措施。这可能会给尝试读取日志的人带来不便,但对系统来说是安全的。Flush:服务器未能按要求刷新记录,这可能会带来不便,但仍是安全的。
另一种设计此协议的方法是使其完全灵活,即让 EnablePIIMode 成为双向方法 (flexible EnablePIIMode() -> ();),以便客户端可以了解服务器是否没有该方法。请注意,这会为客户端带来额外的灵活性;通过此设置,客户端可以选择是否通过关闭连接来响应服务器未识别 EnablePIIMode 的情况,或者只是选择不记录 PII,而使用 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 模式向服务器“停放回调”:
protocol FooProvider {
WatchFoo(struct {
args Args;
}) -> (resource struct {
foo client_end:Foo;
});
};
在这种模式下,客户端发送 WatchFoo 消息,但服务器在有新信息要发送给客户端之前不会回复。客户端使用 foo 并立即发送另一个挂起 GET。客户端和服务器各自针对每个数据项执行一个单位的工作,这意味着两者都不会领先于对方。
当传输的数据项集大小有限且服务器端状态简单时,挂起 GET 模式效果良好,但在客户端和服务器需要同步其工作的情况下,效果不佳。
例如,服务器可能会针对某些可变状态 foo 为每个客户端实现挂起 GET 模式,并使用“脏”位。它会将此位初始化为 true,在每次 WatchFoo 响应时清除此位,并在每次 foo 更改时设置此位。仅当设置了脏位时,服务器才会响应 WatchFoo 消息。
使用确认机制节流推送
在采用推送的协议中提供流量控制的一种方法是确认模式,其中调用方提供确认响应,供调用方用于流量控制。例如,请考虑以下通用监听器协议:
protocol Listener {
OnBar(struct {
args Args;
}) -> ();
};
监听器应在收到 OnBar 消息后立即发送空响应消息。响应不会向调用方传递任何数据。相反,响应可让调用方观察被调用方消耗消息的速率。调用方应限制其生成消息的速率,使其与被调用方消耗消息的速率相匹配。例如,调用方可能会安排仅发送一条(或固定数量)正在传输的消息(即等待确认的消息)。
FIDL 配方:确认模式
确认模式是一种简单的流量控制方法,适用于原本为单向调用的方法。该方法不再是单向调用,而是变成了双向调用,但没有响应,俗称 ack。确认的唯一存在理由是告知发送者消息已收到,发送者可以使用此信息来决定如何继续。
此确认的代价是增加了渠道上的杂音。如果客户端在继续进行下一次调用之前等待确认,此模式也可能会导致性能下降。
来回发送不按流量计费的单向调用会产生简单的设计,但存在潜在的陷阱:如果服务器处理更新的速度比客户端发送更新的速度慢得多,会怎么样?例如,客户端可能会从某个文本文件中加载包含数千条线的绘图,并尝试按顺序发送所有这些线。如何对客户端施加反压力,以防止服务器被这波更新淹没?
通过使用确认模式并将单向调用 AddLine(...); 更改为双向调用 AddLine(...) -> ();,我们可以向客户端提供反馈。这样,客户端就可以根据需要限制其输出。在此示例中,我们只需让客户端等待确认,然后再发送下一个待发消息,不过更复杂的设计可以乐观地发送消息,并且仅在收到异步确认的频率低于预期时才进行节流。
首先,我们需要定义接口定义和自动化测试框架。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::{Context as _, Error, format_err}; use config::Config; use fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); for action in config.script.into_iter() { // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } continue; } // If the action is not a "WAIT", we need to draw a line instead. Parse the string input, // making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let line = [from, to]; // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. println!("AddLine request sent: {:?}", line); // By awaiting on the reply, we prevent the client from sending another request before the // server is ready to handle, thereby syncing the flow rate between the two parties over // this method. instance.add_line(&line).await.context("Error sending request")?; println!("AddLine response received"); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_sync::Mutex; use futures::future::join; use futures::prelude::*; use std::sync::Arc; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, bounding_box: BoundingBox, } impl CanvasState { /// Handler for the `AddLine` method. fn add_line(&mut self, line: [Point; 2]) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut self.bounding_box; for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. self.changed = true } } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock(); if !state.changed { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change tracker. state.changed = false } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLine { line, responder } => { println!("AddLine request received: {:?}", line); state_ref.lock().add_line(line); // Because this is now a two-way method, we must use the generated `responder` // to send an in this case empty reply back to the client. This is the mechanic // which syncs the flow rate between the client and server on this method, // thereby preventing the client from "flooding" the server with unacknowledged // work. responder.send().context("Error responding")?; println!("AddLine response sent"); } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This await does not complete, and thus the function does not return, unless the server errors // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first // wait for the updater task to realize the connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override { auto top_left = event.top_left(); auto bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::Point(x, y); } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.is_ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.error_value().FormatDescription(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override { auto points = request.line(); FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request.line()) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++(有线)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override { auto top_left = event->top_left; auto bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine( const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override { auto points = request->line; FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request->line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. weak->state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
使用事件推送有界数据
在 FIDL 中,服务器可以向客户端发送未经请求的消息,称为事件。使用事件的协议需要特别注意流控制,因为事件机制本身不提供任何流控制。
如果渠道的整个生命周期内最多只会发送一次事件,那么使用事件就是一种不错的选择。在此模式中,协议不需要对事件进行任何流量控制:
protocol DeathWish {
-> OnFatalError(struct {
error_code zx.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 2025 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error, format_err}; use config::Config; use fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}; use fuchsia_component::client::connect_to_protocol; use futures::TryStreamExt; use std::{thread, time}; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); let mut batched_lines = Vec::<[Point; 2]>::new(); for action in config.script.into_iter() { // If the next action in the script is to "PUSH", send a batch of lines to the server. if action == "PUSH" { instance.add_lines(&batched_lines).context("Could not send lines")?; println!("AddLines request sent"); batched_lines.clear(); continue; } // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. println!("Ready request sent"); instance.ready().await.context("Could not send ready call")?; println!("Ready success"); continue; } // Add a line to the next batch. Parse the string input, making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or_else(|| format_err!("line requires 2 points, but has 1"))?; let mut line: [Point; 2] = [from, to]; // Batch a line for drawing to the canvas using the two points provided. println!("AddLines batching line: {:?}", &mut line); batched_lines.push(line); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use anyhow::{Context as _, Error, anyhow}; use fidl::endpoints::RequestStream as _; use fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }; use fuchsia_async::{MonotonicInstant, Timer}; use fuchsia_component::server::ServiceFs; use fuchsia_sync::Mutex; use futures::future::join; use futures::prelude::*; use std::sync::Arc; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, // Tracks whether or not the client has declared itself ready to receive more updated. ready: bool, bounding_box: BoundingBox, } /// Handler for the `AddLines` method. fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut state.bounding_box; for line in lines { println!("AddLines printing line: {:?}", line); for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. state.changed = true } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, ready: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second, but only if the client has declared that it // is ready to receive one. Timer::new(MonotonicInstant::after(zx::MonotonicDuration::from_seconds(1))).await; let mut state = state_ref.lock(); if !state.changed || !state.ready { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change and ready trackers. state.ready = false; state.changed = false; } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLines { lines, .. } => { println!("AddLines request received"); add_lines(&mut state_ref.lock(), lines); } InstanceRequest::Ready { responder, .. } => { println!("Ready request received"); // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` // event; if two "consecutive" `Ready() -> ();` calls are received, this // interaction has entered an invalid state, and should be aborted immediately. let mut state = state_ref.lock(); if state.ready == true { return Err(anyhow!("Invalid back-to-back `Ready` requests received")); } state.ready = true; responder.send().context("Error responding")?; } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This line will only be reached if the server errors out. The stream will await indefinitely, // thereby creating a long-lived server. Here, we first wait for the updater task to realize the // connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override { ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left(); ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::Point(x, y); } using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fit::result<fidl::Error> result = client->AddLines(batched_lines); if (!result.is_ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) { // Check if the FIDL call succeeded or not. if (result.is_ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request.lines()) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++(有线)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override { ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left; ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y}; } using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines)); if (!status.ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>& result) { // Check if the FIDL call succeeded or not. if (result.ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once // the referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request->lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; weak->state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
前馈数据流
有些协议具有前馈数据传输,通过使数据主要沿一个方向(通常是从客户端到服务器)流动来避免往返延迟。该协议仅在必要时同步两个端点。前馈数据流还可提高吞吐量,因为执行给定任务所需的总上下文切换次数更少。
前馈 Dataflow 的关键在于,无需客户端等待先前方法调用的结果,即可发送后续消息。例如,协议请求流水线处理消除了客户端在可以使用协议之前等待服务器回复协议的需要。同样,客户端分配的标识符(见下文)无需客户端等待服务器为服务器所持有的状态分配标识符。
通常,前馈协议会涉及客户端提交一系列单向方法调用,而不等待服务器的响应。提交这些消息后,客户端会通过调用具有回复的方法(例如 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 句柄,因此必须对该类型的使用方式施加额外的限制。该类型必须始终是唯一的,因为底层资源由其他功能管理器(通常是 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 regex::Regex; use std::sync::LazyLock; use fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }; use fuchsia_async as fasync; use fuchsia_sync::Mutex; use std::collections::BTreeMap; use std::collections::btree_map::Entry; use std::ops::Bound::*; use std::sync::Arc; static KEY_VALIDATION_REGEX: LazyLock<Regex> = LazyLock::new(|| { Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile") }); /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤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(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream())) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.C++(有线)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
从设计上保障用户隐私
协议中的客户端和服务器通常有权访问不同的敏感数据。隐私权或安全问题可能是由通过协议无意中泄露了不必要的数据而造成的。
设计协议时,请特别注意协议中具有以下特征的字段:
- 包含个人身份信息,例如姓名、电子邮件地址或付款详情。
- 由用户提供,因此可能包含个人信息。例如设备名称和注释字段。
- 充当可在不同供应商、用户、设备或重置之间相关联的唯一标识符。例如,序列号、MAC 地址、IP 地址和全球账号 ID。
我们会对这些类型的字段进行彻底审核,并可能会限制包含这些字段的协议的可用性。确保您的协议不包含超出所需的信息。
如果某个 API 的使用情形需要个人数据或可关联的数据,而其他使用情形不需要,请考虑使用两种不同的协议,以便单独控制对更敏感的使用情形的访问权限。
请看以下两个假设性示例,它们说明了 API 设计选择导致的隐私权侵犯:
示例 1 - 外围设备控制 API 中的序列号
假设某个外围设备控制 API 包含 USB 外围设备的序列号。序列号不包含个人数据,但它是一种非常稳定的标识符,很容易进行关联。在此 API 中包含序列号会导致许多隐私问题:
- 任何有权访问该 API 的客户端都可以使用同一 Fuchsia 设备关联不同的账号。
- 任何有权访问该 API 的客户端都可以关联账号中的不同角色。
- 不同的软件供应商可能会串通起来,了解他们是否被同一用户使用或在同一设备上使用。
- 如果外围设备在设备之间移动,任何有权访问该 API 的客户端都可以关联外围设备共享的设备和用户集。
- 如果外围设备被出售,有权访问该 API 的客户端可能会将外围设备的新旧所有者关联起来。
- 部分制造商会在序列号中编码信息。这可能会让有权访问该 API 的客户端推断出用户购买外围设备的位置或时间。
在此示例中,序列号的目的是让客户端能够检测到何时重新连接了同一 USB 外围设备。满足此意图需要稳定的标识符,但不需要全局标识符。不同的客户端不需要接收相同的标识符,同一客户端不需要在不同的 Fuchsia 设备上接收相同的标识符,并且标识符不需要在恢复出厂设置事件中保持不变。
在此示例中,一个不错的替代方案是发送仅保证在单个设备上对单个客户端保持稳定的标识符。此标识符可能是外围设备的序列号、Fuchsia 设备标识符和连接的 moniker 的哈希值。
示例 2 - 设备设置 API 中的设备名称
假设有一个设备设置 API,其中包含用于辅助设置设备的手机型号。在大多数情况下,手机的型号字符串由 OEM 设置,但有些手机会将用户提供的设备名称报告为型号。这会导致许多模型字符串包含用户的真实姓名或假名。因此,此 API 可能会将用户与不同身份或不同设备相关联。即使不是由用户提供的,罕见或预发布版型号字符串也可能会泄露敏感信息。
在某些情况下,使用模型字符串并限制哪些客户端可以访问 API 可能是合适的做法。或者,API 可以使用从不受用户控制的字段(例如制造商字符串)。另一种替代方法是清理模型字符串,方法是将该字符串与热门手机型号的许可名单进行比较,然后将罕见的模型字符串替换为通用字符串。
客户端分配的标识符
通常,协议会允许客户端操纵服务器持有的多个状态。在设计对象系统时,解决此问题的典型方法是为服务器保存的每个连贯状态片段创建单独的对象。不过,在设计协议时,为每种状态使用单独的对象有几个缺点。
为每个逻辑对象创建单独的协议实例会消耗内核资源,因为每个实例都需要一个单独的通道对象。每个实例都会维护一个单独的 FIFO 消息队列。为每个逻辑对象使用单独的实例意味着,发送给不同对象的消息可以相对于彼此重新排序,从而导致客户端与服务器之间的交互出现乱序。
客户端分配的标识符模式通过让客户端为服务器保留的对象分配 uint32 或 uint64 标识符来避免这些问题。客户端和服务器之间交换的所有消息都通过单个协议实例进行传递,从而为整个互动提供一致的 FIFO 排序。
让客户端(而非服务器)分配标识符可实现前馈数据流,因为客户端可以为对象分配标识符,然后立即对该对象进行操作,而无需等待服务器回复对象的标识符。在此模式中,标识符仅在当前连接的范围内有效,并且通常保留零标识符作为标记。安全注意事项:客户端不应使用其地址空间中的地址作为标识符,因为这些地址可能会泄露其地址空间的布局。
客户端分配的标识符模式存在一些缺点。例如,客户端更难编写,因为客户端需要管理自己的标识符。开发者通常希望创建一个客户端库,为服务提供面向对象的 facade,以隐藏管理标识符的复杂性,而这本身就是一种反模式(请参阅下文中的客户端库)。
如果您想使用内核的对象功能系统来保护对该对象的访问权限,那么您应该创建一个单独的协议实例来表示该对象,而不是使用客户端分配的标识符。这是一个强烈的信号。例如,如果您希望客户端能够与某个对象互动,但不希望客户端能够与其他对象互动,那么创建单独的协议实例意味着您可以将底层渠道用作控制对该对象访问权限的功能。
命令并集
在使用前馈数据流的协议中,客户端通常会先向服务器发送许多单向消息,然后再发送双向同步消息。如果协议涉及的消息量特别大,则发送消息的开销可能会变得明显。在这种情况下,请考虑使用命令联合模式将多个命令批处理到一条消息中。
在此模式下,客户端会发送一系列命令 (vector),而不是为每个命令发送单独的消息。该向量包含所有可能的命令的并集,服务器除了使用方法序号之外,还使用并集标记作为命令调度的选择器:
type PokeCmd = struct {
x int32;
y int32;
};
type ProdCmd = struct {
message string:64;
};
type MyCommand = strict union {
1: poke PokeCmd;
2: prod ProdCmd;
};
protocol HighVolumeSink {
Enqueue(struct {
commands vector<MyCommand>;
});
Commit() -> (struct {
result MyStatus;
});
};
通常,客户端会在其地址空间中本地缓冲命令,然后以批处理方式将这些命令发送到服务器。客户端应在达到字节数和句柄的渠道容量限制之前将批处理刷新到服务器。
对于消息量更高的协议,请考虑在 zx.Handle:VMO 中为数据平面使用环形缓冲区,并为控制平面使用关联的 zx.Handle:FIFO。此类协议会增加客户端和服务器的实现负担,但如果您需要尽可能高的性能,则适合使用此类协议。例如,块设备协议使用此方法来优化性能。
分页
FIDL 消息通常通过通道发送,通道具有最大消息大小。在许多情况下,最大消息大小足以传输合理的数据量,但也有一些使用情形需要传输大量(甚至无限量)数据。传输大量或无限量信息的一种方法是使用分页模式。
分页写入
一种简单的分页写入服务器的方法是让客户端通过多条消息发送数据,然后使用“finalize”方法让服务器处理已发送的数据:
protocol Foo1 {
AddBars(resource struct {
bars vector<client_end:Bar>;
});
UseTheBars() -> (struct {
args Args;
});
};
例如,fuchsia.process.Launcher 使用此模式让客户端发送任意数量的环境变量。
此模式的更复杂版本会创建一个表示交易的协议,通常称为“分离协议”:
protocol BarTransaction {
Add(resource struct {
bars vector<client_end:Bar>;
});
Commit() -> (struct {
args Args;
});
};
protocol Foo2 {
StartBarTransaction(resource struct {
transaction server_end:BarTransaction;
});
};
当客户端可能同时执行许多操作,并且将写入操作拆分为单独的消息会丢失原子性时,此方法非常有用。请注意,BarTransaction 不需要 Abort 方法。中止交易的更好方法是让客户端关闭 BarTransaction 协议。
分页读取
一种简单的分页读取服务器数据的方法是让服务器使用事件对单个请求发送多个响应:
protocol EventBasedGetter {
GetBars();
-> OnBars(resource struct {
bars vector<client_end:Bar>;
});
-> OnBarsDone();
};
根据特定于网域的语义,此模式可能还需要第二个事件来指示服务器何时完成数据发送。这种方法适用于简单的情况,但存在许多扩展问题。例如,该协议缺乏流控制,如果客户端不再需要额外数据,则无法停止服务器(除非关闭整个协议)。
一种更可靠的方法是使用分离协议来创建迭代器:
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 中,客户端主要使用客户端分配的标识符与场景图中的节点进行交互。不过,从其他进程导入节点需要在进程边界之间关联对该节点的引用。
eventpair 相关性模式通过前馈数据流解决此问题,并依靠内核来提供必要的安全性。首先,希望导出对象的客户端会创建一个 zx::eventpair,并向服务器发送一个纠缠事件以及该对象的客户端分配的标识符。然后,客户端会将另一个纠缠事件发送给另一个客户端,后者会使用自己为当前共享对象分配的客户端标识符将该事件转发给服务器:
protocol Exporter {
ExportThing(resource struct {
client_assigned_id uint32;
export_token zx.Handle:EVENTPAIR;
});
};
protocol Importer {
ImportThing(resource struct {
some_other_client_assigned_id uint32;
import_token zx.Handle:EVENTPAIR;
});
};
为了关联对象,服务器会使用 ZX_INFO_HANDLE_BASIC 调用 zx_object_get_info,并匹配纠缠事件对象中的 koid 和 related_koid 属性。
Eventpair 取消
使用分离协议交易时,客户端可以通过关闭协议的客户端端来取消长时间运行的操作。服务器应侦听 ZX_CHANNEL_PEER_CLOSED 并中止交易,以避免浪费资源。
对于没有专用渠道的操作,也有类似的使用情形。
例如,fuchsia.net.http.Loader 协议具有一个 Fetch 方法,用于发起 HTTP 请求。HTTP 事务完成后,服务器会以 HTTP 响应回复请求,这可能需要相当长的时间。除了关闭整个 Loader 协议(这可能会取消许多其他未完成的请求)之外,客户端没有明显的方法来取消请求。
eventpair 取消模式通过让客户端将 zx::eventpair 中的一个纠缠事件作为参数包含在方法中来解决此问题。然后,服务器会监听 ZX_EVENTPAIR_PEER_CLOSED,并在该信号断言时取消操作。使用 zx::eventpair 比使用 zx::event 或其他信号更好,因为 zx::eventpair 方法可以隐式处理客户端崩溃或以其他方式关闭的情况。当客户端保留的纠缠事件被销毁时,内核会生成 ZX_EVENTPAIR_PEER_CLOSED。
空协议
有时,空协议可以提供价值。例如,创建对象的方法可能还会接收 server_end:FooController 参数。调用方提供此空协议的实现:
protocol FooController {};
FooController 不包含任何用于控制所创建对象的方法,但服务器可以使用协议中的 ZX_CHANNEL_PEER_CLOSED 信号来触发对象的销毁。未来,该协议可能会扩展,添加用于控制所创建对象的方法。
控制设置类数据
服务器通常会公开客户端可以修改的设置。最好使用 table 来表示此类设置。例如,fuchsia.accessibility 库定义了:
type Settings = table {
1: magnification_enabled bool;
2: magnification_zoom_factor float32;
3: screen_reader_enabled bool;
4: color_inversion_enabled bool;
5: color_correction ColorCorrection;
6: color_adjustment_matrix array<float32, 9>;
};
(为提高可读性,省略了注释。)
您可以通过多种方式让客户能够更改这些设置。
部分更新方法公开了一个采用部分设置值的 Update 方法,并且仅当字段存在于部分值中时才更改这些字段。
protocol TheManagerOfSomeSorts {
/// Description how the update modifies the behavior.
///
/// Only fields present in the settings value will be changed.
Update(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
替换方法公开了一个 Replace 方法,该方法接受完整的设置值,并将设置更改为新提供的值。
protocol TheManagerOfOtherSorts {
/// Description how the override modifies the behavior.
///
/// This replaces the setting.
Replace(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
需要避免的事项:
无论是部分更新还是替换方法,都应避免使用动词
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;
});
};
现在,我们来考虑命令联合。在复杂场景中,服务器可能需要能够描述其支持的命令。在这种情况下,如果有一个并行 enum,其成员与 union 的变体一一对应,那么这可以为构建 API 奠定坚实的基础:
type MyCommandVariant = strict enum {
POKE = 1;
PROD = 2;
// ...
};
protocol HighVolumeSinkContinued {
SupportedCommands() -> (struct {
supported_commands vector<MyCommandVariant>;
});
};
请注意,虽然使用 bits 值来表示命令集可能很诱人,但这会导致后续出现一些更难的选择。如果您的 API 发展到需要引用特定命令,那么使用 enum 就很自然了。如果您一开始就选择了 bits 值,那么现在您将面临以下两种糟糕的选择之一:
引入了
enum,这意味着现在有两种方式来引用字段,并且客户端代码中可能存在转换问题(从一种表示形式转换为另一种表示形式);或者继续使用
bits,但限制是任何给定时间只能设置一位,现在映射回设置了哪一位非常麻烦。
总而言之,对于 table:
将
bits命名为table的名称,并添加后缀Fields(复数形式)。每个成员值都应该是序号索引处的位,即1 << (ordinal - 1)。与针对
union的建议类似,您需要使bits和table之间的灵活性保持一致,也就是说,由于 FIDL 目前仅支持灵活的表格,因此bits必须为flexible。
对于 union:
将
enum商品详情命名为union的名称,并添加后缀Variant(单数)。每个成员值都应该是相应变体的序号。使
union和enum之间的灵活性保持一致,即如果union为strict,则enum也必须为strict。
反模式
本部分将介绍几种反模式:通常会产生负面价值的设计模式。学会识别这些模式是避免以错误方式使用它们的第一步。
推送的设置:尽可能避免
Fuchsia 平台通常首选拉取语义。组件的语义提供了一个重要示例;假定功能是从组件中提取的,从而允许组件启动是延迟的,并且组件关闭顺序可以从功能路由的有向图中推断出来。
使用 FIDL 协议将配置从一个组件推送到另一个组件的设计因其表面上的简单性而具有吸引力。当组件 A 将政策推送到组件 B 以供 B 的业务逻辑使用时,就会出现这种情况。这会导致平台误解依赖关系:A 不会自动启动以支持 B 的功能,A 可能会在 B 完成其业务逻辑的执行之前关闭。这反过来会导致需要使用涉及弱依赖关系以及虚幻反向依赖关系的变通方法来产生所需的行为;如果拉取政策而不是推送政策,所有这些都会更简单。
如果可能,最好采用拉取而非推送政策的设计。
客户端库:谨慎使用
理想情况下,客户端使用由 FIDL 编译器生成的特定于语言的客户端库,通过 FIDL 中定义的协议进行交互。虽然这种方法使 Fuchsia 能够为大量目标语言提供高质量的支持,但有时协议过于底层,无法直接进行编程。在这种情况下,可以提供一个手动编写的客户端库,该库与相同的底层协议进行交互,但更易于正确使用。
例如,fuchsia.io 有一个客户端库 libfdio.so,可为该协议提供类似 POSIX 的前端。需要 POSIX 风格的 open/close/read/write 接口的客户端可以链接到 libfdio.so 并通过最少的修改使用 fuchsia.io 协议。此客户端库之所以有价值,是因为该库可在现有库接口和底层 FIDL 协议之间进行适配。
另一种提供正价值的客户端库是框架。框架是一个广泛的客户端库,可为应用的大部分提供结构。通常,框架会针对各种协议提供大量抽象。例如,Flutter 是一个框架,可以看作是 fuchsia.ui 协议的扩展客户端库。
无论 FIDL 协议是否具有关联的客户端库,都应完整记录该协议。一组独立的软件工程师应该能够直接根据协议的定义来理解和正确使用该协议,而无需对客户端库进行逆向工程。如果协议有客户端库,那么应清楚记录协议中那些足够低级和微妙,以至于促使您创建客户端库的方面。
客户端库的主要难点在于,它们需要针对每种目标语言进行维护,这往往意味着对于不太热门的语言,客户端库会缺失(或质量较低)。客户端库还会使底层协议趋于僵化,因为它们会导致每个客户端以完全相同的方式与服务器交互。服务器会逐渐期望这种确切的互动模式,而当客户端偏离客户端库所用的模式时,服务器将无法正常工作。
为了将客户端库纳入 Fuchsia SDK,我们应至少提供两种语言的库实现。
服务中心:谨慎使用
服务中心是一种 Discoverable 协议,可让您发现许多其他协议,通常具有明确的名称:
// BAD
@discoverable
protocol ServiceHub {
GetFoo(resource struct {
foo server_end:Foo;
});
GetBar(resource struct {
bar server_end:Bar;
});
GetBaz(resource struct {
baz server_end:Baz;
});
GetQux(resource struct {
qux server_end:Qux;
});
};
特别是对于无状态协议,ServiceHub 协议在直接发现各个协议服务方面并没有提供太多价值:
@discoverable
protocol Foo {};
@discoverable
protocol Bar {};
@discoverable
protocol Baz {};
@discoverable
protocol Qux {};
无论采用哪种方式,客户端都可以与枚举的服务建立连接。在后一种情况下,客户端可以通过整个系统中用于发现服务的常规机制来发现相同的服务。使用正常机制可让核心平台将适当的政策应用于发现。
不过,在某些情况下,服务中心可能很有用。例如,如果协议是有状态的,或者通过比正常服务发现更复杂的流程获得,那么协议可以通过将状态转移到所获得的服务来提供价值。再举一个例子,如果获取服务的方法需要额外的参数,那么协议可以在连接到服务时考虑这些参数,从而提供价值。
过度面向对象的设计:否
有些库会为协议中的每个逻辑对象创建单独的协议实例,但这种方法存在许多缺点:
不同协议实例之间的消息顺序未定义。通过单个协议发送的消息会按 FIFO 顺序(在每个方向上)处理,但通过不同渠道发送的消息会竞争。当客户端与服务器之间的互动分布在多个渠道中时,如果消息意外重新排序,则出现 bug 的可能性会更大。
每个协议实例在内核资源、等待队列和调度方面都有一定的开销。虽然 Fuchsia 旨在扩展到大量渠道,但成本会累加到整个系统,并且创建大量对象来为系统中的每个逻辑对象建模会给系统带来巨大负担。
错误处理和拆解变得更加复杂,因为错误和拆解状态的数量会随着交互中涉及的协议实例数量呈指数级增长。当您使用单个协议实例时,客户端和服务器都可以通过关闭协议来干净地关闭互动。如果存在多个协议实例,互动可能会进入部分关闭的状态,或者双方对关闭状态的看法不一致。
- 跨协议边界的协调比单个协议内的协调更复杂,因为多种协议需要考虑到不同客户端可能会使用不同协议,而这些客户端可能彼此并不完全信任。
不过,在以下情况下,将功能分离到多个协议中是有用的:
提供单独的协议有助于提高安全性,因为某些客户端可能只能访问其中一种协议,从而限制了它们与服务器的互动。
此外,还可以更轻松地从单独的线程中使用单独的协议。例如,一个协议可能绑定到一个线程,而另一个协议可能绑定到另一个线程。
客户端和服务器需要为协议中的每种方法支付(少量)费用。如果一次只需要几个较小的协议,那么包含所有可能方法的一个大型协议可能不如多个较小的协议高效。
有时,服务器所持有的状态会沿着方法边界清晰地分解。在这种情况下,请考虑沿着这些边界将协议分解为更小的协议,以便为与单独状态的互动提供单独的协议。
避免过度面向对象的一个好方法是使用客户端分配的标识符来对协议中的逻辑对象进行建模。借助该模式,客户端可以通过单个协议与可能包含大量逻辑对象的集合进行交互。
使用魔法值指定缺席:否
我们通常希望指示服务器设置某种状态,但也允许移除状态。以下示例使用 magic 值来指示移除:
// 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;
});
};
-
虽然 FIDL 类型系统在 ABI 方面是结构化类型系统,也就是说名称无关紧要,只有类型结构才重要,但 FIDL 类型系统在 API 方面具有命名类型语义。 ↩