zerodds-endpoint-zig 1.0 — Spec Coverage
Source: docs/specs/zerodds-endpoint-zig-1.0.md — ZeroDDS Zig endpoint SDK spec
(XRCE framing, sync Client, async Reader/Writer, reliable stream via
reliable-endpoint-1.0). The marshalling comes from crates/lang-rules, column Language::Zig;
this document covers the transport.
Implementation:
endpoints/zig/— the pure-Zig endpoint SDK (ADR 0013: from-scratch XCDR wire core, no C):src/zerodds.zig(XRCE framing,Client,AsyncReader),src/reliable.zig(reliable state machine + async-decoupledAsyncWriter),example_sync.zig/example_async.zig/example_reliable.zig.crates/endpoint-e2e/tests/zig.rs— the live ping-pong E2E (raw/sync/async) against the shared Rust XRCE peer.crates/endpoint-e2e/tests/zig_reliable.rs— the reliable E2E (loss recovery, lossless baseline, example, latency bench).
§1 XRCE framing
§1 WRITE_DATA/DATA frame (8-byte header + body)
Spec: 8-byte XRCE header (session, stream, seq LE, submsg id 0x07 WRITE_DATA,
flags, len LE) + body, byte-identical to crates/xrce + endpoints/c.
Repo: endpoints/zig/src/zerodds.zig — xrceWriteFrame/xrceReadFrame,
constants XRCE_SESSION_NOKEY (0x80) and XRCE_STREAM_BEST_EFFORT (0x01);
Transport as a function-pointer vtable (deliver/receive, no heap).
Tests: crates/endpoint-e2e/tests/xrce_write_framing.rs::zig_endpoint_frames_conformant_write_data
builds the frame with zig run over the SDK file in the tree and hands it to
zerodds_xrce::WriteDataPayload::try_from_submessage — the same check every other
language column of the gate goes through; it covers the submessage id, the flag byte (0x01),
the BaseObjectRequest and the sample. Plus the in-file tests
"byte identity vs Rust goldens" in endpoints/zig/src/zerodds.zig (Writer LE+BE
against build/golden_le.bin/build/golden_be.bin),
"a WRITE_DATA frame carries the BaseObjectRequest under FORMAT_DATA flags",
"read frame bounds body to declared length, ignoring appended submessage"
and "read frame rejects over-long length and truncation, writer refuses >0xFFFF".
Status: done.
§1 Raw codec without an XRCE frame (ping-pong E2E)
Spec: evidence that the generated sample codec (zerodds-xcdr2-zig) also
round-trips over a real channel without XRCE framing — the ground the framing sits on.
Repo: crates/endpoint-e2e/tests/zig.rs::ZIG_RAW_MAIN — raw UDP, no XRCE frame,
uses only the generated Ping/Pong codec from
crates/endpoint-e2e/tests/zig.rs::gen_module (written as gen.zig into the
build directory at test time).
Tests: (Linux) zig_raw_udp — generated Ping/Pong codec directly over a raw
UDP socket, no XRCE framing. Passed.
Status: done.
§2 sync Client
§2 Client.write/poll
Spec: A blocking pull path: write(sample) frames + delivers through the
Transport, poll() receives a frame and returns the unframed body.
Repo: endpoints/zig/src/zerodds.zig::Client (write/poll, seq counter,
fixed txbuf/rxbuf, no heap).
Tests: in-file test sync loopback (pull) (in-memory FIFO transport, 5 samples
roundtrip); live E2E crates/endpoint-e2e/tests/zig.rs::ZIG_ENDPOINT_MAIN (mode
sync) via UdpTransport + zerodds.zig.
Tests: (Linux) zig_endpoint_sync — full stack via Client.write/poll
against the shared Rust XRCE peer. Passed.
Status: done.
§3 async Reader/Writer
§3 AsyncReader (callback reactor)
Spec: A callback reactor (push) as the counterpart to sync poll — Zig has no
async/await, so the reactor dispatches each ready frame to a consumer callback.
Repo: endpoints/zig/src/zerodds.zig::AsyncReader (on_sample callback, poll()
dispatches one frame, run(max) drains up to max frames or until nothing more is
ready).
Tests: in-file test async loopback (push / callback reactor) (5 samples,
Collector.on callback collects IDs in order); live E2E
crates/endpoint-e2e/tests/zig.rs::ZIG_ENDPOINT_MAIN (mode async) via
UdpTransport + zerodds.zig.
Tests: (Linux) zig_endpoint_async — full stack via AsyncReader.run against
the shared Rust XRCE peer. Passed.
Status: done.
Note: a standalone best-effort AsyncWriter (without reliability) does not exist
for Zig — the async-decoupled write side is covered exclusively by the reliable
AsyncWriter (§4) (spec §3, docs/specs/zerodds-endpoint-zig-1.0.md). Not an open
item, a deliberate spec decision.
Ping-pong E2E sum (§1/§2/§3): zig_raw_udp/zig_endpoint_sync/zig_endpoint_async
— 3/3 passed (Linux).
§4 reliable stream
§4 State machine (sender + receiver) + byte-golden HEARTBEAT/ACKNACK
Spec: XRCE reliable stream (stream_id 0x80, §8.4.10/§8.4.11), mirroring the
reference crates/xrce/src/reliable.rs: Sender.submit/pendingHeartbeat/
recvAckNack/getInFlight (history + retransmit); Receiver.recvData/drainInto/
pendingAckNack/reset (reorder + dedup). Window 16, receiver buffer 64, heartbeat
500 ms, payload ≤ 65535, RFC-1982 16-bit sequence numbers. HEARTBEAT (0x0B) and
ACKNACK (0x0A) byte-identical to the C SDK’s reference goldens. Full contract
details in reliable-endpoint-1.0.
Repo: endpoints/zig/src/reliable.zig — writeDataFrame/parseWriteData,
heartbeatFrame/parseHeartbeat, acknackFrame/parseAckNack; Sender, Receiver.
Tests: crates/endpoint-e2e/tests/xrce_write_framing.rs::zig_reliable_frames_conformant_write_data
— writeDataFrame gets its own column in the framing gate, because reliable.zig is
a separate framing with its own constants and does not @import zerodds.zig; the
frame goes through zerodds_xrce::WriteDataPayload::try_from_submessage. Plus (Linux)
zig_reliable_unit — zig test on reliable.zig: the
mirror-of-reference unit suite (monotonic seq, payload-too-large, window-full,
heartbeat first/silence/none, ACKNACK partial/full clear, receiver reorder/dedup/
buffer-full, pending-ACKNACK bitmap, reset, in-process end-to-end loss recovery) plus
the byte-golden assertion in the same zig test run: heartbeatFrame(1,3) ==
80 00 01 00 0b 01 05 00 01 00 03 00 80, acknackFrame(1,0) ==
80 00 01 00 0a 01 05 00 01 00 00 00 80 — identical to the reference goldens.
Passed.
Status: done.
§4 Async-decoupled AsyncWriter (SPSC ring) + loss-recovery E2E
Spec: The producer enqueues wait-free into an SPSC ring, a dedicated drain thread
holds the Sender state and does all the I/O (send, heartbeat, 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/zig/src/reliable.zig::AsyncWriter (wait-free SPSC ring
RING_CAP=1024/SLOT_CAP=512 via std.atomic.Value, write/pop/drainLoop/
drainAckNacks/close); endpoints/zig/example_reliable.zig (runnable in-process
demo, no socket).
Tests: (Linux)
- zig_reliable_loss_recovery — peer drops every 3rd sample once; the app
retransmits on ACKNACK; all 12 samples delivered gap-free in order.
- zig_reliable_no_loss — lossless baseline; 12/12.
- zig_reliable_example — example_reliable.zig runs and reports
sequence 0..11 verified in order.
3/3 passed (Linux) — together with zig_reliable_unit (§4 above) 4/5 of the
reliable test set; the latency bench is §5.
Note (honest): while building AsyncWriter.close(), a shutdown deadlock was found
and fixed in drainLoop — a lingering window with no incoming ACKNACKs must not block
close(); drainLoop now checks running on every iteration instead of only while
waiting for an ACKNACK.
Status: done.
§5 Latency
§5 SPSC-ring push vs. inline sendto
Spec: The producer path of AsyncWriter (write → ring-slot memcpy + release
store) must be measurably below the inline sendto syscall — the evidence that async
write removes syscall latency from the producer path, not that it waits for ACKNACK.
Repo: crates/endpoint-e2e/tests/zig_reliable.rs::runBench (Zig app, mode
bench): 4000 iterations of inline sendto vs. 4000 iterations of
AsyncWriter.write after 100 warmup pushes.
Tests: (Linux) zig_reliable_latency_bench — producer push (wait-free ring)
14 ns vs. inline sendto 7950 ns (~568×), asserts decoupled_ns < inline_ns.
Passed.
Status: done.
Audit status
7 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).
Test run (Linux, verified): cargo test -p zerodds-endpoint-e2e --test zig
3/3 (ping-pong: zig_raw_udp/zig_endpoint_sync/zig_endpoint_async);
--test zig_reliable 5/5 (zig_reliable_unit incl. byte-golden,
zig_reliable_loss_recovery, zig_reliable_no_loss, zig_reliable_example,
zig_reliable_latency_bench); latency bench decoupled 14 ns / inline
sendto 7950 ns (~568×).
Open items: none.
zerodds-endpoint-zig 1.0 — Spec-Coverage
Quelle: docs/specs/zerodds-endpoint-zig-1.0.md — ZeroDDS Zig Endpoint-SDK-Spec
(XRCE-Framing, sync Client, async Reader/Writer, reliable Stream via
reliable-endpoint-1.0). Das Marshalling kommt aus crates/lang-rules, Spalte Language::Zig;
hier steht der Transport.
Implementation:
endpoints/zig/— das pure-Zig-Endpoint-SDK (ADR 0013: from-scratch XCDR-Wire-Core, kein C):src/zerodds.zig(XRCE-Framing,Client,AsyncReader),src/reliable.zig(reliable State-Machine + async-entkoppelterAsyncWriter),example_sync.zig/example_async.zig/example_reliable.zig.crates/endpoint-e2e/tests/zig.rs— das Live-Ping-Pong-E2E (raw/sync/async) gegen den geteilten Rust-XRCE-Peer.crates/endpoint-e2e/tests/zig_reliable.rs— das reliable-E2E (Loss-Recovery, lossless Baseline, Example, Latenz-Bench).
§1 XRCE-Framing
§1 WRITE_DATA/DATA-Rahmen (8-Byte-Header + Body)
Spec: 8-Byte-XRCE-Header (session, stream, seq LE, submsg id 0x07 WRITE_DATA,
flags, len LE) + Body, byte-identisch zu crates/xrce + endpoints/c.
Repo: endpoints/zig/src/zerodds.zig — xrceWriteFrame/xrceReadFrame,
Konstanten XRCE_SESSION_NOKEY (0x80) und XRCE_STREAM_BEST_EFFORT (0x01);
Transport als Funktionszeiger-Vtable (deliver/receive, kein Heap).
Tests: crates/endpoint-e2e/tests/xrce_write_framing.rs::zig_endpoint_frames_conformant_write_data
baut den Frame mit zig run über die SDK-Datei im Baum und gibt ihn an
zerodds_xrce::WriteDataPayload::try_from_submessage — dieselbe Prüfung, die jede
andere Sprachspalte des Gatters durchläuft; sie deckt Submessage-Id, Flag-Byte
(0x01), BaseObjectRequest und Sample ab. Dazu die In-File-Tests
"byte identity vs Rust goldens" in endpoints/zig/src/zerodds.zig (Writer LE+BE
gegen build/golden_le.bin/build/golden_be.bin),
"a WRITE_DATA frame carries the BaseObjectRequest under FORMAT_DATA flags",
"read frame bounds body to declared length, ignoring appended submessage"
und "read frame rejects over-long length and truncation, writer refuses >0xFFFF".
Status: done.
§1 Roher Codec ohne XRCE-Frame (Ping-Pong-E2E)
Spec: Nachweis, dass der generierte Sample-Codec (zerodds-xcdr2-zig) auch ohne
XRCE-Framing über einen echten Kanal roundtrippt — die Grundlage, auf der das Framing
aufsetzt.
Repo: crates/endpoint-e2e/tests/zig.rs::ZIG_RAW_MAIN — rohes UDP, kein
XRCE-Frame, nutzt nur den generierten Ping/Pong-Codec aus
crates/endpoint-e2e/tests/zig.rs::gen_module (zur Testlaufzeit als gen.zig
ins Build-Verzeichnis geschrieben).
Tests: (Linux) zig_raw_udp — generierter Ping/Pong-Codec direkt über einen
rohen UDP-Socket, ohne XRCE-Framing. Grün.
Status: done.
§2 sync Client
§2 Client.write/poll
Spec: Ein blockierender Pull-Pfad: write(sample) framet + liefert über den
Transport, poll() empfängt ein Frame und liefert den entrahmten Body.
Repo: endpoints/zig/src/zerodds.zig::Client (write/poll, seq-Zähler,
feste txbuf/rxbuf, kein Heap).
Tests: In-File-Test sync loopback (pull) (In-Memory-FIFO-Transport, 5 Samples
roundtrip); Live-E2E crates/endpoint-e2e/tests/zig.rs::ZIG_ENDPOINT_MAIN (Modus
sync) über UdpTransport + zerodds.zig.
Tests: (Linux) zig_endpoint_sync — voller Stack über Client.write/poll
gegen den geteilten Rust-XRCE-Peer. Grün.
Status: done.
§3 async Reader/Writer
§3 AsyncReader (Callback-Reaktor)
Spec: Ein Callback-Reaktor (push) als Gegenstück zum sync poll — Zig hat
kein async/await, der Reaktor dispatcht jedes bereite Frame an einen
Consumer-Callback.
Repo: endpoints/zig/src/zerodds.zig::AsyncReader (on_sample-Callback,
poll() dispatcht ein Frame, run(max) drained bis max Frames oder bis
nichts mehr bereit ist).
Tests: In-File-Test async loopback (push / callback reactor) (5 Samples,
Collector.on-Callback sammelt IDs in Reihenfolge); Live-E2E
crates/endpoint-e2e/tests/zig.rs::ZIG_ENDPOINT_MAIN (Modus async) über
UdpTransport + zerodds.zig.
Tests: (Linux) zig_endpoint_async — voller Stack über AsyncReader.run
gegen den geteilten Rust-XRCE-Peer. Grün.
Status: done.
Anmerkung: ein eigenständiger best-effort AsyncWriter (ohne Reliability) ist für
Zig nicht angelegt — die async-entkoppelte Schreibseite wird ausschließlich vom
reliable AsyncWriter (§4) abgedeckt (Spec §3, docs/specs/zerodds-endpoint-zig-1.0.md).
Kein offener Punkt, sondern eine bewusste Spec-Entscheidung.
Ping-Pong-E2E-Summe (§1/§2/§3): zig_raw_udp/zig_endpoint_sync/zig_endpoint_async
— 3/3 grün (Linux).
§4 reliable Stream
§4 State-Machine (Sender + Receiver) + Byte-golden HEARTBEAT/ACKNACK
Spec: XRCE reliable Stream (stream_id 0x80, §8.4.10/§8.4.11), spiegelt die
Referenz crates/xrce/src/reliable.rs: Sender.submit/pendingHeartbeat/
recvAckNack/getInFlight (History + Retransmit); Receiver.recvData/
drainInto/pendingAckNack/reset (Reorder + Dedup). Window 16, Receiver-Buffer
64, Heartbeat 500 ms, Payload ≤ 65535, RFC-1982 16-bit Sequenznummern. HEARTBEAT
(0x0B) und ACKNACK (0x0A) byte-identisch zu den Referenz-Goldens des C-SDKs.
Volle Kontraktdetails in reliable-endpoint-1.0.
Repo: endpoints/zig/src/reliable.zig — writeDataFrame/parseWriteData,
heartbeatFrame/parseHeartbeat, acknackFrame/parseAckNack; Sender, Receiver.
Tests: crates/endpoint-e2e/tests/xrce_write_framing.rs::zig_reliable_frames_conformant_write_data
— writeDataFrame hat eine eigene Spalte im Framing-Gatter, weil reliable.zig ein
eigenes Framing mit eigenen Konstanten ist und zerodds.zig nicht importiert; der
Frame geht durch zerodds_xrce::WriteDataPayload::try_from_submessage. Dazu (Linux)
zig_reliable_unit — zig test auf reliable.zig: die
Mirror-of-Reference-Unit-Suite (monotone seq, Payload-zu-groß, Window-full, Heartbeat
first/silence/leer, ACKNACK Teil-/Voll-Clear, Receiver Reorder/Dedup/Buffer-full,
Pending-ACKNACK-Bitmap, Reset, In-Process-End-to-End-Loss-Recovery) plus die
Byte-Golden-Assertion in derselben zig test-Runde:
heartbeatFrame(1,3) == 80 00 01 00 0b 01 05 00 01 00 03 00 80,
acknackFrame(1,0) == 80 00 01 00 0a 01 05 00 01 00 00 00 80 — identisch
zu den Referenz-Goldens. Grün.
Status: done.
§4 async-entkoppelter AsyncWriter (SPSC-Ring) + Loss-Recovery-E2E
Spec: Der Producer enqueued wait-free in einen SPSC-Ring, ein dedizierter
Drain-Thread hält den Sender-State und macht die gesamte I/O (senden, Heartbeat,
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/zig/src/reliable.zig::AsyncWriter (wait-freier SPSC-Ring
RING_CAP=1024/SLOT_CAP=512 via std.atomic.Value, write/pop/drainLoop/
drainAckNacks/close); endpoints/zig/example_reliable.zig (lauffähige
In-Process-Demo, kein Socket).
Tests: (Linux)
- zig_reliable_loss_recovery — Peer dropt jedes 3. Sample einmalig; die App
retransmittet auf ACKNACK; alle 12 Samples lückenlos in Reihenfolge geliefert.
- zig_reliable_no_loss — lossless Baseline; 12/12.
- zig_reliable_example — example_reliable.zig läuft und meldet
sequence 0..11 verified in order.
3/3 grün (Linux) — zusammen mit zig_reliable_unit (§4 oben) 4/5 des reliable-Tests;
Latenz-Bench ist §5.
Anmerkung (ehrlich): Beim Aufbau von AsyncWriter.close() wurde ein
Shutdown-Deadlock in drainLoop gefunden und gefixt — ein hängendes Fenster
ohne eingehende ACKNACKs durfte close() nicht blockieren; drainLoop prüft
running jetzt in jeder Iteration statt nur beim Warten auf ein ACKNACK.
Status: done.
§5 Latenz
§5 SPSC-Ring-Push vs. inline sendto
Spec: Der Producer-Pfad des AsyncWriter (write → Ringslot-Memcpy +
Release-Store) muss messbar unter dem inline sendto-Syscall liegen — das ist
der Beleg, dass Async-Write die Syscall-Latenz aus dem Producer-Pfad nimmt,
nicht das Warten auf ACKNACK.
Repo: crates/endpoint-e2e/tests/zig_reliable.rs::runBench (Zig-App,
Modus bench): 4000 Iterationen inline sendto vs. 4000 Iterationen
AsyncWriter.write nach 100 Warmup-Pushes.
Tests: (Linux) zig_reliable_latency_bench — Producer-Push (wait-free
Ring) 14 ns vs. inline sendto 7950 ns (~568×), Assert
decoupled_ns < inline_ns. Grün.
Status: done.
Audit-Status
7 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).
Test-Lauf: Linux, verifiziert — cargo test -p zerodds-endpoint-e2e --test zig
3/3 (Ping-Pong: zig_raw_udp/zig_endpoint_sync/zig_endpoint_async);
--test zig_reliable 5/5 (zig_reliable_unit inkl. Byte-Golden,
zig_reliable_loss_recovery, zig_reliable_no_loss, zig_reliable_example,
zig_reliable_latency_bench); Latenz-Bench decoupled 14 ns / inline
sendto 7950 ns (~568×).
Offene Punkte: keine.