FIDL指南

本文介绍了Fuchsia系统中FIDL(Fuchsia Interface Definition Language)的使用,详细讲解了如何在C++和Dart语言中实现跨语言的进程间通信(IPC),包括FIDL源代码的获取与编译、接口定义、服务端与客户端实现及示例运行。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

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调用的时候都发生了什么。

  1. Fuchsia加载服务端可执行文件,并开始执行main()函数。

  2. main()函数创建EchoServerApp对象,其在服务接口创建后,绑定到服务接口。

  3. EchoServerApp()通过调用context->outgoing().AddPublicService<Echo>()将自身注册到StartupContext。并且,传递一个lambda函数作为参数,该函数在连接请求到达时被调用。

现在,main函数开始运行循环,表示为async::Loop

  1. 循环时接收到来自其它组件的连接请求,调用在EchoServerApp()中创建的lambda函数。

  2. 此lambda函数将EchoServerApp实例绑定到请求的通道channel

  3. 循环从通道中接收到对EchoString()的调用,分派其到上一步骤中绑定的实例对象。

  4. 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服务的总结。

  1. shell加载客户端可执行文件并调用main
  2. main创建一个EchoClientApp对象来处理连接到服务端,调用Start()开启连接,然后进入消息循环。
  3. Start()函数中,客户端携带服务端组件的URL调用context_->launcher()->CreateComponent。如果服务端组件不在运行,此时将创建它。
  4. 接下来,客户端调用ConnectToService()打开到服务端组件的通道。
  5. main 调用echo_->EchoString(...),并传递回调函数。因为FIDL IPC调用是异步的,EchoString()可能会在服务器处理呼叫前返回。
  6. 随后main阻塞,等待接口的响应。
  7. 最终,响应到达,回调函数执行,带有结果。

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服务的总结。

  1. Startup. FIDL Shell加载Dart runner,它启动虚拟机,加载main.dart,并调用main()
  2. Connect. 指定了目标服务器,如果其还没有启动的话请求其启动。
  3. Bind. 我们将生成的代理类EchoProxy绑定到远程Echo服务。
  4. Invoke. 我们带值调用echoString,并且设置回调函数处理响应。
  5. Wait. main()返回,但fFIDL运行循环仍在等待来自远程通道的消息。
  6. Handle result. 结果到达并执行回调,打印响应。
  7. 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。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值