Using the C++ DDK Template Library

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.