google/safe-mmio

A Rust library crate with types for safe MMIO device access

Rust

42

155 commits

updated Oct 19, 2026

See the code

README

safe-mmio

crates.io page docs.rs page

This crate provides types for safe MMIO device access, especially in systems with an MMU.

This is not an officially supported Google product.

Usage

UniqueMmioPointer

The main type provided by this crate is UniqueMmioPointer. A UniqueMmioPointer<T> is roughly equivalent to an &mut T for a memory-mapped IO device. Suppose you want to construct a pointer to the data register of some UART device, and write some character to it:

use core::ptr::NonNull;
use safe_mmio::UniqueMmioPointer;

let mut data_register =
    unsafe { UniqueMmioPointer::<u8>::new(NonNull::new(0x900_0000 as _).unwrap()) };
unsafe {
    data_register.write_unsafe(b'x');
}

Depending on your platform this will either use write_volatile, some platform-dependent inline assembly, or a custom backend to perform the MMIO write.

Safe MMIO methods

If you know that a particular MMIO field is safe to access, you can use the appropriate wrapper type to mark that. In this case, suppose that the UART data register should only be written to:

use core::ptr::NonNull;
use safe_mmio::{fields::WriteOnly, UniqueMmioPointer};

let mut data_register: UniqueMmioPointer<WriteOnly<u8>> =
    unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
data_register.write(b'x');

Grouping registers with a struct

In practice, most devices have more than one register. To model this, you can create a struct, and then use the field! macro to project from a UniqueMmioPointer to the struct to a pointer to one of its fields:

use core::ptr::NonNull;
use safe_mmio::{
    field,
    fields::{ReadOnly, ReadPure, ReadWrite, WriteOnly},
    UniqueMmioPointer,
};

#[repr(C)]
struct UartRegisters {
    data: ReadWrite<u8>,
    status: ReadPure<u8>,
    pending_interrupt: ReadOnly<u8>,
}

let mut uart_registers: UniqueMmioPointer<UartRegisters> =
    unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
field!(uart_registers, data).write(b'x');

Methods are also provided to go from a UniqueMmioPointer to an array or slice to its elements.

Pure reads vs. side-effects

