Skip to main content

mwdg_ffi/
lib.rs

1//! # mwdg-ffi — C FFI bindings for the mwdg micro-watchdog library
2//!
3//! This crate provides C-compatible bindings for the [`mwdg`] library,
4//! enabling use from C/C++ code. Use the generated `include/mwdg.h` header
5//! for the proper interface declarations.
6//!
7//! A user of the library must provide the following functions:
8//!
9//! ```c
10//! extern uint32_t mwdg_get_time_milliseconds(void);
11//! extern void mwdg_enter_critical(void);
12//! extern void mwdg_exit_critical(void);
13//! ```
14#![no_std]
15
16#[cfg(feature = "pack")]
17use core::panic::PanicInfo;
18
19use core::cell::UnsafeCell;
20use core::pin::Pin;
21use core::ptr;
22
23use mwdg::{WatchdogNode, WatchdogRegistry};
24
25unsafe extern "C" {
26    /// User-provided function that returns the current time in milliseconds.
27    fn mwdg_get_time_milliseconds() -> u32;
28    /// User-provided function to enter a critical section.
29    fn mwdg_enter_critical();
30    /// User-provided function to exit a critical section.
31    fn mwdg_exit_critical();
32}
33
34#[cfg(feature = "pack")]
35#[inline(never)]
36#[panic_handler]
37fn panic(_info: &PanicInfo) -> ! {
38    loop {}
39}
40
41/// A single software watchdog node.
42///
43/// Each RTOS task owns one of these (typically as a static or stack variable
44/// in a long-lived task). The struct is an intrusive linked-list node.
45///
46/// # C Usage
47/// ```c
48/// static struct mwdg_node my_wdg;
49/// mwdg_add(&my_wdg, 200); // 200 ms timeout
50/// ```
51#[repr(C)]
52pub struct mwdg_node {
53    /// Timeout interval in milliseconds. Set during [`mwdg_add`].
54    /// Treat as read-only after registration.
55    timeout_interval_ms: u32,
56
57    /// Timestamp (ms) of the last feed. Updated by [`mwdg_feed`].
58    last_touched_timestamp_ms: u32,
59
60    /// User-assigned identifier for this watchdog node.
61    /// Set via [`mwdg_assign_id`]. Defaults to `0`.
62    /// The library never modifies this field; it is purely for the user's
63    /// benefit when identifying expired nodes via [`mwdg_get_next_expired`].
64    id: u32,
65
66    /// Intrusive linked-list pointer to the next registered watchdog.
67    /// Null if this is the tail of the list.
68    next: *mut mwdg_node,
69}
70
71impl Default for mwdg_node {
72    fn default() -> Self {
73        Self {
74            timeout_interval_ms: 0,
75            last_touched_timestamp_ms: 0,
76            id: 0,
77            next: ptr::null_mut(),
78        }
79    }
80}
81
82// `WatchdogNode` is `#[repr(C)]` with fields (u32, u32, u32, *mut Self,
83// PhantomPinned). `PhantomPinned` is a ZST with alignment 1, so it does not
84// affect the `repr(C)` layout. The first four fields are identical in type and
85// order to `mwdg_node`, therefore the two types share the same size and
86// alignment. Casting `*mut mwdg_node` ↔ `*mut WatchdogNode` is sound.
87const _: () = assert!(
88    core::mem::size_of::<mwdg_node>() == core::mem::size_of::<WatchdogNode>(),
89    "mwdg_node and WatchdogNode must have the same size"
90);
91const _: () = assert!(
92    core::mem::align_of::<mwdg_node>() == core::mem::align_of::<WatchdogNode>(),
93    "mwdg_node and WatchdogNode must have the same alignment"
94);
95
96/// Cast a `*mut mwdg_node` to `*mut WatchdogNode`.
97///
98/// # Safety
99/// The caller must ensure the pointer is either null or points to a valid
100/// `mwdg_node`. The layout of `mwdg_node` and `WatchdogNode` is verified
101/// at compile time to be identical.
102#[inline]
103unsafe fn cast_node(ptr: *mut mwdg_node) -> *mut WatchdogNode {
104    ptr.cast::<WatchdogNode>()
105}
106
107/// Create a `Pin<&mut WatchdogNode>` from a raw `*mut mwdg_node`.
108///
109/// Returns `None` if the pointer is null.
110///
111/// # Safety
112/// The caller must ensure the pointer is valid, properly aligned, and
113/// the pointed-to node will not be moved for the duration of the
114/// returned reference's lifetime.
115#[inline]
116unsafe fn pin_node_mut<'a>(ptr: *mut mwdg_node) -> Option<Pin<&'a mut WatchdogNode>> {
117    if ptr.is_null() {
118        return None;
119    }
120    // SAFETY: ptr is non-null, cast is layout-compatible (verified at compile
121    // time), and the caller guarantees validity. Pin is safe because FFI
122    // callers must not move the node while registered.
123    unsafe { Some(Pin::new_unchecked(&mut *cast_node(ptr))) }
124}
125
126/// Wrapper to allow `WatchdogRegistry` in a `static`.
127///
128/// # Safety
129/// All access to the inner state is protected by the user-provided
130/// critical section callbacks (enter/exit). `mwdg_init` must be called
131/// once from a single context before any other function.
132struct GlobalState(UnsafeCell<WatchdogRegistry>);
133
134// SAFETY: All access is gated by user-provided critical section.
135unsafe impl Sync for GlobalState {}
136
137static STATE: GlobalState = GlobalState(UnsafeCell::new(WatchdogRegistry::new()));
138
139impl GlobalState {
140    #[allow(clippy::mut_from_ref)]
141    fn as_mut(&self) -> &mut WatchdogRegistry {
142        unsafe { &mut *self.0.get() }
143    }
144
145    fn as_ref(&self) -> &WatchdogRegistry {
146        unsafe { &*self.0.get() }
147    }
148}
149
150/// Execute `f` inside the user-provided critical section.
151#[inline]
152fn with_critical_section<R>(f: impl FnOnce(&mut WatchdogRegistry) -> R) -> R {
153    let state = STATE.as_mut();
154    unsafe { mwdg_enter_critical() };
155    let result = f(state);
156    unsafe { mwdg_exit_critical() };
157    result
158}
159
160/// Initialize the multi-watchdog subsystem.
161///
162/// Must be called exactly once before any other `mwdg_*` function,
163/// from a single execution context (e.g., main or init task).
164///
165/// # Safety
166/// - Must be called before any other `mwdg_*` function.
167/// - Must not be called from multiple threads concurrently.
168#[unsafe(no_mangle)]
169pub unsafe extern "C" fn mwdg_init() {
170    STATE.as_mut().init();
171}
172
173/// Register a software watchdog with the given timeout.
174///
175/// Initializes the watchdog fields and prepends it to the global list.
176/// The watchdog's `last_touched_timestamp_ms` is set to the current time.
177///
178/// If the node is already in the list (detected by pointer comparison), the
179/// call acts as a combined feed + timeout update — the node is **not** added
180/// a second time.
181///
182/// # Parameters
183/// - `wdg`: pointer to a caller-owned [`mwdg_node`]. Must remain valid
184///   (not dropped/freed) for as long as it is registered.
185/// - `timeout_ms`: the timeout interval in milliseconds.
186///
187/// # Safety
188/// - `wdg` must be a valid, non-null pointer to a `mwdg_node`.
189/// - `mwdg_init` must have been called.
190#[unsafe(no_mangle)]
191pub unsafe extern "C" fn mwdg_add(wdg: *mut mwdg_node, timeout_ms: u32) {
192    let Some(pinned) = (unsafe { pin_node_mut(wdg) }) else {
193        return;
194    };
195
196    with_critical_section(|registry| {
197        let now = unsafe { mwdg_get_time_milliseconds() };
198        registry.add(pinned, timeout_ms, now);
199    });
200}
201
202/// Remove a previously registered watchdog from the global list.
203///
204/// If `wdg` is null or the node is not found in the list, the function
205/// returns silently.
206///
207/// # Safety
208/// - `wdg` must be either null or a valid pointer to an `mwdg_node`.
209/// - `mwdg_init` must have been called.
210#[unsafe(no_mangle)]
211pub unsafe extern "C" fn mwdg_remove(wdg: *mut mwdg_node) {
212    let Some(pinned) = (unsafe { pin_node_mut(wdg) }) else {
213        return;
214    };
215
216    with_critical_section(|registry| {
217        registry.remove(pinned);
218    });
219}
220
221/// Feed (touch) a watchdog, resetting its timestamp to the current time.
222///
223/// Must be called periodically by the owning task to signal liveness.
224///
225/// # Safety
226/// - `wdg` must be a valid, non-null pointer to a registered `mwdg_node`.
227/// - `mwdg_init` must have been called.
228#[unsafe(no_mangle)]
229pub unsafe extern "C" fn mwdg_feed(wdg: *mut mwdg_node) {
230    let Some(pinned) = (unsafe { pin_node_mut(wdg) }) else {
231        return;
232    };
233
234    with_critical_section(|_| {
235        let now = unsafe { mwdg_get_time_milliseconds() };
236        WatchdogRegistry::feed(pinned, now);
237    });
238}
239
240/// Assign a user-chosen identifier to a watchdog node.
241///
242/// The identifier is stored in the node and can be retrieved later via
243/// [`mwdg_get_next_expired`] to determine which watchdog(s) have expired.
244/// The library never modifies this field internally; it is purely for the
245/// caller's use.
246///
247/// This function may be called at any time — before or after [`mwdg_add`].
248///
249/// # Parameters
250/// - `wdg`: pointer to a caller-owned [`mwdg_node`].
251/// - `id`: the identifier to assign.
252///
253/// # Safety
254/// - `wdg` must be either null or a valid pointer to an `mwdg_node`.
255/// - `mwdg_init` must have been called.
256#[unsafe(no_mangle)]
257pub unsafe extern "C" fn mwdg_assign_id(wdg: *mut mwdg_node, id: u32) {
258    let Some(pinned) = (unsafe { pin_node_mut(wdg) }) else {
259        return;
260    };
261
262    with_critical_section(|_| {
263        WatchdogRegistry::assign_id(pinned, id);
264    });
265}
266
267/// Check all registered watchdogs for expiration.
268///
269/// Iterates the linked list of registered watchdogs. For each one,
270/// computes elapsed time using wrapping arithmetic (safe across `u32` overflow)
271/// and compares against the timeout interval.
272///
273/// # Returns
274/// - `0` if all watchdogs are healthy (fed within their timeout).
275/// - `1` if any watchdog has expired.
276///
277/// # Safety
278/// - `mwdg_init` must have been called.
279/// - All registered `mwdg_node` pointers must still be valid.
280#[unsafe(no_mangle)]
281pub unsafe extern "C" fn mwdg_check() -> i32 {
282    // Fast path: if already expired, skip the critical section entirely.
283    // This is safe because `expired` is only ever set from false to true
284    // (monotonic / latching) inside the critical section, so a stale read
285    // of `true` is always correct.
286    if STATE.as_ref().is_expired() {
287        return 1;
288    }
289
290    with_critical_section(|registry| {
291        let now = unsafe { mwdg_get_time_milliseconds() };
292        i32::from(registry.check(now))
293    })
294}
295
296/// Iterate over registered watchdogs and find the next expired one.
297///
298/// This function implements a cursor-based iterator over the linked list of
299/// registered watchdogs.  On each call it resumes from the position stored in
300/// `*cursor` and scans forward for the next node whose elapsed time exceeds
301/// its timeout interval.
302///
303/// # Precondition
304/// [`mwdg_check`] must have been called **and returned `1`** before using
305/// this function.  Internally the iterator uses the timestamp snapshot
306/// captured by `mwdg_check` (`expired_at_ms`) to evaluate each node, so
307/// nodes are compared against the same point in time that triggered the
308/// expiration — even if a frozen task calls [`mwdg_feed`] between
309/// `mwdg_check` and this function.  If `mwdg_check` has not yet detected
310/// an expiration the function returns `0` immediately.
311///
312/// # Usage (C)
313/// ```c
314/// if (mwdg_check() != 0) {
315///     struct mwdg_node *cursor = NULL;
316///     uint32_t id;
317///     while (mwdg_get_next_expired(&cursor, &id)) {
318///         printf("expired watchdog id: %u\n", id);
319///     }
320/// }
321/// ```
322///
323/// # Parameters
324/// - `cursor`: pointer to a `*mut mwdg_node` that tracks iteration state.
325///   The caller must initialise `*cursor` to `NULL` before the first call.
326///   The function advances `*cursor` to the found node on success.
327/// - `out_id`: pointer to a `u32` where the expired node's identifier
328///   (set via [`mwdg_assign_id`]) will be written on success.
329///
330/// # Returns
331/// - `1` if an expired node was found (`*out_id` is written, `*cursor` is
332///   advanced).
333/// - `0` when no more expired nodes remain (iteration complete), when
334///   [`mwdg_check`] has not detected an expiration, or if `cursor` or
335///   `out_id` is null.
336///
337/// # Note
338/// Each call enters and exits the critical section independently. If the
339/// list is modified between calls the iterator may skip or revisit nodes.
340/// In typical RTOS usage the check loop runs from a single supervisory task,
341/// so this is not a concern.
342///
343/// # Safety
344/// - `cursor` must be either null or a valid pointer to a `*mut mwdg_node`.
345/// - `out_id` must be either null or a valid pointer to a `u32`.
346/// - `mwdg_init` must have been called.
347/// - All registered `mwdg_node` pointers must still be valid.
348#[unsafe(no_mangle)]
349pub unsafe extern "C" fn mwdg_get_next_expired(
350    cursor: *mut *mut mwdg_node,
351    out_id: *mut u32,
352) -> i32 {
353    if cursor.is_null() || out_id.is_null() {
354        return 0;
355    }
356
357    with_critical_section(|registry| {
358        // Convert the C cursor (*mut *mut mwdg_node) to our internal cursor
359        // (*const WatchdogNode).
360        let mut internal_cursor: *const WatchdogNode = if unsafe { (*cursor).is_null() } {
361            ptr::null()
362        } else {
363            unsafe { cast_node(*cursor).cast_const() }
364        };
365
366        match registry.next_expired(&mut internal_cursor) {
367            Some(id) => {
368                unsafe {
369                    *out_id = id;
370                    // Cast back: internal_cursor points to a WatchdogNode
371                    // which is layout-compatible with mwdg_node.
372                    *cursor = internal_cursor.cast_mut().cast::<mwdg_node>();
373                }
374                1
375            }
376            None => 0,
377        }
378    })
379}