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

destroy does

Use

mem_heap

malloc’d bytes

frees bytes + control block

hosted targets

mem_borrowed

nothing (your bytes)

frees only the control block

live/raw, MMIO header, ROM

mem_pool

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 size bytes (refcount = 1).

The returned segment is the caller’s to adopt via tr::view::segment_ptr_t::adopt. A raw segment_t* is returned, not a segment_ptr_t, to keep L0 from naming L1’s owning handle (docs/adr/0016 §2). Allocation-incapable substrates (MMIO, FIFOs) leave this default and return nullptr.

Parameters:

hint – Backend-private allocation hint; NONE for “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_t control block) and nothing it does not — a borrowed backend never frees the user’s bytes. Invoked by segment_ptr_t at 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 dir so 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 dir so 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 DEVICE backend (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).

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.

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 size argument.

Values:

enumerator NONE

“Don’t care” — the default for every alloc call.

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_t control block plus slot_payload usable bytes, payload aligned to align (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_t of size bytes.

Return values:

nullptrsize exceeds the slot payload, or the pool is exhausted.

virtual void destroy(view::segment_t *seg) noexcept override

Return seg's slot 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/destroy are 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.

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 constructionmem_pool makes memory use exactly the caller’s slab; exhaustion is a return value, not an OOM.

  • Zero-copy live datamem_borrowed points a segment at bytes you already have (a register, a program variable) with no copy and no CRC imposed.

See: segment, views, interface map.