config — the build’s named traits type¶
In one paragraph
Every compile-time knob a libtracer build has is a member of one named type,
tr::graph::default_config_t — sizes, padding widths, and the two policies the
build selects (which ACL evaluator, which last-known-value slot). An application
picks its configuration by making tr::graph::config_t name its own traits type,
once, app-wide; every loose spelling in the library is derived from that alias.
A second reader, tr::wire::config_reader_t, is the runtime counterpart: typed
accessors over a config SETTINGS TLV that arrived from a peer.
What it does¶
Two different things are called “configuration” here, and keeping them apart is the point.
Build configuration is default_config_t. It is not a set of macros and not
a template parameter: it is a struct whose members are static constexpr values
and nested type aliases, bound once by an alias. Being one named entity is what
makes a configuration diffable, passable to a test, and assertable on — a scatter
of independent #defines can only be read one at a time. It is bound per build
rather than threaded through the API because threading it through produces
byte-identical machine code while forking the process-global stripe and hazard
tables, which costs exactly the RAM the configuration exists to save.
The knob-by-knob narrative — what each knob costs on which target, and which ones are optimization rather than correctness — is the configuration space. This page is the API surface.
Runtime configuration is config_reader_t. A connection’s settings arrive as
a SPEC config SETTINGS TLV: positional NAME-key / value pairs, string values as
NAME children and integers as VALUE children. All six transport-side consumers
used to hand-roll the same walk — the universal keys plus the tcp, ws, can, quic and
webtransport factories — and this class is their one home. Unknown keys are ignored so
a newer peer can send more than a receiver understands, a key whose value child
has the wrong type is ignored, and a repeated key resolves to its last
well-formed occurrence. Each factory still reads only its own keys — what is
shared is the walk, not the vocabulary.
The walk is pair-consuming: it advances a whole (key, value) pair at a
time, so an unknown key is skipped together with its value. That is what lets
the tolerance coexist with positional pairing. Scanning every offset instead made
the grammar ambiguous — a pair whose string value textually equalled a known key
(link_hint = "addr") had that value re-read as a key, binding the following
child as addr and, under last-wins, destroying a legitimate earlier one. A
child that is not a NAME where a key belongs desynchronizes the stream and the
walk stops there rather than guessing a resync.
Which keys those factories actually read — the universal set and each kind’s private one, with the wire value each takes — is connection config. This page is the walk; that page is the vocabulary.
config_reader_t is the one home for the pair walk itself, everywhere it is read
tolerantly. It lives in tr::wire
(#985) — the layer that owns
the grammar — with tr::net::config_reader_t kept as the transport plane’s alias, so
graph_t::create_child (the creation SPEC) and the SUBSCRIBER QoS SETTINGS parse at
L4 read through the same type rather than carrying hand-written copies of the rule.
The one deliberate exception is graph::parse_acl
(#906) — the same mechanics
under the opposite unknown-key ruling, which is to reject: an ACL is a security
document, and the two duplicate-key families
(#995) must not share code
that could drift one toward the other.
Declaring your own build configuration¶
Inherit, override what differs, and bind the alias in a header your build puts
ahead of libtracer/config.hpp on the include path:
struct my_node_config_t : tr::graph::default_config_t {
static constexpr std::size_t kCacheLineBytes = 0; // single-core: no false sharing
using lkv_slot_t = tr::graph::hazard_slot_t; // many-core: no shared pointer lock
};
using config_t = my_node_config_t;
Inheriting rather than copying means a knob added later inherits its new default instead of failing to compile.
The two policies¶
A policy is a type the configuration selects, not a runtime flag, so the branch it removes is not on the hot path at all.
ACL policy — acl_policy_t picks how an access decision is evaluated.
allow_only_policy_t is the MCU profile: ALLOW entries only, no ordering
question to answer. full_acl_policy_t is the host profile: ordered
first-match-per-bit, DENY included. See security & ACL for
what the entries mean.
LKV slot policy — lkv_slot_t picks how a vertex publishes and reads its
last-known value. sp_atomic_slot_t is the default: an
std::atomic<std::shared_ptr<const rope_t>> whose reclamation is the refcount,
so there is no scheme to implement and no registry to size. hazard_slot_t is
the many-core alternative: a lock-free atomic<node_t*> reclaimed with hazard
pointers, which removes the pointer-lock both load and store take in the
default slot. The trade is explicit — the hazard slot buys nothing at one thread,
costs a fixed registry sized by kHazardReaderSlots, and has a publish that can
fail under memory exhaustion, which the default slot cannot. Both return an
owning handle from load(); that is the contract a third policy would have to
satisfy.
Pitfalls¶
Bind the alias, do not edit the struct. Overriding
config_tis a supported target-configuration change; editingdefault_config_tin place puts your build on a fork of the library.kCacheLineBytes = 0is a legal value. It means “this target has no second core to false-share with”, and it is an optimization knob in both directions — never a correctness one.A
config_reader_tborrows. The reader and everystd::string_viewit returns point into the decoded TLV’s storage; both die with thetlv_t.Ignoring an unknown key is deliberate. A reader that rejects settings it does not recognize breaks forward compatibility with a newer peer. This is the opposite ruling from
parse_acl, which rejects an unknown key: an ACL is a security document, where silently dropping an attribute widens access. The tolerance is safe here only because the skip takes the whole pair.
API reference¶
-
struct default_config_t¶
The target’s build configuration, as ONE named type (ADR-0070).
Every compile-time knob is a member here, and the loose names below are DERIVED from it. That ordering is the point: the configuration is a single diffable entity an application can name, pass to a test, and assert on — rather than a scatter of independent declarations that can only be read one at a time.
It is bound once per build, not threaded as a template parameter. ADR-0070 records why, with measurements: threading it produces byte-identical machine code (verified across eight knob combinations, five optimization levels and two targets), so it buys no latency; its one unique capability — two configurations in one binary — would FORK the process-global stripe and hazard tables, costing exactly the RAM the configuration exists to save; and an app-declared traits type cannot reach the library’s out-of-line translation units anyway, so it would layer on this header rather than replace it.
Declaring your own. Write a
libtracer/config_override.hppand put its directory ahead ofcore/includeon the include path. This header includes it — after this struct, so the defaults are already visible to inherit from — and uses whatever config_t it binds:// libtracer/config_override.hpp #pragma once namespace tr::graph { struct my_node_config_t : default_config_t { static constexpr std::size_t kCacheLineBytes = 0; // single-core: no false sharing using lkv_slot_t = sp_atomic_slot_t; }; using config_t = my_node_config_t; } // namespace tr::graph
Inheriting from default_config_t means a knob added later does not break your preset — it inherits the new default instead of failing to compile. Stating only the differences is also what keeps the override honest: there is no second copy of the defaults to rot.
Public Types
-
using acl_policy_t = allow_only_policy_t¶
The target’s selected ACL policy (ADR-0047 §1 build-time module set).
Default: the ALLOW-only MCU profile. The CMake option
LIBTRACER_ACL_FULL=ONrebinds this to the fullsecurity_aclhost policy (ordered first-match-per-bit with DENY) — a target-configuration change, never an edit tograph.cpp.
-
using lkv_slot_t = sp_atomic_slot_t¶
The target’s selected LKV slot policy (ADR-0069 §1).
How a vertex publishes and reads its last-known value. Default:
sp_atomic_slot_t, thestd::atomic<std::shared_ptr<const rope_t>>libtracer has always used, whose reclamation is the refcount and whose registry cost is zero — the right choice for a write-dominated single-core node, and the reason a raw-Iconsumer builds what it always built.A many-core host is the case for rebinding this: today’s slot INVERTS under concurrent readers, and a reclamation scheme that does not serialize recovers roughly 4x of that at twenty-four readers (ADR-0069 §6 — the real path, not the model bench’s 20.8x). Override fragment:
using lkv_slot_t = hazard_slot_t;. The named type must satisfy the contract inlkv_slot.hpp— in particularload()returns an OWNING handle.
-
using reclaim_policy_t = reclaim_local_t¶
The target’s selected RECLAMATION policy (ADR-0080) — WHEN the library may free the memory behind a retired subscription’s
{fn, ctx}pair.Default:
reclaim_local_t, whose grace point is “this thread’s dispatch stack unwinds
to depth 0”. It is the default because it makes the MCU and the host build behave IDENTICALLY:
reclaim_strict_tforbids unsubscribing from inside a delivery, which would make the same application code legal on a host and illegal on the constrained target — a portability bug that surfaces only where it is hardest to debug.What the default costs, and what rebinding buys, is one non-atomic increment, decrement and branch per
fan_outon a per-thread counter — once per publish, regardless of subscriber count, and nothing at all on a publish nobody subscribed to (the counter sits afterfan_out’s no-subscriber gate). Override fragment:using reclaim_policy_t = reclaim_strict_t;— worth taking only where the deployment can show that re-entrant unsubscribe does not occur, becausereclaim_strict_tcannot see it outside a debug build.reclaim_qsbr— ADR-0080’s third policy, whose grace point spans EVERY thread — is the one to bind when this node dispatches from several threads at once and unsubscribes from another, the case neither policy above covers. Override fragment:using reclaim_policy_t = reclaim_qsbr_t;. It prices one relaxed load and oneseq_cststore per OUTERMOST fan-out (not per edge), and it is the only policy whose release hook may run on a thread other than theunsubscribe()caller — seereclaim.hpp.
Public Static Attributes
-
static constexpr std::size_t kVertexLockStripes = 16¶
The number of lock stripes shared by every vertex in the process (#361 §2).
Override fragment:
static constexpr std::size_t kVertexLockStripes = 8;; ESP-IDF: menuconfigCONFIG_LIBTRACER_VERTEX_LOCK_STRIPES, which writes exactly that line. A small single-core node reclaims RAM at 4-8.What N costs, precisely:
N * sizeof(vertex_stripe_t)bytes of.bssreserved at LINK time (plus the same for the condvar table) — the table is not lazy, whatever the platform does. What IS lazy is the platform primitive behind each handle: on FreeRTOS a stripe’s mutex costs ~90 B of heap on its first lock, so an untouched stripe costs its struct and no heap. Most of the struct is padding, and kCacheLineBytes decides how much.
-
static constexpr std::size_t kCacheLineBytes = 64¶
The target’s cache-line size for false-sharing padding — or 0 where false sharing cannot happen, because the target has no second core to share with.
libtracer pads the shared tables whose slots unrelated threads hit concurrently (the vertex lock stripes; the hazard domain’s cells and retire lists) up to this boundary, so two threads working on two slots never fight over one line. On a single-core node that padding buys nothing and the bytes are pure loss: measured on rv32 (
-Os, realcore/src/graph.cpp, GCC 15.2), 16 stripes cost 1,024 B of at 64 and 128 B at 0 — 896 B of a single-core node’s static RAM spent against a hazard it does not have.This is an OPTIMIZATION knob, never a correctness one: 0 on a multi-core target costs throughput under concurrent control-plane verbs and changes no observable behaviour. Override fragment:
static constexpr std::size_t kCacheLineBytes = 0;. The ESP-IDF component derives it fromCONFIG_FREERTOS_UNICORE— a unicore build has no second core by construction, so the right value is not a question the integrator should be asked.Values below a padded type’s natural alignment are raised to it, not applied ([dcl.align]/5 makes a reduction ill-formed, and GCC ignores it silently); each padded type static_asserts that the alignment it asked for is the one it got.
-
static constexpr std::size_t kHazardReaderSlots = 64¶
How many threads may hold a hazard announcement at once (ADR-0069 §3).
Read this as “threads that concurrently touch a vertex’s LKV” — readers announce, writers park displaced nodes, and both claim one index for the life of the thread. A per-target knob rather than a thread ceiling picked out of the air (RFC-0006); override fragment:
static constexpr std::size_t kHazardReaderSlots = 24;.Sizing: one index per such thread, and nothing at all unless lkv_slot_t is bound to
hazard_slot_t— the default binding never references the registry, so it is never emitted. Undersizing is not a correctness problem: threads past the bound share one reserved index under a spin lock, so they serialize with each other and with nobody else.What the domain costs when it IS bound, measured on rv32 at N = 64 (
-Os, realcore/src/graph.cpp, GCC 15.2) — the padding knob dominates it:registry
.bssTU
.bss+.sbss64
8,384 B
11,649 B
0
1,828 B
4,197 B
Note the third term the registry figure does not cover: binding the slot also pulls in roughly 2 KB of libstdc++
__waiter_pool_base.bss(theatomic::waitback-end), which is why the TU column is not just the registry plus the stripes.
-
static constexpr std::size_t kEdgePinSlots = 32¶
How many threads may hold an EDGE PIN at once (#635) — the per-participant announcement the fan-out snapshot claims while it copies a vertex’s published edge array out.
Read this as “threads that may publish (`graph_t::write`) concurrently”. Each claims one index for the life of the thread; the pin itself is held only across the copy-out, never across a dispatch. A per-target knob rather than a thread ceiling picked out of the air (RFC-0006); override fragment:
static constexpr std::size_t kEdgePinSlots = 24;.Correctness never depends on this number — only scaling does. A thread that finds every index taken falls back to copying the CURRENT array under the vertex stripe mutex, which is safe for the reason the mutex existed in the first place: displacing an array requires that same lock, so the current array cannot be retired underneath the fallback reader. Undersizing costs those threads exactly what every thread paid before #635.
Unlike kHazardReaderSlots this registry is ALWAYS emitted — the publish path is not a policy binding. Its
.bssisN * max(kCacheLineBytes, alignof(void*))bytes: at N = 32 that is 2,048 B on a host (64-byte padding) and 256 B on a single-core MCU profile, which sets kCacheLineBytes to 0 and has no second core to false-share against.
-
static constexpr std::size_t kMaxVertexBytes64 = 96¶
The RAM-diet RATCHET on
sizeof(vertex_t), 64-bit targets (#361 §8).Pinned to the size actually measured, with NO headroom, so the next byte added is a build failure. That is the point: the goal is a lean vertex, and a ceiling held above the measured size cannot express it. A ceiling only answers “did you regress past a
fixed point”; both 112 B and 96 B satisfied the old 120 B ceiling, so the 16 B the diet won after #380 §1 were invisible to the build and free to be spent again by anyone. Pinned to the measurement, every byte reclaimed is kept by construction.
History, newest first: 96 (measured across all three CI legs —
acl_fullOFF/ON and bothlkv_slot_tbindings agree), 112 post-#380 §1, 144 post-packing, 168 post-§3, 160 post-§2, 248 post-§1, 536 pre-split.It lives HERE, in the configuration, because it is a per-target budget — and it is enforced in
vertex.hppbeside the type it constrains, so every build on every target evaluates it under its own binding. It used to sit in a test, which meant it gated exactly one configuration and never the 32-bit one at all (no CI leg compiled that test cross-target, while the ESP-IDF legs compiledvertex_titself on every PR).Two rules follow from pinning it. RAISING one is a reviewed decision, never a way to make a build pass. LOWERING one is the routine half: a change that shrinks
vertex_tlowers the number in the same commit, or the gain is handed back to the next author.
-
static constexpr std::size_t kMaxVertexBytes32 = 72¶
The RAM-diet RATCHET on
sizeof(vertex_t), 32-bit (MCU) targets.Pointer-halved, and pinned to the measurement on the same terms as kMaxVertexBytes64 — measured 72 B on rv32 (
-Os -fno-exceptions -fno-rtti,rv32imac_zicsr_zifencei/ilp32), identical across all three configuration legs.This number was 80 with a note claiming rv32 was “exactly 80” and had zero headroom. It was 72 by then: the struct had shrunk and the prose had not, which is exactly the drift a pinned number cannot have — the assert is re-derived from a measurement, while a sentence describing the size is only as true as the day it was written.
Earlier: raised 72 -> 80 with the #380 §2 name-key SBO, whose inline buffer adds <= 8 struct bytes on 32-bit but deletes a ~32 B heap block per named vertex — and on this target the heap is what is actually scarce.
-
static constexpr std::uint32_t kPinPayloadRatio = 0¶
The RFC-0022 §3.D pin/copy amplification ratio — pin the written value as a subview of the inbound frame iff
payload_bytes * K >= segment_bytes(and the payload is trailer-less).Kis not a synthetic limit: both branches are correct andKselects which correct branch is cheaper. Pinning holds the whole owning RX segment for the value’s lifetime, soKbounds the waste at(K-1)xthe payload where an absolute byte threshold bounded it not at all.segment_bytesis the ALLOCATED size of the segment the pin would keep alive, not the length of the delivered frame view. Those differ by a lot on a real transport:udp_transport_treceives every datagram into akMaxDatagram-sized segment and delivers a length-nwindow over it, so a 1 KB datagram pins 64 KB. Measuring the view length instead would price a cost nobody pays and ignore the one everybody does.kPinNever (0) is the reserved sentinel: never pin. It is the value shipped here, and it reproduces the pre-RFC default behaviour exactly (the old absolute threshold defaulted to 0 and its predicate required
> 0). RFC-0022 §8 Q3 was answered by §6’s measurement (Amendment 2, PR #771): the sentinel is the landing default on BOTH targets — the on-by-default flip does not land.The borrow is APP-OWNED policy, and this constant is the decision surface¶
A pinned value borrows its inbound RX segment for its whole lifetime — not for the delivery window, for as long as the value is the vertex’s last-known value. On a pooled RX backend that borrow is a pool slot, i.e. receive capacity, unavailable to the transport until the value is displaced or the vertex dies. The library makes the deferred release safe — atomic segment refcounts, so a borrow outliving the recv frame is never a use-after-free — but nothing in the library bounds the budget, and nothing can: only the application knows its pool geometry and its retention pattern.
So the quantity to size against is
live pinned values x segment_bytes.Kbounds the waste per value; it does not bound the number of values, which is why noKis a remedy for a retain-heavy workload — measured, seebench/README.md§”RFC-0022 §6 —
receive-pool occupancy”: at the ESP32-C6 RX geometry every
Kthat pins at all collapsed a 29-slot pool identically once the live vertex count crossed the slot count.Target-class guidance, and the reason the shipped value is the sentinel on both:
NARROW — set the sentinel. A fixed, small RX pool cannot fund an indefinite borrow; the same off-by-default-on-NARROW posture as the RFC-0027 label table.
WIDE / MID — may borrow freely; the pool is large relative to the retained set, and the borrow is the zero-copy latency win.
-
static constexpr std::size_t kDeferredReleaseSlots = 16¶
How many retired
{ctx, release}pairs ONE THREAD may hold parked at once, when reclaim_policy_t defers (ADR-0080).Read this as “subscriptions unsubscribed from INSIDE a single delivery stack”. The ordinary unsubscribe — from outside any callback — parks nothing at all, so on most nodes this storage is touched zero times; it exists for the re-entrant case
reclaim_local_tsupports andreclaim_strict_tforbids.Sizing: one retired_callback_t per slot, in per-thread storage that is plain bytes with no destructor and no allocation — 256 B on a 64-bit host at the default, 128 B on a 32-bit MCU, and only on a thread that actually dispatches. Nothing at all under
reclaim_strict_t, which never defers.Overflow is a REFUSAL, not a failure: a pair that finds every slot taken is DROPPED and its hook is never run — a leak, deliberately, because the alternative is running a release hook while the fan-out that is still walking the snapshot names that context.
graph_t::deferred_release_drops()counts every one, so an undersized bound is observable rather than silent. Override fragment:static constexpr std::size_t kDeferredReleaseSlots = 64;Under read it differently.
There the pairs are held across a cross-thread GRACE PERIOD rather than a dispatch stack, in ONE shared table rather than per-thread storage, so the quantity it bounds is “retirements in flight process-wide
while some participant has yet to quiesce” — a larger and less predictable number. Raise it (64 is a sane starting point) on any node that unsubscribes in bulk while other threads dispatch. The overflow rule is identical, and so is its observability: a QSBR build retries the scan once after publishing the pair, so a drop means a participant thread genuinely never reached a quiescent state — an embedder defect
tr::graph::graph_t::deferred_release_drops now names.
-
static constexpr std::size_t kQsbrParticipants = kEdgePinSlots¶
How many threads may participate in the
reclaim_qsbr_tgrace period at once (#1376) — the per-thread quiescent-state announcement a retiring thread scans.Read this as “threads that may dispatch (`fan_out`, or an ADR-0049 durability latch)
concurrently”. Derived from
kEdgePinSlots rather than picked out of the air, the way’slkv_slot.hppkRetireBatchis derived from kHazardReaderSlots — the two sets are the same threads, since a thread that publishes is a thread that dispatches.**Unlike kEdgePinSlots, correctness is not indifferent to this number** — but it fails SAFE, not silently. A thread that finds every index taken cannot announce itself, and a thread a scan cannot see is one no grace period may conclude past; so it counts itself into an overflow tally instead, and any non-zero reading blocks all reclamation until it clears. The failure mode is therefore deferred frees (visible as tr::graph::graph_t::deferred_release_drops rising), never a use-after-free.
Its
.bssisN * max(kCacheLineBytes, alignof(std::uint64_t))bytes — at N = 32 that is 2,048 B on a host — plus the shared retired table. It is emitted only in a build that actually binds :graph.cppreaches the domain exclusively fromif constexprbranches a non-QSBR build discards, and GCC emits nothing at all for those — 0 symbols and 0 B of.bss, verified. Override fragment:static constexpr std::size_t kQsbrParticipants = 64;
-
static constexpr std::size_t kDeviceBackendSlots = 2¶
How many
DEVICE-space memory backends may register a transfer hook at once (#1381) — the bound ontr::mem::register_device_backend’s table.An L0 (
tr::mem) fact, here for the same reason kSpinWaitSafe is: ADR-0070’s rule is that the configuration is ONE named type. tr::mem::kDeviceBackendSlots is its spelling for the memory layer.Read it as “vendor accelerator backends this process binds” — a GPU tier module, an NPU one, a dmabuf one. Two is the honest default: a host that talks to one accelerator family needs one slot, and nothing in-tree registers at all.
Its
.bssisN * (sizeof(void*) + sizeof(void(*)()))— 32 B on a 64-bit host at the default — and it is emitted only in a build that linksdevice_backend.cpp. A single-backend (LIBTRACER_BACKEND_SET_POOL_ONLY) target has no device arm and never links that TU, so the cost there is 0 B rather than “small”. Override fragment:static constexpr std::size_t kDeviceBackendSlots = 4;Overflow is a REFUSAL, not a failure:
register_device_backendreturnsfalseand registers nothing, so a backend that could not claim a slot moves no bytes at all (tr::mem::transferanswersfalsefor its segments) rather than silently sharing another vendor’s hook.
-
static constexpr bool kSpinWaitSafe = true¶
Whether a task on this target may SPIN-WAIT for a lock another task holds (#1158).
An L0 (
tr::mem) fact, but a member HERE because ADR-0070’s rule is that the configuration is ONE named type: a knob that lives outside it cannot be set by an override fragment, which is exactly the defect that kept this one in the build system. tr::mem::kSpinWaitSafe is its spelling for the memory layer, derived like every other loose name below.True on a multi-core host: the holder runs on a different core, so a spinner makes progress possible and the O(1) section costs less than a mutex round-trip. FALSE on a priority-preemptive scheduler, where a spinner that outranks the holder never yields the CPU the holder needs to release the lock — the wait becomes unbounded priority inversion and the board hangs in the watchdog rather than merely running slowly. That is true of a single-core chip and equally of an SMP chip whose spinner and holder share a core.
-
static constexpr bool kWeaklyOrdered = true¶
Whether this target’s memory model may REORDER a later relaxed load ahead of an earlier
seq_cststore — i.e. whether it is anything WEAKER than x86-64’s TSO (#1143).The one knob here that is not a size, a policy or a preference: it states what the hardware does, so that an ordering precondition can be a
static_assertinstead of a paragraph. The precondition it carries today is the delivery-skip Dekker pair (#635, #1140) —vertex_t::own_subs_orderedagainst ADR-0049’s subscribe latch — whoseseq_csthalves are argued from the code rather than from coverage, because a relaxed ablation leaves the whole suite green wherever CI happens to run.kDeliverySkipOrder() is the constant this refuses to see weakened.vertex.hppDefault : assume weak unless a target says otherwise. The two directions are not symmetric. Saying
trueon a TSO host costs exactly nothing — the orders this gates are alreadyseq_cston every target, so the assertion is satisfied as shipped and no instruction changes. Sayingfalseon a target that is actually weak silently disarms the check on the one class of target it exists for, and the shipped set is mostly that class: rv32 (esp32c6/c3), Cortex-M0, and theubuntu-24.04-armCI leg (#1140) are all weakly ordered, and a raw-Iconsumer — a vendored source drop, the footprint gate — states nothing at all. The value that is safe to inherit in silence is therefore the strict one.It never SELECTS a weaker order: nothing reads this to relax an access, so a target that sets it
falsegets the same instructions, only a check that stops firing. Override fragment:static constexpr bool kWeaklyOrdered = false;— worth setting only for an x86-64-only build that wants the freedom to relax those loads, which is a decision to take deliberately rather than by omission.
-
static constexpr bool kBusLinks = true¶
Whether this target carries the ADR-0044 BUS facet at all — peer-named links, per-peer addressing, in-band peer enumeration (#375 deliverable 3).
A
tr::netfact, and a member HERE for the reason kSpinWaitSafe and kDeviceBackendSlots are: ADR-0070’s rule is that the configuration is ONE named type, so a knob that lives outside it cannot be reached by an override fragment. tr::net::kBusLinks is its spelling for the transport plane.What it closes. A bus link reaches MANY peers and names each of them, so the routing plane carries a second addressing tier for it: the registry stamps a mount’s bus SHAPE and resolves a residual segment as a peer (
child_registry_t::resolve_peer,by_name’s peer fallback),fwd_router_t::add_childwires the peer-named receiver and both peer-lifecycle notifiers, and a connection vertex synthesizes its:children[]from the link’s live peer table. Boundfalse, every one of those consumers folds to the point-to-point answer at COMPILE time throughtr::net::bus_of(transport.hpp), and the peer-named machinery behind them is never reached — ADR-0047 §1 link-time module selection, expressed as a configuration member rather than as a TU list, because whether a tcp/ws listener is peer-named is a WIRING-time choice inside a TU that a bus-less target still compiles for its point-to-point half.What it costs to keep (the default) and what closing it buys. Measured on rv32 (
-Os -fno-exceptions -fno-rtti,rv32imac_zicsr_zifencei/ilp32, GCC 15.2, per-TU.text), closing it removes 1,400 B of flash fromfwd_router.cppand 678 B fromtransport_vertex.cpp— 2,078 B — and 0 B of.bss, because the tier is code and per-instance state, not a static table. ALIBTRACER_NET_PLANE=OFFbuild gains nothing: it never compiled those TUs in the first place.Who should set it. A node whose links are point-to-point — one dial upstream, or a listener that serves its peers as one broadcast link (ADR-0001’s originating firmware shape). Override fragment:
static constexpr bool kBusLinks = false;It is a REFUSAL, never a silent downgrade. A build that binds it
falseand then asks for a bus is rejected, loudly and at the earliest door that can speak: compilingLIBTRACER_TRANSPORT_CAN(a bus by construction) is astatic_assert, and apeer_named=truetcp/ws listener is refused by its SPEC factory and reportstransport_t::ok() == falsewhen constructed directly. Quietly serving such a configuration as FLAT would be worse than either: the listener’s own per-frame tier select would keep delivering peer-named into a sink the router never installed.
-
using acl_policy_t = allow_only_policy_t¶
-
using tr::graph::config_t = default_config_t¶
THE configuration this build uses — the one binding, and the one thing to override.
An override fragment binds this alias to its own traits type; with no fragment present it names default_config_t. Everything below is derived from it, so nothing else changes.
-
constexpr std::uint32_t tr::graph::kPinNever = 0¶
The reserved default_config_t::kPinPayloadRatio sentinel: never pin, always take the ADR-0041 §2 one-copy store (RFC-0022 §3.D).
-
constexpr std::size_t tr::graph::kVertexLockStripes = config_t::kVertexLockStripes¶
default_config_t::kVertexLockStripes for this build.
-
constexpr std::size_t tr::graph::kCacheLineBytes = config_t::kCacheLineBytes¶
default_config_t::kCacheLineBytes for this build.
-
constexpr std::size_t tr::graph::kHazardReaderSlots = config_t::kHazardReaderSlots¶
default_config_t::kHazardReaderSlots for this build.
-
constexpr std::uint32_t tr::graph::kPinPayloadRatio = config_t::kPinPayloadRatio¶
default_config_t::kPinPayloadRatio for this build.
-
constexpr bool tr::graph::kWeaklyOrdered = config_t::kWeaklyOrdered¶
default_config_t::kWeaklyOrdered for this build.
-
using tr::graph::acl_policy_t = config_t::acl_policy_t¶
default_config_t::acl_policy_t for this build.
-
using tr::graph::lkv_slot_t = config_t::lkv_slot_t¶
default_config_t::lkv_slot_t for this build.
-
using tr::graph::reclaim_policy_t = config_t::reclaim_policy_t¶
default_config_t::reclaim_policy_t for this build.
-
constexpr std::size_t tr::graph::kDeferredReleaseSlots = config_t::kDeferredReleaseSlots¶
default_config_t::kDeferredReleaseSlots for this build.
-
constexpr std::size_t tr::graph::kQsbrParticipants = config_t::kQsbrParticipants¶
default_config_t::kQsbrParticipants for this build.
The derived constants that are not in tr::graph: the memory layer and the transport
plane read their own spellings, so that neither L0 nor tr::net has to name an L4 type.
-
constexpr bool tr::mem::kSpinWaitSafe = tr::graph::config_t::kSpinWaitSafe¶
Whether a task on this target may SPIN-WAIT for a lock another task holds.
The memory layer’s spelling of tr::graph::default_config_t::kSpinWaitSafe, which carries the full rationale. Derived from tr::graph::config_t exactly as the
tr::graphloose names are, so an override fragment sets it in the one place every knob is set.A target fact, so the BUILD states it and nothing asks the integrator (the same reasoning that derives tr::graph::kCacheLineBytes rather than exposing it). Its one consumer is the guard in
synchronized_pool_t, which refuses to instantiate the spinlock policy where spin-waiting is unsafe.
-
constexpr std::size_t tr::mem::kDeviceBackendSlots = tr::graph::config_t::kDeviceBackendSlots¶
How many
DEVICE-space backends may register a transfer hook at once.The memory layer’s spelling of tr::graph::default_config_t::kDeviceBackendSlots, which carries the full rationale. Derived from tr::graph::config_t exactly as kSpinWaitSafe is, so an override fragment sets it in the one place every knob is set. Its one consumer is the bounded table behind register_device_backend (
device_backend.cpp).
-
constexpr bool tr::net::kBusLinks = tr::graph::config_t::kBusLinks¶
Whether this target carries the ADR-0044 BUS facet at all (peer-named links).
The transport plane’s spelling of tr::graph::default_config_t::kBusLinks, which carries the full rationale, the measured saving and the refusal rule. Derived from tr::graph::config_t exactly as tr::mem::kSpinWaitSafe is, so an override fragment sets it in the one place every knob is set. Its consumers reach it through
tr::net::bus_of(transport.hpp) rather than reading it directly.
The selectable policies¶
-
struct allow_only_policy_t¶
The required-modules MCU profile policy (ADR-0020 core subset): ALLOW-only.
Any applicable ACE grants — order is irrelevant because DENY does not exist in this profile (a
:aclwrite carrying one is rejected at parse time).Public Static Functions
-
static inline acl_verdict_t allows(std::span<const std::byte> subject, std::uint32_t bit, std::span<const ace_t> aces, std::uint64_t now, std::uint8_t required_flags = 0) noexcept¶
Evaluate one ACE list — pure: no locks, no clock, no graph access.
- Parameters:
subject – The resolved subject token bytes (ADR-0018).
bit – The requested right (one
acl_right_tbit).aces – One vertex’s stored ACEs, in stored order.
now – Check-time wall clock, ns since the UNIX epoch.
required_flags – ACEs lacking these flag bits are skipped —
0for the target’s own list,kAceInheritfor an ancestor’s.
- Returns:
ALLOWorNO_MATCH(this profile never returnsDENY).
Public Static Attributes
-
static constexpr bool kAcceptsDeny = false¶
This profile rejects DENY ACEs at parse time.
-
static inline acl_verdict_t allows(std::span<const std::byte> subject, std::uint32_t bit, std::span<const ace_t> aces, std::uint64_t now, std::uint8_t required_flags = 0) noexcept¶
-
struct full_acl_policy_t¶
The
security_aclhost policy (ADR-0020 full model): ordered first-match-per-bit with DENY.For the requested bit, the FIRST applicable ACE in stored order decides —
ALLOWorDENYper its type (NFSv4 evaluation). The graph calls this per effective-ACE list, own list before ancestors, so cross-list ordering follows the effective-ACL definition of ADR-0020.Public Static Functions
-
static inline acl_verdict_t allows(std::span<const std::byte> subject, std::uint32_t bit, std::span<const ace_t> aces, std::uint64_t now, std::uint8_t required_flags = 0) noexcept¶
Evaluate one ACE list — pure: no locks, no clock, no graph access.
- Parameters:
subject – The resolved subject token bytes (ADR-0018).
bit – The requested right (one
acl_right_tbit).aces – One vertex’s stored ACEs, in stored order.
now – Check-time wall clock, ns since the UNIX epoch.
required_flags – ACEs lacking these flag bits are skipped —
0for the target’s own list,kAceInheritfor an ancestor’s.
- Returns:
ALLOWorNO_MATCH(this profile never returnsDENY).
Public Static Attributes
-
static constexpr bool kAcceptsDeny = true¶
The full model stores and evaluates DENY ACEs.
-
static inline acl_verdict_t allows(std::span<const std::byte> subject, std::uint32_t bit, std::span<const ace_t> aces, std::uint64_t now, std::uint8_t required_flags = 0) noexcept¶
-
class sp_atomic_slot_t¶
The slot libtracer ships today:
std::atomic<std::shared_ptr<const rope_t>>.Reclamation is the shared_ptr refcount, so there is no scheme to implement and no registry to size — the reason this is the checked-in default, and the reason a raw
-Iconsumer and the stock ESP-IDF component keep building exactly what they built before the slot became a policy.Lock-free BY CONTRACT, and spin-locked in practice.
std::atomic<std::shared_ptr<T>>::is_lock_free()returns 0 on libstdc++, so both load and store take its internal pointer-lock bit (lock cmpxchgto acquire, anxchgto release). Measured, that is ~77 of the ~316 cycles of an in-process write and the largest single term left on the path — 88% ofstore’s samples land on those three instructions. Do not read “lock-free” here as “no serializing operation”; ADR-0064 §2 records why, and ADR-0069 records what replaces it on a host.Public Types
Public Functions
-
inline bool store(value_ptr_t sp, std::memory_order order = std::memory_order_seq_cst)¶
Publish. Sequentially consistent unless the caller says otherwise — the default is what orders the publish with
write_seq_and the waiter count, which is what makes the waiterless publish (#555) unable to lose a wakeup.- Returns:
Always
true. This policy allocates nothing to publish — it takes a reference it was handed — so it has no failure to report. The signature exists because the contract has one (see the file header), not because this implementation does.
-
inline void clear(std::memory_order order = std::memory_order_seq_cst)¶
Drop the published value. Releases a reference; cannot fail.
-
inline value_ptr_t load() const¶
Read the published value.
A mid-read reader holds its own reference, so a concurrent publish or clear cannot free the value under it — that is the whole of this policy’s reclamation.
-
inline bool store(value_ptr_t sp, std::memory_order order = std::memory_order_seq_cst)¶
-
struct reclaim_strict_t¶
**
reclaim_strict** — the grace point is the momentunsubscribe()returns.The opt-in ZERO-COST mode (ADR-0080 §Decision 2), for an MCU deployment that provably never unsubscribes from inside a dispatch. Nothing is tracked on the dispatch path, no state is held anywhere, and
unsubscribe()runs the release hook inline before it returns — so the caller may free its context on that return with no further ceremony.Re-entrant unsubscribe is FORBIDDEN, not merely discouraged: unsubscribing from inside a delivery would retire a pair the running fan-out’s snapshot still names. A debug build asserts on it (see
graph.cpp); anNDEBUGbuild cannot see it, which is the trade this policy exists to make. Select it only where the deployment can show that re-entrant unsubscribe does not occur — otherwise take the default, which supports it.Selecting it is one line in
libtracer/config_override.hpp:using reclaim_policy_t = reclaim_strict_t;Public Static Attributes
-
static constexpr std::string_view kName = "reclaim_strict"¶
The policy’s name, for a diagnostic that must say which one is bound.
-
static constexpr bool kDefersToDispatchExit = false¶
Whether a retired pair may be DEFERRED past
unsubscribe()’s return.False here: there is no grace period to defer into, so
unsubscribe()releases inline and the dispatch path carries nothing at all.
-
static constexpr bool kReentrantUnsubscribe = false¶
Re-entrant
unsubscribe()(from inside a delivery) is not supported.
-
static constexpr bool kGraceSpansThreads = false¶
Whether the grace point is stated over EVERY thread rather than one.
False here: there is no grace period at all, so there is nothing for a second thread to be inside. See reclaim_qsbr_t for the policy that answers true and what changes.
-
static constexpr std::string_view kName = "reclaim_strict"¶
-
struct reclaim_local_t¶
**
reclaim_local** (the DEFAULT) — the grace point is the moment this thread’s dispatch stack unwinds to depth 0.It is the default because it makes the MCU and the host build behave IDENTICALLY (ADR-0080 §Decision 1).
reclaim_strictwould make the same application code legal on a host that tolerates re-entrant unsubscribe and illegal on the constrained target — a portability bug that surfaces only where it is hardest to debug.What a caller gets¶
Exactly one of two things, and the library decides which — the caller never asks:
**
unsubscribe()was called from outside any delivery** (dispatch depth 0 — the ordinary case). No delivery to that context can be in flight on this thread, so the release hook runs INLINE andunsubscribe()returns already quiescent. This isreclaim_strict’s guarantee, delivered atreclaim_strict’s cost, for the case that dominates.**
unsubscribe()was called from INSIDE a delivery** (a subscriber callback unsubscribing itself or a sibling). The running fan-out is walking a snapshot that still names the retired pair, so the pair is PARKED and the hook runs when the outermost delivery on this thread returns — i.e. before thewrite()/propagate()that started it hands control back.
In both cases the signal is the hook. There is no poll, no wait, and no verb the embedder must remember to call.
The scope of the guarantee¶
It is stated over one thread’s dispatch domain, which is the WIDE / MCU target this policy is for: a single-threaded node, where publish and
unsubscribe()cannot overlap because there is no second thread to overlap with. An embedder that dispatches from several threads concurrently and unsubscribes from another needs a grace period spanning every thread — that is reclaim_qsbr_t, ADR-0080’s third policy (#894, #1376), not this one.What it costs¶
One non-atomic increment, decrement and branch per
fan_out— regardless of subscriber count — on a per-thread counter, so no cache line is ever shared and no atomic is involved. Parking allocates nothing: the retired pairs sit in a bounded per-thread array sized by tr::graph::default_config_t::kDeferredReleaseSlots.Public Static Attributes
-
static constexpr std::string_view kName = "reclaim_local"¶
The policy’s name, for a diagnostic that must say which one is bound.
-
static constexpr bool kDefersToDispatchExit = true¶
Whether a retired pair may be DEFERRED past
unsubscribe()’s return.True here: a re-entrant unsubscribe parks its pair and the outermost dispatch’s exit releases it. The deferral happens ONLY at depth > 0 — at depth 0 the release is inline.
-
static constexpr bool kReentrantUnsubscribe = true¶
Re-entrant
unsubscribe()(from inside a delivery) is supported.
-
static constexpr bool kGraceSpansThreads = false¶
Whether the grace point is stated over EVERY thread rather than one.
False here, and it is the ONE limitation of this policy: the grace point is this thread’s dispatch stack, so a sibling thread’s in-flight fan-out is invisible to it. See The scope of the guarantee.
-
struct reclaim_qsbr_t¶
**
reclaim_qsbr** — the grace point is the moment EVERY dispatching thread has passed a quiescent state (#1376).ADR-0080’s third policy and the MID / NARROW many-core one: bind it when this node dispatches from several threads at once and may unsubscribe from a thread other than the one delivering. That is the single case neither shipped policy covers — reclaim_local_t’s grace point is one thread’s dispatch stack, so a sibling thread’s live snapshot is invisible to it, and reclaim_strict_t has no grace period at all.
Selecting it is one line in
libtracer/config_override.hpp:using reclaim_policy_t = reclaim_qsbr_t;What a caller gets¶
A retired
{fn, ctx}pair is released once no thread can still be walking a snapshot that names it. As under reclaim_local_t the caller never polls and never waits; it registers a subscriber_release_fn_t and is told. Two cases, and again the library picks:No participant is mid-dispatch — every ordinary unsubscribe on a node that is not concurrently publishing. The scan concludes immediately, the hook runs INLINE, and
unsubscribe()returns already quiescent. This is the same property (a) the other two policies deliver, and it still dominates.Some participant IS mid-dispatch. The pair is deferred and released by whichever participant next completes the grace period.
The one API difference, stated rather than hidden¶
In case 2 the hook runs on a thread other than the caller — specifically on whichever participant’s quiescence completed the grace period, or on the caller’s own next dispatch exit, whichever comes first. Both other policies promise the caller’s own thread; a grace period that spans threads structurally cannot, because the only alternatives are to block the caller (ADR-0080 §Decision 4 rejects waiting outright) or to leak the pair.
So a release hook under this policy must be thread-safe with respect to its own context. That is a real widening of the contract and it is why this is an opt-in policy rather than the default: on the single-threaded target reclaim_local_t serves, the distinction does not exist, and ADR-0080 §Decision 1’s parity argument keeps the default where it is.
What it costs¶
Per OUTERMOST
fan_out— never per edge, and nothing at all on a publish nobody subscribed to, because the bracket sits belowfan_out’s no-subscriber gate:entry: one RELAXED load of a read-mostly shared line (the epoch, bumped only on the control plane) and one
seq_cststore to this thread’s own cache-line-isolated cell. No atomic read-modify-write;exit: one
releasestore to that same cell, plus one relaxed load of a read-mostly count that is 0 on any node not mid-teardown, and the predictable branch it guards.
The
O(kQsbrParticipants)scan is on the RECLAIM path only — that is the precise difference from the shape #635 rejected, which put a hazard scan on the READ path. Storage is tr::graph::default_config_t::kQsbrParticipants cache-line-isolated cells plus one shared table of tr::graph::default_config_t::kDeferredReleaseSlots retired pairs, all.bss, and none of it emitted into a build that binds a different policy.What it discharges for #897¶
ADR-0080 §”#897 maps onto the same seam” asks that each thread self-drain its own retired LKV list at its own quiescent point, so
~hazard_slot_tnever has to reach across a live thread’s private list. Under this policy that is exactly what happens, and it costslkv_slot.hppno code at all: the quiescent point calls the already-shippedtr::graph::detail_hp::retire_and_flush(nullptr), whose cheap early-out makes it free on a thread that parked nothing. Nostore()-path atomic is added.Public Static Attributes
-
static constexpr std::string_view kName = "reclaim_qsbr"¶
The policy’s name, for a diagnostic that must say which one is bound.
-
static constexpr bool kDefersToDispatchExit = true¶
Whether a retired pair may be DEFERRED past
unsubscribe()’s return.True: this policy needs the very same dispatch bracket reclaim_local_t does — the transition to depth 0 IS the quiescent state it announces — and defers whenever the scan finds a participant that has not yet reached one.
-
static constexpr bool kReentrantUnsubscribe = true¶
Re-entrant
unsubscribe()is supported — subsumed by the grace period.
-
static constexpr bool kGraceSpansThreads = true¶
The grace point is stated over EVERY dispatching thread. See The one API difference, stated rather than hidden for what that changes for a release hook.
-
struct retired_callback_t¶
One retired subscription’s release obligation — the
{ctx, hook}pair a policy that defers must hold until its grace point.Trivially copyable and free of any owning member, so a bounded array of these is plain storage with no destructor and no initialization guard (which is what lets reclaim_local_t’s per-thread parking allocate nothing, ever).
Public Members
-
void *ctx = nullptr¶
The subscriber’s own context.
-
subscriber_release_fn_t release = nullptr¶
What to call on it, exactly once.
-
void *ctx = nullptr¶
-
class hazard_slot_t¶
The host slot (ADR-0069 §1): a lock-free
atomic<node_t*>reclaimed with hazard pointers, returning the same owningshared_ptrsp_atomic_slot_t does.Why this exists: today’s slot INVERTS under concurrent readers — measured through the real path,
graph_t::readon one shared LKV falls from 21.1 M/s at one reader to 1.7 M/s at twenty-four, because bothloadandstoretake libstdc++’s_Sp_lockerpointer-lock bit. Hazard reclamation deletes that lock; what it cannot delete is the control-block increment an owning read still owes. End to end that is worth 4.2× at twenty-four readers (7.4 M/s) — see the table in this file’s header, and ADR-0069 §6 for why the model bench’s 20.8× did not survive contact with the whole read path.Why the default is still
sp_atomic_slot_t: the gain is entirely a concurrency gain — at one thread the two slots are indistinguishable within run-to-run spread, so a single-core node buys nothing and still pays(kHazardReaderSlots + 1) * 128bytes of registry, a deferred-reclamation lifetime rule (seeretire_and_flush), and a publish that can fail. Bind this one from a host preset:-DLIBTRACER_LKV_SLOT=hazard_slot_t.Publish can fail under memory exhaustion, which sp_atomic_slot_t cannot: an empty free list makes the first publish per participant allocate a 24-byte node. It is reported, not silent —
storereturnsfalseandvertex_t::storeturns that into the samenullptr→BACKPRESSUREsoft-fail an LKV allocation failure already produces (#477), so no write is ever reported as taken when it was not. Every later publish reuses the node its own displacement recycled, so the window is a warm-up one — but it is still a real difference in the policy’s failure surface, and a third reason the MCU does not bind this slot. Note also that the node comes from the global heap, not from a graph’s injectedstd::pmr::memory_resource: the slot policy is never handed one, and a bounded target that needs every byte accounted for is another target that should keep the default.Public Types
Public Functions
-
inline ~hazard_slot_t()¶
Retire the published node rather than free it — a reader may still be pinning it — and flush, so the value cannot outlive the memory it was allocated from.
A slot that was never written costs nothing here: no node, no flush, no domain access.
-
inline bool store(value_ptr_t sp, std::memory_order order = std::memory_order_seq_cst)¶
Publish, sequentially consistent unless the caller says otherwise.
An empty handle is not a publish — use clear.
- Returns:
falseif no node could be obtained for the value, in which case nothing was published and the previous value still stands. Only a participant’s first publish can reach that: every later one reuses the node its own displacement recycled, so the free list makes the steady state allocation-free.
-
inline void clear(std::memory_order order = std::memory_order_seq_cst)¶
Drop the published value. Cannot fail — it publishes
nullptr, which needs no node, so a clear allocates nothing even on a cold participant.
-
inline value_ptr_t load() const¶
Read the published value.
Announce, re-read, then copy the
shared_ptrout of the pinned node — the copy is the promotion that lets the handle outlive the pin, and it is the one shared-cache-line RMW this scheme cannot remove. A slot nobody has written costs a single acquire load and never touches the domain at all.The announce and the re-read are both
seq_cstso that both sit in one total order with the publisher’sexchangeand the reclaimer’s fence: if a scan did not observe this announcement, then in that order the scan’s read precedes it, the displacement precedes the scan, and so this re-read must observe the displacement and retry.acquireon the re-read is the usual spelling and is believed sound, but it leaves the argument resting on coherence rather than on the total order — and it costs nothing to close, since aseq_cstload is a plainmovon x86-64.Reusing a node is deliberately allowed to ABA: a reader can pin
n, have it reclaimed and republished, and revalidate against the same address. That is not a bug —nis live and holds a value some writer published, which is all a read promises.
-
inline ~hazard_slot_t()¶
The runtime settings reader¶
-
class config_reader_t¶
Typed accessors over a positional-pair TLV’s children — a SPEC
configSETTINGS, a SUBSCRIBER QoS SETTINGS, or the creation-SPEC envelope itself.The layout is positional NAME-key / value pairs: a
NAMEchild carrying the key string, immediately followed by the value child — aNAMEfor string values, aVALUEfor integers/flags, or a nestedSETTINGSfor a module namespace. Unknown keys are ignored (forward-compat), a key whose value child has the wrong type (or aVALUEpayload that is not EXACTLY the accessor’s width — a u32 asked of a 2-byte payload is absent, not zero-extended, #928) is ignored too, and when a key appears more than once the LAST well-formed occurrence wins.The walk is pair-consuming: it advances a whole pair at a time, so an unknown key is skipped together with its value and no value child can ever be re-read as a key (#927). Forward-compat tolerance is deliberate here, and is the OPPOSITE ruling from the
graph::parse_aclwalk (#906), which is to REJECT an unknown key: config is where a newer peer legitimately sends more than a receiver understands, whereas an ACL is a security document in which a silently dropped attribute widens access.Note
The returned string_views/spans (and the reader itself) borrow the decoded TLV’s storage — use them while the
tlv_tis alive.Public Functions
-
inline explicit config_reader_t(const tlv_t *config) noexcept¶
Construct over
config'schildren.- Parameters:
config – The decoded pair-container TLV; nullptr = no config (every accessor returns nullopt / nullptr).
-
inline std::optional<std::string_view> name(std::string_view key) const noexcept¶
The string value of
key(aNAMEvalue child), if present.
-
inline std::optional<std::span<const std::byte>> name_bytes(std::string_view key) const noexcept¶
The raw payload bytes of
key(aNAMEvalue child), if present.The byte-span twin of name() for a value that is a wire segment rather than text —
graph_t::create_childreads the creation SPEC’s “name” this way, because the child name is appended to the parent’s key verbatim.
-
inline const tlv_t *settings(std::string_view key) const noexcept¶
The nested
SETTINGSvalue child ofkey, or nullptr.A module namespace (“config” in the creation SPEC, a per-transport block in a connection config). Borrowed from the decoded TLV, same as every accessor.
-
inline std::optional<std::uint8_t> u8(std::string_view key) const noexcept¶
The u8 value of
key(a 1-byteVALUEchild), if present.
-
inline std::optional<std::uint16_t> u16(std::string_view key) const noexcept¶
The u16 value of
key(a 2-byteVALUEchild), if present.
-
inline std::optional<std::uint32_t> u32(std::string_view key) const noexcept¶
The u32 value of
key(a 4-byteVALUEchild), if present.
-
inline std::optional<bool> flag(std::string_view key) const noexcept¶
The boolean value of
key:a 1-byteVALUEchild read as u8, nonzero = true.
-
inline explicit config_reader_t(const tlv_t *config) noexcept¶
See: the configuration space (what each knob costs), security & ACL, graph, fwd-router (which consumes the settings reader).