Allocator v0.1.0

Source
Allocation contracts and the allocators that meet them

Installation

Add Allocator to the [Dependencies] table of your package's Rux.toml:

[Dependencies]
Allocator = { Namespace = "Rux", Version = "0.1.0" }

Standard packages are declared by hand: the registry does not serve them, so rux add Rux/Allocator cannot add them.

Dependencies

Guide

What it provides

ModuleCovers
Layouta validated size-and-alignment pair, and the layouts a type or array needs
ErrorAllocError, and the questions worth asking of one
Contractthe Allocator interface every allocator here implements
SystemSystemAllocator, which asks the operating system for pages
BoxBox<T>, one heap value with exactly one owner
SharedRc<T> and Arc<T>, one heap value shared by counted handles
ArenaArena, which advances a pointer and reclaims everything at once
FixedBufferFixedBuffer, which hands out storage the caller already has
PoolPool, which serves small blocks from per-class free lists

The module names describe where each declaration comes from; they are not part of an import path.

A Layout that exists is valid. Layout::New refuses an alignment that is not a power of two, or a size whose rounding would pass the end of the address space — so nothing downstream checks again. That second case is the one worth naming: it is how a request for a huge array becomes an allocation far smaller than the caller asked for. Use Layout::ForValue<T>() or Layout::ForArray<T>(count) when storage follows a type; neither requires a sample value, so describing a move-only type consumes nothing.

Four promises hold for every allocator here:

  • The address satisfies the layout. Always.
  • The contents are undefined. Nothing here zeroes; the page layer does, because the operating system does.
  • A release must match its allocation — the same pointer, and an equal layout. The allocator needs the size to know what it is taking back.
  • A failure leaves everything alone. A failed reallocation leaves the original block valid and still the caller's.

Every operation returns a native fallible: Allocate and Reallocate give (*var opaque) ! AllocError, and Deallocate gives ! AllocError. Every AllocError case is a failure, so a caller propagates with ?, recovers with catch, and a result left unhandled is diagnosed as discarded. A release on a cleanup path that has nobody to report to says so explicitly with allocator.Deallocate(block, layout) catch { else => {} };.

A zero-sized layout is legal and gives a non-null aligned address that nothing may be read from or written to, and which must still be released. That is cheaper than a null case every caller has to branch on, and it keeps an empty collection from being a special shape.

Arena, Pool, FixedBuffer, and Box<T> prohibit copying. The first two and the box use canonical destructors; the fixed buffer owns no storage but still cannot be copied because duplicate bump state could hand out overlapping blocks. Move an owner explicitly, for example let destination <- source. Box::Create returns Box<T> ! AllocError, destroying the value it was given when the allocator refuses. Destroying a box destroys its value before returning the allocation, while TryTake moves the value to the caller and leaves the box empty.

Rc<T> and Arc<T> are the opposite: copying a handle shares its value and adds to a count, and the value is destroyed when the last handle goes. Rc counts with plain arithmetic for one thread, so it is declared #Unsendable() and cannot be a thread's context; Arc counts atomically, so its copies can be made and dropped on any thread. Both hand out the value for reading through Get, and for writing through GetMutable only while a single handle exists. There are no weak handles, so a cycle of handles is never freed.

Canonical construction is SystemAllocator(), Arena(backing, blockSize), Pool(backing, blocksPerChunk), and FixedBuffer(storage, capacity). Layout::New retains its name because it validates and returns Layout?, so Layout::New(size, alignment) ?? fail AllocError::Unsupported turns an unrepresentable layout into a failure.

An Arena hands out storage by advancing a pointer and takes it all back at once, which is the right shape whenever a program builds many small things whose lives end together. Blocks come from a backing allocator and each is twice the size of the last, so a long-lived arena stops going back to the system. Reset rewinds every allocation while keeping the largest block, so a second round of the same work asks the system for nothing. BytesUsed sums what every block has handed out, alignment padding included; the tail a block is left with when a request moves on to a new block is not counted.

