zerodds-listener-callbacks v1.1 — Spec Coverage

Audit of the vendor spec docs/specs/zerodds-listener-callbacks-1.1.md against the code in crates/zerodds-c-api/src/listener_ffi.rs (with the status counters and inconsistent-topic detection in crates/dcps/ and crates/discovery/).

Source: docs/specs/zerodds-listener-callbacks-1.1.md (ZeroDDS vendor spec).

Implementation:


§1 Architecture

§1.1 Function-pointer table

Spec: §1.1 — listeners as #[repr(C)] structs of function pointers, all optional (NULL = ignored).

Repo: listener_ffi.rs — 6 structs: ZeroDdsDomainParticipantListener, ZeroDdsPublisherListener, ZeroDdsSubscriberListener, ZeroDdsTopicListener, ZeroDdsDataWriterListener, ZeroDdsDataReaderListener.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip, dw_set_listener_clear_via_null.

Status: done

§1.2 user_data slot

Spec: §1.2 — one void* user_data per struct, passed unchanged to every callback.

Repo: all 6 structs carry user_data: *mut c_void as the first field; fire_writer_vtable / fire_reader_vtable pass it to each callback.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip.

Status: done

§1.3 Set/Get API

Spec: §1.3 — one *_set_listener / *_get_listener pair per entity type; NULL clears.

Repo: zerodds_{dp,pub,sub,topic,dw,dr}_set_listener + zerodds_{dp,dw,dr}_get_listener. set_listener(NULL) removes the registry entry and the cached counters.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip, dw_set_listener_clear_via_null.

Status: done


§2 Listener inventory

§2.1 DomainParticipantListener

Spec: §2.1 — on_inconsistent_topic, on_data_on_readers.

Repo: ZeroDdsDomainParticipantListener; both callbacks fired by the DP aggregator in zerodds_poll_listeners (§6.3, §6.4).

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers, poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done

§2.2 PublisherListener

Spec: §2.2 — 4 writer-status callbacks, aggregating contained DataWriters.

Repo: ZeroDdsPublisherListener; the Publisher aggregator walks publisher.datawriters and fires via fire_writer_vtable.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state (poll path), dp_set_get_listener_roundtrip (registry).

Status: done

§2.3 SubscriberListener

Spec: §2.3 — 7 reader-status callbacks + on_data_on_readers.

Repo: ZeroDdsSubscriberListener; the Subscriber aggregator walks subscriber.datareaders, fires the 7 reader callbacks via fire_reader_vtable and on_data_on_readers with set semantics.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§2.4 TopicListener

Spec: §2.4 — on_inconsistent_topic.

Repo: ZeroDdsTopicListener; fired by the Topic-level pass on an inconsistent-topic counter delta.

Tests: listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done

§2.5 DataWriterListener

Spec: §2.5 — on_liveliness_lost, on_offered_deadline_missed, on_offered_incompatible_qos, on_publication_matched.

Repo: ZeroDdsDataWriterListener; fired by the DataWriter level via fire_writer_vtable.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§2.6 DataReaderListener

Spec: §2.6 — all 7 reader-status callbacks.

Repo: ZeroDdsDataReaderListener; fired by the DataReader level via fire_reader_vtable.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done


§3 Status-mask semantics

Spec: §3 — status_mask filters which callbacks fire (bit set in mask AND pointer non-NULL).

Repo: set_listener(p, l, status_mask) stores the mask; every fire site checks mask & STATUS_* before invoking. Bit constants STATUS_INCONSISTENT_TOPIC … STATUS_SUBSCRIPTION_MATCHED match the DDS PSM values.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state (mask 0xFFFFFFFF).

Status: done


§4 Threading contract

§4.1 Caller-driven poll delivery

Spec: §4.1 — callbacks delivered from zerodds_poll_listeners(), never synchronously inside a set_listener/write/take call.

Repo: zerodds_poll_listeners is the only fire site; the registry holds raw pointers and set_listener never invokes callbacks.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§4.2 Re-entrancy

Spec: §4.2 — inside a callback the caller may read; must not free the listener/entity.

