alloc
Everything that touches the heap lives in alloc. It gives you three things: a description of a memory request (Layout), a trait that anything serving those requests implements (Allocator), and the owning pointers built on top — Box, Rc, Arc — plus two allocation strategies, Arena and Pool, that you can substitute for the default.
Types here own storage. Each one exposes drop(mut &this), and its documentation names who is responsible for calling it. In practice the compiler synthesises the call at scope exit; a manual .drop() remains available for early release.
| Item | Import |
|---|---|
Box<T, A> | (prelude) |
Rc<T, A>, Weak<T, A> | (prelude) |
Arc<T, A>, Weak<T, A> | import std::alloc::arc; |
Layout | import std::alloc::layout; |
Allocator, AllocError, GlobalAlloc | import std::alloc::allocator; |
Arena | import std::alloc::arena; |
Pool | import std::alloc::pool; |
Layout
Every Allocator call takes a Layout: a size and an alignment. Constructing one validates that the alignment is a non-zero power of two, so allocators downstream can rely on the invariant without re-checking it.
| Constructor / method | Notes |
|---|---|
static new(size: u64, alignment: u64) | Panics if alignment is zero or not a power of two — that's a caller bug, not a runtime condition. A zero size is allowed; the allocator decides what it means. |
static of<T>() | The layout of a single T. |
static array<T>(count: u64) | count contiguous Ts. Panics on multiplication overflow. |
static try_array<T>(count: u64) | Option<Layout> — None on overflow, for try_* constructors that must not panic. |
size() / alignment() | u64 |
padded_size() | size rounded up to the next multiple of alignment — the stride for packing adjacent values. |
The Allocator trait
type trait Allocator {
allocate(mut &this, layout: Layout) -> Result<NonNull<u8>, AllocError>;
deallocate(mut &this, ptr: NonNull<u8>, layout: Layout) -> void;
reallocate(mut &this, ptr: NonNull<u8>,
old_layout: Layout, new_layout: Layout)
-> Result<NonNull<u8>, AllocError>;
}
allocate returns uninitialized memory — write before you read. deallocate requires that the pointer came from a previous allocate on this allocator with a matching layout.
reallocate has a default: allocate, copy, free. An allocator that can grow a block in place should override it.
Collections take A: Allocator as a type parameter, so the storage strategy stays a caller decision instead of being baked into the container.
AllocError
Failures carry both a kind and the layout that was refused, and describe(&this) -> Str gives a short stable message — the same accessor every standard library error type exposes.
| Kind | Meaning |
|---|---|
OutOfMemory | The underlying allocator returned null: out of memory, or an address-space limit. |
ZeroSized | The allocator refuses zero-sized requests. Handle the zero case before calling. |
Exhausted | A bounded allocator (arena, pool) has no room for this request. |
InvalidLayout | The layout is incompatible with this allocator — the wrong allocator for the request. |
GlobalAlloc
A zero-sized handle to the process-wide allocator; every instance dispatches through the same backend. Construct it with GlobalAlloc::new(). It is the default for every allocator-generic type in the library, which is what keeps the bare Box<T> spelling meaningful everywhere.
Box<T, A>
A Box owns exactly one heap value plus the allocator instance that produced it. Moves transfer ownership; the final holder's drop runs T::drop() and returns the storage.
mut b: Box<Config> = Box<Config>::new(config);
b.get_ref().port = 8080;
| Method | Notes |
|---|---|
static new(value: T) | Allocates via GlobalAlloc. Panics on failure. |
static new_in(value: T, alloc: A) | With your own allocator. Panics on failure. |
static try_new(value: T) | Result<Box<T, GlobalAlloc>, AllocError> |
static try_new_in(value: T, alloc: A) | Result<Box<T, A>, AllocError> |
get_ref(&this) / get_mut(mut &this) | T* — borrow the value. A Box is a unique owner, so get_mut is always sound. |
as_ptr(&this) | T*, valid for the lifetime of the box. |
into_raw(mut this) | T* — gives up ownership; you are now responsible for freeing it. |
leak(mut this) | Alias for into_raw. |
static from_raw(raw: T*) | Re-wrap a pointer from into_raw. It must have come from a Box<T, GlobalAlloc>. |
Box<T> implements Deref<T>, so *b and b.field work directly, and Clone when T: Clone — a deep copy into fresh storage.
Note that into_raw and leak transfer both the value and the allocation, so T::drop() does not run on those paths.
Rc<T, A>
Shared ownership within one thread. Rc shares ownership of a heap value among many holders. The value lives until the last strong handle drops, at which point T::drop() runs; the storage comes back once the last weak handle is gone too.
Rcis not thread-safe. The counts are plainu64s with no synchronization. Sending one across threads, or cloning it from two threads at once, is a data race — useArc.
| Method | Notes |
|---|---|
static new(value: T) / new_in | Panics on allocation failure. |
static try_new / try_new_in | The fallible forms. |
clone(&this) | Another handle to the same value; bumps the strong count. |
downgrade(&this) | A Weak<T, A>; bumps the weak count only. |
get_ref(&this) | T* — shared borrow, sound for reads while any clone is alive. |
get_mut(mut &this) | Option<T*> — Some only when this is the sole owner. |
strong_count() / weak_count() | u64, advisory. |
get_mut is the sound way to mutate an Rc-managed value. It hands back a pointer only when there is exactly one strong reference and no outstanding Weak — either could otherwise alias the value, a Weak by upgrading into a second strong handle.
Weak references
A Weak<T, A> does not keep the value alive. Once every strong handle is gone the value is dropped and upgrade returns None from then on. This is the tool for breaking reference cycles: hold the back-edge of a cycle as a Weak and it will not leak.
Weak::new() allocates nothing and never upgrades — useful as a placeholder before a cycle is wired up.
Why teardown has two phases
The header carries two counts. strong tracks live Rc handles. weak tracks live Weak handles plus one shared unit owned by the strong group, so it never drops below 1 while any Rc exists.
When strong hits zero the value is dropped and the strong group releases its shared unit. When weak then hits zero — on that same drop, or later when the last surviving Weak goes away — the header is freed. That split is what lets a Weak safely outlive the value: the header, and the strong flag upgrade reads, stays mapped until no handle of either kind remains.
Arc<T, A>
Shared ownership across threads. Arc is the thread-safe sibling of Rc. Same shape, same two-phase teardown, but the counts are Atomic<u64>, so handles can be cloned and dropped concurrently without a data race. It is structurally Send + Sync whenever T and A are.
import std::alloc::arc;
import std::thread;
mut shared: Arc<Table> = Arc<Table>::new(table);
mut copy: Arc<Table> = shared.clone();
thread::spawn(worker, copy);
The API mirrors Rc — new, new_in, try_new, try_new_in, clone, downgrade, get_ref, get_mut, strong_count, weak_count — with upgrade on Weak running a compare-exchange loop so it can never resurrect a value whose strong count already reached zero.
get_mut requires strong == 1 && weak == 1. With a single strong reference no other thread holds an Arc to clone or downgrade from, so once both counts read 1 the access really is exclusive. Even then, interior mutation visible across threads needs the value itself to be Sync — an atomic, say.
A raw-pointer escape hatch exists for parking a reference outside the type system, such as in a manual-vtable waker: into_raw consumes a handle without touching the count, from_raw takes the reference back, and increment_strong_count adds one to a header held only as a raw pointer. Each into_raw must be balanced exactly once or the value leaks.
The memory ordering, briefly
Clone bumps the count with Relaxed; drop decrements with Release and the last-decrement path issues an Acquire fence. This is the canonical Boost/Rust pattern. Relaxed is right for the clone because the new reference is only ever observed by callers who already share a happens-before edge with the cloning thread. The release/acquire pair on drop is what synchronizes the final T::drop() against every prior mutation from any thread that has already dropped its own handle.
Routing every free decision through the one atomic weak counter is what makes the race between "the last Arc drops" and "a Weak drops" safe: there is never a moment where two threads read two separate counters and both decide to free.
Arena — bump allocation, bulk release
An arena hands out memory by bumping an offset into a chunk. Allocation is O(1), individual deallocation is unsupported, and everything comes back at once through reset (keep the chunks, reuse the space) or drop (return the chunks to the OS).
The shape it fits is "allocate a pile of things with related lifetimes, then throw them away together" — compiler passes, per-request handlers, parse trees. Long-lived caches are not that shape.
import std::alloc::arena;
mut a: Arena = Arena::new();
mut nodes: Array<Node, Arena> = Array<Node, Arena>::new_in(a);
// ... build the tree ...
a.reset(); // everything above is invalid now; capacity is reused
| Method | Notes |
|---|---|
static new() | Chunks of DEFAULT_CHUNK_SIZE (1 MiB). |
static with_chunk_size(chunk_size: u64) | A hint about typical allocation size, not a cap — a larger request gets its own chunk sized to fit. |
reset(mut &this) | Rewind every chunk to zero. Invalidates every pointer handed out. |
capacity() / used() | Bytes held in chunks / bytes handed out. |
bump(mut &this, size: u64, align: u64) | void*, null on OOM or a zero-sized request. |
grow(mut &this, ptr, old_size, new_size, align) | Extends in place when ptr is the most recent allocation in the frontier chunk. |
owns(&this, ptr: void*) | Whether the pointer lies in a chunk this arena owns. |
Repeated reset is where an arena earns its keep — the same memory gets reused instead of round-tripping through the platform allocator.
Each chunk's data buffer is mapped straight from the OS, so releasing it returns the pages immediately and RSS drops at the call, with none of the main-heap retention you get from freeing many small malloc blocks. The mapping is demand-paged, so untouched pages cost nothing and a small arena stays cheap despite the 1 MiB nominal chunk.
Arena implements Allocator, so it drops into any allocator-generic type. Its deallocate is a no-op by design.
Pool — fixed-size slots
A pool is configured with one slot layout at construction and refuses anything that doesn't match. Freed slots are threaded onto an intrusive free list, so allocation and deallocation are both O(1). When the free list runs dry it allocates a block and carves slots_per_block fresh slots out of it.
Reach for a pool when you have many values of the same type with churn-heavy lifetimes — game entities, AST nodes under frequent edit, request objects. For mixed sizes, use an arena or the global allocator.
| Method | Notes |
|---|---|
static new(slot: Layout) | Default block granularity (64 slots). Panics if the slot is smaller than a pointer — the free list needs that room. |
static with_block_size(slot: Layout, slots_per_block: u64) | Bigger blocks mean fewer allocations and more unused tail. |
allocate_slot(mut &this) | Result<NonNull<u8>, AllocError> |
deallocate_slot(mut &this, ptr: NonNull<u8>) | The pointer must have come from this pool and not been freed. |
Through the Allocator trait, a mismatched layout returns Err(InvalidLayout) — you picked the wrong allocator for that request.