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:
- A
bindfile defining the driver'sbindrules - A component manifest
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
Nodeobject will cause the driver to be shut down, intentionally or not. In most cases this will be unused, but should be stored in theDriverobject anyways (as_nodeto silence the warning about it being unused). This will correctly drop theNodewhen 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_bytecodefuchsia_rust_driverfuchsia_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/writemethods, use real VMOs to back the memory region. - Use VMO Injection: In tests, create a
zx::Vmo, map it usingVmoMapping::map(), and pass the resultingMmioRegionto the driver.