zerodds-endpoint-rust 1.0 — Spec Coverage

Source: docs/specs/zerodds-endpoint-rust-1.0.md — the ZeroDDS Rust endpoint SDK spec (ADR 0013 + DDS-XRCE 1.0 §8.3/§8.4.10/§8.4.11 + reliable-endpoint-1.0).

Implementation:

  • endpoints/rust/ — the native Rust endpoint SDK (ADR 0013): src/lib.rs (XRCE framing, Client, AsyncReader), src/reliable.rs (reliable state machine + async-decoupled writer), examples/{example_sync,example_async,example_reliable}.rs.
  • crates/endpoint-e2e/ — the shared Rust XRCE peer (src/lib.rs) + the reliable E2E (tests/rust_reliable.rs).

§1 XRCE framing

§1 WRITE_DATA frame (8-byte header + body)

Spec: §1 – “A WRITE_DATA frame is an 8-byte header plus an XCDR2 sample body: [session, stream, seq_lo, seq_hi, submsg_id, flags, len_lo, len_hi] + body. session=0x80, stream=0x01 (best-effort), submsg_id=0x07, flags=0x03.”

Repo: endpoints/rust/src/lib.rs::xrce_write_frame/xrce_read_frame (line 62–85, best-effort stream 0x01, session 0x80). The shared peer has its own, independently written framing implementation crates/endpoint-e2e/src/lib.rs::xrce_frame/xrce_unframe (line 49–72) — the same 8-byte layout, the same constants (0x80/0x01/0x07/0x03), byte-identical despite the separate implementation.

Tests: indirect, via example_sync/example_async (CI job endpoints-gate, column rust); no isolated byte-golden unit test for the best-effort frame (the reliable frame has its own, see §4).

Status: done

§1 Peer role in the ping-pong (no standalone Rust ping-pong test)

Spec: §1 – “Because Rust plays the peer role here (the other 13 languages talk to it), there is no standalone Rust-vs-Rust ping-pong test — framing conformance is checked transitively by each of the 13 language E2E tests in crates/endpoint-e2e/tests/.”

Repo: crates/endpoint-e2e/src/lib.rs::ping_pong (line 97–128) is the peer itself. There is no Rust ping-pong file in crates/endpoint-e2e/tests/, because Rust is not “the app” here — it is the peer every other language talks to; a Rust ping-pong test against itself would not check anything additional. That does not hold for the reliable stream: there Rust is the app, and crates/endpoint-e2e/tests/rust_reliable.rs does exist (see §4). endpoints/rust’s own Client/Reader pair is instead exercised through the MemTransport examples (§2/§3), not through the UDP peer path.

Tests: transitive — every one of the 14 language E2E tests (crates/endpoint-e2e/tests/ada.rs, crates/endpoint-e2e/tests/c.rs, crates/endpoint-e2e/tests/cpp.rs, …) exercises ping_pong; no direct Rust-own test.

Status: n/a (informative) — structural, not open: Rust is the peer implementation, not a tested client of this peer.

§2 sync Client

§2 Client::write/poll

Spec: §2 – “A polling, non-blocking receive path — the idiom for callers that own their own run loop. write frames an XCDR2 sample body with the current seq (wrapping_add), poll returns a decoded body or None.”

Repo: endpoints/rust/src/lib.rs::Client (line 113–138): write frames an XCDR2 sample body and delivers it via MemTransport; poll returns a decoded body or None (non-blocking).

Tests: endpoints/rust/examples/example_sync.rs — 5 Reading{id,value,label} samples, decoded field by field (id, value, label), assert!(got == total) + "ALL OK"; CI job endpoints-gate (column rust).

Status: done

§3 async Reader/Writer

§3 AsyncReader (thread + mpsc)

Spec: §3 – “A background reader that delivers decoded samples over a channel — no async runtime, but the idiomatic std concurrency model (thread + channel). start spawns a thread, recv blocks, stop signals via an AtomicBool.”

Repo: endpoints/rust/src/lib.rs::AsyncReader (line 142–180): start spawns a thread that polls the transport and forwards decoded bodies to recv via mpsc::channel; stop signals the thread via an AtomicBool.

