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 writev inherits a gather into one block drawn from the link’s egress source, so a rope’s to_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 send hides the other. Ordinary C++ name hiding, and a kind that omits using 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_recv plus start_receiving() for exactly that window (#1025 / #1045).

  • Owning delivery is a capability, not an assumption. delivers_ropes() is false by 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_t and 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.