Driver sample walkthrough: qemu_edu

The qemu_edu driver (in the SDK driver samples repository) is a good sample to study if you’re interested in creating a new driver or understanding how drivers work in a Fuchsia system.

One of the services that the qemu_edu driver provides is that it enables Fuchsia components to calculate a factorial using the computing power of a device (in fact, a virtual device) in the system.

This development walkthrough guide contains the following sections:

How this driver works

For the qemu_edu driver to work, we first need to launch an instance of the Fuchsia emulator (for instance, using a Fuchsia’s Workstation image). QEMU (which underpins the Fuchsia emulator) then creates a virtual device named edu, which is an educational device for writing drivers. It is this edu device that provides a service that computes a factorial given an integer.

Soon after the emulator instance starts running, Fuchsia’s driver framework discovers the edu device (which is represented as a node) and looks for a driver that is a good match for the edu device. However, at first the driver framework won’t be able to find a driver with bind rules that can satisfy the binding properties of the edu device. At this point, the edu device is left unmatched, therefore its node unbound.

Without a driver, the unmatched edu device cannot provide services to other components in the system. However, here we have the qemu_edu driver, which we know is suitable for serving the edu device. To supply this driver to the edu device in the system, we need to do the following:

  1. Build a Fuchsia package that contains the qemu_edu driver component.
  2. Publish the package to a local Fuchsia package repository.
  3. Register the qemu_edu driver to the Fuchsia emulator instance.

When a new driver is registered, Fuchsia’s driver manager informs the driver index about the arrival of the new driver. The driver index then tries to match this new qemu_edu driver to unbound nodes in the node topology. We’re lucky this time because the qemu_edu driver’s bind rules (see qemu_edu.bind) are precisely designed to match the binding properties of the edu device – to be exact, the binding properties of the node that represents the edu device in the topology.

Once they are matched and bound, the driver manager examines the qemu_edu driver’s component manifest (see meta/qemu_edu.cml) to learn more about the qemu_edu driver. For instance, the driver’s component manifest describes which binary to run, which runtime to use, and whether to place the driver in a new driver host or in the same driver host as its parent driver. In this case, by default the driver manager places the qemu_edu driver in a new driver host.

The driver host (which runs as a Fuchsia component) calls the Start() function in the driver’s code (see qemu_edu.cc) to initialize the driver. Once initialized, the qemu_edu driver can start providing the edu device’s services to other components in the system.

Build rules: BUILD.bazel

This BUILD.bazel file contains the build rules of the qemu_edu driver component and the eductl "tools" component. Using this BUILD.bazel file, these two components are built into the same package labeled qemu_edu (see the fuchsia_package rule in BUILD.bazel).

While the build rules of a driver component mostly look similar to the build rules of other Fuchsia components, there exist a few rules and attributes specific to the driver development.

Below is the load field shown at the top of the qemu_edu driver’s BUILD.bazel file:

load(
    "@rules_fuchsia//fuchsia:defs.bzl",
    "fuchsia_cc_binary",
    "fuchsia_component",
    "fuchsia_component_manifest",
    "fuchsia_driver_bytecode_bind_rules",
    "fuchsia_fidl_library",
    "fuchsia_fidl_llcpp_library",
    "fuchsia_package",
)

Of these entries, the following build rules are specific to the driver development:

And the following build rules contain additional entries that are specific to the driver development:

fuchsia_driver_bytecode_bind_rules

The fuchsia_driver_bytecode_bind_rules rule describes the specifics of the bind rules of a driver. Fuchsia’s driver framework uses a driver’s bind rules to match the driver to a device in a Fuchsia system.

Below are the fuchsia_driver_bytecode_bind_rules attributes of the qemu_edu driver:

fuchsia_driver_bytecode_bind_rules(
    name = "bind_bytecode",
    output = "qemu_edu.bindbc",
    rules = "qemu_edu.bind",
    deps = [
        "@fuchsia_sdk//bind/fuchsia.pci",
    ],
)

The rules attribute above specifies that the qemu_edu.bind file contains the bind rules of this driver. The deps attribute specifies the bind libraries to be used with the bind rules specified in rules. To see all available bind libraries supported by Fuchsia, look in the //bind directory in the Fuchsia SDK. (For more information, see Bind libraries.)

