班卓琴是一个“转译器”(如 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++ 代码,并计划支持 Rust 。
C
C 的实现相对简单:
struct
和union
几乎直接映射到对应的 C 语言版本。enum
和常量会生成为#define
宏。protocol
会生成为两个struct
: <ph type="x-smartling-placeholder">- </ph>
- 一个函数表,以及
- 一个具有指向函数表指针和上下文的结构体。
- 系统还会生成一些辅助函数。
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
] 行几乎是 struct I2cImplOp
的直接映射
来自以上 .fidl
文件(第 [16
.. 23
] 行)。
精明的 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] };
这会创建一个结构定义,其中包含在protocol
原始 .fidl
文件的第 [30]
、[34]
、[37]
、[44]
和 [43]
行。
请注意发生的名称更改 - 这就是您映射
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] };
有以下四个构造函数:
- 默认属性 (
[114]
),用于将ops_
和ctx_
设置为nullptr
; - 一个初始化程序 (
[116]
),接受指向i2c_impl_protocol_t
结构的指针,并填充 结构中与其同名的ops_
和ctx
_ 字段;以及 - 一个初始化程序 (
[119]
),用于从zx_device_t
中提取ops_
和ctx_
信息。 - 初始化程序 (
[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 类
好吧,这部分很简单。 在下一部分,我们将介绍混音 和 CRTP — 好奇的重复性模板 模式。
了解“形状”(为简要说明,删除了注释行) 目的):
[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_base
、get_bus_count
、
get_max_transfer_size
、set_bitrate
和 transact
)绑定(行 [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
为了理解这些布局类型的工作原理,我们假设有两个驱动因素,
A
和B
。
驱动程序 A
会生成一个设备,B
随后会将其附加到该设备(使 B
成为 A
的子项)。
如果为 B
,则向 DDK 查询其父项的“协议”通过 device_get_protocol() 传输
则会获得 ddk-protocol
。
ddk-protocol
是父级向其子级提供的一组回调。
其中一个协议函数可以是注册“reverse-protocol”
而子级则提供一组回调供父级触发。
这是 ddk-interface
。
从代码生成的角度来看,这两个(ddk-protocol
和 ddk-interface
)
除了一些细微的命名差异,看起来几乎完全相同(ddk-protocol
自动附加“协议”一词添加到生成的结构体 / 类的末尾,
而 ddk-interface
则不可以)。
ddk-callback
是对 ddk-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-interface
和 ddk-protocol
)。
因此,ddk-callback
和 ddk-interface
结构通常具有
配对 register() 和 unregister() 调用,以便告知父级设备
何时应停止调用这些回调。
使用
@async
时还有一个注意事项,即必须针对每个 协议方法调用,并且必须提供随附的 Cookie。 否则,会导致不确定的行为(可能会发生泄漏、死锁或 超时或崩溃)。
虽然目前并非如此,但 C++ 和未来的语言绑定(如 Rust) 将提供“未来”/"promise"在生成的代码中基于样式的 API,构建于 这些回调可防止出现错误
好了,关于
@async
还有一个注意事项:@async
属性仅适用 直接转到下面的方法;而不是任何其他方法。
缓冲区属性
此属性适用于 vector
类型的协议方法参数,旨在表明它们
用作缓冲区实际上,它只影响生成的参数的名称。
Callee_allocated 属性
当应用于 vector
类型的协议方法输出参数时,该属性会传达事实
矢量的内容应由方法调用的接收器分配。
generate_debug 属性(仅限 C 绑定)
辅助 *_to_str()
函数应用于枚举声明时
将为 C 绑定生成该绑定,这会为每个绑定返回一个 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 属性
向协议方法输入参数添加此属性可使该参数可变,实际上 并将其转变为“由新到旧”参数。
可变属性
此属性应该用于使 vector
或 string
类型的 struct
/union
字段可变。
命名空间型属性
此属性适用于 const
声明,并使 C 后端在
常量名称带有蛇形命名法的 FIDL 库名称,例如改为library_name_CONSTANT_K
共 CONSTANT_K
个。可能需要此属性,以避免名称与 FIDL hlcpp 常量冲突
绑定。
out_of_line_contents 属性
此属性允许将 struct
/union
中 vector
字段的内容存储在外部
容器的位置
keep_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 模拟从所有异步方法发出回调。