班卓琴教程

Banjo 是一种“转译器”(如 FIDL 的 fidlc),一种将接口定义语言 (IDL) 转换为目标语言特定文件的程序。

本教程的结构如下:

  • Banjo 简介
  • 简单示例 (I2C)
  • 示例中生成的代码的说明

参考信息部分还包含以下内容:

  • 内置关键字和基元类型的列表。

概览

Banjo 会生成 C 和 C++ 代码,可供协议实现者和协议用户使用。

简单示例

首先,我们来看一个相对简单的 Banjo 规范。文件 //sdk/banjo/fuchsia.hardware.i2cimpl/i2cimpl.fidl 如下:

请注意,本教程中代码示例中的行号不属于这些文件。

[01] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[02] // Use of this source code is governed by a BSD-style license that can be
[03] // found in the LICENSE file.
[04] @available(added=7)
[05] library fuchsia.hardware.i2cimpl;
[06]
[07] using zx;
[08]
[09] const I2C_IMPL_10_BIT_ADDR_MASK uint32 = 0xF000;
[10] /// The maximum number of I2cImplOp's that may be passed to Transact.
[11] const I2C_IMPL_MAX_RW_OPS uint32 = 8;
[12] /// The maximum length of all read or all write transfers in bytes.
[13] const I2C_IMPL_MAX_TOTAL_TRANSFER uint32 = 4096;
[14]
[15] /// See `Transact` below for usage.
[16] type I2cImplOp = struct {
[17]     address uint16;
[18]     @buffer
[19]     @mutable
[20]     data vector<uint8>:MAX;
[21]     is_read bool;
[22]     stop bool;
[23] };
[24]
[25] /// Low-level protocol for i2c drivers.
[26] @transport("Banjo")
[27] @banjo_layout("ddk-protocol")
[28] protocol I2cImpl {
[29]     /// First bus ID that this I2cImpl controls, zero-indexed.
[30]     GetBusBase() -> (struct {
[31]         base uint32;
[32]     });
[33]     /// Number of buses that this I2cImpl supports.
[34]     GetBusCount() -> (struct {
[35]         count uint32;
[36]     });
[37]     GetMaxTransferSize(struct {
[38]         bus_id uint32;
[39]     }) -> (struct {
[40]         s zx.Status;
[41]         size uint64;
[42]     });
[43]     /// Sets the bitrate for the i2c bus in KHz units.
[44]     SetBitrate(struct {
[45]         bus_id uint32;
[46]         bitrate uint32;
[47]     }) -> (struct {
[48]         s zx.Status;
[49]     });
[50]     /// |Transact| assumes that all ops buf are not null.
[51]     /// |Transact| assumes that all ops length are not zero.
[52]     /// |Transact| assumes that at least the last op has stop set to true.
[53]     Transact(struct {
[54]         bus_id uint32;
[55]         op vector<I2cImplOp>:MAX;
[56]     }) -> (struct {
[57]         status zx.Status;
[58]     });
[59] };

它定义了一个允许应用在 I2C 总线上读写数据的接口。 在 I2C 总线中,必须先将数据写入设备才能请求响应。 如果需要响应,则可以从设备读取响应。(例如,设置只写寄存器时可能不需要响应。)

让我们逐行查看各个组件:

  • [05] - library 指令会告知 Banjo 编译器它应该对生成的输出使用什么前缀;可将其视为命名空间说明符。
  • [07] - using 指令指示 Banjo 包含 zx 库。
  • [09] [11][13] - 这两个常量引入了两个供程序员使用的常量。
  • [16 .. 23] - 这些定义了一个名为 I2cImplOp 的结构,程序员随后将使用该结构向总线传输数据,或从总线传输数据。
  • [26 .. 59] - 这些代码行定义了此 Banjo 规范提供的接口方法;我们将在下文更详细地讨论这一点。

请勿对 [50 .. 52](和其他位置)上的注释感到困惑,它们是旨在“流经”注释,旨在发送到生成的源代码中。任何以“///”(三!正斜线)开头的注释都是“流式”注释。 普通注释(即“//”)适用于当前模块。我们查看生成的代码时就会明白这一点。

操作结构

在我们的 I2C 示例中,struct I2cImplOp 结构定义了四个元素:

元素 类型 使用情形
address uint16 在总线上与之互动的芯片的地址
data vector<voidptr> 包含发送到总线和从总线接收的数据
is_read bool 指示所需的读取功能的标志
stop bool 标记,指示应在操作之后发送停止字节

该结构定义了将在协议实现(驱动程序)和协议用户(使用总线的程序)之间使用的通信区域。

接口

更有趣的部分是 protocol 规范。

我们暂时会跳过 @transport("Banjo")(第 [26] 行)和 @banjo_layout("ddk-protocol")(第 [27] 行)属性,但会回到下面的属性部分。

protocol 部分定义了五种接口方法:

  • GetBusBase
  • GetBusCount
  • GetMaxTransferSize
  • SetBitrate
  • Transact

无需详细介绍其内部操作(毕竟这不是 I2C 教程),我们来看一下它们如何翻译成目标语言。我们将分别介绍 C 和 C++ 实现,并使用 C 说明添加 C++ 版本通用的结构定义。

