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(())
}