Repo: registry snapshots are taken and all locks released before any callback runs, so a callback may re-enter read APIs without deadlock.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers (callbacks run outside the registry locks).

Status: done

§4.3 Lifetime

Spec: §4.3 — listener pointer is caller-owned; registry holds it weak.

Repo: static OnceLock<ListenerRegistry> stores raw pointers; set_listener(NULL) clears.

Tests: listener_ffi::tests::dw_set_listener_clear_via_null.

Status: done


§5 Aggregator model — caller-driven multi-bind

Spec: §5 — independent multi-level firing (no first-match suppression); the same listener pointer bindable to several entities; aggregators walk their children.

Repo: zerodds_poll_listeners computes each entity’s delta once, then fires the DataWriter/DataReader level and each bound aggregator level independently via collect_publisher_writers / collect_subscriber_readers / collect_participant_subscriber_readers.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers (DataReader + Subscriber both fire for one sample).

Status: done


§6 Active firing

§6.1 Writer statuses

Spec: §6.1 — 4 writer callbacks fire on a writer-counter delta at the DataWriter and Publisher levels.

Repo: read_writer_counters + writer_delta + fire_writer_vtable; counters from DcpsRuntime::user_writer_*.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§6.2 Reader statuses

Spec: §6.2 — 6 reader status callbacks (matched, sample_lost, deadline, incompatible_qos, liveliness_changed, sample_rejected) fire on a reader-counter delta at the DataReader and Subscriber levels.

Repo: read_reader_counters (incl. user_reader_liveliness_status, user_reader_sample_rejected) + reader_delta + fire_reader_vtable.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§6.3 Data availability (set semantics)

Spec: §6.3 — on_data_available on a delivered-sample delta; on_data_on_readers once per poll per subscriber with fresh data.

Repo: UserReaderSlot::samples_delivered_count (bumped only at the user-delivery sites) exposed via DcpsRuntime::user_reader_samples_delivered; the Subscriber/DP aggregators fire on_data_on_readers once per subscriber with a data delta.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§6.4 Inconsistent topic

Spec: §6.4 — local clash prevented at create_topic; remote type-mismatch detected during SEDP matching, fires on_inconsistent_topic on Topic and DomainParticipant listeners.

Repo: zerodds_dp_create_topic rejects local clashes; DiscoveredEndpointsCache::topic_name_conflicts + DcpsRuntime::inconsistent_topic_seq bumped in match_local_{writer,reader}_against_cache, read via inconsistent_topic_count; poll fires on the Topic/DP levels.

Tests: sedp::cache::tests::topic_name_conflicts_detects_type_mismatch, listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done


§7 Cross-language mapping

§7.1 C++ Bridge

Spec: §7.1 — C++ wraps the C vtable.

Repo: crates/cpp/include/dds/pub/DataWriterListener.hpp, crates/cpp/include/dds/sub/DataReaderListener.hpp — one _DataWriterListenerBridge/_DataReaderListenerBridge each, with static extern "C" shims; user_data points at the C++ listener subclass.

Tests: crates/cpp/tests/smoke_dds_psm.cpp::test_listener_bridge_dispatches_to_virtual — reads back the vtable the bridge installed, via zerodds_dw_get_listener/ zerodds_dr_get_listener, and invokes its entries exactly as zerodds_poll_listeners() does (fn(vt->user_data, handle)): user_data is the C++ subclass itself, on_publication_matched and on_liveliness_lost each reach their own virtual method on the writer, on_data_available reaches the reader’s, a NULL user_data fires nothing, and attach(..., nullptr, 0) unregisters again. crates/cpp/tests/smoke_dds_psm.cpp::test_listener_attach_detach covers the no-poll case beside it. Driven through crates/cpp/tests/cpp_smoke_via_cargo.rs::dds_psm_cxx_smoke; locally (macOS) 1/1 green, OK — all DDS-PSM-Cxx smoke tests passed.

Status: done

§7.2 C# Bridge

Spec: §7.2 — IDataWriterListener<T> + GCHandle user_data; ListenerPoll.PollAll().

