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.