C++语言风格
C++比c语言版本稍微复杂一些。让我们来看一下。
- banjo转译器生成三个文件
1、第一个文件在c语言版本中已经介绍讨论过了,其他两个文件在目录`//zircon/build-`_TARGET_`/system/banjo/ddk-protocol-i2c/gen/include/ddktl/protocol/`。
2、`i2c.h`——你的程序需要包含的文件。
3、`i2c-internal.h`——内部文件,由i2c.h文件包含。
通常,_TARGET_ 是目标构建架构,比如x64,arm64等。
"internal"文件包含文件声明和断言,我们可以安全地跳过这些。
C++版本的`i2c.h`文件内容是相当的长,所以我们需要将他分成小片段来查看。下面是文件i2c.h概览图,显示了从开始到每个片段的行数。
Line | Section
--------------|----------------------------
1 | [boilerplate](#a-simple-example-c-boilerplate-2)
20 | [auto generated usage comments](#auto_generated-comments)
55 | [class I2cProtocol](#the-i2cprotocol-mixin-class)
99 | [class I2cProtocolClient](#the-i2cprotocolclient-wrapper-class)
- 样板
这份样板的内容几乎就是你所期望的:
[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] // MODIFY system/banjo/ddk-protocol-i2c/i2c.banjo INSTEAD.
[007]
[008] #pragma once
[009]
[010] #include <ddk/driver.h>
[011] #include <ddk/protocol/i2c.h>
[012] #include <ddktl/device-internal.h>
[013] #include <zircon/assert.h>
[014] #include <zircon/compiler.h>
[015] #include <zircon/types.h>
[016] #include <lib/zx/interrupt.h>
[017]
[018] #include "i2c-internal.h"
它的#include包含了一堆ddk和os的头文件,i2c相关的包含:
C版本的头文件(`[011]`行,这意味着所讨论的所有内容[上面的C部分](#a-simple-example-c-1)也适用于此处);还有生成的`i2c-internal.h`文件,在[018]行。
接下来是自动生成的使用注释部分;我们稍后将回到自动生成注释的地方(#auto_generated-comments),因为一旦我们看到实际代码的类声明,这个注释它会显得更有意义。
这两个类的声明被封装在DDK的域名中,如代码所示:
[053] namespace ddk {
...
[150]} // namespace ddk
- I2cProtocolClient封装类
I2cProtocolClient类是i2c_protocol_t结构体的简单封装,该结构体定义在c头文件中。之前我们在协议结构体中谈论过此结构体。
来看下封装类的实体代码:
[099] class I2cProtocolClient {
[100] public:
[101] I2cProtocolClient()
[102] : ops_(nullptr), ctx_(nullptr) {}
[103] I2cProtocolClient(const i2c_protocol_t* proto)
[104] : ops_(proto->ops), ctx_(proto->ctx) {}
[105]
[106] I2cProtocolClient(zx_device_t* parent) {
[107] i2c_protocol_t proto;
[108] if (device_get_protocol(parent, ZX_PROTOCOL_I2C, &proto) == ZX_OK) {
[109] ops_ = proto.ops;
[110] ctx_ = proto.ctx;
[111] } else {
[112] ops_ = nullptr;
[113] ctx_ = nullptr;
[114] }
[115] }
[116]
[117] void GetProto(i2c_protocol_t* proto) const {
[118] proto->ctx = ctx_;
[119] proto->ops = ops_;
[120] }
[121] bool is_valid() const {
[122] return ops_ != nullptr;
[123] }
[124] void clear() {
[125] ctx_ = nullptr;
[126] ops_ = nullptr;
[127] }
[128] // Writes and reads data on an i2c channel. Up to I2C_MAX_RW_OPS operations can be passed in.
[129] // For write ops, i2c_op_t.data points to data to write. The data to write does not need to be
[130] // kept alive after this call. For read ops, i2c_op_t.data is ignored. Any combination of reads
[131] // and writes can be specified. At least the last op must have the stop flag set.
[132] // The results of the operations are returned asynchronously via the transact_cb.
[133] // The cookie parameter can be used to pass your own private data to the transact_cb callback.
[134] void Transact(const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie) const {
[135] ops_->transact(ctx_, op_list, op_count, callback, cookie);
[136] }
[137] // Returns the maximum transfer size for read and write operations on the channel.
[138] zx_status_t GetMaxTransferSize(size_t* out_size) const {
[139] return ops_->get_max_transfer_size(ctx_, out_size);
[140] }
[141] zx_status_t GetInterrupt(uint32_t flags, zx::interrupt* out_irq) const {
[142] return ops_->get_interrupt(ctx_, flags, out_irq->reset_and_get_address());
[143] }
[144]
[145] private:
[146] i2c_protocol_ops_t* ops_;
[147] void* ctx_;
[148] };
上面代码有三个构造函数。
101行`ops_` 和 `ctx_`被赋值为`nullptr`。
103行的初始化函数使用结构体指针`i2c_protocol_t`,构建`ops_` 和 `ctx_`的对应值。
106行另一个初始化函数,从父设备`zx_device_t`获取`ops_` 和 `ctx_`值。
第三个构造函数是推荐使用的,使用示例如下:
ddk::I2cProtocolClient i2c(parent);
if (!i2c.is_valid()) {
return ZX_ERR_*; // return an appropriate error
}
提供了三个所需要的成员函数:
117行GetProto()获取`ops_` 和 `ctx_`成员到对应的协议结构体中。
121行is_valid()返回一个布尔值,判断是否该类已由一个协议初始化。
124行clear()清除`ops_` 和 `ctx_`成员的指针赋值信息。
接着,我们发现曾在banjo文件中的三个成员函数:
134行的Transact()
138行的GetMaxTransferSize()
141行的GetInterrupt()
这些工作只是喜欢包含C文件的三个包装函数;也就是说,它们通过调用相应的函数指针将它们的参数传递。
事实上,对比c语言版本的i2c_get_max_transfer_size()函数:
[56] static inline zx_status_t i2c_get_max_transfer_size(const i2c_protocol_t* proto, size_t* out_size) {
[57] return proto->ops->get_max_transfer_size(proto->ctx, out_size);
[58] }
和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()。
- The I2cProtocol类
这是容易的部分。对此的下部分章节,我们将在https://en.wikipedia.org/wiki/Mixin讲解。让我们先来理解类的形状。代码如下:
[055] template <typename D, typename Base = internal::base_mixin>
[056] class I2cProtocol : public Base {
[057] public:
[058] I2cProtocol() {
[059] internal::CheckI2cProtocolSubclass<D>();
[060] i2c_protocol_ops_.transact = I2cTransact;
[061] i2c_protocol_ops_.get_max_transfer_size = I2cGetMaxTransferSize;
[062] i2c_protocol_ops_.get_interrupt = I2cGetInterrupt;
[063]
[064] if constexpr (internal::is_base_proto<Base>::value) {
[065] auto dev = static_cast<D*>(this);
[066] // Can only inherit from one base_protocol implementation.
[067] ZX_ASSERT(dev->ddk_proto_id_ == 0);
[068] dev->ddk_proto_id_ = ZX_PROTOCOL_I2C;
[069] dev->ddk_proto_ops_ = &i2c_protocol_ops_;
[070] }
[071] }
[072]
[073] protected:
[074] i2c_protocol_ops_t i2c_protocol_ops_ = {};
[075]
[076] private:
...
[083] static void I2cTransact(void* ctx, const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie) {
[084] static_cast<D*>(ctx)->I2cTransact(op_list, op_count, callback, cookie);
[085] }
...
[087] static zx_status_t I2cGetMaxTransferSize(void* ctx, size_t* out_size) {
[088] auto ret = static_cast<D*>(ctx)->I2cGetMaxTransferSize(out_size);
[089] return ret;
[090] }
[091] static zx_status_t I2cGetInterrupt(void* ctx, uint32_t flags, zx_handle_t* out_irq) {
[092] zx::interrupt out_irq2;
[093] auto ret = static_cast<D*>(ctx)->I2cGetInterrupt(flags, &out_irq2);
[094] *out_irq = out_irq2.release();
[095] return ret;
[096] }
[097] };
I2CProtocol类继承自base类,base由第二个模板参数指定。如果没有人为指定,它将默认使用internal::base_mixin类,并且没有特别的模数产生。然而,如果要显示的指定该base,应该使用ddk::base_protocol类,这会把asserts检查机制加上,双重检查只有一个base protocol混合型。此外,特殊的DDKTL字段设置为在驱动程序触发DdkAdd()时自动将此协议注册为基本协议。
构造函数会调用内部有效性函数,在59行CheckI2cProtocolSubclass()。定义在头文件i2c-internal.h,它有几种静态断言调用。
‘D’类被期望去实现三个成员函数使静态方法有效工作:I2cTransact()、I2cGetMaxTransferSize()、I2cGetInterrupt()。如果没有提供‘D’,编译器会产生一些模板错误。
静态断言用于产生描述不清的错误信息,一般人不能理解。
接下来,在60~62行,指针函数(`transact`,`get_max_transfer_size`,`get_interrupt`)被绑定了。最后,如果需要,`constexpr`表达式提供默认初始化。
- 使用mixin类
I2cProtocol类可以按照如下方式使用。来自文件:`//zircon/system/dev/bus/platform/platform-proxy-device.h`。
[01] class ProxyI2c : public ddk::I2cProtocol<ProxyI2c> {
[02] public:
[03] explicit ProxyI2c(uint32_t device_id, uint32_t index, fbl::RefPtr<PlatformProxy> proxy)
[04] : device_id_(device_id), index_(index), proxy_(proxy) {}
[05]
[06] // I2C protocol implementation.
[07] void I2cTransact(const i2c_op_t* ops, size_t cnt, i2c_transact_callback transact_cb,
[08] void* cookie);
[09] zx_status_t I2cGetMaxTransferSize(size_t* out_size);
[10] zx_status_t I2cGetInterrupt(uint32_t flags, zx::interrupt* out_irq);
[11]
[12] void GetProtocol(i2c_protocol_t* proto) {
[13] proto->ops = &i2c_protocol_ops_;
[14] proto->ctx = this;
[15] }
[16]
[17] private:
[18] uint32_t device_id_;
[19] uint32_t index_;
[20] fbl::RefPtr<PlatformProxy> proxy_;
[21] };
我们看到ProxyI2c类继承自I2cProtocol类,并将自身ProxyI2c作为I2cProtocol的模板参数。这就是Mixin的概念。这导致`ProxyI2c`类型在类的模板定义中替换为'D`。(来自84,88,93行的i2c.h文件)。
作为例子,看一下I2cGetMaxTransferSize()函数,阅读源码发现它简洁高效:
[087] static zx_status_t I2cGetMaxTransferSize(void* ctx, size_t* out_size) {
[088] auto ret = static_cast<ProxyI2c*>(ctx)->I2cGetMaxTransferSize(out_size);
[089] return ret;
[090] }
这最终消除了代码中的指向自己的样板。这种转换是必要的,因为类型信息在DDK边界被擦除;回想一下上下文`ctx`是一个`void *`指针。
- 自动生成注释
banjo会在头文件中自动生成注释,基本总结了写了什么内容。例如:
[020] // DDK i2c-protocol support
[021] //
[022] // :: Proxies ::
[023] //
[024] // ddk::I2cProtocolClient is a simple wrapper around
[025] // i2c_protocol_t. It does not own the pointers passed to it
[026] //
[027] // :: Mixins ::
[028] //
[029] // ddk::I2cProtocol is a mixin class that simplifies writing DDK drivers
[030] // that implement the i2c protocol. It doesn't set the base protocol.
[031] //
[032] // :: Examples ::
[033] //
[034] // // A driver that implements a ZX_PROTOCOL_I2C device.
[035] // class I2cDevice;
[036] // using I2cDeviceType = ddk::Device<I2cDevice, /* ddk mixins */>;
[037] //
[038] // class I2cDevice : public I2cDeviceType,
[039] // public ddk::I2cProtocol<I2cDevice> {
[040] // public:
[041] // I2cDevice(zx_device_t* parent)
[042] // : I2cDeviceType(parent) {}
[043] //
[044] // void I2cTransact(const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie);
[045] //
[046] // zx_status_t I2cGetMaxTransferSize(size_t* out_size);
[047] //
[048] // zx_status_t I2cGetInterrupt(uint32_t flags, zx::interrupt* out_irq);
[049] //
[050] // ...
[051] // };
- 使用banjo
我们还需要FIDL教程和驱动程序编写教程之间的内容,以描述banjo的使用。基本上就是,编写一个简单的协议,然后描述对应它的驱动程序。另一个在协议上绑定并使用该协议的驱动程序。如果有意义的话,可以修改现有的驱动程序编写教程,以获得有关banjo使用的更多详细信息。我认为当前的驱动程序教程也专注于C的使用,而获得C ++版本(使用ddktl)可能会带来最大的价值。这个已经在我的工作队列中,“使用ddktl教程”(C++ DDK wrappers)。
现在我们看见生成的I2C驱动代码,让我们来看一下怎么使用它。
(原英文文档待完成。。。)
参考:这里主要列出内建关键字和原语类型。
属性:例子中的protocol部分有两个属性[layout]和[async]。
Layout
目前有三个可选类型,
* `ddk-protocol`
* `ddk-interface`
* `ddk-callback`
在protoco的前一行就是layout属性,代码如下:
[19] [Layout = "ddk-protocol"]
[20] protocol I2c {
为了理解layout类型怎么工作,我们现在假设有两个驱动A和B。A驱动产出一个设备,然后B连接到该设备上,使得B作为A的子。如果B通过device_get_protocol()获取DDK它的父协议,B会得到一个ddk协议,`ddk-protocol`。`ddk-protocol`就是一组回调的集合,由父设备提供给子设备。
其中一个协议功能可以是注册一个“反向协议”,由此子进程为父进程提供一组回调来代替。这就是ddk接口,`ddk-interface`。
从生成的代码来看,`ddk-protocol`和`ddk-interface`看起来大致相同,除了名字看上去有细微的差异。(`ddk-protocol`会自动将字符"protocol"附加到生成的结构体或者类字符的后面,`ddk-interface`却不会这么做)。
`ddk-callback`是对`ddk-interface`进行了稍微优化,用于当`ddk-interface`只有一个函数接口的时候。并不会生成两个结构体,如下:
struct interface {
void* ctx;
inteface_function_ptr_table* callbacks;
};
struct interface_function_ptr_table {
void (*one_function)(...);
}
`ddk-callback`只会生成一个带有函数指针成员的结构体,如下:
struct callback {
void* ctx;
void (*one_function)(...);
};
- 异步属性
在 有protocol的一行,我们可以看到另一种属性:异步属性。
[20] protocl I2c {
... /// comments (removed)
[27] [Async]
异步属性是一种,使协议消息不同步的方式。它会自动生成一个回调类型,其中输出参数是回调的输入。原始方法不会在其签名中指定任何输出参数。回想一下上面的例子,我们有一个`Transact`方法。
[28] Transact(vector<I2cOp> op) -> (zx.status status, vector<I2cOp> op);
当和`[Async]`属性一起使用时,意味着,我们希望banjo调用一个回调函数,以便我们处理输出数据(如上面代码的`vector<I2cOp>`参数,表示来自I2C总线的数据)。
- 它怎么工作的
我们通过第一个参数`vector<I2cOp>`发送数据到I2C总线。一段时间后,I2C总线可能会发出数据作为我们请求的回应。由于我们设置了异步属性,banjo产生一个带有回调函数作为输入参数的函数。
C语言版本中,`i2c.h`头文件中这两行比较重要:
[19] typedef void (*i2c_transact_callback)(void* ctx, zx_status_t status, const i2c_op_t* op_list, size_t op_count);
[36] void (*transact)(void* ctx, const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie);
C++版本中,我们涉及到两处回调函数:
[083] static void I2cTransact(void* ctx, const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie) {
[084] static_cast<D*>(ctx)->I2cTransact(op_list, op_count, callback, cookie);
[085] }
和
[134] void Transact(const i2c_op_t* op_list, size_t op_count, i2c_transact_callback callback, void* cookie) const {
[135] ops_->transact(ctx_, op_list, op_count, callback, cookie);
[136] }
注意C++和C为什么如此相似,因为生成的代码将c头文件作为c++的部分头文件。
Transaction回调函数有如下几个参数:
Argument | Meaning
-----------|----------------------------------------
`ctx` | the cookie
`status` | status of the asynchronous response (provided by callee)
`op_list` | the data from the transfer
`op_count` | the number of elements in the transfer
这里和我们上面提到的`ddk-callback` `[Layout]`属性有什么不同呢?
第一、这里没有包含callback和cookie成员的结构体,他们被当作内联参数传递进去了。
第二、提供的回调时一次性使用功能。
就是说,它能且只能被调用一次它所提供的协议方法。
相比之下,`ddk-callback`的方法是一种注册一次,调用多次的函数类型(类似于`ddk-interface`和`ddk-protocol`)。因此,`ddk-callback` 和 `ddk-interface`结构体,通常成对调用register()和unregister(),告诉父设备什么时候应该调用回调函数。
另外`[Async]`需要提醒的是,回调必须通过协议方法调用,并且需要提供同行的cookie。不这样做,可能会导致未定义的行为(比如内存泄漏、死锁、超时或者崩溃)。
虽然不是目前的情况,C ++和未来的语言绑定(如Rust)将在生成的代码中提供基于“未来”/“承诺”样式的API这些回调是为了防止错误。还有一个关于“[异步]”的警告; `[Async]`属性仅适用于紧随其后的方法; 没有任何其他方法。