将驱动程序接口更新为 DFv2

本页面提供了有关如何更新 DFv1 驱动程序以开始使用 DFv2 接口的说明、最佳实践和示例。

将依赖项从 DDK 更新为 DFv2

DFv1 驱动程序使用 DDK 库 (//src/lib/ddk)。对于 DFv2 驱动程序,您可以安全地移除此 DDK 库目录下的所有软件包依赖项,并将其替换为以下新库,其中包含 DFv2 驱动程序的大多数基本实用程序:

//sdk/lib/driver/component/cpp:cpp

在头文件中,为新 DFv2 驱动程序接口添加以下库:

#include <lib/driver/component/cpp/driver_base.h>

将依赖项从 DDK 更新为 DFv2 后,驱动程序将无法编译,直到您完成下一部分中的将驱动程序接口从 DFv1 更新为 DFv2 部分。

将驱动程序接口从 DFv1 更新为 DFv2

DFv2 提供了一个名为 DriverBase 的虚拟类,该类封装了驱动程序的常规例程。对于 DFv2 驱动程序,建议在新驱动程序类中继承 DriverBase,以大幅简化接口。

例如,假设您的 DFv1 驱动程序中有以下类:

class MyExampleDriver;

using MyExampleDeviceType = ddk::Device<MyExampleDriver, ddk::Initializable,
   ddk::Unbindable>;

class MyExampleDriver : public MyExampleDeviceType {
 public:
  void DdkInit(ddk::InitTxn txn);
  void DdkUnbind(ddk::UnbindTxn txn);
  void DdkRelease();
}

如果您更新该接口以从 DriverBase 继承,新类可能如下所示:

class MyExampleDriver : public fdf::DriverBase {
  public:
    Driver(fdf::DriverStartArgs start_args,
        fdf::UnownedSynchronizedDispatcher driver_dispatcher);

    zx::result<> Start() override;
    void Stop() override;
}

除了启动和停止驱动程序之外(如上例所示),DriverBase 类还提供使驱动程序能够与其他组件(和驱动程序)进行通信的对象。例如,您的驱动程序现在可以调用 DriverBase 类的 outgoing() 方法来检索传出目录,而不是在 DFv1 中声明和创建您自己的传出目录(如从 Banjo 迁移到 FIDL 阶段):

class DriverBase {
   ...
   // Used to access the outgoing directory that the driver is serving. Can be used to add both
   // zircon and driver transport outgoing services.
   std::shared_ptr<OutgoingDirectory>& outgoing() { return outgoing_; }

   ...
}

(来源:driver_base.h

DFv2 中另一个实用的类是 Node 类。以下示例展示了连接到 Node 服务器并使用 AddChild() 函数添加子节点的 DFv2 驱动程序代码:

zx::result<> MyExampleDriver::Start() {
  fidl::WireSyncClient<fuchsia_driver_framework::Node> node(std::move(node()));
  auto args = fuchsia_driver_framework::wire::NodeAddArgs::Builder(arena)
                    .name(arena, “example_node”)
                    .Build();

  auto controller_endpoints = fidl::Endpoints<fuchsia_driver_framework::NodeController>::Create();

  auto result = node_->AddChild(args, std::move(controller_endpoints.server), {});
  if (!result.ok()) {
    FDF_LOG(ERROR, "Failed to add child: %s", result.status_string());
    return zx::error(result.status());
  }
  return zx::ok();
}

传递给 AddChild() 函数的 NodeController 端点(上例中的 controller_endpoints)可用于控制子节点。例如,此端点可用于从节点拓扑中移除子节点、请求驱动程序框架将节点绑定到特定驱动程序,或在子节点绑定时接收回调。以下示例展示了用于在关闭过程中移除子节点的 DFv2 驱动程序代码:

void MyExampleDriver::Stop() {
  // controller_endpoints defined in the previous example.
  fidl::WireSyncClient<fuchsia_driver_framework::NodeController>
       node_controller(controller_endpoints.client);

  auto status = node_controller->Remove();
  if (!status.ok()) {
    FDF_LOG(ERROR, "Could not remove child: %s", status.status_string());
  }
}

此外,在 DFv2 中,OnBind() 事件是在 NodeController 协议中定义的,该协议由服务器端的子节点调用。DriverBase::PrepareStop() 函数让您有机会在调用 DriverBase::Stop() 之前执行去初始化操作。

下表显示了 DFv1 和 DFv2 之间通用驱动程序和设备接口的对应关系:

DFv1 DFv2
zx_driver_ops::bind() DriverBase::Start()
zx_driver_ops::init() DriverBase::Start()
zx_driver_ops::release() DriverBase::Stop()
device_add()

DdkAdd()
Node::AddChild()
device_add_composite()

DdkAddComposite()
。使用规范添加复合节点。请参阅复合节点
device_add_composite_node_spec()

DdkAddCompositeNodeSpec()
CompositeNodeManager::AddSpec()
zx_protocol_device::init()

DdkInit()
。在 DFv2 中,驱动程序负责控制其添加的节点(设备)的生命周期。
zx_protocol_device::unbind()

DdkUnbind()
。在 DFv2 中,驱动程序负责控制其添加的节点(设备)的生命周期。
zx_protocol_device::release()

DdkRelease()
NodeController::Remove()
zx_protocol_device::get_protocol()

device_get_protocol()
。这些方法基于 Banjo 协议。在 DFv2 中,所有通信均采用 FIDL。
zx_protocol_device::service_connect()

device_service_connect()

DdkServiceConnect()
。这是驱动程序之间建立 FIDL 连接的老式方法。如需了解详情,请参阅使用 DFv2 服务发现
Device_connect_runtime_protocol()

DdkConnectRuntimeProtocol()
。这些是 DFv1 中用于发现服务和协议的新方法。如需了解详情,请参阅使用 DFv2 服务发现

使用 DFv2 日志记录器

DFv2 中的新日志记录机制依赖于 fdf::Logger 对象,该对象是在启动驱动程序时通过 DriverStartArgs 从驱动程序主机传递的,而不是使用 zxlogf()(DFv2 中已废弃)。

fdf::DriverBase 类封装了 fdf::Logger,而驱动程序可以通过调用 logger() 方法获取其引用(请参阅此 wlantap-driver 驱动程序示例)。借助此参考文档,您可以使用 logger.logf() 函数或使用这些输出日志,例如:

FDF_LOG(INFO, "Example log message here");

更新宏

除了更新接口之外,您还需要更改用于填充驱动程序接口函数的宏:

  • From:

    ZIRCON_DRIVER()
    
  • 改后:

    FUCHSIA_DRIVER_EXPORT()
    

设置 compat 设备服务器

如果您的 DFv1 驱动程序与尚未迁移到 DFv2 的其他 DFv1 驱动程序进行通信,则您需要使用兼容性 shim 才能让现在的 DFv2 驱动程序与系统中的其他 DFv1 驱动程序进行通信。如需详细了解如何在 DFv2 驱动程序中设置和使用此兼容性填充码,请参阅在 DFv2 驱动程序中设置兼容型设备服务器指南。

使用 DFv2 服务发现

在进行驱动程序迁移时,您可能会遇到以下三种情形中的一种或多种,其中两个驱动程序会建立 FIDL 连接(采用 child driver -> parent driver 格式):

  • 场景 1:DFv2 驱动程序 -> DFv2 驱动程序
  • 场景 2:DFv1 驱动程序 -> DFv2 驱动程序
  • 场景 3:DFv2 驱动程序 -> DFv1 驱动程序

场景 1 是 DFv2 驱动程序的标准场景(此示例展示了新的 DFv2 语法)。在这种情况下,如需更新驱动程序,请参阅下面的从 DFv2 驱动程序升级到 DFv2 驱动程序部分。

场景 2 和场景 3 更为复杂,因为 DFv1 驱动程序封装在 DFv2 环境中的兼容性填充码中。但是,区别如下:

  • 场景 2 中,此 Gerrit 更改展示了一种将 DFv2 父级中的服务公开给 DFv1 子级的方法。

  • 场景 3 中,驱动程序连接到父驱动程序的兼容性 shim 提供的 fuchsia_driver_compat::Service::Device 协议,驱动程序通过此协议调用 ConnectFidl() 方法以连接到实际协议(例如,查看此 Gerrit 更改)。

如需在场景 2 或 3 下更新您的驱动程序,请参阅下面的从 DFv1 驱动程序到 DFv2 驱动程序(包含兼容性 shim)部分。

从 DFv2 驱动程序转换为 DFv2 驱动程序

如需让其他 DFv2 驱动程序发现您的驱动程序的服务,请执行以下操作:

  1. 更新驱动程序的 .fidl 文件。

    DFv2 中的协议发现需要为驱动程序的协议添加 service 字段,例如:

    library fuchsia.example;
    
    @discoverable
    @transport("Driver")
    protocol MyProtocol {
        MyMethod() -> (struct {
            ...
        });
    };
    
    service Service {
        my_protocol client_end:MyProtocol;
    };
    
  2. 更新子驱动程序。

    DFv2 驱动程序能够以与 FIDL 服务相同的方式连接到协议,例如:

    incoming()->Connect<fuchsia_example::Service::MyProtocol>();
    

    您还需要更新组件清单 (.cml) 文件才能使用驱动程序运行时服务,例如:

    use: [
        { service: "fuchsia.example.Service" },
    ]
    
  3. 更新父级驱动程序。

    您的父级驱动程序需要使用 fdf::DriverBaseoutgoing() 函数来获取 fdf::OutgoingDirectory 对象。请注意,您必须使用服务而不是协议。 如果您的司机未使用 fdf::DriverBase,您必须自行创建和提供 fdf::OutgoingDirectory

    然后,您需要将运行时服务添加到传出目录。以下示例是一个继承自 fdf::DriverBase 类的驱动程序:

    zx::status<> Start() override {
      auto protocol = [this](
          fdf::ServerEnd<fuchsia_example::MyProtocol> server_end) mutable {
        // bindings_ is a class field with type fdf::ServerBindingGroup<fuchsia_example::MyProtocol>
        bindings_.AddBinding(
          dispatcher()->get(), std::move(server_end), this, fidl::kIgnoreBindingClosure);
      };
    
      fuchsia_example::Service::InstanceHandler handler(
           {.my_protocol = std::move(protocol)});
    
      auto status =
            outgoing()->AddService<fuchsia_wlan_phyimpl::Service>(std::move(handler));
      if (status.is_error()) {
        return status.take_error();
      }
    
      return zx::ok();
    }
    

    更新子节点的 NodeAddArgs 以包含运行时服务的优惠,例如:

    auto offers =
        std::vector{fdf::MakeOffer2<fuchsia_example::Service>(arena, name)};
    
    fidl::WireSyncClient<fuchsia_driver_framework::Node> node(std::move(node()));
      auto args = fuchsia_driver_framework::wire::NodeAddArgs::Builder(arena)
                        .name(arena, “example_node”)
                        .offers2(offers)
                        .Build();
    
      zx::result controller_endpoints =
           fidl::CreateEndpoints<fuchsia_driver_framework::NodeController>();
      ZX_ASSERT(controller_endpoints.is_ok());
    
      auto result = node_->AddChild(
          args, std::move(controller_endpoints->server), {});
    

    同样,请更新父驱动程序的组件清单 (.cml) 文件以提供运行时服务,例如:

    capabilities: [
        { service: "fuchsia.example.Service" },
    ],
    
    expose: [
        {
            service: "fuchsia.example.Service",
            from: "self",
        },
    ],
    

从 DFv1 驱动程序转换为 DFv2 驱动程序(具有兼容性填充码)

如需让其他 DFv1 驱动程序发现您的 DFv2 驱动程序的服务,请执行以下操作:

  1. 更新 DFv1 驱动程序。

    您需要按照上面 DFv2 驱动程序到 DFv2 驱动程序部分中所述的方式更新 DFv1 驱动程序的组件清单 (.cml) 文件,例如:

    • 子驱动程序:

      {
          include: [
              "//sdk/lib/driver_compat/compat.shard.cml",
              "inspect/client.shard.cml",
              "syslog/client.shard.cml",
          ],
          program: {
              runner: "driver",
              compat: "driver/child-driver-name.so",
              bind: "meta/bind/child-driver-name.bindbc",
              colocate: "true",
          },
          use: [
              { service: "fuchsia.example.Service" },
          ],
      }
      
    • 父级驱动程序:

      {
          include: [
              "//sdk/lib/driver_compat/compat.shard.cml",
              "inspect/client.shard.cml",
              "syslog/client.shard.cml",
          ],
          program: {
              runner: "driver",
              compat: "driver/parent-driver-name.so",
              bind: "meta/bind/parent-driver-name.bindbc",
          },
          capabilities: [
              { service: "fuchsia.example.Service" },
          ],
          expose: [
              {
                  service: "fuchsia.example.Service",
                  from: "self",
              },
          ],
      }
      
  2. 更新 DFv2 驱动程序。

    以下示例展示了如何将 DFv2 父级中的服务公开给 DFv1 子级:

      fit::result<fdf::NodeError> AddChild() {
        fidl::Arena arena;
    
        auto offer = fdf::MakeOffer2<ft::Service>(kChildName);
    
        // Set the properties of the node that a driver will bind to.
        auto property =
            fdf::MakeProperty(1 /*BIND_PROTOCOL */, bind_fuchsia_test::BIND_PROTOCOL_COMPAT_CHILD);
    
        auto args = fdf::NodeAddArgs{
          {
            .name = std::string(kChildName),
            .properties = std::vector{std::move(property)},
            .offers2 = std::vector{std::move(offer)},
          }
        };
    
        // Create endpoints of the `NodeController` for the node.
        auto endpoints = fidl::CreateEndpoints<fdf::NodeController>();
        if (endpoints.is_error()) {
          return fit::error(fdf::NodeError::kInternal);
        }
    
        auto add_result = node_.sync()->AddChild(fidl::ToWire(arena, std::move(args)),
                                                 std::move(endpoints->server), {});
    

    (来源:root-driver.cc

更新其他驱动程序的组件清单

为了完成从 DFv1 驱动程序到 DFv2 的迁移,您不仅需要更新目标驱动程序的组件清单 (.cml) 文件,还需要更新与现在的 DFv2 驱动程序交互的一些其他驱动程序的组件清单文件。

请执行以下操作:

  1. 使用以下更改更新叶驱动程序(即没有子驱动程序)的组件清单:

    • include 字段中移除 //sdk/lib/driver/compat/compat.shard.cml
    • program.compat 字段替换为 program.binary
  2. 更新执行以下任务的其他驱动程序的组件清单:

    • 访问内核 args
    • 创建复合设备。
    • 检测重新启动、关闭或重新绑定调用。
    • 使用 Banjo 协议与其他驾驶员交谈。
    • 访问父级驱动程序的元数据或转发元数据。
    • 告知绑定到您的驱动程序添加的节点的 DFv1 驱动程序。

    对于这些驱动程序,请通过以下更改更新其组件清单:

    • compat.shard.cml 中的部分 use 功能复制到组件清单中,例如:

      use: [
          {
              protocol: [
                  "fuchsia.boot.Arguments",
                  "fuchsia.boot.Items",
                  "fuchsia.device.manager.SystemStateTransition",
                  "fuchsia.driver.framework.CompositeNodeManager",
              ],
          },
          { service: "fuchsia.driver.compat.Service" },
      ],
      
    • program.runner 字段设置为 driver,例如:

      program: {
          runner: "driver",
          binary: "driver/compat.so",
      },
      

公开 DFv2 驱动程序中的 devfs 节点

如需公开 DFv2 驱动程序中的 devfs 节点,您需要将 device_args 成员添加到 NodeAddArgs。具体而言,它需要指定类名称并实现连接器,您可以使用 Connector 库简化该过程,例如:

zx::result connector = devfs_connector_.Bind(dispatcher());
if (connector.is_error()) {
  return connector.take_error();
}

auto devfs =
    fuchsia_driver_framework::wire::DevfsAddArgs::Builder(arena).connector(
        std::move(connector.value()));

auto args = fuchsia_driver_framework::wire::NodeAddArgs::Builder(arena)
                    .name(arena, name)
                    .devfs_args(devfs.Build())
                    .Build();

(来源:parent-driver.cc

如需了解详情,请参阅 DFv2 驱动程序 Codelab 中的公开驱动程序功能。另请参阅 Codelab 中提到的 ExportToDevfs 方法的实现

使用调度程序

调度程序从 FIDL 客户端与服务器对之间的通道提取数据。默认情况下,此通道中的 FIDL 调用是异步的。

如需在 DFv2 中为驱动程序引入异步,请参阅以下建议:

  • fdf::Dispatcher::GetCurrent() 方法可为您提供运行驱动程序的默认调度程序(请参阅此 aml-ethernet 驱动程序示例)。如果可能,建议单独使用此默认调度程序。

  • 出于以下原因(但不限于):

    • 驱动程序需要并行处理才能提升性能。

    • 驱动程序想要执行阻塞操作(因为它是旧版驱动程序或要移植到 Fuchsia 的非 Fuchsia 驱动程序),并且需要在阻塞状态下处理更多工作。

  • 如果需要多个调度程序,fdf::Dispatcher::Create() 方法可以为驱动程序创建新的调度程序。不过,您必须在默认调度程序上调用此方法(例如,在 Start() 钩子内调用),以便驱动程序主机知道属于您的驱动程序的其他调度程序。

  • 在 DFv2 中,您无需手动关闭调度程序。它们将在 PrepareStop()Stop() 调用之间关停。

如需详细了解如何迁移驱动程序以使用多个调度程序,请参阅更新 DFv1 驱动程序以使用非默认调度程序部分(在从 Banjo 迁移到 FIDL 短语)。

使用 DFv2 检查

如需在 DFv2 中设置驱动程序维护的inspect指标,您需要创建一个 inspect::ComponentInspector 对象,例如:

component_inspector_ =
    std::make_unique<inspect::ComponentInspector>(out, dispatcher, *inspector_);

(来源:driver-inspector.cc

创建 inspect::ComponentInspector 对象需要以下三个输入项:

  • 来自 Context().outgoing()->component() 调用的 component::OutgoingDirectory 对象

  • 调度程序

  • 原始 inspect::Inspector 对象

但是,DFv2 检查不需要将 inspect::Inspector 的 VMO 传递给驱动程序框架。

(可选)实现您自己的 load_ramdisk 方法

如果您的 DFv1 驱动程序调用 DDK 库中的 load_firmware() 函数,则您需要实现您自己的此函数版本,因为 DFv2 中未提供等效函数。

该函数应该很容易实现。您需要从该路径手动获取后备 VMO。如需查看示例,请参阅此 Gerrit 更改

(可选)使用根据 FIDL 服务方案生成的节点属性

DFv2 节点包含其父级通过 FIDL 服务生成的节点属性。

例如,在父驱动程序(服务器)示例中,父驱动程序添加一个名为 "parent" 的节点,其中包含 fidl.examples.EchoService 服务优惠。在 DFv2 中,绑定到此节点的驱动程序可针对该 FIDL 服务节点属性设置绑定规则,例如:

using fidl.examples.echo;

fidl.examples.echo.Echo == fidl.examples.echo.Echo.ZirconTransport;

如需了解详情,请参阅 FIDL 教程页面的生成的绑定库部分。

将单元测试更新为 DFv2

mock_ddk 库(用于在单元测试中用于测试驱动程序和设备生命周期)特定于 DFv1。新的 DFv2 测试框架(请参阅此 Gerrit 变更)会通过 TestEnvironment 类向 DFv2 驱动程序提供模拟的 FIDL 服务器。

以下库可用于对 DFv2 驱动程序进行单元测试:

  • //sdk/lib/driver/testing/cpp

    • TestNode - 此类实现 fuchsia_driver_framework::Node 协议,可提供给驱动程序以创建子节点。测试也使用此类来访问驱动程序创建的子节点。

    • TestEnvironment - OutgoingDirectory 对象的封装容器,用作被测驱动程序的传入命名空间的后备 VFS(虚拟文件系统)。

    • DriverUnderTest - 此类是受测驱动程序的 RAII(资源获取即初始化)封装容器。

    • DriverRuntime - 此类是托管驱动程序运行时线程池的 RAII 封装容器。

  • //sdk/lib/driver/testing/cpp/driver_runtime.h

    • TestSynchronizedDispatcher - 此类是驱动程序调度程序的 RAII 封装容器。

以下库可能有助于编写驱动程序单元测试:

最后,以下示例单元测试涵盖不同的配置和测试用例:

其他资源

部分 DFv2 驱动程序示例:

本部分中提到的所有 Gerrit 更改

本部分中提到的所有源代码文件

本部分中提及的所有文档页面