zerodds-endpoint-cpp 1.0 — Spec Coverage

Source: docs/specs/zerodds-endpoint-cpp-1.0.md (226 lines) — the normative C++17 endpoint SDK spec (XRCE framing, sync client, async Reader/Writer, reliable stream). Implemented in endpoints/cpp/: XRCE framing + the sync client over the C89 wire core zdw (endpoints/c), async Reader/Writer as a thin C++17 facade (zerodds_async.hpp), and the header-only reliable stream (zerodds_reliable.hpp). The generated topic_type_support<T> codec comes from crates/lang-rules, column Language::Cpp; this document covers the endpoint that transports it.

§1 XRCE framing

Spec: zerodds-endpoint-cpp-1.0.en.md §1 – DDS-XRCE 1.0 message header (8 bytes: session, stream, seq LE, submsg id, flags, len LE) + body; best-effort stream 0x01.

Repo: endpoints/c/include/zerodds_endpoint.h — zdw_xrce_write_frame / zdw_xrce_read_frame, ZDW_XRCE_SESSION_NOKEY, ZDW_XRCE_STREAM_BEST_EFFORT. The C++ SDK does not re-implement the framing — it binds the C89 wire core directly.

Tests: crates/endpoint-e2e/tests/cpp.rs::run_sync / run_async frame and deframe every ping/pong over real UDP.

Status: done.

§2 Sync client

Spec: zerodds-endpoint-cpp-1.0.en.md §2 – A blocking transport-poll client: zdw_endpoint_send / zdw_endpoint_recv over an integrator-supplied zdw_transport (deliver/receive callbacks, ADR-0013 frame hook), marshalling via the C++98 facade zerodds::Writer / zerodds::Reader.

Repo: endpoints/c/include/zerodds_endpoint.h (zdw_transport, zdw_endpoint_send/recv); endpoints/cpp/include/zerodds_wire.hpp (zerodds::Writer/Reader, a C++98 facade over the C89 core); C++ call site crates/endpoint-e2e/tests/cpp.rs::run_sync (UDP callbacks udp_deliver / udp_receive, 100 ms poll loop for up to 100 iterations); endpoints/cpp/example_sync.cpp (sensor-telemetry deep example Reading{id,value,label}, C++98 poll loop over an in-memory FIFO, full field decode).

Tests: (Linux, g++/gcc gated) cpp_endpoint_sync — live ping-pong E2E against the shared Rust XRCE peer over real UDP; plus cpp_raw_udp (bare XCDR2, no XRCE framing, a direct codec call through the generated types topic_type_support<Ping>/<Pong> from idl-cpp) as codec build integration for the same app compilation. 2/2 passed. Loud skip only when no compiler is on PATH.

Status: done.

§3 Async Reader/Writer

Spec: zerodds-endpoint-cpp-1.0.en.md §3 – An event-driven, non-blocking reader (a callback per received sample) and a fire-and-forget writer, both thin C++17 facades over the audited C reactor (zerodds_async.c) — additive to the conservative C++98 wire facade (zerodds_wire.hpp).

Repo: endpoints/cpp/include/zerodds_async.hpp — zerodds::AsyncReader (poll() / run(max), RAII, std::function trampoline over zdw_async_reader_init) and zerodds::AsyncWriter (write() over zdw_async_writer_init / zdw_async_write); endpoints/cpp/example_async.cpp (sensor-telemetry deep example, C++17 reactor).

Tests: (Linux, g++/gcc gated) crates/endpoint-e2e/tests/cpp.rs::run_async — AsyncReader dispatches the received pong to a lambda, AsyncWriter sends the ping; live ping-pong E2E against the Rust peer over real UDP; test name cpp_endpoint_async. 1/1 passed. Loud skip only when no compiler is on PATH.

Status: done.

§4 Reliable stream — state machine, async writer, loss recovery, byte-golden, latency

Spec: zerodds-endpoint-cpp-1.0.en.md §4 (normative: reliable-endpoint-1.0.en.md) – DDS-XRCE reliable stream (stream_id ≥ 128, §8.4.10/§8.4.11), mirroring the reference crates/xrce/src/reliable.rs: sender submit/pending_heartbeat/recv_acknack/get_in_flight; receiver recv_data/drain_in_order/pending_acknack/reset. The async writer decouples the producer from I/O: enqueue() is wait-free into a lock-free SPSC ring; a dedicated drain thread owns the Sender state, batches WRITE_DATA via sendmmsg, fires HEARTBEATs periodically, and retransmits on ACKNACK. The producer must never enter the kernel on the async-decoupled path — a measurement should show the decoupling against the inline-sendto path (design rationale from reliable-endpoint-1.0.en.md §2).

