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}