Expose the driver capabilities

Driver components offer the features and services they provide to other drivers and non-driver components through capabilities. This enables Fuchsia's component framework to route those capabilities to the target component when necessary. Drivers can also export their capabilities to devfs to enable other components to discover them as file nodes mounted in the component's namespace.

In this section, you'll expose the qemu_edu driver's factorial capabilities and consume those from a component running elsewhere in the system.

Create a new FIDL library

Create a new project directory in your Bazel workspace for a new FIDL library:

mkdir -p fuchsia-codelab/qemu_edu/fidl

After you complete this section, the project should have the following directory structure:

//fuchsia-codelab/qemu_edu/fidl
                  |- BUILD.bazel
                  |- qemu_edu.fidl

Create the qemu_edu/fidl/BUILD.bazel file and add the following statement to include the necessary build rules from the Fuchsia SDK:

qemu_edu/fidl/BUILD.bazel:

load(
    "@rules_fuchsia//fuchsia:defs.bzl",
    "fuchsia_fidl_library",
    "fuchsia_fidl_llcpp_library",
)

Define a driver service protocol

The driver exposes the capabilities of the edu device using a custom FIDL protocol. Add a new qemu_edu/qemu_edu.fidl file to your project workspace with the following contents:

qemu_edu/fidl/qemu_edu.fidl:

library fuchsia.hardware.qemuedu;

/// The Device protocol provides access to the functions of the QEMU edu hardware.
protocol Device {
    /// Computes the factorial of 'input' using the edu device and returns the
    /// result.
    ComputeFactorial(struct {
        input uint32;
    }) -> (struct {
        output uint32;
    });

    /// Performs a liveness check and return true if the device passes.
    LivenessCheck() -> (struct {
        result bool;
    });
};

/// The Service enables the driver framework to offer the Device protocol to
/// other components.
service Service {
  device client_end:Device;
};

This FIDL protocol provides two methods to interact with the factorial computation and liveness check hardware registers on the edu device.

Add the following build rules to the bottom of the project's build configuration to compile the FIDL library and generate C++ bindings:

  • fuchsia_fidl_library(): Declares the fuchsia.hardware.qemuedu FIDL library and describes the FIDL source files it includes.
  • fuchsia_fidl_llcpp_library(): Describes the generated LLCPP (Low-Level C++) bindings for components to interact with this FIDL library.

qemu_edu/fidl/BUILD.bazel:

fuchsia_fidl_library(
    name = "fuchsia.hardware.qemuedu",
    srcs = [
        "qemu_edu.fidl",
    ],
    library = "fuchsia.hardware.qemuedu",
    visibility = ["//visibility:public"],
)

fuchsia_fidl_llcpp_library(
    name = "fuchsia.hardware.qemuedu_cc",
    library = ":fuchsia.hardware.qemuedu",
    visibility = ["//visibility:public"],
    deps = [
        "@fuchsia_sdk//fidl/zx:zx_llcpp_cc",
        "@fuchsia_sdk//pkg/fidl_cpp_wire",
    ],
)

Implement the driver service protocol

With the FIDL protocol defined, you'll need to update the driver to implement and serve this protocol to other components. Update the driver's component manifest to declare and expose the protocol as a capability:

qemu_edu/drivers/meta/qemu_edu.cml:

{
    include: [
        "syslog/client.shard.cml",
    ],
    program: {
        runner: 'driver',
        binary: 'lib/libqemu_edu.so',
        bind: 'meta/bind/qemu_edu.bindbc'
    },
    use: [
        { service: 'fuchsia.driver.compat.Service' },
    ],
    // Provide the device capability to other components
    capabilities: [
        { service: 'fuchsia.hardware.qemuedu.Service' },
    ],
    expose: [
        {
            service: 'fuchsia.hardware.qemuedu.Service',
            from: 'self',
        },
    ],
}

Include the FIDL bindings for the fuchsia.hardware.qemuedu library and update the driver class to implement the server end of the protocol:

qemu_edu/drivers/qemu_edu.h:

#include <lib/async/dispatcher.h>
#include <lib/driver2/namespace.h>
#include <lib/driver2/record_cpp.h>
#include <lib/driver2/structured_logger.h>
#include <lib/fdf/cpp/dispatcher.h>
#include <lib/sys/component/llcpp/outgoing_directory.h>
#include <lib/zx/status.h>