目前支持生成 C 和 C++ 代码,未来计划支持 Rust。

C

C 实现相对简单:

  • structunion 几乎会直接映射到对应的 C 语言对应项。
  • enum 和常量会生成为 #define 宏。
  • protocol 生成为两个 struct
    • 函数表
    • 包含指向函数表和上下文的指针的结构体。
  • 还会生成一些辅助函数。

C 版本会生成到 $BUILD_DIR/fidling/gen/sdk/banjo/fuchsia.hardware.i2cimpl/fuchsia.hardware.i2cimpl_banjo_c/fuchsia/hardware/i2cimpl/c/banjo.h

此文件相对较长,所以我们将分以下几个部分对其进行查看。

样板文件

第一部分包含一些样板代码,我们直接显示它而不进一步添加注释:

[01] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[02] // Use of this source code is governed by a BSD-style license that can be
[03] // found in the LICENSE file.
[04]
[05] // WARNING: THIS FILE IS MACHINE GENERATED. DO NOT EDIT.
[06] // Generated from the fuchsia.hardware.i2cimpl banjo file
[07]
[08] #pragma once
[09]
[10]
[11] #include <zircon/compiler.h>
[12] #include <zircon/types.h>
[13]
[14] __BEGIN_CDECLS

前向声明

接下来是针对结构和函数的前向声明:

[16] // Forward declarations
[17] typedef struct i2c_impl_op i2c_impl_op_t;
[18] typedef struct i2c_impl_protocol i2c_impl_protocol_t;
[19] typedef struct i2c_impl_protocol_ops i2c_impl_protocol_ops_t;
...
[26] // Declarations
[27] // See `Transact` below for usage.
[28] struct i2c_impl_op {
[29]     uint16_t address;
[30]     uint8_t* data_buffer;
[31]     size_t data_size;
[32]     bool is_read;
[33]     bool stop;
[34] };

请注意,[17 .. 19] 行仅声明类型,实际上并不会定义函数的结构或原型。

请注意“流经”注释(例如,原始 .fidl 文件第 [15] 行)会发送到生成的代码(上面的第 [27] 行)中,其中删除了一个斜杠,使其看起来像普通注释。

如前所述,第 [28 .. 34] 行是上述 .fidl 文件(第 [16 .. 23] 行)中的 struct I2cImplOp 的近乎直接映射。

精明的 C 程序员将立即看到 C++ 样式的 vector<voidptr> data(原始 .fidl 文件行 [20])在 C 中的处理方式:它会转换为指针(“data_buffer”)和大小(“data_size”)。

至于命名,基本名称就是 data(如 .fidl 文件中所指定)。 对于 voidptr 的矢量,转译器会附加 _buffer_size,以将 vector 转换为与 C 兼容的结构。对于所有其他矢量类型,转译器改为附加 _list_count(以确保代码可读性)。

常量

接下来,我们会看到 const uint32 常量已转换为 #define 语句:

[20] // The maximum length of all read or all write transfers in bytes.
[21] #define I2C_IMPL_MAX_TOTAL_TRANSFER UINT32_C(4096)
[22] // The maximum number of I2cImplOp's that may be passed to Transact.
[23] #define I2C_IMPL_MAX_RW_OPS UINT32_C(8)
[24] #define I2C_IMPL_10_BIT_ADDR_MASK UINT32_C(0xF000)

在 C 版本中,我们选择 #define 而不是“传递”const uint32_t 表示法,原因如下:

  • #define 语句仅在编译时存在,并且会内嵌到每个使用网站上,而 const uint32_t 会嵌入到二进制文件中,并且
  • #define 支持更多编译时优化(例如,使用常量值进行数学运算)。

缺点是我们无法获得类型安全,这就是您会看到辅助宏(如上面的 UINT32_C())的原因;它们只是将常量转换为适当的类型。

协议结构

现在,我们就进入正题。

[36] struct i2c_impl_protocol_ops {
[37]     uint32_t (*get_bus_base)(void* ctx);
[38]     uint32_t (*get_bus_count)(void* ctx);
[39]     zx_status_t (*get_max_transfer_size)(void* ctx, uint32_t bus_id, uint64_t* out_size);
[40]     zx_status_t (*set_bitrate)(void* ctx, uint32_t bus_id, uint32_t bitrate);
[41]     zx_status_t (*transact)(void* ctx, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count);
[42] };

这会创建一个结构定义,其中包含在原始 .fidl 文件中的 [30][34][37][44][43] 行定义的五个 protocol 方法。

请注意发生了名称错误 - 您可以按照如下方式将 protocol 方法名称映射到 C 函数指针名称,以便您知道它们的名称:

班卓琴 C 规则
Transact transact 将前导大写转换为小写
GetBusBase get_bus_base 如上所述,将驼峰式大小写转换为以下划线分隔的样式
GetBusCount get_bus_count 同上
SetBitrate set_bitrate 同上
GetMaxTransferSize get_max_transfer_size 同上

接下来,接口定义封装在一个上下文传递结构中:

