将 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 {
     ...
    }
    

    在上面的示例中,所有 FIDL 协议都必须提供 @discoverable 属性。借助此属性,客户端可以使用其生成的名称搜索此协议。

    不过,对于使用驱动程序运行时 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 目标

修改 BUILD.gn 文件(此 .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 文件中,更新 dependencies 字段以包含以下代码行:

    //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() 函数需要 dispatcher 作为输入。您可以使用驱动程序主机 (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() 返回消息。

  • 您可以将竞技场对象移至 FIDL 调用中,也可以直接将其作为 *arena 传递。不过,移动竞技场对象可能会暴露潜在错误。因此,建议改为传递竞技场对象。由于竞技场可能会用于下一级 FIDL 调用,因此竞技场对象在传递后不会被销毁。

  • 如果您的驱动程序发送的格式为 .fidl 文件中的自定义类型的消息,FIDL 会根据定义生成自然类型和线型。不过,在这种情况下,建议使用线缆类型,因为线缆类型是更稳定的选择。

  • 如果 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 的 shutdown 回调中为 B 调用 ShutdownAsync()
  2. 在 B 的 shutdown 回调中调用 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 更改

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

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