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}