[45] struct i2c_impl_protocol {
[46]     i2c_impl_protocol_ops_t* ops;
[47]     void* ctx;
[48] };

最后,我们会看到为五种方法实际生成的代码:

[53] static inline uint32_t i2c_impl_get_bus_base(const i2c_impl_protocol_t* proto) {
[54]     return proto->ops->get_bus_base(proto->ctx);
[55] }
[56]
[57] // Number of buses that this I2cImpl supports.
[58] static inline uint32_t i2c_impl_get_bus_count(const i2c_impl_protocol_t* proto) {
[59]     return proto->ops->get_bus_count(proto->ctx);
[60] }
[61]
[62] static inline zx_status_t i2c_impl_get_max_transfer_size(const i2c_impl_protocol_t* proto, uint32_t bus_id, uint64_t* out_size) {
[63]     return proto->ops->get_max_transfer_size(proto->ctx, bus_id, out_size);
[64] }
[65]
[66] // Sets the bitrate for the i2c bus in KHz units.
[67] static inline zx_status_t i2c_impl_set_bitrate(const i2c_impl_protocol_t* proto, uint32_t bus_id, uint32_t bitrate) {
[68]     return proto->ops->set_bitrate(proto->ctx, bus_id, bitrate);
[69] }
[70]
[71] // |Transact| assumes that all ops buf are not null.
[72] // |Transact| assumes that all ops length are not zero.
[73] // |Transact| assumes that at least the last op has stop set to true.
[74] static inline zx_status_t i2c_impl_transact(const i2c_impl_protocol_t* proto, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) {
[75]     return proto->ops->transact(proto->ctx, bus_id, op_list, op_count);
[76] }

前缀和路径

请注意,前缀 i2c_impl_(来自接口名称,.fidl 文件行 [28])添加到方法名称中;因此,Transact 变成了 i2c_impl_transact,依此类推。这是 .fidl 名称与其 C 语言等效项之间映射的一部分。

此外,library 名称(.fidl 文件中的 [05] 行)会转换为 include 路径:因此 library fuchsia.hardware.i2cimpl 隐含 <fuchsia/hardware/i2cimpl/c/banjo.h> 的路径。

C++

C++ 代码比 C 版本稍微复杂一些。一起来看看吧。

Banjo 转译器生成三个文件:第一个是上面讨论的 C 文件,另外两个位于 $BUILD_DIR/fidling/gen/sdk/banjo/fuchsia.hardware.i2cimpl/fuchsia.hardware.i2cimpl_banjo_c/fuchsia/hardware/i2cimpl/cpp/

  • i2cimpl.h - 您的程序应包含的文件;以及
  • i2cimpl-internal.h - 一个内部文件,由 i2cimpl.h 包含

“内部”文件包含声明和断言,我们可以放心地跳过它们。

C++ 版本的 i2cimpl.h 相当长,所以我们将分几部分了解它。下面是一个概览“地图”,显示了我们将要查看的内容,显示了每段内容的起始行号:

小节
1 样板
20 自动生成的使用情况注释
61 I2cImplProtocol 类
112 I2cImplProtocolClient 类

样板文件

样板代码应该符合您的预期:

[001] // Copyright 2018 The Fuchsia Authors. All rights reserved.
[002] // Use of this source code is governed by a BSD-style license that can be
[003] // found in the LICENSE file.
[004]
[005] // WARNING: THIS FILE IS MACHINE GENERATED. DO NOT EDIT.
[006] // Generated from the fuchsia.hardware.i2cimpl banjo file
[007]
[008] #pragma once
[009]
[010] #include <ddktl/device-internal.h>
[011] #include <fuchsia/hardware/i2cimpl/c/banjo.h>
[012] #include <lib/ddk/device.h>
[013] #include <lib/ddk/driver.h>
[014] #include <zircon/assert.h>
[015] #include <zircon/compiler.h>
[016] #include <zircon/types.h>
[017]
[018] #include "banjo-internal.h"

#include 一批 DDK 和操作系统头文件,包括:

  • 头文件的 C 版本(第 [011] 行,这意味着 C 部分中讨论的所有内容在此处同样适用),并且
  • 生成的 i2cimpl-internal.h 文件(第 [018] 行)。

接下来是“自动生成的用法注释”部分;稍后我们会再次探讨该部分,因为在了解了实际的类声明后,这些内容就会更加有意义。

这两个类声明封装在 DDK 命名空间中:

[057] namespace ddk {
...
[214] } // namespace ddk

I2cImplProtocolClient 封装容器类

I2cImplProtocolClient 类是 i2c_impl_protocol_t 结构(在 C include 文件的第 [45] 行中定义,详见上文的协议结构中讨论)的简单封装容器。