Tests: endpoints/rust/examples/example_async.rs — 5 samples, reader.recv() blocking, every field decoded, "ALL OK"; CI job endpoints-gate (column rust).

Status: done

§4 reliable stream

§4 State machine (sender + receiver)

Spec: §4 – “XRCE reliable stream (stream_id >= 128, §8.4.10/§8.4.11), window 16 (matches the 16-bit ACKNACK bitmap), receiver buffer 64, heartbeat 500 ms, payload ≤ 65535, RFC-1982 16-bit sequence numbers — mirrors crates/xrce/src/reliable.rs.”

Repo: endpoints/rust/src/reliable.rs — ReliableSender (submit/ pending_heartbeat/recv_acknack/get_in_flight/in_flight_seqs, line 201–292), ReliableReceiver (recv_data/drain_in_order/pending_acknack/reset, line 300–376). Self-contained, no zerodds-xrce runtime dependency (deliberately duplicated rather than imported, see the module doc comment, line 1–21).

Tests: cargo test -p zerodds-endpoint-rust --lib — 14 state-machine tests (submit_assigns_monotonic_seqnrs, submit_rejects_payload_too_large, submit_rejects_when_window_full, heartbeat_fires_first_then_silences_until_period, heartbeat_none_when_window_empty, recv_acknack_clears_acked_keeps_missing, recv_acknack_full_clear_when_no_bits, recv_data_delivers_in_order, recv_data_reorders_out_of_order, recv_data_drops_duplicates, recv_data_rejects_when_buffer_full, pending_acknack_marks_missing_slots, reset_clears_receiver, end_to_end_loss_recovery_in_process) + 2 wire/ring tests (write_frame_round_trips, spsc_ring_fifo_and_backpressure) + 2 byte-golden (next item) = 18 of 18 in the crate. Reproduced locally (this run): 18/18 passed, 0 failed.

Status: done

§4 Wire (byte-golden HEARTBEAT/ACKNACK)

Spec: §4 – “HEARTBEAT (0x0B) and ACKNACK (0x0A) byte-identical to the C SDK’s reference goldens (golden_heartbeat_le.bin/golden_acknack_le.bin); XRCE control convention: header stream = NONE (0x00) plus control-message seq, target stream id in the body’s last byte.”

Repo: endpoints/rust/src/reliable.rs::heartbeat_frame/acknack_frame/ parse_heartbeat/parse_acknack (line 125–192).

Tests: heartbeat_frame_byte_golden asserts heartbeat_frame(Heartbeat{first:1,last:3,stream_id:0x80},1) == [128,0,1,0,11,1,5,0,1,0,3,0,128]; acknack_frame_byte_golden asserts acknack_frame(AckNack{first_unacked:1,nack_bitmap:[0,0],stream_id:0x80},1) == [128,0,1,0,10,1,5,0,1,0,0,0,128] — byte-identical to the C goldens.

Status: done

§4 Async-decoupled writer + loss recovery (E2E)

Spec: §4 – “The producer only enqueues (wait-free) into an SPSC ring; a dedicated drain thread owns the socket and the reliable sender state and does all I/O (WRITE_DATA send, the HEARTBEAT timer, ACKNACK-driven retransmit). The producer never enters the kernel. Reliable delivery survives datagram loss — verified live against the shared Rust peer with injected loss.”

Repo: endpoints/rust/src/reliable.rs::SpscRing (line 384–443, wait-free push/pop over two atomics, no lock), ReliableWriterHandle::enqueue (line 458–460, never enters the kernel), AsyncReliableWriter::start/drain_loop (line 469–574: ring → sender window → send, HEARTBEAT when due, ACKNACK drain → retransmit, shutdown() blocks until the ring is empty AND the window is empty, with a 5s safety deadline against a peer that stops ACKing). crates/endpoint-e2e/src/lib.rs::reliable_collect drives the peer as the receiver (with drop_every=Some(3), drops every 3rd distinct sample exactly once).