We distinguish between fields which for which MMIO reads may have side effects (e.g. popping a byte from the UART's receive FIFO or clearing an interrupt status) and those for which reads are 'pure' with no side-effects. Reading from a ReadOnly or ReadWrite field is assumed to have side-effects, whereas reading from a ReadPure or ReadPureWrite must not. Reading from a ReadOnly or ReadWrite field requires an &mut UniqueMmioPointer (the same as writing), whereas reading from a ReadPure or ReadPureWrite field can be done with an &UniqueMmioPointer or &SharedMmioPointer.

Physical addresses

UniqueMmioPointer (and SharedMmioPointer) is used for a pointer to a device which is mapped into the page table and accessible, i.e. a virtual address. Sometimes you may want to deal with the physical address of a device, which may or may not be mapped in. For this you can use the PhysicalInstance type. A PhysicalInstance doesn't let you do anything other than get the physical address and size of the device's MMIO region, but is intended to convey ownership. There should never be more than one PhysicalInstance pointer to the same device. This way your page table management code can take a PhysicalInstance<T> and return a UniqueMmioPointer<T> when a device is mapped into the page table.

Custom MMIO backend

Some environments need to intercept MMIO accesses instead of letting them hit hardware directly. The custom-mmio feature lets the consuming project inject its own read/write implementations, even across the dependency chain, while keeping the rest of the safe-mmio API unchanged.

Enable the feature in your Cargo.toml:

[dependencies]
safe-mmio = { version = "0.3.1", features = ["custom-mmio"] }

Then implement the MmioOps trait and register it with set_mmio_ops!:

use safe_mmio::{MmioOps, set_mmio_ops};

struct MyBackend;

// SAFETY: Each method performs a single MMIO access of the indicated width.
unsafe impl MmioOps for MyBackend {
    unsafe fn read_u8(src: *const u8) -> u8 { /* ... */ }
    unsafe fn read_u16(src: *const u16) -> u16 { /* ... */ }
    unsafe fn read_u32(src: *const u32) -> u32 { /* ... */ }
    unsafe fn read_u64(src: *const u64) -> u64 { /* ... */ }
    unsafe fn write_u8(dst: *mut u8, value: u8) { /* ... */ }
    unsafe fn write_u16(dst: *mut u16, value: u16) { /* ... */ }
    unsafe fn write_u32(dst: *mut u32, value: u32) { /* ... */ }
    unsafe fn write_u64(dst: *mut u64, value: u64) { /* ... */ }
}

set_mmio_ops!(MyBackend);

The set_mmio_ops! macro generates #[no_mangle] functions that the library resolves via extern "Rust" declarations. Exactly one call must exist in the final binary; linking fails otherwise.

When custom-mmio is enabled it replaces both the default volatile backend and the aarch64 inline-assembly backend.

Testing note: Unit tests (cargo test --lib --features custom-mmio) work because the library includes a volatile-based MmioOps implementation gated on #[cfg(test)]. Doc tests build as separate binaries and cannot see #[cfg(test)] items, so use --lib to skip them.

Comparison with other MMIO crates

There are a number of things that distinguish this crate from other crates providing abstractions for MMIO in Rust.

  1. We avoid creating references to MMIO address space. The Rust compiler is free to dereference references whenever it likes, so constructing references to MMIO address space (even temporarily) can lead to undefined behaviour. See https://github.com/rust-embedded/volatile-register/issues/10 for more background on this.
  2. We distinguish between MMIO reads which have side-effects (e.g. clearing an interrupt status, or popping from a queue) and those which don't (e.g. just reading some status). A read which has side-effects should be treated like a write and only be allowed from a unique pointer (passed via &mut) whereas a read without side-effects can safely be done via a shared pointer (passed via '&'), e.g. simultaneously from multiple threads.
  3. On most platforms MMIO reads and writes can be done via read_volatile and write_volatile, but on aarch64 this may generate instructions which can't be virtualised. This is arguably a bug in rustc, but in the meantime we work around this by using inline assembly to generate the correct instructions for MMIO reads and writes on aarch64.
Crate nameLast releaseVersionAvoids referencesDistinguishes reads with side-effectsWorks around aarch64 volatile bugModelField projectionNotes
safe-mmioJuly 20260.3.1✅✅✅struct with field wrappersmacro
derive-mmioJuly 20260.8.0✅✅❌struct with derive macrothrough derive macro
volatileJune 20240.6.1✅❌❌struct with derive macromacro or generated methods
volatile-registerOctober 20230.2.2❌❌❌struct with field wrappersmanual (references)
tock-registersOctober 20250.10.1❌❌❌macros to define fields and structsmanual (references)Also covers CPU registers, and bitfields
mmioMay 20212.1.0✅❌❌only deals with individual fields❌
rumioMarch 20210.2.0✅❌❌macros to define fields and structsgenerated methodsAlso covers CPU registers, and bitfields
vcellJanuary 20210.1.3❌❌❌plain structmanual (references)
registerJanuary 20211.0.2❌❌❌macros to define fields and structsmanual (references)Deprecated in favour of tock-registers. Also covers CPU registers, and bitfields.
voladdressMay 20241.4.0✅❌❌array with stride, no typed fields❌

License

Licensed under either of

at your option.

Contributing

If you want to contribute to the project, see details of how we accept contributions.

mmio
no-std
rust
rust-crate

google/safe-mmio

A Rust library crate with types for safe MMIO device access

Rust

42

155 commits

updated Oct 19, 2026

See the code

README

safe-mmio

crates.io page docs.rs page

This crate provides types for safe MMIO device access, especially in systems with an MMU.

This is not an officially supported Google product.

Usage

UniqueMmioPointer

The main type provided by this crate is UniqueMmioPointer. A UniqueMmioPointer<T> is roughly equivalent to an &mut T for a memory-mapped IO device. Suppose you want to construct a pointer to the data register of some UART device, and write some character to it:

use core::ptr::NonNull;
use safe_mmio::UniqueMmioPointer;

let mut data_register =
    unsafe { UniqueMmioPointer::<u8>::new(NonNull::new(0x900_0000 as _).unwrap()) };
unsafe {
    data_register.write_unsafe(b'x');
}

Depending on your platform this will either use write_volatile, some platform-dependent inline assembly, or a custom backend to perform the MMIO write.

Safe MMIO methods

If you know that a particular MMIO field is safe to access, you can use the appropriate wrapper type to mark that. In this case, suppose that the UART data register should only be written to:

use core::ptr::NonNull;
use safe_mmio::{fields::WriteOnly, UniqueMmioPointer};

let mut data_register: UniqueMmioPointer<WriteOnly<u8>> =
    unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
data_register.write(b'x');

Grouping registers with a struct

In practice, most devices have more than one register. To model this, you can create a struct, and then use the field! macro to project from a UniqueMmioPointer to the struct to a pointer to one of its fields:

use core::ptr::NonNull;
use safe_mmio::{
    field,
    fields::{ReadOnly, ReadPure, ReadWrite, WriteOnly},
    UniqueMmioPointer,
};

#[repr(C)]
struct UartRegisters {
    data: ReadWrite<u8>,
    status: ReadPure<u8>,
    pending_interrupt: ReadOnly<u8>,
}

let mut uart_registers: UniqueMmioPointer<UartRegisters> =
    unsafe { UniqueMmioPointer::new(NonNull::new(0x900_0000 as _).unwrap()) };
field!(uart_registers, data).write(b'x');

Methods are also provided to go from a UniqueMmioPointer to an array or slice to its elements.

Pure reads vs. side-effects

We distinguish between fields which for which MMIO reads may have side effects (e.g. popping a byte from the UART's receive FIFO or clearing an interrupt status) and those for which reads are 'pure' with no side-effects. Reading from a ReadOnly or ReadWrite field is assumed to have side-effects, whereas reading from a ReadPure or ReadPureWrite must not. Reading from a ReadOnly or ReadWrite field requires an &mut UniqueMmioPointer (the same as writing), whereas reading from a ReadPure or ReadPureWrite field can be done with an &UniqueMmioPointer or &SharedMmioPointer.

Physical addresses

UniqueMmioPointer (and SharedMmioPointer) is used for a pointer to a device which is mapped into the page table and accessible, i.e. a virtual address. Sometimes you may want to deal with the physical address of a device, which may or may not be mapped in. For this you can use the PhysicalInstance type. A PhysicalInstance doesn't let you do anything other than get the physical address and size of the device's MMIO region, but is intended to convey ownership. There should never be more than one PhysicalInstance pointer to the same device. This way your page table management code can take a PhysicalInstance<T> and return a UniqueMmioPointer<T> when a device is mapped into the page table.

Custom MMIO backend

Some environments need to intercept MMIO accesses instead of letting them hit hardware directly. The custom-mmio feature lets the consuming project inject its own read/write implementations, even across the dependency chain, while keeping the rest of the safe-mmio API unchanged.

Enable the feature in your Cargo.toml:

[dependencies]
safe-mmio = { version = "0.3.1", features = ["custom-mmio"] }

Then implement the MmioOps trait and register it with set_mmio_ops!:

use safe_mmio::{MmioOps, set_mmio_ops};

struct MyBackend;

// SAFETY: Each method performs a single MMIO access of the indicated width.
unsafe impl MmioOps for MyBackend {
    unsafe fn read_u8(src: *const u8) -> u8 { /* ... */ }
    unsafe fn read_u16(src: *const u16) -> u16 { /* ... */ }
    unsafe fn read_u32(src: *const u32) -> u32 { /* ... */ }
    unsafe fn read_u64(src: *const u64) -> u64 { /* ... */ }
    unsafe fn write_u8(dst: *mut u8, value: u8) { /* ... */ }
    unsafe fn write_u16(dst: *mut u16, value: u16) { /* ... */ }
    unsafe fn write_u32(dst: *mut u32, value: u32) { /* ... */ }
    unsafe fn write_u64(dst: *mut u64, value: u64) { /* ... */ }
}

set_mmio_ops!(MyBackend);

The set_mmio_ops! macro generates #[no_mangle] functions that the library resolves via extern "Rust" declarations. Exactly one call must exist in the final binary; linking fails otherwise.

When custom-mmio is enabled it replaces both the default volatile backend and the aarch64 inline-assembly backend.

Testing note: Unit tests (cargo test --lib --features custom-mmio) work because the library includes a volatile-based MmioOps implementation gated on #[cfg(test)]. Doc tests build as separate binaries and cannot see #[cfg(test)] items, so use --lib to skip them.

Comparison with other MMIO crates

There are a number of things that distinguish this crate from other crates providing abstractions for MMIO in Rust.

  1. We avoid creating references to MMIO address space. The Rust compiler is free to dereference references whenever it likes, so constructing references to MMIO address space (even temporarily) can lead to undefined behaviour. See https://github.com/rust-embedded/volatile-register/issues/10 for more background on this.
  2. We distinguish between MMIO reads which have side-effects (e.g. clearing an interrupt status, or popping from a queue) and those which don't (e.g. just reading some status). A read which has side-effects should be treated like a write and only be allowed from a unique pointer (passed via &mut) whereas a read without side-effects can safely be done via a shared pointer (passed via '&'), e.g. simultaneously from multiple threads.
  3. On most platforms MMIO reads and writes can be done via read_volatile and write_volatile, but on aarch64 this may generate instructions which can't be virtualised. This is arguably a bug in rustc, but in the meantime we work around this by using inline assembly to generate the correct instructions for MMIO reads and writes on aarch64.
Crate nameLast releaseVersionAvoids referencesDistinguishes reads with side-effectsWorks around aarch64 volatile bugModelField projectionNotes
safe-mmioJuly 20260.3.1✅✅✅struct with field wrappersmacro
derive-mmioJuly 20260.8.0✅✅❌struct with derive macrothrough derive macro
volatileJune 20240.6.1✅❌❌struct with derive macromacro or generated methods
volatile-registerOctober 20230.2.2❌❌❌struct with field wrappersmanual (references)
tock-registersOctober 20250.10.1❌❌❌macros to define fields and structsmanual (references)Also covers CPU registers, and bitfields
mmioMay 20212.1.0✅❌❌only deals with individual fields❌
rumioMarch 20210.2.0✅❌❌macros to define fields and structsgenerated methodsAlso covers CPU registers, and bitfields
vcellJanuary 20210.1.3❌❌❌plain structmanual (references)
registerJanuary 20211.0.2❌❌❌macros to define fields and structsmanual (references)Deprecated in favour of tock-registers. Also covers CPU registers, and bitfields.
voladdressMay 20241.4.0✅❌❌array with stride, no typed fields❌

License

Licensed under either of

at your option.

Contributing

If you want to contribute to the project, see details of how we accept contributions.

mmio
no-std
rust
rust-crate

Project health

crates.io

Latest release

0.3.2 · Sep 28, 2026

Releases this year

5

Significant stargazers

Victor Costan

1,328 followers · starred Mar 2026

Nugine

377 followers · starred Sep 2025

Luo Jia / Zhouqi Jiang

1,042 followers · starred Sep 2025

Berkus Decker

285 followers · starred Jan 2026