Borrow a concrete implementation directly for a short-lived interface view, for example let view: &var Allocator = system. Arena, pool, and fixed-buffer handles are storable adapters for allocators whose mutable state must stay with an owner. A handle stores a raw pointer because references cannot be fields or returned; it must not outlive or survive a move of its owner.

An arena accepts a release rather than refusing one, so it can stand in for any allocator in generic code that releases what it takes. Ordinary releases do nothing; the most recent allocation is rewound, which is what lets a growing collection in an arena avoid wasting every earlier size it passed through.

A FixedBuffer is the arena's shape without the arena's appetite: it hands out storage the caller already has, never asks anyone for more, and reports OutOfMemory when the buffer runs out. That makes it the allocator for the places where allocation must not happen — a fixed working set, a signal-safe path, a target with no allocator underneath at all. It owns nothing and has no destructor, but its copy operation is prohibited because copying its state while it is in use would hand out the same storage twice.

A Pool is for the other shape — many small values whose lives end at different times and in no particular order. A request is rounded up to one of five power-of-two classes, each keeping its own free list, so both allocation and release are a pointer swap with no search and no coalescing. A class that runs dry takes one chunk from a backing allocator and carves it, so the backing allocator sees one request per chunk rather than one per block. Rounding up is what pays for the speed: a 17-byte request occupies a 32-byte block, and those 15 bytes are not recoverable until the chunk is released. A request too large for the largest class, or needing a stricter alignment than a chunk is taken at, goes straight to the backing allocator — decided from the layout alone, so a release reaches the same conclusion as its allocation without the pool recording anything per block.

SystemAllocator is page-granular, so a small allocation costs a whole page. It is the backing store for an arena or a pool rather than the allocator a program uses for many small values directly.

Index

Interfaces

NameSummary
AllocatorThe operations every allocator provides.

Types

NameSummary
ArenaBlockOne block of arena storage, with the bookkeeping for what has been handed out of it.
ArenaAn allocator that advances a pointer and reclaims everything at once.
ArenaHandleA copyable interface adapter pointing at an arena.
Box<T>One heap-allocated value of T, owned by exactly one holder.
AllocErrorWhy an allocation, reallocation or release did not do what was asked.
FixedBufferAn allocator that advances a pointer through storage it does not own.
FixedBufferHandleA copyable interface adapter pointing at a FixedBuffer.
LayoutA validated allocation request: a size, and the alignment its address must satisfy.
PoolChunkOne chunk of pool storage, carved into blocks of a single class.
PoolAn allocator that serves small blocks from per-class free lists.
PoolHandleA copyable interface adapter pointing at a pool.
Rc<T>A shared, counted handle to one value, for use on one thread.
Arc<T>A shared, counted handle to one value whose copies may live on different threads.
SystemBlockHeaderWhat a system allocation records about itself, immediately before the address the caller sees.
SystemAllocatorAn allocator that asks the operating system for pages.

Functions

NameSummary
EmptyBlockAddressThe address a zero-sized allocation reports, as a pointer.
IsEmptyBlockWhether block is the address a zero-sized allocation reports.
NaturalAlignmentThe alignment a value of size bytes naturally wants.
ClassIndexForThe class a layout belongs to, or PoolClassCount for a layout no class can serve.

Constants

NameSummary
DefaultArenaBlockSizeThe default payload of an arena's first block.
ArenaBlockAlignmentThe alignment every arena block is taken at, and so the strictest alignment an arena can serve from one.
EmptyBlockThe address a zero-sized allocation reports.
PoolClassCountHow many size classes a pool serves.
PoolSmallestClassThe smallest size class, and so the block a one-byte request occupies.
PoolLargestClassThe largest size class.
PoolAlignmentThe alignment every chunk is taken at, and so the strictest alignment a pooled block can satisfy.
DefaultPoolBlocksPerChunkHow many blocks a chunk holds when the caller does not say.
SystemBlockMagicThe pattern a valid system block header carries.