Fixed buffer
Every allocator so far could, in the end, ask the operating system for more. Sometimes that is exactly what you must not do: on a small device with no system allocator at all, in code that has to run within a fixed budget, or when one part of a program must never be able to use up memory the rest needs. A FixedBuffer is the allocator for those places. It hands out memory you already have, and when that runs out, it says no.
Storage you already own
Here the storage is a 64-byte array on the stack. The buffer is built over its first byte and its size:
var storage: byte[64];
var buffer = FixedBuffer(@storage[0] as *var opaque, 64);
var handle = buffer.Handle();
let allocator: Allocator = handle;
Like an arena, the buffer hands that memory out piece by piece from a moving marker, and is used through a Handle() that implements Allocator. Unlike an arena, it never takes a new block from anyone.
The storage belongs to you, not to the buffer. It must outlive the buffer and everything handed out of it — here all three live in Main, so that holds.
Asking until it says no
A Point is two int64s, 16 bytes, so four fit in 64 bytes. The loop keeps asking and stops at the first refusal:
match allocator.Allocate(layout) {
.Success(block) => {
made += 1;
let point = block as *var Point;
*point = Point { x: made, y: made * made };
PrintLine("point {} at ({}, {}), {} bytes left",
made, point.x, point.y, buffer.BytesRemaining());
},
.Failure(error) => {
let full = error == AllocError::OutOfMemory;
PrintLine("point {} refused, out of memory: {}", made + 1, full);
break;
}
}
This time the failure is not passed on with ?. It is the expected way out of the loop, so a match takes it apart directly, as in Outcome. Code written against Allocator cannot tell a fixed buffer from any other allocator — it just sees a refusal sooner.
Reset
As with an arena, Reset makes the whole buffer available again, and invalidates every address it gave out before:
buffer.Reset();
The next request then starts at the front of storage again, which the last line of the output confirms.
Three allocators side by side
SystemAllocator | Arena | FixedBuffer | |
|---|---|---|---|
| Memory comes from | the operating system | blocks from a backing allocator | storage you pass in |
| When it runs out | the system refuses | it takes a bigger block | OutOfMemory |
Deallocate of one block | gives it back | rewinds only the most recent | rewinds only the most recent |
| Everything at once | — | Reset, and the destructor | Reset |
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// A `FixedBuffer` is an allocator over memory you already have: here, a 64-byte array on the
// stack. It hands that memory out piece by piece, like an arena, but it never asks anyone for
// more. When the buffer is full, `Allocate` fails with `AllocError::OutOfMemory`.
//
// That makes it the allocator for places where real allocation is not allowed or not
// available, and a way to put a hard limit on how much some piece of code may use. Code
// written against `Allocator` does not know the difference; it just sees a refusal sooner.
//
// The storage belongs to you, not to the buffer, so it must outlive the buffer and everything
// handed out of it. As with an arena, `Reset` makes the whole buffer available again and
// invalidates every address it gave out before.
import Allocator::{ AllocError, Allocator, FixedBuffer, Layout };
import Io::PrintLine;
struct Point {
x: int64;
y: int64;
}
func Main() -> ! AllocError {
var storage: byte[64];
var buffer = FixedBuffer(@storage[0] as *var opaque, 64);
var handle = buffer.Handle();
let allocator: Allocator = handle;
// Each point needs 16 bytes, so four fit.
let layout = Layout::ForValue<Point>();
var made: int64 = 0;
loop {
match allocator.Allocate(layout) {
.Success(block) => {
made += 1;
let point = block as *var Point;
*point = Point { x: made, y: made * made };
PrintLine("point {} at ({}, {}), {} bytes left",
made, point.x, point.y, buffer.BytesRemaining());
},
.Failure(error) => {
let full = error == AllocError::OutOfMemory;
PrintLine("point {} refused, out of memory: {}", made + 1, full);
break;
}
}
}
// After a reset the next request starts again at the front of `storage`.
buffer.Reset();
PrintLine("after reset, {} bytes left", buffer.BytesRemaining());
let again = allocator.Allocate(layout)?;
PrintLine("starts at storage[0]: {}", (again as uint) == (@storage[0] as uint));
}
Besides Io, its Rux.toml lists Allocator under [Dependencies].
Run it
cd Examples/Memory/FixedBuffer
rux run
point 1 at (1, 1), 48 bytes left
point 2 at (2, 4), 32 bytes left
point 3 at (3, 9), 16 bytes left
point 4 at (4, 16), 0 bytes left
point 5 refused, out of memory: true
after reset, 64 bytes left
starts at storage[0]: true
Common mistakes
Try it yourself
- Make
storage100 bytes, and passsizeof(byte[100])as the capacity. How many points fit, and how many bytes are left over? - Before the loop, allocate one
int32withLayout::ForValue<int32>(). How many points fit now, and where did the rest of the bytes go? - Move the loop into
func CountPoints(allocator: Allocator) -> int64and call it with the fixed buffer's handle. The function never learns which allocator it was given.