Update DDK interfaces to DFv2

This page provides best practices and examples for migrating driver interfaces from DFv1 to DFv2.

DFv1 drivers use the DDK (Driver Development Kit) library (//src/lib/ddk). In DFv2, this library is replaced with the driver component library, which includes most of the essential utilities for DFv2 drivers.

Update dependencies from DDK to DFv2

Remove all package dependencies under the DDK library in your DFv1 driver and replace them with the new DFv2 library,

Source headers

Replace DDK dependencies in source header files:

DFv1

//sdk/lib/driver/component/cpp:cpp

DFv2

#include <lib/driver/component/cpp/driver_base.h>

Build dependencies

Replace DDK dependencies in the BUILD.gn files:

DFv1

"//src/lib/ddk",
"//src/lib/ddktl",

DFv2

"//sdk/lib/driver/component/cpp:cpp",

Update interfaces from DDK to DFv2

ddk:Device mixin

ddk::Device<> is a mixin defined in ddktl/device.h that simplifies writing DDK drivers in C++.

DFv1

The example of a DFv1 driver below uses the ddk::Device<> mixin:

#include <ddktl/device.h>

class SimpleDriver;
using DeviceType = ddk::Device<SimpleDriver, ddk::Initializable>;

class SimpleDriver : public DeviceType {
 public:
  explicit SimpleDriver(zx_device_t* parent);
  ~SimpleDriver();

  static zx_status_t Bind(void* ctx, zx_device_t* dev);

  void DdkInit(ddk::InitTxn txn);
  void DdkRelease();
};

}  // namespace simple

DFv2

In DFv2, the DriverBase class is used to write drivers. DFv2 drivers need to inherit from the DriverBase class instead of the ddk::Device mixin, for example:

#include <lib/driver/component/cpp/driver_base.h>

class SimpleDriver : public fdf::DriverBase {
 public:
  SimpleDriver(fdf::DriverStartArgs start_args,
               fdf::UnownedSynchronizedDispatcher driver_dispatcher);

  zx::result<> Start() override;
};

Constructor, init hook, and bind hook

In DFv1, the driver is initialized through the constructor, the init hook, and the bind hook.

The init hook is typically implemented as DdkInit(), for example:

void SimpleDriver::DdkInit(ddk::InitTxn txn) {}

The bind hook is a static function that is passed to the zx_driver_ops_t struct, for example:

zx_status_t SimpleDriver::Bind(void* ctx, zx_device_t* dev) {}

static zx_driver_ops_t simple_driver_ops = [][5] -> zx_driver_ops_t {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = SimpleDriver::Bind;
  return ops;
}();

In DFv2, all of the DFv1 logic needs to go into the DriverBase class's Start() function in the following order: Constructor, Init hook, and Bind hook.

DFv1

#include <ddktl/device.h>

class SimpleDriver;
using DeviceType = ddk::Device<SimpleDriver, ddk::Initializable>;

class SimpleDriver : public DeviceType {
 public:
  explicit SimpleDriver(zx_device_t* parent) {
     zxlogf(INFO, "SimpleDriver constructor logic");
  }
  ~SimpleDriver() { delete this; }

  static zx_status_t Bind(void* ctx, zx_device_t* dev) {
     zxlogf(INFO, "SimpleDriver bind logic");
  }

  void DdkInit(ddk::InitTxn txn) {
     zxlogf(INFO, "SimpleDriver initialized");
  }
};

}  // namespace simple

static zx_driver_ops_t simple_driver_ops = [][5] -> zx_driver_ops_t {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = SimpleDriver::Bind;
  return ops;
}();

DFv2

class SimpleDriver : public fdf::DriverBase {
 public:
  SimpleDriver(fdf::DriverStartArgs start_args,
               fdf::UnownedSynchronizedDispatcher driver_dispatcher);

  zx::result<> Start() override {
     zxlogf(INFO, "SimpleDriver constructor logic");
     zxlogf(INFO, "SimpleDriver initialized");
     zxlogf(INFO, "SimpleDriver bind logic");
  }
};

Destructor, DdkSuspend(), DdkUnbind(), and DdkRelease()

In DFv1, the destructor, DdkSuspend(), DdkUnbind(), and DdkRelease() are used for tearing down objects and threads (for more information on these hooks, see this DFv1 simple driver example).

In DFv2, the teardown logic needs to be implemented in PrepareStop() and Stop() methods of the DriverBase class, for example:

class SimpleDriver : public fdf::DriverBase {
 public:
  SimpleDriver(fdf::DriverStartArgs start_args,
               fdf::UnownedSynchronizedDispatcher driver_dispatcher);
  ...

  zx::result<> PrepareStop(fdf::PrepareStopCompleter completer) override{
     // Called before the driver dispatchers are shutdown. Only implement this function
     // if you need to manually clean up objects (ex/ unique_ptrs) in the driver dispatchers");
     completer(zx::ok());
  }

