一般建议
本部分介绍了有关使用紫红色接口定义语言定义协议的技巧、最佳实践和一般建议。
另请参阅 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;
});
};
如果开发者希望更改某个方法的名称,选择器还可用来保持与传输格式的向后兼容性。
诊断
有时需要公开有助于调试或诊断程序的信息。这些数据可以采用统计信息和指标(例如错误数、调用次数、大小等)、对开发有用的信息、组件的运行状况或类似的形式。
人们很容易在测试协议或生产协议中的调试方法中公开这些信息。但是,Fucsia 提供了一种单独的机制来公开此类信息:应考虑检查机制,以做出有关如何公开此类数据的最佳决策。当需要公开有助于在测试中进行调试的程序的诊断信息、开发者工具所使用的诊断信息或者通过崩溃报告或指标在现场检索的诊断信息时,应使用检查,而不是 FIDL 方法/协议,前提是没有其他程序使用这些信息来做出运行时决策。
在根据其他程序的诊断信息做出运行时决策时,应使用 FIDL。检查绝不能用于在程序之间进行通信,它是一种尽力而为的系统,在生产环境中运行时期间不能依赖它来做出决策或更改行为。
用于确定是使用 Inspect 还是 FIDL 的启发式方法,具体如下:
其他程序是否在生产环境中使用这些数据?
- 是:使用 FIDL。
这些数据是供崩溃报告或指标使用吗?
- 是:使用检查。
数据是否用于测试或开发者工具?它有可能在生产环境中使用吗?
- 是:使用 FIDL。
- 否:二者都可以。
库结构
将 FIDL 声明分组到 FIDL 库中有两个具体目标:
- 帮助 FIDL 开发者(使用 FIDL 库的开发者)浏览 API Surface。
- 提供在 FIDL 库中分层限定 FIDL 声明范围的结构。
请仔细考虑如何将类型和协议定义划分到多个库中。将这些定义分解成库的方式会对这些定义的使用者产生很大的影响,因为 FIDL 库是类型和协议的依赖项和分发单元。
FIDL 编译器要求库之间的依赖关系图是一个 DAG,这意味着您不能创建跨库边界的循环依赖关系。不过,您可以在库中创建(某些)循环依赖项。
如需决定是否将库分解为较小的库,请考虑以下问题:
库的客户是否拆分为不同的角色,他们想要使用该库中的部分功能或声明?如果是这样,请考虑将库拆分为针对每个角色的单独库。
库是否与对结构具有普遍理解的行业概念相对应?如果是这样,请考虑构建您的库,使其与业界标准结构保持一致。例如,蓝牙已划入
fuchsia.bluetooth.le
和fuchsia.bluetooth.gatt
中,以便与业界一般理解的这些概念保持一致。同样,fuchsia.net.http
对应于业界标准的 HTTP 网络协议。是否有许多其他库依赖于该库?如果是这样,请检查这些传入的依赖项是否真的需要依赖于整个库,或者是否存在可从库分离出来的“核心”定义集来接收大部分传入依赖项。
理想情况下,我们会为整个 Fuchsia 生成一个 FIDL 库结构,这是一种全局最优结构。然而,康威定律指出,“设计系统 [...] 的组织被迫生产的设计是这些组织通信结构的复制品”。我们应该花适度的时间 反对康威的法律
访问权限控制是以协议为单位的
在决定在哪个库中定义协议时,不要考虑访问权限控制注意事项。通常,访问权限控制以协议粒度表示。定义协议的库与访问权限控制无关,无法用于确定能否访问协议。
例如,进程可以访问 fuchsia.logger.LogSink
,或者进程被赋予 fuchsia.media.StreamSource
协议的客户端端。不过,FIDL 不可用于表达对 fuchsia.logger
库的访问权限,也不能用于阻止对 fuchsia.ldsvc
库的访问。
fuchsia
命名空间
在平台源代码树中定义的 FIDL 库(即在 fuchsia.googlesource.com 中定义)必须位于 fuchsia
顶级命名空间(例如,fuchsia.ui
),除非满足以下任一条件:
- 该库定义了 FIDL 语言本身的各个部分或其一致性测试套件,在这种情况下,顶级命名空间必须为
fidl
。 - 该库仅用于内部测试,不包含在 SDK 或正式版 build 中(在这种情况下,顶级命名空间必须为
test
)。
强烈建议顶级命名空间 fuchsia
命名空间中的 FIDL 库不超过四个组件,即 fuchsia.<api-namespace>
、fuchsia.<api-namespace>.<name>
或 fuchsia.<api-namespace>.<name>.<subname>
。
选择合适的 api-namespace
,并且可能在 API 委员会成员的帮助下。
例如,平台源代码树为了向应用提供硬件功能而定义的 FIDL 库必须位于 fuchsia.hardware
命名空间中。例如,用于公开以太网设备的协议可以命名为 fuchsia.hardware.ethernet.Device
。基于这些 FIDL 协议构建的更高级别功能不属于 fuchsia.hardware
命名空间。例如,网络协议位于 fuchsia.net
下比 fuchsia.hardware
下更适合。
避免嵌套过深
首选包含三个组成部分的库名称(例如 fuchsia.hardware.network
),并避免使用超过四个组成部分的库名称(例如 fuchsia.apps.foo.bar.baz
)。如果您使用四个以上的组件,应该有具体理由做出该选择。
库依赖项
最好将依赖项从具有更具体名称的库引入到名称不太具体的库。例如,fuchsia.foo.bar
可能依赖于 fuchsia.foo
,但 fuchsia.foo
不应依赖于 fuchsia.foo.bar
。这种模式有利于扩展,因为随着时间的推移,我们可以添加更多名称更具体的库,但名称不够具体的库数量有限。
导入库的可见性
为了扩展将 FIDL 声明分组到 FIDL 库中的第二个目标,我们希望改进 FIDL,以提供可见性规则,改变是否可以通过导入库(“子级库”)来使用元素,例如 public
或 private
修饰符。
internal
库组件名称经过特殊处理,表示可见性规则的本地限制。例如,fuchsia.net.dhcp.internal.foo
库中的公开声明可能仅对其父级 fuchsia.net.dhcp
或其同级项(例如 fuchsia.net.dhcp.internal.bar
)可见。
使用多字词库组件
虽然允许使用由多个单词组成的组件(例如 fuchsia.modular.storymodel
)的库名称,但应特别注意。如果库名称会违反嵌套规则,或者从层次结构地考虑库的放置,如果两个单词都不应该优先于另一个单词,库作者可以采取将多个单词连接在一起的方法。
版本字符串
如果库需要进行版本控制,则应为单个版本号添加后缀,例如 fuchsia.io2
或 fuchsia.something.something4.
版本号不应分为多个部分。例如,不得使用 fuchsia.io2.1
,而应采用 fuchsia.io3
。任何库组件都可以带有版本编号,但强烈建议不要使用多个版本化组件,例如,使用 fuchsia.hardware.cpu2.ctrl
而不是 fuchsia.hardware.cpu2.ctrl4
。
版本号应仅指明库的更新版本,而不是截然不同的网域。作为反例,fuchsia.input
库用于较低级别的设备处理,而 fuchsia.ui.input{2,3}
用于与场景互动和呈现界面的软件组件交互的输入。如果仅专注于版本控制,那么将 fuchsia.ui.scenic.input
和 fuchsia.ui.scenic.input2
与 fuchsia.input
提供的其他网域区分开会更为清晰。
类型
正如“一般建议”部分所述,您应特别注意在协议定义中使用的类型。
保持一致
对同一概念使用一致的类型。例如,在整个库中对特定概念一致使用 uint32
或 int32
。如果您为概念创建 struct
,请在使用该结构体来表示该概念时保持一致。
理想情况下,类型也应跨库边界一致使用。请检查相关库是否存在类似概念,并与这些库保持一致。如果库之间有许多共同的概念,请考虑将这些概念的类型定义分解成一个通用库。例如,fuchsia.mem
和 fuchsia.math
包含许多用于表示内存和数学概念的常用类型。
首选语义类型
即使可以使用基元表示常用概念,也可以创建结构体来命名这些概念。例如,IPv4 地址是网络库中的一个重要概念,应使用结构体进行命名,即使数据可以使用基元表示也是如此:
type Ipv4Address = struct {
octets array<uint8, 4>;
};
在性能关键型目标语言中,结构体以内嵌方式表示,这降低了使用结构体命名重要概念的费用。
zx.Time
具有明确定义的时基
zx.Time
类型单调测量基于设备特定时基的纳秒时间。使用 zx.Time
时可以假定采用此时基,无需拼写出来。
谨慎使用匿名类型
匿名类型对于更流利地描述 API 非常有用。具体而言,匿名类型非常适合在以下情况下使用:您已事先知道某个已命名类型的子元素本身与该命名类型相关联,并且在包含的已命名容器的上下文之外使用时,它没有用处或没有意义。
例如,考虑一个 union 变体,它将一些内容汇总到一起。并集变体单独使用的情况极为罕见,也就是说,我们先知道并集变体仅在其特定用途中才有意义。因此,建议对联合变体使用匿名类型。
理想情况下,这两种类型都应该一一对应到 API 的关键概念,并且任何两种类型都不应具有相同的定义。但是,并不总是能同时实现两者,尤其是在类型命名(这引入了一个不同的概念1)的情况下,比起作为 API Surface 元素有意义,更是如此。例如,假设命名标识符 type EntityId = struct { id
uint64; };
和 type OtherEntityId = struct { id uint64; };
表示不同的概念,但其名称除外,其类型定义相同。
使用匿名类型会创建多个类型,这些类型彼此不兼容。因此,如果使用多个匿名类型来表示同一概念,则会导致 API 过于复杂,并妨碍在大多数目标语言中进行通用处理。
因此,使用匿名类型时,您必须避免有多个匿名类型代表同一概念。例如,如果 API 的演变可能会导致多个代表同一概念的匿名类型,则不得使用匿名类型。
考虑使用虚拟内存对象 (VMO)
虚拟内存对象 (VMO) 是一种内核对象,表示虚拟内存的连续区域以及逻辑大小。使用此类型可传输 FIDL 消息中的内存,并使用 ZX_PROP_VMO_CONTENT_SIZE
属性跟踪对象中包含的数据量。
指定矢量和字符串的边界
所有 vector
和 string
声明都应指定长度边界。声明通常分为以下两类:
- 数据存在固有的限制。例如,包含文件系统名称组成部分的字符串不得超过
fuchsia.io.MAX_FILENAME
。 - 除了“尽可能多”之外,没有其他限制。在这些情况下,您应使用内置常量
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. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{Context as _, Error}, config::Config, fuchsia_component::client::connect_to_protocol, std::{thread, time}, }; use { fidl::endpoints::create_proxy, fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}, futures::join, }; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>()?; // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{Context as _, Error}, fuchsia_component::server::ServiceFs, futures::prelude::*, lazy_static::lazy_static, regex::Regex, }; use { fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }, fuchsia_async as fasync, std::collections::btree_map::Entry, std::collections::BTreeMap, std::ops::Bound::*, std::sync::Arc, std::sync::Mutex, }; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream()?)) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++ (Wire)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
字符串编码、字符串内容和长度边界
FIDL string
采用 UTF-8 编码,这是一种可变宽度编码,每个 Unicode 代码点使用 1、2、3 或 4 个字节。
绑定会对字符串强制执行有效的 UTF-8 编码,因此字符串不适用于任意二进制数据。请参阅我应该使用字符串还是矢量?。
由于长度边界声明的目的是为 FIDL 消息的总字节数提供易于计算的上限,因此 string
边界用于指定字段中的最大字节数。安全起见,您通常需要设置 (4 bytes · code points in
string)
预算。(如果您确定文本仅使用单字节 ASCII 范围内的码位,例如电话号码或信用卡号,则每个码位 1 个字节就足够了。)
一个字符串中有多少个码位?回答这个问题可能比较复杂,尤其是对于用户生成的字符串内容,因为 Unicode 代码点与用户可能视作的“字符”之间没有必要一一对应。
例如,字符串
á
会渲染为单个用户感知的“字符”,但实际上由两个码位组成:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
在 Unicode 术语中,这种用户感知的“字符”称为字素聚类。
单个字形集群可以由任意数量的代码点组成。请参考下面这个较长的示例:
á🇨🇦b👮🏽♀️
如果您的系统和字体支持它,您应该会在上方看到四个字形集群:
1. 'a' with acute accent
2. emoji of Canadian flag
3. 'b'
4. emoji of a female police officer with a medium skin tone
这四个字素聚类编码为 10 个代码点:
1. LATIN SMALL LETTER A (U+0061)
2. COMBINING ACUTE ACCENT (U+0301)
3. REGIONAL INDICATOR SYMBOL LETTER C (U+1F1E8)
4. REGIONAL INDICATOR SYMBOL LETTER A (U+1F1E6)
5. LATIN SMALL LETTER B (U+0062)
6. POLICE OFFICER (U+1F46E)
7. EMOJI MODIFIER FITZPATRICK TYPE-4 (U+1F3FD)
8. ZERO WIDTH JOINER (U+200D)
9. FEMALE SIGN (U+2640)
10. VARIATION SELECTOR-16 (U+FE0F)
在 UTF-8 中,此字符串占 28 个字节。
在此示例中,应该很明显,如果应用的界面显示一个允许 N 个任意字素集群(用户把它们视为“字符”)的文本输入框,并且您计划通过 FIDL 传输用户输入的字符串,就必须在 FIDL string
字段中为 4·N
预算分配一些倍数。
该倍数应该是多少?这取决于您的数据。如果您处理的是相当受限的用例(例如人名、邮政地址、信用卡号),您可以假设每个字形集群有 1-2 个码位。如果您正在构建的聊天客户端频繁使用表情符号,则每个字素集群 4-5 个码位可能更安全。在任何情况下,您的输入验证界面都应显示清晰的视觉反馈,以确保用户在存储空间不足时不会感到惊讶。
整数类型
选择适合您的用例的整数类型,并保持一致的使用方式。如果最好将值视为数据字节,请使用 byte
。如果负值没有意义,则使用无符号类型。一般来说,如果您不确定,请使用 32 位值表示数量较少的商品,如果数量较大,请使用 64 位值。
如果可能有更多状态,请避免使用布尔值
添加布尔值字段时,如果可以扩展该字段以表示将来的其他状态,请考虑改用枚举。例如,最好用以下内容来表示布尔值 is_gif
字段:
type FileType = strict enum {
UNKNOWN = 0;
GIF = 1;
};
然后,您可以根据需要通过 JPEG = 2
扩展枚举。
我该如何表示错误?
根据您的用例选择合适的错误类型,并保持一致的错误报告方式。
您可以使用 error
语法清晰地记录和传达可能的错误返回结果,并利用量身定制的目标语言绑定。
(已弃用带错误枚举的可选值模式。)
使用错误语法
方法可以使用可选的 error <type>
说明符来指示它们返回值,或者输出错误并生成 <type>
。示例如下:
// Only erroneous statuses are listed
type MyErrorCode = strict enum {
MISSING_FOO = 1; // avoid using 0
NO_BAR = 2;
};
protocol Frobinator {
Frobinate() -> (struct {
value SuccessValue;
}) error MyErrorCode;
};
使用此模式时,您可以使用 int32
、uint32
或其枚举来表示返回的错误类型。在大多数情况下,返回枚举是首选方法。
最好为协议的所有方法采用同一错误类型。
首选特定于网域的枚举
定义和控制网域时,请使用用途构建的枚举错误类型。例如,在构建协议时定义枚举,传达错误的语义是唯一的设计约束条件。如 enum 部分所述,最好避免使用值 0
。
在某些情况下,最好先使用空的灵活枚举:
type MyEmptyErrorCode = flexible enum {};
protocol Frobinator2 {
Frobinate() -> (struct {
value SuccessValue;
}) error MyEmptyErrorCode;
BadFrobinate() -> (struct {
value SuccessValue;
}) error flexible enum {}; // avoid anonymous enum
};
灵活枚举具有默认的未知成员。因此,空的灵活枚举属于一种类型化占位符,提供改进性。使用此模式时,建议定义一个要由一个协议(或库)中的多个方法重复使用的独立类型,而不是使用匿名枚举。使用匿名枚举会创建多种类型,所有类型彼此都不兼容,从而导致 API 过于复杂,并阻止对大多数目标语言的错误进行泛化处理。
如果您遵循明确定义的规范(如 HTTP 错误代码),并且枚举是一种符合人体工程学的方法,用于表示规范规定的原始值,请使用网域特定的枚举错误类型。
尤其要注意的是,请为与内核对象或 IO 相关的错误使用 zx.Status
类型。例如,fuchsia.process
使用 zx.Status
,因为库主要用于操控内核对象。再举一个例子,fuchsia.io
大量使用 zx.Status
,因为该库与 IO 相关。
结合使用可选值和错误枚举
过去,定义具有两个返回值(可选值和错误代码)的方法略有性能优势。请参阅以下示例:
type MyStatusCode = strict enum {
OK = 0; // The success value should be 0,
MISSING_FOO = 1; // with erroneous status next.
NO_BAR = 2;
};
protocol Frobinator3 {
Frobinate() -> (struct {
value box<SuccessValue>;
err MyStatusCode;
});
};
但是,此模式现在已弃用,取而代之的是错误语法:通过在信封中内嵌小值已废弃了现有的性能优势,现在已经普遍支持联合体。
避免出现错误的消息和说明
在某些不寻常的情况下,如果可能的错误情况范围很大,并且描述性错误消息可能对客户端有用,则除了 status
或枚举值之外,协议可能还包括错误的字符串说明。但是,添加字符串会带来困难。例如,客户端可能会尝试解析字符串以了解发生了什么情况,这意味着字符串的确切格式会成为协议的一部分,这在对字符串进行本地化时尤其重要。
安全说明:同样,向客户端报告堆栈轨迹或异常消息也可能会意外泄露特许信息。
将字符串和错误消息本地化
如果要构建充当界面后端的服务,请使用结构化类型的消息,并将渲染留给界面层。
如果您的所有消息都简单且未参数化,请使用 enum
进行错误报告和常规界面字符串。对于包含名称、数字和位置等参数的更详细的消息,请使用 table
或 xunion
,并将参数作为字符串或数字字段传递。
您可能会想要在服务中生成消息(用英语),并以字符串的形式将其提供给界面,界面只是收到一个字符串,然后弹出一个通知或错误对话框。
不过,这种更简单的方法存在一些严重的缺点:
- 您的服务是否知道界面中使用的是哪种语言区域(语言和区域)?您必须随每个请求传递语言区域(请参阅示例),或跟踪每个连接的客户端的状态,以便使用正确的语言提供消息。
- 您的服务的开发环境是否可为本地化提供良好的支持?如果您使用 C++ 编写代码,可以轻松访问 ICU 库和
MessageFormat
,但如果您使用的是 Rust,则库支持目前有限。 - 是否有任何错误消息都需要包含界面可以熟知但服务却不知道的参数?
- 您的服务是否只提供单一界面实现?该服务是否知道界面需要多少空间来显示消息?
- 错误是否仅以文本形式显示?您可能还需要特定于错误的提醒图标、音效或文字转语音提示。
- 用户是否能在界面仍在运行时更改显示语言区域?如果发生这种情况,预本地化的字符串可能很难更新为新的语言区域,尤其是当它们是由某些非幂等操作引起的时。
除非您要构建与单个界面实现紧密耦合的高度专业的服务,否则不应在 FIDL 服务中公开用户可见的界面字符串。
我应该定义一个结构体来封装方法参数(或响应)吗?
每次定义方法时,您都需要决定是单独传递参数,还是将参数封装在结构体中。做出最佳选择需要权衡多种因素。请考虑以下问题,以帮助您做出决定:
是否存在有意义的封装边界?如果一组参数适合作为一个单元进行传递,因为它们具有不同于此方法的某种内聚性,那么您可能需要将这些参数封装在结构体中。(希望您在开始设计协议时已经确定了这些紧密的群体,因为您遵循了上面的“一般性建议”,并在早期专注于类型。)
除了所调用的方法之外,结构体是否有用?如果没有,请考虑单独传递参数。
您是否在许多方法中重复使用相同的参数组?如果是这样,请考虑将这些参数分组到一个或多个结构中。此外,您还可以考虑重复是否表明这些参数是连贯的,因为它们代表协议中的一些重要概念。
是否存在大量可选参数或通常被指定为默认值的参数?如果是,请考虑使用结构体来减少调用方的样板代码。
是否存在始终同时为 null 或非 null 的参数组?如果是这样,请考虑将这些参数分组到可为 null 的结构体中,以在协议本身中强制执行该不变的结构。例如,上面定义的
FrobinateResult
结构体包含的值在error
不是MyError.OK
时始终为 null。
我应该使用 string
还是 vector<byte>
?
在 FIDL 中,string
数据必须是有效的 UTF-8 数据,这意味着字符串可以表示 Unicode 代码点序列,但不能表示任意二进制数据。相比之下,vector<byte>
或 array<byte, N>
可以表示任意二进制数据,并不表示 Unicode。
对文本数据使用 string
:
使用
string
表示软件包名称,因为软件包名称必须是有效的 UTF-8 字符串(包含某些排除的字符)。使用
string
表示软件包中的文件名,因为软件包中的文件名必须是有效的 UTF-8 字符串(包含某些排除的字符)。使用
string
表示媒体编解码器名称,因为媒体编解码器名称是从固定的有效 UTF-8 字符串词汇表中选择的。使用
string
表示 HTTP 方法,因为 HTTP 方法由固定选择的字符(始终是有效的 UTF-8)组成。
对于小型非文本数据,请使用 vector<byte>
或 array<byte, N>
:
对于 HTTP 标头字段,请使用
vector<byte>
,因为 HTTP 标头字段未指定编码,因此不一定能以 UTF-8 编码表示。请使用
array<byte, 6>
作为 MAC 地址,因为 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
。典型示例是 PIPI,它通常被编码为 const
,因为这样不方便在每次使用此值时都输入 3.141592653589
。
或者,您也可以在值可能会更改时使用 const
,但需要在整个过程中一致地使用该值。给定字段中可提供的最大字符数就是一个很好的例子(例如,MAX_NAME_LEN)。通过使用 const
,您可以集中定义该数字,因此整个代码中最终不会产生不同的值。
选择 const
的另一个原因是,您既可将其用于约束消息,也可稍后在代码中使用。例如:
const MAX_BATCH_SIZE int32 = 128;
protocol Sender {
Emit(struct {
batch vector<uint8>:MAX_BATCH_SIZE;
});
};
然后,您可以在代码中使用常量 MAX_BATCH_SIZE
来组建批次。
枚举
如果枚举值集由 Fuchsia 项目限定和控制,请使用枚举。例如,Fuchsia 项目定义了指针事件输入模型,因此控制了 PointerEventPhase
枚举的值。
在某些情况下,如果可以合理预期想要注册新值的用户会向 Fuchsia 源代码树提交补丁以注册值,那么即使 Fuchsia 项目本身不控制枚举值集,您也应使用枚举。例如,纹理格式需要 Fuchsia 图形驱动程序理解,这意味着,即使纹理格式集由图形硬件供应商控制,开发这些驱动程序的开发者也可以添加新的纹理格式。作为反例,请勿使用枚举表示 HTTP 方法,因为我们不能合理地指望使用新式 HTTP 方法的用户向平台源代码树提交补丁。
对于先验无界限集,如果您希望动态扩展该集,string
可能是更合适的选择。例如,请使用 string
来表示媒体编解码器名称,因为中介或许能够对新颖的媒体编解码器名称执行一些合理的操作。
如果枚举值集合由外部实体控制,请使用(大小适当的)整数或 string
。例如,请使用某个整数(具有特定大小)来表示 USB HID 标识符,因为这组 USB HID 标识符由行业联盟控制。同样,请使用 string
表示 MIME 类型,因为 MIME 类型(至少在理论上)由 IANA 注册表控制。
我们建议开发者尽可能避免将 0
用作枚举值。由于许多目标语言都使用 0
作为整数的默认值,因此很难区分 0
值是有意设置的,还是因为设置了默认值而设置的。例如,fuchsia.module.StoryState
定义了三个值:RUNNING
(值为 1
)、STOPPING
(值为 2
)和 STOPPED
(值为 3
)。
在以下两种情况下,可以使用 0
值:
- 枚举具有自然的默认、初始或未知状态;
- 枚举定义带有错误枚举的可选值模式中使用的错误代码。
位
如果您的协议包含位字段,请使用 bits
值表示其值(如需了解详情,请参阅 RFC-0025
:“位标志”)。
例如:
type InfoFeaturesHex = strict bits : uint32 {
WLAN = 0x00000001; // If present, this device represents WLAN hardware
SYNTH = 0x00000002; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0x00000004; // If present, this device receives all messages it sends
};
这表示 InfoFeatures
位字段由一个无符号的 32 位整数提供支持,然后继续定义使用的 3 位。
您还可以使用 0b
表示法以二进制(而不是十六进制)表示值:
type InfoFeaturesBits = strict bits : uint32 {
WLAN = 0b00000001; // If present, this device represents WLAN hardware
SYNTH = 0b00000010; // If present, this device is synthetic (not backed by h/w)
LOOPBACK = 0b00000100; // If present, this device receives all messages it sends
};
这与上一个示例相同。
我应该使用 resource
吗?
FIDL 编译器将强制要求已经包含 resource
的所有类型都标记为这样。
如果 flexible
类型不包含资源,但将来可能会包含资源,则应提前添加 resource
修饰符,以免日后出现困难的过渡。这种情况很少见:经验表明,大多数消息不包含资源,并且在协议中传递资源需要谨慎并提前规划。
我应该对类型使用 strict
还是 flexible
?
将类型标记为 flexible
可以处理当前 FIDL 架构未知的数据,建议用于将来可能添加或移除成员的类型(例如配置、元数据或错误)。对于现有类型,始终可以在 strict
和 flexible
之间进行软转换。
stylish 在类型允许的情况下始终指定此修饰符。Fuchsia 项目通过 linter 检查强制执行此样式。
使用 strict
或 flexible
不会对性能产生任何显著影响。
处理权利
本部分介绍了针对 FIDL 中的句柄分配权限限制的最佳做法。
如需详细了解如何在绑定中使用权限,请参阅 FIDL 绑定规范或 RFC-0028。
有关 zircon 权限定义,请参阅内核权限。 FIDL 使用 rights.fidl 来解决权利限制。
始终指定标识名权限
所有标识名都应指定相关权利,明确说明预期用途。此要求强制预先决定要授予哪些权限,而不是根据观察到的行为做出决定。拥有明确权利还有助于提高 API Surface 的可审核性。
使用收件人需要的最低权限
在确定要提供哪些权利时,请优先考虑最低限度,即实现所需功能所需的最低权利。例如,如果已知只需要 zx.Rights.READ
和 zx.Rights.WRITE
,则只需指定这两项权限。
请勿根据推测性需求添加权利。如果将来需要添加某项权利,可以通过以下方式添加该权利:从来源来源开始,然后将其添加到调用路径中的每个位置,直到最终使用点。
谨慎使用 zx.Rights.SAME_RIGHTS
zx.Rights.SAME_RIGHTS
非常适用于转发未知权限句柄的协议,但在大多数情况下,应改用一组特定权限。这样做的一部分动机是,zx.Rights.SAME_RIGHTS
会指示绑定跳过权限检查,因此它会停用处理权限可能提供的安全保护。此外,zx.Rights.SAME_RIGHTS
还会使权限设置动态变化,这意味着进程获得的权限可能会少于或大于其实际需要的权限。
值得注意的是,zx.Rights.SAME_RIGHTS
与为类型(例如 zx.DEFAULT_CHANNEL_RIGHTS
)设置的默认权限不同。前者跳过权利检查,而后者要求给定对象类型具有所有正常权限。
良好的设计模式
本部分将介绍几种可在许多 FIDL 协议中重复出现的良好设计模式。
我应该对方法和事件使用 strict
还是 flexible
?
将某个方法或事件标记为 flexible
后,当在不同版本中构建了不同的组件时,可以更轻松地移除相应方法或事件,使一些组件认为该方法或事件存在,而其他组件则不存在。我们通常希望能够灵活地演变协议,因此建议为方法和事件选择 flexible
,除非有充分的理由选择 strict
。
将方法设为 flexible
不会产生单向方法或事件方面的开销。对于双向方法,选择 flexible
会向消息增加少量开销(16 字节或更少),并且可能会额外增加一点时间来解码消息。总体而言,使双向方法变得灵活的成本应该足够小,而不是几乎所有用例都需要考虑。
仅当方法和事件对协议的正确行为非常重要,以至于接收端缺少该方法或事件足够严重,导致两端之间的所有通信都被中止并关闭连接时,才应将方法和事件设为 strict
。
这在设计前馈数据流时特别有用。我们来考虑一下这种日志记录器协议,它支持安全处理包含个人身份信息 (PII) 的日志的模式。它使用前馈模式添加记录,以便客户端可以依序启动许多操作,而无需等待往返时间,而只在最后刷新待处理的操作。
open protocol Logger {
flexible AddRecord(struct {
message string;
});
strict EnablePIIMode();
flexible DisablePIIMode();
flexible Flush() -> ();
};
此处的所有方法都是 flexible
(EnablePIIMode
除外);请考虑如果服务器无法识别其中任一方法,会发生什么情况:
AddRecord
:服务器只是未能将数据添加到日志输出中。发送方应用会正常运行,但其日志记录不再有用。这种做法虽然不便,但很安全。EnablePIIMode
:服务器未能启用 PII 模式,这意味着它可能无法采取安全预防措施,并泄露 PII。这是一个严重问题,因此如果服务器无法识别此方法,最好关闭通道。DisablePIIMode
:服务器对不需要 PII 日志记录的邮件采取不必要的安全预防措施。这对于尝试读取日志的人可能很不方便,但对系统来说是安全的。Flush
:服务器未能按请求刷新记录,这可能带来不便,但仍然安全。
将此协议设计为完全灵活的另一种方法是,将 EnablePIIMode
设为双向方法 (flexible EnablePIIMode() -> ();
),以便客户端可以查明服务器是否没有该方法。请注意这如何为客户端提供了额外的灵活性;借助此设置,客户端可以选择是对无法识别 EnablePIIMode
的服务器做出响应,方法是关闭连接,还是仅选择不记录个人身份信息,而使用 strict
时,协议始终会自动关闭。但是,这样会中断前馈流。
请注意,严格程度取决于发件人。假设您在版本 1 中有某种方法 strict A();
,然后在版本 2 中将其更改为 flexible A();
,然后在版本 3 中将其删除。如果在版本 1 中构建的客户端尝试在版本为 3
构建的服务器上调用 A()
,则该方法将被视为严格方法,因为版本 1 的客户端认为该方法严格,而版本 3 的服务器会接受客户端的说法,因为它根本无法识别该方法。
stylish 应始终指定此修饰符。Fuchsia 项目通过 linter 检查强制执行此样式。
我应该使用 open
、ajar
还是 closed
?
将协议标记为 open
后,当在不同版本下构建了不同的组件时,可以更轻松地处理移除方法或事件,这样每个组件都可以有一个不同的视图来显示存在哪些方法和事件。一般来说,我们希望能够灵活地演变协议,因此,除非有理由选择更封闭的协议,否则建议为协议选择 open
。
决定使用 ajar
或 closed
时,应基于对协议演变的预期约束。使用 closed
或 ajar
不会阻止协议演变,但要求对较长的发布期(其中方法和事件存在但未使用)的要求,以确保所有客户端和服务器就存在的方法达成一致。您可以灵活地使用 flexible
添加和移除方法及事件,具体取决于是先更新客户端还是服务器。
ajar
可能适用于使用前馈数据流但其演变局限于单向方法的协议。例如,这可能适用于表示事务的拆解协议,其中唯一的双向方法是提交操作,该操作必须严格,而针对事务的其他操作可能会发生变化。
ajar protocol BazTransaction {
flexible Add(resource struct {
bars vector<client_end:Bar>;
});
strict Commit() -> (struct {
args Args;
});
};
open protocol Foo3 {
flexible StartBazTransaction(resource struct {
transaction server_end:BazTransaction;
});
};
closed
适用于以下关键协议:任何未知方法都是严重问题,应导致关闭通道,而不是继续处于潜在的不良状态。此外,对于不太可能更改的协议,或至少对任何更改都可能涉及极长的发布周期的协议,也可以使用此参数,这样更改 strict
方法所产生的额外费用会在发布周期内预料到。
协议请求流水线
协议请求管道是最好且使用最广泛的设计模式之一。客户端不会返回支持某个协议的通道,而是发送该通道并请求服务器将该协议的实现绑定到该通道:
// GOOD:
protocol GoodFoo {
GetBar(resource struct {
name string;
bar server_end:Bar;
});
};
// BAD:
protocol BadFoo {
GetBar(struct {
name string;
}) -> (resource struct {
bar client_end:Bar;
});
};
此模式很有用,因为在开始使用 Bar
协议之前,客户端不需要等待往返。相反,客户端可以立即将 Bar
的消息排入队列。这些消息将由内核进行缓冲并最终在 Bar
的实现绑定到协议请求后进行处理。相比之下,如果服务器返回 Bar
协议的实例,则客户端需要等待整个往返,才能将 Bar
的消息加入队列。
如果请求可能会失败,请考虑扩展此模式,并添加一个描述操作是否成功的回复:
protocol CodecProvider {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec;
}) -> (struct {
succeed bool;
});
};
为处理失败情况,客户端会等待回复,并在请求失败时执行一些其他操作。另一种方法是让协议包含服务器在协议开头发送的事件:
protocol Codec2 {
-> OnReady();
};
protocol CodecProvider2 {
TryToCreateCodec(resource struct {
params CodecParams;
codec server_end:Codec2;
});
};
为处理失败情况,如果 Codec2
通道在事件到达前关闭,客户端会等待 OnReady
事件并执行一些其他操作。
但是,如果请求有可能成功,则拥有任一种成功信号都是有害的,因为该信号允许客户端区分不同的失败模式,这些模式通常应该以相同的方式进行处理。例如,客户端应将建立连接后立即失败的服务视为一开始就无法访问的服务。在这两种情况下,服务都不可用,客户端应生成错误或寻找其他方法来完成其任务。
流控制
FIDL 消息由内核进行缓冲。如果一个端点生成的消息多于另一个端点消耗的消息,这些消息将会累积在内核中,占用内存并增加系统恢复的难度。 相反,精心设计的协议应限制消息的生成,以匹配消息的使用速率,这一属性称为“流控制”。
流控制是一个广泛而复杂的主题,并且有许多有效的设计模式。本部分介绍一些比较常见的流控制模式,但并非详尽无遗。这些模式按偏好设置降序列出。如果其中某种模式适合特定用例,则应使用它。但如果不是,协议可以随意使用下文未列出的替代流控制机制。
更喜欢下拉来推
如果不经过仔细设计,服务器将数据推送到客户端所用的协议通常具有很差的流控制能力。如需提供更好的流控制,一种方法是让客户端从服务器拉取一个或一个范围。拉取模型具有内置的流控制,因为客户端会自然地限制服务器生成数据的速率,并避免从服务器推送的消息不堪重负。
使用挂起获取 (Get) 延迟响应
实现基于拉取的协议的一种简单方法是使用挂起 get 模式与服务器“托管回调”:
protocol FooProvider {
WatchFoo(struct {
args Args;
}) -> (resource struct {
foo client_end:Foo;
});
};
在此模式中,客户端发送 WatchFoo
消息,但服务器在有要发送给客户端的新信息之前不会回复。客户端会使用 foo
并立即发送另一个挂起的 get。客户端和服务器分别为每个数据项执行一个工作单元,这意味着两者都不优先。
当要传输的数据项集的大小有限且服务器端状态很简单时,挂起 get 模式可以正常工作,但在客户端和服务器需要同步其工作的情况下,挂起获取模式就无法正常运行。
例如,服务器可能会为每个客户端使用“脏”位,从而针对某些可变状态 foo
实现挂起获取模式。它会将此位初始化为 true,在每个 WatchFoo
响应中将其清除,并在 foo
每次发生更改时进行设置。只有在设置脏位时,服务器才会响应 WatchFoo
消息。
使用确认限制推送
在使用推送的协议中提供流控制的一种方法是确认模式,在这种模式下,调用方提供确认响应,供调用方用于流控制。以下面的通用监听器协议为例:
protocol Listener {
OnBar(struct {
args Args;
}) -> ();
};
监听器应在收到 OnBar
消息后立即发送一条空响应消息。响应不会将任何数据传递给调用方。相反,响应可让调用方观察被调用方使用消息的速率。调用方应限制生成消息的速率,使其与被调用方接收消息的速率一致。例如,调用方可能仅安排一条(或固定数量)的消息传输(即等待确认)。
FIDL 配方:确认模式
确认模式是对原本属于单向调用的方法进行流控制的一种简单方法。该方法会变为单向调用,且没有响应,俗称“ack”。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. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{format_err, Context as _, Error}, config::Config, fidl_examples_canvas_addlinemetered::{InstanceEvent, InstanceMarker, Point}, fuchsia_component::client::connect_to_protocol, futures::TryStreamExt, std::{thread, time}, }; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); for action in config.script.into_iter() { // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } continue; } // If the action is not a "WAIT", we need to draw a line instead. Parse the string input, // making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?; let line = [from, to]; // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. println!("AddLine request sent: {:?}", line); // By awaiting on the reply, we prevent the client from sending another request before the // server is ready to handle, thereby syncing the flow rate between the two parties over // this method. instance.add_line(&line).await.context("Error sending request")?; println!("AddLine response received"); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{Context as _, Error}, fidl::endpoints::RequestStream as _, fidl_examples_canvas_addlinemetered::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }, fuchsia_async::{Time, Timer}, fuchsia_component::server::ServiceFs, fuchsia_zircon::{self as zx}, futures::future::join, futures::prelude::*, std::sync::{Arc, Mutex}, }; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, bounding_box: BoundingBox, } impl CanvasState { /// Handler for the `AddLine` method. fn add_line(&mut self, line: [Point; 2]) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut self.bounding_box; for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. self.changed = true } } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second. Timer::new(Time::after(zx::Duration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change tracker. state.changed = false } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLine { line, responder } => { println!("AddLine request received: {:?}", line); state_ref.lock().unwrap().add_line(line); // Because this is now a two-way method, we must use the generated `responder` // to send an in this case empty reply back to the client. This is the mechanic // which syncs the flow rate between the client and server on this method, // thereby preventing the client from "flooding" the server with unacknowledged // work. responder.send().context("Error responding")?; println!("AddLine response sent"); } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This await does not complete, and thus the function does not return, unless the server errors // out. The stream will await indefinitely, thereby creating a long-lived server. Here, we first // wait for the updater task to realize the connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::Event<examples_canvas_addlinemetered::Instance::OnDrawn>& event) override { auto top_left = event.top_left(); auto bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::Point(x, y); } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples_canvas_addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::Result<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.is_ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.error_value().FormatDescription(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequest& request, AddLineCompleter::Sync& completer) override { auto points = request.line(); FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request.line()) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/add_line_metered/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_addlinemetered::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn(fidl::WireEvent<examples_canvas_addlinemetered::Instance::OnDrawn>* event) override { auto top_left = event->top_left; auto bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_addlinemetered::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_addlinemetered::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_addlinemetered::wire::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::fidl::Array<::examples_canvas_addlinemetered::wire::Point, 2> ParseLine( const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_addlinemetered::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; client->AddLine(line).ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_addlinemetered::Instance::AddLine>& result) { // Check if the FIDL call succeeded or not. if (!result.ok()) { // Check that our two-way call succeeded, and handle the error appropriately. In the // case of this example, there is nothing we can do to recover here, except to log an // error and exit the program. FX_LOGS(ERROR) << "Could not send AddLine request: " << result.status_string(); } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.addlinemetered/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples_canvas_addlinemetered::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_addlinemetered::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) : binding_(fidl::BindServer( dispatcher, std::move(server_end), this, [this](InstanceImpl* impl, fidl::UnbindInfo info, fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; })), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(AddLineRequestView request, AddLineCompleter::Sync& completer) override { auto points = request->line; FX_LOGS(INFO) << "AddLine request received: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : request->line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |completer| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. completer.Reply(); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_addlinemetered::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. weak->state_.changed = false; }, after); } fidl::ServerBindingRef<examples_canvas_addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_addlinemetered::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_addlinemetered::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <examples/fidl/new/canvas/add_line_metered/hlcpp/client/config.h> #include "lib/fpromise/result.h" // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::addlinemetered::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::addlinemetered::Point{.x = x, .y = y}; } // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. ::std::array<::examples::canvas::addlinemetered::Point, 2> ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::addlinemetered::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop]( ::examples::canvas::addlinemetered::Point top_left, ::examples::canvas::addlinemetered::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; for (const auto& action : conf.script()) { // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); continue; } // Draw a line to the canvas by calling the server, using the two points we just parsed // above as arguments. auto line = ParseLine(action); FX_LOGS(INFO) << "AddLine request sent: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; instance_proxy->AddLine(line, [&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do to // recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "AddLine response received"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/addlinemetered/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; examples::canvas::addlinemetered::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::addlinemetered::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) : binding_(fidl::Binding<examples::canvas::addlinemetered::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLine(::std::array<::examples::canvas::addlinemetered::Point, 2> line, AddLineCallback callback) override { FX_LOGS(INFO) << "AddLine request received: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : line) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; // Because this is now a two-way method, we must use the generated |callback| to send an in // this case empty reply back to the client. This is the mechanic which syncs the flow rate // between the client and server on this method, thereby preventing the client from "flooding" // the server with unacknowledged work. callback(fpromise::ok()); FX_LOGS(INFO) << "AddLine response sent"; } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one. if (!weak->state_.changed) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change tracker. state_.changed = false; }, after); } fidl::Binding<examples::canvas::addlinemetered::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached to // the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.addlinemetered.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::addlinemetered::Instance>( [dispatcher](fidl::InterfaceRequest<examples::canvas::addlinemetered::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
使用事件推送有界限数据
在 FIDL 中,服务器可以向客户端发送称为“事件”的垃圾消息。使用事件的协议需要特别关注流控制,因为事件机制本身不提供任何流控制。
在频道的生命周期内最多发送一次事件实例就是一种很好的事件用例。在此模式中,协议不需要对事件进行任何流控制:
protocol DeathWish {
-> OnFatalError(struct {
error_code zx.Status;
});
};
另一个较好的事件用例是客户端请求服务器生成事件,以及服务器生成的事件总数是有界限的。此模式是挂起获取模式的更复杂的版本,在这种模式中,服务器可对“get”请求做出有限次数(而不只是一次)的响应:
protocol NetworkScanner {
ScanForNetworks();
-> OnNetworkDiscovered(struct {
network string;
});
-> OnScanFinished();
};
使用确认限制事件
如果提前没有已知事件数量限制,请考虑让客户端通过发送消息来确认事件。这种模式是确认模式的更尴尬版本,在这种模式中,客户端和服务器的角色会切换。与另一种模式一样,服务器应限制事件生成,以匹配客户端处理事件的速率:
protocol View1 {
-> OnInputEvent(struct {
event InputEvent;
});
NotifyInputEventHandled();
};
与普通确认模式相比,这种模式的一个优势是,客户端可以通过一条消息更轻松地确认多个事件,因为确认会与要确认的事件解除关联。此模式可以减少确认消息量,从而实现更高效的批处理,并且非常适合按顺序处理多个事件类型:
protocol View2 {
-> OnInputEvent(struct {
event InputEvent;
seq uint64;
});
-> OnFocusChangedEvent(struct {
event FocusChangedEvent;
seq uint64;
});
NotifyEventsHandled(struct {
last_seq uint64;
});
};
与使用确认的限制推送不同,此模式不会以 FIDL 语法表示请求和响应之间的关系,因此容易遭到滥用。仅当客户端正确实现通知消息的发送后,流控制才能起作用。
FIDL 方案:节流事件模式
事件是从服务器发起的 FIDL 调用。由于这些调用没有内置客户端响应,因此不受流控制:服务器可能会将大量此类调用加入队列,并对客户端进行泛化处理。此问题的一种解决方案是受限事件模式。这种模式涉及添加一个名为 FIDL 方法的客户端,作为要同步到的一个或多个事件的确认点。
服务器在收到客户端发出的下一个确认调用之前,应该避免发送更多受限事件(此处的确切语义特定于实现协议)。同样,如果服务器在客户端确认发送之前发送的受节流事件超出允许的数量,客户端应关闭连接。这些限制未内置于 FIDL 运行时中,并且需要客户端/服务器实现者进行一些手动实现,以确保行为正确。
若要提高 Instance
协议的性能,一种方法是允许批量处理行:我们可以将多行内容批量转换为对新 AddLines(...);
调用的单次调用,而不是在每次想要向画布中添加新行时发送一个 AddLine(...);
,等待回复,然后对下一行再次执行该操作。现在,客户端可以决定如何最好地分割要绘制的大量线条。
简单地实现时,我们发现服务器和客户端完全不同步:客户端可以使用无界限 AddLines(...);
调用对服务器进行泛洪攻击,而服务器同样可以使用超出其处理能力的 -> OnDrawn(...);
事件泛洪攻击客户端。这两个问题的解决方案是添加一个简单的 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. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{format_err, Context as _, Error}, config::Config, fidl_examples_canvas_clientrequesteddraw::{InstanceEvent, InstanceMarker, Point}, fuchsia_component::client::connect_to_protocol, futures::TryStreamExt, std::{thread, time}, }; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send Instance requests // across the channel. let instance = connect_to_protocol::<InstanceMarker>()?; println!("Outgoing connection enabled"); let mut batched_lines = Vec::<[Point; 2]>::new(); for action in config.script.into_iter() { // If the next action in the script is to "PUSH", send a batch of lines to the server. if action == "PUSH" { instance.add_lines(&batched_lines).context("Could not send lines")?; println!("AddLines request sent"); batched_lines.clear(); continue; } // If the next action in the script is to "WAIT", block until an OnDrawn event is received // from the server. if action == "WAIT" { let mut event_stream = instance.take_event_stream(); loop { match event_stream .try_next() .await .context("Error getting event response from proxy")? .ok_or_else(|| format_err!("Proxy sent no events"))? { InstanceEvent::OnDrawn { top_left, bottom_right } => { println!( "OnDrawn event received: top_left: {:?}, bottom_right: {:?}", top_left, bottom_right ); break; } InstanceEvent::_UnknownEvent { ordinal, .. } => { println!("Received an unknown event with ordinal {ordinal}"); } } } // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. println!("Ready request sent"); instance.ready().await.context("Could not send ready call")?; println!("Ready success"); continue; } // Add a line to the next batch. Parse the string input, making two points out of it. let mut points = action .split(":") .map(|point| { let integers = point .split(",") .map(|integer| integer.parse::<i64>().unwrap()) .collect::<Vec<i64>>(); Point { x: integers[0], y: integers[1] } }) .collect::<Vec<Point>>(); // Assemble a line from the two points. let from = points.pop().ok_or(format_err!("line requires 2 points, but has 0"))?; let to = points.pop().ok_or(format_err!("line requires 2 points, but has 1"))?; let mut line: [Point; 2] = [from, to]; // Batch a line for drawing to the canvas using the two points provided. println!("AddLines batching line: {:?}", &mut line); batched_lines.push(line); } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{anyhow, Context as _, Error}, fidl::endpoints::RequestStream as _, fidl_examples_canvas_clientrequesteddraw::{ BoundingBox, InstanceRequest, InstanceRequestStream, Point, }, fuchsia_async::{Time, Timer}, fuchsia_component::server::ServiceFs, fuchsia_zircon::{self as zx}, futures::future::join, futures::prelude::*, std::sync::{Arc, Mutex}, }; // A struct that stores the two things we care about for this example: the bounding box the lines // that have been added thus far, and bit to track whether or not there have been changes since the // last `OnDrawn` event. #[derive(Debug)] struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. changed: bool, // Tracks whether or not the client has declared itself ready to receive more updated. ready: bool, bounding_box: BoundingBox, } /// Handler for the `AddLines` method. fn add_lines(state: &mut CanvasState, lines: Vec<[Point; 2]>) { // Update the bounding box to account for the new lines we've just "added" to the canvas. let bounds = &mut state.bounding_box; for line in lines { println!("AddLines printing line: {:?}", line); for point in line { if point.x < bounds.top_left.x { bounds.top_left.x = point.x; } if point.y > bounds.top_left.y { bounds.top_left.y = point.y; } if point.x > bounds.bottom_right.x { bounds.bottom_right.x = point.x; } if point.y < bounds.bottom_right.y { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next tick. state.changed = true } /// Creates a new instance of the server, paired to a single client across a zircon channel. async fn run_server(stream: InstanceRequestStream) -> Result<(), Error> { // Create a new in-memory state store for the state of the canvas. The store will live for the // lifetime of the connection between the server and this particular client. let state = Arc::new(Mutex::new(CanvasState { changed: true, ready: true, bounding_box: BoundingBox { top_left: Point { x: 0, y: 0 }, bottom_right: Point { x: 0, y: 0 }, }, })); // Take ownership of the control_handle from the stream, which will allow us to push events from // a different async task. let control_handle = stream.control_handle(); // A separate watcher task periodically "draws" the canvas, and notifies the client of the new // state. We'll need a cloned reference to the canvas state to be accessible from the new // task. let state_ref = state.clone(); let update_sender = || async move { loop { // Our server sends one update per second, but only if the client has declared that it // is ready to receive one. Timer::new(Time::after(zx::Duration::from_seconds(1))).await; let mut state = state_ref.lock().unwrap(); if !state.changed || !state.ready { continue; } // After acquiring the lock, this is where we would draw the actual lines. Since this is // just an example, we'll avoid doing the actual rendering, and simply send the bounding // box to the client instead. let bounds = state.bounding_box; match control_handle.send_on_drawn(&bounds.top_left, &bounds.bottom_right) { Ok(_) => println!( "OnDrawn event sent: top_left: {:?}, bottom_right: {:?}", bounds.top_left, bounds.bottom_right ), Err(_) => return, } // Reset the change and ready trackers. state.ready = false; state.changed = false; } }; // Handle requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. let state_ref = &state; let request_handler = stream.map(|result| result.context("failed request")).try_for_each(|request| async move { // Match based on the method being invoked. match request { InstanceRequest::AddLines { lines, .. } => { println!("AddLines request received"); add_lines(&mut state_ref.lock().unwrap(), lines); } InstanceRequest::Ready { responder, .. } => { println!("Ready request received"); // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` // event; if two "consecutive" `Ready() -> ();` calls are received, this // interaction has entered an invalid state, and should be aborted immediately. let mut state = state_ref.lock().unwrap(); if state.ready == true { return Err(anyhow!("Invalid back-to-back `Ready` requests received")); } state.ready = true; responder.send().context("Error responding")?; } // InstanceRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }); // This line will only be reached if the server errors out. The stream will await indefinitely, // thereby creating a long-lived server. Here, we first wait for the updater task to realize the // connection has died, then bubble up the error. join(request_handler, update_sender()).await.0 } // A helper enum that allows us to treat a `Instance` service instance as a value. enum IncomingService { Instance(InstanceRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Instance` protocol - this will allow the client to see // the server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Instance); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Instance(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_natural/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::AsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::Event<examples_canvas_clientrequesteddraw::Instance::OnDrawn>& event) override { ::examples_canvas_clientrequesteddraw::Point top_left = event.top_left(); ::examples_canvas_clientrequesteddraw::Point bottom_right = event.bottom_right(); FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::Point(x, y); } using Line = ::std::array<::examples_canvas_clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::Client client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fit::result<fidl::Error> result = client->AddLines(batched_lines); if (!result.is_ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << result.error_value(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::Result<examples_canvas_clientrequesteddraw::Instance::Ready> result) { // Check if the FIDL call succeeded or not. if (result.is_ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error_value(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x() << ", y: " << line[1].y() << " }, Point { x: " << line[0].x() << ", y: " << line[0].y() << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/fidl.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::Server<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequest& request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request.lines()) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x() << ", y: " << points[1].y() << " }, Point { x: " << points[0].x() << ", y: " << points[0].y() << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x() < bounds.top_left().x()) { bounds.top_left().x() = point.x(); } if (point.y() > bounds.top_left().y()) { bounds.top_left().y() = point.y(); } if (point.x() > bounds.bottom_right().x()) { bounds.bottom_right().x() = point.x(); } if (point.y() < bounds.bottom_right().y()) { bounds.bottom_right().y() = point.y(); } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto result = fidl::SendEvent(binding_)->OnDrawn(state_.bounding_box); if (!result.is_ok()) { return; } auto top_left = state_.bounding_box.top_left(); auto bottom_right = state_.bounding_box.bottom_right(); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x() << ", y: " << top_left.y() << " }, bottom_right: Point { x: " << bottom_right.x() << ", y: " << bottom_right.y() << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
C++ (Wire)
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/component/incoming/cpp/protocol.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/fidl/new/canvas/client_requested_draw/cpp_wire/client/config.h> // The |EventHandler| is a derived class that we pass into the |fidl::WireClient| to handle incoming // events asynchronously. class EventHandler : public fidl::WireAsyncEventHandler<examples_canvas_clientrequesteddraw::Instance> { public: // Handler for |OnDrawn| events sent from the server. void OnDrawn( fidl::WireEvent<examples_canvas_clientrequesteddraw::Instance::OnDrawn>* event) override { ::examples_canvas_clientrequesteddraw::wire::Point top_left = event->top_left; ::examples_canvas_clientrequesteddraw::wire::Point bottom_right = event->bottom_right; FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop_.Quit(); } void on_fidl_error(fidl::UnbindInfo error) override { FX_LOGS(ERROR) << error; } void handle_unknown_event( fidl::UnknownEventMetadata<examples_canvas_clientrequesteddraw::Instance> metadata) override { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << metadata.event_ordinal; } explicit EventHandler(async::Loop& loop) : loop_(loop) {} private: async::Loop& loop_; }; // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples_canvas_clientrequesteddraw::wire::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples_canvas_clientrequesteddraw::wire::Point{.x = x, .y = y}; } using Line = ::fidl::Array<::examples_canvas_clientrequesteddraw::wire::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop and dispatcher. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace. This can fail so it's wrapped in a // |zx::result| and it must be checked for errors. zx::result client_end = component::Connect<examples_canvas_clientrequesteddraw::Instance>(); if (!client_end.is_ok()) { FX_LOGS(ERROR) << "Synchronous error when connecting to the |Instance| protocol: " << client_end.status_string(); return -1; } // Create an instance of the event handler. EventHandler event_handler(loop); // Create an asynchronous client using the newly-established connection. fidl::WireClient client(std::move(*client_end), dispatcher, &event_handler); FX_LOGS(INFO) << "Outgoing connection enabled"; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { fidl::Status status = client->AddLines(fidl::VectorView<Line>::FromExternal(batched_lines)); if (!status.ok()) { // Check that our one-way call was enqueued successfully, and handle the error // appropriately. In the case of this example, there is nothing we can do to recover here, // except to log an error and exit the program. FX_LOGS(ERROR) << "Could not send AddLines request: " << status.error(); return -1; } batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are // ready for us. FX_LOGS(INFO) << "Ready request sent"; client->Ready().ThenExactlyOnce( [&](fidl::WireUnownedResult<examples_canvas_clientrequesteddraw::Instance::Ready>& result) { // Check if the FIDL call succeeded or not. if (result.ok()) { FX_LOGS(INFO) << "Ready success"; } else { FX_LOGS(ERROR) << "Could not send Ready request: " << result.error(); } // Quit the loop, thereby handing control back to the outer loop of actions being // iterated over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <fidl/examples.canvas.clientrequesteddraw/cpp/wire.h> #include <lib/async-loop/cpp/loop.h> #include <lib/async/cpp/task.h> #include <lib/component/outgoing/cpp/outgoing_directory.h> #include <lib/fidl/cpp/wire/channel.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples_canvas_clientrequesteddraw::wire::BoundingBox bounding_box; }; // An implementation of the |Instance| protocol. class InstanceImpl final : public fidl::WireServer<examples_canvas_clientrequesteddraw::Instance> { public: // Bind this implementation to a channel. InstanceImpl(async_dispatcher_t* dispatcher, fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) : binding_(dispatcher, std::move(server_end), this, std::mem_fn(&InstanceImpl::OnFidlClosed)), weak_factory_(this) { // Start the update timer on startup. Our server sends one update per second ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void OnFidlClosed(fidl::UnbindInfo info) { if (info.reason() != ::fidl::Reason::kPeerClosedWhileReading) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; } void AddLines(AddLinesRequestView request, AddLinesCompleter::Sync& completer) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : request->lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next |OnDrawn| // event. state_.changed = true; } void Ready(ReadyCompleter::Sync& completer) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if two // "consecutive" `Ready() -> ();` calls are received, this interaction has entered an invalid // state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; completer.Reply(); } void handle_unknown_method( fidl::UnknownMethodMetadata<examples_canvas_clientrequesteddraw::Instance> metadata, fidl::UnknownMethodCompleter::Sync& completer) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << metadata.method_ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something has // changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client has // not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = weak->state_.bounding_box.top_left; auto bottom_right = weak->state_.bounding_box.bottom_right; fidl::Status status = fidl::WireSendEvent(weak->binding_)->OnDrawn(top_left, bottom_right); if (!status.ok()) { return; } FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; weak->state_.changed = false; }, after); } fidl::ServerBinding<examples_canvas_clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from the // client. The following initializes the loop, and obtains the dispatcher, which will be used when // binding the server implementation to a channel. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. This // directory is where the outgoing FIDL protocols are installed so that they can be provided to // other components. component::OutgoingDirectory outgoing = component::OutgoingDirectory(dispatcher); // The `ServeFromStartupInfo()` function sets up the outgoing directory with the startup handle. // The startup handle is a handle provided to every component by the system, so that they can // serve capabilities (e.g. FIDL protocols) to other components. zx::result result = outgoing.ServeFromStartupInfo(); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to serve outgoing directory: " << result.status_string(); return -1; } // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. result = outgoing.AddUnmanagedProtocol<examples_canvas_clientrequesteddraw::Instance>( [dispatcher](fidl::ServerEnd<examples_canvas_clientrequesteddraw::Instance> server_end) { // Create an instance of our InstanceImpl that destroys itself when the connection closes. new InstanceImpl(dispatcher, std::move(server_end)); }); if (result.is_error()) { FX_LOGS(ERROR) << "Failed to add Instance protocol: " << result.status_string(); return -1; } // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
HLCPP
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <charconv> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <examples/fidl/new/canvas/client_requested_draw/hlcpp/client/config.h> // A helper function that takes a coordinate in string form, like "123,-456", and parses it into a // a struct of the form |{ in64 x; int64 y; }|. ::examples::canvas::clientrequesteddraw::Point ParsePoint(std::string_view input) { int64_t x = 0; int64_t y = 0; size_t index = input.find(','); if (index != std::string::npos) { std::from_chars(input.data(), input.data() + index, x); std::from_chars(input.data() + index + 1, input.data() + input.length(), y); } return ::examples::canvas::clientrequesteddraw::Point{.x = x, .y = y}; } using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // A helper function that takes a coordinate pair in string form, like "1,2:-3,-4", and parses it // into an array of 2 |Point| structs. Line ParseLine(const std::string& action) { auto input = std::string_view(action); size_t index = input.find(':'); if (index != std::string::npos) { return {ParsePoint(input.substr(0, index)), ParsePoint(input.substr(index + 1))}; } return {}; } int main(int argc, const char** argv) { FX_LOGS(INFO) << "Started"; // Retrieve component configuration. auto conf = config::Config::TakeFromStartupHandle(); // Start up an async loop. async::Loop loop(&kAsyncLoopConfigNeverAttachToThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Connect to the protocol inside the component's namespace, then create an asynchronous client // using the newly-established connection. examples::canvas::clientrequesteddraw::InstancePtr instance_proxy; auto context = sys::ComponentContext::Create(); context->svc()->Connect(instance_proxy.NewRequest(dispatcher)); FX_LOGS(INFO) << "Outgoing connection enabled"; instance_proxy.set_error_handler([&loop](zx_status_t status) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; loop.Quit(); }); // Provide a lambda to handle incoming |OnDrawn| events asynchronously. instance_proxy.events().OnDrawn = [&loop](::examples::canvas::clientrequesteddraw::Point top_left, ::examples::canvas::clientrequesteddraw::Point bottom_right) { FX_LOGS(INFO) << "OnDrawn event received: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; loop.Quit(); }; instance_proxy.events().handle_unknown_event = [](uint64_t ordinal) { FX_LOGS(WARNING) << "Received an unknown event with ordinal " << ordinal; }; std::vector<Line> batched_lines; for (const auto& action : conf.script()) { // If the next action in the script is to "PUSH", send a batch of lines to the server. if (action == "PUSH") { instance_proxy->AddLines(batched_lines); batched_lines.clear(); FX_LOGS(INFO) << "AddLines request sent"; continue; } // If the next action in the script is to "WAIT", block until an |OnDrawn| event is received // from the server. if (action == "WAIT") { loop.Run(); loop.ResetQuit(); // Now, inform the server that we are ready to receive more updates whenever they are ready // for us. FX_LOGS(INFO) << "Ready request sent"; instance_proxy->Ready([&](fpromise::result<void, fidl::FrameworkErr> result) { if (result.is_error()) { // Check that our flexible two-way call was known to the server and handle the case of an // unknown method appropriately. In the case of this example, there is nothing we can do // to recover here, except to log an error and exit the program. FX_LOGS(ERROR) << "Server does not implement AddLine"; } FX_LOGS(INFO) << "Ready success"; // Quit the loop, thereby handing control back to the outer loop of actions being iterated // over. loop.Quit(); }); // Run the loop until the callback is resolved, at which point we can continue from here. loop.Run(); loop.ResetQuit(); continue; } // Batch a line for drawing to the canvas using the two points provided. Line line = ParseLine(action); batched_lines.push_back(line); FX_LOGS(INFO) << "AddLines batching line: [Point { x: " << line[1].x << ", y: " << line[1].y << " }, Point { x: " << line[0].x << ", y: " << line[0].y << " }]"; } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. sleep(2); return 0; }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. #include <lib/async-loop/cpp/loop.h> #include <lib/async-loop/default.h> #include <lib/async/cpp/task.h> #include <lib/fidl/cpp/binding.h> #include <lib/sys/cpp/component_context.h> #include <lib/syslog/cpp/macros.h> #include <unistd.h> #include <examples/canvas/clientrequesteddraw/cpp/fidl.h> #include <src/lib/fxl/macros.h> #include <src/lib/fxl/memory/weak_ptr.h> // A struct that stores the two things we care about for this example: the set of lines, and the // bounding box that contains them. struct CanvasState { // Tracks whether there has been a change since the last send, to prevent redundant updates. bool changed = true; // Tracks whether or not the client has declared itself ready to receive more updated. bool ready = true; examples::canvas::clientrequesteddraw::BoundingBox bounding_box; }; using Line = ::std::array<::examples::canvas::clientrequesteddraw::Point, 2>; // An implementation of the |Instance| protocol. class InstanceImpl final : public examples::canvas::clientrequesteddraw::Instance { public: // Bind this implementation to an |InterfaceRequest|. InstanceImpl(async_dispatcher_t* dispatcher, fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) : binding_(fidl::Binding<examples::canvas::clientrequesteddraw::Instance>(this)), weak_factory_(this) { binding_.Bind(std::move(request), dispatcher); // Gracefully handle abrupt shutdowns. binding_.set_error_handler([this](zx_status_t status) mutable { if (status != ZX_ERR_PEER_CLOSED) { FX_LOGS(ERROR) << "Shutdown unexpectedly"; } delete this; }); // Start the update timer on startup. Our server sends one update per second. ScheduleOnDrawnEvent(dispatcher, zx::sec(1)); } void AddLines(std::vector<Line> lines) override { FX_LOGS(INFO) << "AddLines request received"; for (const auto& points : lines) { FX_LOGS(INFO) << "AddLines printing line: [Point { x: " << points[1].x << ", y: " << points[1].y << " }, Point { x: " << points[0].x << ", y: " << points[0].y << " }]"; // Update the bounding box to account for the new line we've just "added" to the canvas. auto& bounds = state_.bounding_box; for (const auto& point : points) { if (point.x < bounds.top_left.x) { bounds.top_left.x = point.x; } if (point.y > bounds.top_left.y) { bounds.top_left.y = point.y; } if (point.x > bounds.bottom_right.x) { bounds.bottom_right.x = point.x; } if (point.y < bounds.bottom_right.y) { bounds.bottom_right.y = point.y; } } } // Mark the state as "dirty", so that an update is sent back to the client on the next // |OnDrawn| event. state_.changed = true; } void Ready(ReadyCallback callback) override { FX_LOGS(INFO) << "Ready request received"; // The client must only call `Ready() -> ();` after receiving an `-> OnDrawn();` event; if // two "consecutive" `Ready() -> ();` calls are received, this interaction has entered an // invalid state, and should be aborted immediately. if (state_.ready == true) { FX_LOGS(ERROR) << "Invalid back-to-back `Ready` requests received"; } state_.ready = true; callback(fpromise::ok()); } void handle_unknown_method(uint64_t ordinal, bool method_has_response) override { FX_LOGS(WARNING) << "Received an unknown method with ordinal " << ordinal; } private: // Each scheduled update waits for the allotted amount of time, sends an update if something // has changed, and schedules the next update. void ScheduleOnDrawnEvent(async_dispatcher_t* dispatcher, zx::duration after) { async::PostDelayedTask( dispatcher, [&, dispatcher, after, weak = weak_factory_.GetWeakPtr()] { // Halt execution if the binding has been deallocated already. if (!weak) { return; } // Schedule the next update if the binding still exists. weak->ScheduleOnDrawnEvent(dispatcher, after); // No need to send an update if nothing has changed since the last one, or the client // has not yet informed us that it is ready for more updates. if (!weak->state_.changed || !weak->state_.ready) { return; } // This is where we would draw the actual lines. Since this is just an example, we'll // avoid doing the actual rendering, and simply send the bounding box to the client // instead. auto top_left = state_.bounding_box.top_left; auto bottom_right = state_.bounding_box.bottom_right; binding_.events().OnDrawn(top_left, bottom_right); FX_LOGS(INFO) << "OnDrawn event sent: top_left: Point { x: " << top_left.x << ", y: " << top_left.y << " }, bottom_right: Point { x: " << bottom_right.x << ", y: " << bottom_right.y << " }"; // Reset the change and ready trackers. state_.ready = false; state_.changed = false; }, after); } fidl::Binding<examples::canvas::clientrequesteddraw::Instance> binding_; CanvasState state_ = CanvasState{}; // Generates weak references to this object, which are appropriate to pass into asynchronous // callbacks that need to access this object. The references are automatically invalidated // if this object is destroyed. fxl::WeakPtrFactory<InstanceImpl> weak_factory_; }; int main(int argc, char** argv) { FX_LOGS(INFO) << "Started"; // The event loop is used to asynchronously listen for incoming connections and requests from // the client. The following initializes the loop, and obtains the dispatcher, which will be // used when binding the server implementation to a channel. // // Note that unlike the new C++ bindings, HLCPP bindings rely on the async loop being attached // to the current thread via the |kAsyncLoopConfigAttachToCurrentThread| configuration. async::Loop loop(&kAsyncLoopConfigAttachToCurrentThread); async_dispatcher_t* dispatcher = loop.dispatcher(); // Create an |OutgoingDirectory| instance. // // The |component::OutgoingDirectory| class serves the outgoing directory for our component. // This directory is where the outgoing FIDL protocols are installed so that they can be // provided to other components. auto context = sys::ComponentContext::CreateAndServeOutgoingDirectory(); // Register a handler for components trying to connect to // |examples.canvas.clientrequesteddraw.Instance|. context->outgoing()->AddPublicService( fidl::InterfaceRequestHandler<examples::canvas::clientrequesteddraw::Instance>( [dispatcher]( fidl::InterfaceRequest<examples::canvas::clientrequesteddraw::Instance> request) { // Create an instance of our |InstanceImpl| that destroys itself when the connection // closes. new InstanceImpl(dispatcher, std::move(request)); })); // Everything is wired up. Sit back and run the loop until an incoming connection wakes us up. FX_LOGS(INFO) << "Listening for incoming connections"; loop.Run(); return 0; }
前馈数据流
某些协议具有前馈数据流,它使数据流主要是单向流动(通常是从客户端到服务器),从而避免往返延迟。该协议仅在必要时同步这两个端点。此外,前馈数据流还可以提高吞吐量,因为执行给定任务所需的上下文切换总数更少。
使用前馈数据流的关键是,让客户端在发送后续消息之前无需等待之前的方法调用的结果。例如,协议请求管道无需客户端等待服务器使用协议进行回复,然后客户端才能使用该协议。同样,客户端分配的标识符(见下文)无需客户端等待服务器为服务器保留的状态分配标识符。
通常,前馈协议会涉及客户端提交一系列单向方法调用,而不等待服务器响应。提交这些消息后,客户端会通过调用 Commit
或 Flush
等有回复的方法,与服务器明确同步。回复可能是空消息,也可能包含有关提交的序列是否成功的信息。在更复杂的协议中,单向消息表示为命令对象的并集,而不是单个方法调用;请参阅下面的命令并集模式。
使用前馈数据流的协议非常适合乐观错误处理策略。不要让服务器使用状态值来回复每个方法(这会鼓励客户端等待每条消息之间的往返),而应仅在方法因不受客户端控制的原因导致失败时添加状态回复。如果客户端发送一条消息,指出客户端本应知道该消息是无效的(例如,引用了无效的客户端分配的标识符),请通过关闭连接来表明错误。如果客户端发送的消息可能不知道是无效的,您可以提供一个表明成功或失败的响应(需要客户端进行同步),或者记住错误并忽略后续的依赖请求,直到客户端以某种方式同步并从错误中恢复为止。
示例:
protocol Canvas {
Flush() -> (struct {
code zx.Status;
});
Clear();
UploadImage(struct {
image_id uint32;
image Image;
});
PaintImage(struct {
image_id uint32;
x float32;
y float32;
});
DiscardImage(struct {
image_id uint32;
});
PaintSmileyFace(struct {
x float32;
y float32;
});
PaintMoustache(struct {
x float32;
y float32;
});
};
FIDL 方案:大小限制
FIDL 矢量和字符串可以带有大小限制,用于指定类型可以包含的成员数量限制。对于矢量而言,此值是指存储在矢量中的元素数量;对于字符串,则是指字符串包含的字节数。
强烈建议使用大小限制,因为它会为本来就属于无界大型类型设置上限。
对键值对存储来说,一个有用的操作是有序迭代:也就是说,在给定键时,可按顺序返回其后出现的元素列表(通常为分页)。
推理
在 FIDL 中,最好使用迭代器来实现此目的,该迭代器通常作为可以发生此迭代的单独协议来实现。使用单独的协议(因此使用单独的通道)有诸多好处,包括可将迭代拉取请求与通过主协议执行的其他操作进行解交错。
协议 P
的通道连接的客户端和服务器端可以分别表示为 FIDL 数据类型,分别表示为 client_end:P
和 server_end:P
。这些类型统称为协议端,表示将 FIDL 客户端连接到其相应服务器的另一种(非 @discoverable
)方式:通过现有 FIDL 连接!
协议结束是一般 FIDL 概念(资源类型)的特定实例。资源类型应包含 FIDL 句柄,这需要对类型的使用方式进行额外的限制。该类型必须始终唯一,因为底层资源由其他功能管理器(通常是 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. { protocol: [ "fuchsia.inspect.InspectSink", "fuchsia.logger.LogSink", ], from: "parent", to: [ "#client", "#server", ], }, ], }
然后,可以使用任何支持的语言编写客户端和服务器实现:
Rust
客户端
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{Context as _, Error}, config::Config, fuchsia_component::client::connect_to_protocol, std::{thread, time}, }; use { fidl::endpoints::create_proxy, fidl_examples_keyvaluestore_additerator::{Item, IteratorMarker, StoreMarker}, futures::join, }; #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Load the structured config values passed to this component at startup. let config = Config::take_from_startup_handle(); // Use the Component Framework runtime to connect to the newly spun up server component. We wrap // our retained client end in a proxy object that lets us asynchronously send `Store` requests // across the channel. let store = connect_to_protocol::<StoreMarker>()?; println!("Outgoing connection enabled"); // This client's structured config has one parameter, a vector of strings. Each string is the // path to a resource file whose filename is a key and whose contents are a value. We iterate // over them and try to write each key-value pair to the remote store. for key in config.write_items.into_iter() { let path = format!("/pkg/data/{}.txt", key); let value = std::fs::read_to_string(path.clone()) .with_context(|| format!("Failed to load {path}"))?; match store.write_item(&Item { key: key, value: value.into_bytes() }).await? { Ok(_) => println!("WriteItem Success"), Err(err) => println!("WriteItem Error: {}", err.into_primitive()), } } if !config.iterate_from.is_empty() { // This helper creates a channel, and returns two protocol ends: the `client_end` is already // conveniently bound to the correct FIDL protocol, `Iterator`, while the `server_end` is // unbound and ready to be sent over the wire. let (iterator, server_end) = create_proxy::<IteratorMarker>()?; // There is no need to wait for the iterator to connect before sending the first `Get()` // request - since we already hold the `client_end` of the connection, we can start queuing // requests on it immediately. let connect_to_iterator = store.iterate(Some(config.iterate_from.as_str()), server_end); let first_get = iterator.get(); // Wait until both the connection and the first request resolve - an error in either case // triggers an immediate resolution of the combined future. let (connection, first_page) = join!(connect_to_iterator, first_get); // Handle any connection error. If this has occurred, it is impossible for the first `Get()` // call to have resolved successfully, so check this error first. if let Err(err) = connection.context("Could not connect to Iterator")? { println!("Iterator Connection Error: {}", err.into_primitive()); } else { println!("Iterator Connection Success"); // Consecutively repeat the `Get()` request if the previous response was not empty. let mut entries = first_page.context("Could not get page from Iterator")?; while !&entries.is_empty() { for entry in entries.iter() { println!("Iterator Entry: {}", entry); } entries = iterator.get().await.context("Could not get page from Iterator")?; } } } // TODO(https://fxbug.dev/42156498): We need to sleep here to make sure all logs get drained. Once the // referenced bug has been resolved, we can remove the sleep. thread::sleep(time::Duration::from_secs(2)); Ok(()) }
服务器
// Copyright 2022 The Fuchsia Authors. All rights reserved. // Use of this source code is governed by a BSD-style license that can be // found in the LICENSE file. use { anyhow::{Context as _, Error}, fuchsia_component::server::ServiceFs, futures::prelude::*, lazy_static::lazy_static, regex::Regex, }; use { fidl_examples_keyvaluestore_additerator::{ Item, IterateConnectionError, IteratorRequest, IteratorRequestStream, StoreRequest, StoreRequestStream, WriteError, }, fuchsia_async as fasync, std::collections::btree_map::Entry, std::collections::BTreeMap, std::ops::Bound::*, std::sync::Arc, std::sync::Mutex, }; lazy_static! { static ref KEY_VALIDATION_REGEX: Regex = Regex::new(r"^[A-Za-z]\w+[A-Za-z0-9]$").expect("Key validation regex failed to compile"); } /// Handler for the `WriteItem` method. fn write_item(store: &mut BTreeMap<String, Vec<u8>>, attempt: Item) -> Result<(), WriteError> { // Validate the key. if !KEY_VALIDATION_REGEX.is_match(attempt.key.as_str()) { println!("Write error: INVALID_KEY, For key: {}", attempt.key); return Err(WriteError::InvalidKey); } // Validate the value. if attempt.value.is_empty() { println!("Write error: INVALID_VALUE, For key: {}", attempt.key); return Err(WriteError::InvalidValue); } // Write to the store, validating that the key did not already exist. match store.entry(attempt.key) { Entry::Occupied(entry) => { println!("Write error: ALREADY_EXISTS, For key: {}", entry.key()); Err(WriteError::AlreadyExists) } Entry::Vacant(entry) => { println!("Wrote value at key: {}", entry.key()); entry.insert(attempt.value); Ok(()) } } } /// Handler for the `Iterate` method, which deals with validating that the requested start position /// exists, and then sets up the asynchronous side channel for the actual iteration to occur over. fn iterate( store: Arc<Mutex<BTreeMap<String, Vec<u8>>>>, starting_at: Option<String>, stream: IteratorRequestStream, ) -> Result<(), IterateConnectionError> { // Validate that the starting key, if supplied, actually exists. if let Some(start_key) = starting_at.clone() { if !store.lock().unwrap().contains_key(&start_key) { return Err(IterateConnectionError::UnknownStartAt); } } // Spawn a detached task. This allows the method call to return while the iteration continues in // a separate, unawaited task. fasync::Task::spawn(async move { // Serve the iteration requests. Note that access to the underlying store is behind a // contended `Mutex`, meaning that the iteration is not atomic: page contents could shift, // change, or disappear entirely between `Get()` requests. stream .map(|result| result.context("failed request")) .try_fold( match starting_at { Some(start_key) => Included(start_key), None => Unbounded, }, |mut lower_bound, request| async { match request { IteratorRequest::Get { responder } => { println!("Iterator page request received"); // The `page_size` should be kept in sync with the size constraint on // the iterator's response, as defined in the FIDL protocol. static PAGE_SIZE: usize = 10; // An iterator, beginning at `lower_bound` and tracking the pagination's // progress through iteration as each page is pulled by a client-sent // `Get()` request. let held_store = store.lock().unwrap(); let mut entries = held_store.range((lower_bound.clone(), Unbounded)); let mut current_page = vec![]; for _ in 0..PAGE_SIZE { match entries.next() { Some(entry) => { current_page.push(entry.0.clone()); } None => break, } } // Update the `lower_bound` - either inclusive of the next item in the // iteration, or exclusive of the last seen item if the iteration has // finished. This `lower_bound` will be passed to the next request // handler as its starting point. lower_bound = match entries.next() { Some(next) => Included(next.0.clone()), None => match current_page.last() { Some(tail) => Excluded(tail.clone()), None => lower_bound, }, }; // Send the page. At the end of this scope, the `held_store` lock gets // dropped, and therefore released. responder.send(¤t_page).context("error sending reply")?; println!("Iterator page sent"); } } Ok(lower_bound) }, ) .await .ok(); }) .detach(); Ok(()) } /// Creates a new instance of the server. Each server has its own bespoke, per-connection instance /// of the key-value store. async fn run_server(stream: StoreRequestStream) -> Result<(), Error> { // Create a new in-memory key-value store. The store will live for the lifetime of the // connection between the server and this particular client. // // Note that we now use an `Arc<Mutex<BTreeMap>>`, replacing the previous `RefCell<HashMap>`. // The `BTreeMap` is used because we want an ordered map, to better facilitate iteration. The // `Arc<Mutex<...>>` is used because there are now multiple async tasks accessing the: one main // task which handles communication over the protocol, and one additional task per iterator // protocol. `Arc<Mutex<...>>` is the simplest way to synchronize concurrent access between // these racing tasks. let store = &Arc::new(Mutex::new(BTreeMap::<String, Vec<u8>>::new())); // Serve all requests on the protocol sequentially - a new request is not handled until its // predecessor has been processed. stream .map(|result| result.context("failed request")) .try_for_each(|request| async { // Match based on the method being invoked. match request { StoreRequest::WriteItem { attempt, responder } => { println!("WriteItem request received"); // The `responder` parameter is a special struct that manages the outgoing reply // to this method call. Calling `send` on the responder exactly once will send // the reply. responder .send(write_item(&mut store.clone().lock().unwrap(), attempt)) .context("error sending reply")?; println!("WriteItem response sent"); } StoreRequest::Iterate { starting_at, iterator, responder } => { println!("Iterate request received"); // The `iterate` handler does a quick check to see that the request is valid, // then spins up a separate worker task to serve the newly minted `Iterator` // protocol instance, allowing this call to return immediately and continue the // request stream with other work. responder .send(iterate(store.clone(), starting_at, iterator.into_stream()?)) .context("error sending reply")?; println!("Iterate response sent"); } // StoreRequest::_UnknownMethod { ordinal, .. } => { println!("Received an unknown method with ordinal {ordinal}"); } } Ok(()) }) .await } // A helper enum that allows us to treat a `Store` service instance as a value. enum IncomingService { Store(StoreRequestStream), } #[fuchsia::main] async fn main() -> Result<(), Error> { println!("Started"); // Add a discoverable instance of our `Store` protocol - this will allow the client to see the // server and connect to it. let mut fs = ServiceFs::new_local(); fs.dir("svc").add_fidl_service(IncomingService::Store); fs.take_and_serve_directory_handle()?; println!("Listening for incoming connections"); // The maximum number of concurrent clients that may be served by this process. const MAX_CONCURRENT: usize = 10; // Serve each connection simultaneously, up to the `MAX_CONCURRENT` limit. fs.for_each_concurrent(MAX_CONCURRENT, |IncomingService::Store(stream)| { run_server(stream).unwrap_or_else(|e| println!("{:?}", e)) }) .await; Ok(()) }
C++(自然)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Natural) implementation.
C++ (Wire)
客户端
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
服务器
// TODO(https://fxbug.dev/42060656): C++ (Wire) implementation.
HLCPP
客户端
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
服务器
// TODO(https://fxbug.dev/42060656): HLCPP implementation.
从设计上保障用户隐私
使用某个协议的客户端和服务器经常需要访问不同的敏感数据集。隐私或安全问题可能是由于意外泄露了超过协议必要数据而造成的。
在设计协议时,请特别注意协议中符合以下条件的字段:
- 包含个人身份信息,例如姓名、电子邮件地址或付款信息。
- 由用户提供,因此可能包含个人信息。示例包括设备名称和注释字段。
- 用作可以在供应商、用户、设备或重置之间建立关联的唯一标识符。例如序列号、MAC 地址、IP 地址和全局帐号 ID。
这些类型的字段会经过全面审核,包含它们的协议的可用性可能会受到限制。请确保您的协议包含的信息量不超过所需信息。
如果某个 API 用例需要个人数据或可链接的数据,而其他用例不需要,请考虑使用两个不同的协议,以便分别控制对更敏感的用例的访问。
我们通过两个假设的示例来说明 API 设计选择导致的侵犯隐私权的行为:
示例 1 - 外围设备控制 API 中的序列号
假设有一个外设控制 API,其中包含 USB 外设的序列号。序列号不包含个人数据,但它是一个非常稳定且易于关联的标识符。在此 API 中包含序列号会引起许多隐私问题:
- 有权访问该 API 的任何客户端都可以使用同一 Fuchsia 设备关联不同帐号。
- 任何有权访问 API 的客户都可以将同一帐号内的不同角色关联起来。
- 不同的软件供应商可能会相互协作,以了解不同的软件供应商使用的是它们还是同一设备。
- 如果在设备之间移动外围设备,则任何有权访问 API 的客户端都可以在设备组和共用该外围设备的用户之间建立关联。
- 如果销售外围设备,则有权访问该 API 的客户端可以将外围设备的新旧所有者相关联。
- 一些制造商会在其序列号中对信息进行编码。这样,有权访问该 API 的客户端就可以推断用户购买外围设备的位置或时间。
在此示例中,序列号的目的是允许客户端检测何时重新连接了同一 USB 外设。满足此意图需要稳定的标识符,但不需要全局标识符。不同的客户端不需要接收相同的标识符,同一客户端不需要在不同 Fuchsia 设备上接收相同的标识符,并且标识符也不需要在恢复出厂设置事件之间保持不变。
在此示例中,一种很好的替代方案是发送一个标识符,该标识符只能保证对单设备上的单个客户端保持稳定。此标识符可能是外围设备序列号的哈希值、Fuchsia 设备标识符以及连接名称。
示例 2 - Device Setup API 中的设备名称
假设有一个设备设置 API,其中包含用于协助设置设备设置的手机型号。在大多数情况下,手机的型号字符串由 OEM 设置,但有些手机报告的是用户提供的设备名称作为其型号。这会导致许多模型字符串包含用户的真实姓名或假名。因此,此 API 存在跨身份或跨设备关联用户的风险。罕见或预发布模型字符串也可能会泄露敏感信息,即使用户未提供该字符串也是如此。
在某些情况下,可以使用模型字符串,但要限制哪些客户端可以访问 API。或者,该 API 可以使用从不由用户控制的字段,例如制造商字符串。另一种替代方案是清理型号字符串,方法是将其与热门手机型号许可名单进行比较,并使用通用字符串替换罕见的型号字符串。
客户端分配的标识符
通常,协议可让客户端操控服务器保留的多个状态。在设计对象系统时,解决此问题的典型方法是为服务器保留的每个连贯状态创建单独的对象。但是,在设计协议时,为每个状态部分使用单独的对象会带来一些弊端。
为每个逻辑对象创建单独的协议实例会消耗内核资源,因为每个实例都需要单独的通道对象。每个实例都维护着一个单独的 FIFO 消息队列。为每个逻辑对象使用单独的实例意味着发送到不同对象的消息可以相对于彼此进行重新排序,从而导致客户端和服务器之间无序交互。
客户端分配的标识符模式可以通过让客户端为服务器保留的对象分配 uint32
或 uint64
标识符来避免这些问题。客户端和服务器之间交换的所有消息都通过单个协议实例传输,这为整个互动提供了一致的 FIFO 排序方式。
让客户端(而不是服务器)分配标识符可以实现前馈数据流,因为客户端可以将标识符分配给对象,然后立即对该对象执行操作,而无需等待服务器使用对象的标识符进行回复。在此模式中,标识符仅在当前连接的范围内有效,并且通常将零标识符保留为标记。安全注意事项:客户端不应将其地址空间中的地址用作其标识符,因为这些地址可能会泄露其地址空间的布局。
客户端分配的标识符模式存在一些缺点。例如,客户端更难以编写,因为客户端需要管理自己的标识符。开发者通常希望创建一个客户端库来为服务提供面向对象的外观,从而降低标识符管理的复杂性,而标识符本身也是一种反模式(请参阅下面的客户端库)。
当您想要使用内核的对象功能系统来保护对该对象的访问时,有强烈的信号需要创建一个单独的协议实例来表示对象,而不是使用客户端分配的标识符。例如,如果您希望客户端能够与对象交互,但又不希望客户端与其他对象交互,那么创建单独的协议实例意味着您可以使用底层通道作为一项功能来控制对该对象的访问。
命令联合
在使用前馈数据流的协议中,客户端通常会在发送双向同步消息之前向服务器发送许多单向消息。如果协议涉及大量消息,则发送消息的开销可能会变得明显。在这些情况下,请考虑使用命令联合模式将多个命令批量处理为一条消息。
在此模式中,客户端发送命令 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
中,客户端主要使用客户端分配的标识符与场景图中的节点进行交互。但是,从其他进程导入节点需要跨进程边界关联对该节点的引用。
事件对关联模式可依靠内核提供必要的安全性,使用前馈数据流来解决此问题。首先,要导出对象的客户端会创建一个 zx::eventpair
,并将其中一个纠缠事件连同由客户端分配的对象标识符一起发送到服务器。然后,客户端将另一个纠缠事件发送给其他客户端,另一个客户端再将事件转发给服务器,并包含由客户端为现在共享的对象分配的标识符:
protocol Exporter {
ExportThing(resource struct {
client_assigned_id uint32;
export_token zx.Handle:EVENTPAIR;
});
};
protocol Importer {
ImportThing(resource struct {
some_other_client_assigned_id uint32;
import_token zx.Handle:EVENTPAIR;
});
};
为了将对象关联起来,服务器会使用 ZX_INFO_HANDLE_BASIC
调用 zx_object_get_info
,并匹配纠缠事件对象中的 koid
和 related_koid
属性。
事件对取消
使用拆解协议事务时,客户端可以通过关闭协议的客户端端来取消长时间运行的操作。服务器应监听 ZX_CHANNEL_PEER_CLOSED
并中止事务,以避免浪费资源。
对于没有专用通道的操作,也有一个类似的用例。
例如,fuchsia.net.http.Loader
协议具有可启动 HTTP 请求的 Fetch
方法。HTTP 事务完成后,服务器会使用 HTTP 响应来回复请求,这可能需要大量时间。客户端没有明显的取消请求的方法,无法关闭整个 Loader
协议,而这么做可能会取消许多其他待处理的请求。
事件对取消模式通过让客户端包含来自 zx::eventpair
中的一个纠缠事件作为参数来解决此问题,从而解决此问题。然后,服务器会监听 ZX_EVENTPAIR_PEER_CLOSED
,并在断言该信号时取消操作。使用 zx::eventpair
优于使用 zx::event
或其他一些信号,因为 zx::eventpair
方法会隐式处理客户端崩溃或以其他方式崩溃的情况。当客户端保留的纠缠事件被销毁时,内核会生成 ZX_EVENTPAIR_PEER_CLOSED
。
空协议
有时,一个空协议可以提供价值。例如,创建对象的方法也可能接收 server_end:FooController
参数。调用方会提供以下空协议的实现:
protocol FooController {};
FooController
不包含任何用于控制所创建对象的方法,但服务器可以使用协议上的 ZX_CHANNEL_PEER_CLOSED
信号来触发对象的销毁。将来,可以使用用于控制所创建对象的方法扩展该协议。
控制设置等数据
通常,服务器会显示客户端可修改的设置。首选使用 table
来表示此类设置。例如,fuchsia.accessibility
库定义了以下内容:
type Settings = table {
1: magnification_enabled bool;
2: magnification_zoom_factor float32;
3: screen_reader_enabled bool;
4: color_inversion_enabled bool;
5: color_correction ColorCorrection;
6: color_adjustment_matrix array<float32, 9>;
};
(为便于阅读,省略了注释。)
有多种方式让客户能够更改这些设置。
部分更新方法公开了采用部分设置值的 Update
方法,并且仅当字段出现在部分值中时才会更改字段。
protocol TheManagerOfSomeSorts {
/// Description how the update modifies the behavior.
///
/// Only fields present in the settings value will be changed.
Update(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
replace 方法公开了采用完整设置值的 Replace
方法,并将设置更改为新提供的设置。
protocol TheManagerOfOtherSorts {
/// Description how the override modifies the behavior.
///
/// This replaces the setting.
Replace(struct {
settings Settings;
}) -> (struct {
args Args;
});
};
需要避免的事项:
避免将动词
Set
或Override
用于部分更新或替换方法,因为提供的语义不明确。请避免使用单独的方法来更新
SetMagnificationEnabled
等设置的字段。这种个体方法的维护负担更大,调用方很少希望更新单个值。避免移除具有魔法值的设置(例如
-1
)。而是通过缺少该设置字段来移除设置。
引用并集变体和表字段
引用类型的字段(例如引用表的一个或多个字段,或引用特定的并集变体)通常很有用。
假设有一个 API,它以包含许多字段的 table
形式提供元数据。如果此元数据变得非常大,通常可以通过一种机制让接收者向发送者指明将读取此元数据中的哪些字段,从而避免发送接收者不会考虑的多余字段。在这种情况下,具有成员与 table
字段一对一匹配的并行 bits
可以为构建 API 打下坚实的基础:
type MetadataFields = flexible bits {
VERSION = 0b1;
COUNT = 0b10;
};
type Metadata = table {
1: version string;
2: count uint32;
// ...
};
protocol MetadataGetter {
Get(struct {
fields MetadataFields;
}) -> (struct {
metadata Metadata;
});
};
现在,请考虑命令联合。在复杂的场景中,服务器可能需要能够描述其支持的命令。在这种情况下,拥有成员与 union
的变体一对一匹配的并行 enum
可以为构建 API 奠定坚实的基础:
type MyCommandVariant = strict enum {
POKE = 1;
PROD = 2;
// ...
};
protocol HighVolumeSinkContinued {
SupportedCommands() -> (struct {
supported_commands vector<MyCommandVariant>;
});
};
请注意,虽然您可能很想使用 bits
值来表示这组命令,但这样会导致一些后续更加难以做出选择的问题。如果您的 API 不断发展,导致您需要引用特定命令,那么 enum
自然会适合您。如果您从 bits
值开始,现在将面临以下两个错误选择之一:
引入了
enum
,这意味着现在有两种引用字段的方法,并且客户端代码中可能存在转换问题(从一种表示形式转到另一种表示形式);或者继续使用
bits
,但存在在任何给定时间都只能设置一个位的限制,而现在映射回所设置的特定位非常麻烦。
总的来说,对于 table
:
使用
table
的名称和后缀Fields
(复数)为bits
命名。每个成员值都应是序数索引处的位,即1 << (ordinal - 1)
。与针对
union
的建议类似,您希望使bits
和table
之间的灵活性保持一致,即由于 FIDL 目前仅支持灵活表,因此bits
必须为flexible
。
对于 union
:
按
union
的名称和后缀Variant
(单数)为列出所有变体的enum
命名。每个成员值都应为其描述的变体的序数。匹配
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 中包含客户端库,我们应至少以两种语言提供该库的实现。
服务中心:使用时需谨慎
Service Hub 是一种 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 {};
无论采用哪种方式,客户端都可以与枚举服务建立连接。在后一种情况下,客户端可以通过整个系统中用于发现服务的常规机制发现相同的服务。使用常规机制可让核心平台对发现应用适当的政策。
但在某些情况下,Service Hub 非常有用。例如,如果协议是有状态的,或者是通过比正常服务发现更复杂的某种流程获得的,则该协议可以通过将状态传输到所获取的服务来提供价值。再举一个例子,如果用于获取服务的方法采用额外的参数,协议可以在连接到服务时考虑这些参数来提供值。
过于面向对象的设计:否
某些库会为协议中的每个逻辑对象创建单独的协议实例,但这种方法有许多缺点:
未定义不同协议实例之间的消息排序。通过单个协议发送的消息会按 FIFO 顺序(在每个方向上)进行处理,但通过不同通道发送的消息会争用。如果客户端和服务器之间的交互涉及多个通道,那么当消息被意外重新排序时,出现错误的可能性更大。
每个协议实例在内核资源、等待队列和调度方面都有费用。尽管 Fuchsia 设计为可以扩展到大量通道,但整个系统的费用总和,导致对系统中的每个逻辑对象进行建模时对象数量激增,这会给系统带来巨大的负担。
错误处理和拆解要复杂得多,因为错误和拆解状态的数量会随着互动中涉及的协议实例数量呈指数级增长。使用单个协议实例时,客户端和服务器都可以通过关闭协议来彻底关闭交互。使用多个协议实例时,交互可能会进入交互状态部分关闭的状态,或者各方对关闭状态的视图不一致。
- 跨协议边界进行协调比在单个协议内更复杂,因为多个协议需要允许不同的客户端使用不同的协议,这些客户端可能不会完全信任彼此。
但在一些用例中,需要将功能分离到多个协议中:
提供单独的协议有助于提高安全性,因为某些客户端可能只能访问其中一个协议,因此在与服务器的交互方面会受到限制。
从独立线程中也可更轻松地使用独立协议。例如,一个协议可能会绑定到一个线程,而另一个协议可能会绑定到另一个线程。
客户端和服务器会为协议中的每种方法支付少量费用。 如果一次只需要几个较小的协议,则拥有一个包含所有可能方法的大型协议可能比使用多个较小的协议效率低下。
有时,服务器保留的状态会沿着方法边界干净利落。在此类情况下,请考虑将协议分解为沿着这些相同边界的较小协议,以提供与单独的状态进行交互的单独协议。
避免过度朝向对象的一种好方法是使用客户端分配的标识符对协议中的逻辑对象进行建模。该模式允许客户端通过单个协议与大量可能的逻辑对象进行交互。
使用魔法值指定缺失值:no
我们通常希望指示服务器设置某种状态,但也允许移除状态。以下命令使用魔法值指示进行移除:
// BAD
protocol View3 {
ReplaceHitRegion(struct {
id uint64;
// Set to { -1, -1 } to remove.
size fuchsia.math.Rect;
});
ReplaceName(struct {
// Set to "" to remove.
name string;
});
};
不过,FIDL 针对许多数据类型提供了可选性。使用可选性会生成一个更符合习惯的接口:
protocol View4 {
ReplaceHitRegion(struct {
id uint64;
size box<fuchsia.math.Rect>;
});
ReplaceName(struct {
name string:optional;
});
};
-
当涉及 ABI 时,FIDL 类型系统是一种结构类型系统,即名称没有规定,只有类型结构很重要,而 FIDL 类型系统涉及 API 时,则具有命名类型语义。 ↩