zerodds-endpoint-csharp 1.0 — Spec Coverage
Source: docs/specs/zerodds-endpoint-csharp-1.0.md — the ZeroDDS C#
endpoint SDK spec. The marshalling comes from crates/lang-rules, column Language::CSharp; this document covers the transport.
Implementation:
endpoints/csharp/Zerodds.cs(namespaceZeroDDS) — XCDR2 wire core (Writer/Reader), XRCE framing (Xrce.WriteFrame/Xrce.ReadFrame),ITransport/MemTransport, syncClient, asyncAsyncReader.endpoints/csharp/Reliable.cs(namespaceZeroDDS.Endpoint) — reliable sender/receiver state machine + HEARTBEAT/ACKNACK wire codec +AsyncReliableWriter/ReliableWriterHandle.crates/endpoint-e2e/tests/csharp.rs— ping-pong E2E;crates/endpoint-e2e/tests/csharp_reliable.rs— reliable-stream E2E + unit/golden + latency bench.
§1 XRCE framing
Spec: §1 — 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/csharp/Zerodds.cs::Xrce — WriteFrame/ReadFrame,
constants SessionNoKey (0x80) and StreamBestEffort (0x01).
Tests: crates/endpoint-e2e/tests/csharp.rs::csharp_raw_udp (raw XCDR2
over a plain UDP socket, no XRCE frame — its own minimal harness); the
framing itself is exercised live via csharp_endpoint_sync and
csharp_endpoint_async (§4).
Status: done.
§2 Sync Client
Spec: §2 — blocking Client: Write frames + delivers synchronously,
Poll is a single non-blocking receive (null when nothing is queued).
Repo: endpoints/csharp/Zerodds.cs::ITransport (Deliver/Receive, the
sole integration point), MemTransport (a lock-based in-memory reference),
Client (Client(ITransport), Write/Poll, monotonic ushort seq
counter, default session Xrce.SessionNoKey/Xrce.StreamBestEffort).
Tests: endpoints/csharp/ExampleSync.cs (5 Reading samples over
MemTransport, full field decode, prints ALL OK); live E2E
csharp_endpoint_sync (§4).
Status: done.
§3 Async Reader
Spec: §3 — AsyncReader.Stream() returns an IAsyncEnumerable<byte[]>;
the consumer iterates it with await foreach. No separate AsyncWriter
type for the best-effort path — on the send side, async shares the same
Client.Write.
Repo: endpoints/csharp/Zerodds.cs::AsyncReader
(AsyncReader(ITransport), Stream(CancellationToken) — a non-blocking
poll per tick, else Task.Delay(1)).
Tests: endpoints/csharp/ExampleAsync.cs (5 Reading samples,
await foreach, full field decode, prints ALL OK); live E2E
csharp_endpoint_async (§4).
Status: done.
§4 Ping-pong E2E (live)
Spec: §5.1/§5.2 — a C# app exchanges a typed sample with the shared Rust XRCE peer over a real UDP socket: once as raw generated codec with no XRCE frame, once as the full stack (generated types + endpoint SDK) sync and async.
Repo: crates/endpoint-e2e/tests/csharp.rs — builds a real dotnet app
(net8.0, a ProjectReference onto the real ZeroDDS.Cdr.csproj) that
encodes a Ping with the GENERATED PingTypeSupport.Encode, ships it via
UdpTransport : ZeroDDS.ITransport (sync Client.Write/Poll or async
AsyncReader.Stream), and decodes the Pong with PongTypeSupport.Decode;
mode selected by CLI argument (sync/async/raw).
Tests: (Linux, gated on dotnet)
- csharp_raw_udp — generated Ping/Pong codec directly over a raw UDP
socket, no XRCE framing.
- csharp_endpoint_sync — full stack via ZeroDDS.Client.
- csharp_endpoint_async — full stack via ZeroDDS.AsyncReader.
3/3 passed (Linux, dotnet >= .NET 8 on PATH or ~/.dotnet/dotnet; no
dotnet ⇒ a loud skip, no false-green).
Status: done.
§5 Reliable stream — state machine, wire, async writer
Spec: §4 (references reliable-endpoint v1.0 §3/§4) — XRCE reliable
stream (stream_id 0x80, §8.4.10/§8.4.11), mirroring the reference
crates/xrce/src/reliable.rs: ReliableSender.Submit/PendingHeartbeat/
RecvAckNack/GetInFlight/InFlightSeqs; ReliableReceiver.RecvData/
DrainInOrder/PendingAckNack/Reset. Window 16, receiver buffer 64,
heartbeat 500 ms, payload ≤ 65535, RFC-1982 16-bit sequence numbers.
Alongside it, the async-decoupled AsyncReliableWriter: the producer
enqueues via ReliableWriterHandle.Enqueue wait-free onto a bounded
System.Threading.Channels ring (TryWrite, false on a full ring), a
dedicated drain Thread owns the ReliableSender state and the UDP
Socket exclusively and does all the I/O (send, heartbeat, ACKNACK-driven
retransmit) — the producer never enters the kernel.
Repo: endpoints/csharp/Reliable.cs — ReliableWire
(WriteFrame/TryUnframeWrite, HeartbeatFrame/TryParseHeartbeat,
AckNackFrame/TryParseAckNack, constants); ReliableSender,
ReliableReceiver; WriterCloseState, ReliableWriterHandle
(Enqueue/Close), AsyncReliableWriter (Start/Shutdown/Dispose,
drain Thread named zerodds-reliable-drain);
endpoints/csharp/ExampleReliable.cs (all three modes: E2E sender, bench,
in-process demo).
Tests: (Linux, gated on dotnet)
- csharp_reliable_unit_and_golden
(crates/endpoint-e2e/tests/csharp_reliable.rs) builds + runs
endpoints/csharp/ReliableTests.cs (ZeroDDS.Tests.ReliableTests.Main) —
a single driver with 32 Check(...) assertions covering: sender
monotonic seq ("monotonic seq 0"/"monotonic seq 1",
"in-flight count"), payload-too-large ("payload too large"), window
fill + window-full ("fill window" ×16, "window full"), heartbeat
first/body/silenced/after-500ms/none ("heartbeat fires first"/
"heartbeat body"/"heartbeat silenced <500ms"/"heartbeat after 500ms"/
"no heartbeat when empty"), ACKNACK partial/full clear
("acknack clears acked"/"seq2 retransmittable"/
"acknack full clear"), receiver in-order/reorder/duplicate-drop/
buffer-full ("in-order drain"/"expected advanced"/
"reorder: only seq0"/"reorder: seq1+2"/"duplicate dropped"/
"fill recv buffer" ×64/"recv buffer full"), pending-ACKNACK bitmap
("slot 0 missing"/"slot 2 missing"/"slot 1 present"/
"slot 3 present"), reset ("reset clears receiver"), in-process
end-to-end loss recovery ("only seq0 before recovery"/
"seq1 retransmittable"/"seq1+2 after recovery"), byte-golden
("heartbeat byte-golden (hardcoded)" == 80 00 01 00 0b 01 05 00 01 00 03 00 80, "acknack byte-golden (hardcoded)" == 80 00 01 00 0a 01 05 00 01 00 00 00 80; additionally, when the Rust goldens could be generated
via zerodds-endpoint-golden, "heartbeat byte-identical to golden file"/"acknack byte-identical to golden file" against the real
golden_heartbeat_le.bin/golden_acknack_le.bin). Prints ALL OK at 0
failures.
- csharp_reliable_loss_recovery — the peer drops every 3rd sample once;
the ExampleReliable app (mode <port>) retransmits via
AsyncReliableWriter on ACKNACK; all 12 samples delivered gap-free in
order (asserted against Reading.Id == Index).
- csharp_reliable_no_loss_baseline — lossless baseline; 12/12.
- csharp_reliable_standalone_example — ExampleReliable with no
arguments (in-process demo: lossy receiver thread + AsyncReliableWriter
sender) reports OK: 12 samples delivered gap-free in order despite injected loss.
5/5 passed (Linux), 4 of which belong to this section (latency bench in §6).
Status: done.
§6 Latency — ring enqueue vs. inline Socket.Send
Spec: §5.4 — the producer path of AsyncReliableWriter
(ReliableWriterHandle.Enqueue → channel push) must be measurably below
the inline Socket.Send syscall — the evidence that async write removes
syscall latency from the producer path, not that it waits for ACKNACK.
Repo: endpoints/csharp/ExampleReliable.cs::Bench — 50000 iterations of
ReliableWriterHandle.Enqueue (drain thread plus a separate sink thread
drain the ring/socket so Enqueue never hits backpressure) vs. 50000
iterations of inline Socket.Send (no live peer needed, loopback sink),
measured via Stopwatch (1 tick = 100 ns).
Tests: (Linux) csharp_reliable_latency_bench
(crates/endpoint-e2e/tests/csharp_reliable.rs) — mode bench; the Rust
test only asserts the LATENCY line is present (no hardcoded threshold),
the measured figure is logged. Observed order of magnitude (Linux): ring
enqueue ~6–10 µs vs. inline Socket.Send ~442–504 µs (~50–70×) —
the .NET JIT/GC overhead makes the absolute numbers higher than Go/Rust,
the ratio (enqueue with no kernel transition vs. an inline syscall) remains
the same evidence.
Status: done.
Audit Status
6 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).
Test run (Linux, verified): cargo test -p zerodds-endpoint-e2e --test csharp
3/3 (ping-pong: csharp_raw_udp/csharp_endpoint_sync/csharp_endpoint_async);
--test csharp_reliable 5/5 (csharp_reliable_loss_recovery,
csharp_reliable_no_loss_baseline, csharp_reliable_unit_and_golden — 32
inline assertions incl. byte-golden, csharp_reliable_standalone_example,
csharp_reliable_latency_bench); latency bench ring enqueue ~6–10 µs /
inline Socket.Send ~442–504 µs (~50–70×).
Gated on dotnet (>= .NET 8, PATH or ~/.dotnet/dotnet); without the
toolchain, a loud skip, no false-green.
Open items: none.
zerodds-endpoint-csharp 1.0 — Spec-Coverage
Quelle: docs/specs/zerodds-endpoint-csharp-1.0.md — ZeroDDS C#
Endpoint-SDK-Spec. Das Marshalling kommt aus crates/lang-rules, Spalte Language::CSharp; hier steht der Transport.
Implementation:
endpoints/csharp/Zerodds.cs(namespaceZeroDDS) — XCDR2-Wire-Core (Writer/Reader), XRCE-Framing (Xrce.WriteFrame/Xrce.ReadFrame),ITransport/MemTransport, syncClient, asyncAsyncReader.endpoints/csharp/Reliable.cs(namespaceZeroDDS.Endpoint) — reliable Sender/Receiver-State-Machine + HEARTBEAT/ACKNACK-Wire-Codec +AsyncReliableWriter/ReliableWriterHandle.crates/endpoint-e2e/tests/csharp.rs— Ping-Pong-E2E;crates/endpoint-e2e/tests/csharp_reliable.rs— reliable-Stream-E2E + Unit/Golden + Latenz-Bench.
§1 XRCE-Framing
Spec: §1 — 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/csharp/Zerodds.cs::Xrce — WriteFrame/ReadFrame,
Konstanten SessionNoKey (0x80) und StreamBestEffort (0x01).
Tests: crates/endpoint-e2e/tests/csharp.rs::csharp_raw_udp (rohes
XCDR2 über einen bloßen UDP-Socket, kein XRCE-Frame — eigener Mini-Harness);
das Framing selbst wird über csharp_endpoint_sync und csharp_endpoint_async
(§4) live geübt.
Status: done.
§2 Sync Client
Spec: §2 — blockierender Client: Write framet + liefert synchron,
Poll ist ein nicht-blockierender Einzel-Receive (null wenn nichts
ansteht).
Repo: endpoints/csharp/Zerodds.cs::ITransport (Deliver/Receive, der
einzige Integrationspunkt), MemTransport (Lock-basierte In-Memory-Referenz),
Client (Client(ITransport), Write/Poll, monotoner ushort-Seq-Zähler,
Default-Session Xrce.SessionNoKey/Xrce.StreamBestEffort).
Tests: endpoints/csharp/ExampleSync.cs (5 Reading-Samples über
MemTransport, volles Feld-Decode, druckt ALL OK); Live-E2E
csharp_endpoint_sync (§4).
Status: done.
§3 Async Reader
Spec: §3 — AsyncReader.Stream() liefert ein IAsyncEnumerable<byte[]>;
der Consumer iteriert mit await foreach. Kein separater AsyncWriter-Typ
für den Best-Effort-Pfad — Sendeseitig teilt sich async denselben
Client.Write.
Repo: endpoints/csharp/Zerodds.cs::AsyncReader
(AsyncReader(ITransport), Stream(CancellationToken) — nicht-blockierender
Poll pro Tick, sonst Task.Delay(1)).
Tests: endpoints/csharp/ExampleAsync.cs (5 Reading-Samples,
await foreach, volles Feld-Decode, druckt ALL OK); Live-E2E
csharp_endpoint_async (§4).
Status: done.
§4 Ping-Pong-E2E (live)
Spec: §5.1/§5.2 — eine C#-App tauscht mit dem geteilten Rust-XRCE-Peer über einen echten UDP-Socket ein typisiertes Sample aus: einmal roher generierter Codec ohne XRCE-Frame, einmal voller Stack (generierte Typen + Endpoint-SDK) sync und async.
Repo: crates/endpoint-e2e/tests/csharp.rs — baut eine echte
dotnet-App (net8.0, ProjectReference auf das reale
ZeroDDS.Cdr.csproj), die einen Ping mit dem GENERIERTEN
PingTypeSupport.Encode kodiert, über UdpTransport : ZeroDDS.ITransport
(sync Client.Write/Poll bzw. async AsyncReader.Stream) verschickt und
den Pong mit PongTypeSupport.Decode dekodiert; Modus per CLI-Argument
(sync/async/raw).
Tests: (Linux, dotnet-gated)
- csharp_raw_udp — generierter Ping/Pong-Codec direkt über einen rohen
UDP-Socket, ohne XRCE-Framing.
- csharp_endpoint_sync — voller Stack über ZeroDDS.Client.
- csharp_endpoint_async — voller Stack über ZeroDDS.AsyncReader.
3/3 grün (Linux, dotnet >= .NET 8 auf PATH oder ~/.dotnet/dotnet; kein
dotnet ⇒ lauter Skip, kein false-green).
Status: done.
§5 Reliable Stream — State-Machine, Wire, Async-Writer
Spec: §4 (verweist auf reliable-endpoint v1.0 §3/§4) — XRCE reliable
Stream (stream_id 0x80, §8.4.10/§8.4.11), spiegelt die Referenz
crates/xrce/src/reliable.rs: ReliableSender.Submit/PendingHeartbeat/
RecvAckNack/GetInFlight/InFlightSeqs; ReliableReceiver.RecvData/
DrainInOrder/PendingAckNack/Reset. Window 16, Receiver-Buffer 64,
Heartbeat 500 ms, Payload ≤ 65535, RFC-1982 16-bit Sequenznummern. Dazu der
async-entkoppelte AsyncReliableWriter: der Producer enqueued über
ReliableWriterHandle.Enqueue wait-free in einen gebundenen
System.Threading.Channels-Ring (TryWrite, false bei vollem Ring), ein
dedizierter Drain-Thread hält den ReliableSender-State und den
UDP-Socket exklusiv und macht die gesamte I/O (Senden, Heartbeat,
ACKNACK-getriebenes Retransmit) — der Producer geht nie in den Kernel.
Repo: endpoints/csharp/Reliable.cs — ReliableWire
(WriteFrame/TryUnframeWrite, HeartbeatFrame/TryParseHeartbeat,
AckNackFrame/TryParseAckNack, Konstanten); ReliableSender,
ReliableReceiver; WriterCloseState, ReliableWriterHandle
(Enqueue/Close), AsyncReliableWriter (Start/Shutdown/Dispose,
Drain-Thread zerodds-reliable-drain); endpoints/csharp/ExampleReliable.cs
(alle drei Modi: E2E-Sender, Bench, In-Process-Demo).
Tests: (Linux, dotnet-gated)
- csharp_reliable_unit_and_golden
(crates/endpoint-e2e/tests/csharp_reliable.rs) baut + läuft
endpoints/csharp/ReliableTests.cs (ZeroDDS.Tests.ReliableTests.Main) —
ein einzelner Treiber mit 32 Check(...)-Assertions über: Sender
monotone Seq ("monotonic seq 0"/"monotonic seq 1",
"in-flight count"), Payload-zu-groß ("payload too large"), Window-Fill
+ Window-full ("fill window" × 16, "window full"), Heartbeat
first/body/silenced/nach-500ms/leer ("heartbeat fires first"/
"heartbeat body"/"heartbeat silenced <500ms"/"heartbeat after 500ms"/
"no heartbeat when empty"), ACKNACK Teil-/Voll-Clear
("acknack clears acked"/"seq2 retransmittable"/
"acknack full clear"), Receiver In-Order/Reorder/Duplikat-Drop/
Buffer-full ("in-order drain"/"expected advanced"/
"reorder: only seq0"/"reorder: seq1+2"/"duplicate dropped"/
"fill recv buffer" × 64/"recv buffer full"), Pending-ACKNACK-Bitmap
("slot 0 missing"/"slot 2 missing"/"slot 1 present"/
"slot 3 present"), Reset ("reset clears receiver"), In-Process
Loss-Recovery End-to-End ("only seq0 before recovery"/
"seq1 retransmittable"/"seq1+2 after recovery"), Byte-Golden
("heartbeat byte-golden (hardcoded)" == 80 00 01 00 0b 01 05 00 01 00 03 00 80, "acknack byte-golden (hardcoded)" == 80 00 01 00 0a 01 05 00 01 00 00 00 80; zusätzlich, wenn die Rust-Goldens per
zerodds-endpoint-golden generiert werden konnten,
"heartbeat byte-identical to golden file"/
"acknack byte-identical to golden file" gegen die echten
golden_heartbeat_le.bin/golden_acknack_le.bin). Druckt ALL OK bei
0 Failures.
- csharp_reliable_loss_recovery — Peer dropt jedes 3. Sample einmalig; die
ExampleReliable-App (Modus <port>) retransmittiert via
AsyncReliableWriter auf ACKNACK; alle 12 Samples lückenlos in Reihenfolge
geliefert (Assert auf Reading.Id == Index).
- csharp_reliable_no_loss_baseline — lossless Baseline; 12/12.
- csharp_reliable_standalone_example — ExampleReliable ohne Argumente
(In-Process-Demo: lossy Receiver-Thread + AsyncReliableWriter-Sender)
meldet OK: 12 samples delivered gap-free in order despite injected loss.
5/5 grün (Linux), davon 4 in diesem Abschnitt (Latenz-Bench in §6).
Status: done.
§6 Latenz — Ring-Enqueue vs. inline Socket.Send
Spec: §5.4 — der Producer-Pfad des AsyncReliableWriter
(ReliableWriterHandle.Enqueue → Channel-Push) muss messbar unter dem
inline Socket.Send-Syscall liegen — der Beleg, dass Async-Write die
Syscall-Latenz aus dem Producer-Pfad nimmt, nicht das Warten auf ACKNACK.
Repo: endpoints/csharp/ExampleReliable.cs::Bench — 50000 Iterationen
ReliableWriterHandle.Enqueue (Drain-Thread + separater Sink-Thread leeren
den Ring/Socket, damit Enqueue nie Backpressure trifft) vs. 50000
Iterationen inline Socket.Send (kein Live-Peer nötig, Loopback-Sink), via
Stopwatch gemessen (1 Tick = 100 ns).
Tests: (Linux) csharp_reliable_latency_bench
(crates/endpoint-e2e/tests/csharp_reliable.rs) — Modus bench; Assert nur
auf Vorhandensein der LATENCY-Zeile (kein hartcodierter Schwellwert im
Rust-Test), Messwert wird geloggt. Beobachtete Größenordnung (Linux):
Ring-Enqueue ~6–10 µs vs. inline Socket.Send ~442–504 µs
(~50–70×) — der .NET-JIT/GC-Overhead macht die absoluten Werte gegenüber
Go/Rust höher, das Verhältnis (Enqueue ohne Kernel-Übergang vs. inline
Syscall) bleibt dieselbe Evidenz.
Status: done.
Audit-Status
6 done / 0 partial / 0 open / 0 blocked / 0 n/a (informative).
Test-Lauf: Linux, verifiziert — cargo test -p zerodds-endpoint-e2e --test csharp
3/3 (Ping-Pong: csharp_raw_udp/csharp_endpoint_sync/csharp_endpoint_async);
--test csharp_reliable 5/5 (csharp_reliable_loss_recovery,
csharp_reliable_no_loss_baseline, csharp_reliable_unit_and_golden — 32
inline Assertions inkl. Byte-Golden, csharp_reliable_standalone_example,
csharp_reliable_latency_bench); Latenz-Bench Ring-Enqueue ~6–10 µs / inline
Socket.Send ~442–504 µs (~50–70×).
Gated auf dotnet (>= .NET 8, PATH oder ~/.dotnet/dotnet); ohne Toolchain
lauter Skip, kein false-green.
Offene Punkte: keine.