transport — the wire seam (L4)

In one paragraph

transport_t is the seam between the FWD router and one wire technology: send framed bytes (single buffer or a scatter-gather iovec), register a receiver_t for inbound frames. It never sees TLV semantics — only bytes. Implementations: loopback_channel_t (in-process dev/test), udp_transport_t (localhost/LAN UDP), tcp_transport_t (reliable TCP stream, 4-byte u32-LE length-prefix framing — the prefix is transport framing, not part of the TLV), transport_ws_client / transport_ws_server (the browser↔robot WebSocket keystone, RFC 6455), transport_can (SocketCAN, classic + CAN-FD), quic_transport_t (the separate libtracer_quic module, msquic — TLS 1.3, connection migration; one bidirectional stream carrying the same length-prefix framing as TCP; a host that talks QUIC links the module and registers quic_transport_factory(); ADR-0043 Phase A — the hosted, secure link; the MCU class keeps UDP/CAN), and webtransport_transport_t (same module, ADR-0043 Phase B — WebTransport over HTTP/3, the browser-reachable form of QUIC: a minimal module-private H3/QPACK handshake layer in front of one WebTransport bidirectional stream carrying the same length-prefix framing; kind webtransport via webtransport_transport_factory(); the TS twin is @avatarsd-llc/libtracer-webtransport — the #92 / ADR-0031 browser↔robot path).

What it does

A transport accepts a complete frame’s bytes (a FWD frame, or a route-handle control frame — ADVERTISE/COMPACT/HANDLE_NACK) and emits them; inbound frames arrive on the registered receiver (which may fire on an internal transport thread). The reference catalog defines a poll-based transport_vtable; the C++ seam is callback + recv-thread — an implementation choice that matches how a real socket’s receive loop feeds the FWD router.

A transport that can hand up owning frames additionally implements the rope-receiver seam (ADR-0042, generalized to ropes by ADR-0053): it overrides delivers_ropes() and delivers each inbound frame as a rope_t of refcounted links — a contiguous frame is the single-link case (e.g. tcp_transport_t reads a frame straight into one segment); a scattered one (a CAN reassembly group, a fragmented WS message) crosses the seam as the rope it already is, never a flatten copy. fwd_router_t::add_child installs whichever receiver matches the link’s capability; every other transport keeps the borrowed-span receiver.

loopback_channel_t wires two endpoints: a frame sent on one is delivered to the other’s receiver on that endpoint’s thread (modeling async cross-“wire” delivery). shutdown() joins the receive threads before the receivers are destroyed.

Interface

using peer_id_t = std::array<std::byte, 16>;       // the node identity

class transport_t {
    virtual void send(std::span<const std::byte> frame) = 0;
    // Scatter-gather: ship a rope's to_iovec() as one frame, no flatten copy.
    // Default gathers+send(); native transports override (sendmsg/writev/RDMA-SGE).
    virtual void send(std::span<const std::span<const std::byte>> iov);
    using receiver_t = std::function<void(std::span<const std::byte>)>;
    virtual void set_receiver(receiver_t) = 0;
};

class loopback_channel_t {                          // dev/test transport
    loopback_endpoint_t& a();  loopback_endpoint_t& b();  // each a transport_t
    void shutdown();                                // join recv threads
};

class udp_transport_t : public transport_t {        // real UDP (M5)
    udp_transport_t(uint16_t bind_port, const std::string& peer_host, uint16_t peer_port);
    // send(span) = one sendto; send(iov) = one sendmsg(iovec) — the structural
    // batch (a composite rope in one syscall; see Performance).
};

The forward hop that feeds a transport (zero-heap)

What the FWD router hands send(iov) on a forward hop is not a re-encoded frame: the hop reads a few headers of the inbound frame by offset, builds the shortened headers in small stack buffers, and scatter-gathers those heads with untouched views of the inbound frame — zero heap allocations, CI-gated (bench_forward_heap, ZEROHEAP_MAX=0).

        flowchart LR
    IN["inbound frame bytes"] --> PEEK["offset peek:<br/>first dst NAME"]
    PEEK --> DEMUX["child_registry_t<br/>NAME → transport"]
    DEMUX --> SG["stack-built heads<br/>+ untouched frame views"]
    SG -->|"send(iov) — one syscall"| T(["transport_t"])
    

Two nodes over a wire

        flowchart LR
    FA["fwd_router A"] -->|send| EA["endpoint a"]
    EA -->|enqueue| QB[("inbox B")]
    QB -->|recv thread| EB["endpoint b"]
    EB -->|receiver| FB["fwd_router B"]
    FB -->|FWD REPLY send| EB2["endpoint b"]
    EB2 -->|enqueue| QA[("inbox A")]
    QA -->|recv thread| EA2["endpoint a"]
    EA2 -->|receiver| FA
    classDef m fill:#fef9c3,stroke:#92400e
    class EA,EB,EA2,EB2 m
    

Benefits

  • One seam, many wires — the FWD router and the whole stack above are transport- agnostic; a new socket transport plugs in with no changes upstream.

  • Deterministic testing — the loopback exercises the full encode→FWD-route→decode path with no sockets, so forward/reply behavior is unit-testable (and the benchmark measures it).

  • Bytes only — a transport can’t accidentally depend on graph semantics.

API reference

Generated from core/include/libtracer/transport.hpp by Doxygen.

class transport_t

A point-to-point (or bus-facet-exposing) transport link: the byte seam between the routing plane and one wire (ws/tcp/udp/quic/CAN).

The router sends complete TLV frames via send and receives them through an installed sink (set_receiver for borrowed spans, or set_rope_receiver for owning refcounted rope frames when delivers_ropes is true). A multi-peer bus link additionally exposes a bus_link_t facet via bus.

Subclassed by tr::net::loopback_endpoint_t, tr::net::quic_transport_t, tr::net::tcp_transport_t, tr::net::transport_can, tr::net::transport_ws_client, tr::net::transport_ws_server, tr::net::udp_transport_t, tr::net::webtransport_transport_t

Public Types

using receiver_t = std::function<void(std::span<const std::byte>)>

The borrowed-span inbound sink: a frame valid only for the callback.

using rope_receiver_t = std::function<void(view::rope_t)>

The OWNING inbound sink (ADR-0042, generalized to ropes per ADR-0053): each frame is a rope_t of refcounted links the receiver may keep, subrope, or forward — a contiguous frame is the trivial single-link case (“delivers views” and “delivers ropes” are ONE capability, not two tiers — CONTEXT.md §ingress rope delivery).

Public Functions

virtual void send(std::span<const std::byte> frame) = 0

Emit one frame (a complete TLV’s bytes) onto the wire.

inline virtual void send(std::span<const std::span<const std::byte>> iov)

Scatter-gather send: emit the gathered spans as ONE frame, no flatten copy.

Hand a rope’s to_iovec() straight to the wire. The default gathers into a temporary and calls send(std::span<const std::byte>); transports with native scatter-gather (sendmsg/writev/RDMA SGE) override this to avoid the copy.

Parameters:

iov – The spans to emit, in order, as a single frame.

virtual void set_receiver(receiver_t receiver) = 0

Register the borrowed-span sink for inbound frames (the bridge’s ingest).

Must be set before frames flow; delivery may occur on an internal transport thread. The delivered span is valid only for the callback — a receiver that needs to keep the frame uses set_rope_receiver instead.

Parameters:

receiver – The inbound frame sink.

inline virtual void set_rope_receiver(rope_receiver_t receiver)

Register the optional OWNING inbound sink (the ADR-0042 receiver seam).

A transport that can hand up owning frames (its delivers_ropes returns true) delivers each inbound frame to receiver as a view::rope_t whose links are refcounted views over segments drawn from a host-injected mem_backend_t — the receiver may pin, subrope, or forward the frame beyond the callback (unlike the borrowed span of set_receiver, which dies when the callback returns). A contiguous frame arrives as a single-link rope; a scattered one (a CAN reassembly group, fragmented WS message) crosses this seam AS THE ROPE IT ALREADY IS — reassembly is chaining views, never a memcpy (ADR-0053 §5). Must be set before frames flow; delivery may occur on an internal transport thread.

The base implementation is a documented no-op: a span-only transport ignores an installed rope receiver, honestly — there is NO adapter that wraps a borrowed span into a rope whose refcounts would lie about lifetime (ADR-0042 §1). A transport that honors this seam MUST also override delivers_ropes to return true, so fwd_router_t::add_child installs the receiver matching the link’s capability.

inline virtual bool delivers_ropes() const

The owning-delivery capability (ADR-0042 §1): true iff this transport honors set_rope_receiver by delivering refcounted rope frames.

inline virtual bus_link_t *bus()

The multi-peer (bus) capability (ADR-0044): non-null iff this link reaches many peers and exposes them via bus_link_t.

A point-to-point transport keeps the default nullptr; a bus transport (the CAN binding) returns its own bus_link_t facet, which the router and the connection vertex consult for peer resolution and peer enumeration.

The optional multi-peer (bus) capability of a transport link (ADR-0044).

A point-to-point link (ws/tcp/udp/quic) carries exactly one peer, so its child NAME fully addresses the far side. A BUS link (CAN) reaches many peers over one wire; this interface is how such a link exposes them to the routing plane with ZERO stored graph state (ADR-0044 §1 — no vertex is ever created for a peer):

  • enumerate_peers synthesizes, on the fly, the names of the peers currently audible on the bus (from the transport kind’s own live announce/heartbeat traffic) — the :children[] listing of the link’s connection vertex;

  • peer_link resolves one such NAME to a directed sending endpoint, the seam child_registry_t falls back to when a FWD’s next dst segment names no static child — so a peer name IS a routable hop segment;

  • set_peer_receiver replaces the flat inbound sink with a peer-named one: each inbound frame arrives tagged with the SENDING peer’s name, which the router uses as the hop’s inbound NAME — so the return route grown into src names the bus peer to route the reply back to, symmetrically, with no per-request state.

Peer names are transport-defined but MUST be deterministic and collision-safe within the bus (the CAN binding derives them from the structured ID’s node field). All three calls may race the transport’s receive thread; impls synchronize internally.

Subclassed by tr::net::transport_can

Public Types

Visitor invoked once per currently-audible peer name.

The peer-named inbound sink: (sending peer’s name, frame bytes).

The OWNING peer-named sink (ADR-0053 §5): (sending peer’s name, the reassembled frame as the rope it already is — refcounted links the receiver may keep, subrope, or forward past the callback).

Public Functions

Visit the peers currently audible on the bus (a live-traffic snapshot).

Note

Synthesized on the fly — no call allocates peer state or graph structure.

Resolve a peer NAME to a directed sending endpoint on this bus.

The returned transport sends to THAT peer only (the bus binding’s directed framing); it is owned by this link and stays valid for the link’s lifetime.

Return values:

nullptrpeer names no currently-known bus peer.

Register the peer-named inbound sink (used INSTEAD of set_receiver).

Must be set before frames flow; delivery may occur on an internal transport thread. When set, it takes precedence over a flat transport_t receiver.

Register the OWNING peer-named sink (ADR-0053 §5) — used INSTEAD of set_peer_receiver when the bus delivers_ropes.

A reassembling bus (CAN groups, fragmented WS) hands the frame up as the rope its reassembly already built — chained refcounted slice views, never a flatten memcpy; transport padding is trimmed by shortening the tail link. The base implementation is a documented no-op, the same honesty rule as transport_t::set_rope_receiver: a span-only bus must not fake ownership.

True iff this bus honors set_peer_rope_receiver (ADR-0053 §5).

See: interface-map, reference §communication-flows.