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}; /// 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<(), Event> { let event = Event { timestamp_ms, code }; self.entries.push(event).map_err(|e| e) } /// Returns how many events have been recorded. fn len(&self) -> usize { self.entries.len() } /// Returns the most recent event, if any. fn latest(&self) -> Option<&Event> { self.entries.last() } } /// 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>, core::fmt::Error> { use core::fmt::Write; let mut buf: String<32> = String::new(); write!(buf, "S{sensor_id}={value:.1}")?; Ok(buf) } fn main() { // A log that holds at most 8 events — zero heap allocation. let mut log: EventLog<8> = EventLog::new(); log.record(100, 0x01).expect("log not full"); log.record(200, 0x02).expect("log not full"); log.record(300, 0xFF).expect("log not full"); println!("logged {} events", log.len()); println!("latest: {:?}", log.latest().unwrap()); // Stack-allocated string formatting. let label = format_label(42, 3.14).expect("fits in 32 bytes"); println!("label: {label}"); // Demonstrate capacity enforcement — the 9th push returns Err. let mut full_log: EventLog<2> = EventLog::new(); full_log.record(0, 1).unwrap(); full_log.record(1, 2).unwrap(); let overflow = full_log.record(2, 3); assert!(overflow.is_err()); println!("overflow correctly rejected"); }