Repo: endpoints/cpp/include/zerodds_reliable.hpp (381 lines, header-only C++17) — Sender/Receiver state machine, wire codec (write_frame/unframe/acknack_frame/heartbeat_frame/parse_acknack/ parse_heartbeat), AsyncWriter (SPSC ring with std::atomic<size_t> head_/tail_, std::thread drain, enqueue/finish/stop). Pure C++17, header-only; no linking against the Rust layer, cross-compile-safe. endpoints/cpp/example_reliable.cpp (in-process demo: sender + receiver, every 3rd sample dropped in the first round, ACKNACK-driven recovery rounds). endpoints/cpp/test/test_reliable_cpp.cpp (12 unit checks + byte-golden + latency bench).

Tests: (Linux, g++12)

  • cpp_reliable_unit_and_golden — 12 unit checks (sender: monotonic seq, payload-too-large, window-full, heartbeat first/silence/period; receiver: acknack-clear, acknack-full-clear, in-order, reorder, duplicate-drop, buffer-full, pending-acknack bitmap, reset) + byte-golden (acknack_frame(0x80, NONE, 1, 1, 0, 0, 0x80) == golden_acknack_le.bin, heartbeat_frame(0x80, NONE, 1, 1, 3, 0x80) == golden_heartbeat_le.bin, 13 bytes each, byte-identical to the Rust-generated goldens) + a latency bench (median over K=20000 iterations: AsyncWriter::enqueue, no-op drain hooks, no syscall, vs write_frame + inline send to a bound loopback sink) → ALL OK.
  • cpp_reliable_loss_recovery — the Rust peer drops every 3rd datagram; the app retransmits on ACKNACK; 12/12 samples delivered gap-free in order.
  • cpp_reliable_lossless_baseline — lossless; 12/12.
  • cpp_reliable_example — in-process demo → RELIABLE OK N=16.

4/4 passed. Latency measured: 31–40 ns (enqueue) vs 3427 ns (inline sendto), ~85–110× depending on the run (run-to-run variance, not a single value).

Finding during development: a drain deadlock with no responder — finish() waits until both window and ring are empty, which never happens without an ACKNACK reply. Fixed by adding stop() (unconditional teardown, for responder-less contexts such as the latency bench).

Status: done — sender and receiver state machine unit-verified; the live E2E against the Rust peer covers the sender role (app sends, Rust peer receives + ACKNACKs); the receiver role is verified in-process (example_reliable.cpp), not live-network against an external sender; latency decoupling measured.

§5 Test and evidence obligations

Spec: zerodds-endpoint-cpp-1.0.en.md §5 (mirrors reliable-endpoint-1.0.en.md §5) – unit, byte-golden, E2E loss recovery, a latency bench, a runnable example_reliable_*; no false-green, a loud skip only when the toolchain is missing.

Repo: All five required artifacts present, evidenced in §4: test_reliable_cpp.cpp (unit + byte-golden + latency bench), crates/endpoint-e2e/tests/cpp_reliable.rs (loss recovery + lossless baseline against the Rust peer), example_reliable.cpp (a runnable example, not a stub). Loud skip only when g++/gcc is missing from PATH.

Tests: see §4 (cpp_reliable_unit_and_golden, cpp_reliable_loss_recovery, cpp_reliable_lossless_baseline, cpp_reliable_example) and §2/§3 (cpp_raw_udp, cpp_endpoint_sync, cpp_endpoint_async).

Status: done.

Audit status

5 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).

Test run (Linux, g++12, verified): cargo test -p zerodds-endpoint-e2e --test cpp → 3/3 (cpp_raw_udp, cpp_endpoint_sync, cpp_endpoint_async); --test cpp_reliable → 4/4 (cpp_reliable_unit_and_golden incl. 12-checks+byte-golden+bench, cpp_reliable_loss_recovery, cpp_reliable_lossless_baseline, cpp_reliable_example); latency bench enqueue 31–40 ns / inline sendto 3427 ns.

Open items: none.

zerodds-endpoint-cpp 1.0 — Spec-Coverage

Quelle: docs/specs/zerodds-endpoint-cpp-1.0.md (226 Zeilen) — die normative C++17-Endpoint-SDK-Spec (XRCE-Framing, sync Client, async Reader/Writer, reliable Stream). Implementiert in endpoints/cpp/: das XRCE-Framing + der sync Client über den C89-Wire-Core zdw (endpoints/c), async Reader/Writer als dünne C++17-Fassade (zerodds_async.hpp), und der header-only reliable Stream (zerodds_reliable.hpp). Der generierte topic_type_support<T>-Codec kommt aus crates/lang-rules, Spalte Language::Cpp; hier steht der Endpoint, der ihn transportiert.