Tests: (Linux) cargo test -p zerodds-endpoint-e2e --test rust_reliable — rust_reliable_loss_recovery (peer drops every 3rd sample) and rust_reliable_no_loss_baseline (lossless) — each delivers 12/12 samples gap-free in order (N = 12 in tests/rust_reliable.rs). 2/2 passed. Runs in the CI job endpoint-e2e (cargo test -p zerodds-endpoint-e2e). Reproduced locally (this run): 2/2 passed in 2.47s.

Status: done

§4 Rust as the shared peer (no standalone Rust loss-recovery test)

Spec: §4 – “Rust is simultaneously the shared reliable peer (crates/endpoint-e2e, reliable_collect) for all other languages — as with §1 there is no standalone Rust-vs-Rust loss-recovery test; the loss-recovery conformance of endpoints/rust’s own AsyncReliableWriter is checked via crates/endpoint-e2e/tests/rust_reliable.rs (Rust SDK as sender, shared peer as the dropping receiver).”

Repo: identical to the previous item — rust_reliable.rs drives endpoints/rust’s AsyncReliableWriter as the sender against crates/endpoint-e2e’s reliable_collect as the receiver; structurally this is the only “Rust loss-recovery test” there is, because Rust supplies both the sender-under-test and the reference receiver implementation at once.

Tests: see previous item (rust_reliable_loss_recovery, rust_reliable_no_loss_baseline).

Status: n/a (informative) — structural, not open: the test obligation is already met by the previous item; this entry only documents why there is no separate Rust-vs-Rust test.

§4 Latency proof: decoupled vs. inline

Spec: §4 – “MUST — latency proof (test obligation 4 from reliable-endpoint-1.0 §5): enqueue (wait-free, ring) measured against an inline send (kernel transition) on the same producer path, against the same idle UDP sink.”

Repo: endpoints/rust/examples/example_reliable.rs::bench (line 71–115) — 50,000 iterations, handle.enqueue against the ring vs. isock.send(&write_frame(...)) inline, both measured against the same idle UDP sink.

Tests: (Linux) cargo run -p zerodds-endpoint-rust --example example_reliable -- bench → enqueue(decoupled)=30ns inline(send)=3985ns (~133×); the loss-recovery path the bench exercises is covered by crates/endpoint-e2e/tests/rust_reliable.rs::rust_reliable_loss_recovery and ::rust_reliable_no_loss_baseline. Reproduced locally (this run, a different machine): enqueue=124ns inline=4422ns (~35×) — order of magnitude confirmed, absolute value machine-dependent.

Status: done


Audit status

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

Test run (Linux figures + reproduced locally): cargo test -p zerodds-endpoint-rust --lib — 18/18 (state machine + wire round-trip + SPSC ring + byte-golden); cargo test -p zerodds-endpoint-e2e --test rust_reliable — 2/2 (rust_reliable_loss_recovery, rust_reliable_no_loss_baseline); cargo run -p zerodds-endpoint-rust --example example_sync|example_async — 5/5 field decode each, "ALL OK" (CI job endpoints-gate, column rust); latency bench enqueue=30ns vs. inline=3985ns (~133×, Linux) — reproduced locally at 124ns/4422ns (~35×).

Open items: none.

zerodds-endpoint-rust 1.0 — Spec-Coverage

Quelle: docs/specs/zerodds-endpoint-rust-1.0.md — ZeroDDS Rust-Endpoint-SDK-Spec (ADR 0013 + DDS-XRCE 1.0 §8.3/§8.4.10/§8.4.11 + reliable-endpoint-1.0).

Implementation:

  • endpoints/rust/ — das native Rust-Endpoint-SDK (ADR 0013): src/lib.rs (XRCE-Framing, Client, AsyncReader), src/reliable.rs (reliable State-Machine + async-entkoppelter Writer), examples/{example_sync,example_async,example_reliable}.rs.
  • crates/endpoint-e2e/ — der geteilte Rust-XRCE-Peer (src/lib.rs) + das reliable-E2E (tests/rust_reliable.rs).

§1 XRCE-Framing

§1 WRITE_DATA-Rahmen (8-Byte-Header + Body)

Spec: §1 – “Ein WRITE_DATA-Rahmen ist ein 8-Byte-Header + XCDR2-Sample-Body: [session, stream, seq_lo, seq_hi, submsg_id, flags, len_lo, len_hi] + body. session=0x80, stream=0x01 (best-effort), submsg_id=0x07, flags=0x03.”