fuchsia_fidl_llcpp_library

The fuchsia_fidl_llcpp_library rule describes the specifics of the llcpp (Low-Level C++) FIDL library.

This Low-Level C++ FIDL library provides a collection of low-level C++ FIDL calls used by drivers in a Fuchsia system. The library is optimized to meet the needs of low-level systems programming and multi-thread environments for increased security. (For more information, see Comparing C, Low-Level C++, and High-Level C++ Language Bindings.)

Notice there are two FIDL library build rules in this BUILD.bazel file:

  • fuchsia_fidl_library()
  • fuchsia_fidl_llcpp_library()

Below are the qemu_edu driver’s attributes of these two FIDL library build rules:

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-llcpp",
    ],
)

The generic fuchsia_fidl_library rule defines a Fuchsia component’s FIDL capabilities, which are specified in qemu_edu.fidl for the qemu_edu driver. The fuchsia_fidl_llcpp_library rule describes additional dependencies for generating the FIDL code that is based on the Low-Level C++ FIDL library.

cc_binary

The cc_binary rule specifies the source and header files (for instance, qemu_edu.cc, qemu_edu.h, and registers.h) for building a C++ binary for the qemu_edu driver.

Notice some driver-specific dependencies in this cc_binary rule, mainly the pci_llcpp FIDL library for PCI FIDL communication and hwreg and mmio packages for working with a device’s hardware registers and MMIO (Memory-Mapped I/O):

cc_binary(
    name = "qemu_edu",
    srcs = [
        "qemu_edu.cc",
        "qemu_edu.h",
        "registers.h",
    ],
    linkshared = True,
    deps = [
        ":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-llcpp",
        "@fuchsia_sdk//pkg/hwreg",
        "@fuchsia_sdk//pkg/mmio",
        "@fuchsia_sdk//pkg/sys_component_llcpp",
        "@fuchsia_sdk//pkg/zx-status",
    ],
)

What’s also important for Fuchsia drivers is that the line linkshared = True, must be included in this build rule. This line enables the binary of a Fuchsia driver to be shared with other processes.

fuchsia_component

For a driver component, the fuchsia_component rule mainly describes where to place binaries and artifacts within the component’s storage space.

Below are the fuchsia_component attributes of the qemu_edu driver:

fuchsia_component(
    name = "component",
    content = {
        ":qemu_edu": "lib/",
        ":bind_bytecode": "meta/bind/",
    },
    manifest = ":manifest",
)

According to these attributes, the content of the driver’s binary (generated from the qemu_edu target) is to be placed in the lib directory, and the content of the bytecode that contains the driver’s bind rules (generated from the bind_bytecode target) is to be placed in the meta/bind directory of the component.

The file placement convention above is recommended for driver components. The Fuchsia platform does not compress files under a component’s meta directory. So it helps to place files that need to be loaded fast, such as bytecode containing bind rules, in the meta directory.

Driver component: qemu_edu

The qemu_edu component is the qemu_edu driver itself.

The qemu_edu driver component comprises the following files:

meta/qemu_edu.cml

The meta/qemu_edu.cml file contains the component manifest of the qemu_edu component.

Below are the attributes of the component manifest for the qemu_edu component:

{
    program: {
        runner: 'driver',
        binary: 'lib/libqemu_edu.so',
        bind: 'meta/bind/qemu_edu.bindbc'
    },
    use: [
        {
            protocol: [
              'fuchsia.logger.LogSink',
              'fuchsia.device.fs.Exporter'
            ],
        },
        {
            directory: 'fuchsia.driver.compat.Service-default',
            rights: ['rw*'],
            path: '/fuchsia.driver.compat.Service/default',
        }
    ],
}

The program field describes which program to start when this component starts running on the Fuchsia platform. For driver components, the runner attribute needs to be driver , which means to use the driver runtime. The binary attribute indicates that the lib/libqemu_edu.so file contains the binary of the driver. According to the fuchsia_component rule in the BUILD.bazel file, the driver’s binary (libqemu_edu.so) is located in the lib directory, and the bind rules (qemu_edu.bindbc) is located in the meta/bind directory of the component’s storage space.

The use field specifies the capabilities that this component uses when it starts running on the Fuchsia platform.

qemu_edu.bind

