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}