| # Rubric for display drivers written in Rust |
| |
| ## Numeric types |
| |
| **Guideline:** Follow [the Rust guidance][rust-book-integers] 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][rust-option-representation]. |
| |
| **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: |
| |
| ```rust |
| 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: |
| |
| ```rust |
| 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: |
| |
| ```rust |
| 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:** |
| |
| - `Copy` makes it easy to reason about pointer operations |
| - `Clone` is required by `Copy` |
| - `zerocopy::FromBytes` proves that the type can be used to read any bit pattern |
| - `zerocopy::FromZeros` is implied by `zerocopy::FromBytes` |
| - `zerocopy::Immutable` proves that the type does not use interior mutability |
| - `zerocopy::IntoBytes` proves that the type can be treated as a sequence of bytes |
| - `zerocopy::KnownLayout` is required by other `zerocopy` derived traits |
| |
| Examples: |
| |
| ```rust |
| 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][rust-book-idiomatic-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][rust-book-idiomatic-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][rust-book-idiomatic-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: |
| |
| ```rust |
| 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: |
| |
| ```rust |
| 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: |
| |
| ```rust |
| 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][logging-rfc]. |
| |
| **Guideline:** Use the WARNING level to report hardware failures that can occur |
| assuming correct driver operation. |
| |
| **Explanation:** Follows from RFC-0003. |
| |
| Examples: |
| |
| ```rust |
| 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: |
| |
| ```rust |
| 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][rustdoc-link-by-name]. |
| |
| **Explanation:** Recommended by |
| [Rust API guidelines on documentation][rust-api-guidelines-documentation-links]. |
| [rust-analyzer][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: |
| |
| ```rust |
| /// 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][rust-api-guidelines]{.external} |
| - [The Rust Style Guide][rust-style-guide]{.external} as implemented by `fx format-code` |
| - [The rustdoc book][rustdoc-howto]{.external} |
| - [The Rustonomicon][rustonomicon]{.external} (mostly informative, covers `unsafe` code) |
| |
| [google-cpp-style-integers]: https://google.github.io/styleguide/cppguide.html#Integer_Types |
| [logging-rfc]: /docs/contribute/governance/rfcs/0003_logging.md |
| [rust-analyzer]: https://rust-analyzer.github.io/ |
| [rust-api-guidelines]: https://rust-lang.github.io/api-guidelines/ |
| [rust-api-guidelines-documentation-links]: https://rust-lang.github.io/api-guidelines/documentation.html#prose-contains-hyperlinks-to-relevant-things-c-link |
| [rust-book-idiomatic-paths]: https://doc.rust-lang.org/book/ch07-04-bringing-paths-into-scope-with-the-use-keyword.html#creating-idiomatic-use-paths |
| [rust-book-integers]: https://doc.rust-lang.org/book/ch03-02-data-types.html#integer-types |
| [rust-option-representation]: https://doc.rust-lang.org/std/option/#representation |
| [rust-style-guide]: https://doc.rust-lang.org/style-guide/ |
| [rustdoc-howto]: https://doc.rust-lang.org/rustdoc/how-to-write-documentation.html |
| [rustdoc-link-by-name]: https://doc.rust-lang.org/rustdoc/write-documentation/linking-to-items-by-name.html |
| [rustonomicon]: https://doc.rust-lang.org/nomicon/ |