Numeric types
Guideline: Follow the Rust guidance to default to
i32 and use explicitly sized signed and unsigned integer types when
appropriate.
Explanation: Most driver writers come from a C / C++ background, where unsized signed integers are often used for arithmetic.
Guideline: Use std::num::NonZero types when the constraint holds.
Explanation: Ensures we handle zero as a special case. Enables Option representation optimizations.
Guideline: Use Option<std::num::NonZero<T>> types when zero represents a
special case.
Explanation: Ensures we handle zero as a special case. Nudges us to handle the special case higher in the stack and pass a bare non-zero type to lower levels.
Guideline: Use std::num::NonZero<usize> for logical memory addresses that
will be turned into non-nullable pointers, and
Option<std::num::NonZero<usize>> for nullable pointers.
Explanation: Straightforward conclusion from the guidelines above. The
Option type forces code that uses nullable pointers to be explicit about the
null case handling. Using the created pointers is unsafe Rust, and is
discouraged in a later section.
Guideline: Use zx_sys::zx_paddr_t for CPU physical memory addresses that
are passed to / obtained from Zircon.
Explanation: Clearly communicates intended usage, matches the type expected by Zircon APIs.
Guideline: Use u32 (or std::num::NonZero<u32> if a non-zero assumption
applies) / u64 (or std::num::NonZero<u64>) for CPU or device physical memory
addresses that will be written to registers.
Examples:
use std::num::NonZero;
use zx_sys::zx_paddr_t;
// Useful displays have at least one pixel.
let display_width: NonZero<u16>;
// Our FIDL APIs use 0 as an invalid ID.
let imported_image_id: Option<NonZero<u64>>;
// To be obtained from a memory pinning API.
let mut image_physical_address: zx_paddr_t;
// Will be written into a register.
let image_physical_address: NonZero<u32>;
// [`None`] when the plane is disabled.
let image_physical_address_reg_value: Option<NonZero<u32>>;
Instance creation
Guideline: Default to naming factory functions new(). This default applies
to fallible and async functions.
Explanation: Matches current prevailing Rust usage.
Examples:
use zx;
struct Data {}
impl Data {
pub async fn new() -> Result<Data, zx::Status> {
// ...
}
}
Guideline: Use the try_new() name for a fallible factory function when a
type also exposes an infallible factory function.
Explanation: Matches current prevailing Rust usage. (This is a rare case, though.)
Guideline: Implement From<OtherType> or TryFrom<OtherType> for
infallible / fallible conversion from other types. Do not provide constructors
that effectively do type conversions.
Explanation: Helps distinguish between conversion and more involved instance creation.
FIDL bindings
Guideline: Exclusively use the fidl_next bindings.
Explanation: fidl_next bindings are required for the driver transport. Standardizing on them saves (human and AI) developers from context-switching between two bindings.
Unused code
Guideline: Use #[expect(dead_code)] with an explanatory comment. Do not
use #[allow(dead_code)].
Explanation: The #[expect] version is enforced by the compiler, and
therefore protected from falling out of date.
Guideline: Do not use the underscore (_) variable name prefix where
#[expect(dead_code)] can be used instead.
Explanation: The underscore prefix is equivalent to #[allow(dead_code)],
so the reasoning above applies.
Guideline: Use the underscore expression where it applies. Explain the
reasoning behind discarding values (like Result) when it's not immediately
obvious.
Explanation: The underscore expression is a different language construct from the underscore prefix in variable names.
Examples:
use fdf_component::{Driver, Node};
use zx;
struct DisplayDriver {
// We must keep the Node alive for the lifetime of the driver.
#[expect(dead_code)]
device_node: Node,
}
impl Driver for DisplayDriver {
async fn stop(&self) {
// Intentionally ignoring failure during device shutdown. There's
// nothing we can do at this point.
let _ = fallible_function_that_logs();
}
}
fn fallible_function_that_logs() -> Result<(), zx::Status> { /* ... */ }
Representation
Guideline: Place #[repr(...)] attributes above all other attributes.
Explanation: Having the representation defined early makes it more likely that macros operating on the structure use the correct data layout.
Guideline: Stick to the default representation (rust) unless the type's
in-memory representation must be fixed. The in-memory representation must be
fixed if and only if values are directly loaded from or stored into memory
shared between the driver and a different piece of software or hardware. Follow
the guidelines below to choose a non-default representation.
Explanation: The default representation maximizes the compiler's
opportunities for optimization. We can use the rust representation in shared
memory, when we're guaranteed that the memory is used by multiple instances of
the same compiled binary. We have to use a fixed memory representation when
there are multiple pieces of software (different binaries) or hardware (devices)
using the in-memory values.
Guideline: Use #[repr(transparent)] for Rust "newtypes" that need a fixed
in-memory representation.
Explanation: #[repr(transparent)] encodes the "newtype" intent. The
compiler enforces that the struct wraps a single non-zero-sized type field.
Guideline: Use #[repr(C)] for multi-field composite types that need a
fixed in-memory representation.
Explanation: #[repr(C)] encodes the intent to produce a composite type
with deterministic field offsets and alignments.
Guideline: For every type that needs a fixed in-memory representation, have unit tests checking each type's size and alignment, and each type member's offset.
Explanation: Translating from vendor documentation to Rust is non-trivial, and we use tests to reduce the risk of errors.
Guideline: When specifying a custom representation, #[derive()] the
following traits: Copy, Clone, zerocopy::FromBytes, zerocopy::Immutable,
zerocopy::IntoBytes, zerocopy::KnownLayout.
Explanation:
Copymakes it easy to reason about pointer operationsCloneis required byCopyzerocopy::FromBytesproves that the type can be used to read any bit patternzerocopy::FromZerosis implied byzerocopy::FromByteszerocopy::Immutableproves that the type does not use interior mutabilityzerocopy::IntoBytesproves that the type can be treated as a sequence of byteszerocopy::KnownLayoutis required by otherzerocopyderived traits
Examples:
use bitfield::bitfield;
use zerocopy::{FromBytes, Immutable, IntoBytes, KnownLayout};
bitfield! {
#[repr(transparent)]
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
struct CommandFlags(u32) {}
}
#[repr(C)]
#[derive(Copy, Clone, FromBytes, Immutable, IntoBytes, KnownLayout)]
struct Command {
pub flags: CommandFlags;
pub id: u32;
}
#[cfg(test)]
mod tests {
use super::*;
use std::mem::{align_of, offset_of, size_of};
#[fuchsia::test]
fn test_command_abi() {
assert_eq!(size_of::<Command>(), 8);
assert_eq!(align_of::<Command>(), 4);
assert_eq!(offset_of!(Command, flags), 0);
assert_eq!(offset_of!(Command, id), 4);
}
}
use paths
Guideline: Follow the official defaults for idiomatic use paths:
- Bring into scope: types, derive and attribute-like macros
- Bring parent module into scope: functions, function-like macros
Explanation: Matches the established recommendation in The Rust Programming Book section on idiomatic use paths.
Guideline: Bring into scope the top-level module of the zx crate.
Explanation:
The zx crate exports generic type names such as Channel and Event, which
were intended to be read with a zx:: prefix -- for example, zx::Event
reads as "Zircon event". This is an intentional deviation from
the Rust Programming Book section on idiomatic use paths,
which recommends bringing into scope the parent modules for both types involved
in a name conflict.
Guideline: Bring into scope the parent module for register or ABI definition
types. When it makes sense, alias the modules as abi or registers.
Explanation: Similar reasoning to the rule above. We deviate from the Rust convention because register and ABI type names are likely to overlap Rust driver type names, as they cover the same domain. We reuse the practice in C++ drivers that gave us a good tradeoff between clarity and conciseness.
Guideline: Alias each fidl_next binding module. Use the fidl_ prefix for
all aliases. Use the alias to qualify access to both structs and functions.
Explanation: Same reasoning as above. FIDL and Rust driver type names are likely to overlap.
Guideline: Bring the logging macros into scope directly.
Explanation: The Rust Book section on idiomatic use paths recommends module qualifiers on function calls, with an exception for common functions that are close to language-level features. Logging falls under the exception.
Examples:
use fidl_next_fuchsia_sysmem2 as fidl_sysmem2;
use fidl_next;
use log::warn;
pub async fn use_buffer_collection(
sysmem_buffer_collection: &mut fidl_next::Client<fidl_sysmem2::BufferCollection>,
) {
/* ... */
warn!("Failed to retrieve hardware pixel formats, falling back to safe set");
/* ... */
}
MMIO region management
Guideline: Use the MmioRegion<VmoMemory, Arc<VmoMemory>> type for all MMIO
*memory regions.
Explanation: std::sync::Arc meets the MmioSplit trait constraints,
allowing any module to further subdivide the MMIO region it receives. Arc is
compatible with any threading model (as opposed to Rc). The atomic overhead is
negligible, because the reference count is only changed during driver start and
stop.
Guideline: Use split_off. Do not use try_split_off.
Explanation: Hardware presents well-known static MMIO maps. The driver code for building the maps should not need any conditional logic.
Examples:
use fidl_next_fuchsia_hardware_platform_device as fidl_platform_device;
use mmio::MmioSplit;
use mmio::region::MmioRegion;
use mmio::vmo::VmoMemory;
use std::sync::Arc;
/// Obtains an MMIO region from the Platform Device.
///
/// Logs on failure.
async fn map_mmio_range(
platform_device: &fidl_next::Client<fidl_platform_device::Device>,
range_name: &str,
) -> Result<MmioRegion<VmoMemory, Arc<VmoMemory>>, zx::Status> {
let region = platform_device.map_mmio_by_name(range_name).await.map_err(|err| {
error!("Failed to map MMIO range {range_name}: {err:?}");
err.log_to_status()
})?;
Ok(region.into_split_send())
}
impl FunctionalUnit {
pub fn new(mmio: MmioRegion<VmoMemory, Arc<VmoMemory>>) {
// Subunit 1 manages the MMIO range 0x0000..0x1000.
let subunit1_mmio = mmio.split_off(0x1000);
let subunit1 = SubUnit1::new(subunit1_mmio);
// Subunit 2 manages the MMIO range 0x1000..0x2000.
let subunit2_mmio = mmio.split_off(0x1000);
let subunit2 = SubUnit1::new(subunit1_mmio);
// Subunit 3 manages the MMIO range from 0x2000 onwards.
let subunit3 = SubUnit3::new(mmio);
Self { subunit1, subunit2, subunit3 }
}
}
Pointers and shared memory
Guideline: Use pointers to access memory that is shared with hardware.
Explanation: The Rust memory model’s assumptions trigger on reference creation, and do not require references to be accessed.
Guideline: Immediately convert a usize returned by a system call to a
std::num::NonZero<usize>.
Explanation: Forcing function to assert that the system call returns a non-null pointer on success.
Guideline: When a pointer’s target can be expressed by a Rust type,
immediately convert std::num::NonZero<usize> into a std::ptr::NonNull to the
type.
Explanation: Minimize the potential for errors.
Guideline: When pointer arithmetic is necessary, immediately convert
std::num::NonZero<usize> into std::ptr::NonNull, then use methods like
add() / byte_add() and cast().
Explanation: Reduced potential for errors. Pointer provenance is maintained.
Examples:
use std::num::NonZero;
use std::ptr::NonNull;
use zx;
#[repr(...)]
#[derive(...)]
struct Header { /* ... */ }
#[repr(...)]
#[derive(...)]
struct Trailer { /* ... */ }
struct SharedMemory {
header_ptr: NonNull<Header>,
trailer_ptr: NonNull<Trailer>,
}
impl SharedMemory {
pub fn new() -> Result<Self, zx::Status> {
let data_address = fuchsia_runtime::vmar_root_self().map(...)?;
let data_address = NonZero::<usize>::new(data_address)
.expect("zx::vmar::map() returned null address");
// [`Option::unwrap()`] is guaranteed not to panic. The [`NonZero::new()`]
// call above already checked that the pointer is non-null.
let header_ptr = NonNull::new(
std::ptr::with_exposed_provenance_mut(data_address.get())
).unwrap();
// SAFETY: The memory allocation covers both [`Header`] and [`Trailer`].
let trailer_ptr = unsafe { header_ptr.add(1) }.cast::<Trailer>();
Ok(Self { header_ptr, trailer_ptr })
}
}
Logging
Guideline: Only use the ERROR level to report conditions caused by bugs in Fuchsia. Do not assume that the ERROR level is always appropriate when returning an error.
Explanation: Corrects a popular misunderstanding of RFC-0003.
Guideline: Use the WARNING level to report hardware failures that can occur assuming correct driver operation.
Explanation: Follows from RFC-0003.
Examples:
use log::warn;
use zx;
/// Errors if the hardware returns an invalid version.
///
/// All error conditions are logged.
pub fn read_version() -> Result<u32, zx::Status> {
let version_value: u32 = read_from_register();
if version_value == 0 {
warn!("Invalid version, device probably powered off: {}", version_value);
// ...
}
// ...
}
Use the try operator
Guideline: Prefer the try operator (?) over all other error handling
alternatives. Prioritize your callers’ ability to use the try operator when
designing function interfaces.
Explanation: Concise error handling lets readers focus on the higher-level
picture. The try operator matches the error handling in C++ display drivers,
where zx::result<> errors are bubbled up the stack.
Guideline: Design the errors used in returned value types to facilitate the
use of ?. In particular, prefer using zx::Status as the error type in a
Result.
Explanation: For now, follow the same error handling approach as the C++ display drivers.
Guideline: Clearly document when a function logs a condition that produces an error result.
Explanation: Developers who are assured that the condition is logged can use the try operator. This produces more concise code, and reduces redundant logging.
Examples:
use zx;
/// Errors if the hardware returns an invalid version.
///
/// All error conditions are logged.
pub fn read_version() -> Result<u32, zx::Status> { /* ... */ }
/// Initializes the hardware so it can receive commands.
///
/// All error conditions are logged.
pub fn initialize_hardware() -> Result<(), zx::Status> {
let version_value = read_version()?;
/* ... */
}
Naming: length vs size vs capacity
Guideline: Use length to name variables that count the number of elements
in a collection. Use len() for functions.
Explanation: Matches Rust common practice, such as Vec::len().
Guideline: Use size_bytes to name variables and functions that count the
number of bytes used to store or transmit something. Do not create functions
that would be redundant with invocations of core::mem::size_of.
Explanation: "Size" is idiomatically used in Rust to name this concept. However, the "size" term shows up a lot in hardware-related documents. The "_bytes" suffix helps disambiguate.
Guideline: Use capacity to name variables and functions that report the
maximum number of elements supported by the memory backing a collection. In
particular, a collection whose backing storage never changes has fixed capacity,
and its length changes as elements are inserted and removed.
Explanation: Matches Rust common practice, such as Vec::capacity.
Links in documentation
Guideline: Link all identifiers supported by Rustdoc’s link-by-name feature.
Explanation: Recommended by Rust API guidelines on documentation. rust-analyzer can navigate the links.
Curated list of disambiguator prefixes that suggest what Rustdoc can link to: value, constant, primitive, module, function, type, typealias, struct, field, method, trait, enum, variant, union, macro, derive.
Example:
/// See [`DeviceBuilder`] for obtaining instances.
pub struct Device {}
Minimize nesting level
Guideline: After calling a fallible function, immediately check for errors, optionally log the error, and return.
Explanation: Human reviewers prefer reading a process laid out as a sequence of steps, rather than nested conditional blocks.
Guideline: Extract non-trivial error handling to a dedicated function. Trivial error handling is logging and returning.
Explanation: Human reviewers have an easier time analyzing functions that implement a single process.
Field visibility
Guideline: Fields on composite types must be all public - this means pub
or pub(crate) or all private.
Explanation: Public fields are not amenable to invariants.
Additional guides
This document focuses on issues commonly encountered while reviewing code produced by AI agents.
Display drivers also follow the best practices below.
- The Rust API Guidelines{.external}
- The Rust Style Guide{.external} as implemented by
fx format-code - The rustdoc book{.external}
- The Rustonomicon{.external} (mostly informative, covers
unsafecode)