§1 XRCE-Framing

Spec: zerodds-endpoint-cpp-1.0.md §1 – DDS-XRCE-1.0-Message-Header (8 Byte: session, stream, seq LE, submsg id, flags, len LE) + Body; best-effort Stream 0x01.

Repo: endpoints/c/include/zerodds_endpoint.h — zdw_xrce_write_frame / zdw_xrce_read_frame, ZDW_XRCE_SESSION_NOKEY, ZDW_XRCE_STREAM_BEST_EFFORT. Der C++-SDK re-implementiert das Framing nicht, sondern bindet den C89-Wire-Core direkt ein.

Tests: crates/endpoint-e2e/tests/cpp.rs::run_sync / run_async framen bzw. deframen jedes Ping/Pong über echtes UDP.

Status: done.

§2 sync Client

Spec: zerodds-endpoint-cpp-1.0.md §2 – Ein blockierender Transport-Poll-Client: zdw_endpoint_send / zdw_endpoint_recv über einen integrator-gestellten zdw_transport (deliver/receive-Callbacks, ADR-0013-Frame-Hook), Marshalling per C++98-Fassade zerodds::Writer / zerodds::Reader.

Repo: endpoints/c/include/zerodds_endpoint.h (zdw_transport, zdw_endpoint_send/recv); endpoints/cpp/include/zerodds_wire.hpp (zerodds::Writer/Reader, C++98-Fassade über den C89-Kern); C++-Aufrufstelle crates/endpoint-e2e/tests/cpp.rs::run_sync (UDP-Callbacks udp_deliver / udp_receive, 100-ms-Poll-Loop bis zu 100 Iterationen); endpoints/cpp/example_sync.cpp (Sensor-Telemetrie-Deep-Example Reading{id,value,label}, C++98-Poll-Loop über eine In-Memory-FIFO, voller Feld-Decode).

Tests: (Linux, g++/gcc gated) cpp_endpoint_sync — Live-Ping-Pong-E2E gegen den geteilten Rust-XRCE-Peer über echtes UDP; sowie cpp_raw_udp (bares XCDR2 ohne XRCE-Framing, direkter Codec-Aufruf über die generierten Typen topic_type_support<Ping>/<Pong> von idl-cpp) als Codec-Build-Integration derselben App-Kompilierung. 2/2 passed. Lauter Skip nur bei fehlendem Compiler auf dem PATH.

Status: done.

§3 async Reader/Writer

Spec: zerodds-endpoint-cpp-1.0.md §3 – Ein event-driven, nicht-blockierender Reader (Callback pro empfangenem Sample) und ein Fire-and-forget-Writer, beide dünne C++17-Fassaden über den auditierten C-Reaktor (zerodds_async.c) — additiv zur konservativen C++98-Wire-Fassade (zerodds_wire.hpp).

Repo: endpoints/cpp/include/zerodds_async.hpp — zerodds::AsyncReader (poll() / run(max), RAII, std::function-Trampolin über zdw_async_reader_init) und zerodds::AsyncWriter (write() über zdw_async_writer_init / zdw_async_write); endpoints/cpp/example_async.cpp (Sensor-Telemetrie-Deep-Example, C++17-Reactor).

Tests: (Linux, g++/gcc gated) crates/endpoint-e2e/tests/cpp.rs::run_async — AsyncReader dispatcht das empfangene Pong an eine Lambda, AsyncWriter schickt das Ping; Live-Ping-Pong-E2E gegen den Rust-Peer über echtes UDP; Testname cpp_endpoint_async. 1/1 passed. Lauter Skip nur bei fehlendem Compiler auf dem PATH.

Status: done.

§4 Reliable Stream — State-Machine, Async-Writer, Loss-Recovery, Byte-Golden, Latenz

Spec: zerodds-endpoint-cpp-1.0.md §4 (normativ: reliable-endpoint-1.0.md) – DDS-XRCE reliable Stream (stream_id ≥ 128, §8.4.10/§8.4.11), spiegelt die Referenz crates/xrce/src/reliable.rs: Sender submit/pending_heartbeat/recv_acknack/get_in_flight; Receiver recv_data/drain_in_order/pending_acknack/reset. Der Async-Writer entkoppelt den Producer vom I/O: enqueue() ist wait-free in einen lock-free SPSC-Ring, ein dedizierter Drain-Thread besitzt den Sender-State, batched WRITE_DATA via sendmmsg, feuert HEARTBEATs periodisch und retransmittet auf ACKNACK. Der Producer darf im async-entkoppelten Pfad nie in den Kernel eintreten — ein Messwert soll die Entkopplung gegen den inline-sendto-Pfad zeigen (Design-Motiv aus reliable-endpoint-1.0.md §2).

