驱动程序通信

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

驾驶员主要使用服务功能(本文档为简洁起见将其称为“服务”)与其他驾驶员和非驾驶员进行通信。涉及驱动程序的服务通信与非驱动程序之间的服务通信之间只有两个区别,这涉及服务的路由方式。

  1. 驱动程序到驱动程序路由使用动态 capability 路由,其中驱动程序管理器会在子驱动程序的组件创建期间创建从父节点到子节点的路由。
  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 是服务 capability
    • 此值在 .cml 文件中使用,是服务目录的名称
  • fuchsia_example::EchoService 是服务的 C++ 类型
  • fuchsia_example::EchoService::Speaker服务成员的 C++ 类型
    • 此类型实际上仅供 ServiceMemberWatcher 等工具使用。
    • 此服务成员将在 <instance_name> 目录中显示为 speaker
    • 连接到 /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>()});

这会指示 Driver Manager 将该服务从您路由到您的子账号。由于实例名称不会随机化,因此建议将实例名称作为子组件名称指定为 AddService 的参数。在 DFv1 中无法实现此操作。如需详细了解如何将服务路由到子服务,请参阅适用于服务的 DFv2 迁移指南

路由服务

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

从驱动程序公开服务

您必须将该服务添加到驱动程序的 cml 文件的 CapabilityExpose 字段中。capabilities 节定义了 capability,而 expose 指定了 capability 的来源。

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

实例名称是随机生成的。

如需连接到服务成员,建议的方法是监控服务目录,以便实例显示。有各种工具可以帮助您实现这一点,但对于使用单一协议的服务,建议为 C++ 使用 ServiceMemberWatcher,为 Rust 使用 Service

同步 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(fidl_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. 在调用 realm.Build() 之前,您需要调用 AddDtrExposes

    C++

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

    Rust

    // Create the RealmBuilder.
    let builder = RealmBuilder::new().await?;
    builder.driver_test_realm_setup().await?;
    
    let expose = fuchsia_component_test::Capability::service::<ft::DeviceMarker>().into();
    let dtr_exposes = vec![expose];
    
    builder.driver_test_realm_add_dtr_exposes(&dtr_exposes).await?;
    // Build the Realm.
    let realm = builder.build().await?;
    
  2. 将公开内容添加到 Realm 启动参数中:

    C++

    auto realm_args = fuchsia_driver_test::RealmArgs();
    realm_args.root_driver("fuchsia-boot:///dtr#meta/root_driver.cm");
    realm_args.dtr_exposes(exposes);
    fidl::Result result = fidl::Call(*client)->Start(std::move(realm_args));
    

    Rust

    // Start the DriverTestRealm.
    let args = fdt::RealmArgs {
        root_driver: Some("#meta/v1_driver.cm".to_string()),
        dtr_exposes: Some(dtr_exposes),
        ..Default::default()
    };
    realm.driver_test_realm_start(args).await?;
    
  3. 连接到 realm 的 exposed() 目录,等待服务实例:

    C++

    fidl::UnownedClientEnd<fuchsia_io::Directory> svc = launcher.GetExposedDir();
    component::SyncServiceMemberWatcher<fuchsia_examples::EchoService::MyDevice> watcher(
        svc);
    // Wait indefinitely until a service instance appears in the service directory
    zx::result<fidl::ClientEnd<fuchsia_examples::Echo>> peripheral =
        watcher.GetNextInstance(false);
    

    Rust

    // Connect to the `Device` service.
    let device = client::Service::open_from_dir(realm.root.get_exposed_dir(), ft::DeviceMarker)
        .context("Failed to open service")?
        .watch_for_any()
        .await
        .context("Failed to find instance")?;
    // Use the `ControlPlane` protocol from the `Device` service.
    let control = device.connect_to_control()?;
    control.control_do().await?;
    

示例:本部分中的代码来自以下 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 通道进行。