FIDL总览
本文档概述了Fuchsia接口定义语言(FIDL),FIDL是一种用于描述在Fuchsia内运行程序使用的进程间通信(IPC)协议的语言。本概述介绍了FIDL背后的概念—熟悉相关概念的开发者已经可以根据tutorials开始编写代码,或者通过阅读language或者bindings参考来进一步了解。
什么是FIDL
FIDL代表了”Fuchsia Interface Definition Language“,这个单词本身可用于参考为以下不同概念
- FIDL wire format:FIDL 线型格式,明确FIDL消息在内存内的代表方式,以便通过IPC传输
- FIDL language:FIDL语言是定义协议在.fidl文件内的语法
- FIDL compiler:FIDL编译器生成程序使用和实现协议的代码
- FIDL bindings:FIDL绑定是特定语言运行时支持库和代码的生成器,提供用于操作FIDL数据结构和协议的API
FIDL的主要工作是允许多种client和service的互连。通过IPC机制从定义上的实现解耦来达成客户端多样性,并通过自动代码生成实现简化。
FIDL语言是一种类C(尽管是简化的)定义语法,允许service提供者详细定义其通信协议。基本数据类型,例如integers, float和string可以组合成更为复杂的聚合结构体(structures)和联合体(unions)。基本数据类型和聚合类型可以组成固定array和动态长度vector,并且还可以结合组成更加复杂的数据结构。
因为客户端有多种实现语言(C, C++, Rust, Dart等),我们不希望开发者因为服务端提供对应语言协议实现,从而增加开发负担。
所以FIDL工具链应运而生,服务端开发者仅需要创建相关 .fidl
定义文件描述其通信协议。FIDL编译器通过这些文件生成任何支持语言类型中的客户端和服务端代码。
在很多使用场景中,服务端只有一个实现方式(例如,特定服务可能使用C++实现),尽管如此,可能存在很多语言实现的客户端。
注意:Fuchsia操作系统没有内置FIDL相关内容,FIDL绑定器使用Fuchsia标准通道通信机制。FIDL绑定器和库对该通道的使用方式实施了一套语义行为和持久化格式。
FIDL架构
从开发者角度,以下为其主要的组件:
FIDL定义文件 — 这是一个定义变量和协议(方法和参数)的文本文件(以
.fidl
约定结尾)客户端代码 — 通过FIDL编译器 (
fidlc
)工具链生成每种目标语言的代码服务端代码—同样通过FDIL编译工具链生成
以下为一个“echo”服务的非常简单的FIDL定义文件示例,客户端发送到服务端后,服务端回复到客户端
为了清楚起见,增加了行号,这不是
.fidl
文件的内容
1 library fidl.examples.echo;
2
3 [Discoverable]
4 protocol Echo {
5 EchoString(string? value) -> (string? response);
6 };
<!—-
Let’s go through it line by line.
Line 1: The library
keyword is used to define a namespace for this
protocol. FIDL protocols in different libraries might have the same name, so the
namespace is used to distinguish amongst them.
Line 3: The [Discoverable]
attribute indicates that the
protocol that follows should be made available for clients to connect to.
Line 4: The protocol
keyword introduces the name of the protocol, here
it’s called Echo
.
Line 5: The method, its parameters, and return values. There are two unusual aspects of this line:
- Note the declaration
string?
(for bothvalue
andresponse
). Thestring
part indicates that the parameters are strings (sequences of characters), while the question mark indicates that the parameter is optional. - The
->
part indicates the return, which appears after the method declaration, not before. Unlike C++ or Java, a method can return multiple values.
—->
让我们来逐句解释该文件
第一句:关键字library
用于定义协议的命名空间。不同库中的FIDL协议可能具有相同名字,所以采用命名空间来区分它们。
第三句: [Discoverable]
attribute表示后面的协议提供给客户端连接使用。
第四句:protocol
关键字引入协议的名称,示例中叫Echo
。
第五句:方法、参数和返回值定义,这句中有两个不同以往的方面:
注意:
string?
(value
和response
)声明。string
部分表示参数为字符串(字符的序列),问号表示参数为可选。->
部分表示返回,出现于方法调用声明后而不是之前。不同于C++或者java,一个方法调用可以返回多个值。
上述FIDL文件,只定义了一个Echo
的协议和一个EchoString
的方法调用,它可接收一个可空字符串并返回一个可空字符串。
上述的简单示例只使用了一种数据类型, string
作为其方法调用的输入和输出。
FIDL数据类型也可以非常灵活:
{%includecode gerrit_repo="fuchsia/fuchsia" gerrit_path="examples/fidl/fuchsia.examples.docs/misc.test.fidl" region_tag="example-struct" %}
上述定义了一个叫做MyRequest
的结构体和三个成员:一个32位无符号整数serial
,一个字符串key
和一个32位无符号整数向量 options
消息模型
为了了解FIDL消息传递,我们需要将内容拆分为两个层次并阐明相关定义。
在系统底层(操作系统层面),有一个面向发送方和接收方独立的异步通信方案:
发送方 — 发出消息的一方,
接收方 — 接收消息的一方,
发送消息采用非阻塞的方式:发送方发出消息后,然后可以继续处理,而不用管接收方在做什么。而接收方愿意,可以阻塞自身等待消息传入。
顶层实现FIDL消息,并使用底层(异步)的层,被分为客户端和服务端
client — 对(服务器)发出请求的一方
server — 处理请求的一方(代表客户端)
注意: 当我们讨论消息本身时,术语”sender”和”receiver”是有意义的—底层通信方案并不关心我们分配给各方的角色,只关心一方是发送方,一方是接收方而已。
在我们讨论各方角色扮演时,术语“client”和“server”是有意义的。特别是,客户端可以一个时间段扮演发送方,也可以在不同时间扮演接收方,服务端也是一样。
实际上来说,在client/server交互中,存在以下几种模式:
blocking call — 客户端发送到服务端后,等待其返回结果
fire and forget — 客户端发送到服务端后,不期待其返回
callback or async call — 客户端发送到服务端后,不阻塞自身,稍后以异步的方式返回结果
event — 服务端发送给客户端,即使客户端并没有主动请求相关数据
除了第一种方式是同步,其余均为异步。我们将逐个讨论这些通信模式。
客户端发送给服务端后,等待其回复
当前模型是在大多数编程语言中都有的典型“阻塞调用”或者“函数调用”,除了调用是通过一个通道完成的,因此可能由于传输级别错误产生调用失败。
从客户端来看,它包含了一个阻塞调用,同时服务端响应一些处理。
接下来是逐步的描述:
- 客户端发起调用(可选包含数据)并阻塞。
- 服务端接收客户端调用(可选数据),并执行一些处理响应。
- 由服务端判断,回复到客户端(和可选数据)。
- 服务端回复消息,解除客户端阻塞。
为了在异步消息方案中实现这种同步消息模型是很简单的。回顾一下,在协议底层,客户端到服务端和服务端到客户端的消息传输都是异步的。通过客户端阻塞等待服务端消息到达,使同步发生的客户终端。
基本地,在这个模型中,客户端和服务端达成以下共识:
- 数据流由客户端初始化,
- 客户端应当最多仅有一条消息未完成,
- 服务端应当仅发送一条消息作为回复到客户端,
- 客户端继续运行前应当等待服务端回复,
阻塞模型被广泛使用在客户端需要拿到当前请求调用回复后才能继续运行的使用场景中。
例如,客户端可能从服务端请求数据,然后在数据达到之前不能做任何其他有用的处理。
或者,客户端需要以特殊顺序执行步骤,因此来确保在下一条消息初始化之前完成每一个步骤。如果有错误发生,客户端需要依赖于操作进行到什么程度来执行纠正措施——这也是采用同步完成每个步骤的另一个原因。
客户端发送给客户端后,不等待其返回
此模型同样被称为“发后即忘”。在此模型中,客户端发送消息给服务端,然后继续其操作。对比阻塞式模型,客户端不会自身阻塞,也不期望收到回复。
这种模型被用在客户端不需要(或者不能)同步处理其请求的使用场景中。
典型的例子为日志系统。客户端发送日志信息到日志服务端(见上图圈1和圈2),但是没有理由需要阻塞。在客户端会可能存在很多错误运行。
- 客户端忙,当前无法处理写入请求,
- 多媒体已满,客户端无法写入数据,
- 客户端遇到故障,
- 等等情况
尽管如此,客户端也无法对这样的问题做出处理,所以阻塞也只会产生更多问题。
客户端发送到服务端,但是不阻塞
当前模型和下一个(“客户端没有请求数据时,服务端发送到客户端”)是类似的。
在当前模型中,客户端发送消息到服务端,但是不发生阻塞。尽管如此,客户端还是期待从服务端的回复,但是关键在于这不是同步调用。
这样可以保持在客户端/服务端交互中很大的灵活性。
同步调用模型中强制要求客户端等待服务端的回复响应,但是当前模型解放客户端可以在服务端响应时,做一些自己其他的操作。
上图和模型1中细微的差别在于在图示圈1后,客户端还是依然在运行。客户端可选什么时候让出CPU;这是一种非同步调用的消息形式。
这里实际上有两个子案例—一个是客户端仅收到一个回复,而另一个则是客户端可以收到多条回复。(客户端没有收到回复是“发后即忘”模式,是我们上一个讨论的模型)
单一请求,单一响应
单一响应案例是最接近同步调用模型:客户端发送一条消息后,最终,服务端回复。你可以使用此模型代替多线程,例如,你知道客户端可以在服务端回复的时候做有用的工作,同时等待其回复。
单一请求,多条回复
多条回复的场景可以应用于“订阅”模型。客户端信息”激励“服务器,例如,每当有事发生时,要求通知到客户端。
然后客户端就会去做自己的事情。
过一会儿后,服务端通知到客户端其关注的事件发生,发送消息给客户端。从客户端/服务端的角度来看,这条消息是一条“回复响应”,即客户端异步收到其调用。
当另一个关注事件发生时,服务端没有理由不发送另一条信息;这就是“多条回复”的通信模型。注意,第二条回复(和后续回复)是不需要客户端发送额外信息的。
注意客户端发送消息时是没有必要等待服务端响应。在上图中,我们展示了客户端在圈3前的阻塞状态——客户端可以同样运行。
没有客户端请求数据时,服务端发给客户端
这种模型同样也被称为“事件”模型。
在图中,客户端准备好从服务端收取信息,但是不知道什么时候才能收到——消息不仅对客户端是不同步的,而且(从客户端/服务端的角度看)也是“主动提供”的,在客户端中没有明确请求它们(就像之前的模式一样)。
客户端指定一个功能函数被回调(“事件句柄函数”),当收到从服务端来的信息被调用,但除此之外继续运行其他业务。
在服务端判定中(上图圈1和2),消息被异步发送到客户端,然后被客户端指定的函数处理。
注意当消息已经被发送(圈1),客户端可能已经正在运行,或者客户端可能没有事情做正在等待消息发送(图2)。
并没有要求客户端要正在等待消息。
异步消息复杂度
如上分解异步消息(有点随意)的类型是为了展示典型的使用模式,但这并不意味着能详细描述。
在大多数异步消息的使用场景中,你可以有0个或者多个客户端消息与0个或者多个服务端的回复松散地联系在一起。这就是“松联系”增加设计过程的复杂度。
IPC模型在FIDL中
现在我们已经理解了IPC模型和怎样使用FIDL进行异步消息交互,让我们继续来看怎么定义它们。
我们将添加其他的模型(发后即忘,和异步调用)到协议定义文件中。
1 library fidl.examples.echo;
2
3 [Discoverable]
4 protocol Echo {
5 EchoString(string? value) -> (string? response);
6 SendString(string? value);
7 -> ReceiveString (string? response);
8 };
Line 5是 我们上述讨论的EchoString
方法——这是一个传统的方法调用消息,当客户端调用 EchoString
输入一个可选字符串,然后阻塞,等待客户端回复另一个可选字符串。
Line 6 是 SendString
的调用方法。它不包含 ->
的返回声明——这让它成为”发后即忘“的模型(仅发送),因为我们已经告诉给FIDL编译器,这个特殊的方法调用不包含相关的返回。
注意这不是缺少返回 参数 ,而是缺少返回声明才是关键——放置“
-> ()
”在SendString
后会改变其声明意义,从声明发后即忘风格方法调用到声明没有返回参数的函数调用风格。
Line 7 是 ReceiveString
方法调用。这有一些小差别——它在第一部分不含有调用名称,而是在->
操作符后给出。这告诉给FIDL编译器,这是一个“异步调用”模型声明。
FIDL绑定
FIDL工具链接收FIDL协议和类型定义,例如上述示例所见,然后用每种目标语言生成对应代码来“讲述”它的协议。这种生成代码被称为FIDL绑定,它可以根据语言的不同而有不同的风格。
- 本机绑定:为高敏感性上下文设计,例如设备驱动和高吞吐量服务,利用就地存取,避免内存分配,但是可能需要开发者对协议的限制有更多的认识。
- 惯用绑定:为更开发者友好的方式设计,通过从线型格式的数据拷贝到更容易使用的数据类型(例如堆栈支持的字符串或者数组),但相应地降低了效率。
取决于语言不通,绑定提供了几种不同的调用协议方式。
- 发送/接受:直接在通道内读写信息,没有内置等待循环(C)
- 基于回调:在事件循环上,接收信息并作为回调的异步分发(C++, Dart)
- 基于端口:接收信息并分发到端口或者将值(future)(Rust)
- 同步调用:等待回复并返回(Go, C++单元测试)
绑定提供以下一些或所有原则操作:
- 编码:就地转换本地数据结构到线型格式(结合验证)
- 解码:就地转换线型格式到本地数据结构(结合验证)
- 拷贝/移动到惯用格式:拷贝本地数据结构目录到惯用数据结构,句柄转移
- 拷贝/移动到本地格式:拷贝惯用数据结构目录到本地数据结构,句柄转移
- 克隆:拷贝本地或者惯用数据结构(不包含仅移动类型)
- 调用:调用协议方法
客户端实现
不管目标语言是什么, fidlc
FIDL编译器遵循基本结构生成客户端代码。
第一部分包含管理和背景处理,其中包括:
- 提供一些连接到服务端的方法
- 启动一个异步(“后台”)消息处理循环
- 异步调用方式和事件调用放方法,如有,则绑定到消息循环中
第二部分包含传统函数调用或发后即忘式方法的实现,适配其目标语言。
通常来讲,其中包含:
- 创建可调用的API和声明
- 对每个API生成代码,将调用数据整理成FIDL格式的缓存区,以便传输到服务端
- 生成传输数据到服务端的代码
- 在函数调用类型的使用场景中,生成代码:
- 等待从服务端的回复
- 从FIDL格式化的缓冲区内解读数据,并且
- 通过API函数返回数据
很明显,由于语言实现的差异,具体步骤可能有所不同,但是列出的为基本概要。
服务端实现
fidlc
FIDL编译器同样根据目标语言生成服务端代码。就像客户端代码一样,不管是哪种目标语言,这段代码都有一个共同的结构。代码中包含:
- 创建对象让客户端可以连接,
- 开始主处理循环,其中:
- 等待信息
- 通过调用执行函数来处理消息
- 如果指定的话,发出异步调用,返回给客户端输出
在下一章节中,我们可以看到每种语言实现客户端和服务端代码的细节。
为什么使用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),但是它的设计依旧把Fuchsia放在首位。
- 提供方便的API用于创建,发送,接受和消耗消息。
- 进行充分验证确保协议的不变性(但不过如此)
效率
- 和手写的数据结构拥有一样的效率(速度和内存)。
- 线型格式使用非压缩本地数据类型,并包含小端定向和正确对齐方式,支持就地访问消息内容。
- 当消息大小静态已知或有界限的,不需要动态内存分配去产生和消费消息。
- 明确处理所有权,只需移动语义。
- 数据结构打包顺序是规范的,无歧义的,并且有最小填充。
- 避免后补的指针。
- 避免高昂的校验成本。
- 避免可能溢出的计算。
- 利用协议管道请求异步操作。
- 结构大小固定;可变大小的数据是线外存储的。
- 结构不是自我描述的;由FIDL文件描述它们的内容。
- 没有结构的版本,但协议可以用新的方式来扩展进化。
工效学
- Fuchsia团队维护以下编程语言绑定:
- C, Low-Level C++, High-Level C++, Dart, Go, Rust
- 考虑到我们将来可能要支持其他语言,例如:
- Java, JavaScript等
- 依赖于特定应用,绑定和生成代码有本地或习惯的两种方式。
- 使用编译时代码生成来优化消息的序列化,反序列化和验证。
- FIDL语法是熟悉的,易于理解的,并且与编程语言无关。
- FIDL提供一个库系统来简化部署和让其他开发者使用。
- FIDL表达了系统API最通用的数据类型;它并不寻求为所有编程语言提供所有类型的全面一对一映射。
工作流程
本章回顾了使用FIDL描述的IPC协议的作者,发布者和消费者的工作流程。
编写FIDL
基于FIDL协议的作者编写一个或多个*.fidl files来描述他们的数据结构,协议和方法调用。
FIDL文件被作者分组成一个或多个FIDL libraries 。每一个独特命名的库代表了一组逻辑相关的功能。在同一个库中的FIDL文件可以访问同一库中的所有其他声明。组成一个库的FIDL文件声明的先后顺序并不重要。
一个库中的FIDL文件可以通过importing其他FIDL模块的方式访问其他FIDL库。导入其他FIDL库可以使用它们的符号,从而可以构建由它们衍生的协议。引入的符号必须由库名或别名来限定,以防止命名空间冲突。
发布FIDL
基于FIDL协议的发布者负责将FIDL库提供给消费者。例如,作者在公共代码仓库发布FIDL库,或者以SDK的部分进行分发。
消费者仅需要在包含FIDL的路径下指定FIDL编译器去生成对应库的代码。如何做到这一点的明确细节一般由消费者的构建系统来解决。
使用FIDL
基于FIDL协议的消费者使用FIDL编译器生成代码去适配使用它们的语言运行时的特定绑定。对于某种语言运行时,消费者可以选择一些不同的生成代码偏好,所有的这些代码都是在线型格式中可以互操作的,但是在代码层面上就可能不一样了。
在Fuchsia世界构建环境中,从FIDL库中生成代码将由每个库的单独FIDL构建目标为所有相关语言自动完成。
在Fuchsia SDK环境中,从FIDL库生成的代码将被用作编译应用时的一部分。
开始
如果你想了解更多关于使用FIDL的内容,可以尝试了解导章节中有一些开发者指南 和教程。如果你将在Fuchsia上开发,可以参考FIDL绑定参考了解怎样使用现有FIDL API绑定。最后,如果你想要了解更多关于FIDL或者发布内容,请参考FIDL语言参考或者发布文档。