Repo: endpoints/cpp/include/zerodds_reliable.hpp (381 Zeilen, header-only C++17) — Sender/Receiver-State-Machine, Wire-Codec (write_frame/unframe/acknack_frame/heartbeat_frame/parse_acknack/ parse_heartbeat), AsyncWriter (SPSC-Ring mit std::atomic<size_t> head_/tail_, std::thread-Drain, enqueue/finish/stop). Pure C++17, header-only; kein Linking gegen Rust-Layer, cross-compile-fest. endpoints/cpp/example_reliable.cpp (In-Process-Demo: Sender + Receiver, jedes 3. Sample im ersten Round gedroppt, ACKNACK-getriebene Recovery-Runden). endpoints/cpp/test/test_reliable_cpp.cpp (12 Unit-Checks + Byte-Golden + Latenz-Bench).

Tests: (Linux, g++12)

  • cpp_reliable_unit_and_golden — 12 Unit-Checks (Sender: monotone seq, payload-too-large, window-full, heartbeat first/silence/period; Receiver: acknack-clear, acknack-full-clear, in-order, reorder, duplicate-drop, buffer-full, pending-acknack-Bitmap, reset) + Byte-Golden (acknack_frame(0x80, NONE, 1, 1, 0, 0, 0x80) == golden_acknack_le.bin, heartbeat_frame(0x80, NONE, 1, 1, 3, 0x80) == golden_heartbeat_le.bin, je 13 Byte, byte-identisch zu den Rust-generierten Goldens) + Latenz-Bench (Median über K=20000 Iterationen: AsyncWriter::enqueue, No-op-Drain-Hooks, kein Syscall, vs. write_frame + inline send auf einen gebundenen Loopback-Sink) → ALL OK.
  • cpp_reliable_loss_recovery — der Rust-Peer dropt jedes 3. Datagramm; App retransmittet auf ACKNACK; 12/12 Samples lückenlos in-order geliefert.
  • cpp_reliable_lossless_baseline — lossless; 12/12.
  • cpp_reliable_example — In-Process-Demo → RELIABLE OK N=16.

4/4 passed. Latenz gemessen: 31–40 ns (enqueue) vs. 3427 ns (inline sendto), ~85–110× je nach Lauf (Lauf-zu-Lauf-Streuung, kein Einzelwert).

Fund während der Entwicklung: ein Drain-Deadlock ohne Responder — finish() wartet, bis Fenster und Ring leer sind, was ohne ACKNACK-Antwort nie eintritt. Gefixt durch stop() (unconditionaler Teardown, für responder-lose Kontexte wie den Latenz-Bench).

Status: done — Sender- und Receiver-State-Machine unit-verifiziert; das live-E2E gegen den Rust-Peer deckt die Sender-Rolle ab (App sendet, Rust-Peer empfängt + ACKNACKt); die Receiver-Rolle ist in-process (example_reliable.cpp) verifiziert, nicht live-network gegen einen externen Sender; Latenz-Entkopplung gemessen.

§5 Test- und Beleg-Pflicht

Spec: zerodds-endpoint-cpp-1.0.md §5 (spiegelt reliable-endpoint-1.0.md §5) – Unit, Byte-Golden, E2E-Loss-Recovery, Latenz-Bench, ein lauffähiges example_reliable_*; kein false-green, lauter Skip nur bei fehlender Toolchain.

Repo: Alle fünf Pflicht-Artefakte vorhanden, belegt in §4: test_reliable_cpp.cpp (Unit + Byte-Golden + Latenz-Bench), crates/endpoint-e2e/tests/cpp_reliable.rs (Loss-Recovery + Lossless-Baseline gegen den Rust-Peer), example_reliable.cpp (lauffähiges Example, kein Stub). Lauter Skip nur bei fehlendem g++/gcc auf dem PATH.

Tests: siehe §4 (cpp_reliable_unit_and_golden, cpp_reliable_loss_recovery, cpp_reliable_lossless_baseline, cpp_reliable_example) sowie §2/§3 (cpp_raw_udp, cpp_endpoint_sync, cpp_endpoint_async).

Status: done.

Audit-Status

5 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).

Test-Lauf: Linux, g++12, verifiziert — cargo test -p zerodds-endpoint-e2e --test cpp → 3/3 (cpp_raw_udp, cpp_endpoint_sync, cpp_endpoint_async); --test cpp_reliable → 4/4 (cpp_reliable_unit_and_golden inkl. 12-Checks+Byte-Golden+Bench, cpp_reliable_loss_recovery, cpp_reliable_lossless_baseline, cpp_reliable_example); Latenz-Bench enqueue 31–40 ns / inline sendto 3427 ns.

Offene Punkte: keine.