#include <fidl/fuchsia.hardware.pci/cpp/wire.h>
#include <lib/mmio/mmio.h>
#include <lib/zx/interrupt.h>

#include <fidl/fuchsia.hardware.qemuedu/cpp/wire.h>

namespace qemu_edu {

class QemuEduDriver : public fidl::WireServer<fuchsia_hardware_qemuedu::Device> {
 public:
  QemuEduDriver(async_dispatcher_t* dispatcher,
                fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
                driver::Logger logger)
      : outgoing_(component::OutgoingDirectory::Create(dispatcher)),
        node_(std::move(node)),
        ns_(std::move(ns)),
        logger_(std::move(logger)) {}

  virtual ~QemuEduDriver() = default;

  // Report driver name to driver framework
  static constexpr const char* Name() { return "qemu-edu"; }
  // Start hook called by driver framework
  static zx::status<std::unique_ptr<QemuEduDriver>> Start(
      fuchsia_driver_framework::wire::DriverStartArgs& start_args,
      fdf::UnownedDispatcher dispatcher,
      fidl::WireSharedClient<fuchsia_driver_framework::Node> node, driver::Namespace ns,
      driver::Logger logger);
  // fuchsia.hardware.qemuedu/Device protocol implementation.
  void ComputeFactorial(ComputeFactorialRequestView request,
                        ComputeFactorialCompleter::Sync& completer);
  void LivenessCheck(LivenessCheckRequestView request, LivenessCheckCompleter::Sync& completer);

 private:
  zx::status<> Run(async_dispatcher* dispatcher,
                   fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir);
  zx::status<> MapInterruptAndMmio(fidl::ClientEnd<fuchsia_hardware_pci::Device> pci);

  component::OutgoingDirectory outgoing_;
  fidl::WireSharedClient<fuchsia_driver_framework::Node> node_;
  driver::Namespace ns_;
  driver::Logger logger_;

  std::optional<fdf::MmioBuffer> mmio_;
  zx::interrupt irq_;
};

}  // namespace qemu_edu

Similar to the identification register in the previous section, these methods interact with the MMIO region to read and write data into the respective edu device registers:

  • ComputeFactorial(): Write an input value to the factorial computation register and wait for the status register to report that the computation is complete.
  • LivenessCheck(): Write a challenge value to the liveness check register and confirm the expected result.

Add the following code to implement the fuchsia.hardware.qemuedu/Device method in your driver class:

qemu_edu/drivers/qemu_edu.cc:

namespace qemu_edu {
// ...

// Driver Service: Compute factorial on the edu device
void QemuEduDriver::ComputeFactorial(ComputeFactorialRequestView request,
                                     ComputeFactorialCompleter::Sync& completer) {
  // Write a value into the factorial register.
  uint32_t input = request->input;

  mmio_->Write32(input, edu_device_registers::kFactorialCompoutationOffset);

  // Busy wait on the factorial status bit.
  while (true) {
    const auto status = edu_device_registers::Status::Get().ReadFrom(&*mmio_);
    if (!status.busy())
      break;
  }

  // Return the result.
  uint32_t factorial = mmio_->Read32(edu_device_registers::kFactorialCompoutationOffset);

  FDF_SLOG(INFO, "Replying with", KV("factorial", factorial));
  completer.Reply(factorial);
}

// Driver Service: Complete a liveness check on the edu device
void QemuEduDriver::LivenessCheck(LivenessCheckRequestView request,
                                  LivenessCheckCompleter::Sync& completer) {
  constexpr uint32_t kChallenge = 0xdeadbeef;
  constexpr uint32_t kExpectedResponse = ~(kChallenge);

  // Write the challenge and observe that the expected response is received.
  mmio_->Write32(kChallenge, edu_device_registers::kLivenessCheckOffset);
  auto value = mmio_->Read32(edu_device_registers::kLivenessCheckOffset);

  const bool alive = value == kExpectedResponse;

  FDF_SLOG(INFO, "Replying with", KV("result", alive));
  completer.Reply(alive);
}

}  // namespace qemu_edu

