Deterministic Memory
Deterministic Memory with Heapless Collections
Safety-critical and real-time systems often forbid the heap entirely. The global allocator is non-deterministic—allocation time varies, and long-running firmware risks fragmentation and silent OOM failures.
heapless provides Vec, String, Deque, and map types that store data
on the stack (or in a static) with a capacity fixed at compile time.
Pushing beyond capacity returns Err instead of panicking or allocating,
letting the caller decide how to handle it.
std type | heapless equivalent | Guarantee |
|---|---|---|
Vec<T> | heapless::Vec<T, N> | At most N elements, no allocator |
String | heapless::String<N> | At most N bytes, no allocator |
VecDeque<T> | heapless::Deque<T, N> | Fixed-capacity ring buffer |
HashMap<K,V> | heapless::IndexMap<K,V,S,N> | Fixed-capacity hash map |
The example below builds a stack-allocated event log and demonstrates capacity enforcement—the kind of predictable, constant-memory behavior required by embedded and real-time systems.
use heapless::{String, Vec}; use thiserror::Error; #[derive(Debug, Error)] enum LogError { #[error("log full (capacity exceeded)")] Full, #[error("log is empty")] Empty, #[error(transparent)] Fmt(#[from] core::fmt::Error), } /// A fixed-capacity event log that never touches the heap. /// /// In real-time and safety-critical systems the global allocator is /// forbidden because allocation time is unbounded and fragmentation /// can cause silent OOM failures in long-running firmware. /// /// [`heapless::Vec`] stores up to `N` elements on the stack (or in a /// `static`). The capacity is fixed at compile time, so the memory /// footprint is constant and predictable. struct EventLog<const N: usize> { entries: Vec<Event, N>, } #[derive(Debug, Clone)] struct Event { timestamp_ms: u32, code: u16, } impl<const N: usize> EventLog<N> { /// Creates an empty log. fn new() -> Self { Self { entries: Vec::new(), } } /// Records an event. Returns `Err` if the log is full instead /// of panicking or allocating—the caller decides what to do. fn record(&mut self, timestamp_ms: u32, code: u16) -> Result<(), LogError> { let event = Event { timestamp_ms, code }; self.entries.push(event).map_err(|_| LogError::Full) } /// Returns how many events have been recorded. fn len(&self) -> usize { self.entries.len() } /// Returns the most recent event, if any. fn latest(&self) -> Result<&Event, LogError> { self.entries.last().ok_or(LogError::Empty) } } /// Formats a sensor label without heap allocation. /// /// [`heapless::String<N>`] works like `std::string::String` but /// stores up to `N` bytes on the stack. `write!` returns `Err` if /// the formatted text would exceed capacity. fn format_label(sensor_id: u16, value: f32) -> Result<String<32>, LogError> { use core::fmt::Write; let mut buf: String<32> = String::new(); write!(buf, "S{sensor_id}={value:.1}")?; Ok(buf) } fn main() -> Result<(), LogError> { // A log that holds at most 8 events — zero heap allocation. let mut log: EventLog<8> = EventLog::new(); log.record(100, 0x01)?; log.record(200, 0x02)?; log.record(300, 0xFF)?; println!("logged {} events", log.len()); let latest = log.latest()?; println!( "latest: timestamp={}ms code=0x{:02X}", latest.timestamp_ms, latest.code ); // Stack-allocated string formatting. let label = format_label(42, 3.14)?; println!("label: {label}"); // Demonstrate capacity enforcement — the 9th push returns Err. let mut full_log: EventLog<2> = EventLog::new(); full_log.record(0, 1)?; full_log.record(1, 2)?; assert!(full_log.record(2, 3).is_err()); println!("overflow correctly rejected"); Ok(()) }