In this section, we'll look at the C++ DDK Template Library, or "DDKTL" for short. It's a set of C++ templated classes that simplify the work of writing a driver by providing mixins that ensure type safety and perform basic functionality.
If you're not familiar with mixins, you should read the Wikipedia articles on: * mixins and * CRTPs — or Curiously Recurring Template Patterns.
The mixins that we'll be discussing are defined in
//src/lib/ddktl/include/ddktl/device.h
.
The following mixins are provided:
Mixin class | Function | Purpose |
---|---|---|
ddk::GetProtocolable |
DdkGetProtocol() | fetches the protocol |
ddk::Initializable |
DdkInit() | called after DdkAdd(), for completing initialization of a device safely |
ddk::Unbindable |
DdkUnbind() | called when this device is being removed |
ddk::Suspendable |
DdkSuspend() | to suspend device |
ddk::Resumable |
DdkResume() | to resume device |
ddk::PerformanceTunable |
DdkSetPerformanceState() | to transition the performant state |
ddk::AutoSuspendable |
DdkConfigureAutoSuspend() | to configure whether a driver can auto suspend the device |
ddk::Rxrpcable |
DdkRxrpc() | remote messages for bus devices |
When defining the class for your device, you specify which functions it will support by including the appropriate mixins. For example (line numbers added for documentation purposes only):
[01] using DeviceType = ddk::Device<MyDevice,
[02] ddk::Initializable, // safely initialize after **DdkAdd()**
[03] ddk::Unbindable>; // safely clean up before **DdkRelease()**
This creates a shortcut to DeviceType
.
The ddk::Device
templated class takes one or more arguments, with the
first argument being the base class (here, MyDevice
).
The additional template arguments are the mixins that define
which FDF device member functions are implemented.
Once defined, we can then declare our device class (MyDevice
) as inheriting
from DeviceType
:
[05] class MyDevice : public DeviceType {
[06] public:
[07] explicit MyDevice(zx_device_t* parent)
[08] : DeviceType(parent) {}
[09]
[10] zx_status_t Bind() {
[11] // Any other setup required by MyDevice.
[12] // The device_add_args_t will be filled out by the base class.
[13] return DdkAdd("my-device-name");
[14] }
[15]
[16] // Methods required by the ddk mixins
[17] void DdkInit(ddk::InitTxn txn);
[18] void DdkUnbind(ddk::UnbindTxn txn);
[19] void DdkRelease();
[20] };
Because the DeviceType
class contains mixins (lines [02
.. 03]
:
Initializable
and Unbindable
), we're required to provide the respective
function implementations (lines [17
.. 18]
) in our class.
All DDKTL classes must provide a release function (here, line [19]
provides
DdkRelease()), so that's why we didn't specify this in the mixin definition
for DeviceType
.
Keep in mind that once you reply to the
InitTxn
(provided in DdkInit()) you cannot safely use the device instance — other threads may call DdkUnbind(), which typically calls DdkRelease(), and that frees the driver's device context. This would constitute a "use-after-free" violation. For devices that do not implement DdkInit(), this would apply after you call DdkAdd().
Recall from the preceding sections that your device must register with the driver manager in order to be usable. This is accomplished as follows:
[26] zx_status_t my_bind(zx_device_t* device,
[27] void** cookie) {
[28] auto dev = std::make_unique<MyDevice>(device);
[29] auto status = dev->Bind();
[30] if (status == ZX_OK) {
[31] // driver manager is now in charge of the memory for dev
[32] dev.release();
[33] }
[34] return status;
[35] }
Here, my_bind() creates an instance of MyDevice
, calls the Bind() routine,
and then returns a status.
Bind() (line [12]
in the class MyDevice
declaration above), performs whatever
setup it needs to, and then calls DdkAdd() with the device name.
Since the device is Initializable
, the driver manager will then call your implementation
of DdkInit() with an InitTxn
. The device will be invisible and not able to be
unbound until the device replies to the InitTxn
. This reply can be done from any
thread — it does not necessarily need to be before returning from DdkInit().
After replying to the InitTxn
, your device will be visible in the Device
filesystem.
As an example, in the directory //src/devices/block/drivers/zxcrypt
we have a typical device declaration (device.h
):
[01] class Device;
[02] using DeviceType = ddk::Device<Device,
[03] ddk::GetProtocolable,
[04] ddk::Unbindable>;
...
[05] class Device final : public DeviceType,
[06] public ddk::BlockImplProtocol<Device, ddk::base_protocol>,
[07] public ddk::BlockPartitionProtocol<Device>,
[08] public ddk::BlockVolumeProtocol<Device> {
[09] public:
...
[10] // ddk::Device methods; see ddktl/device.h
[11] zx_status_t DdkGetProtocol(uint32_t proto_id, void* out);
[12] void DdkUnbind(ddk::UnbindTxn txn);
[13] void DdkRelease();
...
Lines [01
.. 05]
declare the shortcut DeviceType
with the base class
Device
and its mixins, GetProtocolable
and Unbindable
.
What's interesting here is line [06]
: we not only inherit from the DeviceType
,
but also from other classes on lines [07
.. 09]
.
Lines [11
.. 15]
provide the prototypes for the three optional mixins and the
mandatory DdkRelease() member function.
Here's an example of the zxcrypt
device's DdkGetProtocol
implementation (from
device.cc
):
zx_status_t Device::DdkGetProtocol(uint32_t proto_id, void* out) {
auto* proto = static_cast<ddk::AnyProtocol*>(out);
proto->ctx = this;
switch (proto_id) {
case ZX_PROTOCOL_BLOCK_IMPL:
proto->ops = &block_impl_protocol_ops_;
return ZX_OK;
case ZX_PROTOCOL_BLOCK_PARTITION:
proto->ops = &block_partition_protocol_ops_;
return ZX_OK;
case ZX_PROTOCOL_BLOCK_VOLUME:
proto->ops = &block_volume_protocol_ops_;
return ZX_OK;
default:
return ZX_ERR_NOT_SUPPORTED;
}
}
As seen in a driver
Let's take a look at how a driver uses the DDKTL.
We're going to use the USB XHCI driver for this set of code samples; you can find it
here: //src/devices/usb/drivers/xhci/usb-xhci.cpp
.
Drivers have a driver declaration (usually at the bottom of the source file), like this:
ZIRCON_DRIVER(driver_name, driver_ops, "zircon", "0.1");
The second parameter to the ZIRCON_DRIVER() macro is a zx_driver_ops_t
structure.
In the C++ version we use a lambda function to help with initialization:
namespace usb_xhci {
...
static zx_driver_ops_t driver_ops = [](){
zx_driver_ops_t ops = {};
ops.version = DRIVER_OPS_VERSION;
ops.bind = UsbXhci::Create;
return ops;
}();
} // namespace usb_xhci
ZIRCON_DRIVER(usb_xhci, usb_xhci::driver_ops, "zircon", "0.1");
This executes the driver_ops() lambda, which returns an initialized zx_driver_ops_t
structure.
Why the lambda? C++ doesn't like partial initialization of structures, so we start with an
empty instance of ops
, set the fields we're interested in, and then return the structure.
The UsbXhci::Create() function is as follows:
[01] zx_status_t UsbXhci::Create(void* ctx, zx_device_t* parent) {
[02] fbl::AllocChecker ac;
[03] auto dev = std::unique_ptr<UsbXhci>(new (&ac) UsbXhci(parent));
[04] if (!ac.check()) {
[05] return ZX_ERR_NO_MEMORY;
[06] }
[07]
[08] auto status = dev->Init();
[09] if (status != ZX_OK) {
[10] return status;
[11] }
[12]
[13] // driver manager is now in charge of the device.
[14] [[maybe_unused]] auto* unused = dev.release();
[15] return ZX_OK;
[16] }
First, note the constructor for dev
(it's the new ... UsbXhci(parent)
call
on line [03]
) — we'll come back to it shortly.
Once dev
is constructed, line [08]
calls dev->Init(), which serves as
a de-multiplexing point calling one of two initialization functions:
zx_status_t UsbXhci::Init() {
if (pci_.is_valid()) {
return InitPci();
} else if (pdev_.is_valid()) {
return InitPdev();
} else {
return ZX_ERR_NOT_SUPPORTED;
}
}
Parent protocol usage
Let's follow the path of the pci_
member by way of the InitPci() function.
We'll see how the device uses the functions from the parent protocol.
In UsbXhci::Create() the constructor for dev
initialized the member pci_
from the parent
argument.
Here are the relevant excerpts from the class definition:
class UsbXhci: ... {
public:
explicit UsbXhci(zx_device_t* parent)
: UsbXhciType(parent), pci_(parent), pdev_(parent) {}
...
private:
ddk::PciProtocolClient pci_;
...
};
The first use that InitPci() makes of the pci_
member is to get a
BTI (Bus Transaction Initiator) object:
zx_status_t UsbXhci::InitPci() {
...
zx::bti bti;
status = pci_.GetBti(0, &bti);
if (status != ZX_OK) {
return status;
}
...
This usage is typical.