Repo: endpoints/rust/src/lib.rs::xrce_write_frame/xrce_read_frame (Zeile 62–85, best-effort Stream 0x01, Session 0x80). Der geteilte Peer hat seine eigene, unabhängig geschriebene Framing-Implementierung crates/endpoint-e2e/src/lib.rs::xrce_frame/ xrce_unframe (Zeile 49–72) — gleiches 8-Byte-Layout, gleiche Konstanten (0x80/0x01/0x07/0x03), byte-identisch trotz getrennter Implementierung.

Tests: indirekt über example_sync/example_async (CI-Job endpoints-gate, Spalte rust); kein isolierter Byte-Golden-Unit-Test für den best-effort Rahmen (der reliable Rahmen hat einen eigenen, siehe §4).

Status: done

§1 Peer-Rolle im Ping-Pong (kein eigenständiger Rust-Ping-Pong-Test)

Spec: §1 – “Weil Rust hier die Peer-Rolle einnimmt (die 13 anderen Sprachen sprechen gegen ihn), gibt es keinen eigenständigen Rust-gegen-Rust-Ping-Pong-Test — die Framing-Konformität wird transitiv durch jeden der 13 Sprach-E2E-Tests in crates/endpoint-e2e/tests/ geprüft.”

Repo: crates/endpoint-e2e/src/lib.rs::ping_pong (Zeile 97–128) ist der Peer selbst. In crates/endpoint-e2e/tests/ gibt es keine Rust-Ping-Pong-Datei, weil Rust hier nicht “die App” ist, sondern der Peer, den jede andere Sprache anspricht — ein eigener Rust-Ping-Pong-Test gegen sich selbst würde nichts zusätzlich prüfen. Für den reliable Stream gilt das nicht: dort ist Rust die App, und crates/endpoint-e2e/tests/rust_reliable.rs existiert (siehe §4). endpoints/rusts eigenes Client/Reader-Paar wird stattdessen über die MemTransport-Beispiele (§2/§3) geprüft, nicht über den UDP-Peer-Pfad.

Tests: transitiv — jeder der 14 Sprach-E2E-Tests (crates/endpoint-e2e/tests/ada.rs, crates/endpoint-e2e/tests/c.rs, crates/endpoint-e2e/tests/cpp.rs, …) übt ping_pong aus; kein direkter Rust-eigener Test.

Status: n/a (informative) — strukturell, nicht offen: Rust ist die Peer-Implementierung, nicht ein getesteter Client dieses Peers.

§2 sync Client

§2 Client::write/poll

Spec: §2 – “Ein pollender, nicht-blockierender Empfangspfad — das Idiom für Aufrufer, die die Run-Loop selbst besitzen. write framet einen XCDR2-Sample-Body mit der aktuellen seq (wrapping_add), poll liefert einen dekodierten Body oder None.”

Repo: endpoints/rust/src/lib.rs::Client (Zeile 113–138): write framet einen XCDR2-Sample-Body und liefert ihn über MemTransport; poll liefert einen dekodierten Body oder None (nicht-blockierend).

Tests: endpoints/rust/examples/example_sync.rs — 5 Reading{id,value,label}-Samples, Feld-für-Feld dekodiert (id, value, label), assert!(got == total) + "ALL OK"; CI-Job endpoints-gate (Spalte rust).

Status: done

§3 async Reader/Writer

§3 AsyncReader (Thread + mpsc)

Spec: §3 – “Ein Hintergrund-Reader, der dekodierte Samples über einen Kanal liefert — kein Async-Runtime, sondern das idiomatische std-Concurrency-Modell (Thread + Channel). start spawnt einen Thread, recv blockiert, stop signalisiert per AtomicBool.”

Repo: endpoints/rust/src/lib.rs::AsyncReader (Zeile 142–180): start spawnt einen Thread, der die Transport pollt und dekodierte Bodies über mpsc::channel an recv liefert; stop signalisiert den Thread per AtomicBool.