The qemu_edu.bind file contains the bind rules for the qemu_edu driver.

A driver’s bind rules are a series of Boolean expressions that the driver must provide to ensure that it’s matched to the right device running in a system. These rules are compared against the binding properties of devices in the system. When a device’s binding properties can satisfy the driver’s bind rules (thus returning true for all conditions in the bind rules), then the driver is said to be matched to the device. Once they are matched, the driver can provide the device’s services to other components in the system.

Below are the bind rules of the qemu_edu driver:

using fuchsia.pci;

fuchsia.BIND_FIDL_PROTOCOL == fuchsia.pci.BIND_FIDL_PROTOCOL.DEVICE;
fuchsia.BIND_PCI_VID == 0x1234;
fuchsia.BIND_PCI_DID == 0x11e8;

The line using fuchsia.pci; indicates the bind library necessary to generate the bind rules into bytecode. This bind library is also specified in the fuchsia_driver_bytecode_bind_rules rule in BUILD.bazel.

The bind rules for the qemu_edu driver say the following:

  • The device is a PCI device.
  • The vendor ID (BIND_PCI_VID) needs to be 0x1234, which is a globally unique ID that identifies the vendor.
  • The device ID (BIND_PCI_DID) needs to be 0x11e8, which is a globally unique ID that the vendor has assigned to the device.

qemu_edu.fidl

The qemu_edu.fdl file describes the FIDL interface of the qemu_edu driver.

Below are the two FIDL services that the qemu_edu driver provides:

library fuchsia.hardware.qemuedu;

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 line library fuchsia.hardware.qemuedu indicates the FIDL library used by these FIDL services (see FIDL reference docs) This FIDL library needs to match the library attribute specified in the fuchsia_fidl_library rule in BUILD.bazel.

The implementation of the ComputeFactorial() and LivenessCheck() FIDL services is in the qemu_edu.cc file.

qemu_edu.h

The qemu_edu.h file is the C++ header for the implementation of the qemu_edu component (qemu_edu.cc).

Below are the public C++ function declarations for the qemu_edu driver:

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;
  static constexpr const char* Name() { return "qemu-edu"; }
  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 implementation.
  void ComputeFactorial(ComputeFactorialRequestView request,
                        ComputeFactorialCompleter::Sync& completer);
  void LivenessCheck(LivenessCheckRequestView request, LivenessCheckCompleter::Sync& completer);
  static uint32_t ComputeFactorial(uint32_t input);
...

All driver components are required to supply the following two public functions:

  • Name() – The name returned from this function is often used in qemu_edu.cc to identify the driver (for instance, to identify the driver’s placement in the topology).
  • Start() – This is the main function that gets executed as the start hook for the driver.

registers.h

The registers.h file is the header file that describes the registers of the edu device.

Below are the offset values of the registers in the edu device (for which the qemu_edu driver provides services):

constexpr uint32_t kIdentificationOffset = 0x00;
constexpr uint32_t kLivenessCheckOffset = 0x04;
constexpr uint32_t kFactorialCompoutationOffset = 0x08;
constexpr uint32_t kStatusRegisterOffset = 0x20;
constexpr uint32_t kInterruptStatusRegisterOffset = 0x24;
constexpr uint32_t kInterruptRaiseRegisterOffset = 0x60;
constexpr uint32_t kInterruptAcknowledgeRegisterOffset = 0x64;
constexpr uint32_t kDmaSourceAddressOffset = 0x80;
constexpr uint32_t kDmaDestinationAddressOffset = 0x80;
constexpr uint32_t kDmaTransferCountOffset = 0x90;
constexpr uint32_t kDmaCommandRegisterOffset = 0x98;

The qemu_edu driver uses these offset values to understand how to read and write values from the registers in the edu device. In general, driver developers would use their target device’s specification sheet to provide this offset information.

qemu_edu.cc

The qemu_edu.cc file contains the source code of the qemu_edu component, which is the qemu_edu driver itself.

Implementing the start hook

A driver is required to provide the implementation of a public Start() function, which is the main function that gets executed as the start hook for the driver. The code below is the Start() function of the qemu_edu driver:

