segment — refcounted bytes (L0↔L1)

In one paragraph

A tr::view::segment_t is real bytes owned by a backend, plus an intrusive atomic refcount — the boundary object where L0’s bytes acquire L1’s ownership. The tr::view::segment_ptr_t handle threads that one buffer’s lifetime through fan-out: copying a handle is a relaxed increment (a clone), dropping the last one reclaims the bytes through the backend. This is what lets many views share one buffer with no copies, and what makes a decoded TLV safe to hold past the receive call.

What it does

L0 is “real bytes in real memory owned by some real allocator”; L1 adds the ownership. segment_t is the control block over one such buffer and the single sanctioned object on that boundary — L0 backends vend it, L1 views hold it, and no other type crosses. segment_ptr_t is the owning handle. The refcount lives inside the segment (not in a side shared_ptr block) so a static MMIO descriptor, a pool slot, and a heap allocation all carry their own count.

A segment is never copied or moved; it is always handled through segment_ptr_t, and it caches its backend’s address space and module-set tag at construction (segment_t, core/include/libtracer/segment.hpp). When the last handle drops, segment_ptr_t::reset calls tr::mem::destroy_dispatch, which switches on that tag to a direct call for a linked backend and falls back to the backend’s virtual destroy for any other — the result is identical to seg->backend->destroy(seg) for every backend (core/include/libtracer/backend.hpp:251-260; ADR-0047 — build-time-closed module sets, compile-time seams §2). There is no separate release() step.

The atomic orderings are the canonical intrusive_ptr pattern, specified once in reference/02 §required atomic operations: increment relaxed (core/include/libtracer/segment.hpp:52 — the caller already holds a reference, so the data dependency travels through it), decrement acq_rel (:53-55 — release the writes before another thread observes the count drop, acquire on observing the drop to zero), inspect acquire (:56-58). The decrement returns the value before it, so a return of 1 identifies the caller that dropped the last reference.

LIBTRACER_NO_ATOMIC replaces the atomic with a plain uint_least32_t for single-threaded and Cortex-M0/M0+ targets that have no LDREX/STREX (core/include/libtracer/segment.hpp:21,44). It is a compile definition, not a CMake option: the constrained-target footprint build sets it (tools/cortexm0_footprint.py:158) and the substrate test is built a second time with it (core/tests/CMakeLists.txt:1727,1742-1743).

API reference

struct segment_t

A refcounted span of real bytes: the L0↔L1 boundary object.

Real bytes + the backend that reclaims them + an intrusive refcount. Never copied or moved (the atomic refcount pins it in place); always handled through segment_ptr_t.

Note

bytes is writable at the type level, but whether writes are legal is the backend’s contract — a const/ROM borrow must not be written through (see mem_borrowed.hpp).

Public Functions

inline segment_t(mem::mem_backend_t *b, std::span<std::byte> by, std::uint_least32_t initial = 1) noexcept

Construct a segment over by, reclaimed by b, with initial refcount.

The address space and module-set tag are taken from the backend (b->space() / b->tag()); a DEVICE segment must not be CPU-dereferenced (docs/adr/0024).

Public Members

detail::ref_count_t refcount

Intrusive refcount (spec orderings).

mem::mem_backend_t *backend

Reclaimer; non-const (cache hooks mutate it).

std::span<std::byte> bytes

The backing bytes this segment holds a reference to.

mem::mem_space_t space

Address space (HOST/DEVICE), inherited from backend.

mem::backend_tag btag

Module-set tag, inherited from backend (ADR-0047 §2).

class segment_ptr_t

Intrusive owning handle for a segment_t.

Copy = clone (refcount bump, relaxed); destruction = release (acq_rel); the backend’s destroy fires when the last handle drops. This is what makes a borrowed (zero-copy) view safe to hold: the spans in a decoded TLV stay valid as long as the view — and thus this handle — lives.

Public Functions

inline segment_ptr_t(const segment_ptr_t &other) noexcept

Clone — a new shared reference to the same segment (relaxed increment).

inline segment_ptr_t(segment_ptr_t &&other) noexcept

Transfer ownership of other's reference, leaving it empty.

inline segment_ptr_t &operator=(segment_ptr_t other) noexcept

Copy-and-swap assignment — one operator covers copy- and move-assign.

inline void reset() noexcept

Drop this reference (acq_rel); fires the backend’s destroy at zero.

inline segment_t *get() const noexcept

The raw segment pointer (borrowed — no ownership transfer).

inline segment_t &operator*() const noexcept

Dereference to the owned segment.

inline segment_t *operator->() const noexcept

Member access on the owned segment.

inline explicit operator bool() const noexcept

True when this handle owns a segment.

inline std::uint_least32_t use_count() const noexcept

Current refcount — debug / metrics only (acquire load), NOT a sync primitive.

Public Static Functions

