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 read and write, 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 HANDLER vertex registered with an empty handlers_t allocates none, and a STORED_VALUE vertex given an on_children does (reference 02 §Vertex lifecycle; value_handlers_t in core/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 a ctx the 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_write is where the device acts. It receives the written value as a const value_t& — for a subscription delivery, the very block the source published, with no copy — and the writer’s write_ctx_t. Both are borrowed for the call only: a handler that keeps the value takes value_ref_t::keep(value), never the reference itself. The handler returns a result_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.