本文档简要介绍了 Fuchsia 接口定义 语言 (FIDL),用于描述进程间 在 Fuchsia 上运行的程序使用的通信 (IPC) 协议。此概览 介绍了 FIDL 背后的概念 - 熟悉这些概念的开发者 已经可以按照教程开始编写代码,或者 阅读语言或 绑定引用。
什么是 FIDL?
“FIDL”是“Fuchsia Interface Definition Language”(紫红色接口定义语言)的首字母缩写,单词 本身可以用来指代多个不同的概念:
- FIDL 传输格式:FIDL 传输格式指定了 FIDL 采用 消息在内存中表示,以便通过 IPC 传输
- FIDL 语言:FIDL 语言是用来
.fidl
文件对协议进行了说明 - FIDL 编译器:FIDL 编译器可为 使用和实现
- FIDL 绑定:FIDL 绑定 针对特定语言的运行时支持库和代码生成器,可提供 用于操控 FIDL 数据结构和协议的 API。
FIDL 的主要职责是让不同的客户和服务实现互操作。 通过分离 IPC 机制的实现,有助于实现客户端多样性 与定义相较而言,通过自动生成代码进行了简化。
FIDL 语言提供类似 C 语言的简单声明, 语法,使服务提供商能精确定义其协议。基本 整数、浮点数和字符串等数据类型可以分成更多 以及复杂聚合结构和联合体。固定数组和动态大小 矢量可通过基本类型和聚合类型进行构造, 所有这些都可以组合成更复杂的数据结构。
由于客户端实现的目标语言(C、C++、Rust、Dart、 等),我们不想给服务开发者带来负担 每个组件的协议实现。
这正是 FIDL 工具链的用武之地。该服务的开发者
一个用于定义协议的 .fidl
定义文件。通过使用此文件
然后,FIDL 编译器会使用任何受支持的
目标语言。
在许多情况下,只会有一种服务器实现(例如, 特定服务可能用 C++ 实现) 客户端的实现代码。
请注意,Fuchsia 操作系统本身并不了解 FIDL。通过 FIDL 绑定使用 Fuchsia 中的标准通道通信机制。通过 FIDL 绑定和库会强制执行一组语义行为和持久性 渠道使用方式的说明。
FIDL 架构
从开发者的角度来看,主要组件包括:
- FIDL 定义文件 - 这是一个文本文件(以
.fidl
结尾,由 定义值的规范)和协议(具有 参数), - 客户端代码 - 由 FIDL 编译器 (
fidlc
) 工具链生成, 每种特定的目标语言 - 服务器代码 - 也由 FIDL 编译器工具链生成。
作为 FIDL 定义文件的一个非常简单的例子,可以考虑使用“echo”服务 — 无论客户端向服务器发送什么内容,服务器都会回显到 客户端。
为清晰起见,添加了行号,这些行号不是
.fidl
文件中的一部分。
1 library fidl.examples.echo;
2
3 @discoverable
4 protocol Echo {
5 EchoString(struct {
6 value string:optional;
7 }) -> (struct {
8 response string:optional;
9 });
10 };
我们来逐行浏览一下。
第 1 行:library
关键字用于定义
协议。不同库中的 FIDL 协议可能具有相同的名称,因此
来区分它们
第 3 行:@discoverable
属性表示
以下协议应可供客户端进行连接。
第 4 行:protocol
关键字用于介绍协议的名称,在此示例中
名为 Echo
。
第 5-9 行:方法、其参数和返回值。有两个异常 这行代码的各个方面:
- 请注意
string:optional
声明(适用于value
和response
)。通过string
部分表示参数是字符串( 字符),而optional
约束条件表明该参数是 可选属性。 - 这些参数封装在
struct
(顶级类型)中 包含方法参数。 ->
部分表示返回,后者显示在方法之后。 而不是在之前进行声明。与 C++ 或 Java 不同,一个方法可以返回多个 值。
然后,上述 FIDL 文件声明了一个名为 Echo
的协议,其中包含一个
名为 EchoString
的方法,该方法接受可为 null 的字符串并返回一个可为 null 性
字符串。
上面的简单示例仅使用了一种数据类型,即 string
作为两种输入
添加到方法和输出中。
可能的 FIDL 数据类型非常灵活:
type MyRequest = struct {
serial uint32;
key string;
options vector<uint32>;
};
以上代码声明了一个名为 MyRequest
的结构,其中包含三个成员:
名为 serial
的无符号 32 位整数,一个名为 key
的字符串,以及一个矢量
称为 options
的无符号 32 位整数
消息传递模型
为了理解 FIDL 的信息,我们需要将其分为两部分 并阐明一些定义。
在底部(操作系统层),有一个异步 便于发件人和 receiver:
- sender - 发起消息的一方。
- 接收方 - 接收消息的一方;
发送邮件属于非阻塞操作:发件人发送邮件,并且 然后,无论接收方在做什么,都可以继续进行处理。
接收者可以根据需要阻止消息,以便等待消息。
顶层用于实现 FIDL 消息,使用底层(异步) 层。它处理客户端和服务器:
- client - 发出请求的一方(对服务器)、
- 服务器 - 处理请求的一方(代表 客户端)。
术语“发件人”和“接收方”探讨这些 因此底层通信机制不能 我们只关心我们为各相关方分配的角色, 一个是正在发送,另一个是接收
术语“客户”和“服务器”我们讨论这些角色 以及各方所扮演的角色具体来说,一个客户端可以同时成为一个发件人, 接收者对服务器而言是相同的
实际上,在客户端 / 服务器交互的背景下 则意味着有多个模型:
- 阻塞调用 - 客户端向服务器发送信息,等待回复
- 即发即弃 - 客户端将消息发送到服务器,不需要回复
- callback 或 async call - 客户端发送到服务器,但不 block;稍后会异步传送回复
- 事件 - 服务器向客户端发送事件,客户端未询问 用于数据
第一个是同步的,其他的则是异步的。我们将在本课程中 订单。
客户端向服务器发送内容,等待回复
这种模型是传统的“阻塞调用”或“函数调用”适用于 只不过是在通过通道进行调用, 因此可能会因传输级别错误而失败。
从客户端的角度来看,它包含一个阻塞的调用,而 服务器会执行一些处理。
以下是分步说明:
- 客户端进行调用(可选择包含数据)并阻止。
- 服务器接收客户端的调用(和可选数据),并执行一些 处理量。
- 由服务器自行决定是否回复客户端(使用可选数据)。
- 服务器的回复导致客户端解除阻止。
为了通过异步消息传递模型实现此同步消息传递模型 架构非常简单回想一下,客户端到服务器和服务器到客户端 在协议的底层,消息传输是异步进行的。通过 同步是在客户端进行的,即让客户端一直阻止, 。
基本上,在此模型中,客户端和服务器已达成一致:
- 数据流由客户端发起
- 客户端最多只能有一条未完成的消息,
- 服务器只应向客户端发送消息以响应客户端的 消息
- 客户端必须先等待服务器响应,然后才能继续。
这种阻塞模型通常用于客户端需要响应 然后才能继续
例如,客户端可能会从服务器请求数据,而不能执行 在数据到达之前进行任何其他有用的处理。
或者,客户可能需要按特定顺序执行步骤,因此必须 确保每个步骤都已完成,然后再开始下一个步骤。如果出现错误 客户可能需要执行纠正措施,具体取决于 操作已经继续 — 另一个需要同步到 每个步骤的完成情况
客户端向服务器发送消息,但没有回复
此模型也称为“即发即弃”。在其中,客户端会将 将消息发送到服务器。然后继续执行操作与此相反, 响应。
此模型适用于客户端不需要(或不可以)的情况 与其请求的处理同步
典型的示例是日志记录系统。客户端将日志信息发送到 日志记录服务器(上图中的圆圈“1”和“2”),但没有原因 要屏蔽的广告服务器端可能会出现很多问题:
- 服务器正忙,目前无法处理写入请求,
- 媒体已满,服务器无法写入数据,
- 服务器发生故障时
- 依此类推。
然而,客户无法针对这些问题采取任何行动, 屏蔽只会造成更多问题
客户端向服务器发送内容,但未阻止
这个模型和下一个模型(“服务器不要求客户 类似。
在当前模型中,客户端向服务器发送消息,但不会阻止。 不过,客户端希望服务器收到某种响应,但密钥 它与请求不同步。
这为客户端 / 服务器交互提供了极大的灵活性。
虽然同步模式会强制客户端等待服务器回复, 当前模型使客户端能够执行其他任务, 请求:
此图与上面类似的图表的细微差别在于, “1”圈子则该客户端仍在运行。客户选择放弃 CPU;它与消息不同步。
这里实际上有两种子情况 - 一种是客户获得 一个响应,而另一个客户端可以获得多个响应。( 客户端获得零响应的情况是“即发即弃”模型, 。)
单一请求,单一响应
单一响应情况最接近同步模型:客户端 发送消息,最终服务器进行回复。您将使用此模型 而不是多线程处理 在等待服务器回复的同时执行有用的工作。
单个请求,多个响应
多响应案例可用于“订阅”模型。客户的 消息“primes”例如,在请求调用通知时, 情况。
然后,客户继续自己的业务。
一段时间后,服务器发现客户端 并因此向客户端发送消息。来自客户 / 从服务器角度看来,此消息是一个“回复”,客户端收到它 与其请求异步通信
服务器在发送其他消息时,由于自身原因, 感兴趣的事件发生时;这就是“多重响应”模型。 请注意,第二个(及后续)响应在没有客户端的情况下发送 发送任何其他消息。
请注意,客户端不需要等待服务器发送 消息。在上图中,我们展示了客户端处于屏蔽状态 “3”圆圈前- 该客户端可能同样在运行。
服务器向客户端发送数据,客户端不要求提供数据
此模型也称为“事件”模型。
在这里,客户端准备接收来自服务器的消息,但不知道 预期时间 - 这些消息不仅与 但从客户端 / 服务器的角度看,也属于“非请求”状态, 因为客户端并未明确请求它们(就像在上一张幻灯片中 模型)。
客户端指定要调用的函数(“事件处理函数”) 当邮件从服务器到达时, 业务。
由服务器自行决定(上图中的圆圈“1”和“2”) 会异步发送到客户端,并由客户端的指定 函数。
请注意,在发送消息时,客户端可能已经在运行(如圆圈中所示) “1”),或者客户端可能没有任何操作,正在等待 已发送(如圆圈“2”)。
这并不是要求客户端等待消息。
异步消息传递复杂性
将异步消息传递分解为以上各部分(有点任意) 用来显示典型的使用模式,但并不是 详尽无遗。
在最普遍的异步消息传递情况下,您有零个或更多个客户端 与零个或多个服务器回复松散关联的消息。正是这种“宽松” 关联”这会增加设计流程的复杂性。
FIDL 中的 IPC 模型
现在,我们已经了解了 IPC 模型,以及它们如何与 FIDL 的异步消息传递,我们来看看它们是如何定义的。
我们会将其他模型(触发和遗忘、异步调用或事件)添加到协议中 定义文件:
1 library fidl.examples.echo;
2
3 @discoverable
4 protocol Echo {
5 EchoString(struct {
6 value string:optional;
7 }) -> (struct {
8 response string:optional;
9 });
10
11 SendString(struct { value string:optional; });
12
13 ->ReceiveString(struct { response string:optional; });
14 };
第 5-9 行是我们在上面讨论过的 EchoString
方法,
传统的函数调用消息,其中客户端使用一个EchoString
可选字符串,然后进行块阻止,等待服务器使用另一个
可选字符串。
第 11 行是 SendString
方法。它没有 ->
返回值
使其变为“即发即弃”model(仅发送),
因为我们已告知 FIDL 编译器,这种特定方法
与它相关联。
请注意,这不是缺少返回参数,而是缺少 返回声明,这是此处的键 - 输入“
-> ()
”晚于SendString
会更改声明“即发即弃”样式的含义 方法,用于声明没有任何返回值的函数调用样式方法 参数。
第 13 行是 ReceiveString
方法。它稍有不同
第一部分没有方法名称,而是在
->
运算符。这会告知 FIDL 编译器这是一次“异步调用”或
“event”模型声明。
FIDL 绑定
FIDL 工具链接受 FIDL 协议和类型定义,如 上述示例,并生成每种目标语言的代码, 这些协议生成的代码称为 FIDL 绑定, 根据语言的不同提供不同的口味:
- 原生绑定:专为设备等高度敏感的上下文而设计 驱动程序和高吞吐量服务器,利用就地访问,避免内存不足 但可能需要更多地了解 开发者的协议。
- 惯用的绑定:旨在通过复制 将数据从传输格式转换为更易于使用的数据类型(例如堆支持的数据格式) 字符串或向量),但与之对应的是 结果。
绑定提供了多种不同的协议方法调用方式,具体取决于 语言:
- 发送/接收:将消息直接读取或写入到某个通道,无内置功能 等待循环 (C)
- 基于回调:收到的消息按照如下方式异步分派 事件循环回调(C++、Dart)
- 基于端口:接收到的消息会传递到某个端口或将来 (Rust)
- 同步调用:等待回复并返回(Go、C++ 单元测试)
绑定可提供以下部分或全部主要操作:
- 编码:就地将原生数据结构转换为传输格式 (结合验证)
- 解码:就地将传输格式数据转换为原生数据结构 (结合验证)
- 复制/移至惯用表单:复制原生数据结构的内容 转换为惯用的数据结构,句柄被移动
- 复制/移至原生表单:复制惯用数据结构的内容 转换为原生数据结构,句柄移动
- Clone:复制原生或惯用数据结构(不包含 仅可移动类型)
- 调用:调用协议方法
客户端实现
无论目标语言如何,fidlc
FIDL 编译器都会生成客户端
基本结构的代码。
第一部分包括管理和后台处理, 包括:
- 提供一些连接到服务器的方法
- 启动异步(“后台”)消息处理循环
- 异步调用样式和事件样式方法(如果有)绑定到消息 支撑环
第二部分包含传统函数调用或 触发和忘记样式方法。 一般来说,这包括:
- 创建可调用的 API 和声明
- 为每个 API 生成代码,以便将调用中的数据编组到 FIDL 格式化的缓冲区,适合传输至服务器
- 生成用于将数据传输到服务器的代码
- 如果是函数调用样式调用,则会生成代码以执行以下操作:
<ph type="x-smartling-placeholder">
- </ph>
- 等待服务器响应
- 从 FIDL 格式的缓冲区中解组数据,以及
- 通过 API 函数返回数据。
显然,具体步骤可能会因语言实现方面的差异而有所不同 以上只是基本概况
服务器实现
fidlc
FIDL 编译器还会为给定目标生成服务器代码
语言。与客户端代码一样,此代码具有通用的结构
目标语言。代码如下:
- 会创建一个客户端可连接的对象
- 启动一个主处理循环,该循环:
<ph type="x-smartling-placeholder">
- </ph>
- 等待消息
- 通过调用实现函数来处理消息
- 如果已指定,则向客户端发出异步回调以返回 输出
在接下来的章节中,我们将详细介绍每种语言的 客户端和服务器代码
为何使用 FIDL?
Fuchsia 广泛依赖于 IPC,因为大多数功能都是在 内核外的用户空间,包括设备等特权组件 。因此,IPC 机制必须高效、确定性 功能强大且易于使用:
IPC 效率是指生成、分析代码所需的 在进程之间传输和接收消息IPC 会参与到 因此必须保持高效FIDL 编译器 生成紧密的代码,而不会产生过多的间接或隐藏成本。它应该位于 在最重要的方面不如人工代码。
IPC 确定性涉及在 已知资源信封。重要系统将广泛使用 IPC 文件系统等服务,这些服务为许多客户端提供服务, 可预测的方式。FIDL 传输格式必须提供强静态保证, 确保结构尺寸和布局不变,从而减少 需要使用动态内存分配或复杂的验证规则。
IPC 稳健性表明了将 IPC 视为 操作系统的 ABI。保持二进制文件的稳定性至关重要。机制 必须保守地设计协议演变,以免违反 保持现有服务及其客户端的不变性,尤其是在需要 确定性原则FIDL 绑定必须有效 轻量级且严格的验证。
IPC 易用性涉及到 IPC 协议 是操作系统 API 的一部分。务必要提供优秀的开发者 通过 IPC 访问服务的工效学设计。FIDL 代码生成器可移除 不必手动编写 IPC 绑定。此外,FIDL 代码生成器 生成不同的绑定,以适应不同受众群体的需求, 习语。
目标
FIDL 旨在针对这些特性进行优化。在 具体而言,FIDL 的设计旨在满足以下目标:
明确性
- 介绍了 Zircon 上的 IPC 使用的数据结构和协议。
- 针对进程间通信进行了优化。虽然 FIDL 还用于 存储到磁盘和用于网络传输,其设计没有针对 这些次要用例
- 高效传输由数据(字节)和功能组成的消息 (句柄)通过 Zircon 通道在同一设备上运行的进程之间传输。
- 专为帮助有效使用 Zircon 基元而设计。 尽管 FIDL 也用于其他平台(例如,通过 ffx),但其设计 紫红色优先。
- 提供便捷的 API,用于创建、发送、接收和使用 消息。
- 执行足够的验证以保持协议不变(但不再 之后)。
效率
- 与使用手动数据结构一样高效(速度和内存) 会是什么样子。
- 有线格式使用未压缩的原生数据类型,这些类型采用小端字节序, 正确对齐,以支持就地访问消息内容。
- 无需动态内存分配即可生成或使用消息 当它们的大小已知或有界限时。
- 使用仅移动语义明确处理所有权。
- 数据结构打包顺序是规范的、清晰明确,并且具有 内边距。
- 避免反向修补指针。
- 避免成本高昂的验证。
- 避免可能会溢出的计算。
- 利用协议请求的流水线处理异步操作。
- 结构是固定的;可变大小的数据离线存储。
- 结构不是自描述形式;FIDL 文件用于描述其内容。
- 没有结构的版本控制,但可以使用新方法扩展协议 这一点非常重要。
工效学设计
- Fuchsia 团队维护的编程语言绑定:
<ph type="x-smartling-placeholder">
- </ph>
- C、新 C++、高级 C++(旧)、Dart、Go、Rust
- 请注意,我们以后可能会支持其他语言,例如
以:
<ph type="x-smartling-placeholder">
- </ph>
- Java、JavaScript 等
- 绑定和生成的代码以原生或惯用方式提供 具体取决于目标应用。
- 使用编译时代码生成来优化消息序列化, 反序列化和验证
- FIDL 语法熟悉且易于访问,且 不可知。
- FIDL 提供了一个库系统,以简化其他组织 开发者。
- FIDL 表示系统 API 所需的最常见的数据类型;是的 未尝试对所有提供的类型进行全面的一对一映射 所有编程语言。
工作流程
此部分概要介绍了 IPC 作者、发布者和使用者的工作流程 FIDL 描述的协议。
编写 FIDL
基于 FIDL 的协议的作者创建了一个或多个 *.fidl 文件, 描述其数据结构、协议和方法。
FIDL 文件由作者归入一个或多个 FIDL 库。每个 库代表一组逻辑相关的功能,具有唯一的 库名称。同一库中的 FIDL 文件可隐式访问 同一个库中的其他声明。在 构成库的 FIDL 文件并不重要。
一个库的 FIDL 文件可以通过以下方法访问另一个 FIDL 库中的声明: 导入另一个 FIDL 模块。导入其他 FIDL 库会使其 可以用来构建派生协议。 。导入的符号必须由库名称或别名限定 以防止命名空间冲突
发布 FIDL
基于 FIDL 的协议的发布者负责制作 FIDL 库 。例如,作者可能传播 FIDL 库 放在公共源代码库中,或将其作为 SDK 的一部分进行分发。
使用者只需将 FIDL 编译器指向包含 用于为库(及其依赖项)生成代码的 FIDL 文件 库。有关具体实现方法的详细信息通常由 使用方的构建系统
使用 FIDL
基于 FIDL 的协议的使用方使用 FIDL 编译器来生成代码 适合与特定于其语言运行时的绑定搭配使用。对于特定 语言运行时,消费者可以选择几种不同风格的 生成的代码,所有这些代码都可以在传输格式级别实现互操作, 可能不在源代码级别
在 Fuchsia 世界构建环境中,从 FIDL 库生成代码将 通过单个 FIDL build 针对所有相关语言自动完成 每个库的定位
在 Fuchsia SDK 环境中,将通过 FIDL 库生成代码 以编译使用这些容器的应用的一部分。
使用入门
如果您想详细了解如何使用 FIDL,请参阅“Guides”(指南)部分, 开发者指南和教程 可以尝试。如果您在 Fuchsia 上开发,想要了解如何 对现有 FIDL API 使用绑定,您可以参阅 FIDL 绑定 参考。最后,如果您想详细了解 FIDL 或 请参阅 FIDL 语言 参考文档或贡献文档。