Repo: crates/cs/csharp/ZeroDDS/src/Listener.cs — IDataWriterListener<T>/IDataReaderListener<T>, the [UnmanagedCallersOnly(CallConvs = [CallConvCdecl])] trampolines in ListenerTrampolines (no delegate marshalling, no reflection), WriterListenerRegistration/ReaderListenerRegistration holding the vtable in Marshal.AllocHGlobal memory and the sink in a GCHandle whose GCHandle.ToIntPtr is the user_data; ListenerPoll.PollAll() calls zerodds_poll_listeners. Bound to the C FFI through crates/cs/csharp/ZeroDDS/src/Native.cs (DwSetListenerRaw/DrSetListenerRaw, PollListeners).

Tests: crates/cs/csharp/ZeroDDS.Tests/Program.cs, the “Listener attach + active firing” block: no callback before the first poll (§4.1), one on_publication_matched and one on_subscription_matched on the first poll with the status read back (§6.1/§6.2), no second callback without a delta (§6), on_data_available after a Write (§6.3), and no callback at all once the registration is disposed (§1.3/§9). Run through crates/cs/csharp/build_cs_smoke.sh: OK — all C# DDS-PSM-Cxx smoke tests passed.

Status: done

§7.3 Java Bridge

Spec: §7.3 — in-process Java-heap listeners on the InProcessBus + multi-process gRPC bridge.

Repo: crates/java-omgdds/java/src/main/java/org/omg/dds/sub/DataReaderListener.java and .../pub/DataWriterListener.java (Java-heap objects, with no vtable and no user_data in between), bound through DataReader.setListener/getListener and DataWriter.setListener/getListener; the match events come from org/zerodds/internal/InProcessBus.java (Endpoint.onWriterPresenceChanged, WriterPresence.onReaderPresenceChanged) and on_data_available straight out of the delivery path DataReader.onWrite. Status types org/omg/dds/core/status/{SubscriptionMatchedStatus,PublicationMatchedStatus}.java. Multi-process through org/zerodds/bridge/GrpcBridgeClient.java.

Tests: crates/java-omgdds/java/src/test/java/org/omg/dds/ListenerTest.java (5 cases: the match plus one callback per sample, the writer-side on_publication_matched including the un-match, setListener(null) delivering nothing further while the samples still arrive, a non-overlapping PARTITION not being a match, and the status staying readable with no listener bound), crates/java-omgdds/java/src/test/java/org/zerodds/bridge/GrpcBridgeClientTest.java with crates/java-omgdds/run_grpc_bridge_e2e.sh (3/3 against a running zerodds-grpc-bridged --features dds-runtime). mvn test overall: 95 tests, 0 failures.

Status: done

§7.4 Python Bridge

Spec: §7.4 — caller-driven polling API as the idiomatic listener-equivalent under the GIL.

Repo: crates/py/src/ffi.rs (wait_for_data, wait_for_matched_subscription, wait_for_matched_publication — the waiting operations release the GIL via py.detach), crates/py/src/listener.rs (DataWriterListener/DataReaderListener as callback holders).

Tests: crates/py/python/tests/test_listener.py (6 cases over listener construction and set_listener on the reader and the writer), crates/py/python/tests/test_smoke.py.

Status: done

§7.5 TypeScript Bridge

Spec: §7.5 — caller-driven polling API matching the single-threaded Node event loop.

Repo: crates/ts-node/src/dds.ts (DataReader.waitForMatched, DataWriter.waitForMatched, plus the promise forms waitForMatchedSubscription/waitForMatchedPublication).

Tests: crates/ts-node/test/smoke.test.ts — "ts pub-sub roundtrip" waits for the match with DataWriter.waitForMatched(1, 5000n) before writing; "typed facade: createTypedTopic + createTypedWriter", "async layer: writeAsync / takeAsync / streamSample" and "fluent facade: bytes pub/sub roundtrip" use waitForMatchedSubscription/waitForMatchedPublication as the caller-driven listener equivalent; "dds-psm-cxx api: GuardCondition + WaitSet" covers the wait path. crates/ts-node/test/qos.test.ts exercises the same API over nine QoS cases. Run npm test in crates/ts-node.

Status: done


§8 Test obligations

