将 Banjo 协议转换为 FIDL 协议

本页提供了有关在从 DFv1 到 DFv2 驱动程序迁移过程中将 Banjo 协议转换为 FIDL 协议的说明、最佳实践和示例。

将 DFv1 驱动程序从 Banjo 更新为 FIDL

更新驱动程序的 .fidl 文件是迁移驱动程序的一个很好的起点,因为所有内容都源自 .fidl 文件。幸运的是,Banjo 和 FIDL 通过同一个 IDL(即 FIDL)生成代码,因此您可能不需要对现有的 .fidl 文件进行重大更改。

以下示例 .fidl 文件显示了从 Banjo 到 FIDL 迁移前后的更改:

如需将 DFv1 驱动程序从 Banjo 更新为 FIDL,请进行以下更改(主要是在驱动程序的 .fidl 文件中进行更改):

  1. 在定义协议之前更新属性
  2. 更新函数定义以使用 FIDL 错误语法
  3. 在 BUILD.gn 中更新 FIDL 目标
  4. 将 FIDL 文件移动到 SDK/FIDL 目录

1. 在定义协议之前更新属性

如需使用 Banjo,.fidl 文件中的 @transport("Banjo") 属性是必需的。不过,FIDL 不需要此属性(因为 FIDL 是默认值)。因此,您可以从驱动程序的 .fidl 文件中删除 @transport("Banjo") 属性,例如:

  • 使用 Banjo:

    @transport("Banjo")
    @banjo_layout("ddk-protocol")
    protocol MyExampleProtocol {
     ...
    }
    
  • 如需使用 FIDL,请执行以下操作:

    @discoverable
    @transport("Driver")
    protocol MyExampleProtocol {
     ...
    }
    

    在上面的示例中,@discoverable 属性对于所有 FIDL 协议都是必需属性。此属性允许客户端使用其生成的名称搜索此协议。

    不过,对于使用驱动程序运行时 FIDL 的驱动程序,@transport("Driver") 属性(表示这是驱动程序传输协议)是可选的。只有当驱动程序与位于同一驱动程序主机中的其他驱动程序进行通信时,才需要迁移驱动程序运行时。

    如需查看有关驱动程序传输协议的更多示例,请参阅此驱动程序传输示例目录。

2. 更新函数定义以使用 FIDL 错误语法

