decode_into: a flat arena drawn from a stack slab (L2/L3 codec)¶
decode returns an owning tlv_t whose children vectors allocate on the global heap by
construction — fine on a host, wrong at a terminus that must not touch the heap on the receive
path. wire::decode_into
(ADR-0041)
answers the identical grammar with a flat, pre-order arena_tlv_t array whose storage comes
from an injected block source (CONTEXT.md §Block source). Point that at
a bump_source_t over a std::array and the whole decode allocates nothing.
What to notice¶
The arena holds structure; the bytes stay where they were. Every span in a node borrows the input buffer, which must outlive the arena. The example asserts a node’s
bodypoints into the frame, not beside it.Navigation is index arithmetic. Children of node
ibegin ati + 1, andendis one past the last descendant — sonext_siblingis a single load, and skipping a subtree costs the same as reading one field. No pointer chasing, no per-node allocation.null_source()upstream makes the slab a hard bound. Abump_source_tspills to its upstream once full; passing the source that serves nothing means an overflowing frame is refused (TLV_NESTING_TOO_DEEP— see decode refusals) instead of quietly reaching the heap. That is the seam doing its job: exhaustion by value, never a throw and never anabort()on a-fno-exceptionstarget.A bump source is scope-lifetime storage. Blocks are never individually reclaimed, so it is constructed per operation (or
resetbetween them) — not wired as a long-lived receiver seam, where it would monotonically fill and then refuse everything.This is a resolve-scoped object. Read it, take the ownership copies you need, drop it. Storing a borrowed span is the one thing the ADR-0041 contract forbids.
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 — `decode_into`: the same frame as a flat arena, drawn from a stack slab.
9 *
10 * `decode` returns an OWNING `tlv_t` whose `children` vectors allocate on the global heap by
11 * construction. `wire::decode_into` (ADR-0041) answers the same grammar with a flat,
12 * pre-order `arena_tlv_t` array whose storage comes from an injected
13 * `tr::mem::block_source_t` — point that at a `bump_source_t` over a stack buffer and the
14 * whole decode touches no heap. Every span in the arena borrows the input buffer; the arena
15 * holds structure only, never bytes.
16 *
17 * Navigation is index arithmetic rather than pointer chasing: children of node `i` start at
18 * `i + 1`, and `end` is one past the last descendant, so `next_sibling` is a single load.
19 *
20 * Runs under ctest as `example_wire_arena_decode`; returns non-zero on any failed check.
21 */
22
23#include <array>
24#include <cstddef>
25#include <cstdint>
26#include <cstdio>
27#include <span>
28#include <vector>
29
30#include "libtracer/mem_source.hpp"
31#include "libtracer/tracer.hpp"
32
33namespace {
34
35using tr::wire::tlv_t;
36using tr::wire::type_t;
37
38/** @brief Report expectation @p what and record a failure on @p ok. */
39void check(bool& ok, bool cond, const char* what) {
40 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
41 ok = ok && cond;
42}
43
44} // namespace
45
46int main() {
47 bool ok = true;
48 const std::vector<std::byte> x{std::byte{0x11}, std::byte{0x22}};
49 const std::vector<std::byte> y{std::byte{0x33}, std::byte{0x44}};
50
51 tlv_t point;
52 point.type = type_t::POINT;
53 point.opt.pl = true;
54 point.children.push_back(tlv_t{.type = type_t::VALUE, .payload = std::span(x)});
55 point.children.push_back(tlv_t{.type = type_t::VALUE, .payload = std::span(y)});
56 const std::vector<std::byte> frame = tr::wire::encode(point);
57
58 // The whole decode's storage: one stack buffer. null_source() upstream makes the buffer
59 // the HARD bound — an overflowing frame is refused, never quietly served from the heap.
60 std::array<std::byte, 1024> slab{};
61 tr::mem::bump_source_t bump{slab, tr::mem::null_source()};
62
63 const auto arena = tr::wire::decode_into(frame, bump);
64 check(ok, arena.has_value(), "the frame decodes into the caller's slab");
65 if (!arena) return 1;
66
67 std::printf("%zu nodes, %zu slab bytes used, zero heap allocations\n", arena->size(),
68 bump.used());
69 check(ok, arena->size() == 3, "root plus two children, flat and pre-order");
70 check(ok, arena->root().type == type_t::POINT, "index 0 is the root");
71
72 const std::uint32_t first = tr::wire::tlv_arena_t::first_child(0);
73 check(ok, first == 1 && (*arena)[first].type == type_t::VALUE, "a child starts at i + 1");
74 const std::uint32_t second = arena->next_sibling(first);
75 check(ok, second == 2 && (*arena)[second].type == type_t::VALUE,
76 "and its sibling is one load away — next_sibling IS the subtree end");
77 check(ok, arena->next_sibling(second) == arena->root().end,
78 "walking off the last child lands exactly on the parent's end");
79 const std::span<const std::byte> body = (*arena)[second].body;
80 check(ok, body.data() == frame.data() + (frame.size() - body.size()),
81 "and the node's body is a span INTO the input buffer — no bytes were copied");
82 return ok ? 0 : 1;
83}
See also: frame codec · backends · memory substrate reference.