memory-region · API

memory_region.bump

A cursor that only moves forward.

BumpAllocator holds one number — how far into the region it has got — and allocating is "give me where the cursor is, then move it on by n". No free list, no search, no per-allocation header: two instructions. The price is that an individual piece can never be freed, which is the usual arena trade and suits data that is born together and dies together.

Offsets, not addresses

claim returns an offset from the start of the region, and anything stored inside a region refers to the rest of it by offset. A shared mapping lands at a different address in every process that maps it, so an address written into it means something only to the process that wrote it; a reader adds its own base() and everything resolves.

span is how to read and write what has been claimed: a typed view that borrows the allocator, so the compiler knows when it stops being valid. unsafe_address is the way back to a raw address, for the moment one has to be written into a C structure whose consumer is this process.

Structs

BumpAllocator

struct BumpAllocator[R: Region & Deinitable]

Hands out 8-byte-aligned offsets into a region, front to back.

Arrow wants every buffer 8-byte aligned — consumers cast them in place — so alignment is the allocator's job rather than each caller's.

Implements Movable

Fields
region: R
used: Int: How far the cursor has moved: the region's occupied prefix.

__init__

def __init__(out self, var region: R)

claim

def claim(mut self, n: Int) -> Int

Reserve n bytes and return their offset from the base.

Raises rather than growing: a region is a fixed range, and nothing that has already handed out an offset could survive being moved.

append

def append[dtype: DType, //](mut self, data: Span[Scalar[dtype]]) -> Int

Claim room for data, copy it in, and return where it went.

Any scalar element type, so a buffer of Int32 offsets goes in as itself rather than as an address and a byte count.

span

def span[dtype: DType](ref self, offset: Int, count: Int) -> Span[Scalar[dtype], origin_of(self)]

count values of dtype at offset, as a view of this allocator.

The view borrows the allocator: writable when the allocator is reached through mut, read-only otherwise, and unusable once it is closed, moved or dropped — all checked by the compiler. Claiming more while a view is alive is fine, because claim never moves anything.

The offset is one this allocator handed out, so a bad one is a bug rather than bad input: it is a debug_assert against what has been claimed, not a raise.

unsafe_ptr

def unsafe_ptr(self, offset: Int) -> Pointer[UInt8, MutUntrackedOrigin]

A writable pointer to offset, valid in this process only.

unsafe_address

def unsafe_address(self, offset: Int) -> Int

offset as an address in this process.

The only place an address belongs: writing one into a C structure whose consumer is this process. Anything a peer will read stores the offset instead.

close

def close(deinit self)

Give the region back now; it would go at end of scope anyway.

into_raw

def into_raw(deinit self) -> Int

Give up ownership of the region; returns its base address.

What an export does when the bytes become the consumer's — the Arrow C Data Interface release callback frees them later.