Skip to main content

nros_node/executor/
storage.rs

1//! phase-271 — per-entry [`Executor`](super::spin::Executor) storage (issue 0110).
2//!
3//! The executor's six sized arrays (callback table + arena + scheduling-context
4//! tables) used to be inline fields sized by build-time consts baked into
5//! `nros-node` — one size for every entry sharing a compiled `nros-node`. Here the
6//! ENTRY supplies its own storage, sized to its topology, so a fat native entry
7//! and a lean embedded entry in one workspace each get the right size with no
8//! workspace-global env.
9//!
10//! Per the "C/C++ is a thin wrapper of Rust" principle the PUBLIC API stays
11//! generic-free: the entry hands a raw, 8-aligned `&mut [MaybeUninit<u64>]` backing
12//! (sized via [`executor_storage_u64_len`]); `nros-node` carves it privately into
13//! the typed sub-slices ([`carve`]). The only `unsafe` is that carve, validated
14//! against the `#[repr(C)]` reference [`ExecutorStorage`] layout by unit test.
15
16use core::{
17    alloc::Layout,
18    mem::{MaybeUninit, align_of, size_of},
19};
20
21use super::{
22    arena::CallbackMeta,
23    sched_context::{SchedContext, SchedContextId, SporadicState},
24};
25
26#[cfg(feature = "alloc")]
27type SporadicAtomic = (
28    portable_atomic_util::Arc<super::sched_context::AtomicSporadicState>,
29    super::spin::OpaqueTimerHandle,
30);
31
32/// The typed reference layout the [`carve`] mirrors. `#[repr(C)]` so its field
33/// offsets are the deterministic declaration-order layout the const-fn below
34/// reproduces; a unit test asserts they agree. Only referenced by tests.
35#[cfg(test)]
36#[repr(C)]
37pub(crate) struct ExecutorStorage<const CBS: usize, const SC: usize, const ARENA: usize> {
38    arena: [MaybeUninit<u8>; ARENA],
39    entries: [Option<CallbackMeta>; CBS],
40    sched_contexts: [Option<SchedContext>; SC],
41    sched_context_bindings: [SchedContextId; CBS],
42    sporadic_states: [Option<SporadicState>; SC],
43    #[cfg(feature = "alloc")]
44    sporadic_atomic_states: [Option<SporadicAtomic>; SC],
45}
46
47/// The typed, mutable sub-slices an [`Executor`](super::spin::Executor) borrows
48/// from a carved backing. Element memory is initialised by [`carve`].
49pub(crate) struct ExecutorSlices<'s> {
50    pub(crate) arena: &'s mut [MaybeUninit<u8>],
51    pub(crate) entries: &'s mut [Option<CallbackMeta>],
52    pub(crate) sched_contexts: &'s mut [Option<SchedContext>],
53    pub(crate) sched_context_bindings: &'s mut [SchedContextId],
54    pub(crate) sporadic_states: &'s mut [Option<SporadicState>],
55    #[cfg(feature = "alloc")]
56    pub(crate) sporadic_atomic_states: &'s mut [Option<SporadicAtomic>],
57}
58
59/// Byte offsets of each field within the backing + total size/align. Computed
60/// identically by [`executor_storage_layout`] and [`carve`] (single source of
61/// truth), reproducing `#[repr(C)]` declaration-order layout.
62struct FieldOffsets {
63    arena: usize,
64    entries: usize,
65    sched_contexts: usize,
66    sched_context_bindings: usize,
67    sporadic_states: usize,
68    #[cfg(feature = "alloc")]
69    sporadic_atomic_states: usize,
70    size: usize,
71    align: usize,
72}
73
74const fn align_up(off: usize, align: usize) -> usize {
75    off.div_ceil(align) * align
76}
77
78const fn compute_offsets(cbs: usize, sc: usize, arena: usize) -> FieldOffsets {
79    let mut off = 0usize;
80    let mut max_align = 1usize;
81
82    // arena: [MaybeUninit<u8>; arena] — align 1, at offset 0.
83    let arena_off = 0usize;
84    off += arena;
85
86    macro_rules! place {
87        ($n:expr, $ty:ty) => {{
88            let a = align_of::<$ty>();
89            if a > max_align {
90                max_align = a;
91            }
92            off = align_up(off, a);
93            let at = off;
94            off += $n * size_of::<$ty>();
95            at
96        }};
97    }
98
99    let entries = place!(cbs, Option<CallbackMeta>);
100    let sched_contexts = place!(sc, Option<SchedContext>);
101    let sched_context_bindings = place!(cbs, SchedContextId);
102    let sporadic_states = place!(sc, Option<SporadicState>);
103    #[cfg(feature = "alloc")]
104    let sporadic_atomic_states = place!(sc, Option<SporadicAtomic>);
105
106    let size = align_up(off, max_align);
107    FieldOffsets {
108        arena: arena_off,
109        entries,
110        sched_contexts,
111        sched_context_bindings,
112        sporadic_states,
113        #[cfg(feature = "alloc")]
114        sporadic_atomic_states,
115        size,
116        align: max_align,
117    }
118}
119
120/// Byte [`Layout`] of the backing needed for a `(cbs, sc, arena)`-sized executor.
121/// Public + non-generic so the macro / FFI can size a raw backing.
122pub const fn executor_storage_layout(cbs: usize, sc: usize, arena: usize) -> Layout {
123    let o = compute_offsets(cbs, sc, arena);
124    // SAFETY: `align` is a power of two (a `max` of `align_of` results) and `size`
125    // is rounded up to it; both are non-zero.
126    unsafe { Layout::from_size_align_unchecked(o.size, o.align) }
127}
128
129/// Number of `u64` words a backing must hold for a `(cbs, sc, arena)`-sized
130/// executor. `u64` backing is 8-aligned, which covers every field (all
131/// `align_of ≤ 8`; asserted in tests), so the entry never hand-aligns. The macro
132/// emits `[MaybeUninit<u64>; executor_storage_u64_len(N, SC, A)]`.
133pub const fn executor_storage_u64_len(cbs: usize, sc: usize, arena: usize) -> usize {
134    executor_storage_layout(cbs, sc, arena).size().div_ceil(8)
135}
136
137/// Per-entry executor sizing — the entity counts an [`Executor`](super::spin::Executor)
138/// is built to hold. **Public + non-generic** (the "C/C++ is a thin wrapper"
139/// principle): the entry / macro / FFI supplies these as plain `usize`s rather
140/// than as type/const generics C can't name. Used to size + carve the backing.
141///
142/// `cbs` is capped at 64 by the executor's `u64` ready-set bitmask (asserted in
143/// [`carve`]-time / `open_in`).
144#[derive(Clone, Copy)]
145pub struct ExecutorSizing {
146    /// Callback-table slots (`entries` + per-entry SC bindings). ≤ 64.
147    pub cbs: usize,
148    /// Scheduling-context slots (`sched_contexts` + sporadic state tables).
149    pub sc: usize,
150    /// Bump-allocator arena size in bytes.
151    pub arena: usize,
152}
153
154impl ExecutorSizing {
155    /// The build-time default (`MAX_CBS`/`MAX_SC`/`ARENA_SIZE` consts) — the
156    /// backward-compatible size the `alloc` convenience constructors leak.
157    pub const DEFAULT: Self = Self {
158        cbs: crate::config::MAX_CBS,
159        sc: crate::config::MAX_SC,
160        arena: crate::config::ARENA_SIZE,
161    };
162
163    /// `u64` words a backing must hold for this sizing (see
164    /// [`executor_storage_u64_len`]).
165    pub const fn u64_len(&self) -> usize {
166        executor_storage_u64_len(self.cbs, self.sc, self.arena)
167    }
168}
169
170/// The exact `#[repr(C)]` byte layout the C/C++ FFI's inline executor buffer must
171/// hold: an [`Executor`](super::spin::Executor)`<'static>` header immediately
172/// followed by a default-sized ([`ExecutorSizing::DEFAULT`]) storage backing.
173///
174/// The FFI keeps the executor inline (heap-free — matching the Rust no-alloc
175/// requirement) and carves its per-entry tables from the SAME buffer's
176/// [`backing`](Self::backing) tail. Because that buffer is **pinned** — the C
177/// caller allocates it, it is initialised in place, and it is only ever reached
178/// through a stable `nros_executor_t*` (never moved after init) — the resulting
179/// self-borrow (the header's slices pointing into the same struct's tail) is
180/// sound. The FFI probes `size_of` of THIS type (not bare `Executor`) to size
181/// its `_opaque` array, and reinterprets `_opaque` as `*mut ExecutorInlineStorage`
182/// (the executor stays at offset 0, so existing offset-0 accessors are unchanged).
183#[repr(C)]
184pub struct ExecutorInlineStorage {
185    /// The executor, written in place (offset 0) by `from_session_ptr_in`.
186    pub exec: MaybeUninit<super::spin::Executor<'static>>,
187    /// The carved backing the executor's slices borrow (the buffer's tail).
188    pub backing: [MaybeUninit<u64>; ExecutorSizing::DEFAULT.u64_len()],
189}
190
191/// Carve an 8-aligned `u64` backing into the typed, initialised executor slices.
192///
193/// # Safety
194/// - `backing.len() * 8` must be ≥ `executor_storage_layout(cbs, sc, arena).size()`.
195/// - The returned slices alias `backing`; it must outlive them (the `'s` bound)
196///   and not be otherwise accessed while they live.
197///
198/// Element memory is initialised here (`entries`/SC tables → `None`, bindings →
199/// `SchedContextId(0)`), so the returned `&mut [T]` reference validly-init memory.
200pub(crate) unsafe fn carve<'s>(
201    backing: &'s mut [MaybeUninit<u64>],
202    cbs: usize,
203    sc: usize,
204    arena: usize,
205) -> ExecutorSlices<'s> {
206    let o = compute_offsets(cbs, sc, arena);
207    // Fail-loud on EVERY profile (not `debug_assert!`): embedded release builds
208    // strip debug-assertions, and a backing that is too small is silent memory
209    // corruption — the carved `entries`/`sched_contexts` tables run past the end
210    // of `backing` into whatever .bss follows (e.g. a C carrier's `__nros_c_inst`),
211    // leaving a NULL `drop_fn` that faults in `Executor::drop`. This is exactly
212    // how a STALE config-header mirror (C buffer sized from an out-of-date
213    // `NROS_*_STORAGE_SIZE`) manifested as a `jalr -> 0` on threadx-riscv64 (#131).
214    // Panic here instead, at open, with the two sizes named.
215    assert!(
216        backing.len() * 8 >= o.size,
217        "executor backing too small: {} bytes < {} required — the storage buffer \
218         (NROS_*_STORAGE_SIZE) disagrees with the executor layout; rebuild clean so \
219         the generated config header matches",
220        backing.len() * 8,
221        o.size
222    );
223    let base = backing.as_mut_ptr() as *mut u8;
224
225    unsafe {
226        // arena — no init needed (MaybeUninit).
227        let arena_s =
228            core::slice::from_raw_parts_mut(base.add(o.arena) as *mut MaybeUninit<u8>, arena);
229
230        let entries_p = base.add(o.entries) as *mut Option<CallbackMeta>;
231        let mut i = 0;
232        while i < cbs {
233            entries_p.add(i).write(None);
234            i += 1;
235        }
236        let entries_s = core::slice::from_raw_parts_mut(entries_p, cbs);
237
238        let sc_p = base.add(o.sched_contexts) as *mut Option<SchedContext>;
239        let mut i = 0;
240        while i < sc {
241            sc_p.add(i).write(None);
242            i += 1;
243        }
244        let sched_contexts_s = core::slice::from_raw_parts_mut(sc_p, sc);
245
246        let bind_p = base.add(o.sched_context_bindings) as *mut SchedContextId;
247        let mut i = 0;
248        while i < cbs {
249            bind_p.add(i).write(SchedContextId(0));
250            i += 1;
251        }
252        let bindings_s = core::slice::from_raw_parts_mut(bind_p, cbs);
253
254        let sp_p = base.add(o.sporadic_states) as *mut Option<SporadicState>;
255        let mut i = 0;
256        while i < sc {
257            sp_p.add(i).write(None);
258            i += 1;
259        }
260        let sporadic_s = core::slice::from_raw_parts_mut(sp_p, sc);
261
262        #[cfg(feature = "alloc")]
263        let atomic_s = {
264            let ap = base.add(o.sporadic_atomic_states) as *mut Option<SporadicAtomic>;
265            let mut i = 0;
266            while i < sc {
267                ap.add(i).write(None);
268                i += 1;
269            }
270            core::slice::from_raw_parts_mut(ap, sc)
271        };
272
273        ExecutorSlices {
274            arena: arena_s,
275            entries: entries_s,
276            sched_contexts: sched_contexts_s,
277            sched_context_bindings: bindings_s,
278            sporadic_states: sporadic_s,
279            #[cfg(feature = "alloc")]
280            sporadic_atomic_states: atomic_s,
281        }
282    }
283}
284
285#[cfg(test)]
286mod tests {
287    use super::*;
288
289    const CBS: usize = crate::config::MAX_CBS;
290    const SC: usize = crate::config::MAX_SC;
291    const ARENA: usize = crate::config::ARENA_SIZE;
292
293    #[test]
294    fn layout_matches_typed_repr_c() {
295        // The manual const-fn layout must equal the compiler's `#[repr(C)]` layout
296        // of the typed storage — proof the carve offsets are the real field offsets.
297        let got = executor_storage_layout(CBS, SC, ARENA);
298        let want = Layout::new::<ExecutorStorage<CBS, SC, ARENA>>();
299        assert_eq!(got.size(), want.size(), "size");
300        assert_eq!(got.align(), want.align(), "align");
301    }
302
303    #[test]
304    fn u64_backing_covers_all_field_aligns() {
305        assert!(align_of::<Option<CallbackMeta>>() <= 8);
306        assert!(align_of::<Option<SchedContext>>() <= 8);
307        assert!(align_of::<SchedContextId>() <= 8);
308        assert!(align_of::<Option<SporadicState>>() <= 8);
309        assert!(executor_storage_layout(CBS, SC, ARENA).align() <= 8);
310    }
311
312    #[test]
313    fn carve_yields_right_lengths_and_inits() {
314        // Heap-allocate: the default test config (MAX_CBS/MAX_SC/ARENA_SIZE from
315        // build.rs) makes this backing array tens of KB, well past
316        // `clippy::large_stack_arrays`'s threshold — and the size here is
317        // incidental (mirrors production config), not the point under test, so
318        // boxing is the right fix rather than an allow.
319        let mut backing =
320            alloc::vec![const { MaybeUninit::<u64>::uninit() }; executor_storage_u64_len(CBS, SC, ARENA)]
321                .into_boxed_slice();
322        let s = unsafe { carve(&mut backing, CBS, SC, ARENA) };
323        assert_eq!(s.arena.len(), ARENA);
324        assert_eq!(s.entries.len(), CBS);
325        assert_eq!(s.sched_contexts.len(), SC);
326        assert_eq!(s.sched_context_bindings.len(), CBS);
327        assert_eq!(s.sporadic_states.len(), SC);
328        assert!(s.entries.iter().all(|e| e.is_none()));
329        assert!(s.sched_context_bindings.iter().all(|b| b.0 == 0));
330    }
331}