驱动程序通信

在 Fuchsia 中,所有通信都通过功能(例如协议和服务)进行。组件框架负责处理组件(包括驱动程序和非驱动程序)之间的功能路由。功能会添加到某个组件的出站目录中,如果正确路由,则可在另一个组件的入站命名空间中使用。

司机主要通过服务功能(本文档为简洁起见,将简称为“服务”)与其他司机和非司机进行通信。涉及驱动程序的服务通信与非驱动程序之间的服务通信只有两点不同,而且都涉及服务的路由方式。

  1. 驱动程序到驱动程序的路由使用动态功能路由,其中驱动程序管理器在子驱动程序的组件创建期间创建从父节点到子节点的路由。
  2. 源自驱动程序并传递给非驱动程序的服务是汇总的,因为所有驱动程序都位于集合中。这意味着,驱动程序公开的所有服务实例都将聚合到以服务名称命名的同一目录中

服务详解

服务实例表示包含一个或多个协议的目录。所包含的协议称为服务成员。服务成员有自己的类型,然后可以在 ServiceMemberWatcher 等工具中使用该类型来指示服务以及其中的特定协议。

请考虑以下 FIDL 定义:

library fuchsia.example;

protocol Echo { ... }
protocol Broadcast { ... }

// Note: most services only contain one protocol.
service EchoService {
    speaker client_end:Broadcast;
    loopback client_end:Echo;
};

以下是对示例中的值的描述:

  • fuchsia.example.EchoService 是服务功能
    • 此属性用于 .cml 文件中,是服务目录的名称
  • fuchsia_example::EchoService 是服务的 C++ 类型
  • fuchsia_example::EchoService::Speaker服务成员的 C++ 类型
    • 此类型实际上仅供 ServiceMemberWatcher 等工具使用。
    • 此服务成员将以 speaker 的形式显示在 <instance_name> 目录中
    • 连接到 /svc/fuchsia.example.EchoService/<instance_name>/speaker 将为您提供一个需要 fuchsia_example::Broadcast 协议的渠道。

驱动因素:宣传服务

为每种协议创建服务器实现

对于服务中的每种协议,您都必须有一个 fidl 协议的服务器实现 - 一个从 fidl::WireServerfidl::Server 继承的类。如果您的服务有多个具有相同协议的成员,您可以为每个协议使用相同的服务器实现。

创建 ServiceInstanceHandler

接下来,您需要创建一个 ServiceInstanceHandler:一组在客户端连接到服务协议时调用的函数。幸运的是,fidl::ServerBindingGroup 使此操作变得非常简单。

将绑定组添加到服务器类:

fidl::ServerBindingGroup<fuchsia_examples::Echo> loopback_bindings_;
fidl::ServerBindingGroup<fuchsia_examples::Broadcast> speaker_bindings_;

然后,您可以创建 ServiceInstanceHandler。在此示例中,this 指向上一步中确定的服务实例。

  fuchsia_examples::EchoService::InstanceHandler handler({
      .loopback = loopback_bindings_.CreateHandler(this, dispatcher(), fidl::kIgnoreBindingClosure),
      .speaker = speaker_bindings_.CreateHandler(this, dispatcher(), fidl::kIgnoreBindingClosure),
  });

请注意,您需要为服务中的每个协议单独进行 ServerBindingGroup 或至少 CreateHandler 调用。(大多数服务只有一个协议。)

在 DFv1 和 DFv2 中,宣传服务的方式略有不同:

DFv1

zx::result add_result =
      DdkAddService<fuchsia_examples::EchoService>(std::move(handler));

DFv2

  zx::result add_result =
      outgoing()->AddService<fuchsia_examples::EchoService>(std::move(handler));

从驾驶员到驾驶员:在创建子账号时添加优惠

如果您刚刚宣传的服务应路由到子级,则需要在创建子级时将其添加到您传递的优惠中。在此示例中:

  // Add a child with a `fuchsia_examples::EchoService` offer.
  std::vector<fuchsia_driver_framework::NodeProperty2> properties = {};
  zx::result child_result = AddChild("my_child_name", properties,
      std::array{fdf::MakeOffer2<fuchsia_examples::EchoService>()});

