Wire codec round-trip (L2/L3)¶
The frame codec is the one place bytes become a tlv_t tree
and back. This example builds a structured PATH TLV for /sensor/temp (two NAME
children) with a CRC trailer, encodes it to wire bytes, decodes those bytes into a
fresh tree, and proves the round-trip is exact.
What to notice¶
Decode borrows, never copies — the decoded
NAMEpayloads arestd::spans that point into the encoded buffer; the example checks the payload address lies insidewire. Keeping a decodedtlv_tmeans keeping its backing bytes alive (that is what views provide).The CRC trailer is verified on decode —
opt.crmakesencodeappend a CRC-32C over the body;decoderecomputes and checks it, and surfaces the parsedcrc_t.The round-trip invariant — re-encoding the decoded tree reproduces the exact wire bytes (
encode(decode(bytes)) == bytes), CRC and all.
Source¶
1/*
2 * SPDX-License-Identifier: Apache-2.0
3 * SPDX-FileCopyrightText: Copyright 2026 avatarsd LLC
4 */
5
6/**
7 * @file
8 * @brief L2/L3 wire codec round-trip — build a TLV, encode to bytes, decode back.
9 *
10 * The wire codec is the one place bytes become a `tlv_t` tree and back
11 * (`docs/modules/frame-codec.md`). This example builds a structured PATH TLV
12 * (`/sensor/temp` — two NAME children) with a CRC trailer, `encode`s it to wire
13 * bytes, `decode`s those bytes into a fresh tree, and checks that re-encoding
14 * reproduces the exact wire bytes.
15 * It also shows the zero-copy nature of decode: the decoded payloads are
16 * `std::span`s that BORROW the encoded buffer, so no payload bytes are copied.
17 *
18 * Runs under ctest as `example_wire_roundtrip`: it checks structure, byte-identity,
19 * and the verified CRC trailer, returning non-zero on any mismatch.
20 */
21
22#include <algorithm>
23#include <cstddef>
24#include <cstdio>
25#include <span>
26#include <string>
27#include <vector>
28
29#include "libtracer/tracer.hpp"
30
31namespace {
32
33using tr::wire::opt_t;
34using tr::wire::tlv_t;
35using tr::wire::type_t;
36
37/** @brief A byte span over the characters of @p s (no copy; @p s must outlive the span). */
38std::span<const std::byte> bytes_of(const std::string& s) {
39 return {reinterpret_cast<const std::byte*>(s.data()), s.size()};
40}
41
42/** @brief A NAME TLV borrowing @p name's bytes. */
43tlv_t name_tlv(const std::string& name) {
44 tlv_t t;
45 t.type = type_t::NAME;
46 t.payload = bytes_of(name);
47 return t;
48}
49
50/** @brief Record a failed expectation on @p ok and report it. */
51void check(bool& ok, bool cond, const char* what) {
52 if (!cond) {
53 std::printf(" [FAIL] %s\n", what);
54 ok = false;
55 }
56}
57
58} // namespace
59
60int main() {
61 // The NAME segment bytes must outlive every TLV that borrows them.
62 const std::string seg0 = "sensor";
63 const std::string seg1 = "temp";
64
65 // Build a PACKED PATH TLV (RFC-0018 — `opt.PL = 0`, the body is a run of
66 // `[u8 len][bytes]` segment records) with a CRC trailer (opt.cr — encode recomputes
67 // the CRC-32C over the body).
68 std::vector<std::byte> packed;
69 (void)tr::wire::emit_path_segment(packed, seg0);
70 (void)tr::wire::emit_path_segment(packed, seg1);
71 tlv_t path;
72 path.type = type_t::PATH;
73 path.opt = opt_t{.cr = true};
74 path.payload = std::span<const std::byte>(packed);
75
76 // Encode the model to wire bytes, then decode those bytes back into a tree.
77 const std::vector<std::byte> wire = tr::wire::encode(path);
78 std::printf("encoded /sensor/temp PATH TLV: %zu bytes\n", wire.size());
79
80 const std::expected<tlv_t, tr::wire::err_t> decoded =
81 tr::wire::decode(std::span<const std::byte>(wire));
82
83 bool ok = true;
84 check(ok, decoded.has_value(), "decode succeeds (CRC trailer verifies)");
85 if (decoded) {
86 std::printf("decoded: type=0x%02X, %zu children, trailer.crc=%s\n",
87 static_cast<unsigned>(decoded->type), decoded->children.size(),
88 (decoded->trailer && decoded->trailer->crc) ? "present" : "absent");
89 check(ok, decoded->type == type_t::PATH, "decoded root is a PATH");
90 check(ok, !decoded->opt.pl, "a packed PATH is NOT structured (opt.PL = 0, RFC-0018)");
91 check(ok, decoded->children.empty(), "a packed PATH has no child TLVs");
92 check(ok, decoded->payload.size() == 1 + seg0.size() + 1 + seg1.size(),
93 "the body is one length byte per segment plus the segment text");
94 check(ok, decoded->trailer && decoded->trailer->crc.has_value(),
95 "decoded PATH carries the verified CRC trailer");
96 // The decoded payload borrows the encoded buffer — zero copy.
97 {
98 const auto body = decoded->payload;
99 check(ok, body.data() >= wire.data() && body.data() < wire.data() + wire.size(),
100 "the packed body is a span INTO the encoded buffer (zero copy)");
101 const auto want = bytes_of(seg0);
102 const bool same =
103 body.size() > want.size() &&
104 static_cast<std::size_t>(static_cast<std::uint8_t>(body[0])) == want.size() &&
105 std::equal(want.begin(), want.end(), body.begin() + 1);
106 check(ok, same, "the first packed record round-trips to \"sensor\"");
107 }
108 // The strongest round-trip invariant: re-encoding the decoded tree
109 // reproduces the exact wire bytes (byte-identical, CRC and all).
110 check(ok, tr::wire::encode(*decoded) == wire, "encode(decode(bytes)) == bytes");
111 }
112
113 std::printf("%s\n", ok ? "round-trip OK" : "round-trip FAILED");
114 return ok ? 0 : 1;
115}
See also: frame-codec module · bit-level wire walkthrough · data-format reference.