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_resourcestructurally cannot do this. That type’sallocateis annotatedreturns_nonnullin libstdc++, so a caller’sif (p == nullptr)is undefined-behaviour-deletable — and inspectingriscv32-esp-elf-g++ 15.2.0with the deployment flags shows the branch surviving at-O0…-O3and 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 examplestatic_asserts it. On the shipping profile a throw reaches ESP-IDF’s link-wrapped__cxa_throw→abort()stub, so “it rarely throws” is not a weaker version of this guarantee — it is a peer-reachable reboot.releaseis 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 propertymem_pool_sourceturns 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_tin 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.