The failable block seam (L0 substrate)

tr::mem::block_source_t is the seam every allocation a peer can provoke draws from (reference 09 §the second L0 seam). It is three members wide — a nothrow try_alloc(bytes, align), a sized release(p, bytes, align), and a name() — and its entire failure vocabulary is nullptr. There is no throwing spelling, no fallback to the global heap, and no abort(); the caller turns the nullptr into whatever reject its own operation owns.

The example shows both sides of the seam, because both are the reader’s: the default source a node gets for free, and a source of one’s own, which is how a deployment states its bound.

What to notice

  • The type exists because std::pmr::memory_resource structurally cannot do this. That type’s allocate is annotated returns_nonnull in libstdc++, so a caller’s if (p == nullptr) is undefined-behaviour-deletable — and inspecting riscv32-esp-elf-g++ 15.2.0 with the deployment flags shows the branch surviving at -O0-O3 and gone at -Os/-Oz, which is the level an ESP-IDF node ships at (ADR-0065). A seam whose failure signal disappears at exactly the optimization level the target uses is not a seam.

  • Nothrow is a compile-time property, asserted as one. Both halves are noexcept, and the example static_asserts it. On the shipping profile a throw reaches ESP-IDF’s link-wrapped __cxa_throwabort() stub, so “it rarely throws” is not a weaker version of this guarantee — it is a peer-reachable reboot.

  • release is sized, and that is load-bearing. The caller hands back the (bytes, align) it asked for, so a bump or pool source needs no per-block header at all — the property mem_pool_source turns into exact packing.

  • A source names itself. A bounded node’s operator watching a refusal needs to know which seam ran out; name() is that, and it costs one borrowed literal.

  • Implementing one is the point. The whole extension surface is two overrides. The budget_source_t in the example is ~15 lines and is also the shape a test uses to inject a failure deliberately.

  • The refusal is provoked on a budgeted source, never on the platform heap. A request the real allocator cannot serve is a sanitizer’s fatal error, not a nullptr, so an example that asked the heap for an impossible block would fail the ASan leg rather than demonstrate anything.

  • Nothing here is conditional — the target builds and runs under every CI leg.

Source

  1/*
  2 * SPDX-License-Identifier: Apache-2.0
  3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
  4 */
  5
  6/**
  7 * @file
  8 * @brief ONE CONCEPT — the failable block seam: exhaustion arrives as a VALUE.
  9 *
 10 * `tr::mem::block_source_t` is the L0 seam every allocation a PEER can provoke draws from
 11 * (ADR-0065). It is three members wide: a nothrow `try_alloc(bytes, align)`, a SIZED
 12 * `release(p, bytes, align)`, and a `name()`. There is no throwing spelling and no fallback
 13 * to the global heap — `nullptr` is the entire failure vocabulary, and the caller turns it
 14 * into whatever reject its own operation owns.
 15 *
 16 * Both sides are shown here, because both are the reader's: `heap_source()` is the default
 17 * a node gets for free, and a source of one's own is how a deployment states its bound. The
 18 * refusal is provoked on a source with a budget rather than on the platform heap — an
 19 * allocation the real allocator cannot serve is a sanitizer's fatal error, not a `nullptr`
 20 * (`core/tests/mem_source_test.cpp`).
 21 *
 22 * Runs under ctest as `example_mem_block_source`; returns non-zero on any failed check.
 23 */
 24
 25#include <cstddef>
 26#include <cstdint>
 27#include <cstdio>
 28#include <cstring>
 29#include <new>
 30
 31#include "libtracer/mem_source.hpp"
 32
 33namespace {
 34
 35/** @brief Report expectation @p what and record a failure on @p ok. */
 36void check(bool& ok, bool cond, const char* what) {
 37    std::printf("  [%s] %s\n", cond ? "ok" : "FAIL", what);
 38    ok = ok && cond;
 39}
 40
 41/**
 42 * @brief A source of one's own: serve a fixed number of blocks, then refuse — and record
 43 *        the `(bytes, align)` every reclaim arrived with.
 44 *
 45 * The whole seam is these two overrides. Implementing it is what a deployment does to make
 46 * "how much memory may this node use" its own property rather than the library's.
 47 */
 48class budget_source_t final : public tr::mem::block_source_t {
 49   public:
 50    /** @brief Serve at most @p budget blocks. */
 51    explicit budget_source_t(int budget) noexcept
 52        : tr::mem::block_source_t("budget"), left_(budget) {}
 53
 54    /** @brief Serve while the budget lasts; `nullptr` — never a throw — once it does not. */
 55    [[nodiscard]] void* try_alloc(std::size_t bytes, std::size_t align) noexcept override {
 56        if (left_ <= 0) return nullptr;
 57        --left_;
 58        return ::operator new(bytes, std::align_val_t{align}, std::nothrow);
 59    }
 60
 61    /** @brief Sized reclaim — @p bytes and @p align are the ones @ref try_alloc was asked for. */
 62    void release(void* p, std::size_t bytes, std::size_t align) noexcept override {
 63        last_bytes_ = bytes;
 64        last_align_ = align;
 65        ::operator delete(p, bytes, std::align_val_t{align});
 66    }
 67
 68    std::size_t last_bytes_ = 0; /**< @brief Size the most recent reclaim carried. */
 69    std::size_t last_align_ = 0; /**< @brief Alignment the most recent reclaim carried. */
 70
 71   private:
 72    int left_; /**< @brief Blocks this source will still serve. */
 73};
 74
 75}  // namespace
 76
 77int main() {
 78    bool ok = true;
 79    tr::mem::block_source_t& heap = tr::mem::heap_source();
 80    std::printf("every source names itself; the process-wide default is \"%s\"\n", heap.name());
 81
 82    // The contract is a compile-time property first: neither half may throw, because the
 83    // shipping profile builds with -fno-exceptions and a throw there reaches an abort stub.
 84    static_assert(noexcept(heap.try_alloc(1, 1)), "try_alloc is nothrow");
 85    static_assert(noexcept(heap.release(nullptr, 1, 1)), "release is nothrow");
 86
 87    void* const block = heap.try_alloc(96, 64);
 88    check(ok, block != nullptr, "the default source serves a 96-byte block");
 89    check(ok, reinterpret_cast<std::uintptr_t>(block) % 64 == 0,
 90          "aligned to at least the requested boundary");
 91    std::memset(block, 0xA5, 96);  // writable for its whole length; there is no header to dodge
 92    heap.release(block, 96, 64);
 93
 94    budget_source_t budget{2};
 95    void* const first = budget.try_alloc(48, 8);
 96    void* const second = budget.try_alloc(48, 8);
 97    check(ok, first != nullptr && second != nullptr, "a bounded source serves within its budget");
 98    check(ok, budget.try_alloc(48, 8) == nullptr,
 99          "past it the answer is nullptr — never a throw, never an abort()");
100    check(ok, std::strcmp(budget.name(), "budget") == 0,
101          "and it names itself, for a census that has to say WHICH seam ran out");
102
103    // SIZED reclaim: the caller hands back the size and alignment it asked for, which is what
104    // lets a bump or pool source carry no per-block header at all (see mem_pool_source).
105    budget.release(first, 48, 8);
106    budget.release(second, 48, 8);
107    check(ok, budget.last_bytes_ == 48 && budget.last_align_ == 8,
108          "release carries the ORIGINAL (bytes, align) — the source stores no header to recover "
109          "them from");
110    return ok ? 0 : 1;
111}

See also: backends · memory substrate reference · the two L0 seams.