这会指示驾驶员管理器将该服务从您路由到您的孩子。由于实例名称不会随机化,建议将实例名称指定为子组件的名称,作为 AddService 的实参。 在 DFv1 中无法实现。如需详细了解如何将服务路由到子级,请参阅 DFv2 服务迁移指南

路由服务

现在,您的服务已进行广播,需要从驱动程序中进行 expose,并由客户端进行 use。如果客户端是子驱动程序,则无需进行额外的路由。如果客户端是非驱动程序组件,则必须将服务 expose 到客户端和驱动程序共享的祖先节点(通常是 #core 组件),然后 offer 到客户端。

从驱动程序公开服务

您必须将服务添加到驱动程序的 CML 文件中,同时添加到 CapabilityExpose 字段。功能 stanza 定义了一项功能,而 expose 指定了该功能的来源。

capabilities: [
    { service: "fuchsia.examples.EchoService" },
],
expose: [
    {
        service: "fuchsia.examples.EchoService",
        from: "self",
    },
],

路由服务

在驱动程序和组件之间,将服务添加到 offerexpose 字段。您很可能需要修改多个 cml 文件。请参阅以下示例,了解其他司机如何规划服务路线。记下您的服务需要访问哪个领域。例如,对于 bootstrap 领域中的组件,您的服务必须来自 #boot-drivers#base-drivers

{
      service: "fuchsia.example.EchoService",
      from: "parent",
      to: "#my_client",
},

如需了解其他司机如何规划服务路线,请参阅以下示例:

如需详细了解路由功能,请参阅连接组件页面。如果您遇到问题,问题排查部分以及 Devfs 迁移指南调试部分可能会对您有所帮助。

Use 客户端中的服务

在客户端中,将服务添加到“使用”列表中:

use: [
    { service: "fuchsia.example.EchoService", },
 ],

以客户端身份连接到服务

服务只是一个包含协议的目录,由组件框架通过服务名称在 /svc/ 目录中提供。因此,您可以连接到服务在以下位置提供的协议:

/svc/<ServiceName>/<instance_name>/<ServiceMemberName>

对于第 1 步中的示例,此值应为:

/svc/fuchsia.example.EchoService/<instance_name>/loopback
   and
/svc/fuchsia.example.EchoService/<instance_name>/speaker

实例名称是随机生成的。

如需连接到服务成员,建议的方法是监控服务目录,等待实例显示。有多种工具可以帮助您完成此任务,但对于使用单一协议的服务,建议使用 ServiceMemberWatcher(适用于 C++)和 Service(适用于 Rust)。

同步 C++

SyncServiceMemberWatcher<fuchsia_examples::EchoService::Loopback> watcher;
zx::result<ClientEnd<fuchsia_examples::Echo>> result = watcher.GetNextInstance(true);

异步 C++

#include <lib/component/incoming/cpp/service_watcher.h>
using component::SyncServiceMemberWatcher;
// Define a callback function:
void OnInstanceFound(ClientEnd<fuchsia_examples::Echo> client_end) {...}
// Optionally define an idle function, which will be called when all
// existing instances have been enumerated:
void AllExistingEnumerated() {...}
// Create the ServiceMemberWatcher:
ServiceMemberWatcher<fuchsia_examples::EchoService::Loopback> watcher;
watcher.Begin(get_default_dispatcher(), &OnInstanceFound, &AllExistingEnumerated);
// If you want to stop watching for new service entries:
watcher.Cancel()

Rust

  use fuchsia_component::client::Service;
  let device = Service::open(fuchsia_examples::EchoServiceMarker)
      .context("Failed to open service")?
      .watch_for_any()
      .await
      .context("Failed to find instance")?
      .connect_to_device()
      .context("Failed to connect to device protocol")?;

您现在应该可以访问司机服务了。

附录

将服务与 DriverTestRealm 搭配使用

使用 DriverTestRealm 的测试客户端可以按照以下步骤将被测驱动程序的服务功能路由到测试代码。

  1. 在选项中添加 driver_exposes,以设置驱动程序测试 realm:

    C++

    async::Loop loop(&kAsyncLoopConfigNeverAttachToThread);
    std::vector<fuchsia_component_test::Capability> exposes = { {
        fuchsia_component_test::Capability::WithService(
            fuchsia_component_test::Service{ {.name = fuchsia_examples::EchoService::Name}}),
    }};
    auto realm_builder = component_testing::RealmBuilder::Create();
    driver_test_realm::Setup(
        realm_builder,
        loop.dispatcher(),
        driver_test_realm::OptionsBuilder().driver_exposes(exposes).Build(),
        {});
    
    auto realm = realm_builder.Build(loop.dispatcher());
    

    Rust

    let echo_capability = fuchsia_component_test::Capability::service::<fuchsia_examples::EchoServiceMarker>().into();
    let exposed_capabilities = vec![echo_capability];
    
    // Create the RealmBuilder.
    let builder = RealmBuilder::new().await?;
    builder.driver_test_realm_setup(Options::new().driver_exposes(exposed_capabilities), {}).await?;
    
    // Build the Realm.
    let realm = builder.build().await?;
    
  2. 连接到 realm 的 exposed() 目录以等待服务实例:

    C++

    fidl::UnownedClientEnd<fuchsia_io::Directory> svc_dir{
        realm.component().exposed().unowned_channel()->get()};
    component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::Device> watcher(
        svc_dir);
    // Wait indefinitely until a service instance appears in the service directory
    zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> echo_client =
        watcher.GetNextInstance(false);
    

    Rust

    use fuchsia_component::client::Service;
    // Connect to the `Device` service.
    let device = Service::open_from_dir(realm.root.get_exposed_dir(), fuchsia_examples::EchoServiceMarker)
        .context("Failed to open service")?
        .watch_for_any()
        .await
        .context("Failed to find instance")?
        .connect_to_device()
        .context("Failed to connect to device protocol")?;
    

示例:本部分中的代码来自以下 CL:

使用 devfs 的旧版驱动程序通信

驱动程序管理器托管着一个名为 devfs(表示“设备文件系统”)的虚拟文件系统。此虚拟文件系统可为 Fuchsia 的用户空间服务(即驱动程序外部的组件)提供对 Fuchsia 系统中所有驱动程序服务的统一访问权限。这些非驱动程序组件通过在 devfs 中发现目标驱动程序的服务来与驱动程序建立初始联系。

严格来说,devfs 是驱动程序管理器公开的目录功能。因此,按照惯例,希望访问驱动程序的组件会在其命名空间的 /dev 目录下挂载 devfs(尽管并非强制要求 devfs 始终挂载在 /dev 下)。

devfs 托管虚拟文件,使 Fuchsia 组件能够将消息路由到 Fuchsia 系统中运行的驱动程序所实现的接口。换句话说,当客户端(即非驱动程序组件)打开 /dev 目录下的文件时,它会收到一个可用于直接向映射到该文件的驱动程序发出 FIDL 调用的通道。例如,Fuchsia 组件可以通过打开并写入类似于 /dev/class/input-report/000 的文件来连接到输入设备。在这种情况下,客户端可能会收到一个使用 fuchsia.input.report FIDL 的渠道。

驱动程序可以在添加新节点时使用 DevfsAddArgs 表将其自身导出到 devfs

在非司机与司机之间的通信中,会发生以下事件:

  1. 为了发现系统中的驱动程序服务,非驱动程序组件会扫描 devfs 中的目录和文件。
  2. 非驱动程序组件在 devfs 中找到一个表示目标驱动程序所提供服务的文件。
  3. 非驱动程序组件会打开此文件,从而与目标驱动程序建立连接。
  4. 在初始联系之后,非驱动程序组件和驱动程序之间会建立 FIDL 连接。
  5. 从这一刻起,所有通信都通过 FIDL 渠道进行。