Skip to main content

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}