Spec: §8 — identity round-trip, NULL clear, poll fires on delta / nothing without delta, aggregator firing, inconsistent-topic firing.

Repo: crates/zerodds-c-api/src/listener_ffi.rs — the set/get API and zerodds_poll_listeners() together with its aggregators.

Tests: crates/zerodds-c-api/src/listener_ffi.rs — five tests, one per obligation in the clause: - Obligation 1 (identity round-trip): listener_ffi::tests::dp_set_get_listener_roundtrip. - Obligation 2 (NULL clear): listener_ffi::tests::dw_set_listener_clear_via_null. - Obligation 3 (poll fires on a delta, nothing without one): listener_ffi::tests::poll_listeners_returns_count_and_clears_state. - Obligation 4 (aggregator firing incl. on_data_on_readers set semantics): listener_ffi::tests::poll_fires_data_available_and_data_on_readers. - Obligation 5 (inconsistent topic on the Topic and the DomainParticipant): listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Run with cargo test -p zerodds-c-api --lib listener_ffi; locally (macOS) 5/5 green, 0 failures.

Status: done


§9 Memory ownership

Spec: §9 — caller owns the listener struct; registry holds it weak; caller must clear before freeing.

Repo: raw-pointer registry; set_listener(NULL) clears; tests clear listeners before entity teardown so no dangling pointer survives a later poll.

Tests: listener_ffi::tests::dw_set_listener_clear_via_null.

Status: done


§10 Stability

Spec: §10 — semver policy: append-only struct evolution, major bump for breaking changes.

Repo: —

Tests: —

Status: n/a (informative) — a forward-looking versioning policy, not an implementable requirement.


§11 Spec-conformance notes

Spec: §11 — all DDS 1.4 §2.2.4 listener methods exposed 1:1 and actively fired; user_data is a vendor detail.

Repo: all 17 callbacks across the 6 structs are exposed and fired (§6); user_data threaded through every fire site.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers, poll_fires_inconsistent_topic_for_topic_and_participant, poll_listeners_returns_count_and_clears_state.

Status: done


Audit-Status

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

Test run: cargo test -p zerodds-c-api -p zerodds-discovery -p zerodds-dcps --lib — 791 tests green, 0 failed, 1 ignored. Language sides: crates/cs/csharp/build_cs_smoke.sh with no failure; mvn test in crates/java-omgdds/java/ 95 tests, 0 failures, ListenerTest 5/5 among them; run_grpc_bridge_e2e.sh 3/3; cargo test -p zerodds-cpp --test cpp_smoke_via_cargo 1/1 (§7.1, locally macOS); npm test in crates/ts-node — smoke.test.ts 8/8 (§7.5, locally macOS); cargo test -p zerodds-c-api --lib listener_ffi 5/5 (§8, locally macOS).

Open items: none.

zerodds-listener-callbacks v1.1 — Spec-Coverage

Audit der Vendor-Spec docs/specs/zerodds-listener-callbacks-1.1.md gegen den Code in crates/zerodds-c-api/src/listener_ffi.rs (mit den Status-Countern und der Inconsistent-Topic-Detektion in crates/dcps/ und crates/discovery/).

Quelle: docs/specs/zerodds-listener-callbacks-1.1.md (ZeroDDS Vendor-Spec).

Implementierung:


§1 Architektur

§1.1 Funktions-Pointer-Tabelle

Spec: §1.1 — Listener als #[repr(C)]-Strukturen aus Funktions- Pointern, alle optional (NULL = ignoriert).

Repo: listener_ffi.rs — 6 Strukturen: ZeroDdsDomainParticipantListener, ZeroDdsPublisherListener, ZeroDdsSubscriberListener, ZeroDdsTopicListener, ZeroDdsDataWriterListener, ZeroDdsDataReaderListener.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip, dw_set_listener_clear_via_null.

Status: done

§1.2 user_data-Slot

Spec: §1.2 — ein void* user_data pro Struktur, unverändert an jeden Callback gereicht.

Repo: alle 6 Strukturen tragen user_data: *mut c_void als erstes Feld; fire_writer_vtable / fire_reader_vtable reichen es an jeden Callback.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip.

Status: done

§1.3 Set/Get-API

