Skip to main content

nros_rmw/
event.rs

1//! Status-event surface (Phase 108).
2//!
3//! Tier-1 events that backends optionally surface — liveliness
4//! changes, deadline misses, message loss. Dispatched via
5//! callback-on-entity (registration on `Subscriber` / `Publisher`),
6//! not via upstream's waitset-take pattern.
7//!
8//! Events fire from inside [`Session::drive_io`](crate::Session::drive_io)
9//! on the executor thread, the same as message callbacks. They count
10//! against the executor's `max_callbacks_per_spin` cap (Phase 105) the
11//! same way.
12//!
13//! See `book/src/concepts/status-events.md` for the user-facing
14//! patterns and `book/src/design/rmw-vs-upstream.md` Section 8 for
15//! the design rationale.
16//!
17//! ### Tier-2 / Tier-3 deliberately skipped
18//!
19//! Upstream `rmw_event_type_t` includes `MATCHED`, `QOS_INCOMPATIBLE`,
20//! and `INCOMPATIBLE_TYPE`. The first is deferred until dynamic-
21//! discovery use cases appear; additive without an ABI break. The
22//! latter two are surfaced synchronously at create time as
23//! `TransportError::IncompatibleQos` / `TopicNameInvalid` (mapped to
24//! `NROS_RMW_RET_INCOMPATIBLE_QOS` / `NROS_RMW_RET_TOPIC_NAME_INVALID`
25//! at the C boundary). No runtime event needed.
26
27/// Tier-1 status-event kinds. `#[non_exhaustive]` so adding a Tier-2
28/// (`MATCHED`) variant later is not an ABI break for matchers.
29#[non_exhaustive]
30#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
31#[repr(u8)]
32pub enum EventKind {
33    /// Subscriber: a tracked publisher's liveliness state changed
34    /// (started / stopped asserting).
35    LivelinessChanged = 0,
36    /// Subscriber: an expected sample didn't arrive within the
37    /// configured deadline.
38    RequestedDeadlineMissed = 1,
39    /// Subscriber: the backend dropped a sample (overflow / etc.).
40    MessageLost = 2,
41    /// Publisher: this publisher missed its own liveliness assertion.
42    LivelinessLost = 3,
43    /// Publisher: this publisher promised X Hz, fell behind.
44    OfferedDeadlineMissed = 4,
45}
46
47/// Liveliness-status payload. Mirrors DDS
48/// `rmw_liveliness_changed_status_t` shape.
49#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
50#[repr(C)]
51pub struct LivelinessChangedStatus {
52    /// Number of currently-alive matched publishers (subscriber side)
53    /// or self-asserting state (publisher side, always 0/1).
54    pub alive_count: u16,
55    /// Number of currently-not-alive matched publishers.
56    pub not_alive_count: u16,
57    /// Change in `alive_count` since the last callback fire.
58    pub alive_count_change: i16,
59    /// Change in `not_alive_count` since the last callback fire.
60    pub not_alive_count_change: i16,
61}
62
63/// Deadline / message-lost payload. Used for
64/// `RequestedDeadlineMissed`, `LivelinessLost`,
65/// `OfferedDeadlineMissed`, `MessageLost` — same shape.
66#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
67#[repr(C)]
68pub struct CountStatus {
69    /// Cumulative count over the entity's lifetime.
70    pub total_count: u32,
71    /// Change since the last callback fire.
72    pub total_count_change: u32,
73}
74
75/// Type alias: the deadline-missed shape is identical to
76/// [`CountStatus`].
77pub type DeadlineMissedStatus = CountStatus;
78
79/// Type alias: the message-lost shape is identical to [`CountStatus`].
80pub type MessageLostStatus = CountStatus;
81
82/// Borrow-shaped event payload union. Variant selection mirrors
83/// [`EventKind`].
84#[derive(Debug, Clone, Copy)]
85pub enum EventPayload<'a> {
86    LivelinessChanged(&'a LivelinessChangedStatus),
87    RequestedDeadlineMissed(&'a DeadlineMissedStatus),
88    MessageLost(&'a MessageLostStatus),
89    LivelinessLost(&'a CountStatus),
90    OfferedDeadlineMissed(&'a DeadlineMissedStatus),
91}
92
93impl<'a> EventPayload<'a> {
94    /// Returns the [`EventKind`] this payload corresponds to.
95    pub fn kind(&self) -> EventKind {
96        match self {
97            EventPayload::LivelinessChanged(_) => EventKind::LivelinessChanged,
98            EventPayload::RequestedDeadlineMissed(_) => EventKind::RequestedDeadlineMissed,
99            EventPayload::MessageLost(_) => EventKind::MessageLost,
100            EventPayload::LivelinessLost(_) => EventKind::LivelinessLost,
101            EventPayload::OfferedDeadlineMissed(_) => EventKind::OfferedDeadlineMissed,
102        }
103    }
104}
105
106/// Raw event callback. Identical Rust + C ABI; no_std + alloc-free.
107///
108/// The backend invokes this with the [`EventKind`] selecting which
109/// payload-pointer variant is valid. `user_ctx` is opaque application
110/// state passed at registration.
111///
112/// **Lifetime.** `payload_ptr` is valid for the duration of this call
113/// only. Copy fields out before returning if needed beyond.
114///
115/// **Threading.** Invoked from inside `drive_io` on the executor
116/// thread; do not block.
117///
118/// **Safety contract.** The callback is `unsafe extern "C" fn`:
119/// implementors guarantee `payload_ptr` is non-null and points to a
120/// valid payload of the variant selected by `kind`.
121pub type EventCallback = unsafe extern "C" fn(
122    kind: EventKind,
123    payload_ptr: *const core::ffi::c_void,
124    user_ctx: *mut core::ffi::c_void,
125);
126
127/// Helper: convert a raw `(kind, payload_ptr)` pair into a typed
128/// [`EventPayload`] borrow. Used inside `EventCallback` trampolines.
129///
130/// # Safety
131///
132/// Caller guarantees `payload_ptr` matches the variant indicated by
133/// `kind` and is valid for the borrow lifetime.
134pub unsafe fn payload_from_raw<'a>(
135    kind: EventKind,
136    payload_ptr: *const core::ffi::c_void,
137) -> EventPayload<'a> {
138    match kind {
139        EventKind::LivelinessChanged => unsafe {
140            EventPayload::LivelinessChanged(&*(payload_ptr as *const LivelinessChangedStatus))
141        },
142        EventKind::RequestedDeadlineMissed => unsafe {
143            EventPayload::RequestedDeadlineMissed(&*(payload_ptr as *const CountStatus))
144        },
145        EventKind::MessageLost => unsafe {
146            EventPayload::MessageLost(&*(payload_ptr as *const CountStatus))
147        },
148        EventKind::LivelinessLost => unsafe {
149            EventPayload::LivelinessLost(&*(payload_ptr as *const CountStatus))
150        },
151        EventKind::OfferedDeadlineMissed => unsafe {
152            EventPayload::OfferedDeadlineMissed(&*(payload_ptr as *const CountStatus))
153        },
154    }
155}