zx::status<std::unique_ptr<QemuEduDriver>> QemuEduDriver::Start(
    fdf::wire::DriverStartArgs& start_args, fdf::UnownedDispatcher dispatcher,
    fidl::WireSharedClient<fdf::Node> node, driver::Namespace ns, driver::Logger logger) {
  auto driver = std::make_unique<QemuEduDriver>(dispatcher->async_dispatcher(), std::move(node),
                                                std::move(ns), std::move(logger));
  auto result = driver->Run(dispatcher->async_dispatcher(), std::move(start_args.outgoing_dir()));
  if (result.is_error()) {
    return result.take_error();
  }
  return zx::ok(std::move(driver));
}

This Start() function initializes an instance of the QemuEduDriver class and assigns it to the driver variable. The function then calls the private Run() function, which performs most of the initialization tasks.

In the Run() function, the first task is to connect to Fuchsia’s device filesystem (devfs), which enables the driver to expose the edu device and its services to other components in the system:

auto exporter = ns_.Connect<fuchsia_device_fs::Exporter>();
if (exporter.is_error()) {
  return exporter.take_error();
}
exporter_ = fidl::WireClient(std::move(*exporter), dispatcher);

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 storage space) and scan for the directories and files under this filesystem. Once the non-driver component locates a service, which shows up as a file in devfs, the component can contact the driver by opening this file in and writing to it (similar to a POSIX system).

After this initial contact, the driver sets up a FIDL connection (more precisely, by exchanging FIDL protocols in /svc) to the non-driver component. From this point, all communication between the component and the driver takes place using FIDL calls. (For more information, see Driver communication.) The rest of the code in the Run() function implements the setting up and mapping of this devfs-to-FIDL connection.

Computing a factorial

Once initialized, the qemu_edu driver provides the following two services to its clients (which include both non-driver components and other drivers):

  • ComputeFactorial(): Write an integer to the edu device and read the value, which is the factorial of the integer, returned from the device.
  • LivenessCheck(): Write a “challenge” value to the edu device’s register and read the value returned from the device to confirm that the device is working as expected.

When passing values to the edu device, the qemu_edu driver uses predefined offsets in the registers.h file to determine exactly which memory locations to write the input values in the device’s registers.

In Line 133 of qemu_edu.cc, the Run() function includes the following function call:

auto pci_status = MapInterruptAndMmio(std::move(pci_endpoints->client));

The ​​MapInterruptAndMmio() function (whose implementation starts in Line 45) sets up access to the register and extracts Fuchsia’s VMO (Virtual Memory Object), which enables MMIO operations (such as reading and writing to the register). And in the same function, the SetInterruptMode function in Line 82 initializes interrupt calls for the driver.

After reading an integer value from its register, the edu device uses its own factorial function (see edu.c) and its own (virtual) computing power to compute a factorial using the input integer retrieved from the register.

The ComputeFactorial() function (starting at Line 177) handles the factorial computation by writing a value into the edu device’s register (using an offset) and reading a value from the register:

void QemuEduDriver::ComputeFactorial(ComputeFactorialRequestView request,
                                     ComputeFactorialCompleter::Sync& completer) {
  // Write a value into the factorial register.
  uint32_t input = request->input;
  mmio_->Write32(input, regs::kFactorialCompoutationOffset);
  // Busy wait on the factorial status bit.
  while (true) {
    const auto status = regs::Status::Get().ReadFrom(&*mmio_);
    if (!status.busy())
      break;
  }
  // Return the result.
  uint32_t factorial = mmio_->Read32(regs::kFactorialCompoutationOffset);
  FDF_SLOG(INFO, "Replying with", KV("factorial", factorial));
  completer.Reply(factorial);
}

When the eductl “tools” component calls this ComputeFactorial() function on the driver (using a FIDL call) and passes an integer as input, the function writes this input value into the edu device’s register (using the mmio object). The function then waits until the register is ready to read. When the device finishes computing the factorial and writes its result back into the register, the while loop breaks out and the driver reads the value from the register to obtain the value of the factorial computation performed by the edu device.