[112] class I2cImplProtocolClient {
[113] public:
[114]     I2cImplProtocolClient()
[115]         : ops_(nullptr), ctx_(nullptr) {}
[116]     I2cImplProtocolClient(const i2c_impl_protocol_t* proto)
[117]         : ops_(proto->ops), ctx_(proto->ctx) {}
[118]
[119]     I2cImplProtocolClient(zx_device_t* parent) {
[120]         i2c_impl_protocol_t proto;
[121]         if (device_get_protocol(parent, ZX_PROTOCOL_I2C_IMPL, &proto) == ZX_OK) {
[122]             ops_ = proto.ops;
[123]             ctx_ = proto.ctx;
[124]         } else {
[125]             ops_ = nullptr;
[126]             ctx_ = nullptr;
[127]         }
[128]     }
[129]
[130]     I2cImplProtocolClient(zx_device_t* parent, const char* fragment_name) {
[131]         i2c_impl_protocol_t proto;
[132]         if (device_get_fragment_protocol(parent, fragment_name, ZX_PROTOCOL_I2C_IMPL, &proto) == ZX_OK) {
[133]             ops_ = proto.ops;
[134]             ctx_ = proto.ctx;
[135]         } else {
[136]             ops_ = nullptr;
[137]             ctx_ = nullptr;
[138]         }
[139]     }
[140]
[141]     // Create a I2cImplProtocolClient from the given parent device + "fragment".
[142]     //
[143]     // If ZX_OK is returned, the created object will be initialized in |result|.
[144]     static zx_status_t CreateFromDevice(zx_device_t* parent,
[145]                                         I2cImplProtocolClient* result) {
[146]         i2c_impl_protocol_t proto;
[147]         zx_status_t status = device_get_protocol(
[148]                 parent, ZX_PROTOCOL_I2C_IMPL, &proto);
[149]         if (status != ZX_OK) {
[150]             return status;
[151]         }
[152]         *result = I2cImplProtocolClient(&proto);
[153]         return ZX_OK;
[154]     }
[155]
[156]     // Create a I2cImplProtocolClient from the given parent device.
[157]     //
[158]     // If ZX_OK is returned, the created object will be initialized in |result|.
[159]     static zx_status_t CreateFromDevice(zx_device_t* parent, const char* fragment_name,
[160]                                         I2cImplProtocolClient* result) {
[161]         i2c_impl_protocol_t proto;
[162]         zx_status_t status = device_get_fragment_protocol(parent, fragment_name,
[163]                                  ZX_PROTOCOL_I2C_IMPL, &proto);
[164]         if (status != ZX_OK) {
[165]             return status;
[166]         }
[167]         *result = I2cImplProtocolClient(&proto);
[168]         return ZX_OK;
[169]     }
[170]
[171]     void GetProto(i2c_impl_protocol_t* proto) const {
[172]         proto->ctx = ctx_;
[173]         proto->ops = ops_;
[174]     }
[175]     bool is_valid() const {
[176]         return ops_ != nullptr;
[177]     }
[178]     void clear() {
[179]         ctx_ = nullptr;
[180]         ops_ = nullptr;
[181]     }
[182]
[183]     // First bus ID that this I2cImpl controls, zero-indexed.
[184]     uint32_t GetBusBase() const {
[185]         return ops_->get_bus_base(ctx_);
[186]     }
[187]
[188]     // Number of buses that this I2cImpl supports.
[189]     uint32_t GetBusCount() const {
[190]         return ops_->get_bus_count(ctx_);
[191]     }
[192]
[193]     zx_status_t GetMaxTransferSize(uint32_t bus_id, uint64_t* out_size) const {
[194]         return ops_->get_max_transfer_size(ctx_, bus_id, out_size);
[195]     }
[196]
[197]     // Sets the bitrate for the i2c bus in KHz units.
[198]     zx_status_t SetBitrate(uint32_t bus_id, uint32_t bitrate) const {
[199]         return ops_->set_bitrate(ctx_, bus_id, bitrate);
[200]     }
[201]
[202]     // |Transact| assumes that all ops buf are not null.
[203]     // |Transact| assumes that all ops length are not zero.
[204]     // |Transact| assumes that at least the last op has stop set to true.
[205]     zx_status_t Transact(uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) const {
[206]         return ops_->transact(ctx_, bus_id, op_list, op_count);
[207]     }
[208]
[209] private:
[210]     i2c_impl_protocol_ops_t* ops_;
[211]     void* ctx_;
[212] };

共有四个构造函数:

  • ops_ctx_ 设置为 nullptr 的默认值 ([114]),
  • 一个初始化程序 ([116]),它接受指向 i2c_impl_protocol_t 结构的指针,并根据其在结构中的同名字段填充 ops_ctx_ 字段;
  • 用于从 zx_device_t 中提取 ops_ctx_ 信息的初始化程序 ([119])。
  • 初始化程序 ([130]),但从设备 fragment 获取 ops_ctx_

后两个构造函数为首选构造函数,可按如下方式使用:

ddk::I2cImplProtocolClient i2cimpl(parent);
if (!i2cimpl.is_valid()) {
  return ZX_ERR_*; // return an appropriate error
}
ddk::I2cImplProtocolClient i2cimpl(parent, "i2c-impl-fragment");
if (!i2cimpl.is_valid()) {
  return ZX_ERR_*; // return an appropriate error
}

系统提供了三个方便的成员函数:

  • [171] GetProto()ctx_ops_ 成员提取到协议结构中,
  • [175] is_valid() 会返回 bool,指示该类是否已使用协议初始化;
  • [178] clear() 会使 ctx_ops_ 指针失效。