Spec: §1.3 — ein *_set_listener / *_get_listener-Paar pro Entity-Typ; NULL clears.

Repo: zerodds_{dp,pub,sub,topic,dw,dr}_set_listener + zerodds_{dp,dw,dr}_get_listener. set_listener(NULL) entfernt den Registry-Eintrag und die gecachten Counter.

Tests: listener_ffi::tests::dp_set_get_listener_roundtrip, dw_set_listener_clear_via_null.

Status: done


§2 Listener-Inventar

§2.1 DomainParticipantListener

Spec: §2.1 — on_inconsistent_topic, on_data_on_readers.

Repo: ZeroDdsDomainParticipantListener; beide Callbacks vom DP-Aggregator in zerodds_poll_listeners gefeuert (§6.3, §6.4).

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers, poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done

§2.2 PublisherListener

Spec: §2.2 — 4 Writer-Status-Callbacks, aggregiert über die enthaltenen DataWriter.

Repo: ZeroDdsPublisherListener; der Publisher-Aggregator läuft publisher.datawriters durch und feuert via fire_writer_vtable.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state (Poll-Pfad), dp_set_get_listener_roundtrip (Registry).

Status: done

§2.3 SubscriberListener

Spec: §2.3 — 7 Reader-Status-Callbacks + on_data_on_readers.

Repo: ZeroDdsSubscriberListener; der Subscriber-Aggregator läuft subscriber.datareaders durch, feuert die 7 Reader-Callbacks via fire_reader_vtable und on_data_on_readers mit Set-Semantik.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§2.4 TopicListener

Spec: §2.4 — on_inconsistent_topic.

Repo: ZeroDdsTopicListener; vom Topic-Ebenen-Pass auf einen Inconsistent-Topic-Counter-Delta gefeuert.

Tests: listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done

§2.5 DataWriterListener

Spec: §2.5 — on_liveliness_lost, on_offered_deadline_missed, on_offered_incompatible_qos, on_publication_matched.

Repo: ZeroDdsDataWriterListener; von der DataWriter-Ebene via fire_writer_vtable gefeuert.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§2.6 DataReaderListener

Spec: §2.6 — alle 7 Reader-Status-Callbacks.

Repo: ZeroDdsDataReaderListener; von der DataReader-Ebene via fire_reader_vtable gefeuert.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done


§3 Status-Mask-Semantik

Spec: §3 — status_mask filtert, welche Callbacks feuern (Bit in der Mask gesetzt UND Pointer non-NULL).

Repo: set_listener(p, l, status_mask) speichert die Mask; jede Fire-Stelle prüft mask & STATUS_* vor dem Aufruf. Die Bit-Konstanten STATUS_INCONSISTENT_TOPIC … STATUS_SUBSCRIPTION_MATCHED entsprechen den DDS-PSM-Werten.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state (Mask 0xFFFFFFFF).

Status: done


§4 Threading-Vertrag

§4.1 Caller-driven Poll-Delivery

Spec: §4.1 — Callbacks aus zerodds_poll_listeners(), nie synchron in einem set_listener/write/take-Aufruf.

Repo: zerodds_poll_listeners ist die einzige Fire-Stelle; die Registry hält rohe Pointer, und set_listener ruft nie Callbacks.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§4.2 Re-Entrancy

Spec: §4.2 — im Callback darf der Caller lesen; nicht den Listener/die Entity freigeben.

Repo: Registry-Snapshots werden gezogen und alle Locks vor jedem Callback freigegeben, sodass ein Callback Read-APIs deadlock-frei re-entrant aufrufen kann.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers (Callbacks laufen außerhalb der Registry-Locks).

Status: done

§4.3 Lifetime

Spec: §4.3 — Listener-Pointer ist Caller-owned; Registry hält ihn weak.

Repo: static OnceLock<ListenerRegistry> speichert rohe Pointer; set_listener(NULL) clears.

Tests: listener_ffi::tests::dw_set_listener_clear_via_null.

Status: done


§5 Aggregator-Modell — Caller-driven Multi-Bind

