将 Banjo 协议转换为 FIDL 协议

本页提供了与以下内容相关的说明、最佳做法和示例: 在 DFv1-to-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 是 默认值)。因此,您只需从命令行中删除 @transport("Banjo") 属性即可 驱动程序的 .fidl 文件,例如:

  • 使用 Banjo 时:

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

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

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

    不过,@transport("Driver") 属性(表示 这是一个驱动程序传输协议)可选 使用驱动程序运行时 FIDL 的驱动程序。 只有在驱动程序发出语音指令时才需要进行驱动程序运行时迁移 位于同一驱动程序主机中的其他驱动程序。

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

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 移至其他位置, 此文件不允许。在这种情况下,您可以保留这份.fidl的副本 文件(位于 sdk/banjo 目录中)。

(可选)更新 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 客户端对象:

    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. 声明一个调度程序对象 (fdf::Dispatcher),以便 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_;
     ...
    

    (来源: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::ServerEnd<ProtocolName> 对象 fdf::BindServer() 函数,例如:

    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>() 对象 创建 Deployment 清单

请参阅下面的语法,了解如何进行 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() 即可返回该消息。

  • 您可以将 Area 对象移到 FIDL 调用中,也可以直接将它传递给它 名称:*arena。但是,移动场景对象可能会暴露潜在错误。 因此,建议改为传递 arena 对象。由于 Area 可以重复用于下一级别 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 中使用“hanging get”模式( 流控制设计模式中建议的 FIDL 评分准则)。

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

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

  • 序列化 - 如果您需要两种协议, 会按照写入通道的顺序进行序列化。

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

  • 当消息从服务器发送到 客户端。

  • 您需要控制消息的流动。

以下协议实施了选项 2,其中的协议以 .fidl 文件,用于指示两个不同的方向:

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

根据你的需要来电(选项 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 更改如下:

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

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