Rust 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

The Rust driver source code should be in a src subdirectory in a file called lib.rs. The Rust source code should use the fdf_component library to define the driver.

The driver must be defined as a struct that implements the fdf_component::Driver trait. The implemented start method receives a DriverContext struct which contains structures necessary to connect to and serve protocols and logs for the driver. Additionally, the driver code must use the driver_register!() macro to register the driver with the Driver Framework.

Component manifest

The component manifest should not declare the main dispatcher to allow_sync_calls since Rust drivers must be asynchronous.

Initializing a driver

All initialization logic should be placed in the start method and must store the Node handle from the DriverContext to the Driver struct. The initialization logic contains:

  • Take (using DriverContext::take_node) and store the Node object for the driver until shutdown. Dropping the Node object will cause the driver to be shut down, intentionally or not. In most cases this will be unused, but should be stored in the Driver object anyways (as _node to silence the warning about it being unused). This will correctly drop the Node when the driver shuts down.
  • 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 own service).
  • Release BTI from quarantine.

Shutting down a driver

In the majority of scenarios, Driver stop() implementation is empty. Refrain from adding more to it unless explicit clean up specifically required for your driver's functionality, such as performing graceful hardware shutdowns or unpinning DMA.

If a driver controls when it should be shut down, it should store the Node object as something that can be dropped, like in an Option<Node> or potentially behind a mutex if necessary. Dropping the Node object will start the driver shutting down.

Build file

All Rust drivers must use GN for the build process and must include the following targets:

  • fuchsia_driver_bind_bytecode
  • fuchsia_rust_driver
  • fuchsia_driver_component

Driver communication

Drivers communicate with their parent drivers through FIDL services.

Serving a service

Use examples/drivers/transport/driver/rust_next/parent and examples/drivers/transport/zircon/rust_next/parent/ as guidance for using the rust_next bindings, which Fuchsia recommends for normal FIDL transport services, and requires for driver transport services (as the old bindings do not support it).

Drivers serving FIDL services must implement the FIDL service as a trait. During initialization, it needs to add a ServiceOffer in an outgoing directory and then serve the directory to the DriverContext with the serve_outgoing() function. Once the directory is served, spawn a task with the fuchsia_async Scope library to run the ServiceFs event loop in.

The CML file must specify the service in the capability and expose it from self.

Using a service

Use examples/drivers/transport/driver/rust_next/child and examples/drivers/transport/zircon/rust_next/child/ as guidance for using the rust_next bindings, which Fuchsia recommends for normal FIDL transport services, and requires for driver transport services (as the old bindings do not support it).

To use a service, drivers should include it in their bind rules and specify it within the uses section of the CML. When connecting to services, use the service capability instead of the protocol.

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 necessary, the child node should be added as part of the initialization logic in the start() function.

Child nodes should be added using the Rust wrapper Node::add_child. All child nodes should be unowned unless it’s being used to support devfs, which is unsupported in Rust drivers. Therefore, there should be no owned children.

Logging

Use the standard Rust logging API for all logs. Follow the Fuchsia logging guidelines by using warning or error log levels when documenting failures such as FIDL errors.

Zircon resources

Use the Zircon kernel bindings for resources such as Vmo, and Interrupt.

Interrupts

Avoid storing interrupts as raw zx::Handle objects, as this necessitates using unsafe code during wait operations. Instead, define your device struct as generic over K: zx::InterruptKind (for example, Device<K>) using the Zircon interrupt bindings (sdk/rust/zx/src/interrupt.rs) and use it to wrap and store the handle within a zx::Interrupt<K> object.

Use fasync::OnInterrupt to create a stream of interrupts and process them in an async task instead of spawning a dedicated thread with std::thread::spawn and blocking on irq.wait().

DMA

MMIO handles must be mapped into a MmioRegion object using VmoMapping::map() (sdk/lib/driver/mmio/rust/). Drivers must never perform manual bitwise operations (for example, val |= 1 << 5;) on raw integers when accessing MMIO registers. Instead, they should use the mmio::register! and mmio::register_block! macros

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.

Asynchronous code

Do not detach tasks using .detach(). Instead, initialize a fuchsia_async::Scope within the driver's start method and use it to spawn concurrent work. The driver must retain ownership of the Scope.

Testing

Verify that build targets include the necessary driver tests.

Unit Testing

You can and should test as much of your driver’s code in normal unit tests. To "unit test" something while including driver startup and shutdown, you can use the fdf_component::testing::TestHarness to start your driver and interact with it.

If your driver declaration had an output_name of my_driver, then the GN targets for your driver would be my_driver_test. See the GN rules for any of the rust example drivers in //examples/drivers for examples.

Integration Tests

Use the DriverTestRealm library to write integration tests, just like you would with a C++ driver.

General testing advice

When testing drivers that use MmioRegion:

  • Avoid Manual Mocks: Instead of mocking read/write methods, use real VMOs to back the memory region.
  • Use VMO Injection: In tests, create a zx::Vmo, map it using VmoMapping::map(), and pass the resulting MmioRegion to the driver.