Spec: §5 — unabhängiges Multi-Level-Firing (kein First-Match-Suppress); derselbe Listener-Pointer an mehrere Entities bindbar; die Aggregatoren laufen ihre Children durch.

Repo: zerodds_poll_listeners berechnet das Delta jeder Entity einmal und feuert dann die DataWriter/DataReader-Ebene sowie jede gebundene Aggregator-Ebene unabhängig via collect_publisher_writers / collect_subscriber_readers / collect_participant_subscriber_readers.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers (DataReader + Subscriber feuern beide für ein Sample).

Status: done


§6 Active-Firing

§6.1 Writer-Status

Spec: §6.1 — 4 Writer-Callbacks feuern auf einen Writer-Counter-Delta an der DataWriter- und der Publisher-Ebene.

Repo: read_writer_counters + writer_delta + fire_writer_vtable; Counter aus DcpsRuntime::user_writer_*.

Tests: listener_ffi::tests::poll_listeners_returns_count_and_clears_state.

Status: done

§6.2 Reader-Status

Spec: §6.2 — 6 Reader-Status-Callbacks (matched, sample_lost, deadline, incompatible_qos, liveliness_changed, sample_rejected) feuern auf einen Reader-Counter-Delta an der DataReader- und der Subscriber-Ebene.

Repo: read_reader_counters (inkl. user_reader_liveliness_status, user_reader_sample_rejected) + reader_delta + fire_reader_vtable.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§6.3 Daten-Verfügbarkeit (Set-Semantik)

Spec: §6.3 — on_data_available auf einen Delivered-Sample-Delta; on_data_on_readers einmal pro Poll pro Subscriber mit frischen Daten.

Repo: UserReaderSlot::samples_delivered_count (nur an den User-Delivery-Stellen erhöht), exponiert via DcpsRuntime::user_reader_samples_delivered; die Subscriber/DP-Aggregatoren feuern on_data_on_readers einmal pro Subscriber mit Daten-Delta.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers.

Status: done

§6.4 Inconsistent-Topic

Spec: §6.4 — lokale Kollision bei create_topic verhindert; Remote- Typ-Mismatch beim SEDP-Matching erkannt, feuert on_inconsistent_topic auf Topic- und DomainParticipant-Listenern.

Repo: zerodds_dp_create_topic lehnt lokale Kollisionen ab; DiscoveredEndpointsCache::topic_name_conflicts + DcpsRuntime::inconsistent_topic_seq in match_local_{writer,reader}_against_cache erhöht, gelesen via inconsistent_topic_count; Poll feuert auf Topic-/DP-Ebene.

Tests: sedp::cache::tests::topic_name_conflicts_detects_type_mismatch, listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Status: done


§7 Cross-Language-Mapping

§7.1 C++ Bridge

Spec: §7.1 — C++ wrappt die C-vtable.

Repo: crates/cpp/include/dds/pub/DataWriterListener.hpp, crates/cpp/include/dds/sub/DataReaderListener.hpp — je ein _DataWriterListenerBridge/_DataReaderListenerBridge mit statischen extern "C"-Shims, user_data zeigt auf die C++-Listener-Subklasse.

Tests: crates/cpp/tests/smoke_dds_psm.cpp::test_listener_bridge_dispatches_to_virtual — liest die vom Bridge installierte vtable über zerodds_dw_get_listener/ zerodds_dr_get_listener zurück und ruft die Einträge genau so auf, wie es zerodds_poll_listeners() tut (fn(vt->user_data, handle)): user_data ist identisch mit der C++-Subklasse, on_publication_matched und on_liveliness_lost erreichen je ihre eigene virtuelle Methode (Writer), on_data_available die des Readers, ein NULL-user_data feuert nichts, und attach(..., nullptr, 0) räumt die Registrierung wieder ab. crates/cpp/tests/smoke_dds_psm.cpp::test_listener_attach_detach deckt daneben den Fall ohne Poll ab. Gefahren über crates/cpp/tests/cpp_smoke_via_cargo.rs::dds_psm_cxx_smoke; lokal (macOS) 1/1 grün, OK — all DDS-PSM-Cxx smoke tests passed.

Status: done

§7.2 C# Bridge

