A HANDLER vertex executes its write (L4 graph)¶
The vertex’s role decides what a write means at that address. STORED_VALUE assigns;
HANDLER — roles 3–7 of
reference 11 — runs the owner’s on_write
instead, and its on_read supplies a value the graph never held: an MMIO register, a
computation, a device command. This example registers a relay whose write toggles the device
and whose read reports the device’s state.
What to notice¶
The role is invisible on the wire. A peer sees one address with
readandwrite, exactly as for a stored value; no role code is ever transmitted (CONTEXT.md §Structural vertex, on why a role is never on the wire).The seam allocation is keyed on presence, not on role. A vertex bears a value-seam block iff a handler was installed at registration, so a
HANDLERvertex registered with an emptyhandlers_tallocates none, and aSTORED_VALUEvertex given anon_childrendoes (reference 02 §Vertex lifecycle;value_handlers_tincore/include/libtracer/vertex.hpp).Each seam is a
{fn, ctx}hook (core/include/libtracer/hook.hpp, RFC-0028 D10): a captureless function whose first argument is actxthe owner keeps alive for as long as the vertex is registered. Nothing is captured and nothing is owned, so registering a seam allocates nothing beyond the seam block itself.on_writeis where the device acts. It receives the written value as aconst value_t&— for a subscription delivery, the very block the source published, with no copy — and the writer’swrite_ctx_t. Both are borrowed for the call only: a handler that keeps the value takesvalue_ref_t::keep(value), never the reference itself. The handler returns aresult_t<void>— a refusal is the device’s, not the graph’s.This is the sink shape a subscription delivers into. Delivery is a write (CONTEXT.md §SUBSCRIBER direction), so a handler vertex is what a spec-faithful
subscribe(src, target)re-dispatches to — see in-process pub/sub, which wires exactly that.
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 — a HANDLER vertex EXECUTES its write instead of storing it.
9 *
10 * The vertex's role decides what a write means at that address. `STORED_VALUE` assigns;
11 * `HANDLER` (roles 3–7 of `docs/reference/11-vertex-roles-and-aggregation.md`) runs the
12 * owner's `on_write`, and its `on_read` supplies a value the graph never held — an MMIO
13 * register, a computation, a device command. The role is host state and appears on no wire:
14 * a peer sees one address with `read`/`write`, exactly as for a stored value.
15 *
16 * Each seam is a `{fn, ctx}` hook (`libtracer/hook.hpp`): a captureless function and a
17 * pointer to state the caller keeps alive for as long as the vertex is registered. `on_write`
18 * receives the written value by reference — a handler that keeps it past the call takes
19 * `tr::graph::value_ref_t::keep(value)`.
20 *
21 * The seam block is allocated on the PRESENCE of a handler, not on the role
22 * (`core/include/libtracer/vertex.hpp`, `value_handlers_t`), so a `HANDLER` vertex
23 * registered with an empty `handlers_t` allocates none.
24 *
25 * Runs under ctest as `example_graph_handler_vertex`; returns non-zero on any failed check.
26 */
27
28#include <cstdio>
29#include <cstring>
30#include <span>
31#include <string_view>
32
33#include "libtracer/tracer.hpp"
34
35namespace {
36
37using tr::graph::handlers_t;
38using tr::graph::path_t;
39using tr::graph::role_t;
40
41/** @brief An owned one-segment view over @p text. */
42tr::view::view_t value_of(std::string_view text) {
43 return *tr::view::over_bytes(std::as_bytes(std::span<const char>(text.data(), text.size())));
44}
45
46/** @brief Report expectation @p what and record a failure on @p ok. */
47void check(bool& ok, bool cond, const char* what) {
48 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
49 ok = ok && cond;
50}
51
52} // namespace
53
54int main() {
55 tr::graph::graph_t g;
56 bool ok = true;
57 int commands = 0;
58
59 // `ctx` is `&commands`: it outlives `g`'s use of the seams, which is the whole contract.
60 handlers_t relay;
61 relay.on_write = {[](void* ctx, const tr::graph::value_t& in,
62 const tr::graph::write_ctx_t&) -> tr::graph::result_t<void> {
63 int& n = *static_cast<int*>(ctx);
64 ++n; // the device acts here; nothing is assigned to the vertex
65 std::printf(" [relay] command #%d, %zu bytes\n", n, in.total_length());
66 return {};
67 },
68 &commands};
69 // `on_read` answers the one read type (RFC-0028 D11): a value the handler mints with
70 // `value_ref_t::copy` costs one block, the bytes inline, and is never stored.
71 relay.on_read = {[](void* ctx) -> tr::graph::result_t<tr::graph::value_ref_t> {
72 const int n = *static_cast<const int*>(ctx);
73 const std::string_view state = n % 2 ? "ON" : "OFF";
74 return tr::graph::value_ref_t::copy(std::as_bytes(std::span(state)));
75 },
76 &commands};
77 const auto sw = g.register_vertex(path_t("/dev/relay0"), role_t::HANDLER, relay);
78
79 const auto before = g.read(sw);
80 check(ok, before && before->get()->only().bytes().size() == 3,
81 "on_read supplies a value the graph never stored");
82
83 (void)g.write(sw, value_of("toggle"));
84 check(ok, commands == 1, "a write to a HANDLER vertex runs on_write");
85
86 const auto after = g.read(sw);
87 check(ok, after && after->get()->only().bytes().size() == 2,
88 "the next read reflects the device, not a store");
89 return ok ? 0 : 1;
90}
See also: graph module · vertex roles and aggregation · in-process pub/sub.