Using the C++ DDK Template Library

This document is part of the Driver Development Kit tutorial documentation.

The preceeding sections in the tutorial have illustrated the basics of what a device driver needs to do (e.g., register its name, handle operations, etc.)

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:


@@@ I‘m just dumping stuff here for right now, so there’s no rhyme or reason to any of this :-)

The mixins are defined in //zircon/system/ulib/ddktl/include/ddktl/device.h.

The following mixins are provided:

Mixin classFunctionPurpose
ddk::GetProtocolableDdkGetProtocol()fetches the protocol
ddk::OpenableDdkOpen()client's open()
ddk::ClosableDdkClose()client's close()
ddk::UnbindableDdkUnbind()called when parent of this device is being removed
ddk::ReadableDdkRead()client's read()
ddk::WritableDdkWrite()client's write()
ddk::GetSizableDdkGetSize()returns size of device
ddk::IoctlableDdkIoctl()client's ioctl()
ddk::MessageableDdkMessage()for FIDL RPC messages
ddk::SuspendableDdkSuspend()to suspend device
ddk::ResumableDdkResume()to resume device
ddk::RxrpcableDdkRxrpc()RPC message 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::Openable,     // we support open()
[03]                                ddk::Closable,     // close()
[04]                                ddk::Readable,     // read()
[05]                                ddk::Unbindable>;  // and the device can be unbound

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 DDK device member functions are implemented.

Once defined, we can then declare our device class (MyDevice) as inheriting from DeviceType:

[07] class MyDevice : public DeviceType {
[08]   public:
[09]     MyDevice(zx_device_t* parent)
[10]       : DeviceType(parent) {}
[11]
[12]     zx_status_t Bind() {
[13]         // Any other setup required by MyDevice.
[14]         // The device_add_args_t will be filled out by the base class.
[15]         return DdkAdd("my-device-name");
[16]     }
[17]
[18]     // Methods required by the ddk mixins
[19]     zx_status_t DdkOpen(zx_device_t** dev_out, uint32_t flags);
[20]     zx_status_t DdkClose(uint32_t flags);
[21]     zx_status_t DdkRead(void* buf, size_t count, zx_off_t off, size_t* actual);
[22]     void DdkUnbind();
[23]     void DdkRelease();
[24] };

Because the DeviceType class contains four mixins (lines [02 .. 05]: Openable, Closable, Readable, and Unbindable), we're required to provide the respective function implementations (lines [18 .. 22]) in our class.

Because all DDKTL classes must provide a release function (here, line [23] provides DdkRelease()), we didn't specify this in the mixin definition for DeviceType.

Keep in mind that once you call DdkAdd() 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.

Recall from the preceeding sections that your device must register with the device manager in order to be usable. This is accomplished as follows:

[26] extern "C" zx_status_t my_bind(zx_device_t* device,
[27]                                void** cookie) {
[28]     auto dev = make_unique<MyDevice>(device);
[29]     auto status = dev->Bind();
[30]     if (status == ZX_OK) {
[31]         // devmgr is now in charge of the memory for dev
[32]         dev.release();
[33]     }
[34]     return status;
[35] }

Here, we declare my_bind() as an extern "C" function (because it needs to be callable by the C code that handles binding). The function creates an instance of MyDevice, calls the Bind() routine, and returns a status.

Bind() (line [12], above), performs whatever setup it needs to, and then calls DdkAdd() with the device name.

After this point, your device is registered with the device manager, and any client calls from the client's open(), close(), and read() will now flow to your implementations of DdkOpen(), DdkClose(), and DdkRead(), respectively.

As an example, in the directory zircon/system/dev/block/zxcrypt we have a typical device declaration (device.h):

[01] class Device;
[02] using DeviceType = ddk::Device<Device,
[03]                                ddk::GetProtocolable,
[04]                                ddk::GetSizable,
[05]                                ddk::Unbindable>;
...
[06] class Device final : public DeviceType,
[07]                      public ddk::BlockImplProtocol<Device, ddk::base_protocol>,
[08]                      public ddk::BlockPartitionProtocol<Device>,
[09]                      public ddk::BlockVolumeProtocol<Device> {
[10] public:
...
[11]     // ddk::Device methods; see ddktl/device.h
[12]     zx_status_t DdkGetProtocol(uint32_t proto_id, void* out);
[13]     zx_off_t DdkGetSize();
[14]     void DdkUnbind();
[15]     void DdkRelease();
...

Here we see lines [01 .. 05] declare the shortcut DeviceType with the base class Device and three mixins, GetProtocolable, GetSizable, and Unbindable.

What's interesting in this case is that on line [06] we inherit from the DeviceType as usual, but we also inherit 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 that device’s DdkGetProtocol implementation (from device.cpp):

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;
    }
}

A FIDL, and a Banjo

FIDL is the IPC system for Fuchsia, and Banjo, built on top of it, provides device-to-device communications.

In this section, we'll see how a DDKTL driver:

  • interacts with parent protocols using Banjo,
  • implements a protocol, and
  • implements a FIDL interface.