Spec: §7.2 — IDataWriterListener<T> + GCHandle-user_data; ListenerPoll.PollAll().

Repo: crates/cs/csharp/ZeroDDS/src/Listener.cs — IDataWriterListener<T>/IDataReaderListener<T>, die [UnmanagedCallersOnly(CallConvs = [CallConvCdecl])]-Trampoline in ListenerTrampolines (kein Delegate-Marshalling, keine Reflection), WriterListenerRegistration/ReaderListenerRegistration halten die Vtable in Marshal.AllocHGlobal-Speicher und den Sink in einem GCHandle, dessen GCHandle.ToIntPtr das user_data ist; ListenerPoll.PollAll() ruft zerodds_poll_listeners. Bindung an die C-FFI über crates/cs/csharp/ZeroDDS/src/Native.cs (DwSetListenerRaw/DrSetListenerRaw, PollListeners).

Tests: crates/cs/csharp/ZeroDDS.Tests/Program.cs, Block „Listener attach + active firing”: kein Callback vor dem ersten Poll (§4.1), je einmal on_publication_matched und on_subscription_matched beim ersten Poll mit zurückgelesenem Status (§6.1/§6.2), kein zweiter Callback ohne Delta (§6), on_data_available nach einem Write (§6.3), und kein Callback mehr nach Dispose der Registrierung (§1.3/§9). Lauf über crates/cs/csharp/build_cs_smoke.sh: OK — all C# DDS-PSM-Cxx smoke tests passed.

Status: done

§7.3 Java Bridge

Spec: §7.3 — In-Process-Java-Heap-Listener am InProcessBus + Multi-Process-gRPC-Bridge.

Repo: crates/java-omgdds/java/src/main/java/org/omg/dds/sub/DataReaderListener.java und .../pub/DataWriterListener.java (Java-Heap-Objekte, keine Vtable und kein user_data dazwischen), gebunden über DataReader.setListener/getListener und DataWriter.setListener/getListener; die Match-Ereignisse kommen aus org/zerodds/internal/InProcessBus.java (Endpoint.onWriterPresenceChanged, WriterPresence.onReaderPresenceChanged), on_data_available direkt aus dem Zustellpfad DataReader.onWrite. Status-Typen org/omg/dds/core/status/{SubscriptionMatchedStatus,PublicationMatchedStatus}.java. Multi-Prozess über org/zerodds/bridge/GrpcBridgeClient.java.

Tests: crates/java-omgdds/java/src/test/java/org/omg/dds/ListenerTest.java (5 Fälle: Match plus ein Callback je Sample, Writer-Seite on_publication_matched mit Un-Match, setListener(null) stellt nichts mehr zu während die Samples weiter ankommen, nicht überlappende PARTITION ist kein Match, und der Status bleibt ohne Listener lesbar), crates/java-omgdds/java/src/test/java/org/zerodds/bridge/GrpcBridgeClientTest.java mit crates/java-omgdds/run_grpc_bridge_e2e.sh (3/3 gegen den laufenden zerodds-grpc-bridged --features dds-runtime). mvn test gesamt: 95 Tests, 0 Failures.

Status: done

§7.4 Python Bridge

Spec: §7.4 — Caller-driven Polling-API als idiomatisches Listener-Äquivalent unter dem GIL.

Repo: crates/py/src/ffi.rs (wait_for_data, wait_for_matched_subscription, wait_for_matched_publication — die Warte-Operationen geben den GIL per py.detach frei), crates/py/src/listener.rs (DataWriterListener/DataReaderListener als Callback-Halter).

Tests: crates/py/python/tests/test_listener.py (6 Fälle über die Listener-Konstruktion und set_listener auf Reader und Writer), crates/py/python/tests/test_smoke.py.

Status: done

§7.5 TypeScript Bridge

Spec: §7.5 — Caller-driven Polling-API passend zum single-threaded Node-Event-Loop.

Repo: crates/ts-node/src/dds.ts (DataReader.waitForMatched, DataWriter.waitForMatched, dazu die Promise-Formen waitForMatchedSubscription/waitForMatchedPublication).

