Rust 105 🦀 Custom Memory Allocators (#[global_allocator])
Every Box, Vec, and String in a Rust program eventually asks the global allocator for memory. By default, that request goes to the system allocator—malloc on Unix, HeapAlloc on Windows—or to jemalloc on some targets. The #[global_allocator] attribute is the mechanism that replaces that default with a custom implementation, routing every heap allocation in the program through code the programmer controls .
This chapter covers the GlobalAlloc trait and the attribute that registers an implementation. It begins with the two required methods—alloc and dealloc—and the safety contract that makes the trait unsafe. It then examines the three common use cases: tracking and accounting, arena and bump allocation for predictable workloads, and the no_std environment where no system allocator exists. The final sections address the constraints that make a global allocator different from an ordinary abstraction—re-entrance, panic safety, and the single-instance rule.
Key point: A global allocator is called from everywhere, including the panic handler and the standard library’s own internals. It must not panic, must not allocate, and must be correct under concurrency. The compiler cannot verify any of this.
Why custom allocators exist
The accounting problem. A server that wants to know how much memory each request consumes cannot ask the system allocator. malloc tracks totals, not call sites. A custom allocator that wraps the system allocator and increments atomic counters on every allocation gives per-subsystem visibility without changing the allocation strategy . This is the same technique memory profilers use, implemented at zero external overhead.
The arena problem. Some workloads allocate many small objects and free them all at once—a request handler, a compiler pass, a frame in a game loop. The system allocator’s free list is designed for arbitrary lifetimes, and it pays for that generality with fragmentation and per-allocation overhead. A bump allocator that never frees individual objects and resets its offset at the end of the arena is dramatically faster for this pattern .
The no_std problem. A bare-metal program has no operating system and therefore no malloc. The alloc crate provides Vec and Box, but it requires a #[global_allocator] to be defined. Without one, the linker fails. The custom allocator is not an optimization in this context; it is the only way to have heap-allocated types at all .
The control problem. The default allocator is unspecified and may change between platforms or compiler versions . A program that needs deterministic allocation behavior—a real-time system, an embedded device with a fixed memory budget, a security-sensitive application that wants to zero memory on free—must take control of the allocation path.
a. The GlobalAlloc trait and its safety contract
The trait has two required methods and two provided ones. alloc receives a Layout and returns a *mut u8. dealloc receives a pointer and the same Layout that was used to allocate it. The provided methods—alloc_zeroed and realloc—have default implementations that call alloc and dealloc, and can be overridden for efficiency .
use std::alloc::{GlobalAlloc, Layout, System};
// A wrapper that delegates to the system allocator.
// This is the minimal correct implementation.
struct MyAllocator;
unsafe impl GlobalAlloc for MyAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Delegate to the system allocator.
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
// The layout must be the same one used to allocate.
System.dealloc(ptr, layout);
}
}
#[global_allocator]
static GLOBAL: MyAllocator = MyAllocator;
The safety contract has three parts. First, alloc must return a pointer to a block of at least layout.size() bytes, aligned to layout.align(), or return null if the allocation fails . Second, dealloc must receive a pointer that was returned by a previous alloc call and the exact same Layout that was used for that call . Third, and most critically, the allocator must not unwind. A panic from any of these functions is undefined behavior .
The Layout type encodes both size and alignment. The allocator cannot assume anything about the alignment beyond what the layout specifies, and the caller cannot assume anything about the memory beyond what the layout requests. The contract is symmetric and strict.
b. Tracking and accounting allocators
The simplest useful custom allocator wraps the system allocator and records statistics. The pattern is a struct holding the inner allocator, with AtomicUsize counters for live bytes and allocation count. The alloc method increments the counters after a successful allocation; the dealloc method decrements them before or after delegating .
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
static LIVE_BYTES: AtomicUsize = AtomicUsize::new(0);
static ALLOC_COUNT: AtomicUsize = AtomicUsize::new(0);
struct TrackingAllocator {
inner: System,
}
unsafe impl GlobalAlloc for TrackingAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = self.inner.alloc(layout);
if !ptr.is_null() {
// Relaxed is sufficient: the counters are independent state.
LIVE_BYTES.fetch_add(layout.size(), Ordering::Relaxed);
ALLOC_COUNT.fetch_add(1, Ordering::Relaxed);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
LIVE_BYTES.fetch_sub(layout.size(), Ordering::Relaxed);
self.inner.dealloc(ptr, layout);
}
}
#[global_allocator]
static ALLOCATOR: TrackingAllocator = TrackingAllocator { inner: System };
The counters must be atomics because the allocator is called from multiple threads concurrently. Relaxed ordering is sufficient because the counters do not synchronize any other data; they are independent statistics . The dealloc path does not check for null because the contract guarantees that a pointer passed to dealloc is valid.
A more sophisticated version can track per-subsystem statistics by using thread-local counters or by inspecting the call stack, but the atomic-counter approach is the foundation and is sufficient for most memory-budgeting needs.
c. Bump allocators and the no_std case
A bump allocator manages a fixed region of memory and allocates by advancing a pointer. It never frees individual allocations; the entire arena is reset at once. This is the standard choice for no_std environments where the memory region is a static array .
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
use core::ptr::null_mut;
use core::sync::atomic::{AtomicUsize, Ordering};
const ARENA_SIZE: usize = 64 * 1024;
struct BumpAllocator {
arena: UnsafeCell<[u8; ARENA_SIZE]>,
offset: AtomicUsize,
}
unsafe impl Sync for BumpAllocator {}
unsafe impl GlobalAlloc for BumpAllocator {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let mut current = self.offset.load(Ordering::Relaxed);
loop {
// Align the current offset up to the requested alignment.
let aligned = (current + layout.align() - 1) & !(layout.align() - 1);
let next = aligned + layout.size();
if next > ARENA_SIZE {
return null_mut(); // out of memory
}
// Try to claim the range with a CAS.
match self.offset.compare_exchange_weak(
current, next, Ordering::SeqCst, Ordering::Relaxed,
) {
Ok(_) => {
let base = self.arena.get() as *mut u8;
return base.add(aligned);
}
Err(actual) => current = actual,
}
}
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Bump allocators do not free individual allocations.
// The entire arena is reset when the workload completes.
}
}
#[global_allocator]
static ALLOC: BumpAllocator = BumpAllocator {
arena: UnsafeCell::new([0; ARENA_SIZE]),
offset: AtomicUsize::new(0),
};
The UnsafeCell is necessary because the arena field is mutated through a shared reference, and the Sync implementation is necessary because the allocator is shared across threads. The CAS loop ensures that concurrent allocations do not claim overlapping ranges .
The bump allocator’s dealloc is a no-op. This is correct for the arena pattern: the arena is reset by storing zero to the offset after the workload completes, not by freeing individual objects. The cost is that memory is not reused until the reset, which is acceptable when the workload’s lifetime matches the arena’s lifetime.
Complete Example Session
// ============================================
// PART 1: THE SYSTEM ALLOCATOR WRAPPER
// ============================================
// The minimal custom allocator: delegate to System.
use std::alloc::{GlobalAlloc, Layout, System};
struct Delegate;
unsafe impl GlobalAlloc for Delegate {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
System.alloc(layout)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
System.dealloc(ptr, layout)
}
}
// ============================================
// PART 2: THE TRACKING COUNTERS
// ============================================
// Atomics for live bytes and allocation count.
use std::sync::atomic::{AtomicUsize, Ordering};
static LIVE: AtomicUsize = AtomicUsize::new(0);
static COUNT: AtomicUsize = AtomicUsize::new(0);
// ============================================
// PART 3: THE TRACKING ALLOCATOR
// ============================================
// Wrap System and record statistics.
struct Tracking;
unsafe impl GlobalAlloc for Tracking {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = System.alloc(layout);
if !ptr.is_null() {
LIVE.fetch_add(layout.size(), Ordering::Relaxed);
COUNT.fetch_add(1, Ordering::Relaxed);
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
LIVE.fetch_sub(layout.size(), Ordering::Relaxed);
System.dealloc(ptr, layout);
}
}
// ============================================
// PART 4: THE GLOBAL REGISTRATION
// ============================================
// Only one global allocator per program.
#[global_allocator]
static GLOBAL: Tracking = Tracking;
// ============================================
// PART 5: THE STATISTICS QUERY
// ============================================
// Expose the counters to the application.
fn stats() -> (usize, usize) {
(LIVE.load(Ordering::Relaxed), COUNT.load(Ordering::Relaxed))
}
// ============================================
// PART 6: THE PANIC HAZARD
// ============================================
// The allocator must not panic.
// This dealloc is correct because it does not unwind.
// A panic here would be undefined behavior.
// Avoid format!, unwrap, and any allocating call inside alloc/dealloc.
// ============================================
// PART 7: THE RE-ENTRANCE HAZARD
// ============================================
// The allocator may be called from within the allocator
// if it uses a type that allocates (e.g., a Mutex that
// allocates on first use). Keep the allocator dependency-free.
// ============================================
// PART 8: THE ALIGNMENT HANDLING
// ============================================
// Layout provides size and alignment.
// The allocator must honor both.
unsafe fn assert_layout(layout: Layout) {
assert!(layout.align().is_power_of_two());
}
// ============================================
// PART 9: THE OVERRIDDEN REALLOC
// ============================================
// The default realloc calls alloc and dealloc.
// Override it for a more efficient implementation.
unsafe impl Tracking {
unsafe fn realloc(&self, ptr: *mut u8, layout: Layout, new_size: usize) -> *mut u8 {
let new_layout = Layout::from_size_align_unchecked(new_size, layout.align());
let new_ptr = self.alloc(new_layout);
if !new_ptr.is_null() {
std::ptr::copy_nonoverlapping(ptr, new_ptr, layout.size().min(new_size));
self.dealloc(ptr, layout);
}
new_ptr
}
}
// ============================================
// PART 10: THE FULL PICTURE
// ============================================
// A global allocator is called from everywhere.
// Correctness is the programmer's responsibility.
fn main() {
let v: Vec<u32> = vec![1, 2, 3];
let (live, count) = stats();
assert!(live > 0);
assert!(count > 0);
drop(v);
}
The ten parts build a tracking allocator from the delegation wrapper to the statistics query, with the hazards—panic, re-entrance, alignment—addressed as constraints rather than afterthoughts. The key observation is that the allocator’s correctness depends on invariants that the compiler cannot check: no unwinding, no recursion into the allocator, and exact layout matching.
Quick Reference
Trait Methods
| Method | Required | Purpose |
|---|---|---|
alloc | Yes | Allocate layout.size() bytes |
dealloc | Yes | Free a block allocated by alloc |
alloc_zeroed | No | Allocate and zero |
realloc | No | Resize an existing allocation |
Safety Rules
| Rule | Consequence of Violation |
|---|---|
| No unwinding | Undefined behavior |
| Return aligned pointer or null | Undefined behavior |
dealloc receives same layout | Undefined behavior |
| No double free | Undefined behavior |
| No allocation inside allocator | Deadlock or recursion |
Use Cases
| Pattern | Strategy | Fits |
|---|---|---|
| Accounting | Wrap System, atomic counters | Profiling, budgeting |
| Arena | Bump pointer, no individual free | Request handlers, frames |
no_std | Bump over static array | Bare metal, embedded |
| Pool | Fixed-size free list | Uniform object sizes |
Constraints
| Constraint | Reason |
|---|---|
One #[global_allocator] per program | Linker symbol collision |
Must be Sync | Shared across threads |
| Must not panic | Called from panic handler |
| Must not allocate | Re-entrance |
Best Practices
✅ Do This:
// Delegate to System for the baseline.
System.alloc(layout) // ✅
// Use atomics for shared counters.
LIVE.fetch_add(layout.size(), Ordering::Relaxed); // ✅
// Check for null before recording success.
if !ptr.is_null() { ... } // ✅
// Keep the allocator dependency-free.
// No Mutex, no Vec, no format!. // ✅
// Override realloc for efficiency when possible.
unsafe fn realloc(...) { ... } // ✅
❌ Don’t Do This:
// Don't panic inside alloc or dealloc.
panic!("out of memory"); // undefined behavior // ❌
// Don't allocate inside the allocator.
let v = Vec::new(); // re-entrance, possible deadlock // ❌
// Don't return unaligned memory.
ptr // must satisfy layout.align() // ❌
// Don't ignore the layout in dealloc.
self.inner.dealloc(ptr, Layout::new::<u8>()); // wrong layout // ❌
// Don't register more than one global allocator.
#[global_allocator] static A: A1 = ...; // ❌
#[global_allocator] static B: A2 = ...;
Common Pitfalls
| Pitfall | Why It Happens | Fix |
|---|---|---|
| Linker error: duplicate allocator | Two #[global_allocator] in dependency tree | Remove one; only one per program |
| Deadlock or stack overflow | Allocator uses a type that allocates | Use only primitives and atomics |
| Undefined behavior | Panic inside allocator | Never unwind; use abort if necessary |
| Misaligned pointer | Alignment not honored | Align the offset or delegate to System |
| Layout mismatch | dealloc called with different layout | Store or recompute the exact layout |
Real-World Examples
1. System Delegation
unsafe impl GlobalAlloc for A {
unsafe fn alloc(&self, l: Layout) -> *mut u8 { System.alloc(l) }
unsafe fn dealloc(&self, p: *mut u8, l: Layout) { System.dealloc(p, l) }
}
2. Tracking Counter
LIVE.fetch_add(layout.size(), Ordering::Relaxed);
3. Bump Allocation
let aligned = (current + align - 1) & !(align - 1);
4. Global Registration
#[global_allocator]
static GLOBAL: MyAlloc = MyAlloc;
5. No-op Dealloc
unsafe fn dealloc(&self, _: *mut u8, _: Layout) {}
6. Realloc Override
unsafe fn realloc(&self, p: *mut u8, l: Layout, n: usize) -> *mut u8 { ... }
7. Stats Query
LIVE.load(Ordering::Relaxed)
8. Null Check
if !ptr.is_null() { ... }
9. Arena Reset
offset.store(0, Ordering::SeqCst);
10. Static Arena
arena: UnsafeCell::new([0; N])
Visual
The Global Allocator Position
┌─────────────────────────────────────────────────────────┐
│ APPLICATION │
│ Vec::new(), Box::new(), String::from() │
│ │ │
│ ▼ │
│ ┌───────────────────────────────────────────────────┐ │
│ │ GLOBAL ALLOCATOR │ │
│ │ #[global_allocator] static │ │
│ │ alloc(), dealloc(), realloc() │ │
│ └───────────────────────────────────────────────────┘ │
│ │ │
│ ┌────┴────┬──────────────┐ │
│ ▼ ▼ ▼ │
│ System Tracking Bump │
│ (malloc) (System+stats) (static arena) │
└─────────────────────────────────────────────────────────┘
The Safety Contract
┌─────────────────────────────────────────────────────────┐
│ alloc(layout) → *mut u8 │
│ │
│ MUST: return ptr aligned to layout.align() │
│ MUST: return block of at least layout.size() │
│ MAY: return null on failure │
│ MUST NOT: panic, unwind, allocate │
│ │
│ dealloc(ptr, layout) │
│ │
│ MUST: ptr was returned by alloc with same layout │
│ MUST NOT: panic, unwind, double-free │
└─────────────────────────────────────────────────────────┘
The Bump Allocator
┌─────────────────────────────────────────────────────────┐
│ arena: [0; 64K] │
│ │
│ offset: 0 │
│ ┌───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ │
│ │ │ │ │ │ │ │ │ │ │ │ │ │ │
│ └───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┴───┘ │
│ ▲ │
│ offset │
│ │
│ alloc(8, align 4): offset 0→8, return &arena[0] │
│ alloc(16, align 8): offset 8→24, return &arena[8] │
│ alloc(4, align 4): offset 24→28, return &arena[24] │
│ │
│ dealloc: no-op │
│ reset: offset = 0 │
└─────────────────────────────────────────────────────────┘
The Re-entrance Hazard
┌─────────────────────────────────────────────────────────┐
│ alloc() called │
│ │ │
│ ▼ │
│ Allocator uses a Mutex │
│ Mutex allocates on first lock │
│ │ │
│ ▼ │
│ alloc() called again │
│ │ │
│ ▼ │
│ DEADLOCK or infinite recursion │
│ │
│ Fix: allocator must use only atomics and raw memory. │
└─────────────────────────────────────────────────────────┘
Summary
| Item | Value |
|---|---|
| Attribute | #[global_allocator] |
| Trait | GlobalAlloc (unsafe) |
| Required methods | alloc, dealloc |
| Provided methods | alloc_zeroed, realloc |
| Safety | No unwinding, correct layout, aligned pointer |
| Registration | One static per program |
| Common use | Tracking, arena, no_std |
| Constraint | Must be Sync, no allocation, no panic |
| Default | System or jemalloc depending on target |
Key takeaways:
- The global allocator is the lowest layer of Rust’s memory model. Every
Box,Vec, andStringgoes through it. Replacing it intercepts every heap allocation . GlobalAllocis an unsafe trait for a reason. The compiler cannot verify that the returned pointer is aligned, that the layout matches on deallocation, or that the allocator does not unwind. Those are the programmer’s responsibilities .- The allocator must not panic. A panic from
allocordeallocis undefined behavior, because the allocator may be called from the panic handler itself . - The allocator must not allocate. Using a
Mutex, aVec, or any type that allocates inside the allocator creates a re-entrance hazard that can deadlock or recurse infinitely. - Tracking allocators wrap the system allocator. Atomic counters for live bytes and allocation count give per-program memory visibility without changing the allocation strategy .
- Bump allocators trade deallocation for speed. They never free individual objects; the arena is reset as a whole. This is the right choice for request-scoped or frame-scoped workloads .
- Only one
#[global_allocator]is allowed per program. The attribute creates a linker symbol, and duplicate definitions fail at link time . - The
no_stdcase is the allocator’s original purpose. Without a system allocator, thealloccrate needs a#[global_allocator]to provideVecandBoxat all .
Remember: A custom global allocator is a program-wide decision. It affects every heap allocation, including the ones inside the standard library and the panic handler. The trait is small—two methods—but the contract is strict: correct alignment, exact layout matching, no unwinding, no recursion. The three common patterns—tracking, arena, and no_std bump—each solve a different problem, and each is correct only when its constraints match the program’s allocation behavior. Write the allocator, register it with the attribute, and test it under concurrency. The compiler will not catch the mistakes; the program’s memory safety depends on the programmer getting the invariants right.
Stop using slow, ad-bloated tool sites! 🤮
🔎 Search “KandZ Tools” on Google to use many professional utilities for free.
KandZ.me is the ultimate minimalist hub for:
✅ Finance (Mortgage, Interest, Inflation)
✅ Tech (Base64, JSON, Dev Suite, IP)
✅ Health (BMI, BMR, TDEE)
✅ Productivity (Timer, Workspace, QR)
⚡️ Fast & Private
🔒 No data leaves your device
💎 100% Free
🔗 Use it now: https://tools.kandz.me
🔖 Bookmark it—you’ll need it later!