Skip to main content

nros_platform_api/
task.rs

1//! phase-359 W10 — one place that spawns a platform task from Rust.
2//!
3//! Moved here from `nros-node::executor::platform_task`: three Rust callers now
4//! need it (`nros-node`'s worker pool and `open_threaded`, `nros-cpp`'s native
5//! tier runtime), and a helper for an ABI belongs beside the ABI rather than
6//! inside one of its consumers.
7//!
8//! Two executor-owned workers need a thread: the per-OS-priority pool
9//! (`os_priority` in `nros-node`'s executor) and the signalfd forwarder in `spin.rs`. Both were
10//! `std::thread`; both are platform tasks now, so the allocate-spawn-join
11//! sequence lives here rather than being written twice.
12//!
13//! The storage size is ASKED FOR (`nros_platform_task_storage_{size,align}`),
14//! never assumed. A hard-coded size is issue 0570 exactly: a Rust-side
15//! `pthread_attr_t` mirror was 36 bytes shorter than NuttX's, and
16//! `pthread_attr_init` wrote the difference into the caller's frame. The probes
17//! exist so no caller has to guess.
18
19// Both consumers are feature-selected AND one is `target_os = "linux"`, so
20// there are reachable combinations (e.g. `signal-fd-wake` off-Linux with no
21// `scheduler-os-priority`) where this module compiles with no caller. Enumerating
22// them in the `mod` gate would be a predicate nobody can keep correct; saying so
23// once here is the cheaper truth.
24#![allow(dead_code)]
25
26use core::ffi::c_void;
27
28/// The ABI's task attributes, mirrored for the `extern` declaration below.
29///
30/// phase-364 W3 — this is `nros_platform_task_attr_t` from `<nros/platform.h>`.
31/// It is declared here rather than imported because `nros-node` does not depend
32/// on `nros-platform-cffi` (where the generated bindings live) — the same reason
33/// the wake and task symbols are declared here by hand. The layout is checked
34/// against the header by `check-ffi-struct-mirrors`; see the note at the spawn
35/// site for what a drift would cost.
36#[repr(C)]
37struct TaskAttr {
38    name: *const core::ffi::c_char,
39    stack_bytes: usize,
40    stack_mem: *mut c_void,
41    priority: i32,
42    core: i8,
43    flags: u8,
44}
45
46/// `INT32_MIN` — inherit the creating task's priority.
47const PRIORITY_INHERIT: i32 = i32::MIN;
48
49unsafe extern "C" {
50    fn nros_platform_task_init(
51        task: *mut c_void,
52        attr: *mut c_void,
53        entry: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
54        arg: *mut c_void,
55    ) -> i8;
56    fn nros_platform_task_join(task: *mut c_void) -> i8;
57    fn nros_platform_task_storage_size() -> usize;
58    fn nros_platform_task_storage_align() -> usize;
59}
60
61/// A spawned platform task plus the storage the platform tracks it in.
62///
63/// `alloc`-gated, and it is the ONLY thing in this module that is: it owns a
64/// heap block sized from the platform's own storage probes. The bare queries
65/// beside it (`stack_unused_bytes`) allocate nothing and stay available to a
66/// no-alloc image.
67///
68/// Joining is [`join`](Self::join) rather than `Drop`, because both callers
69/// have to signal their worker to stop BEFORE waiting for it — a `Drop` that
70/// joined implicitly would deadlock against a worker still blocked on its own
71/// wait.
72#[cfg(feature = "alloc")]
73pub struct PlatformTask {
74    ptr: *mut u8,
75    layout: core::alloc::Layout,
76}
77
78#[cfg(feature = "alloc")]
79impl PlatformTask {
80    /// Spawn `entry(arg)`, or `None` when this platform cannot host a task
81    /// (no storage sizing, allocation failure, or a refused spawn).
82    ///
83    /// # Safety
84    /// `arg` must remain valid and pointed-to until [`join`](Self::join)
85    /// returns — the task dereferences it.
86    pub unsafe fn spawn(
87        entry: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
88        arg: *mut c_void,
89        stack_bytes: usize,
90        name: *const core::ffi::c_char,
91    ) -> Option<Self> {
92        // SAFETY: forwarded unchanged; the caller's contract is unchanged.
93        unsafe { Self::spawn_with(entry, arg, name, stack_bytes, PRIORITY_INHERIT as i64) }
94    }
95
96    /// Spawn `entry(arg)` stating a PRIORITY as well.
97    ///
98    /// phase-359 W10 — `spawn` inherits the creating task's priority, which is
99    /// right for the executor's own workers and wrong for a TIER: a tier's
100    /// priority is the thing its author declared. `priority <= 0` means
101    /// "unstated" and inherits, matching how the board descriptors spell an
102    /// absent priority; anything positive is the kernel's own number and is
103    /// passed through the band's RAW escape hatch, because a
104    /// `[tiers.<name>.<rtos>] priority` is already in the kernel's units.
105    ///
106    /// # Safety
107    /// Same as [`spawn`](Self::spawn).
108    pub unsafe fn spawn_with(
109        entry: unsafe extern "C" fn(*mut c_void) -> *mut c_void,
110        arg: *mut c_void,
111        name: *const core::ffi::c_char,
112        stack_bytes: usize,
113        priority: i64,
114    ) -> Option<Self> {
115        /// `NROS_PLATFORM_PRIORITY_RAW(n)` from `<nros/platform.h>`.
116        const fn raw(n: i32) -> i32 {
117            -0x4000_0000 - n
118        }
119        let priority = i32::try_from(priority)
120            .ok()
121            .filter(|p| *p > 0)
122            .map_or(PRIORITY_INHERIT, raw);
123        // SAFETY: both probes are documented pure functions, callable before
124        // any task exists.
125        let (size, align) = unsafe {
126            (
127                nros_platform_task_storage_size(),
128                nros_platform_task_storage_align(),
129            )
130        };
131        if size == 0 || align == 0 {
132            return None;
133        }
134        let layout = core::alloc::Layout::from_size_align(size, align).ok()?;
135        // SAFETY: `layout` has non-zero size.
136        let ptr = unsafe { alloc::alloc::alloc(layout) };
137        if ptr.is_null() {
138            return None;
139        }
140        // phase-364 W3 — a real attribute, not `NULL`.
141        //
142        // Passing `NULL` was correct on four ports and a guaranteed failure on
143        // ThreadX, whose `task_init` required an attr carrying the stack. W3
144        // made `NULL` mean "every default" everywhere, so `NULL` would work now
145        // — but the executor's workers do have a stack size to state, and
146        // stating it is what phase-359 W7 had to write a bespoke C shim to do.
147        let mut attr = TaskAttr {
148            name,
149            stack_bytes,
150            stack_mem: core::ptr::null_mut(),
151            priority,
152            core: -1,
153            flags: 0,
154        };
155        // SAFETY: `ptr` is storage of the size/alignment the platform asked
156        // for; `attr` outlives the call (the port copies what it needs);
157        // `entry`/`arg` are the caller's contract.
158        let rc = unsafe {
159            nros_platform_task_init(
160                ptr as *mut c_void,
161                (&raw mut attr) as *mut c_void,
162                entry,
163                arg,
164            )
165        };
166        if rc != 0 {
167            // SAFETY: same pair just returned by `alloc`; no task took it.
168            unsafe { alloc::alloc::dealloc(ptr, layout) };
169            return None;
170        }
171        Some(Self { ptr, layout })
172    }
173
174    /// Block until the task exits, then release its storage.
175    ///
176    /// The caller must already have told the task to stop; this only waits.
177    pub fn join(self) {
178        // SAFETY: `ptr` holds the handle `task_init` wrote.
179        unsafe { nros_platform_task_join(self.ptr as *mut c_void) };
180        // SAFETY: the task has exited, so nothing else references the storage;
181        // `ptr`/`layout` are the pair `spawn` allocated.
182        unsafe { alloc::alloc::dealloc(self.ptr, self.layout) };
183        core::mem::forget(self);
184    }
185}
186
187#[cfg(feature = "alloc")]
188impl Drop for PlatformTask {
189    fn drop(&mut self) {
190        // Reached only if a caller dropped the handle without joining, which
191        // leaves a running task pointed at storage we are about to free. Leak
192        // the storage instead: a leak is recoverable, a use-after-free by a
193        // live task is not.
194        //
195        // `join` calls `mem::forget`, so the normal path never lands here.
196    }
197}