C++ driver rubric

Writing a basic driver

The code structure should follow the fx create driver goldens template.

Meta directory

Every driver directory is required to have a meta subdirectory containing these files:

Driver source code

Write drivers using the Driver Component library (sdk/lib/driver/component/). Drivers should inherit from the fdf::DriverBase2 class defined in the header <lib/driver/component/cpp/driver_base2.h>.

The driver provides the FUCHSIA_DRIVER_EXPORT2 macro used to export the driver symbol which is defined in the header <lib/driver/component/cpp/driver_export2.h>. This macro should be located in a .cc file.

Initializing a driver

Avoid placing any logic within the driver's constructor. Rather than using a constructor, you must implement the driver’s initialization logic by overriding exactly one of the Start() methods provided by fdf::DriverBase2, even if the initialization logic is empty. Ensure that only a single Start() variant is used for this purpose.

The initialization logic contains:

  • Fetching and configuring all driver resources.
  • Establishing service connections.
  • Adding the driver's own service to the outgoing directory.
  • Add child nodes (to be performed after resource setup and providing its own service).
  • Release BTI from quarantine with zx_bti_release_quarantine().

Shutting down a driver

The driver's destructor should remain free of any logic. In the majority of scenarios, overriding and providing implementation for DriverBase2::Stop() is unnecessary. Refrain from implementing DriverBase2::Stop() unless it is specifically required for your driver's functionality, such as performing graceful hardware shutdowns or unpinning DMA. The fdf::StopCompleter in the function parameter must be called at the end of the function.

Build file

All new drivers must use Bazel for the build process and must include the following targets:

  • fuchsia_driver_bind_bytecode
  • fuchsia_cc_driver
  • fuchsia_driver_component

Driver communication

Drivers communicate with their parent drivers through FIDL services.

Serving a service

Drivers serving FIDL services are required to maintain a fidl::ServerBindingGroup (when using Zircon transport) or an fdf::ServerBindingGroup (for the driver transport). Unless the driver must be restricted to a single client connection, avoid using fidl::ServerBinding or fdf::ServerBinding.

The server bindings must be added to the driver’s outgoing directory within the Start() function before any child nodes are created. Furthermore, the CML file must specify the service in the capability and expose it from self.

Using a service

To use a service, drivers should include it in their bind rules and specify it within the uses section of the CML.

Adding a child

The primary reason for adding a child node is if the driver needs to provide services or resources for another driver. Avoid adding a child without reason. Unless the driver needs to add child nodes dynamically, the child node should be added as part of the initialization logic in the driver’s Start() function.

Child nodes should be added using DriverBase2's AddChild() or the add_child helper library (sdk/lib/driver/node/cpp/add_child.h). The helper library should only be used if the child node needs to use a different logger.

All child nodes should be unowned unless it’s being used to support devfs, which is deprecated. If the child node is owned, then the driver must store the fuchsia_driver_framework::Node client end that it receives from the helper functions.

When adding a child node, explicitly define the child node’s name instead of using name() from DriverBase2. Create the offers with the node_offers helper library (sdk/lib/driver/component/cpp/node_offers.h). Create the properties using the node_properties library (sdk/lib/driver/component/cpp/node_properties.h). The node property key and values should use the generated bind library code bindings.

Logging

For logging, drivers should use format-based log APIs like fdf::info() found in the driver logger library (sdk/lib/driver/logging/cpp/logger.h). If you need to log a custom type, implement the std::format.

Follow the Fuchsia logging guidelines by using warning or error log levels when documenting failures such as FIDL errors.

Zircon resources

Interrupts

Interrupts should be retrieved and stored in the driver during initialization. The interrupts should be wrapped by an async::Irq object (sdk/lib/async/include/lib/async/cpp/irq.h).

An IrqMethod object should be used to handle interrupt triggers. Once the interrupt trigger is handled, the Irq object should be acknowledged with ack() so the interrupt is re-armed and can be triggered again.

DMA

For memory operations, drivers should use MmioBuffer, located at sdk/lib/driver/mmio/cpp/mmio-buffer.h. This library provides a wrapper around the raw mmio_block_t object for reading and writing.

To ensure safe access to register bitfields, it is recommended to use the hwreg/bitfields library. This library is found at zircon/system/ulib/hwreg/include/hwreg/bitfields.h.

The driver should take control of its BTI and stop any DMA which might be ongoing during initialization. Once that is complete, it should tell the BTI that it has regained control of the hardware by calling zx_bti_release_quarantine() on it. Drivers that intend to share DMA with hardware are required to pin the BTI beforehand. After the hardware has finished accessing the memory, the driver is responsible for unpinning it. Under no circumstances should the BTI be unpinned within the destructor.

Clocks

Clocks are controlled via the fuchsia.hardware.clock FIDL service. Drivers are required to call Enable() on all clocks they depend on and subsequently call Disable() once the clock signal is no longer needed. Drivers must not call Disable() without first enabling the clock.

Testing

Verify that build targets include the necessary driver tests.

Unit tests

Unit tests should be written using gtests (third_party/googletest/src/googletest/include/gtest/gtest.h).

Testing the entire driver

If the entire driver is being tested, the driver should be wrapped by ForegroundDriverTest or BackgroundDriverTest from the driver test library (sdk/lib/driver/testing/cpp/driver_test.h). The unit test should call StartDriver() right after the test is initialized and call StopDriver() in the Teardown() function. All services and resources needed by the driver should be initialized and served in the custom driver test’s Environment class.

If the driver requires a platform device service, the test should instantiate a FakePlatformDevice (sdk/lib/driver/fake-platform-device/) DriverTestEnvironment and serve it. Similarly, if the driver requires a FIDL service, a test implementation of the service should be instantiated and served in the DriverTestEnvironment.

Testing parts of the driver

Tests may require specific driver environment components if only a subset of the driver's logic is being isolated for testing.

When evaluating logic that utilizes the driver logger library (sdk/lib/driver/logging/cpp/logger.h), the test is required to instantiate and maintain a fdf_testing::ScopedGlobalLogger instance (sdk/lib/driver/testing/cpp/scoped_global_logger.h).

Furthermore, if a driver dispatcher is necessary for the logic, the test must configure and hold a fdf_testing::DriverRuntime object (sdk/lib/driver/testing/cpp/driver_runtime.h).

Fakes/mocks libraries

Tests requiring fake FIDL services or resources should utilize the fake and mock libraries provided in sdk/lib/driver/ whenever they are available.

The following libraries are available for common FIDL services:

The following libraries are available for common internal Zircon objects, memory regions, or driver-runtime resources:

Integration tests

Integration tests should use the Driver Test Realm (sdk/lib/driver_test_realm/) framework. To begin, the test must establish a connection to the DriverTestRealm service, perform necessary configuration in RealmArgs, and then call Start() with the arguments.