To emulate a virtual device that processes requests from the qemu_edu driver, the edu device (which is implemented as part of QEMU) provides the following functions in edu.c:

  • The edu_mmio_write function (in Line 251) writes the input value to the edu device’s register and signals the factorial worker thread :

    case 0x08:
        if (atomic_read(&edu->status) & EDU_STATUS_COMPUTING) {
            break;
        }
        /* EDU_STATUS_COMPUTING cannot go 0->1 concurrently, because it is only
         * set in this function and it is under the iothread mutex.
         */
        qemu_mutex_lock(&edu->thr_mutex);
        edu->fact = val;
        atomic_or(&edu->status, EDU_STATUS_COMPUTING);
        qemu_cond_signal(&edu->thr_cond);
        qemu_mutex_unlock(&edu->thr_mutex);
        break;
    
  • The factorial thread (see the edu_fact_thread() function in Line 314) wakes up and computes the factorial and stores the result in the device’s register.

  • The edu_mmio_read() function (in Line 206) reads a value from the device’s register:

    case 0x08:
        qemu_mutex_lock(&edu->thr_mutex);
        val = edu->fact;
        qemu_mutex_unlock(&edu->thr_mutex);
        break;
    

However, keep in mind that the edu device performs operations that are far more complex than necessary in order to make itself behave like a real hardware device, such as producing results asynchronously.

Tools component: eductl

The eductl component is a helper component (known as a “tools” component) that serves as a tool for testing the features of the qemu_edu driver while it’s running in the system.

The eductl “tools” component comprises the following files:

meta/eductl.cml

The meta/eductl.cml file contains the component manifest of the eductl component

Below are the attributes of the component manifest for the eductl component:

{
    program: {
        runner: 'elf',
        binary: 'bin/eductl',
        forward_stderr_to: "log",
        forward_stdout_to: "log",
        args: [
          'factorial',
          '12',
        ]
    },
    use: [
        {
            directory: "dev",
            rights: [ "rw*" ],
            path: "/dev",
        },
        { protocol: "fuchsia.logger.LogSink" },
    ],
}

The program field describes which program to start when this component starts running on the Fuchsia platform. The runner attribute specifies that this component uses the elf runtime, which is commonly used to run C++ and Rust binaries on the Fuchsia platform. The binary attribute indicates that the bin/eductl file contains the binary of the program. Lastly, the args field shows that the default input arguments provided to the program is factorial 12.

The use field sets up access to Fuchsia’s device filesystem (devfs) for the component. This setup is required for non-driver components to interact with drivers in a Fuchsia system. (For more information on the role of the device filesystem, see Driver communication.)

eductl.cc

The eductl.cc file contains the source code of the “tools” component, which enables driver developers to interact with the qemu_edu driver while the driver is running in a Fuchsia system. It’s common for driver developers to create these tools components for the purpose of testing and debugging during development. However, in production, the ability to use these driver-specific tools will be disabled.

Below are the hardcoded device paths that are mapped to the qemu_edu device running in a Fuchsia system:

constexpr char kEduDevicePath[] =
    "/dev/sys/platform/platform-passthrough/PCI0/bus/00:06.0_/qemu-edu";
constexpr char kEduDevicePath2[] = "/dev/sys/platform/platform-passthrough/acpi/acpi-KBLT/qemu-edu";

Using paths in Fuchsia’s device filesystem (devfs), non-driver components, such as the eductl component, can discover services provided by the drivers running in the system. (For more information, see Service discovery (using devfs).)

Below is the function that uses the device paths to establish a FIDL connection to the qemu_edu driver:

fidl::WireSyncClient<fuchsia_hardware_qemuedu::Device> OpenDevice() {
  int device = open(kEduDevicePath, O_RDWR);
  if (device < 0) {
    device = open(kEduDevicePath2, O_RDWR);
  }
  if (device < 0) {
    fprintf(stderr, "Failed to open qemu edu device: %s\n", strerror(errno));
    return {};
  }
  fidl::ClientEnd<fuchsia_hardware_qemuedu::Device> client_end;
  zx_status_t st = fdio_get_service_handle(device, client_end.channel().reset_and_get_address());
  if (st != ZX_OK) {
    fprintf(stderr, "Failed to get service handle: %s\n", zx_status_get_string(st));
    return {};
  }
  return fidl::BindSyncClient(std::move(client_end));
}

In this OpenDevice() function, the eductl component uses the device paths in devfs to contact the qemu_edu driver in the system. Once the initial contact is made, a FIDL connection is established between the eductl component and the qemu_edu driver. From this point, all communication between these two takes place using this FIDL channel.