如果 .fidl 文件中的某些函数定义包含返回状态,您需要将其更新为使用 FIDL 错误语法,而不是在返回结构中添加状态,例如:

  • 从使用退货结构开始:

    protocol MyExampleProtocol {
       MyExampleFunction() -> (struct {
           Error zx.status;
       });
    };
    

    (来源:wlanphy-impl.fidl

  • 如需返回 FIDL 错误语法,请执行以下操作:

    protocol MyExampleProtocol {
       BMyExampleFunction() -> () error zx.status;
    };
    

    (来源:phyimpl.fidl

使用 FIDL 错误语法具有以下优势:

  • 返回结构现在可以侧重于需要发送到服务器端的数据。

  • 服务器端的错误处理更加简洁,因为提取状态不需要读入返回结构。

3. 更新 BUILD.gn 中的 FIDL 目标

修改(此 .fidl 文件的)BUILD.gn 文件,以在 FIDL 目标中添加以下代码行:

contains_drivers = true

以下示例展示了 FIDL 目标中的 contains_drivers = true 行:

import("//build/fidl/fidl.gni")

fidl("fuchsia.wlan.phyimpl") {
 sdk_category = "partner"
 sources = [ "phyimpl.fidl" ]
 public_deps = [
   "//sdk/fidl/fuchsia.wlan.common",
   "//sdk/fidl/fuchsia.wlan.ieee80211",
   "//zircon/vdso/zx",
 ]
 contains_drivers = true
 enable_banjo = true
}

(来源:BUILD.gn

如果没有 contains_drivers = true 行,系统就无法将具有 @transport("Driver") 属性的协议正确生成到 FIDL 库中。

4. 将 FIDL 文件移至 SDK/FIDL 目录

将更新后的 .fidl 文件从 sdk/banjo 目录移至 sdk/fidl 目录。

sdk/fidl 目录是用于存储生成 FIDL 代码的所有文件的默认位置。但是,如果 Banjo 结构或函数仍保留在其他位置(即驱动程序通信之外),则不允许移动此 .fidl 文件。在这种情况下,您可以在 sdk/banjo 目录中保留此 .fidl 文件的副本。

(可选)更新 DFv1 驱动程序以使用驱动程序运行时

本部分要求上一部分中更新后的 .fidl 文件能够成功生成 FIDL 代码。此 FIDL 代码用于在驱动程序之间建立驱动程序运行时 FIDL 通信。

要更新 DFv1 驱动程序以使用驱动程序运行时,请执行以下步骤:

  1. 更新驱动程序运行时的依赖项
  2. 为驱动程序运行时 FIDL 设置客户端和服务器对象
  3. 更新驱动程序以使用驱动程序运行时 FIDL
  4. 发出 FIDL 请求

1. 更新驱动程序运行时的依赖项

更新服务器和客户端端以包含用于使用驱动程序运行时的新依赖项:

  1. BUILD.gn 文件中,更新依赖项字段以包含以下代码行:

    //sdk/fidl/<YOUR_FIDL_LIB>_cpp_wire
    //sdk/fidl/<YOUR_FIDL_LIB>_cpp_driver
    //src/devices/lib/driver:driver_runtime
    

    YOUR_FIDL_LIB 替换为您的 FIDL 库的名称,例如:

    public_deps = [
      ...
      "//sdk/fidl/fuchsia.factory.wlan:fuchsia.factory.wlan_cpp_wire",
      "//sdk/fidl/fuchsia.wlan.fullmac:fuchsia.wlan.fullmac_cpp_driver",
      "//sdk/fidl/fuchsia.wlan.phyimpl:fuchsia.wlan.phyimpl_cpp_driver",
      ...
      "//src/devices/lib/driver:driver_runtime",
      ...
    ]
    

    (来源:BUILD.gn

  2. 在源代码的头文件中,更新 include 行,例如:

    #include <fidl/<YOUR_FIDL_LIB>/cpp/driver/wire.h>
    #include <lib/fdf/cpp/arena.h>
    #include <lib/fdf/cpp/channel.h>
    #include <lib/fdf/cpp/channel_read.h>
    #include <lib/fdf/cpp/dispatcher.h>
    #include <lib/fidl/cpp/wire/connect_service.h>
    #include <lib/fidl/cpp/wire/vector_view.h>
    ...
    

    (来源:wlanphy-impl-device.h

2. 为驱动程序运行时 FIDL 设置客户端和服务器对象

更新服务器和客户端端以使用驱动程序运行时 FIDL。

在客户端上,执行以下操作:

  1. 在设备类中声明一个 FIDL 客户端对象(fdf::WireSharedClient<ProtocolName>fdf::WireSyncClient<ProtocolName>)。

    通过此 FIDL 客户端对象,您可以发出向服务器端发送请求的 FIDL 调用。

    以下示例代码显示了设备类中的一个 FIDL 客户端对象:

    class Device : public fidl::WireServer<fuchsia_wlan_device::Phy>,
                   public ::ddk::Device<Device, ::ddk::MessageableManual, ::ddk::Unbindable> {
     ...
     private:
      // Dispatcher for being a FIDL server listening MLME requests.
      async_dispatcher_t* server_dispatcher_;
    
      // The FIDL client to communicate with iwlwifi
      fdf::WireSharedClient<fuchsia_wlan_wlanphyimpl::WlanphyImpl> client_;
     ...
    

    (来源:device_dfv2.h

  2. 仅适用于异步调用)在设备类中声明调度程序对象 (fdf::Dispatcher)。

    需要一个调度程序来绑定第 1 步中的 FIDL 客户端对象。

    以下示例代码显示了设备类中的 FIDL 客户端和调度程序对象:

    class Device : public fidl::WireServer<fuchsia_wlan_device::Phy>,
                   public ::ddk::Device<Device, ::ddk::MessageableManual, ::ddk::Unbindable> {
     ...
     private:
      // Dispatcher for being a FIDL server listening MLME requests.
      async_dispatcher_t* server_dispatcher_;
    
      // The FIDL client to communicate with iwlwifi
      fdf::WireSharedClient<fuchsia_wlan_wlanphyimpl::WlanphyImpl> client_;
    
      // Dispatcher for being a FIDL client firing requests to WlanphyImpl device.
      fdf::Dispatcher client_dispatcher_;
     ...
    

    (来源:device_dfv2.h

    您可以使用 fdf::Dispatcher::GetCurrent() 方法检索驱动程序的默认调度程序,或创建新的非默认调度程序(请参阅更新 DFv1 驱动程序以使用非默认调度程序)。

在服务器端,执行以下操作:

  1. 从设备类继承 FIDL 服务器类 (fdf::WireServer<ProtocolName>)。

  2. 声明一个 FIDL 服务器绑定到的调度程序对象 (fdf::Dispatcher)。

    与客户端不同,服务器端始终需要一个调度程序来绑定 FIDL 服务器对象。

    以下示例代码显示了设备类中的 FIDL 服务器和调度程序对象:

    class Device : public fidl::WireServer<fuchsia_wlan_device::Phy>,
                   public ::ddk::Device<Device, ::ddk::MessageableManual, ::ddk::Unbindable> {
     ...
     private:
      // Dispatcher for being a FIDL server listening MLME requests.
      async_dispatcher_t* server_dispatcher_;
     ...
    

    (来源:device_dfv2.h

    您可以使用 fdf::Dispatcher::GetCurrent() 方法检索驱动程序的默认调度程序,或创建新的非默认调度程序(请参阅更新 DFv1 驱动程序以使用非默认调度程序)。

.fidl 文件中,执行以下操作:

  • 为客户端和服务器端定义驱动程序服务协议

    以下示例代码显示了 .fidl 文件中定义的驱动程序服务协议:

    service Service {
        wlan_phy_impl client_end:WlanPhyImpl;
    };
    

    (来源:phyimpl.fidl

3. 更新驱动程序以使用驱动程序运行时 FIDL

利用上一步中的更改,您可以开始更新驱动程序实现,以使用驱动程序运行时 FIDL。

在客户端上,执行以下操作:

  1. 如需连接到父设备驱动程序添加到其传出目录的协议,请调用 DdkConnectRuntimeProtocol() 函数,例如:

    auto client_end = DdkConnectRuntimeProtocol<fuchsia_wlan_softmac::Service::WlanSoftmac>();
    

    (来源:device.cc

    此函数会创建一对端点:

    • 该函数会将 fdf::ClientEnd<ProtocolName> 对象返回给调用方。
    • fdf::ServerEnd<ProtocolName> 对象会以静默方式转到父设备驱动程序。
  2. 当调用方获取客户端对象时,将该对象传递给 fdf::WireSharedClient<ProtocolName>()(或 fdf::WireSyncClient<ProtocolName>())的构造函数,例如:

    client_ = fdf::WireSharedClient<fuchsia_wlan_phyimpl::WlanPhyImpl>(std::move(client), client_dispatcher_.get());
    

    (来源:device.cc

在服务器端,执行以下操作:

  1. 在设备类中声明传出目录对象,例如:

    #include <lib/driver/outgoing/cpp/outgoing_directory.h>
    
    class Device : public DeviceType,
                   public fdf::WireServer<fuchsia_wlan_phyimpl::WlanPhyImpl>,
                   public DataPlaneIfc {
    ...
    
       fdf::OutgoingDirectory outgoing_dir_;
    

    (来源:device.h

  2. 调用 fdf::OutgoingDirectory::Create() 函数,以便父级驱动程序创建一个传出目录对象,例如:

    #include <lib/driver/outgoing/cpp/outgoing_directory.h>
    
    ...
    
    Device::Device(zx_device_t *parent)
        : DeviceType(parent),
          outgoing_dir_(
              fdf::OutgoingDirectory::Create(
                 fdf::Dispatcher::GetCurrent()->get()))
    

    (来源:wlan_interface.cc

  3. 将服务添加到传出目录中并进行传送。

    父级驱动程序的服务协议将传送至此传出目录,以便子级驱动程序可以连接到该目录。

    在父驱动程序的服务回调函数(在子节点连接到服务时调用的函数)中,使用 fdf::BindServer() 函数将 fdf::ServerEnd<ProtocolName> 对象绑定到自身,例如:

    zx_status_t Device::ServeWlanPhyImplProtocol(
            fidl::ServerEnd<fuchsia_io::Directory> server_end) {
      // This callback will be invoked when this service is being connected.
      auto protocol = [this](
          fdf::ServerEnd<fuchsia_wlan_phyimpl::WlanPhyImpl> server_end) mutable {
        fdf::BindServer(fidl_dispatcher_.get(), std::move(server_end), this);
        protocol_connected_.Signal();
      };
    
      // Register the callback to handler.
      fuchsia_wlan_phyimpl::Service::InstanceHandler handler(
           {.wlan_phy_impl = std::move(protocol)});
    
      // Add this service to the outgoing directory so that the child driver can
      // connect to by calling DdkConnectRuntimeProtocol().
      auto status =
           outgoing_dir_.AddService<fuchsia_wlan_phyimpl::Service>(
                std::move(handler));
      if (status.is_error()) {
        NXPF_ERR("%s(): Failed to add service to outgoing directory: %s\n",
             status.status_string());
        return status.error_value();
      }
    
      // Serve the outgoing directory to the entity that intends to open it, which
      // is DFv1 in this case.
      auto result = outgoing_dir_.Serve(std::move(server_end));
      if (result.is_error()) {
        NXPF_ERR("%s(): Failed to serve outgoing directory: %s\n",
             result.status_string());
        return result.error_value();
      }
    
      return ZX_OK;
    }
    

    (来源:device.cc

    请注意,fdf::BindServer() 函数需要一个调度程序作为输入。您可以使用驱动程序主机提供的默认驱动程序调度程序 (fdf::Dispatcher::GetCurrent()->get()),也可以创建新的非默认调度程序来单独处理 FIDL 请求(请参阅更新 DFv1 驱动程序以使用非默认调度程序)。

    此时,客户端和服务器端可以使用驱动程序运行时 FIDL 相互通信。

4. 发出 FIDL 请求

如需进行 FIDL 调用,请使用在前面步骤中在客户端构建的 fdf::WireSharedClient<ProtocolName>() 对象的代理。

请参阅以下语法,了解如何进行 FIDL 调用(其中 CLIENT_ 是实例的名称):

  • 异步 FIDL 调用:

    CLIENT_.buffer(*std::move(arena))->MyExampleFunction().ThenExactlyOnce([](fdf::WireUnownedResult<FidlFunctionName>& result) mutable {
      // Your result handler.
    });
    

    (来源:device.cc

  • 同步 FIDL 调用:

    auto result = CLIENT_.sync().buffer(*std::move(arena))->MyExampleFunction();
    // Your result handler.
    

    (来源:device.cc

您可能会发现以下做法对于进行 FIDL 调用很有帮助:

  • 使用 FIDL 错误语法,您可以调用 result.is_error() 来检查调用是否返回域错误。同样,result.error_value() 调用会返回确切的错误值。

  • 进行异步双向客户端 FIDL 调用时,您可以使用 .Then(callback).ThenExactlyOnce(callback) 指定待处理的回调所需的取消语义,而不是传递回调。

  • 同步 FIDL 调用将等待服务器端的回调。因此,此调用需要在 .fidl 文件中定义回调。如果没有它,调用将只执行“触发并忘记”并立即返回。

    如需了解回调定义,请参阅 .fidl 文件中的以下示例:

    protocol MyExampleProtocol {
    // You can only make async calls based on this function.
      FunctionWithoutCallback();
    
      // You can make both sync and async calls based on this function.
      FunctionWithCallback() -> ();
    }
    

    创建回调定义后,需要在服务器端实现一个函数(在上例中的同步回调和异步回调中都会调用该函数)(请参阅下面的 MyExampleFunction())。

    当服务器端设备类继承 fdf::WireServer<ProtocolName> 对象时,系统会根据协议定义生成虚拟函数,类似于 fidl::WireServer<ProtocolName>。此函数的格式如下:

    void MyExampleFunction(MyExampleFunctionRequestView request, fdf::Arena& arena, MyExampleFunctionCompleter::Sync& completer);
    

    此函数采用三个参数:

    • MyExampleFunctionRequestView - 由 FIDL 生成,包含您要从客户端发送到服务器的请求结构。

    • fdf::Arena - 它是该 FIDL 消息的缓冲区。系统会从客户端传递或移动此缓冲区。您可以将其用作缓冲区,以通过 completer 对象返回消息或错误语法。您还可以重复使用它对下一级别的驱动程序进行 FIDL 调用。

    • MyExampleFunctionCompleter - 由 FIDL 生成,用于调用回调并将此 FIDL 调用的结果返回给客户端。 如果定义了 FIDL 错误语法,您可以使用 completer.ReplySuccess()completer.ReplyError() 返回消息和错误状态;如果未定义 FIDL 错误语法,只能使用 completer.Reply() 返回消息。

  • 您可以将 arena 对象移入 FIDL 调用,也可以直接将其作为 *arena 传递。不过,移动 arena 对象可能会出错。 因此,建议您改为传递 arena 对象。由于 Arena 可以重复用于下一级 FIDL 调用,因此 arena 对象在传递后不会被销毁。

  • 如果您的驱动程序发送 .fidl 文件中自定义类型格式的消息,FIDL 会根据定义同时生成自然类型和传输类型。不过,在这种情况下,我们推荐使用导线类型,因为导线类型是更稳定的选择,而自然类型是 HLCPP 的临时类型。

  • 如果 FIDL 注意到端点交换了无效值(称为验证错误),就会关闭其通道:

    • 当传递 0(对于非灵活枚举)而只定义值 1 到 5 时,就是一个验证错误。如果发生验证错误,通道将永久关闭,且所有后续消息都会导致失败。此行为与 Banjo 不同。(如需了解详情,请参阅严格限制与灵活设置。)

    • 无效值还包括 zx_handle_t 值设置为 0 的内核对象。例如 zx::vmo(0)

(可选)更新 DFv1 驱动程序以使用非默认调度程序

fdf::Dispatcher::GetCurrent() 方法可为您提供运行驱动程序的默认调度程序。如果可能,建议单独使用此默认调度程序。不过,如果您需要创建新的非默认调度程序,驱动程序运行时需要了解调度程序属于哪个驱动程序。这样,驱动程序框架就可以设置适当的属性并正确关闭调度程序。

以下部分介绍了分配和管理您自己的非默认调度程序的高级用例:

  1. 分配调度程序
  2. 关闭调度程序

1. 分配调度程序

为了确保在由驱动程序框架管理的线程中创建和运行调度程序,必须在驱动程序框架调用的函数(如 DdkInit())中分配调度程序,例如:

void Device::DdkInit(ddk::InitTxn txn) {
  bool fw_init_pending = false;
  const zx_status_t status = [&]() -> zx_status_t {
    auto dispatcher = fdf::SynchronizedDispatcher::Create(
        {}, "nxpfmac-sdio-wlanphy",
        [&](fdf_dispatcher_t *) { sync_completion_signal(&fidl_dispatcher_completion_); });
    if (dispatcher.is_error()) {
      NXPF_ERR("Failed to create fdf dispatcher: %s", dispatcher.status_string());
      return dispatcher.status_value();
    }
    fidl_dispatcher_ = std::move(*dispatcher);
  ...

(来源:device.cc

2. 关闭调度程序

同样,出于同样的原因,也需要在驱动程序框架调用的函数中关闭调度程序。DdkUnbind()device_unbind() 方法非常适合执行此操作。

请注意,调度程序关闭是异步进行的,需要妥善处理。例如,如果调度程序在 DdkUnbind() 调用中关闭,我们需要使用 ddk::UnbindTxn 对象(之前通过 Unbind() 调用传递)在调度程序的关闭回调中调用 ddk::UnbindTxn::Reply() 调用,以确保正常关闭。

以下代码段示例演示了上述关停过程:

  1. ddk::UnbindTxn 对象保存在 DdkUnbind() 中:

    void DdkUnbind(ddk::UnbindTxn txn) {
      // Move the txn here because it’s not copyable.
      unbind_txn_ = std::move(txn);
      ...
    }
    
  2. 在绑定或 DdkInit() 钩子中,创建一个具有可调用 ddk::UnbindTxn::Reply() 的关闭回调的调度程序:

      auto dispatcher = fdf::Dispatcher::Create(0, [&](fdf_dispatcher_t*) {
        if (unbind_txn_)
          unbind_txn_->Reply();
        unbind_txn_.reset();
      });
    
  3. DdkUnbind() 末尾从调度程序调用 ShutdownAsync()

    void DdkUnbind(ddk::UnbindTxn txn) {
      // Move the txn here because it’s not copyable.
      unbind_txn_ = std::move(txn);
      ...
      dispatcher.ShutDownAsync();
    }
    

但是,如果驱动程序中分配了多个调度程序,因为 ddk::UnbindTxn::Reply() 只会调用一次,所以您需要实现一系列关闭操作。例如,假设调度程序 A 和 B(可互换),您可以:

  1. 在 A 的关闭回调中对 B 调用 ShutdownAsync()
  2. 在 B 的关闭回调中调用 ddk::UnbindTxn::Reply()

(可选)更新 DFv1 驱动程序以使用双向通信

通常只有客户端上的设备需要主动向服务器端设备发出请求。但在某些情况下,两台设备都需要通过信道发送消息,而无需等待另一端响应。

如需在 DFv1 驱动程序中建立双向通信,您有以下三个选项:

  • 方法 1 - 通过 FIDL 协议定义事件(请参阅实现 C++ FIDL 服务器)。

  • 方式 2 - 以相反方向实现第二个 FIDL 协议,使两端的设备同时为服务器和客户端。

  • 方法 3 - 在 FIDL 中使用挂起 get 模式(FIDL 评分准则推荐的一种流控制设计模式)。

使用事件的原因(选项 1)包括:

  • 简单 - 一个协议比两个协议简单。

  • 序列化 - 如果您需要两个协议,则系统必定会按照写入通道的事件和回复进行序列化处理。

不使用事件的原因(选项 1)包括:

  • 您需要在消息从服务器发送到客户端时进行响应。

  • 您需要控制消息流。

以下协议实现了选项 2,其中它们在同一 .fidl 文件中针对两个不同的方向进行定义:

对于 WLAN 驱动程序迁移,该团队选择了方案 2,因为它不会引入来自未知网域的额外 FIDL 语法。但请注意,WLAN 驱动程序是驱动程序运行时迁移的第一个实例,并且迁移时驱动程序传输中的 FIDL 事件不受支持。

根据您的需要,“挂起的 get”调用(选项 3)通常比事件更好,因为它允许客户端指定它已准备好处理事件,这也提供了流控制。(不过,如有必要,还可以通过其他方法向事件添加流控制,具体说明请参阅 FIDL 评分准则的使用确认限制事件部分。)

更新 DFv1 驱动程序的单元测试以使用 FIDL

如果存在基于 Banjo API 的适用于驱动程序的单元测试,您需要迁移这些测试来提供模拟 FIDL 服务器,而不是 Banjo 服务器(或模拟 FIDL 客户端)。

在 DFv1 中,如需模拟 FIDL 服务器或客户端,您可以使用 MockDevice::FakeRootParent() 方法,例如:

std::shared_ptr<MockDevice> fake_parent_ = MockDevice::FakeRootParent();

(来源:ft_device_test.cc

MockDevice::FakeRootParent() 方法已与 DriverRuntime 测试库(DFv1 唯一受支持的测试库)集成。MockDevice::FakeRootParent() 方法为用户创建一个 fdf_testing::DriverRuntime 实例。然后,该实例会启动驱动程序运行时,并为用户创建前台驱动程序调度程序。 不过,您也可以通过此对象创建后台调度程序。您可以使用 fdf_testing::DriverRuntime::GetInstance() 方法获取该实例。

如需查看示例,请参阅此单元测试,该测试模拟 DFv1 驱动程序的 PWM 和 vreg FIDL 协议。

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

其他资源

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

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

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