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