nros/levels.rs
1//! phase-509 P10 (RFC-0108 "As built in P9b") -- the Rust tier runners' LEVEL
2//! core: one executor per core, a worker per level over it.
3//!
4//! The C-ABI runners reach this through `nros-cpp`'s scheduler FFI
5//! (`nros_cpp_level_open`, `nros_cpp_level_spin_once`); a Rust runner holds
6//! the core's `ExecutorNodeRuntime` itself and uses the same three steps:
7//!
8//! 1. `LevelCore::new` -- the boot level's runtime, leaked for the image's
9//! life, with the port's recursive priority-inheriting mutex and the
10//! calling thread's identity installed as the executor's lock
11//! ([`nros_node::executor::exec_lock::ExecLock`]) when more than one level
12//! runs, the level count stated, and the boot level made current.
13//! 2. `LevelCore::open_level` + `LevelCore::with_runtime` -- on a level's
14//! own thread: its worker record (leaked with the level), its waiter
15//! linked into the executor's arrival routing, then the image's setup run
16//! on the shared runtime with the level's groups active and the level
17//! registering, all under the lock.
18//! 3. `LevelCore::spin_level_once` / `LevelCore::spin_boot_once` -- one
19//! pass of a level's worker (its band, the lock released around each
20//! callback, then the ticks of the components its level registered) or
21//! of the boot level (the executor's own worker: transport, executor-wide
22//! chores, the boot level's component ticks). Issue 2020: a component
23//! ticks on the level that registered it, never on two.
24//!
25//! 4. `LevelCore::into_runtime` -- issue 2016: once every other level's
26//! thread has stopped and been joined, the boot level takes the runtime
27//! back (the lock removed), so a hosted runner can close its session and
28//! return. An RTOS runner never calls it: its levels spin for the image's
29//! life.
30//!
31//! Every access goes through the raw runtime pointer with the lock taken
32//! first, so no reference to the runtime or its executor is live across
33//! another thread's pass.
34
35use core::{ffi::c_void, time::Duration};
36
37use nros_node::executor::{
38 Band, BandWaiter, BandWorker,
39 exec_lock::{ExecGuard, ExecLock, ExecLockFn, ThreadIdFn},
40};
41
42use crate::{Executor, node_runtime::ExecutorNodeRuntime};
43
44/// The core's runtime, shared by every level of the image. `Copy`: each
45/// level's thread carries the handle.
46#[derive(Clone, Copy)]
47pub struct LevelCore {
48 rt: *mut ExecutorNodeRuntime,
49}
50
51// SAFETY: every access takes the executor's lock first (module docs).
52unsafe impl Send for LevelCore {}
53unsafe impl Sync for LevelCore {}
54
55/// Why a level could not be set up.
56#[derive(Debug, Clone, Copy, PartialEq, Eq)]
57pub enum LevelError {
58 /// More levels than `MAX_LEVELS`, or a level past the count.
59 BadLevel,
60 /// The port's mutex was NULL or would not initialise.
61 NoLock,
62}
63
64impl LevelCore {
65 /// Make `runtime` the core's level runtime (leaked for the image's life).
66 ///
67 /// `n_levels` is the image's level count and `boot_level` the level of
68 /// the calling thread. With more than one level, `mutex` must be an
69 /// initialised recursive priority-inheriting mutex that `lock` / `unlock`
70 /// take and give and `thread_id` must name the calling thread: the
71 /// executor carries them so every pass releases the lock around its
72 /// callbacks and the levels preempt each other on one executor.
73 ///
74 /// # Safety
75 /// Called before any second thread can reach the runtime. `mutex` lives
76 /// for the image's life; `thread_id` is equal on one thread and distinct
77 /// across live threads.
78 pub unsafe fn new(
79 runtime: ExecutorNodeRuntime,
80 n_levels: u8,
81 boot_level: u8,
82 mutex: *mut c_void,
83 lock: ExecLockFn,
84 unlock: ExecLockFn,
85 thread_id: ThreadIdFn,
86 ) -> Result<Self, LevelError> {
87 if usize::from(n_levels) > crate::MAX_LEVELS || boot_level >= n_levels.max(1) {
88 return Err(LevelError::BadLevel);
89 }
90 let rt: &'static mut ExecutorNodeRuntime =
91 alloc::boxed::Box::leak(alloc::boxed::Box::new(runtime));
92 if !crate::set_level_count(n_levels.max(1)) {
93 return Err(LevelError::BadLevel);
94 }
95 let exec = rt.executor_mut();
96 if n_levels > 1 {
97 // SAFETY: the caller's contract on `mutex` and `thread_id`.
98 let Some(l) = (unsafe { ExecLock::new(mutex, lock, unlock) }) else {
99 return Err(LevelError::NoLock);
100 };
101 exec.set_exec_lock(Some(unsafe { l.with_thread_id(thread_id) }));
102 }
103 if !exec.set_level(boot_level) {
104 return Err(LevelError::BadLevel);
105 }
106 exec.set_registering_level(boot_level);
107 Ok(Self { rt })
108 }
109
110 /// The core's executor, through the raw pointer (no reference made).
111 fn exec(&self) -> *mut Executor<'static> {
112 // SAFETY: `rt` is the leaked runtime; only the field address is taken.
113 unsafe { ExecutorNodeRuntime::executor_ptr(self.rt) }
114 }
115
116 /// Run `f` on the core's runtime with the lock held.
117 ///
118 /// # Safety
119 /// No reference to the runtime obtained here outlives `f`.
120 pub unsafe fn with_runtime<R>(&self, f: impl FnOnce(&mut ExecutorNodeRuntime) -> R) -> R {
121 // SAFETY: the executor is live; the lock is taken before the reference.
122 let _g = unsafe { ExecGuard::acquire(self.exec()) };
123 // SAFETY: under the lock no other thread touches the runtime (the
124 // callback windows of a pass touch only the executor's own state).
125 f(unsafe { &mut *self.rt })
126 }
127
128 /// Open level `level` on the calling thread: its worker (leaked with the
129 /// level) attached to the executor's arrival routing and the level made
130 /// the registering one. Returns the worker the thread's passes use.
131 ///
132 /// # Safety
133 /// Called once per level, on the level's own thread.
134 pub unsafe fn open_level(&self, level: u8) -> Result<&'static mut BandWorker, LevelError> {
135 if usize::from(level) >= crate::MAX_LEVELS {
136 return Err(LevelError::BadLevel);
137 }
138 let worker = match BandWaiter::platform() {
139 Some(w) => BandWorker::new(level, Band::ALL).with_waiter(w),
140 None => BandWorker::new(level, Band::ALL),
141 };
142 let worker: &'static mut BandWorker =
143 alloc::boxed::Box::leak(alloc::boxed::Box::new(worker));
144 // SAFETY: as `with_runtime`.
145 unsafe {
146 self.with_runtime(|rt| {
147 let exec = rt.executor_mut();
148 exec.attach_band_worker(worker);
149 exec.set_registering_level(level);
150 })
151 };
152 Ok(worker)
153 }
154
155 /// One pass of a spawned level's worker: it parks (its waiter, the lock
156 /// released) for at most `timeout` or until its band's next timer or an
157 /// arrival, then runs its band's ready callbacks and (issue 2020) the
158 /// ticks of the components its level registered -- the executor's
159 /// enrolled ones inside the band pass, the runtime's own after it, the
160 /// lock held. Returns the pass's counts.
161 ///
162 /// # Safety
163 /// `worker` is the calling thread's, from [`Self::open_level`].
164 pub unsafe fn spin_level_once(
165 &self,
166 worker: &mut BandWorker,
167 timeout: Duration,
168 ) -> nros_node::SpinOnceResult {
169 let exec = self.exec();
170 // SAFETY: the lock is held once; no reference to the executor lives.
171 let _g = unsafe { ExecGuard::acquire(exec) };
172 let result = unsafe { Executor::spin_band_once(exec, worker, timeout) };
173 // SAFETY: under the lock no other thread touches the runtime.
174 unsafe { (*self.rt).run_ticks_for(Some(worker.level())) };
175 result
176 }
177
178 /// Issue 2016 -- the teardown: take the runtime back from the levels,
179 /// the executor's lock removed, for the caller to close its session
180 /// (`Executor::close`) and drop it. The level workers stay leaked, as
181 /// they were for the image's life, and the level count stays stated.
182 ///
183 /// # Safety
184 /// Called once, on the boot level's thread, after every other thread
185 /// that reached the runtime (each level's worker, any callback it ran)
186 /// has stopped and been joined; no copy of this `LevelCore` is used
187 /// afterwards.
188 pub unsafe fn into_runtime(self) -> ExecutorNodeRuntime {
189 // SAFETY: `rt` came from `Box::leak` in `new`, and the caller's
190 // contract makes this the only live handle to it.
191 let mut rt = unsafe { alloc::boxed::Box::from_raw(self.rt) };
192 rt.executor_mut().set_exec_lock(None);
193 *rt
194 }
195
196 /// phase-521 W6 (RFC-0113 D2) -- the budget a level runner passes to
197 /// [`Self::spin_level_once`] / [`Self::spin_boot_once`]: the runtime's
198 /// [`ExecutorNodeRuntime::entry_spin_budget`]. Each park already ends at
199 /// the level's next deadline or on an arrival, so the budget is not a
200 /// cadence; passing the tier's period instead woke every idle level once a
201 /// period. Ask it after the levels' setup, which is what registers the
202 /// components whose ticks keep the short budget.
203 ///
204 /// # Safety
205 /// As [`Self::with_runtime`].
206 pub unsafe fn entry_spin_budget(&self) -> Duration {
207 // SAFETY: the caller's contract.
208 unsafe { self.with_runtime(|rt| rt.entry_spin_budget()) }
209 }
210
211 /// phase-521 W6 -- wake every level's worker and the boot level, for a
212 /// runner that is stopping them: a level parked on the long budget would
213 /// otherwise see its stop flag only at its next deadline. Lock-free.
214 pub fn wake_all(&self) {
215 // SAFETY: the runtime is live for the image's life (leaked in `new`);
216 // `wake_all_workers` reads the executor's wake context, written at
217 // open and never replaced, and signals lock-free.
218 unsafe { (*self.exec()).wake_all_workers() }
219 }
220
221 /// One pass of the boot level: the executor's own worker (transport,
222 /// executor-wide chores), then the boot level's components' ticks, the
223 /// lock held except around each callback.
224 ///
225 /// # Safety
226 /// Called from the boot level's thread only.
227 pub unsafe fn spin_boot_once(&self, timeout: Duration) -> nros_node::SpinOnceResult {
228 // SAFETY: as `spin_level_once`; the ticks run under the same guard.
229 unsafe { ExecutorNodeRuntime::spin_once_shared(self.rt, timeout) }
230 }
231}