std.memory
The std.memory modules define the Allocator interface and the allocators that implement it. The builtins alloc and free are sugar over the allocator in scope: a function marks a parameter with using to designate it as the ambient allocator for its body, and alloc/free inside that body dispatch to it. With no allocator in scope, they use the heap. See memory in the reference for the language rules and the memory guide for a walkthrough.
Both modules require @paradigm procedural and @paradigm oop internally; importing them from a procedural file works as shown below.
std.memory.allocator
Section titled “std.memory.allocator”@import std.memory.allocatorThe Allocator interface and three allocators that implement it. Choosing the allocator type chooses the implementation that alloc resolves to. A stateful allocator advances in place across calls, since a method takes its receiver by pointer.
| Item | Description |
|---|---|
interface Allocator | alloc(size: int64, align: int64) -> *void and free(p: *void) -> void. |
Heap | The libc backed allocator, and the default. |
heap() -> Heap | A heap allocator value to pass through using. |
FixedBuffer | A bump allocator over a caller buffer, no heap. |
fixed_buffer(base: *raw int8, cap: int64) -> FixedBuffer | A fixed buffer allocator over base. |
Debug | A debug allocator that reports leaks, catches a double free, and poisons freed memory with 0xDD. |
debug() -> Debug | A debug allocator value to pass through using. |
The counters debug_leaks() -> int64 and debug_double_frees() -> int64 are builtins, not module exports: debug_leaks reports how many allocations are not yet freed, and debug_double_frees how many double or invalid frees were seen.
The Allocator interface
Section titled “The Allocator interface”interface Allocator { alloc(size: int64, align: int64) -> *void free(p: *void) -> void}Any type that implements this interface is an allocator, so users can write their own and pass it with using. free must run under the allocator that produced the pointer, since a using scope routes free to the scope’s allocator.
The libc backed heap allocator. This is the default when no allocator is in scope, so an explicit Heap is only needed to pass one through using.
func work(using a: Heap) -> void { p: *int64 = alloc(8) // heap allocation, via the passed allocator free(p)}
func main() -> int32 { h := heap() work(h) return 0}As of 0.5.4, pass the allocator with its concrete type, as here; an interface typed parameter such as using a: Allocator rejects a concrete Heap argument at the call site.
FixedBuffer
Section titled “FixedBuffer”A bump allocator over a caller provided buffer. It touches no heap. Allocations carve forward from the buffer, aligned to the request, and individual frees are a no op. Exhausting the buffer prints fatal: fixed buffer exhausted and aborts rather than handing out memory past its end.
@paradigm procedural
@import std.memory.allocator
// Two eight byte allocations from a 64 byte buffer leave the bump// offset at 16.
func fill(using a: FixedBuffer) -> int64 { p: *int64 = alloc(8) *p = 1 q: *int64 = alloc(8) *q = 2 return a.used}
func main() -> int32 { buf: *raw int8 = alloc_bytes(64) mut fb := fixed_buffer(buf, 64) println(fill(fb)) // 16 free(buf) return 0}FixedBuffer exposes its fields base: *raw int8, cap: int64, and used: int64; reading used shows how far the bump offset has advanced. alloc_bytes is a builtin that returns a raw byte block.
A debug allocator. It tracks each allocation to report leaks and catch a double free, and poisons freed memory with 0xDD. Route a section of code through it with using, then read debug_leaks() and debug_double_frees().
@paradigm procedural
@import std.memory.allocator
// p is never freed, so it leaks. q is freed twice, the second a// double free. The counters report 1 and 1.
func work(using a: Debug) -> void { p: *int64 = alloc(8) *p = 1 q: *int64 = alloc(8) *q = 2 free(q) free(q)}
func main() -> int32 { mut d := debug() work(d) println(debug_leaks()) // 1 println(debug_double_frees()) // 1 return 0}std.memory.arena
Section titled “std.memory.arena”@import std.memory.arenaA bump allocator over one backing buffer. Allocations carve forward from the buffer; individual frees are a no op, and the whole arena is reset or destroyed at once. Pass the arena by pointer so the bump offset persists. Exhausting the buffer prints fatal: arena exhausted and aborts.
| Item | Description |
|---|---|
Arena | The arena struct: base: *raw int8, cap: int64, used: int64. |
arena_new(cap: int64) -> Arena | An arena backed by a heap allocated cap byte buffer. |
arena_alloc(a: *Arena, size: int64) -> *void | Carve size bytes, aligned to 8, and return the pointer. |
arena_reset(a: *Arena) -> void | Roll the offset back to zero, keeping the buffer. |
arena_destroy(a: *Arena) -> void | Free the backing buffer. |
Direct use
Section titled “Direct use”@paradigm procedural
@import std.memory.arena
func main() -> int32 { a: *Arena = alloc(arena_new(1024)) p: *int64 = arena_alloc(a, 8) *p = 7 println(*p) // 7 println((*a).used) // 8 arena_reset(a) println((*a).used) // 0 arena_destroy(a) free(a) return 0}arena_destroy frees the backing buffer, not the Arena struct itself; here the struct was heap allocated with alloc, so it needs its own free.
As an Allocator
Section titled “As an Allocator”Arena also implements Allocator, so it can be passed with using and the alloc builtin dispatches to it. The interface implementation aligns each allocation to the requested alignment rather than the fixed 8 of arena_alloc.
@paradigm procedural
@import std.memory.arena
// Arena passed through `using`. It implements Allocator, so the// builtins alloc and free dispatch to it inside fill.
func fill(using a: Arena) -> int64 { p: *int64 = alloc(8) *p = 1 q: *int64 = alloc(8) *q = 2 return a.used}
func main() -> int32 { a: *Arena = alloc(arena_new(64)) println(fill(*a)) // 16 arena_destroy(a) free(a) return 0}Inside fill, free on an arena pointer is a no op, matching the arena model: nothing needs individual free, and the arena is reset or destroyed as a whole.
std.memory.collector
Section titled “std.memory.collector”@import std.memory.collectorControl and gauges for the conservative collected heap. Unlike the allocators above, the collected heap is not an allocator you pass with using. You reach it only by minting a collector<T> value, which the checker confines to the main thread and reclaims by a conservative mark and sweep. This module exposes the collection trigger and four read only counters over that heap, so no pointer crosses the boundary and every wrapper is safe to call anywhere on the main thread. See the collected heap for the language rules.
| Function | Description |
|---|---|
gc_collect() -> void | Force one full mark and sweep now. Main thread only. |
gc_live_blocks() -> int64 | How many collected blocks are live. |
gc_live_bytes() -> int64 | Total live collected payload bytes, an upper bound. |
gc_collections() -> int64 | How many collections have run since start, monotonic. |
Collection is automatic and amortized: it runs on its own once the byte debt since the last collection crosses a threshold that doubles with the live set. gc_collect forces one immediately, which is what a test or a latency sensitive section reaches for. Because the scan is conservative, gc_live_bytes reports an upper bound: a stray stack word that merely looks like a pointer can keep a block one collection longer than it needed.
No Collector type implementing Allocator ships. The interface hands back an untyped *void, which would erase the collector<T> tracking the checker uses to keep a collected reference on its anchor thread, so a collected block routed through the allocator seam could cross a channel or a spawn boundary as a bare pointer with no diagnostic. The typed mint stays the one checked surface for collected memory.
@paradigm procedural
@import std.memory.collector
// Mint collected blocks, hold one on the stack across a forced collection,// and read the gauges. The held block is a root the scan finds, so it// survives the sweep with its value intact.
func main() -> int32 { keep := collector<int64>(99)
mut sum: int64 = 0 mut i: int64 = 0 while i < 8 { c := collector<int64>(i) sum = sum + *c i = i + 1 }
before := gc_live_blocks() g0 := gc_collections()
gc_collect()
g1 := gc_collections()
println(before) // 9 blocks live before the collection println(g1 - g0) // exactly one collection ran println(sum) // 0 + 1 + ... + 7 println(*keep) // the held block survived, value intact return 0}See also
Section titled “See also”- Memory guide for when to reach for each allocator.
- Memory reference for
alloc,free,defer, and generational references. - The collected heap for the
collector<T>wrapper behindgc_collectand the gauges. - Builtins reference for
alloc_bytes,ptr_add,debug_leaks, anddebug_double_frees.