Low-Level Systems Programming

The core and alloc Libraries

Rust's standard library is architected in three concentric layers:

  1. core: The dependency-free foundation providing language primitives, slices, iterators, formatting traits, futures, and atomics. Requires no OS and no heap allocator.
  2. alloc: The heap-allocation layer (extern crate alloc;) providing smart pointers (Box, Rc, Arc) and dynamically sized collections (Vec, String, VecDeque, BTreeMap, BTreeSet, BinaryHeap, LinkedList) on top of any #![no_std] target that defines a #[global_allocator].
  3. std: The hosted OS layer that re-exports core and alloc while adding OS threads, filesystems, networking, environment variables, and HashMap (which sits in std because its default RandomState seeds SipHash from OS entropy!).

Mapping Modules Across core, alloc, and std

Module / Type Available in core? Available in alloc? Available Only in std? Notes for #![no_std] Code
option, result, iter, slice, mem, ptr, ops, cmp Yes (core::*) Re-exported Re-exported Always available everywhere.
fmt::Display, fmt::Debug, fmt::Write Yes (core::fmt) Re-exported Re-exported Use core::write! into a stack buffer or UART without heap allocation!
error::Error, ffi::CStr, net::IpAddr, net::SocketAddr Yes (core::error, core::ffi, core::net) Re-exported Re-exported core::net (Rust 1.77+) and core::error::Error (Rust 1.81+) work in core!
Box, Rc, Arc, Cow, Vec, String, BTreeMap No Yes (alloc::*) Re-exported Requires extern crate alloc; and a registered #[global_allocator].
format! macro No Yes (alloc::format!) Re-exported Allocates a String on the heap.
HashMap, HashSet No No Yes (std::collections) In no_std + alloc, use alloc::collections::BTreeMap or the hashbrown crate directly.
fs, net::TcpStream, thread, process, env, io::Read/Write No No Yes (std::*) Require operating system kernel services.

Formatting Without Heap Allocation Using core::fmt::Write

Even in a pure #![no_std] environment without alloc, you can use Rust's full write! formatting machinery ({:08x}, {:?}, {:.2}) by implementing core::fmt::Write on a fixed-size stack buffer or hardware serial port:

use core::fmt::{self, Write};
use core::str;

/// A fixed-capacity stack string buffer that implements `core::fmt::Write` with zero heap allocation!
pub struct StackString<const CAP: usize> {
    buf: [u8; CAP],
    len: usize,
}

impl<const CAP: usize> StackString<CAP> {
    pub const fn new() -> Self {
        Self {
            buf: [0u8; CAP],
            len: 0,
        }
    }

    pub fn as_str(&self) -> &str {
        // SAFETY: `write_str` only appends valid UTF-8 `&str` slices into `self.buf[..self.len]`.
        unsafe { str::from_utf8_unchecked(&self.buf[..self.len]) }
    }
}

impl<const CAP: usize> Write for StackString<CAP> {
    fn write_str(&mut self, s: &str) -> fmt::Result {
        let bytes = s.as_bytes();
        let end = self.len.checked_add(bytes.len()).ok_or(fmt::Error)?;
        if end > CAP {
            return Err(fmt::Error); // Capacity exceeded
        }
        self.buf[self.len..end].copy_from_slice(bytes);
        self.len = end;
        Ok(())
    }
}

fn main() {
    let mut line = StackString::<64>::new();
    write!(&mut line, "sensor={:#04x}, temp={}C", 0x2a, 37).unwrap();

    assert_eq!(line.as_str(), "sensor=0x2a, temp=37C");
    println!("Formatted via core::fmt::Write on stack: {}", line.as_str());
}
Formatted via core::fmt::Write on stack: sensor=0x2a, temp=37C

Enabling alloc and Custom Global Allocators (#[global_allocator])

To use alloc (Vec, Box, String, BTreeMap) inside a #![no_std] crate:

  1. Declare extern crate alloc; at the top of src/lib.rs or src/main.rs.
  2. In the final binary crate, register a type implementing core::alloc::GlobalAlloc with the #[global_allocator] attribute (such as embedded-alloc::LlffHeap on Cortex-M or a bump/arena allocator):
extern crate alloc;

use alloc::collections::BTreeMap;
use alloc::vec::Vec;
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};

/// A wrapper around the system allocator that tracks total bytes allocated.
pub struct CountingAllocator;

static ALLOCATED_BYTES: AtomicUsize = AtomicUsize::new(0);

unsafe impl GlobalAlloc for CountingAllocator {
    unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
        ALLOCATED_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
        unsafe { System.alloc(layout) }
    }

    unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
        ALLOCATED_BYTES.fetch_sub(layout.size(), Ordering::Relaxed);
        unsafe { System.dealloc(ptr, layout) }
    }
}

#[global_allocator]
static GLOBAL: CountingAllocator = CountingAllocator;

fn main() {
    let before = ALLOCATED_BYTES.load(Ordering::Relaxed);
    let mut map = BTreeMap::new();
    map.insert("node_1", Vec::from([10u64, 20, 30]));
    let during = ALLOCATED_BYTES.load(Ordering::Relaxed);

    assert!(during > before);
    println!("Custom #[global_allocator] tracked {} active heap bytes", during - before);
}
Custom #[global_allocator] tracked 312 active heap bytes