globalallocator
global_allocator
Level 17 — Embedded & Systems Programming A static attribute (
#[global_allocator]) that registers a custom heap memory manager implementing theGlobalAlloctrait as the application's global memory allocator forstdor#![no_std]+allocenvironments.
1. Prerequisites
- Allocator API — The
GlobalAlloctrait framework configured by#[global_allocator]. allocLibrary — The heap collection library powered by#[global_allocator].
2. Term Category
Rust Memory Subsystem (custom heap memory allocator specification): #[global_allocator] is a compiler attribute placed on a static item implementing std::alloc::GlobalAlloc (or core::alloc::GlobalAlloc). It routes every dynamic memory request (Box, Vec, String) in the application through the designated allocator instance.
3. Explanation
(1) Design Motivation — "Why did we design this?"
In #![no_std] embedded applications or custom OS kernels:
- There is no default operating system
malloc/free. - If you use the
alloccrate for dynamic vectors or strings, Cargo needs to know which memory allocator implementation should handle raw memory requests.
#[global_allocator] provides a single point of registration:
- Implement
unsafe impl GlobalAlloc for MyAllocator. - Annotate a static instance with
#[global_allocator]. - All
Vec::push,Box::new, andString::fromcalls automatically invoke your allocator'sallocanddeallocmethods!
(2) Code Examples
Registering an Embedded Bump Allocator with #[global_allocator]
#![no_std]
extern crate alloc;
use core::alloc::{GlobalAlloc, Layout};
use core::ptr::null_mut;
/// A trivial dummy allocator for embedded demonstration
pub struct DummyEmbeddedAllocator;
unsafe impl GlobalAlloc for DummyEmbeddedAllocator {
unsafe fn alloc(&self, _layout: Layout) -> *mut u8 {
// Return raw memory pointer from embedded heap buffer
null_mut()
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Free raw memory pointer
}
}
// 1. Register static allocator instance globally!
#[global_allocator]
static ALLOCATOR: DummyEmbeddedAllocator = DummyEmbeddedAllocator;
pub fn allocate_test() {
// Uses `ALLOCATOR.alloc()` under the hood!
let _v: alloc::vec::Vec<u8> = alloc::vec::Vec::with_capacity(16);
}
4. Common Mistakes & Pitfalls
Mistake 2: Invoking Heap Allocation Functions Recursively Inside Custom Allocator Implementation
The mistake: Calling Vec::push or format! inside the alloc or dealloc methods of a custom allocator.
Why it's wrong: Triggers infinite recursion and stack overflow crashes because the memory allocation call re-invokes the allocator itself.
Fix: Implement GlobalAlloc using raw static memory arrays or direct OS/hardware calls without heap allocations.
Mistake 3: Returning Unaligned Memory Pointers from alloc Violating Layout::align()
The mistake: Ignoring the alignment constraint in Layout::align() during pointer calculation.
Why it's wrong: Passing unaligned memory pointers to hardware instructions causes hardware alignment traps or undefined behavior.
Fix: Align raw memory pointers using (ptr + align - 1) & !(align - 1) arithmetic.
Mistake 1: Defining Multiple #[global_allocator] Statics in a Workspace
The mistake: Defining #[global_allocator] in multiple sub-crates of a single binary.
Why it's wrong: An application can have only ONE global memory allocator. Multiple statics cause a link-time duplicate symbol error.
5. Practice Exercises
Exercise 1: Thread-Safe Fixed-Buffer Bump Allocator with Alignment Handling
Scenario: In bare-metal embedded microcontrollers without OS heap management, dynamic memory must be allocated from static RAM buffers. Implement a #![no_std] thread-safe Bump (Arena) Allocator BumpAllocator<const SIZE: usize> that implements core::alloc::GlobalAlloc. The allocator must guarantee proper memory alignment for requested types, track total allocated bytes and count via atomic operations, and handle out-of-memory situations gracefully by returning a null pointer. Include a unit test demonstrating heap allocation with alloc::vec::Vec and alloc::boxed::Box.
Answer
Implementation
#![no_std]
extern crate alloc;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
use core::ptr::null_mut;
use core::sync::atomic::{AtomicUsize, Ordering};
/// A thread-safe bump allocator using a static backing buffer.
pub struct BumpAllocator<const SIZE: usize> {
heap: UnsafeCell<[u8; SIZE]>,
next: AtomicUsize,
allocations: AtomicUsize,
}
unsafe impl<const SIZE: usize> Sync for BumpAllocator<SIZE> {}
impl<const SIZE: usize> BumpAllocator<SIZE> {
pub const fn new() -> Self {
BumpAllocator {
heap: UnsafeCell::new([0u8; SIZE]),
next: AtomicUsize::new(0),
allocations: AtomicUsize::new(0),
}
}
pub fn allocated_bytes(&self) -> usize {
self.next.load(Ordering::Relaxed)
}
pub fn allocation_count(&self) -> usize {
self.allocations.load(Ordering::Relaxed)
}
}
unsafe impl<const SIZE: usize> GlobalAlloc for BumpAllocator<SIZE> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let align = layout.align();
let size = layout.size();
let mut current = self.next.load(Ordering::Relaxed);
loop {
let heap_start = self.heap.get() as usize;
let current_addr = heap_start + current;
// Align upwards to the nearest multiple of `align`
let aligned_addr = (current_addr + align - 1) & !(align - 1);
let offset = aligned_addr - heap_start;
if offset + size > SIZE {
return null_mut(); // Out of memory
}
match self.next.compare_exchange_weak(
current,
offset + size,
Ordering::AcqRel,
Ordering::Relaxed,
) {
Ok(_) => {
self.allocations.fetch_add(1, Ordering::Relaxed);
return aligned_addr as *mut u8;
}
Err(actual) => current = actual,
}
}
}
unsafe fn dealloc(&self, _ptr: *mut u8, _layout: Layout) {
// Bump allocators reset all memory at once rather than freeing individual blocks
}
}
#[global_allocator]
static ALLOCATOR: BumpAllocator<4096> = BumpAllocator::new();
pub fn test_bump_allocator() {
let initial_bytes = ALLOCATOR.allocated_bytes();
let initial_count = ALLOCATOR.allocation_count();
// Allocate dynamic collection using alloc crate
let mut numbers: alloc::vec::Vec<u32> = alloc::vec::Vec::with_capacity(4);
numbers.push(100);
numbers.push(200);
numbers.push(300);
assert_eq!(numbers.len(), 3);
assert_eq!(numbers[0], 100);
assert_eq!(numbers[1], 200);
assert_eq!(numbers[2], 300);
// Verify tracking counters increased
assert!(ALLOCATOR.allocated_bytes() > initial_bytes);
assert!(ALLOCATOR.allocation_count() > initial_count);
// Test Box allocation
let boxed_val = alloc::boxed::Box::new(42u64);
assert_eq!(*boxed_val, 42);
}
Technical Explanation
UnsafeCell&SyncImplementation: In Rust, static variables mutated across threads require interior mutability. Wrapping the raw heap array inUnsafeCellallows interior mutability, whileunsafe impl Syncasserts that atomic lock-free operations protect concurrent access.- Alignment Calculation: Memory allocations require addresses to be aligned to powers of two matching
layout.align().(current_addr + align - 1) & !(align - 1)rounds the pointer up to the required alignment boundary. - Lock-Free Bump Allocation:
compare_exchange_weakatomically updatesself.nextfromcurrenttooffset + size. If another core or interrupt thread modifiednextconcurrently, the loop retries with the updated offset. - Registration via
#[global_allocator]: Annotatingstatic ALLOCATORroutes allallocrequests (Vec::with_capacity,Box::new) directly to ourBumpAllocator::allocimplementation.
Exercise 2: Memory Profiling & Allocation Tracking Wrapper Allocator
Scenario: During systems debugging and software profiling, developers must monitor heap memory consumption, detect memory leaks, and identify peak usage. Implement a generic decorator allocator TrackingAllocator<A: GlobalAlloc> that wraps an inner allocator and tracks active memory usage, peak memory usage, and total allocation/deallocation call counts. Write a test function using std::alloc::System proving that dropping heap data correctly decrements active memory metrics.
Answer
use std::alloc::{GlobalAlloc, Layout, System};
use std::sync::atomic::{AtomicUsize, Ordering};
/// Wrapper allocator tracking allocation metrics in real-time.
pub struct TrackingAllocator<A: GlobalAlloc> {
inner: A,
currently_allocated: AtomicUsize,
total_allocations: AtomicUsize,
total_deallocations: AtomicUsize,
peak_memory: AtomicUsize,
}
impl<A: GlobalAlloc> TrackingAllocator<A> {
pub const fn new(inner: A) -> Self {
TrackingAllocator {
inner,
currently_allocated: AtomicUsize::new(0),
total_allocations: AtomicUsize::new(0),
total_deallocations: AtomicUsize::new(0),
peak_memory: AtomicUsize::new(0),
}
}
pub fn current_usage(&self) -> usize {
self.currently_allocated.load(Ordering::Relaxed)
}
pub fn peak_usage(&self) -> usize {
self.peak_memory.load(Ordering::Relaxed)
}
pub fn allocation_count(&self) -> usize {
self.total_allocations.load(Ordering::Relaxed)
}
pub fn deallocation_count(&self) -> usize {
self.total_deallocations.load(Ordering::Relaxed)
}
}
unsafe impl<A: GlobalAlloc> GlobalAlloc for TrackingAllocator<A> {
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
let ptr = self.inner.alloc(layout);
if !ptr.is_null() {
let size = layout.size();
self.total_allocations.fetch_add(1, Ordering::Relaxed);
let prev = self.currently_allocated.fetch_add(size, Ordering::Relaxed);
let current = prev + size;
// Atomically update peak memory usage
let mut peak = self.peak_memory.load(Ordering::Relaxed);
while current > peak {
match self.peak_memory.compare_exchange_weak(
peak,
current,
Ordering::Relaxed,
Ordering::Relaxed,
) {
Ok(_) => break,
Err(actual) => peak = actual,
}
}
}
ptr
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
self.inner.dealloc(ptr, layout);
let size = layout.size();
self.total_deallocations.fetch_add(1, Ordering::Relaxed);
self.currently_allocated.fetch_sub(size, Ordering::Relaxed);
}
}
#[global_allocator]
static ALLOCATOR: TrackingAllocator<System> = TrackingAllocator::new(System);
pub fn test_tracking_allocator() {
let initial_allocated = ALLOCATOR.current_usage();
let initial_allocs = ALLOCATOR.allocation_count();
let initial_deallocs = ALLOCATOR.deallocation_count();
{
let mut data: Vec<u8> = Vec::with_capacity(1024);
data.extend_from_slice(&[0xAA; 512]);
assert!(ALLOCATOR.current_usage() >= 1024);
assert!(ALLOCATOR.peak_usage() >= 1024);
assert_eq!(ALLOCATOR.allocation_count(), initial_allocs + 1);
assert_eq!(ALLOCATOR.deallocation_count(), initial_deallocs);
} // `data` goes out of scope here, triggering `dealloc` via RAII!
assert_eq!(ALLOCATOR.current_usage(), initial_allocated);
assert_eq!(ALLOCATOR.deallocation_count(), initial_deallocs + 1);
}
Technical Explanation
- Decorator Allocator Design Pattern:
TrackingAllocatorwraps an existingGlobalAllocimplementation (such as system mallocstd::alloc::Systemor custom OS heap allocator). It delegates actual raw byte allocation toself.innerwhile interceptingallocanddealloccalls to maintain statistics. - Atomic Accounting:
fetch_addandfetch_subprovide thread-safe counter modifications without full mutex locking overhead. - Compare-And-Swap (CAS) Peak Tracking: Updating
peak_memoryuses acompare_exchange_weakloop to ensure that concurrent allocations across multiple threads correctly update the maximum recorded high-water mark without race conditions. - RAII Drop Verification: When
datadrops at the end of the inner block scope, Rust automatically invokesGlobalAlloc::dealloc, decrementscurrently_allocated, and incrementsdeallocation_count.
Exercise 3: Constant-Time Fixed-Size Block Pool Allocator for Real-Time Embedded Systems
Scenario: In mission-critical real-time embedded applications (such as flight controllers or automotive microcontrollers), memory allocation must take deterministic constant time with zero heap fragmentation. Implement a #![no_std] fixed-size block pool allocator FixedBlockAllocator<const BLOCK_SIZE: usize, const NUM_BLOCKS: usize> managing a static memory pool using an embedded free-list index. Implement GlobalAlloc to return blocks when requests fit BLOCK_SIZE and handle block recycling on dealloc. Include unit tests validating block allocation, deallocation, block reuse, and out-of-memory behavior.
Answer
Implementation
#![no_std]
extern crate alloc;
use core::alloc::{GlobalAlloc, Layout};
use core::cell::UnsafeCell;
use core::ptr::null_mut;
use core::sync::atomic::{AtomicBool, AtomicUsize, Ordering};
/// Fixed-size block pool allocator providing O(1) allocation/deallocation.
pub struct FixedBlockAllocator<const BLOCK_SIZE: usize, const NUM_BLOCKS: usize> {
pool: UnsafeCell<[[u8; BLOCK_SIZE]; NUM_BLOCKS]>,
next_free: UnsafeCell<[i32; NUM_BLOCKS]>,
free_head: UnsafeCell<i32>,
lock: AtomicBool,
active_allocations: AtomicUsize,
}
unsafe impl<const BLOCK_SIZE: usize, const NUM_BLOCKS: usize> Sync
for FixedBlockAllocator<BLOCK_SIZE, NUM_BLOCKS> {}
impl<const BLOCK_SIZE: usize, const NUM_BLOCKS: usize> FixedBlockAllocator<BLOCK_SIZE, NUM_BLOCKS> {
pub const fn new() -> Self {
let mut next_free = [-1i32; NUM_BLOCKS];
let mut i = 0;
while i < NUM_BLOCKS - 1 {
next_free[i] = (i + 1) as i32;
i += 1;
}
FixedBlockAllocator {
pool: UnsafeCell::new([[0u8; BLOCK_SIZE]; NUM_BLOCKS]),
next_free: UnsafeCell::new(next_free),
free_head: UnsafeCell::new(0),
lock: AtomicBool::new(false),
active_allocations: AtomicUsize::new(0),
}
}
fn acquire_lock(&self) {
while self.lock.swap(true, Ordering::Acquire) {
core::hint::spin_loop();
}
}
fn release_lock(&self) {
self.lock.store(false, Ordering::Release);
}
pub fn active_count(&self) -> usize {
self.active_allocations.load(Ordering::Relaxed)
}
}
unsafe impl<const BLOCK_SIZE: usize, const NUM_BLOCKS: usize> GlobalAlloc
for FixedBlockAllocator<BLOCK_SIZE, NUM_BLOCKS>
{
unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
// Reject allocations exceeding fixed block size or alignment constraints
if layout.size() > BLOCK_SIZE || layout.align() > BLOCK_SIZE {
return null_mut();
}
self.acquire_lock();
let head = *self.free_head.get();
if head == -1 {
self.release_lock();
return null_mut(); // Pool exhausted
}
let block_idx = head as usize;
let next_idx = (*self.next_free.get())[block_idx];
*self.free_head.get() = next_idx;
self.active_allocations.fetch_add(1, Ordering::Relaxed);
self.release_lock();
let pool_ptr = self.pool.get() as *mut u8;
pool_ptr.add(block_idx * BLOCK_SIZE)
}
unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
if ptr.is_null() {
return;
}
let pool_start = self.pool.get() as *mut u8;
let offset = ptr as usize - pool_start as usize;
let block_idx = offset / BLOCK_SIZE;
self.acquire_lock();
let current_head = *self.free_head.get();
(*self.next_free.get())[block_idx] = current_head;
*self.free_head.get() = block_idx as i32;
self.active_allocations.fetch_sub(1, Ordering::Relaxed);
self.release_lock();
}
}
#[global_allocator]
static ALLOCATOR: FixedBlockAllocator<64, 8> = FixedBlockAllocator::new();
pub fn test_block_allocator() {
let initial_active = ALLOCATOR.active_count();
// 1. Allocate block
let b1 = alloc::boxed::Box::new([0x11u8; 32]);
assert_eq!(ALLOCATOR.active_count(), initial_active + 1);
// 2. Allocate second block
let b2 = alloc::boxed::Box::new([0x22u8; 64]);
assert_eq!(ALLOCATOR.active_count(), initial_active + 2);
assert_eq!(b1[0], 0x11);
assert_eq!(b2[0], 0x22);
// 3. Drop b1 to return block to pool free-list
core::mem::drop(b1);
assert_eq!(ALLOCATOR.active_count(), initial_active + 1);
// 4. Allocate b3, re-using freed block #0
let b3 = alloc::boxed::Box::new([0x33u8; 16]);
assert_eq!(ALLOCATOR.active_count(), initial_active + 2);
assert_eq!(b3[0], 0x33);
}
Technical Explanation
- Free-List Pool Strategy: Instead of scanning heap metadata or splitting contiguous spans,
FixedBlockAllocatorlinks free blocks in a stack-like single-linked list indexed bynext_free.allocpops fromfree_headin time, anddeallocpushes back ontofree_headin time. - Spinlock Synchronization:
AtomicBool::swap(true, Acquire)provides lightweight mutual exclusion in#![no_std]bare-metal environments where OS mutexes are unavailable.core::hint::spin_loop()notifies CPU execution hardware of busy waiting. - Pointer Arithmetic for Deallocation:
dealloccalculates the index of the freed block by finding(ptr - pool_start) / BLOCK_SIZE. This avoids searching through tables or maintaining per-block header metadata overhead. - Deterministic OOM & Recycling: If all blocks are allocated (
free_head == -1),allocreturnsnull_mut(). When objects drop, their block indices are immediately returned tofree_head, enabling deterministic memory reuse without external fragmentation.
6. Related Terms
- None!
7. Key Takeaways
#[global_allocator]registers the global memory allocator static instance.- Must implement the
GlobalAlloctrait. - Essential for enabling
allocheap collections (Vec,Box) in#![no_std]environments. - Only one
#[global_allocator]can be defined per binary application.