Tests: endpoints/rust/examples/example_async.rs — 5 Samples, reader.recv() blockierend, alle Felder dekodiert, "ALL OK"; CI-Job endpoints-gate (Spalte rust).

Status: done

§4 reliable Stream

§4 State-Machine (Sender + Receiver)

Spec: §4 – “XRCE reliable Stream (stream_id >= 128, §8.4.10/§8.4.11), Fenster 16 (entspricht der 16-bit-ACKNACK-Bitmap), Receiver-Puffer 64, Heartbeat 500 ms, Payload ≤ 65535, RFC-1982 16-bit Sequenznummern — spiegelt crates/xrce/src/reliable.rs.”

Repo: endpoints/rust/src/reliable.rs — ReliableSender (submit/ pending_heartbeat/recv_acknack/get_in_flight/in_flight_seqs, Zeile 201–292), ReliableReceiver (recv_data/drain_in_order/pending_acknack/reset, Zeile 300–376). Self-contained, keine zerodds-xrce-Laufzeit-Abhängigkeit (bewusst dupliziert statt importiert, siehe Modul-Doc-Kommentar Zeile 1–21).

Tests: cargo test -p zerodds-endpoint-rust --lib — 14 State-Machine-Tests (submit_assigns_monotonic_seqnrs, submit_rejects_payload_too_large, submit_rejects_when_window_full, heartbeat_fires_first_then_silences_until_period, heartbeat_none_when_window_empty, recv_acknack_clears_acked_keeps_missing, recv_acknack_full_clear_when_no_bits, recv_data_delivers_in_order, recv_data_reorders_out_of_order, recv_data_drops_duplicates, recv_data_rejects_when_buffer_full, pending_acknack_marks_missing_slots, reset_clears_receiver, end_to_end_loss_recovery_in_process) + 2 Wire-/Ring-Tests (write_frame_round_trips, spsc_ring_fifo_and_backpressure) + 2 Byte-Golden (nächstes Item) = 18 von 18 im Crate. Lokal reproduziert (dieser Lauf): 18/18 passed, 0 failed.

Status: done

§4 Wire (byte-golden HEARTBEAT/ACKNACK)

Spec: §4 – “HEARTBEAT (0x0B) und ACKNACK (0x0A) byte-identisch zu den Referenz- Goldens des C-SDKs (golden_heartbeat_le.bin/golden_acknack_le.bin); XRCE-Control- Konvention: Header-Stream = NONE (0x00) + Control-Message-Seq, Ziel-Stream-Id im letzten Body-Byte.”

Repo: endpoints/rust/src/reliable.rs::heartbeat_frame/acknack_frame/ parse_heartbeat/parse_acknack (Zeile 125–192).

Tests: heartbeat_frame_byte_golden assertiert heartbeat_frame(Heartbeat{first:1,last:3,stream_id:0x80},1) == [128,0,1,0,11,1,5,0,1,0,3,0,128]; acknack_frame_byte_golden assertiert acknack_frame(AckNack{first_unacked:1,nack_bitmap:[0,0],stream_id:0x80},1) == [128,0,1,0,10,1,5,0,1,0,0,0,128] — byte-identisch zu den C-Goldens.

Status: done

§4 Async-entkoppelter Writer + Loss-Recovery (E2E)

Spec: §4 – “Der Producer enqueued nur (wait-free) in einen SPSC-Ring; ein dedizierter Drain-Thread besitzt Socket + reliable Sender-State und macht die gesamte I/O (WRITE_DATA-Send, HEARTBEAT-Timer, ACKNACK-getriebenes Retransmit). Der Producer geht nie in den Kernel. Reliable Delivery überlebt Datagramm-Verlust — verifiziert live gegen den geteilten Rust-Peer mit injiziertem Loss.”

Repo: endpoints/rust/src/reliable.rs::SpscRing (Zeile 384–443, wait-free push/pop über zwei Atomics, kein Lock), ReliableWriterHandle::enqueue (Zeile 458–460, nie im Kernel), AsyncReliableWriter::start/drain_loop (Zeile 469–574: Ring → Sender-Window → send, HEARTBEAT wenn fällig, ACKNACK-Drain → Retransmit, shutdown() blockiert bis Ring leer UND Fenster leer, mit 5s-Sicherheits-Deadline gegen einen verstummten Peer). crates/endpoint-e2e/src/lib.rs::reliable_collect treibt den Peer als Receiver (dropt bei drop_every=Some(3) jedes 3. distinct Sample genau einmal).

