When you need the heap but not the OS
You are writing a microkernel. You need a dynamic list of process IDs. Vec<u32> is exactly what you want. You type use std::vec::Vec and hit compile. The compiler rejects you. std requires an operating system to exist. Your code is the operating system. You are stuck. You need the heap, but you cannot use the standard library.
The alloc crate bridges that gap. It provides heap-allocated types like Vec, Box, String, and Rc without pulling in OS-specific code like file I/O or networking. You get the dynamic data structures you need while keeping your code compatible with bare metal, kernels, and embedded targets.
The library hierarchy
Rust's standard library is layered into three crates. Understanding this hierarchy explains why alloc works in no_std environments.
core is the foundation. It contains types that require no external support: Option, Result, Iterator, str, and primitive traits. core is always available, even in no_std.
alloc sits on top of core. It adds heap allocation. It defines Vec, Box, String, Rc, and the GlobalAlloc trait. alloc does not implement an allocator. It defines the interface and provides types that use it.
std sits on top of alloc. It adds OS integration: std::fs, std::io, std::net, and a default global allocator that talks to the operating system. std depends on alloc, so using std automatically pulls in alloc.
When you write #![no_std], you remove std from the build. You keep core. You can opt-in to alloc manually. This gives you the middle layer: heap types without OS types.
Think of it like a construction site. core is the foundation and basic hand tools. alloc is the crane. std is the finished building with plumbing and electricity. You can use a crane on a site without plumbing, but you must bring your own fuel for the crane. alloc brings the crane. You must provide the fuel.
Minimal setup
To use alloc in a no_std crate, you need three things. You need the #![no_std] attribute. You need to link the alloc crate explicitly. You need to use types from alloc instead of std.
#![no_std]
// Disable the standard library.
// This removes std::io, std::fs, std::net, and the default allocator.
// core is still available.
#![no_main]
// Remove the default entry point and runtime setup.
// std provides a main function that calls libc.
// In no_std, you define your own entry point.
extern crate alloc;
// Explicitly link the alloc crate.
// std depends on alloc, so std pulls this in automatically.
// In no_std, alloc is not linked by default.
// This line ensures the crate is available to the linker.
use alloc::vec::Vec;
// Import Vec from alloc, not std.
// Vec lives in alloc::vec.
// std::vec::Vec is just a re-export of alloc::vec::Vec.
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
// Provide a panic handler.
// std provides a default that prints to stderr.
// In no_std, you must define one.
loop {}
}
#[no_mangle]
pub extern "C" fn _start() -> ! {
// Define the entry point.
// _start is the convention for no_std binaries.
// It must not return.
let mut v: Vec<u8> = Vec::new();
v.push(42);
// Vec works. The type is available.
// However, this code will fail to link if no allocator is provided.
// See the next section.
loop {}
}
The code above defines the structure. It compiles the types. It fails at the linking stage. Vec::new calls into the allocation system. alloc defines the system but does not provide an implementation. You must wire one up.
Wiring the allocator
alloc requires a global allocator. It defines the GlobalAlloc trait. Any type can implement this trait to provide heap memory. You register your implementation with the #[global_allocator] attribute.
#![no_std]
#![no_main]
extern crate alloc;
use alloc::vec::Vec;
use core::alloc::GlobalAlloc;
use core::ptr::NonNull;
// A minimal allocator for demonstration.
// In production code, use a crate like wee_alloc or bumpalo.
// This implementation is unsafe and incomplete.
struct MyAllocator;
unsafe impl GlobalAlloc for MyAllocator {
// SAFETY: This implementation is a placeholder.
// A real allocator must:
// 1. Return a pointer to memory aligned to layout.align().
// 2. Return memory of at least layout.size() bytes.
// 3. Never return a pointer that was already allocated.
// 4. Handle Layout::size() == 0 correctly.
// 5. Be thread-safe if used in a multi-threaded context.
// This dummy implementation violates all of these.
unsafe fn alloc(&self, layout: core::alloc::Layout) -> *mut u8 {
// Return a pointer to heap memory.
// Real allocators manage memory pools or call OS syscalls.
core::ptr::null_mut()
}
// SAFETY: Dealloc must match the alloc call.
// ptr must be a pointer returned by alloc with the same layout.
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: core::alloc::Layout) {
// Free memory.
}
}
#[global_allocator]
static ALLOC: MyAllocator = MyAllocator;
// Register the allocator as the global default.
// The static must be const-compatible or initialized at compile time.
// alloc::vec::Vec will use this to grow its buffer.
#[panic_handler]
fn panic(_info: &core::panic::PanicInfo) -> ! {
loop {}
}
#[no_mangle]
pub extern "C" fn _start() -> ! {
let mut v: Vec<u8> = Vec::new();
v.push(1);
// Vec now links successfully.
// It calls MyAllocator::alloc to get memory.
loop {}
}
The #[global_allocator] attribute marks a static variable as the default allocator. The compiler generates calls to this allocator whenever alloc types need memory. The implementation must be unsafe because the compiler cannot verify memory safety guarantees. You are promising that alloc returns valid memory and dealloc frees it correctly. If you lie, you get undefined behavior.
Convention aside: extern crate alloc; is the community standard in no_std crates. Even if you only use use alloc::vec::Vec, the explicit extern crate ensures the linker includes the crate. Some toolchains may drop unused crates, and alloc has no types in the prelude, so it can be missed without the explicit declaration.
Dependencies and the alloc feature
Real code uses dependencies. Dependencies often depend on std. If you add a crate that pulls in std, your no_std build breaks. You must configure dependencies carefully.
[dependencies]
# Example: using serde without std
serde = { version = "1.0", default-features = false, features = ["alloc"] }
# default-features = false prevents pulling in std.
# features = ["alloc"] enables alloc support if the crate provides it.
# Example: a crate that supports no_std
my-kernel-lib = { version = "0.1", default-features = false }
Many crates support no_std via feature flags. They disable std by default or provide an alloc feature. You must check the crate's documentation. Setting default-features = false is essential. It stops the dependency from pulling in std. Adding features = ["alloc"] enables heap types if the crate supports them.
If a crate does not support no_std, you cannot use it. You must find an alternative or fork it. The ecosystem is maturing, but not every crate works in no_std.
Convention aside: When publishing a no_std crate, provide an alloc feature. Users expect to be able to enable heap support without pulling in std. Document which types require alloc. This makes your crate usable in more environments.
Pitfalls
Missing the allocator is the most common error. If you use alloc types but forget #[global_allocator], the linker fails with "no global memory allocator found". The types exist, but they cannot allocate. Add the allocator registration.
Forgetting extern crate alloc causes E0432 (unresolved import). The compiler cannot find alloc::vec::Vec because the crate is not linked. Add the extern crate line.
Dependencies pulling in std is a silent trap. You add a crate and suddenly std is back. You get errors about std types or your binary size explodes. Check your Cargo.toml. Set default-features = false on all dependencies. Verify with cargo tree that std is not in the dependency graph.
Using String without alloc fails. String lives in alloc. If you want string data without heap allocation, use &str or &[u8]. These live in core. They are borrowed slices and do not require allocation.
Trust the borrow checker here. It will catch many misuse patterns, but it cannot catch allocator bugs. If your allocator returns bad memory, the borrow checker cannot save you.
Decision matrix
Use std when you are writing an application on a standard OS and want file I/O, networking, and a default allocator out of the box.
Use no_std with alloc when you are building a kernel, a hypervisor, or an embedded application that needs dynamic data structures like Vec or String but cannot rely on OS services.
Use no_std without alloc when you are targeting bare-metal microcontrollers with strict memory constraints, or when you need to guarantee zero heap allocation for safety and predictability.
Use a third-party allocator crate like wee_alloc or bumpalo when the default allocator is unavailable or when you need a specialized allocation strategy for your target.
Pick the allocator that matches your hardware. There is no free lunch in memory management.