Update the driver's build configuration to depend on the FIDL bindings for the new fuchsia.hardware.qemuedu library:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
        "driver_compat.h",
        "registers.h",
    ],
    linkshared = True,
    deps = [
        "//fuchsia-codelab/qemu_edu/fidl:fuchsia.hardware.qemuedu_cc",
        "@fuchsia_sdk//fidl/fuchsia.driver.compat:fuchsia.driver.compat_llcpp_cc",
        "@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc",
        "@fuchsia_sdk//fidl/zx:zx_cc",
        "@fuchsia_sdk//pkg/driver2-llcpp",
        "@fuchsia_sdk//pkg/driver_runtime_cpp",
        "@fuchsia_sdk//pkg/fidl_cpp_wire",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
        "@fuchsia_sdk//pkg/sys_component_llcpp",
        "@fuchsia_sdk//pkg/zx",
    ],
)

Export and serve the protocol

The qemu_edu driver makes the fuchsia.hardware.qemuedu/Device protocol discoverable to other components using devfs. To discover which driver services are available in the system, a non-driver component would look up the device filesystem (usually mounted to /dev in a component’s namespace) and scan for the directories and files under this filesystem.

Driver manager can alias entries in devfs to a specific device class entry (for example, /dev/class/input) when a matching protocol ID to a known device class is provided. If a non-driver component does not know the exact path of the driver service in devfs, but rather a specific type. For this exercise, the edu device does not conform to a known class so you'll configure this entry as an unclassified device.

Update the driver component's manifest to request the fuchsia.devics.fs.Exporter capability:

qemu_edu/drivers/meta/qemu_edu.cml:

{
    include: [
        "syslog/client.shard.cml",
    ],
    program: {
        runner: 'driver',
        binary: 'lib/libqemu_edu.so',
        bind: 'meta/bind/qemu_edu.bindbc'
    },
    use: [
        { protocol: 'fuchsia.device.fs.Exporter' },
        { service: 'fuchsia.driver.compat.Service' },
    ],
    // Provide the device capability to other components
    capabilities: [
        { service: 'fuchsia.hardware.qemuedu.Service' },
    ],
    expose: [
        {
            service: 'fuchsia.hardware.qemuedu.Service',
            from: 'self',
        },
    ],
}

Add the following code to use the fuchsia.device.fs.Exporter capability to create a devfs entry:

qemu_edu/drivers/driver_compat.h:

#include <fidl/fuchsia.device.fs/cpp/wire.h>
#include <fidl/fuchsia.driver.compat/cpp/wire.h>

namespace edu_driver_compat {
// ...

// Connect to the fuchsia.devices.fs.Exporter protocol
zx::status<fidl::ClientEnd<fuchsia_device_fs::Exporter>> ConnectToDeviceExporter(
    const driver::Namespace* ns) {
  auto exporter = ns->Connect<fuchsia_device_fs::Exporter>();
  if (exporter.is_error()) {
    return exporter.take_error();
  }
  return exporter;
}

// Create an exported directory handle using fuchsia.devices.fs.Exporter
zx::status<fidl::ServerEnd<fuchsia_io::Directory>> ExportDevfsEntry(const driver::Namespace* ns,
                                                                    fidl::StringView service_dir,
                                                                    fidl::StringView devfs_path,
                                                                    int protocol_id) {
  // Connect to the devfs exporter service
  auto exporter_client = ConnectToDeviceExporter(ns);
  if (exporter_client.is_error()) {
    return exporter_client.take_error();
  }
  auto exporter = fidl::BindSyncClient(std::move(exporter_client.value()));

  // Serve a connection for devfs clients
  auto endpoints = fidl::CreateEndpoints<fuchsia_io::Directory>();
  if (endpoints.is_error()) {
    return endpoints.take_error();
  }

  // Export the client side of the service connection to devfs
  auto result =
      exporter->Export(std::move(endpoints->client), service_dir, devfs_path, protocol_id);
  if (!result.ok()) {
    return zx::error(result.status());
  }
  if (result->is_error()) {
    return zx::error(result->error_value());
  }

  return zx::ok(std::move(endpoints->server));
}

}  // namespace driver_compat

Update the driver's Run() method to begin serving the fuchsia.hardware.qemuedu/Device protocol to a new devfs entry at /dev/sys/platform/platform-passthrough/PCI0/bus/00:06.0_/qemu-edu, which matches the device node's topological path:

qemu_edu/drivers/qemu_edu.cc:

// Initialize this driver instance
zx::status<> QemuEduDriver::Run(async_dispatcher* dispatcher,
                                fidl::ServerEnd<fuchsia_io::Directory> outgoing_dir) {
  // ...

  // Report the version information from the edu device.
  auto version_reg = edu_device_registers::Identification::Get().ReadFrom(&*mmio_);
  FDF_SLOG(INFO, "edu device version", KV("major", version_reg.major_version()),
           KV("minor", version_reg.minor_version()));

  // Add the fuchsia.hardware.qemuedu/Device protocol to be served as
  // "/svc/fuchsia.hardware.qemuedu/default/device"
  component::ServiceHandler handler;
  fuchsia_hardware_qemuedu::Service::Handler service(&handler);

  auto device_handler =
      [this, dispatcher](fidl::ServerEnd<fuchsia_hardware_qemuedu::Device> server_end) -> void {
    fidl::BindServer<fidl::WireServer<fuchsia_hardware_qemuedu::Device>>(
        dispatcher, std::move(server_end), this);
  };
  auto result = service.add_device(device_handler);
  ZX_ASSERT(result.is_ok());

  result = outgoing_.AddService<fuchsia_hardware_qemuedu::Service>(std::move(handler));
  if (result.is_error()) {
    FDF_SLOG(ERROR, "Failed to add Device service", KV("status", result.status_string()));
    return result.take_error();
  }

  const std::string kExportName = std::string("svc/") + fuchsia_hardware_qemuedu::Service::Name +
                                  "/" + component::kDefaultInstance + "/device";
  auto service_dir = fidl::StringView::FromExternal(kExportName);

  // Construct a devfs path that matches the device nodes topological path
  auto path_result = fidl::WireCall(*parent)->GetTopologicalPath();
  if (!path_result.ok()) {
    FDF_SLOG(ERROR, "Failed to get topological path", KV("status", path_result.status_string()));
    return zx::error(path_result.status());
  }
  std::string parent_path(path_result->path.get());
  auto devfs_path = fidl::StringView::FromExternal(parent_path.append("/").append(Name()));

  // Export an entry to devfs for fuchsia.hardware.qemuedu as a generic device
  auto devfs_dir = edu_driver_compat::ExportDevfsEntry(&ns_, service_dir, devfs_path, 0);
  if (devfs_dir.is_error()) {
    FDF_SLOG(ERROR, "Failed to export service", KV("status", devfs_dir.status_string()));
    return devfs_dir.take_error();
  }

  // Serve the driver's FIDL protocol to clients
  auto status = outgoing_.Serve(std::move(devfs_dir.value()));
  if (status.is_error()) {
    FDF_SLOG(ERROR, "Failed to serve devfs directory", KV("status", status.status_string()));
    return status.take_error();
  }
  status = outgoing_.Serve(std::move(outgoing_dir));
  if (status.is_error()) {
    FDF_SLOG(ERROR, "Failed to serve outgoing directory", KV("status", status.status_string()));
    return status.take_error();
  }

  return zx::ok();
}

Update the driver's build configuration to include the FIDL bindings for the fuchsia.device.fs library:

qemu_edu/drivers/BUILD.bazel:

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
        "driver_compat.h",
        "registers.h",
    ],
    linkshared = True,
    deps = [
        "//fuchsia-codelab/qemu_edu/fidl:fuchsia.hardware.qemuedu_cc",
        "@fuchsia_sdk//fidl/fuchsia.device.fs:fuchsia.device.fs_llcpp_cc",
        "@fuchsia_sdk//fidl/fuchsia.driver.compat:fuchsia.driver.compat_llcpp_cc",
        "@fuchsia_sdk//fidl/fuchsia.hardware.pci:fuchsia.hardware.pci_llcpp_cc",
        "@fuchsia_sdk//fidl/zx:zx_cc",
        "@fuchsia_sdk//pkg/driver2-llcpp",
        "@fuchsia_sdk//pkg/driver_runtime_cpp",
        "@fuchsia_sdk//pkg/fidl_cpp_wire",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
        "@fuchsia_sdk//pkg/sys_component_llcpp",
        "@fuchsia_sdk//pkg/zx",
    ],
)

The qemu_edu driver's capabilities are now discoverable by other components.

Rebuild the driver

Use the bazel build command to verify that the driver builds successfully with your code changes:

bazel build --config=fuchsia_x64 //fuchsia-codelab/qemu_edu/drivers:pkg

Congratulations! You've successfully exposed FIDL services from a Fuchsia driver.