Skip to main content

zerodds_dcps/
listener.rs

1// SPDX-License-Identifier: Apache-2.0
2// Copyright 2026 ZeroDDS Contributors
3//! Listener hierarchy (DDS DCPS 1.4 §2.2.4.2 + §2.2.2.*.3 set_listener).
4//!
5//! Listeners are asynchronous notification hooks that the middleware
6//! layer calls as soon as a communication status changes. Per entity
7//! type there is one listener trait with one callback per relevant
8//! status:
9//!
10//! ```text
11//! DomainParticipantListener   (13 callbacks — all bubble-up targets)
12//! ├── PublisherListener       (4  callbacks — writer-related)
13//! │   └── DataWriterListener  (4  callbacks)
14//! ├── SubscriberListener      (8  callbacks — reader + on_data_on_readers)
15//! │   └── DataReaderListener  (7  callbacks — reader-specific)
16//! └── TopicListener           (1  callback — on_inconsistent_topic)
17//! ```
18//!
19//! ## Bubble-up (Spec §2.2.4.2.3)
20//!
21//! When **no** listener is set on the "smallest" entity (e.g.
22//! DataReader) — or the listener does not have the status bit in its
23//! mask — the event bubbles up to the next larger entity:
24//! `Reader → Subscriber → Participant`. Likewise
25//! `Writer → Publisher → Participant`. Topic events bubble directly to
26//! the participant. The `bubble_up_consumed` helpers in
27//! [`listener_dispatch`](crate::listener_dispatch) encapsulate this
28//! resolution.
29//!
30//! ## Object safety
31//!
32//! All 6 traits are **object-safe** (no `Self` returns, no generics, no
33//! associated types). So that each trait is usable generically over
34//! `T: DdsType` (we have `Topic<T>`, `DataWriter<T>`, `DataReader<T>`),
35//! we pass the entity handle as an opaque [`InstanceHandle`] — analogous
36//! to the DDS-DCPS IDL-PSM, which also gives listener callbacks only the
37//! entity *handle* (DCPS 1.4 §2.3.3 IDL).
38//!
39//! We store the listener as `Box<dyn ListenerTrait + Send + Sync>` in
40//! the entity state, so that it is visible cross-thread (the spec does
41//! not say that listener callbacks must run from the application
42//! thread).
43//!
44//! All methods take `&self` (not `&mut self`), because the listener is
45//! shared in the hot path; the callback body must use interior
46//! mutability if it carries state.
47//!
48//! ## Default impls
49//!
50//! Every method has an empty body. Users override only the callbacks
51//! they actually need.
52
53extern crate alloc;
54
55use alloc::boxed::Box;
56
57use crate::entity::StatusMask;
58use crate::instance_handle::InstanceHandle;
59use crate::psm_constants::status as status_bits;
60use crate::status::{
61    InconsistentTopicStatus, LivelinessChangedStatus, LivelinessLostStatus,
62    OfferedDeadlineMissedStatus, OfferedIncompatibleQosStatus, PublicationMatchedStatus,
63    RequestedDeadlineMissedStatus, RequestedIncompatibleQosStatus, SampleLostStatus,
64    SampleRejectedStatus, SubscriptionMatchedStatus,
65};
66
67// ============================================================================
68// TopicListener (Spec §2.2.2.3.2)
69// ============================================================================
70
71/// `TopicListener` — Spec §2.2.2.3.2 + §2.2.4.2.5.
72///
73/// Exactly one callback: `on_inconsistent_topic`. The `topic` parameter
74/// is passed as an opaque [`InstanceHandle`] (Spec §2.3.3 IDL-PSM).
75pub trait TopicListener: Send + Sync {
76    /// Spec §2.2.4.2.5 — called when another topic with the same name
77    /// but a different type definition is discovered.
78    fn on_inconsistent_topic(&self, _topic: InstanceHandle, _status: InconsistentTopicStatus) {}
79}
80
81// ============================================================================
82// DataWriterListener (Spec §2.2.2.4.4)
83// ============================================================================
84
85/// `DataWriterListener` — Spec §2.2.2.4.4 + §2.2.4.2.4.
86///
87/// 4 callbacks: `on_offered_deadline_missed`, `on_offered_incompatible_qos`,
88/// `on_liveliness_lost`, `on_publication_matched`.
89pub trait DataWriterListener: Send + Sync {
90    /// Spec §2.2.4.2.4.1 — the writer did not honor the offered DEADLINE
91    /// promise.
92    fn on_offered_deadline_missed(
93        &self,
94        _writer: InstanceHandle,
95        _status: OfferedDeadlineMissedStatus,
96    ) {
97    }
98
99    /// Spec §2.2.4.2.4.2 — a matched reader has incompatible requested
100    /// QoS.
101    fn on_offered_incompatible_qos(
102        &self,
103        _writer: InstanceHandle,
104        _status: OfferedIncompatibleQosStatus,
105    ) {
106    }
107
108    /// Spec §2.2.4.2.4.3 — the writer was declared not_alive from the
109    /// readers' point of view.
110    fn on_liveliness_lost(&self, _writer: InstanceHandle, _status: LivelinessLostStatus) {}
111
112    /// Spec §2.2.4.2.4.4 — a new compatible reader matched (or one
113    /// disappeared).
114    fn on_publication_matched(&self, _writer: InstanceHandle, _status: PublicationMatchedStatus) {}
115}
116
117// ============================================================================
118// PublisherListener (Spec §2.2.2.4.3)
119// ============================================================================
120
121/// `PublisherListener` — Spec §2.2.2.4.3.
122///
123/// Inheritance form (Spec): "is a listener of the writers contained
124/// within the publisher". We mirror the 4 DataWriterListener methods
125/// 1:1, so that the publisher works as a bubble-up target.
126pub trait PublisherListener: Send + Sync {
127    /// Bubble-up from [`DataWriterListener::on_offered_deadline_missed`].
128    fn on_offered_deadline_missed(
129        &self,
130        _writer: InstanceHandle,
131        _status: OfferedDeadlineMissedStatus,
132    ) {
133    }
134
135    /// Bubble-up from [`DataWriterListener::on_offered_incompatible_qos`].
136    fn on_offered_incompatible_qos(
137        &self,
138        _writer: InstanceHandle,
139        _status: OfferedIncompatibleQosStatus,
140    ) {
141    }
142
143    /// Bubble-up from [`DataWriterListener::on_liveliness_lost`].
144    fn on_liveliness_lost(&self, _writer: InstanceHandle, _status: LivelinessLostStatus) {}
145
146    /// Bubble-up from [`DataWriterListener::on_publication_matched`].
147    fn on_publication_matched(&self, _writer: InstanceHandle, _status: PublicationMatchedStatus) {}
148}
149
150// ============================================================================
151// DataReaderListener (Spec §2.2.2.5.7)
152// ============================================================================
153
154/// `DataReaderListener` — Spec §2.2.2.5.7 + §2.2.4.2.6.
155///
156/// 7 reader-specific callbacks (the eighth, `on_data_on_readers`,
157/// belongs to the [`SubscriberListener`]).
158pub trait DataReaderListener: Send + Sync {
159    /// Spec §2.2.4.2.6.1 — new data has arrived at the reader.
160    fn on_data_available(&self, _reader: InstanceHandle) {}
161
162    /// Spec §2.2.4.2.6.2 — a sample was never received (e.g. overwritten
163    /// by a newer one).
164    fn on_sample_lost(&self, _reader: InstanceHandle, _status: SampleLostStatus) {}
165
166    /// Spec §2.2.4.2.6.3 — a sample was rejected (RESOURCE_LIMITS).
167    fn on_sample_rejected(&self, _reader: InstanceHandle, _status: SampleRejectedStatus) {}
168
169    /// Spec §2.2.4.2.6.4 — the reader did not receive a sample within
170    /// the requested DEADLINE.
171    fn on_requested_deadline_missed(
172        &self,
173        _reader: InstanceHandle,
174        _status: RequestedDeadlineMissedStatus,
175    ) {
176    }
177
178    /// Spec §2.2.4.2.6.5 — a matched writer has incompatible offered
179    /// QoS.
180    fn on_requested_incompatible_qos(
181        &self,
182        _reader: InstanceHandle,
183        _status: RequestedIncompatibleQosStatus,
184    ) {
185    }
186
187    /// Spec §2.2.4.2.6.6 — the liveliness status of the matched writers
188    /// has changed.
189    fn on_liveliness_changed(&self, _reader: InstanceHandle, _status: LivelinessChangedStatus) {}
190
191    /// Spec §2.2.4.2.6.7 — a new compatible writer matched (or went away).
192    fn on_subscription_matched(&self, _reader: InstanceHandle, _status: SubscriptionMatchedStatus) {
193    }
194}
195
196// ============================================================================
197// SubscriberListener (Spec §2.2.2.5.6)
198// ============================================================================
199
200/// `SubscriberListener` — Spec §2.2.2.5.6 + §2.2.4.2.7.
201///
202/// Inherits all 7 reader callbacks + 1 additional `on_data_on_readers`.
203pub trait SubscriberListener: Send + Sync {
204    /// Spec §2.2.4.2.7.1 — some reader of the subscriber has new data
205    /// (subscriber-level notification).
206    fn on_data_on_readers(&self, _subscriber: InstanceHandle) {}
207
208    /// Bubble-up from [`DataReaderListener::on_data_available`].
209    fn on_data_available(&self, _reader: InstanceHandle) {}
210
211    /// Bubble-up from [`DataReaderListener::on_sample_lost`].
212    fn on_sample_lost(&self, _reader: InstanceHandle, _status: SampleLostStatus) {}
213
214    /// Bubble-up from [`DataReaderListener::on_sample_rejected`].
215    fn on_sample_rejected(&self, _reader: InstanceHandle, _status: SampleRejectedStatus) {}
216
217    /// Bubble-up from [`DataReaderListener::on_requested_deadline_missed`].
218    fn on_requested_deadline_missed(
219        &self,
220        _reader: InstanceHandle,
221        _status: RequestedDeadlineMissedStatus,
222    ) {
223    }
224
225    /// Bubble-up from [`DataReaderListener::on_requested_incompatible_qos`].
226    fn on_requested_incompatible_qos(
227        &self,
228        _reader: InstanceHandle,
229        _status: RequestedIncompatibleQosStatus,
230    ) {
231    }
232
233    /// Bubble-up from [`DataReaderListener::on_liveliness_changed`].
234    fn on_liveliness_changed(&self, _reader: InstanceHandle, _status: LivelinessChangedStatus) {}
235
236    /// Bubble-up from [`DataReaderListener::on_subscription_matched`].
237    fn on_subscription_matched(&self, _reader: InstanceHandle, _status: SubscriptionMatchedStatus) {
238    }
239}
240
241// ============================================================================
242// DomainParticipantListener (Spec §2.2.2.2.3)
243// ============================================================================
244
245/// `DomainParticipantListener` — Spec §2.2.2.2.3 + §2.2.4.2.8.
246///
247/// Unifies all status callbacks of all subordinate entities, because
248/// every event can — per spec — bubble all the way to the top if no
249/// listener is installed on the narrower entity.
250///
251/// The spec lists **13 callbacks** (the union of all status hooks):
252/// - 1 topic       (`on_inconsistent_topic`)
253/// - 4 writer      (`on_offered_*`, `on_liveliness_lost`, `on_publication_matched`)
254/// - 7 reader      (`on_data_available`, `on_sample_*`,
255///                  `on_requested_*`, `on_liveliness_changed`,
256///                  `on_subscription_matched`)
257/// - 1 subscriber  (`on_data_on_readers`)
258pub trait DomainParticipantListener: Send + Sync {
259    // -------- Topic --------
260
261    /// Bubble-up from [`TopicListener::on_inconsistent_topic`].
262    fn on_inconsistent_topic(&self, _topic: InstanceHandle, _status: InconsistentTopicStatus) {}
263
264    // -------- Writer side --------
265
266    /// Bubble-up from [`PublisherListener::on_offered_deadline_missed`].
267    fn on_offered_deadline_missed(
268        &self,
269        _writer: InstanceHandle,
270        _status: OfferedDeadlineMissedStatus,
271    ) {
272    }
273
274    /// Bubble-up from [`PublisherListener::on_offered_incompatible_qos`].
275    fn on_offered_incompatible_qos(
276        &self,
277        _writer: InstanceHandle,
278        _status: OfferedIncompatibleQosStatus,
279    ) {
280    }
281
282    /// Bubble-up from [`PublisherListener::on_liveliness_lost`].
283    fn on_liveliness_lost(&self, _writer: InstanceHandle, _status: LivelinessLostStatus) {}
284
285    /// Bubble-up from [`PublisherListener::on_publication_matched`].
286    fn on_publication_matched(&self, _writer: InstanceHandle, _status: PublicationMatchedStatus) {}
287
288    // -------- Reader side --------
289
290    /// Bubble-up from [`SubscriberListener::on_data_on_readers`].
291    fn on_data_on_readers(&self, _subscriber: InstanceHandle) {}
292
293    /// Bubble-up from [`SubscriberListener::on_data_available`].
294    fn on_data_available(&self, _reader: InstanceHandle) {}
295
296    /// Bubble-up from [`SubscriberListener::on_sample_lost`].
297    fn on_sample_lost(&self, _reader: InstanceHandle, _status: SampleLostStatus) {}
298
299    /// Bubble-up from [`SubscriberListener::on_sample_rejected`].
300    fn on_sample_rejected(&self, _reader: InstanceHandle, _status: SampleRejectedStatus) {}
301
302    /// Bubble-up from [`SubscriberListener::on_requested_deadline_missed`].
303    fn on_requested_deadline_missed(
304        &self,
305        _reader: InstanceHandle,
306        _status: RequestedDeadlineMissedStatus,
307    ) {
308    }
309
310    /// Bubble-up from [`SubscriberListener::on_requested_incompatible_qos`].
311    fn on_requested_incompatible_qos(
312        &self,
313        _reader: InstanceHandle,
314        _status: RequestedIncompatibleQosStatus,
315    ) {
316    }
317
318    /// Bubble-up from [`SubscriberListener::on_liveliness_changed`].
319    fn on_liveliness_changed(&self, _reader: InstanceHandle, _status: LivelinessChangedStatus) {}
320
321    /// Bubble-up from [`SubscriberListener::on_subscription_matched`].
322    fn on_subscription_matched(&self, _reader: InstanceHandle, _status: SubscriptionMatchedStatus) {
323    }
324}
325
326// ============================================================================
327// Boxed listener aliases (for storage in the entity state)
328// ============================================================================
329
330/// Heap-allocated, thread-safe box wrapper for the 6 listener traits.
331/// This is how each entity stores its listener.
332pub type BoxedTopicListener = Box<dyn TopicListener>;
333/// Cf. [`BoxedTopicListener`].
334pub type BoxedDataWriterListener = Box<dyn DataWriterListener>;
335/// Cf. [`BoxedTopicListener`].
336pub type BoxedPublisherListener = Box<dyn PublisherListener>;
337/// Cf. [`BoxedTopicListener`].
338pub type BoxedDataReaderListener = Box<dyn DataReaderListener>;
339/// Cf. [`BoxedTopicListener`].
340pub type BoxedSubscriberListener = Box<dyn SubscriberListener>;
341/// Cf. [`BoxedTopicListener`].
342pub type BoxedDomainParticipantListener = Box<dyn DomainParticipantListener>;
343
344/// Arc variant: per slot we store the listener as `Arc<dyn ...>`,
345/// because the hot path briefly clones the listener under the slot mutex
346/// and then calls it outside the lock (to avoid deadlocks). A Box would
347/// not allow that.
348pub type ArcTopicListener = alloc::sync::Arc<dyn TopicListener>;
349/// Cf. [`ArcTopicListener`].
350pub type ArcDataWriterListener = alloc::sync::Arc<dyn DataWriterListener>;
351/// Cf. [`ArcTopicListener`].
352pub type ArcPublisherListener = alloc::sync::Arc<dyn PublisherListener>;
353/// Cf. [`ArcTopicListener`].
354pub type ArcDataReaderListener = alloc::sync::Arc<dyn DataReaderListener>;
355/// Cf. [`ArcTopicListener`].
356pub type ArcSubscriberListener = alloc::sync::Arc<dyn SubscriberListener>;
357/// Cf. [`ArcTopicListener`].
358pub type ArcDomainParticipantListener = alloc::sync::Arc<dyn DomainParticipantListener>;
359
360// ============================================================================
361// Bubble-up helpers
362// ============================================================================
363
364/// True if `mask` sets the bit for `status` **and** the listener is
365/// non-`None`. This combination decides whether an event is consumed at
366/// the current level (Spec §2.2.4.2.3).
367#[inline]
368#[must_use]
369pub fn listener_handles(listener_present: bool, mask: StatusMask, status_bit: u32) -> bool {
370    listener_present && (mask & status_bit) != 0
371}
372
373/// Convenience helper: returns the status-bit value for a status name.
374/// Used only in tests + doc examples — the hot path uses the constants
375/// in [`crate::psm_constants::status`] directly.
376#[must_use]
377pub fn status_bit_for_inconsistent_topic() -> u32 {
378    status_bits::INCONSISTENT_TOPIC
379}
380
381#[cfg(test)]
382#[allow(clippy::expect_used, clippy::unwrap_used)]
383mod tests {
384    use super::*;
385    use core::sync::atomic::{AtomicU32, Ordering};
386
387    // -------- Object safety: all 6 traits must be usable as `dyn` --------
388
389    #[test]
390    fn topic_listener_is_object_safe() {
391        struct Counter(AtomicU32);
392        impl TopicListener for Counter {
393            fn on_inconsistent_topic(
394                &self,
395                _topic: InstanceHandle,
396                _status: InconsistentTopicStatus,
397            ) {
398                self.0.fetch_add(1, Ordering::Relaxed);
399            }
400        }
401        let _: BoxedTopicListener = Box::new(Counter(AtomicU32::new(0)));
402    }
403
404    #[test]
405    fn datawriter_listener_is_object_safe() {
406        struct L;
407        impl DataWriterListener for L {}
408        let _: BoxedDataWriterListener = Box::new(L);
409    }
410
411    #[test]
412    fn publisher_listener_is_object_safe() {
413        struct L;
414        impl PublisherListener for L {}
415        let _: BoxedPublisherListener = Box::new(L);
416    }
417
418    #[test]
419    fn datareader_listener_is_object_safe() {
420        struct L;
421        impl DataReaderListener for L {}
422        let _: BoxedDataReaderListener = Box::new(L);
423    }
424
425    #[test]
426    fn subscriber_listener_is_object_safe() {
427        struct L;
428        impl SubscriberListener for L {}
429        let _: BoxedSubscriberListener = Box::new(L);
430    }
431
432    #[test]
433    fn participant_listener_is_object_safe() {
434        struct L;
435        impl DomainParticipantListener for L {}
436        let _: BoxedDomainParticipantListener = Box::new(L);
437    }
438
439    // -------- Default impls may have an empty body --------
440
441    #[test]
442    fn default_callbacks_do_not_panic() {
443        // Empty impl on all 6 traits.
444        struct Noop;
445        impl TopicListener for Noop {}
446        impl DataWriterListener for Noop {}
447        impl PublisherListener for Noop {}
448        impl DataReaderListener for Noop {}
449        impl SubscriberListener for Noop {}
450        impl DomainParticipantListener for Noop {}
451        // We can at least construct + box them — the actual call needs
452        // an entity (see tests in entity.rs).
453        let _: BoxedDomainParticipantListener = Box::new(Noop);
454    }
455
456    #[test]
457    fn listener_handles_respects_mask_and_presence() {
458        let bit = status_bit_for_inconsistent_topic();
459        assert!(listener_handles(true, bit, bit));
460        assert!(!listener_handles(false, bit, bit));
461        assert!(!listener_handles(true, 0, bit));
462        // Bit not in mask.
463        assert!(!listener_handles(true, status_bits::SAMPLE_LOST, bit));
464    }
465
466    #[test]
467    fn status_bit_for_inconsistent_topic_matches_psm() {
468        assert_eq!(
469            status_bit_for_inconsistent_topic(),
470            status_bits::INCONSISTENT_TOPIC
471        );
472    }
473
474    #[test]
475    fn all_listener_traits_default_methods_invoke_safely() {
476        // Exercises the default bodies of all 6 listener traits.
477        // Since all default methods have empty bodies, we simply go
478        // through and call them on a Noop instance.
479        struct Noop;
480        impl TopicListener for Noop {}
481        impl DataWriterListener for Noop {}
482        impl PublisherListener for Noop {}
483        impl DataReaderListener for Noop {}
484        impl SubscriberListener for Noop {}
485        impl DomainParticipantListener for Noop {}
486
487        let h = InstanceHandle::from_raw(1);
488        let n = Noop;
489        TopicListener::on_inconsistent_topic(&n, h, InconsistentTopicStatus::default());
490
491        DataWriterListener::on_offered_deadline_missed(
492            &n,
493            h,
494            OfferedDeadlineMissedStatus::default(),
495        );
496        DataWriterListener::on_offered_incompatible_qos(
497            &n,
498            h,
499            OfferedIncompatibleQosStatus::default(),
500        );
501        DataWriterListener::on_liveliness_lost(&n, h, LivelinessLostStatus::default());
502        DataWriterListener::on_publication_matched(&n, h, PublicationMatchedStatus::default());
503
504        PublisherListener::on_offered_deadline_missed(
505            &n,
506            h,
507            OfferedDeadlineMissedStatus::default(),
508        );
509        PublisherListener::on_offered_incompatible_qos(
510            &n,
511            h,
512            OfferedIncompatibleQosStatus::default(),
513        );
514        PublisherListener::on_liveliness_lost(&n, h, LivelinessLostStatus::default());
515        PublisherListener::on_publication_matched(&n, h, PublicationMatchedStatus::default());
516
517        DataReaderListener::on_data_available(&n, h);
518        DataReaderListener::on_sample_lost(&n, h, SampleLostStatus::default());
519        DataReaderListener::on_sample_rejected(&n, h, SampleRejectedStatus::default());
520        DataReaderListener::on_requested_deadline_missed(
521            &n,
522            h,
523            RequestedDeadlineMissedStatus::default(),
524        );
525        DataReaderListener::on_requested_incompatible_qos(
526            &n,
527            h,
528            RequestedIncompatibleQosStatus::default(),
529        );
530        DataReaderListener::on_liveliness_changed(&n, h, LivelinessChangedStatus::default());
531        DataReaderListener::on_subscription_matched(&n, h, SubscriptionMatchedStatus::default());
532
533        SubscriberListener::on_data_on_readers(&n, h);
534        SubscriberListener::on_data_available(&n, h);
535        SubscriberListener::on_sample_lost(&n, h, SampleLostStatus::default());
536        SubscriberListener::on_sample_rejected(&n, h, SampleRejectedStatus::default());
537        SubscriberListener::on_requested_deadline_missed(
538            &n,
539            h,
540            RequestedDeadlineMissedStatus::default(),
541        );
542        SubscriberListener::on_requested_incompatible_qos(
543            &n,
544            h,
545            RequestedIncompatibleQosStatus::default(),
546        );
547        SubscriberListener::on_liveliness_changed(&n, h, LivelinessChangedStatus::default());
548        SubscriberListener::on_subscription_matched(&n, h, SubscriptionMatchedStatus::default());
549
550        DomainParticipantListener::on_inconsistent_topic(&n, h, InconsistentTopicStatus::default());
551        DomainParticipantListener::on_offered_deadline_missed(
552            &n,
553            h,
554            OfferedDeadlineMissedStatus::default(),
555        );
556        DomainParticipantListener::on_offered_incompatible_qos(
557            &n,
558            h,
559            OfferedIncompatibleQosStatus::default(),
560        );
561        DomainParticipantListener::on_liveliness_lost(&n, h, LivelinessLostStatus::default());
562        DomainParticipantListener::on_publication_matched(
563            &n,
564            h,
565            PublicationMatchedStatus::default(),
566        );
567        DomainParticipantListener::on_data_on_readers(&n, h);
568        DomainParticipantListener::on_data_available(&n, h);
569        DomainParticipantListener::on_sample_lost(&n, h, SampleLostStatus::default());
570        DomainParticipantListener::on_sample_rejected(&n, h, SampleRejectedStatus::default());
571        DomainParticipantListener::on_requested_deadline_missed(
572            &n,
573            h,
574            RequestedDeadlineMissedStatus::default(),
575        );
576        DomainParticipantListener::on_requested_incompatible_qos(
577            &n,
578            h,
579            RequestedIncompatibleQosStatus::default(),
580        );
581        DomainParticipantListener::on_liveliness_changed(&n, h, LivelinessChangedStatus::default());
582        DomainParticipantListener::on_subscription_matched(
583            &n,
584            h,
585            SubscriptionMatchedStatus::default(),
586        );
587    }
588
589    #[test]
590    fn datareader_listener_call_runs_default_methods() {
591        struct Counters {
592            avail: AtomicU32,
593            lost: AtomicU32,
594        }
595        impl DataReaderListener for Counters {
596            fn on_data_available(&self, _r: InstanceHandle) {
597                self.avail.fetch_add(1, Ordering::Relaxed);
598            }
599            fn on_sample_lost(&self, _r: InstanceHandle, _s: SampleLostStatus) {
600                self.lost.fetch_add(1, Ordering::Relaxed);
601            }
602        }
603        let c = Counters {
604            avail: AtomicU32::new(0),
605            lost: AtomicU32::new(0),
606        };
607        let h = InstanceHandle::from_raw(1);
608        c.on_data_available(h);
609        c.on_data_available(h);
610        c.on_sample_lost(h, SampleLostStatus::default());
611        // Methods we did not override should work as a default no-op.
612        c.on_subscription_matched(h, SubscriptionMatchedStatus::default());
613        assert_eq!(c.avail.load(Ordering::Relaxed), 2);
614        assert_eq!(c.lost.load(Ordering::Relaxed), 1);
615    }
616}