Tests: (Linux) cargo test -p zerodds-endpoint-e2e --test rust_reliable — rust_reliable_loss_recovery (Peer dropt jedes 3. Sample) und rust_reliable_no_loss_baseline (lossless) — je 12/12 Samples lückenlos in-order geliefert (N = 12 in tests/rust_reliable.rs). 2/2 passed. Läuft im CI-Job endpoint-e2e (cargo test -p zerodds-endpoint-e2e). Lokal reproduziert (dieser Lauf): 2/2 passed in 2,47s.

Status: done

§4 Rust als geteilter Peer (kein eigenständiger Rust-Loss-Recovery-Test)

Spec: §4 – “Rust ist zugleich der geteilte reliable Peer (crates/endpoint-e2e, reliable_collect) für alle anderen Sprachen — analog zu §1 existiert kein eigenständiger Rust-gegen-Rust-Loss-Recovery-Test; die Loss-Recovery-Konformität des endpoints/rust-eigenen AsyncReliableWriter wird über crates/endpoint-e2e/tests/rust_reliable.rs geprüft (Rust-SDK als Sender, geteilter Peer als droppender Receiver).”

Repo: identisch zum vorigen Item — rust_reliable.rs treibt endpoints/rusts AsyncReliableWriter als Sender gegen crates/endpoint-e2es reliable_collect als Receiver; strukturell ist das der einzige “Rust-Loss-Recovery-Test”, weil Rust hier Sender-unter-Test und Referenz-Receiver-Implementierung zugleich stellt.

Tests: siehe voriges Item (rust_reliable_loss_recovery, rust_reliable_no_loss_baseline).

Status: n/a (informative) — strukturell, nicht offen: die Test-Pflicht ist bereits im vorigen Item erfüllt; dieser Eintrag dokumentiert nur, warum es keinen separaten Rust-gegen-Rust-Test gibt.

§4 Latenz-Nachweis: entkoppelt vs. inline

Spec: §4 – “MUSS — Latenz-Nachweis (Beleg-Pflicht 4 aus reliable-endpoint-1.0 §5): enqueue (wait-free, Ring) gemessen gegen ein inline send (Kernelübergang) auf demselben Producer-Pfad, gegen denselben leerlaufenden UDP-Sink.”

Repo: endpoints/rust/examples/example_reliable.rs::bench (Zeile 71–115) — 50 000 Iterationen, handle.enqueue gegen den Ring vs. isock.send(&write_frame(...)) inline, beide gegen denselben leerlaufenden UDP-Sink gemessen.

Tests: (Linux) cargo run -p zerodds-endpoint-rust --example example_reliable -- bench → enqueue(decoupled)=30ns inline(send)=3985ns (~133×); der Loss-Recovery-Pfad, den der Bench belastet, ist über crates/endpoint-e2e/tests/rust_reliable.rs::rust_reliable_loss_recovery und ::rust_reliable_no_loss_baseline abgesichert. Lokal reproduziert (dieser Lauf, andere Maschine): enqueue=124ns inline=4422ns (~35×) — Größenordnung bestätigt, Absolutwert maschinenabhängig.

Status: done


Audit-Status

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

Test-Lauf: Linux-Vorgabe + lokal reproduziert — cargo test -p zerodds-endpoint-rust --lib — 18/18 (State-Machine + Wire-Rundtrip + SPSC-Ring + Byte-Golden); cargo test -p zerodds-endpoint-e2e --test rust_reliable — 2/2 (rust_reliable_loss_recovery, rust_reliable_no_loss_baseline); cargo run -p zerodds-endpoint-rust --example example_sync|example_async — je 5/5 Feld-Decode, "ALL OK" (CI-Job endpoints-gate, Spalte rust); Latenz-Bench enqueue=30ns vs. inline=3985ns (~133×, Linux) — lokal 124ns/4422ns (~35×) reproduziert.

Offene Punkte: keine.