Deterministic Memory

Deterministic Memory with Heapless Collections

heapless-badge cat-no-std-badge cat-data-structures-badge

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 typeheapless equivalentGuarantee
Vec<T>heapless::Vec<T, N>At most N elements, no allocator
Stringheapless::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");
}