Tests: crates/ts-node/test/smoke.test.ts — "ts pub-sub roundtrip" wartet mit DataWriter.waitForMatched(1, 5000n) auf das Match, bevor geschrieben wird; "typed facade: createTypedTopic + createTypedWriter", "async layer: writeAsync / takeAsync / streamSample" und "fluent facade: bytes pub/sub roundtrip" nutzen waitForMatchedSubscription/waitForMatchedPublication als Caller-getriebenes Listener-Äquivalent; "dds-psm-cxx api: GuardCondition + WaitSet" deckt den Wartepfad. crates/ts-node/test/qos.test.ts übt dieselbe API über neun QoS-Fälle. Lauf npm test in crates/ts-node.

Status: done


§8 Test-Pflicht

Spec: §8 — Identity-Roundtrip, NULL-Clear, Poll feuert auf Delta / nichts ohne Delta, Aggregator-Firing, Inconsistent-Topic-Firing.

Repo: crates/zerodds-c-api/src/listener_ffi.rs — Set/Get-API, zerodds_poll_listeners() samt Aggregatoren.

Tests: crates/zerodds-c-api/src/listener_ffi.rs — fünf Tests, einer je Pflicht der Klausel: - Pflicht 1 (Identity-Roundtrip): listener_ffi::tests::dp_set_get_listener_roundtrip. - Pflicht 2 (NULL-Clear): listener_ffi::tests::dw_set_listener_clear_via_null. - Pflicht 3 (Poll feuert auf Delta, nichts ohne Delta): listener_ffi::tests::poll_listeners_returns_count_and_clears_state. - Pflicht 4 (Aggregator-Firing inkl. on_data_on_readers-Set-Semantik): listener_ffi::tests::poll_fires_data_available_and_data_on_readers. - Pflicht 5 (Inconsistent-Topic auf Topic und DomainParticipant): listener_ffi::tests::poll_fires_inconsistent_topic_for_topic_and_participant.

Lauf cargo test -p zerodds-c-api --lib listener_ffi; lokal (macOS) 5/5 grün, 0 Fehler.

Status: done


§9 Memory-Ownership

Spec: §9 — Caller besitzt die Listener-Struktur; Registry hält sie weak; Caller muss vor dem Freigeben clearen.

Repo: Raw-Pointer-Registry; set_listener(NULL) clears; Tests clearen Listener vor dem Entity-Teardown, sodass kein dangling Pointer einen späteren Poll überlebt.

Tests: listener_ffi::tests::dw_set_listener_clear_via_null.

Status: done


§10 Stabilität

Spec: §10 — Semver-Policy: append-only Struktur-Evolution, Major-Bump für Breaking-Changes.

Repo: —

Tests: —

Status: n/a (informative) — eine vorausschauende Versionierungs-Policy, keine implementierbare Anforderung.


§11 Spec-Konformitäts-Hinweise

Spec: §11 — alle DDS 1.4 §2.2.4 Listener-Methoden 1:1 exponiert und aktiv gefeuert; user_data ist ein Vendor-Detail.

Repo: alle 17 Callbacks über die 6 Strukturen sind exponiert und gefeuert (§6); user_data durch jede Fire-Stelle gereicht.

Tests: listener_ffi::tests::poll_fires_data_available_and_data_on_readers, poll_fires_inconsistent_topic_for_topic_and_participant, poll_listeners_returns_count_and_clears_state.

Status: done


Audit-Status

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

Test-Lauf: cargo test -p zerodds-c-api -p zerodds-discovery -p zerodds-dcps --lib — 791 Tests grün, 0 failed, 1 ignored. Sprachseiten: crates/cs/csharp/build_cs_smoke.sh ohne Fehlschlag; mvn test in crates/java-omgdds/java/ 95 Tests, 0 Failures, darunter ListenerTest 5/5; run_grpc_bridge_e2e.sh 3/3; cargo test -p zerodds-cpp --test cpp_smoke_via_cargo 1/1 (§7.1, lokal macOS); npm test in crates/ts-node — smoke.test.ts 8/8 (§7.5, lokal macOS); cargo test -p zerodds-c-api --lib listener_ffi 5/5 (§8, lokal macOS).

Offene Punkte: keine.