接下来,我们找到在 .fidl 文件中指定的四个成员函数:

  • [138] GetBusBase()
  • [138] GetBusCount()
  • [138] GetMaxTransferSize()
  • [138] SetBitrate()
  • [134] Transact()

这些方式与 C 版本 include 文件中的四个封装容器函数类似,也就是说,它们通过相应的函数指针将其参数传递到调用中。

事实上,请比较 C 版本的 i2c_impl_get_max_transfer_size()

[138] zx_status_t GetMaxTransferSize(size_t* out_size) const {
[139]     return ops_->get_max_transfer_size(ctx_, out_size);
[140] }

替换为以上 C++ 版本:

[138] zx_status_t GetMaxTransferSize(size_t* out_size) const {
[139]     return ops_->get_max_transfer_size(ctx_, out_size);
[140] }

如前所述,该类的作用只是存储操作和上下文指针以备后用,这样通过封装容器进行的调用会更加简洁。

您还将注意到,C++ 封装容器函数没有任何名称篡改 - 为了使用含义,GetMaxTransferSize()GetMaxTransferSize()

I2cImplProtocol mixin 类

好吧,这部分比较轻松。 在下一部分中,我们将介绍 mixinsCRTP(即奇怪的重复模板模式)

我们首先来了解该类的“形状”(出于概括性目的而删除了注释行):

[060] template <typename D, typename Base = internal::base_mixin>
[061] class I2cImplProtocol : public Base {
[062] public:
[063]     I2cImplProtocol() {
[064]         internal::CheckI2cImplProtocolSubclass<D>();
[065]         i2c_impl_protocol_ops_.get_bus_base = I2cImplGetBusBase;
[066]         i2c_impl_protocol_ops_.get_bus_count = I2cImplGetBusCount;
[067]         i2c_impl_protocol_ops_.get_max_transfer_size = I2cImplGetMaxTransferSize;
[068]         i2c_impl_protocol_ops_.set_bitrate = I2cImplSetBitrate;
[069]         i2c_impl_protocol_ops_.transact = I2cImplTransact;
[070]
[071]         if constexpr (internal::is_base_proto<Base>::value) {
[072]             auto dev = static_cast<D*>(this);
[073]             // Can only inherit from one base_protocol implementation.
[074]             ZX_ASSERT(dev->ddk_proto_id_ == 0);
[075]             dev->ddk_proto_id_ = ZX_PROTOCOL_I2C_IMPL;
[076]             dev->ddk_proto_ops_ = &i2c_impl_protocol_ops_;
[077]         }
[078]     }
[079]
[080] protected:
[081]     i2c_impl_protocol_ops_t i2c_impl_protocol_ops_ = {};
[082]
[083] private:
...
[085]     static uint32_t I2cImplGetBusBase(void* ctx) {
[086]         auto ret = static_cast<D*>(ctx)->I2cImplGetBusBase();
[087]         return ret;
[088]     }
...
[090]     static uint32_t I2cImplGetBusCount(void* ctx) {
[091]         auto ret = static_cast<D*>(ctx)->I2cImplGetBusCount();
[092]         return ret;
[093]     }
[094]     static zx_status_t I2cImplGetMaxTransferSize(void* ctx, uint32_t bus_id, uint64_t* out_size) {
[095]         auto ret = static_cast<D*>(ctx)->I2cImplGetMaxTransferSize(bus_id, out_size);
[096]         return ret;
[097]     }
...
[099]     static zx_status_t I2cImplSetBitrate(void* ctx, uint32_t bus_id, uint32_t bitrate) {
[100]         auto ret = static_cast<D*>(ctx)->I2cImplSetBitrate(bus_id, bitrate);
[101]         return ret;
[102]     }
...
[106]     static zx_status_t I2cImplTransact(void* ctx, uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count) {
[107]         auto ret = static_cast<D*>(ctx)->I2cImplTransact(bus_id, op_list, op_count);
[108]         return ret;
[109]     }
[110] };

I2CImplProtocol 类继承自由第二个模板参数指定的基类。 如果未指定,则默认为 internal::base_mixin,且不会发生任何特殊情况。不过,如果明确指定了基类,则基类应为 ddk::base_protocol,在这种情况下,系统会添加其他断言(以仔细检查是否只有一个 mixin 是基本协议)。此外,特殊 DDKTL 字段设置为在驱动程序触发 DdkAdd() 时自动将此协议注册为基本协议。

该构造函数会调用内部验证函数 CheckI2cImplProtocolSubclass() [32](在生成的 i2c-impl-internal.h 文件中定义),该函数具有几个 static_assert() 调用。D 类应实现五个成员函数(I2cImplGetBusBase()I2cIImplGetBusCount()I2cImplGetMaxTransferSize()I2cImplSetBitrate()I2cImplTransact()),才能使静态方法正常运行。如果它们不是由 D 提供的,则编译器(在没有静态断言的情况下)会生成严重的模板错误。静态断言用于生成人类可以理解的诊断错误。