  zx::result<> Stop() {
     // This is called after all driver dispatchers are shutdown.
     // Use this function to perform any remaining teardowns
  }
};

Also, when migrating to DFv2, exclude self-delete statements in the DdkRelease() methods, for example:

void DdkRelease() {
    delete this; // Do not migrate this statement to DFv2
}

ZIRCON_DRIVER() macro

In DFv1, a driver is declared through the ZIRCON_DRIVER() macro. In DFv2, the driver_export library replaces the ZIRCON_DRIVER() macro. The driver export needs to be declared in the driver implementation, typically in the .cc file.

DFv1

An example of the ZIRCON_DRIVER() macro:

static zx_driver_ops_t simple_driver_ops = [][5] -> zx_driver_ops_t {
  zx_driver_ops_t ops = {};
  ops.version = DRIVER_OPS_VERSION;
  ops.bind = SimpleDriver::Bind;
  return ops;
}();

ZIRCON_DRIVER(SimpleDriver, simple_driver_ops, "zircon", "0.1");

DFv2

The driver_export example below migrates the DFv1 ZIRCON_DRIVER() macro example:

#include <lib/driver/component/cpp/driver_export.h>
...
FUCHSIA_DRIVER_EXPORT(simple::SimpleDriver);

ddk::Messageable mixin

In DFv1, the ddk::Messageable<> mixin is used to implement a FIDL server. In DFv2, the ddk:Messageable<> mixin can be replaced by directly inheriting from the FIDL server.

DFv1

using I2cDeviceType = ddk::Device<I2cChild, ddk::Messageable<fuchsia_hardware_i2c::Device>::Mixin>;

class I2cDriver : public I2cDeviceType {
  ...
  // Functions from the fuchsia.hardware.i2c Device protocol
  void Transfer(TransferRequestView request, TransferCompleter::Sync& completer) override;
  void GetName(GetNameCompleter::Sync& completer) override;
}

DFv2

class I2cDriver : public DriverBase, public fidl::WireServer<fuchsia_hardware_i2c::Device> {
  ...
  // Functions from the fuchsia.hardware.i2c Device protocol
  void Transfer(TransferRequestView request, TransferCompleter::Sync& completer) override;
  void GetName(GetNameCompleter::Sync& completer) override;
}

After implementing the FIDL server, you need to add the service to the outgoing directory:

  1. Add a ServerBindingGroup to your driver class.

  2. Use fidl::ServerBindingGroup for Zircon transport and fdf::ServerBindingGroup for driver transport, for example:

    class I2cDriver: public DriverBase {
      ...
      private:
        fidl::ServerBindingGroup<fidl_i2c::Device> bindings_;
    }
    
  3. Add the bindings to the outgoing directory, for example:

    auto serve_result = outgoing()->AddService<fuchsia_hardware_i2c::Service>(
          fuchsia_hardware_i2c::Service::InstanceHandler({
              .device = bindings_.CreateHandler(this, dispatcher()->async_dispatcher(),
                  fidl::kIgnoreBindingClosure),
          }));
    if (serve_result.is_error()) {
        FDF_LOG(ERROR, "Failed to add Device service %s",
            serve_result.status_string());
        return serve_result.take_error();
    }
    

DdkAdd() and device_add()

The DdkAdd() and device_add() methods are used to add child nodes, for example:

