One seam, every wire technology (transport plane)¶
tr::net::transport_t is the entire contract a transport kind has to satisfy. Three calls —
send(span), the optional scatter-gather send(iov), and set_receiver /
set_rope_receiver — and no fourth.
There is no send_read, no send_reply, and no TLV type anywhere in it. That absence is the
design: the router owns addressing (the dst source route), the codec owns structure, and a
transport owns exactly one question — how do these bytes cross this wire. It is why udp,
tcp, ws, can, quic, webtransport and the in-process loopback are interchangeable to
everything above them, and why writing a new kind is writing this one class.
The example writes a complete kind in about twenty lines, then drives the shipped
loopback_channel_t with the identical calls.
What to notice¶
A transport never sees TLV semantics. It is handed
std::span<const std::byte>and hands back the same. Callback-plus-recv-thread is an implementation choice about this C++ seam, not a protocol property (ADR-0013) — two conforming nodes need not share it, only the wire.The iovec overload is optional, and the base pays for the kinds that skip it. A kind that cannot
writevinherits a gather into one block drawn from the link’s egress source, so a rope’sto_iovec()reaches every kind. Overriding it elides that copy — never changes the contract; the bytes on the wire are one record either way.Declaring one
sendhides the other. Ordinary C++ name hiding, and a kind that omitsusing transport_t::send;silently loses the entry point it meant to inherit. The example carries the line and says why.The sink goes in before frames flow, and that is not advice. A kind whose receive thread starts in its own constructor is draining the wire while the owner is still wiring; a frame landing in an empty slot is dropped with no counter moving. Dialling kinds offer
defer_recvplusstart_receiving()for exactly that window (#1025 / #1045).Owning delivery is a capability, not an assumption.
delivers_ropes()isfalseby default and the example’s own kind leaves it there. There is deliberately no adapter that wraps a borrowed span in a rope whose refcounts would lie (ADR-0042 §1).Nothing here is conditional.
transport_tand the loopback channel are the required core, so this target builds and runs under every CI leg — including the minimal module set with the net plane and all four transports off.
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 — every wire technology reaches the routing plane through the SAME
9 * three-call seam, and the seam is deliberately ignorant: a `transport_t` moves
10 * framed BYTES and is never told what they mean.
11 *
12 * `tr::net::transport_t` is the whole contract a kind has to satisfy (ADR-0013):
13 *
14 * - `send(std::span<const std::byte>)` — emit one complete frame. Mandatory.
15 * - `send(std::span<const std::span<const std::byte>>)` — emit the SAME frame from
16 * scattered spans, as one record. Optional: the base gathers into one buffer drawn
17 * from the link's egress source, so a kind that has a real writev implements it and
18 * a kind that does not simply inherits the copy.
19 * - `set_receiver` / `set_rope_receiver` — where inbound frames land. Both may fire on
20 * the kind's own receive thread, and both must be installed BEFORE frames flow.
21 *
22 * That is the entire surface. There is no `send_read`, no `send_reply`, no TLV type
23 * anywhere in it, and that absence is the design: the router owns addressing (the `dst`
24 * source route), the codec owns structure, and the transport owns exactly one question —
25 * how do these bytes cross this wire. It is why `udp`, `tcp`, `ws`, `can`, `quic`,
26 * `webtransport` and the in-process loopback are interchangeable to everything above,
27 * and why writing a new kind is writing this class and nothing else.
28 *
29 * The example writes its own kind — 22 lines, no sockets — and then swaps in the shipped
30 * `loopback_channel_t` to show the identical calls driving a link with a real receive
31 * thread. Runs under ctest as `example_net_transport_seam`; returns non-zero on any
32 * failed check.
33 */
34
35#include <chrono>
36#include <condition_variable>
37#include <cstddef>
38#include <cstdio>
39#include <mutex>
40#include <span>
41#include <vector>
42
43#include "libtracer/loopback.hpp"
44#include "libtracer/transport.hpp"
45
46namespace {
47
48using namespace std::chrono_literals;
49
50/** @brief Report expectation @p what and record a failure on @p ok. */
51void check(bool& ok, bool cond, const char* what) {
52 std::printf(" [%s] %s\n", cond ? "ok" : "FAIL", what);
53 ok = ok && cond;
54}
55
56/**
57 * @brief A complete transport kind, in the smallest form the seam admits: it keeps
58 * every frame handed to it and can hand one back.
59 *
60 * Only the mandatory `send(span)` is overridden. The scatter-gather overload is
61 * deliberately NOT implemented, so the second half of this example can show what the base
62 * does about that — which is the reason the seam has two overloads instead of one.
63 */
64class memo_link_t final : public tr::net::transport_t {
65 public:
66 /** @brief Frames this link was asked to emit, in order. */
67 std::vector<std::vector<std::byte>> sent;
68
69 /**
70 * @brief Keep the base's scatter-gather overload visible.
71 *
72 * Declaring one `send` HIDES the other — ordinary C++ name hiding, and a kind that omits
73 * this line silently loses the iovec entry point it meant to inherit.
74 */
75 using tr::net::transport_t::send;
76
77 void send(std::span<const std::byte> frame) override {
78 sent.emplace_back(frame.begin(), frame.end());
79 }
80
81 /** @brief Deliver @p frame inbound, exactly as a receive thread would. */
82 void inject(std::span<const std::byte> frame) { rx_.deliver_borrowed(frame); }
83};
84
85/** @brief A thread-safe borrowed-span sink: a receive thread pushes, `main` waits. */
86class sink_t {
87 public:
88 /** @brief The receiver callback — copies the borrowed span, which dies at return. */
89 void operator()(std::span<const std::byte> frame) {
90 {
91 const std::lock_guard lock(m_);
92 frames_.emplace_back(frame.begin(), frame.end());
93 }
94 cv_.notify_all();
95 }
96
97 /** @brief Wait until at least @p n frames have landed, or @p budget expires. */
98 [[nodiscard]] bool wait_for(std::size_t n, std::chrono::milliseconds budget) {
99 std::unique_lock lock(m_);
100 return cv_.wait_for(lock, budget, [&] { return frames_.size() >= n; });
101 }
102
103 /** @brief How many frames have landed so far. */
104 [[nodiscard]] std::size_t count() const {
105 const std::lock_guard lock(m_);
106 return frames_.size();
107 }
108
109 /** @brief Frame @p i, by value. */
110 [[nodiscard]] std::vector<std::byte> at(std::size_t i) const {
111 const std::lock_guard lock(m_);
112 return frames_.at(i);
113 }
114
115 private:
116 mutable std::mutex m_;
117 std::condition_variable cv_;
118 std::vector<std::vector<std::byte>> frames_;
119};
120
121/** @brief @p n bytes counting up from @p seed — a stand-in for an encoded frame. */
122std::vector<std::byte> frame_of(std::size_t n, unsigned seed) {
123 std::vector<std::byte> f(n);
124 for (std::size_t i = 0; i < n; ++i) f[i] = static_cast<std::byte>(seed + i);
125 return f;
126}
127
128} // namespace
129
130int main() {
131 bool ok = true;
132
133 // 1. Egress. A frame is a span of bytes; the transport is told nothing else about it.
134 std::printf("the seam, on a kind written here:\n");
135 memo_link_t link;
136 const auto f1 = frame_of(6, 0x10);
137 link.send(f1);
138 check(ok, link.sent.size() == 1 && link.sent[0] == f1, "send(span) emitted the frame verbatim");
139
140 // 2. The scatter-gather overload this kind did NOT implement. The base concatenates the
141 // spans into ONE block from the link's egress source and calls the span overload — so a
142 // rope's `to_iovec()` reaches every kind, whether or not the kind can writev. What a kind
143 // gains by overriding it is the elision of exactly this copy, never a different contract:
144 // the bytes on the wire are the same one record either way.
145 const auto head = frame_of(3, 0x20);
146 const auto tail = frame_of(4, 0x30);
147 const std::span<const std::byte> parts[] = {head, tail};
148 link.send(std::span<const std::span<const std::byte>>{parts});
149 std::vector<std::byte> joined = head;
150 joined.insert(joined.end(), tail.begin(), tail.end());
151 check(ok, link.sent.size() == 2 && link.sent[1] == joined,
152 "send(iov) arrived as ONE frame — the base gathered it for a kind that cannot");
153
154 // 3. Ingress. The sink is installed before anything is delivered, because that ordering is
155 // the contract: a kind whose receive thread starts in its constructor is already draining
156 // the wire while the owner is still wiring, and a frame that lands in an empty slot is
157 // dropped with no counter moving. Kinds that dial offer `defer_recv` + `start_receiving()`
158 // for exactly this reason.
159 sink_t inbound;
160 link.set_receiver(inbound);
161 const auto f2 = frame_of(5, 0x40);
162 link.inject(f2);
163 check(ok, inbound.count() == 1 && inbound.at(0) == f2, "the borrowed-span sink got the frame");
164 check(ok, !link.delivers_ropes(), "and this kind claims no OWNING delivery (the default)");
165
166 // 4. The same three calls against a shipped kind with a real receive thread. Nothing about
167 // the call sites changes — that interchangeability IS the seam.
168 std::printf("the same seam, on the shipped in-process loopback kind:\n");
169 tr::net::loopback_channel_t channel;
170 sink_t at_b;
171 channel.b().set_receiver(at_b);
172 const auto f3 = frame_of(8, 0x50);
173 channel.a().send(f3);
174 check(ok, at_b.wait_for(1, 2s), "a frame sent on endpoint a arrived at endpoint b");
175 check(ok, at_b.count() == 1 && at_b.at(0) == f3, "byte-identical across the 'wire'");
176 channel.shutdown();
177
178 std::printf("one seam, %zu egress calls and %zu inbound frames, zero TLV semantics\n",
179 link.sent.size(), inbound.count() + at_b.count());
180 return ok ? 0 : 1;
181}
See also: transport module · module catalog reference · transports are vertices · a kind is a name.