接下来,绑定五个指向函数的操作成员(get_bus_baseget_bus_countget_max_transfer_sizeset_bitratetransact)(第 [065 行、第 069] 行)。

最后,constexpr 表达式会根据需要提供默认初始化。

使用 mixin 类

I2cImplProtocol 类可按如下方式使用(通过 //src/devices/i2c/drivers/intel-i2c/intel-i2c-controller.h):

[135] class IntelI2cController : public IntelI2cControllerType,
[136]                            public ddk::I2cImplProtocol<IntelI2cController, ddk::base_protocol> {
[137]  public:
[138]   explicit IntelI2cController(zx_device_t* parent)
[139]       : IntelI2cControllerType(parent), pci_(parent, "pci") {}
[140]
[141]   static zx_status_t Create(void* ctx, zx_device_t* parent);
[142]
[143]   void DdkInit(ddk::InitTxn txn);
...
[170]   uint32_t I2cImplGetBusBase();
[171]   uint32_t I2cImplGetBusCount();
[172]   zx_status_t I2cImplGetMaxTransferSize(const uint32_t bus_id, size_t* out_size);
[173]   zx_status_t I2cImplSetBitrate(const uint32_t bus_id, const uint32_t bitrate);
[174]   zx_status_t I2cImplTransact(const uint32_t bus_id, const i2c_impl_op_t* op_list,
[175]                               const size_t op_count);
[176]
[177]   void DdkUnbind(ddk::UnbindTxn txn);
[178]   void DdkRelease();
[179]
[180]  private:
...

在这里,我们看到 class IntelI2cController 继承自 DDK 的 I2cImplProtocol,并将自己作为参数提供给模板 - 这就是“mixin”概念。这会使 IntelI2cController 类型被替换为类的模板定义中的 D(来自上面的 i2c-impl.h 头文件中的 [086][091][95][100][107] 行)。

我们只以 I2cImplGetMaxTransferSize() 函数为例,它实际上就好像源代码如下所示:

[094] static zx_status_t I2cImplGetMaxTransferSize(void* ctx, uint32_t bus_id, uint64_t* out_size) {
[095]     auto ret = static_cast<IntelI2cController*>(ctx)->I2cImplGetMaxTransferSize(bus_id, out_size);
[096]     return ret;
[097] }

这最终消除了代码中的自动转换样板。这种类型转换是必要的,因为类型信息会在 DDK 边界被清空 - 请注意,上下文 ctx 是一个 void * 指针。

自动生成的评论

Banjo 会自动在包含文件中生成注释,这些注释大致总结了我们上述内容:

[020] // DDK i2cimpl-protocol support
[021] //
[022] // :: Proxies ::
[023] //
[024] // ddk::I2cImplProtocolClient is a simple wrapper around
[025] // i2c_impl_protocol_t. It does not own the pointers passed to it.
[026] //
[027] // :: Mixins ::
[028] //
[029] // ddk::I2cImplProtocol is a mixin class that simplifies writing DDK drivers
[030] // that implement the i2c-impl protocol. It doesn't set the base protocol.
[031] //
[032] // :: Examples ::
[033] //
[034] // // A driver that implements a ZX_PROTOCOL_I2C_IMPL device.
[035] // class I2cImplDevice;
[036] // using I2cImplDeviceType = ddk::Device<I2cImplDevice, /* ddk mixins */>;
[037] //
[038] // class I2cImplDevice : public I2cImplDeviceType,
[039] //                      public ddk::I2cImplProtocol<I2cImplDevice> {
[040] //   public:
[041] //     I2cImplDevice(zx_device_t* parent)
[042] //         : I2cImplDeviceType(parent) {}
[043] //
[044] //     uint32_t I2cImplGetBusBase();
[045] //
[046] //     uint32_t I2cImplGetBusCount();
[047] //
[048] //     zx_status_t I2cImplGetMaxTransferSize(uint32_t bus_id, uint64_t* out_size);
[049] //
[050] //     zx_status_t I2cImplSetBitrate(uint32_t bus_id, uint32_t bitrate);
[051] //
[052] //     zx_status_t I2cImplTransact(uint32_t bus_id, const i2c_impl_op_t* op_list, size_t op_count);
[053] //
[054] //     ...
[055] // };

使用 Banjo

现在,我们已经了解了为 I2C 驱动程序生成的代码,下面我们来看看如何使用它。

待完成@@@

参考文档

@@@ 此处应列出所有内置关键字和基元类型

属性

回想一下上面的示例,protocol 部分有两个属性:@transport("Banjo")@banjo_layout("ddk-protocol") 属性。

transport 属性

所有 Banjo 协议都必须包含 @transport("Banjo"),以指明使用的是 Banjo(而非 FIDL)。

banjo_layout 属性

protocol 前面那一行是 banjo_layout 属性:

[27] @banjo_layout("ddk-protocol")
[28] protocol I2cImpl {

此属性适用于下一项;在本示例中,就是整个 protocol。 每个接口只允许有一个布局。

事实上,目前支持 3 种 BanjoLayout 属性类型:

  • ddk-protocol
  • ddk-interface
  • ddk-callback

为理解这些布局类型的工作原理,假设我们有两个驱动程序:AB。 驱动程序 A 生成一个设备,B 随后会附加到该设备(使 B 成为 A 的子级)。

如果 B 随后通过 device_get_protocol() 向 DDK 查询其父项的“协议”,则会收到 ddk-protocolddk-protocol 是父级向其子级提供的一组回调。

一种协议函数可以是注册一个“反向协议”,这样子函数就可以提供一组回调,供父项改为触发。这是ddk-interface

从代码生成的角度来看,这两个(ddk-protocolddk-interface)看起来几乎完全相同,但存在一些细微的命名差异(ddk-protocol 会自动将“protocol”一词附加到生成的结构体 / 类的末尾,而 ddk-interface 则不会)。

ddk-callbackddk-interface 进行了细微优化,适用于接口只有一个函数的情况。而不是生成两个结构,例如:

struct interface {
   void* ctx;
   interface_function_ptr_table* callbacks;
};

struct interface_function_ptr_table {
   void (*one_function)(...);
}

ddk-callback 将生成一个内联函数指针的结构:

struct callback {
  void* ctx;
  void (*one_function)(...);
};

async 属性

如需查看 @async 属性的示例,请参阅 fuchsia.hardware.block Block 协议。

protocol 部分中,我们会看到 @async 属性:

[254] protocol Block {
...       /// comments (removed)
[268]     @async

@async 属性是一种用于使协议消息不同步的方法。它会自动生成一个回调类型,在该类型中,输出参数是回调的输入。原始方法将不包含其签名中指定的任何输出参数。

在上述协议中,有一个 Queue 方法声明为:

[268] @async
[269] Queue(resource struct {
[270]     @in_out
[271]     txn BlockOp;
[272] }) -> (resource struct {
[273]     status zx.Status;
[274]     @mutable
[275]     op BlockOp;
[276] });

如果(如上所述)与 @async 属性结合使用,则意味着我们希望 Banjo 调用一个回调函数,以便我们可以处理输出数据(上面的第二个 BlockOp,表示来自块存储设备的数据)。

具体流程如下。 我们通过第一个 BlockOp 参数将数据发送到块存储设备。一段时间后,块存储设备可能会应我们的请求生成数据。由于我们指定了 @async,Banjo 会生成函数以将回调函数作为输入。

在 C 代码中,block.h 文件中的以下两行非常重要:

[085] typedef void (*block_queue_callback)(void* ctx, zx_status_t status, block_op_t* op);
...
[211] void (*queue)(void* ctx, block_op_t* txn, block_queue_callback callback, void* cookie);

在 C++ 中,我们在两个位置引用回调:

[113] static void BlockQueue(void* ctx, block_op_t* txn, block_queue_callback callback, void* cookie) {
[114]     static_cast<D*>(ctx)->BlockQueue(txn, callback, cookie);
[115] }

[201] void Queue(block_op_t* txn, block_queue_callback callback, void* cookie) const {
[202]     ops_->queue(ctx_, txn, callback, cookie);
[203] }

请注意 C++ 与 C 的相似之处:这是因为生成的代码包含 C++ 头文件作为 C++ 头文件的一部分。

事务回调具有以下参数:

参数 含义
ctx Cookie
status 异步响应的状态(由被调用方提供)
op 传输的数据

这与我们前面讨论的 @banjo_layout("ddk-callback") 属性有何不同?

首先,没有包含回调和 Cookie 值的 struct,它们会作为参数内嵌。

其次,提供的回调是“一次性使用”函数。 也就是说,对于提供给协议方法的每次调用,都应调用一次且只能调用一次。 相比之下,ddk-callback 提供的方法是“注册一次,调用多次”的函数(类似于 ddk-interfaceddk-protocol)。因此,ddk-callbackddk-interface 结构通常会将 register()unregister() 调用配对,以便通知父设备何时应停止调用这些回调。

使用 @async 时还需要注意,每次调用协议方法时都必须调用其回调,并且必须提供随附的 Cookie。否则会导致出现未定义的行为(可能是泄露、死锁、超时或崩溃)。

虽然目前情况并非如此,但 C++ 和未来语言绑定(如 Rust)将在生成的代码中提供基于“未来”/“promise”样式的 API,这些 API 基于这些回调构建,以避免出错。

关于 @async,还有一点需要注意:@async 属性应用于紧随其后的方法,而不适用于任何其他方法。

缓冲区属性

此属性适用于 vector 类型的协议方法参数,以表明它们将用作缓冲区。实际上,它只会影响生成的参数的名称。

callee_allocated 属性

当应用于 vector 类型的协议方法输出参数时,此属性会传达以下事实:向量的内容应由方法调用的接收器分配。

derive_debug 属性(仅限 C 绑定)

应用于枚举声明时,系统会为 C 绑定生成一个辅助 *_to_str() 函数,该函数为枚举的每个值返回 const char*。例如,使用此属性声明的枚举,如

@derive_debug
enum ExampleEnum {
    VAL_ONE = 1;
    VAL_TWO = 2;
};

将生成以下定义。

#ifndef FUNC_EXAMPLE_ENUM_TO_STR_
#define FUNC_EXAMPLE_ENUM_TO_STR_
static inline const char* example_enum_to_str(example_enum_t value) {
  switch (value) {
    case EXAMPLE_ENUM_VAL_ONE:
      return "EXAMPLE_ENUM_VAL_ONE";
    case EXAMPLE_ENUM_VAL_TWO:
      return "EXAMPLE_ENUM_VAL_TWO";
  }
  return "UNKNOWN";
}
#endif

inner_pointer 属性

vector 类型的协议输入参数的上下文中,此属性会将矢量的内容转换为指向对象的指针,而不是对象本身。

in_out 属性

将此属性添加到协议方法的输入参数会使该参数可变,从而有效地将其转换为“in-out”参数。

可变属性

此属性应用于使 vectorstring 类型的 struct/union 字段可变。

命名空间型属性

此属性适用于 const 声明,并使得 C 后端在常量名称前面加上蛇形 FIDL 库名称,例如 library_name_CONSTANT_K,而不是 CONSTANT_K。可能需要此属性,以避免与同一 build 目标中的 FIDL hlcpp 常量绑定发生名称冲突。

out_of_line_contents 属性

此属性允许 struct/unionvector 字段的内容存储在容器之外。

prepare_c_names 属性

此属性适用于 struct 声明,并使其字段名称在通过 C 后端运行时保持不变。

班卓琴

Banjo 会为每个协议生成一个 C++ 模拟类。此模拟可以传递给测试中的协议用户。

建筑

Zircon 中的测试会自动获取模拟头文件。如果测试的 Zircon 过大,则测试必须依赖于带有 _mock 后缀的协议目标,例如 //sdk/banjo/fuchsia.hardware.gpio:fuchsia.hardware.gpio_banjo_cpp_mock

使用模拟

测试代码必须包含带 -mock 后缀的协议标头,例如 #include <fuchsia/hardware/gpio/cpp/banjo-mock.h>

我们来考虑以下 Banjo 协议代码段:

[20] @transport("Banjo")
[21] @banjo_layout("ddk-protocol")
[22] protocol Gpio {
 ...
[53]     /// Gets an interrupt object pertaining to a particular GPIO pin.
[54]     GetInterrupt(struct {
[55]         flags uint32;
[56]     }) -> (resource struct {
[57]         s zx.Status;
[58]         irq zx.Handle:INTERRUPT;
[59]     });
 ...
[82] };

以下是 Banjo 生成的模拟类的相应位:

[034] class MockGpio : ddk::GpioProtocol<MockGpio> {
[035] public:
[036]     MockGpio() : proto_{&gpio_protocol_ops_, this} {}
[037]
[038]    virtual ~MockGpio() {}
[039]
[040]     const gpio_protocol_t* GetProto() const { return &proto_; }
 ...
[067]     virtual MockGpio& ExpectGetInterrupt(zx_status_t out_s, uint32_t flags, zx::interrupt out_irq) {
[068]         mock_get_interrupt_.ExpectCall({out_s, std::move(out_irq)}, flags);
[069]         return *this;
[070]     }
 ...
[092]     void VerifyAndClear() {
 ...
[098]         mock_get_interrupt_.VerifyAndClear();
 ...
[103]     }
 ...
[131]     virtual zx_status_t GpioGetInterrupt(uint32_t flags, zx::interrupt* out_irq) {
[132]         std::tuple<zx_status_t, zx::interrupt> ret = mock_get_interrupt_.Call(flags);
[133]         *out_irq = std::move(std::get<1>(ret));
[134]         return std::get<0>(ret);
[135]     }

MockGpio 类可实现 GPIO 协议。ExpectGetInterrupt 用于就 GpioGetInterrupt 的调用方式设定预期。GetProto 用于获取可传递给被测代码的 gpio_protocol_t。此代码将调用 GpioGetInterrupt,以确保使用正确的参数调用它,并返回 ExpectGetInterrupt 指定的值。最后,测试可以调用 VerifyAndClear 来验证是否满足所有预期。以下是使用此模拟的示例测试:

TEST(SomeTest, SomeTestCase) {
    ddk::MockGpio gpio;

    zx::interrupt interrupt;
    gpio.ExpectGetInterrupt(ZX_OK, 0, zx::move(interrupt))
        .ExpectGetInterrupt(ZX_ERR_INTERNAL, 100, zx::interrupt());

    CodeUnderTest dut(gpio.GetProto());
    EXPECT_OK(dut.DoSomething());

    ASSERT_NO_FATAL_FAILURE(gpio.VerifyAndClear());
}

等式运算符替换

使用具有结构类型的 Banjo 模拟的测试必须定义等式运算符替换项。例如,对于结构体类型 some_struct_type,测试必须定义一个具有签名的函数

bool operator==(const some_struct_type& lhs, const some_struct_type& rhs);

位于顶级命名空间中

自定义模拟

预计某些测试可能需要更改默认的模拟行为。为了帮助做到这一点,所有预期方法和协议方法均为 virtual,并且所有 MockFunction 成员均为 protected

异步方法

默认情况下,Banjo 会模拟所有异步方法发出回调。