static inline segment_ptr_t adopt(segment_t *seg) noexcept

Adopt an existing reference (e.g. from alloc, refcount = 1) WITHOUT bumping.

static inline segment_ptr_t retain(segment_t *seg) noexcept

Take a NEW shared reference to an already-live segment (bumps the count).

The handle-producing conveniences live in tr::view rather than with the backends, because what they produce is an L1 handle:

segment_ptr_t tr::view::heap_alloc(std::size_t size)

Allocate a fresh, owned heap segment of size bytes, wrapped in an adopting segment_ptr_t.

An L1 helper (it produces an owning handle), so it lives in tr::view, not tr::mem (docs/adr/0016 §2). Exactly segment_alloc over mem::heap_backend.

Return values:

{} – An empty handle on allocation failure.

inline segment_ptr_t tr::view::borrow(std::span<std::byte> bytes)

Wrap writable caller-owned bytes in a segment without owning them.

An L1 handle producer (docs/adr/0016 §2). The caller guarantees the bytes outlive every view that holds them.

inline segment_ptr_t tr::view::borrow_const(std::span<const std::byte> bytes)

Wrap read-only caller-owned bytes (ROM, a const table, an MMIO read view).

The span is const; libtracer never writes through a borrowed-const segment, so the const_cast only restores the segment’s uniform writable-at-the-type- level base.

inline segment_ptr_t tr::view::borrow_device(std::span<std::byte> bytes)

Wrap caller-owned bytes as a DEVICE-space (non-CPU) segment.

The resulting view reports view_t::is_device; the codec must not CPU-dereference it (docs/adr/0024). A real device backend lives in the backends/ tier and registers its own byte-move (register_device_backend); this borrow tags existing memory DEVICE (e.g. for tests or a custom binding), registers nothing, and so mem::transfer refuses it.

segment_ptr_t tr::view::cuda_alloc(std::size_t size)

Allocate a CUDA device segment of size bytes (DEVICE space).

An L1 handle producer (docs/adr/0016 §2). The bytes live in GPU memory; the resulting view reports view_t::is_device and must not be CPU-dereferenced.

Return values:

{} – An empty handle if cudaMalloc fails.

Refcount lifecycle (fan-out)

        sequenceDiagram
    participant TX as producer
    participant V as views
    participant S1 as subscriber 1
    participant S2 as subscriber 2
    participant B as backend
    TX->>V: make segment (count=1)
    V->>S1: clone (relaxed ++ → 2)
    V->>S2: clone (relaxed ++ → 3)
    TX->>V: drop producer ref (acq_rel -- → 2)
    S1->>V: release (acq_rel -- → 1)
    S2->>V: release (acq_rel -- → 0)
    V->>B: destroy_dispatch(seg) — bytes reclaimed
    

Consequences

  • Zero-copy fan-out — N subscribers share one buffer; delivery is N relaxed increments, no memcpy.

  • A decoded TLV outlives its receive call — a tlv_t borrows segment bytes via spans; the segment_ptr_t keeps them alive exactly as long as some view needs them, which is what makes borrowed (zero-copy) decode safe at all.

  • No hidden allocation — the count is in the segment, so MMIO, pool and borrowed segments need no separate control block.

  • Reclaim is devirtualizable — the cached module-set tag turns per-release reclaim into a switch, foldable to one direct call on a target that links a single backend.

  • Portable to cores without atomicsLIBTRACER_NO_ATOMIC drops to a plain counter where the application guarantees no cross-thread sharing.

Pitfalls

  • adopt and retain are not interchangeable. adopt takes over an existing reference without bumping — the shape mem_backend_t::alloc returns (a raw segment_t* at refcount 1); retain adds a new reference to an already-live segment (segment.hpp:116,120). Adopting a segment twice double-frees it; retaining an alloc result leaks it, because the reference alloc already created is never dropped.

  • use_count is not a synchronization primitive. It is an acquire load for debug and metrics (segment.hpp:154-157). A count of 1 does not mean no other thread is about to clone the handle, and branching on it reintroduces the race the refcount exists to remove.

  • LIBTRACER_NO_ATOMIC is an application promise, not a portability switch. With a plain counter, one cross-thread clone or release races the count and corrupts the lifetime silently. Set it only where the application serializes all access to segments.

  • bytes is writable at the type level; legality is the backend’s contract. A borrow over ROM or a caller’s const buffer hands out a mutable std::span<std::byte> all the same (segment.hpp:73-76,81); writing through it is undefined even though it compiles.

  • A DEVICE segment must not be CPU-dereferenced. The span looks ordinary, but space records that the bytes are not CPU-addressable (segment.hpp:82, backend.hpp:57-68); such a segment may back only an opaque VALUE payload, with header and trailer kept in HOST segments (ADR-0024 — mem_cuda GPU backend, heterogeneous rope).

See: backends (who creates segments), views (who holds them), and the interface map.