FIDL 指南
受众: 初级FIDL开发者.
前提: 至少具有C++或Dart语言的初级技能.
文章目录
关于本指南
本教程描述了在Fuchsia系统中,利用FIDL interprocess communication(IPC,进程间通信)系统如何在C++和Dart语言中进行调用。FIDL表示"Fuchsia Interface Definition Language",但是"FIDL"一词通常指代进行这些调用所需的基础设施,包括FIDL语言,编译器和生成的绑定。
Fuchsia操作系统没有关于FIDL的先天性知识。FIDL bindings绑定使用Fuchsia中标准的通道通信机制。FIDL绑定和库强制定义了一组语义行为和持久性格式,表示如何使用此通道。
关于FIDL详细的设计和实现文档参见zircon/docs/fidl/
获取和编译FIDL源代码
参见 instructions for getting and building Fuchsia.
我们将用于本教程的大多数示例位于Garnet中:
https://fuchsia.googlesource.com/garnet/+/master/examples/fidl/
Dart 示例位于 Topaz 中:
https://fuchsia.googlesource.com/topaz/+/master/examples/fidl/
在继续阅读的同时,开启编译:
# 稍后你将因Dart需要Topaz
fx set-petal topaz
# 当编译Topaz时包含进garnet示例
fx set x64 --packages topaz/packages/default,garnet/packages/examples/fidl
fx full-build
或者,你对Dart不感兴趣:
fx set-petal garnet
fx full-build
FIDL 架构
FIDL的组织方式如下:
FIDL Component 设计为与FIDL接口交互工作的软件。FIDL组件中的主线程通常含有一个分派调用的循环,也可能有额外包含循环的线程。
组件通过其**Interface(s)**被调用。Interface 接口 是可复用的,并且可由多个组件提供。接口通过FIDL定义。将接口和语言绑定的类,由FIDL编译器生成。
Service 是对接口的实现。所以,在特定的FIDL组件中,每个接口有一个服务。在FIDL客户库中,你将会看到一些调用,类似AddPublicService()
或 ConnectToService()
.
Channel 通道
虽然是一种 操作系统中的IPC结构 (InterProcess
Communication),
但是其在进程、线程之间,甚至线程内部都能很好工作。FIDL使用通道在组件之间通信。
Connection 表示两个组件之间的初始通道结构体。
本文中使用的Client表示一个连接到Server的FIDL组件。在本文档中client与server之间的差别是一种人为的区分。 你实现的FIDL组件可以是client、server或者两种角色都有,或许多。
Hello World “Echo” interface
我们将以一个C++回显服务开始,其回显它的输入信息,并且打印"hello world".
打开garnet/examples/fidl/services/echo2.fidl. 这是一个.fidl文件,其定义了接口以及FIDL组件可提供的相关数据结构(开放给其它FIDL组件使用)。接口可被FIDL支持的任意语言使用,允许你容易的执行跨语言的调用。
示例文件 echo2.fidl
, 增加了行号, 看起来如下所示:
1. library fidl.examples.echo2;
2. [Discoverable]
3. interface Echo {
4. 1: EchoString(string? value) -> (string? response);
5. };
让我们逐行的看一遍。
行 1: 库定义用于定义命名空间。位于不同库中的FIDL接口可使用相同的名称。
行 2: Discoverable
属性自动生成名字,客户可使用此名字发现和连接到此服务。
行 3: 接口Interface
名称。
行 4: 方法名称。此行有三个不寻常的方面:
-
注意,方法名之前的
1:
。这是一个sequence number序号,当有多个版本的相同接口时,用来确保向后兼容。所有的方法必须有唯一的序列号。 -
注意带有问号?的
string?
声明。问号标记意味着这些参数也许为空null
。 -
返回值类型位于方法声明之后,而不是之前。不同于C++ 或 Java,方法可返还多个值。
Generated files
当你编译FIDL源码树的时候,FIDL编译器自动运行。它将产生允许接口在不同语言中使用的融合代码。以下是为C++语言创建的实现文件,假设你的编译类别为x64
.
./out/x64/fidling/gen/fidl/examples/echo/cpp/fidl.cc
./out/x64/fidling/gen/fidl/examples/echo/cpp/fidl.h
为C++生成的文件遵循相同的模型。对于其它语言,FIDL编译器可能应用更合适这些语言的策略。
Generated files for C++ will follow the same pattern.
For other languages, the tools may employ strategies that are more suitable for
these languages.
C++语言的Echo
服务端
我们来看一下C++语言实现的服务端:
garnet/examples/fidl/echo2_server_cpp/echo2_server.cc
找到main函数的实现,和Echo
接口的实现。
Find the implementation of the main function, and that of the Echo
interface.
为了理解代码是如何工作的,这里总结了在服务端执行IPC调用的时候都发生了什么。
-
Fuchsia加载服务端可执行文件,并开始执行
main()
函数。 -
main()
函数创建EchoServerApp
对象,其在服务接口创建后,绑定到服务接口。 -
EchoServerApp()
通过调用context->outgoing().AddPublicService<Echo>()
将自身注册到StartupContext
。并且,传递一个lambda函数作为参数,该函数在连接请求到达时被调用。
现在,main
函数开始运行循环,表示为async::Loop
。
-
循环时接收到来自其它组件的连接请求,调用在
EchoServerApp()
中创建的lambda
函数。 -
此lambda函数将
EchoServerApp
实例绑定到请求的通道channel
。 -
循环从通道中接收到对
EchoString()
的调用,分派其到上一步骤中绑定的实例对象。 -
EchoString()
使用callback(value)
到客户发出一个异步回调,之后,返回到运行循环。
让我们详细看一遍以上是如何工作的。
头文件 File headers
首先是命名空间定义。这与在FIDL文件的"library"声明中定义的命名空间是匹配的,但这是偶然的:
namespace echo2 {
这里是服务端实现中使用的#include文件:
#include <fuchsia/cpp/echo2.h>
#include <lib/async-loop/cpp/loop.h>
#include <lib/zx/channel.h>
#include "lib/app/cpp/startup_context.h"
-
echo2.h
包含生成的Echo
FIDL接口的C++定义。 -
EchoServerApp
使用startup_context.h
开放服务端实现。
main 函数
各FIDL组件的main()
函数大都看起来非常相似。他们创建了一个使用async::Loop
或其他一些结构体的运行循环,并绑定了服务实现。Loop.Run()
函数进入消息循环中处理通过通道到达的请求。
最后,另一个FIDL组件将尝试连接到我们的组件。
The EchoServerApp()
constructor
在进一步讨论之前,请快速回顾一下FIDL Architecture一节。连接定义为与另一个组件的 第一个 通道。因此,在运行循环之前,以及第一次连接之前,执行服务注册。
EchoServerApp
构造函数如下所示:
EchoServerApp()
: context_(fuchsia::sys::StartupContext::CreateFromStartupInfo()) {
context_->outgoing().AddPublicService<Echo>(
[this](fidl::InterfaceRequest<Echo> request) {
bindings_.AddBinding(this, std::move(request));
});
函数对其提供给其它组件的每个服务调用一次AddPublicService
(请记住,每个服务开放一个接口)。相关信息由StartupContext
缓存,用于决定对于额外到达通道使用哪个Interface
工厂。当另外一端(客户端)调用ConnectToService()
时,每次都会创建一个通道。
结束。
如果仔细阅读代码,你将看到AddPublicService
的参数实际上是一个lambda
函数,用于捕获this
。这意味着lambda函数在通道尝试绑定接口之前不会执行,此时对象绑定到了通道,并可接收来自其他组件的调用。注意这些调用有线程亲核性,因此只能从相同线程进行调用。
传递给AddPublicService
的函数可以用不同的方式实现。EchoServerApp
中对所有通道使用相同的对象。这对当前情况是很好,因为实现的功能是无状态的。另外,更复杂的实现可以为每个通道创建不同的对象,或者可能在某种缓存方案中重新使用对象。
连接总是点对点的。没有多播连接。
函数EchoString()
最后,我们到了服务器讨论的结束部分。当在消息循环中由通道接收到消息,去调用Echo
接口内的EchoString()
函数,它将导到以下实现:
void EchoString(fidl::StringPtr value, EchoStringCallback callback) override {
printf("EchoString: %s\n", value->data());
callback(std::move(value));
}
以下是此代码的有趣之处:
EchoString()
的第一个参数是fidl::StringPtr
。由其名字可知,fidl::StringPtr
可能为空。FIDL中的字符串假定是UTF-8格式,但这不是由FIDL客户库强制要求的。EchoString()
函数返回void,因为FIDL调用是异步的。所有我们返回的任何值都没有去处。EchoString()
的最后一个参数是客户端的回调函数。在此情况下,回调参数为fidl::StringPtr
。- 通过执行回调函数,
EchoServerApp::EchoString()
将回复返回客户端。回调函数也是异步的,因此本函数通常在客户端运行回调之前返回。 - 因为回调是异步的,所以其返回值也是void。
在FIDL中对接口的任何调用都是异步的。这是一个大转变,如果之前你处于一个程序性的世界,其中函数调用在执行完成后才能返回。因为它是异步的,所以无法保证调用是否真正发生,所以你的回调可能永远不会执行。远端的FIDL组件可能会关闭、崩溃、繁忙等。
C++语言的Echo
客户端
让我们看一下C++实现的客户端。
garnet/examples/fidl/echo2_client_cpp/echo2_client.cc
客户端的代码结构与服务端类似,包括main
函数和一个async::Loop
。区别在于客户端一旦初始化完成立即开始工作。相反,服务器在接受连接之前不工作。
**注意:**一个组件可以是客户端、服务端,或者两者都是,或者多个。在这个例子中,客户端和服务端之间的区别纯粹是为了演示目的。
下面是客户端如何连接到Echo服务的总结。
- shell加载客户端可执行文件并调用
main
。 main
创建一个EchoClientApp
对象来处理连接到服务端,调用Start()
开启连接,然后进入消息循环。- 在
Start()
函数中,客户端携带服务端组件的URL调用context_->launcher()->CreateComponent
。如果服务端组件不在运行,此时将创建它。 - 接下来,客户端调用
ConnectToService()
打开到服务端组件的通道。 main
调用echo_->EchoString(...)
,并传递回调函数。因为FIDL IPC调用是异步的,EchoString()
可能会在服务器处理呼叫前返回。- 随后
main
阻塞,等待接口的响应。 - 最终,响应到达,回调函数执行,带有结果。
main 函数
客户端中的main()函数与服务端非常不同,因为它同步在服务端响应上。
int main(int argc, const char** argv) {
std::string server_url = "echo2_server_cpp";
std::string msg = "hello world";
for (int i = 1; i < argc - 1; ++i) {
if (!strcmp("--server", argv[i])) {
server_url = argv[++i];
} else if (!strcmp("-m", argv[i])) {
msg = argv[++i];
}
}
async::Loop loop(&kAsyncLoopConfigMakeDefault);
echo2::EchoClientApp app;
app.Start(server_url);
app.echo()->EchoString(msg, [](fidl::StringPtr value) {
printf("***** Response: %s\n", value->data());
});
return app.echo().WaitForResponse();
}
Start 函数
Start()
负责连接到远端的 Echo
服务.
void Start(std::string server_url) {
fuchsia::sys::LaunchInfo launch_info;
launch_info.url = server_url;
launch_info.directory_request = echo_provider_.NewRequest();
context_->launcher()->CreateComponent(std::move(launch_info),
controller_.NewRequest());
echo_provider_.ConnectToService(echo_.NewRequest().TakeChannel(),
Echo::Name_);
}
首先,Start()
调用CreateComponent()
启动echo_server
。随后,它调用ConnectToService()
以绑定到服务端的Echo
接口。确切地机制有一定程度的隐藏,但指定的接口是可自动的从EchoPtr
类型推断出来,该类型是fidl::InterfacePtr<Echo>
。
ConnectToService()
的第二个参数是服务名。
接下来,客户端在返回的接口中调用EchoString()
。由于FIDL接口是异步的,此调用本身返回时不等待EchoString()
的远端执行完成。由于此异步行为,EchoString()
返回void。
因为客户端在服务端响应到达之前无需执行任何操作,并且在之后立即完成工作,main()
随后使用loop.Run()
阻塞,然后退出。当响应到达时,赋与
EchoString()
的回调函数将首先执行,然后Run()
返回,允许main()
返回并终止程序。
运行示例
你可以这样运行Hello World示例:
$ run echo2_client_cpp
您不需要专门运行服务端,因为在客户端中调用CreateComponent()
将自动启动服务端。
Dart语言的Echo
服务端
echo服务端的Dart实现可在此获得:
topaz/examples/fidl/echo_server_dart/lib/main.dart
此文件实现了main()
函数和EchoImpl
类:
main()
函数在加载组件时执行。main()
注册处理来自FIDL的连接的可用服务。EchoImpl
处理Echo
接口上的请求。为每个通道创建一个新对象。
为了理解代码是如何工作的,下面是服务端中如何执行IPC调用的总结。我们将深入了解每一行代码的含义,因此在你继续之前不需要理解所有这些。
1。Startup. FIDL Shell加载Dart runner,它启动虚拟机,加载main.dart
,并调用main()
。
1。Registration main()
注册EchoImpl
将自身绑定到传入Echo
接口上的请求。main()
返回,但程序不退出,因为处理到达请求的事件循环还在运行。
1。Service request. Echo
服务端接收到请求,绑定Echo
服务到一个新通道,因此它调用上一步骤中传入的bind()
函数。
1。Service request. bind()
使用EchoImpl
实例。
1。API request. Echo
服务端由通道接收到对echoString()
的调用,并将其分配到EchoImpl
对象实例中的echoString()
函数。EchoImpl
在上一步骤中绑定。
1。API request. echoString()
调用给定的callback()
函数返回响应结果。
现在,我们来详细介绍一下这是如何工作的。
头文件 File headers
以下是Dart服务端实现中的导入声明:
import 'package:fidl/fidl.dart';
import 'package:fuchsia.fidl.echo2/echo2.dart';
import 'package:lib.app.dart/app.dart';
fidl.dart
为Dart开放了FIDL运行库。我们程序的InterfaceRequest
需要它。StartupContext
需要app.dart
,我们在其中注册我们的服务。echo2.dart
包含Echo
接口的绑定。此文件是从echo2.fidl
中定义的接口生成。
main() 函数
Everything starts with main():
void main(List<String> args) {
_context = new StartupContext.fromStartupInfo();
_echo = new _EchoImpl();
_context.outgoingServices.addServiceForName<Echo>(_echo.bind, Echo.$serviceName);
}
main()
在加载服务时由Dart VM调用,类似于C或C++组件中的main()
。它绑定一个EchoImpl
的实例、Echo
接口实现,到Echo
服务的名称。
最后,另一个FIDL组件将尝试连接到我们的组件。
bind()
函数
它看起来是这样的:
void bind(InterfaceRequest<Echo> request) {
_binding.bind(this, request);
}
当从另一个组件接收到第一个通道时,调用bind()
函数。此函数为其提供给其它组件的每个服务绑定一次(请记住,每个服务开放一个接口)。这个信息缓存在FIDL运行时拥有的数据结构中,并用于创建对象作为额外传入通道的端点。
与C++不同,Dart每个isolate只有一个线程,所以不可能混淆哪个线程拥有一个通道。
真的只有一个线程吗?
是和否都可以。组件的VM中只有一个线程,但是handle watcher isolate 有自己的独立线程,所以组件isolate不必阻塞。组件isolate也可以产生新的isolate,其将在不同的线程上运行。
echoString
函数
最后,我们到了服务端API的实现部分。你的EchoImpl
对象接收到对echoString()
函数的调用。它接收函数的参数,以及返回结果参数的回调函数:
void echoString(String value, void callback(String response)) {
print('EchoString: $value');
callback(value);
}
Dart语言的Echo
客户端
Dart中的echo客户端实现可以在以下位置找到:
topaz/examples/fidl/echo_client_dart/lib/main.dart.
我们的简单客户端在main()
中实现所有操作。
注意: 一个组件可以是客户端、服务端,或者两者都是,或者多个。在这个例子中,客户端和服务端之间的区别纯粹是为了演示目的。
下面是客户端如何连接到Echo服务的总结。
- Startup. FIDL Shell加载Dart runner,它启动虚拟机,加载
main.dart
,并调用main()
。 - Connect. 指定了目标服务器,如果其还没有启动的话请求其启动。
- Bind. 我们将生成的代理类
EchoProxy
绑定到远程Echo
服务。 - Invoke. 我们带值调用
echoString
,并且设置回调函数处理响应。 - Wait.
main()
返回,但fFIDL运行循环仍在等待来自远程通道的消息。 - Handle result. 结果到达并执行回调,打印响应。
- Shutdown.
dart_echo_client
退出。
main() 函数
客户端中的main()
函数包含所有客户端代码。
void main(List<String> args) {
String server = 'fuchsia-pkg://fuchsia.com/echo_dart#meta/echo_server_dart.cmx';
if (args.length >= 2 && args[0] == '--server') {
server = args[1];
}
_context = new StartupContext.fromStartupInfo();
final Services services = new Services();
final LaunchInfo launchInfo = new LaunchInfo(
url: server, directoryRequest: services.request());
_context.launcher.createComponent(launchInfo, null);
_echo = new EchoProxy();
_echo.ctrl.bind(services.connectToServiceByName<Echo>(Echo.$serviceName));
_echo.echoString('hello', (String response) {
print('***** Response: $response');
});
}
同样,记住FIDL中的所有都是异步的。调用_echo.echoString()
将立即返回,然后main()
返回。FIDL客户端库保留自己的指向组件对象的指针,这样可以防止组件退出。一旦响应到达,所有句柄都关闭,组件将在回调返回后终止。
运行示例
你可以这样运行Hello World示例:
$ run echo_client_dart
你不需要专门运行服务端,因为在客户端调用connectToServiceByName()
将自动要求加载服务端。
Echo
跨语言运行
作为最后一个练习,你现在可以将Echo
客户端和服务端进行混合和匹配。让我们尝试让Dart客户端调用C++服务端。
$ run echo_client_dart --server echo2_server_cpp
Dart客户端将启动C++服务器并连接到它。EchoString()
跨语言边界工作,重要的是两端都遵守FIDL定义的ABI。