backends — the allocator seam (L0)¶
In one paragraph
tr::mem::mem_backend_t is a small, user-implementable interface: subclass it
to bind libtracer to any memory — a heap, a fixed arena, live registers, a DMA
ring. libtracer never allocates on its own; it asks a backend. Three ship today:
mem_heap (owns malloc’d bytes), mem_borrowed (wraps your bytes, frees
nothing), and mem_pool (a bounded fixed-slab, alloc-or-null).
What it does¶
The protocol treats application data as opaque, and mem_backend_t extends that
to the memory plane: libtracer is a transparent byte router ([ADR-0012] in the
repo). A backend declares its own per-architecture contract (alignment, cache
hooks, ISR-safety) and owns reclamation; the layers above see only segment_ts. The
interface deliberately makes allocation optional (alloc may return nullptr)
because many substrates — MMIO, hardware FIFOs — cannot allocate at all.
Backend |
Owns |
|
Use |
|---|---|---|---|
|
malloc’d bytes |
frees bytes + control block |
hosted targets |
|
nothing (your bytes) |
frees only the control block |
live/raw, MMIO header, ROM |
|
a caller slab |
returns the slot to a free list |
bounded / MCU / deterministic |
mem_pool is the bounded “custom allocator”: it carves a caller-owned slab
into fixed slots with the free list threaded through the slab (no auxiliary
heap), and returns nullptr when full — the BACKPRESSURE signal.
The seam lives at L0 (tr::mem); the segments it produces are owned at L1
(tr::view). A backend constructs and reclaims tr::view::segment_t (the one
sanctioned L0↔L1 boundary type, [ADR-0016] §2) and alloc returns a raw
segment_t* — the caller adopts it with tr::view::segment_ptr_t::adopt. The
handle-producing conveniences heap_alloc / borrow / borrow_const therefore
live in tr::view, not here.
Interface¶
Generated from the
core/headers by Doxygen — these are the reference implementation’s own declarations, not a hand-maintained copy.
-
class mem_backend_t¶
A memory backend: the L0 seam libtracer binds any substrate behind.
Subclass this to bind libtracer to any allocator — a heap, a fixed caller-owned arena, live registers, lwIP pbufs, DMA descriptors. The interface deliberately does not make allocation mandatory: many substrates cannot allocate (MMIO, hardware FIFOs), so alloc may return
nullptr.Note
Each backend declares its own concurrency/ISR-safety contract; the protocol mandates none (docs/adr/0012).
Subclassed by tr::mem::detail::borrowed_backend_t, tr::mem::detail::borrowed_device_backend_t, tr::mem::heap_backend_t, tr::mem::pool_t
Public Functions
-
inline explicit mem_backend_t(const char *name) noexcept¶
Construct a backend with a stable, human-readable
name(e.g. “mem_heap”).
-
inline virtual view::segment_t *alloc(std::size_t size, alloc_hint_t hint = alloc_hint_t::NONE)¶
Allocate a fresh segment of at least
sizebytes (refcount = 1).The returned segment is the caller’s to adopt via
tr::view::segment_ptr_t::adopt. A rawsegment_t*is returned, not asegment_ptr_t, to keep L0 from naming L1’s owning handle (docs/adr/0016 §2). Allocation-incapable substrates (MMIO, FIFOs) leave this default and returnnullptr.- Parameters:
hint – Backend-private allocation hint;
NONEfor “don’t care”.- Return values:
nullptr – Backpressure (pool exhausted / OOM) or allocation unsupported.
-
virtual void destroy(view::segment_t *seg) noexcept = 0¶
Reclaim a segment whose refcount has reached zero (the only reclaim path).
Frees whatever the backend owns (the bytes and/or the
segment_tcontrol block) and nothing it does not — a borrowed backend never frees the user’s bytes. Invoked bysegment_ptr_tat zero, never by user code.Warning
Never called on a live segment.
-
inline virtual void before_io(view::segment_t*, io_dir_t) noexcept¶
Cache prep before handing the segment to a DMA transfer.
Clean or invalidate per
dirso the device sees coherent memory. No-op by default and on cacheless cores (Cortex-M0/M3/M4); only DMA-class backends override it (docs/reference/09 §cache coherency).
-
inline virtual void after_io(view::segment_t*, io_dir_t) noexcept¶
Cache reconcile after a DMA transfer completes.
Invalidate per
dirso the next CPU reader sees HW’s writes. No-op by default and on cacheless cores.
-
inline virtual std::size_t alignment() const noexcept¶
The alignment (bytes) this backend guarantees for allocated bytes.
-
inline virtual std::size_t max_segment_size() const noexcept¶
The largest single segment this backend can produce.
-
inline virtual mem_space_t space() const noexcept¶
The address space this backend’s segments live in (default
HOST).A
DEVICEbackend (e.g.mem_cuda) must override this; segments inherit it (segment.hpp), and the codec uses it to skip CPU access to device links.
-
inline virtual backend_tag tag() const noexcept¶
The build-time-closed module-set tag (default
UNKNOWN, ADR-0047 §2).A backend that participates in the fast destroy dispatch overrides this to return its backend_tag; segments read it once at construction (like space). A backend that leaves the default is dispatched through its virtual
destroy.
-
inline const char *name() const noexcept¶
The backend’s stable identifier (e.g. for introspection / metrics).
-
inline explicit mem_backend_t(const char *name) noexcept¶
The DMA/allocation enums the seam uses:
-
enum class tr::mem::io_dir_t : std::uint8_t¶
Direction of a DMA / cache-coherency transfer, for the cache hooks.
The hook method carries the timing (before/after the transfer); this enum carries the direction; the backend maps the pair to clean/invalidate.
Values:
-
enumerator DEVICE_TO_CPU¶
After DMA-in: invalidate so the CPU reads HW’s writes.
-
enumerator CPU_TO_DEVICE¶
Before DMA-out: clean so HW reads the CPU’s writes.
-
enumerator DEVICE_TO_CPU¶
-
enum class tr::mem::alloc_hint_t : std::uint32_t¶
Opaque, backend-private allocation hint.
A hint’s meaning is private to the backend that defines it: there is no cross-backend hint registry, no two backends share a value’s meaning, and a hint-ignoring backend accepts any value (docs/adr/0016 §”Considered options”). This strong typedef also stops a hint being swapped for a
sizeargument.Values:
-
enumerator NONE¶
“Don’t care” — the default for every
alloccall.
-
enumerator NONE¶
The bounded reference backend:
-
class pool_t : public tr::mem::mem_backend_t¶
A fixed-slot allocator over a caller-owned slab;
alloc-or-nullptr.Carves the slab into equal slots with the free list threaded through the slab (no auxiliary heap), so memory use is exactly the caller’s slab and exhaustion is a return value, not an OOM. The deterministic MCU choice.
Public Functions
-
pool_t(std::span<std::byte> slab, std::size_t slot_payload, std::size_t align = alignof(std::max_align_t)) noexcept¶
Carve
slab(caller-owned; must outlive the pool) into slots.Each slot holds a
segment_tcontrol block plusslot_payloadusable bytes, payload aligned toalign(a power of two). The slot count is whatever fits after aligning the slab base.
-
virtual view::segment_t *alloc(std::size_t size, alloc_hint_t hint = alloc_hint_t::NONE) override¶
Hand out the next free slot as a
segment_tofsizebytes.- Return values:
nullptr –
sizeexceeds the slot payload, or the pool is exhausted.
-
virtual void destroy(view::segment_t *seg) noexcept override¶
Return
seg'sslot to the free list (placement-destroying it).
-
inline virtual std::size_t alignment() const noexcept override¶
The alignment (bytes) this backend guarantees for allocated bytes.
-
inline virtual std::size_t max_segment_size() const noexcept override¶
The largest single segment this backend can produce.
-
inline virtual backend_tag tag() const noexcept override¶
The build-time-closed module-set tag (default
UNKNOWN, ADR-0047 §2).A backend that participates in the fast destroy dispatch overrides this to return its backend_tag; segments read it once at construction (like space). A backend that leaves the default is dispatched through its virtual
destroy.
-
inline std::size_t capacity() const noexcept¶
Total slots.
-
inline std::size_t available() const noexcept¶
Free slots.
Public Static Attributes
-
static constexpr bool needs_cache_ops = false¶
No DMA cache maintenance (plain RAM slab).
-
static constexpr bool is_isr_safe = true¶
alloc/destroyare O(1) free-list ops — no heap, no syscall.
-
static constexpr bool owns_bytes = true¶
Bytes are backend-managed (freed only on
destroy) — durably storable.
-
pool_t(std::span<std::byte> slab, std::size_t slot_payload, std::size_t align = alignof(std::max_align_t)) noexcept¶
The seam¶
classDiagram
class mem_backend_t { <<interface>> +alloc() +destroy() +before_io() +after_io() +alignment() }
mem_backend_t <|-- heap_backend_t
mem_backend_t <|-- borrowed_backend_t
mem_backend_t <|-- pool_t
mem_backend_t <|-- YourBackend
segment_t --> mem_backend_t : backend*
note for YourBackend "bind a DMA ring,\nlwIP pbuf, MMIO, …"
Benefits¶
Don’t-limit-the-user — the same protocol runs on a heap, a 4 KB MCU pool, or a live register; you pick the point on the spectrum.
Bounded by construction —
mem_poolmakes memory use exactly the caller’s slab; exhaustion is a return value, not an OOM.Zero-copy live data —
mem_borrowedpoints a segment at bytes you already have (a register, a program variable) with no copy and no CRC imposed.
See: segment, views, interface map.