Skip to main content

nros_node/executor/
node_wake.rs

1//! Phase 130.3 / 130.5 — `NodeWake`: heap-backed wake primitive
2//! used by `Executor::spin_once` on builds where the
3//! `nros_platform_wake_*` ABI is wired (Phase 130.1 + 130.5).
4//!
5//! Originally added for Zephyr+std because Zephyr's libc
6//! `pthread_cond_timedwait` hangs past its deadline (Phase
7//! 127.C.4). Now also active on FreeRTOS, NuttX, and ThreadX
8//! when those platforms are linked — each owns a kernel-native
9//! binary semaphore (`k_sem` / `xSemaphoreBinary` / `sem_t` /
10//! `tx_semaphore`) that honors its timeout. POSIX-only hosts
11//! still use the existing `std::sync::Condvar` path.
12//!
13//! Construction is fallible — if the platform provider hasn't
14//! linked a wake primitive (`wake_storage_size() == 0`) or
15//! `wake_init` returns non-zero, the caller falls back to
16//! driving the transport for the full timeout (matches the
17//! Phase 127.C.4 expedient gate behaviour but without skipping
18//! reliable RTOS stream retransmission).
19//!
20//! Phase 141.A.2 — cfg relaxed from `std` to `alloc` so the same
21//! `NodeWake` type works for the embedded no_std FreeRTOS wake-cb
22//! path (target for the Cortex-M3 P99 acceptance). The
23//! `Box`/`Vec`/`Arc` types switch to the `alloc` crate; the
24//! kernel-side semantics are unchanged.
25
26// Phase 248 (C2) — platform-agnostic: no `platform-*` feature gate.
27// `NodeWake` calls the `nros_platform_wake_*` C ABI (the platform
28// vtable) generically; availability is decided at runtime by the
29// `wake_storage_size() == 0` probe in [`NodeWake::new`], not by a
30// compile-time per-RTOS cfg. Compiled for any `alloc + rmw-cffi` build;
31// platforms without a wake primitive simply report size 0 and the
32// caller falls back to driving the transport for the full timeout.
33#![cfg(all(feature = "alloc", feature = "rmw-cffi"))]
34// Phase 141.A.2 — `NodeWake` is callable from the std-gated
35// `install_wake_signal_on_*` path today; the matching no_std
36// caller for the FreeRTOS-embedded wake-cb path is the
37// follow-on 141.A.3 work. Until that lands, the no_std build of
38// this module has no consumer — `#[allow(dead_code)]` keeps the
39// type compilable so 141.A.3 only has to add the caller, not
40// re-introduce the type.
41#![cfg_attr(not(feature = "alloc"), allow(dead_code))]
42
43use core::ffi::c_void;
44
45unsafe extern "C" {
46    fn nros_platform_wake_init(w: *mut c_void) -> i8;
47    fn nros_platform_wake_drop(w: *mut c_void) -> i8;
48    fn nros_platform_wake_wait_ms(w: *mut c_void, timeout_ms: u32) -> i8;
49    fn nros_platform_wake_signal(w: *mut c_void) -> i8;
50    #[allow(dead_code)] // Wired by 124.B.7 ISR callers in a follow-up.
51    fn nros_platform_wake_signal_from_isr(w: *mut c_void) -> i8;
52    fn nros_platform_wake_park_handles_us(
53        w: *mut c_void,
54        handles: *const i32,
55        n: usize,
56        deadline_us: u64,
57    ) -> i8;
58    fn nros_platform_wake_storage_size() -> usize;
59    fn nros_platform_wake_storage_align() -> usize;
60}
61
62/// Heap-backed wake primitive. Sized at runtime from the platform
63/// probe (`nros_platform_wake_storage_size`).
64pub(crate) struct NodeWake {
65    storage: alloc::boxed::Box<[u8]>,
66    /// Issue 2225 (RFC-0113 D9) -- a framework scheduler's wake, called after
67    /// every [`signal`](Self::signal). The other half of the yielding park
68    /// slot: a PX4 tier is a work item that never waits on this object, so an
69    /// arrival that only signals it would be dispatched at the tier's next
70    /// deadline. `hook_fn` is the `WakeHookFn` as a `usize`, 0 when unset;
71    /// `hook_ctx` is stored before it and cleared after it.
72    hook_fn: portable_atomic::AtomicUsize,
73    hook_ctx: portable_atomic::AtomicPtr<c_void>,
74}
75
76/// Issue 2225 -- the hook [`NodeWake::set_hook`] installs. Called from the
77/// signalling context (a transport thread, a publisher's context, an ISR
78/// forward), so it must be thread-safe, non-blocking and O(1).
79pub(crate) type WakeHookFn = unsafe extern "C" fn(ctx: *mut c_void);
80
81// SAFETY: per `<nros/platform.h>`'s wake contract, signal/wait are
82// callable from any thread (k_sem on Zephyr is kernel-managed).
83unsafe impl Send for NodeWake {}
84unsafe impl Sync for NodeWake {}
85
86impl NodeWake {
87    /// Allocate + init. Returns `None` if the platform provider
88    /// reports the primitive unavailable (`nros_platform_wake_storage_size() == 0`)
89    /// or if `wake_init` returns non-zero.
90    pub(crate) fn new() -> Option<Self> {
91        // SAFETY: probe functions are documented pure (no global
92        // state, may be called before init).
93        let size = unsafe { nros_platform_wake_storage_size() };
94        let align = unsafe { nros_platform_wake_storage_align() };
95        if size == 0 {
96            return None;
97        }
98        // Round capacity up so the boxed slice's data ptr is
99        // aligned to the requested boundary. `Box::<[u8]>` only
100        // guarantees byte alignment; layout-allocate via Vec
101        // capacity + a manual alignment check, or fall back to
102        // `vec![0; size + align]` and offset. Simplest: use a
103        // `Vec<u64>` for >=8B alignment (covers every platform we
104        // ship today; `k_sem` and `sem_t` are <= 8B-aligned).
105        if align > core::mem::align_of::<u64>() {
106            return None;
107        }
108        let u64s = size.div_ceil(8);
109        let boxed: alloc::boxed::Box<[u64]> = alloc::vec![0u64; u64s].into_boxed_slice();
110        // Reinterpret the boxed [u64] as a boxed [u8]. The
111        // capacity in bytes is `u64s * 8 >= size`; the data
112        // pointer inherits 8-byte alignment from `Vec<u64>`'s
113        // allocator request.
114        // Take the data pointer as a THIN `*mut u8` (not `as *mut [u8]`, which
115        // would keep the `u64s` element count for a u8 slice — wrong length, and
116        // trips `clippy::cast_slice_different_sizes`). The fat slice is rebuilt
117        // below with the correct byte length.
118        let raw = alloc::boxed::Box::into_raw(boxed) as *mut u8;
119        // SAFETY: `raw` came from `Box::<[u64]>::into_raw`; we
120        // re-box as `Box<[u8]>` with the same total byte length.
121        // The allocator only cares about the total size + the
122        // pointer originally returned, both preserved.
123        let storage: alloc::boxed::Box<[u8]> = unsafe {
124            alloc::boxed::Box::from_raw(core::ptr::slice_from_raw_parts_mut(raw, u64s * 8))
125        };
126        let ptr = storage.as_ptr() as *mut c_void;
127        // SAFETY: `ptr` references at least `size` bytes aligned
128        // to 8 (verified above). `wake_init` initialises in place
129        // or returns non-zero — in which case we drop `storage`
130        // without calling `wake_drop`, matching the "init failed,
131        // no teardown" contract.
132        let rc = unsafe { nros_platform_wake_init(ptr) };
133        if rc != 0 {
134            return None;
135        }
136        Some(Self {
137            storage,
138            hook_fn: portable_atomic::AtomicUsize::new(0),
139            hook_ctx: portable_atomic::AtomicPtr::new(core::ptr::null_mut()),
140        })
141    }
142
143    /// Block until signaled or `timeout_ms` elapses. Returns
144    /// `true` on signal, `false` on timeout or error.
145    /// The raw primitive a PORT must wait on if it installs its own park.
146    ///
147    /// phase-436 W7 — a park primitive that waits on its own fresh object
148    /// cannot be broken by the backend's listener, because the listener
149    /// signals THIS one. Installing such a park makes latency worse, not
150    /// better: `spin_once` drops the transport drain to non-blocking once a
151    /// port has parked, so an unbreakable park sleeps out its whole deadline
152    /// with data already waiting.
153    ///
154    /// A port's `ParkUntilFn` therefore takes this pointer as its `ctx`.
155    pub(crate) fn raw_ptr(&self) -> *mut c_void {
156        self.storage.as_ptr() as *mut c_void
157    }
158
159    pub(crate) fn wait_ms(&self, timeout_ms: u32) -> bool {
160        let ptr = self.storage.as_ptr() as *mut c_void;
161        // SAFETY: ptr was init'd in `new`; the underlying
162        // primitive supports concurrent calls per platform spec.
163        unsafe { nros_platform_wake_wait_ms(ptr, timeout_ms) == 0 }
164    }
165
166    /// phase-521 W3 (RFC-0113 D3) — park on this object AND on the OS
167    /// `handles` (the sessions' `ReadinessSource::Waitable` descriptors) for
168    /// at most `deadline_us`.
169    ///
170    /// `Some(true)` when signalled or a handle became readable, `Some(false)`
171    /// when the deadline expired, `None` when the port cannot wait on handles —
172    /// the caller then keeps the wait it had.
173    pub(crate) fn park_handles_us(&self, handles: &[i32], deadline_us: u64) -> Option<bool> {
174        let ptr = self.storage.as_ptr() as *mut c_void;
175        // SAFETY: ptr was init'd in `new`; `handles` is a live slice for the
176        // call, and the port reads it only during the call.
177        match unsafe {
178            nros_platform_wake_park_handles_us(ptr, handles.as_ptr(), handles.len(), deadline_us)
179        } {
180            0 => Some(true),
181            1 => Some(false),
182            _ => None,
183        }
184    }
185
186    /// Call the hook if one is installed, then wake one waiter. Safe from
187    /// any thread.
188    ///
189    /// The hook goes FIRST: a framework that installs one (a PX4 work item)
190    /// has nothing waiting on this object, so its reschedule must not queue
191    /// behind the platform signal's syscall.
192    pub(crate) fn signal(&self) {
193        let f = self.hook_fn.load(portable_atomic::Ordering::Acquire);
194        if f != 0 {
195            let ctx = self.hook_ctx.load(portable_atomic::Ordering::Acquire);
196            // SAFETY: `f` was stored from a `WakeHookFn` by `set_hook`, and
197            // its caller keeps `ctx` alive until it clears the hook.
198            let hook: WakeHookFn = unsafe { core::mem::transmute::<usize, WakeHookFn>(f) };
199            unsafe { hook(ctx) };
200        }
201        let ptr = self.storage.as_ptr() as *mut c_void;
202        // SAFETY: see wait_ms.
203        let _ = unsafe { nros_platform_wake_signal(ptr) };
204    }
205
206    /// Issue 2225 -- install (`Some`) or clear (`None`) the hook
207    /// [`signal`](Self::signal) calls. `ctx` must outlive the hook: clear it
208    /// before `ctx` goes away.
209    pub(crate) fn set_hook(&self, hook: Option<WakeHookFn>, ctx: *mut c_void) {
210        match hook {
211            Some(f) => {
212                self.hook_ctx.store(ctx, portable_atomic::Ordering::Release);
213                self.hook_fn
214                    .store(f as usize, portable_atomic::Ordering::Release);
215            }
216            None => {
217                self.hook_fn.store(0, portable_atomic::Ordering::Release);
218                self.hook_ctx
219                    .store(core::ptr::null_mut(), portable_atomic::Ordering::Release);
220            }
221        }
222    }
223}
224
225impl Drop for NodeWake {
226    fn drop(&mut self) {
227        let ptr = self.storage.as_ptr() as *mut c_void;
228        // SAFETY: ptr was init'd in `new`; this is the matching
229        // teardown call. NodeWake is owned exclusively by the
230        // Executor (held inside Arc), so no in-flight wait can
231        // overlap Drop.
232        let _ = unsafe { nros_platform_wake_drop(ptr) };
233    }
234}
235
236/// phase-509 P9b item 4 (RFC-0108 "As built in P9b") -- a band worker's wake
237/// object over the platform's (`nros_platform_wake_*`): what a level runner
238/// gives each spawned level.
239impl super::scheduler::BandWaiter {
240    /// A waiter over a new platform wake object, leaked with it: one per
241    /// level, for the image's life. `None` when the platform links no wake
242    /// primitive; the level then polls.
243    pub fn platform() -> Option<&'static Self> {
244        unsafe extern "C" fn wake(ctx: *mut c_void) {
245            // SAFETY: `ctx` is the leaked `NodeWake` below.
246            unsafe { &*(ctx as *const NodeWake) }.signal();
247        }
248        unsafe extern "C" fn wait(ctx: *mut c_void, us: u64) -> i32 {
249            // The wake primitive's own resolution is the millisecond; a
250            // band's park rounds UP, so it never wakes before its timer.
251            let ms = us.div_ceil(1000).min(u64::from(u32::MAX)) as u32;
252            // SAFETY: as `wake`.
253            if unsafe { &*(ctx as *const NodeWake) }.wait_ms(ms) {
254                0
255            } else {
256                -1
257            }
258        }
259        let w: &'static NodeWake =
260            alloc::boxed::Box::leak(alloc::boxed::Box::new(NodeWake::new()?));
261        let ctx = w as *const NodeWake as *mut c_void;
262        // SAFETY: the wake object is leaked, so it outlives every executor;
263        // its signal and wait are callable from any thread (the platform's
264        // wake contract).
265        let waiter = unsafe { Self::new(wake, wait, ctx) };
266        Some(alloc::boxed::Box::leak(alloc::boxed::Box::new(waiter)))
267    }
268}