zx_status_t status = device->DdkAdd(ddk::DeviceAddArgs("i2c");

To add a child node in DFv2, see the Add a child node section in the Write a minimal DFv2 driver guide.

DdkAddCompositeNodeSpec()

The DdkAddCompositeNodeSpec() method adds a composite node spec to the system, for example:

auto status = DdkAddCompositeNodeSpec("ft3x27_touch", spec);

For instructions on how to add a composite node spec in DFv2, see the Driver Framework v2 (DFv2) section in the Composite nodes guide.

device_get_protocol()

The device_get_protocol() method is used to retrieve and connect to a Banjo server. In DFv2, we replace the call with the compat/cpp/banjo_client library.

DFv1

misc_protocol_t misc;
zx_status_t status = device_get_protocol(parent, ZX_PROTOCOL_MISC, &misc);
if (status != ZX_OK) {
  return status;
}

DFv2

zx::result<ddk::MiscProtocolClient> client =
     compat::ConnectBanjo<ddk::MiscProtocolClient>(incoming());

FIDL connect functions

In DFv1, DDK provides the following functions for connecting to FIDL protocols:

In DFv2, you use the driver namespace library to connect to the FIDL protocols.

DdkConnectFidlProtocol()

DFv1

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> client_end =
  DdkConnectFidlProtocol<fuchsia_examples_gizmo::Service::Device>();
if (client_end.is_error()) {
  zxlogf(ERROR, "Failed to connect fidl protocol");
  return client_end.status_value();
}

(Source: DFv1 driver transport example)

DFv2

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> connect_result =
    incoming()->Connect<fuchsia_examples_gizmo::Service::Device>();
if (connect_result.is_error()) {
  FDF_SLOG(ERROR, "Failed to connect gizmo device protocol.",
           KV("status", connect_result.status_string()));
  return connect_result.take_error();
}

(Source: DFv2 driver transport example)

DdkConnectFragmentFidlProtocol()

DFv1

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> client_end =
  DdkConnectFragmentFidlProtocol<fuchsia_examples_gizmo::Service::Device>(parent, "gizmo");
if (codec_client_end.is_error()) {
  zxlogf(ERROR, "Failed to connect to fidl protocol: %s",
      zx_status_get_string(codec_client_end.status_value()));
  return codec_client_end.status_value();
}

DFv2

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> connect_result =
    incoming()->Connect<fuchsia_examples_gizmo::Service::Device>("gizmo");
if (connect_result.is_error()) {
  FDF_LOG(ERROR, "Failed to connect gizmo device protocol: %s",
      connect_result.status_string());
  return connect_result.take_error();
}

DdkConnectRuntimeProtocol()

DFv1

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> client_end =
    DdkConnectRuntimeProtocol<fuchsia_examples_gizmo::Service::Device>();
if (client_end.is_error()) {
  zxlogf(ERROR, "Failed to connect fidl protocol");
  return client_end.status_value();
}

(Source: DFv1 driver transport example)

DFv2

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> connect_result =
    incoming()->Connect<fuchsia_examples_gizmo::Service::Device>();
if (connect_result.is_error()) {
  FDF_SLOG(ERROR, "Failed to connect gizmo device protocol.",
           KV("status", connect_result.status_string()));
  return connect_result.take_error();
}

(Source: DFv2 driver transport example)

DdkConnectFragmentRuntimeProtocol()

DFv1

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> client_end =
    DdkConnectFragmentRuntimeProtocol<fuchsia_examples_gizmo::Service::Device>("gizmo_parent");
if (client_end.is_error()) {
  zxlogf(ERROR, "Failed to connect fidl protocol");
  return client_end.status_value();
}

DFv2

zx::result<fidl::ClientEnd<fuchsia_examples_gizmo::Device>> connect_result =
    incoming()->Connect<fuchsia_examples_gizmo::Service::Device>("gizmo_parent");
if (connect_result.is_error()) {
  FDF_SLOG(ERROR, "Failed to connect gizmo device protocol.",
           KV("status", connect_result.status_string()));
  return connect_result.take_error();
}

Metadata

In DFv1, drivers can add and retrieve metadata using functions such as DdkAddMetadata() and ddk::GetEncodedMetadata<>(). See the code examples below.

To migrate these metadata functions to DFv2, do the following:

  1. Complete the Set up a compat device server guide.

  2. Follow the instructions in the Forward, add, and parse DFv1 metadata section.

DdkAddMetadata()

DFv1

zx_status_t status = dev->DdkAddMetadata(DEVICE_METADATA_I2C_DEVICE,
    metadata.value().data(), sizeof(metadata.value());
if (status != ZX_OK) {
  zxlogf(ERROR, "DdkAddMetadata failed: %s", zx_status_get_string(status));
}

DFv2

compat_server->inner().AddMetadata(DEVICE_METADATA_I2C_DEVICE,
    metadata.value().data(), sizeof(metadata.value()));

ddk::GetEncodedMetadata<>()

DFv1

auto decoded =
    ddk::GetEncodedMetadata<fuchsia_hardware_i2c_businfo::wire::I2CBusMetadata>(
       parent, DEVICE_METADATA_I2C_CHANNELS);

DFv2

fidl::Arena arena;
zx::result i2c_bus_metadata =
    compat::GetMetadata<fuchsia_hardware_i2c_businfo::wire::I2CBusMetadata>(
        incoming(), arena, DEVICE_METADATA_I2C_CHANNELS);

Migrate other interfaces

TRACE_DURATION

The TRACE_DURATION() method is available for both DFv1 and DFv2.

DFv1

In DFv1, the TRACE_DURATION macro is imported from the following DDK library:

#include <lib/ddk/trace/event.h>

DFv2

To use the TRACE_DURATION macro in DFv2, change the include line to the following header:

#include <lib/trace/event.h>

And add the following line to the build dependencies:

fuchsia_driver("i2c-driver") {
  ...
  deps = [
    ...
    "//zircon/system/ulib/trace",
  ]
}

zxlogf()

The zxlogf() method is not available in DFv2 and needs to be migrated to FDF_LOG() or FDF_SLOG().

For instructions, see the Add logs section in the Write a minimal DFv2 driver guide.