Skip to main content

nros_executor_layout/
lib.rs

1//! The executor backing's placement arithmetic — issue 1197.
2//!
3//! # Why this is its own crate
4//!
5//! phase-392 W6 moved the executor's per-entry storage into a `.bss` static. On
6//! an RTOS the allocator reservation it used to come out of is itself fixed, so
7//! whatever sizes that reservation has to know how big the backing is.
8//!
9//! The size is `arena + tables`. The arena is plain arithmetic
10//! `nros-node/build.rs` already computes. The tables are `repr(Rust)` layout —
11//! field order, padding and niche packing are the compiler's to choose and to
12//! CHANGE between versions — so they can never be re-derived outside it. That is
13//! the sizes-header mirror class (0088 -> 0114 -> 0122 -> 0123 -> 0245 -> 0268)
14//! this tree has re-fixed six times.
15//!
16//! So the arithmetic takes the per-type facts as DATA. `nros-node` supplies what
17//! its compiler says while building for the target; a build script supplies
18//! units recovered for a target it is not itself compiling (the way `nros-c`
19//! recovers `EXECUTOR_OPAQUE_U64S` through `nros-sizes-build`). One
20//! implementation, two sources of the inputs.
21//!
22//! It is a separate crate rather than a `pub fn` in `nros-node` for one reason:
23//! a build script runs on the HOST, and a build-dependency on `nros-node` would
24//! give it a host-compiled `nros-node` — the wrong layout, silently. This crate
25//! is `no_std` and dependency-free so both sides can reach it.
26//!
27//! # What it does NOT decide
28//!
29//! The counts. `cbs`/`sc`/`nodes`/`arena` come from `nros-node/build.rs` and,
30//! on a synced leaf, from the derived env `nros sync` writes (issues 0827,
31//! 1061). This crate only places regions.
32
33#![no_std]
34#![forbid(unsafe_code)]
35
36/// Size and alignment of one region's element type.
37#[derive(Clone, Copy, Debug, PartialEq, Eq)]
38pub struct RegionUnit {
39    /// `size_of::<T>()`.
40    pub size: usize,
41    /// `align_of::<T>()`. Must be a power of two; zero is rejected as 1.
42    pub align: usize,
43}
44
45/// Every element type the backing carves a table of, in declaration order.
46///
47/// The arena is absent on purpose: it is `[MaybeUninit<u8>]`, size 1 align 1 by
48/// definition, and its LENGTH is a count rather than a unit.
49#[derive(Clone, Copy, Debug, PartialEq, Eq)]
50pub struct RegionUnits {
51    pub callback_meta: RegionUnit,
52    pub sched_context: RegionUnit,
53    pub sched_context_id: RegionUnit,
54    pub sporadic_state: RegionUnit,
55    /// Placed only when [`Counts::alloc`] is set.
56    pub sporadic_atomic: RegionUnit,
57    pub remap_rule: RegionUnit,
58    pub node_record: RegionUnit,
59    pub concrete_session: RegionUnit,
60    pub extra_session_id: RegionUnit,
61    pub node_sched_entry: RegionUnit,
62    pub dispatch_slot: RegionUnit,
63    pub component_slot: RegionUnit,
64    pub group_name: RegionUnit,
65    pub group_sched_entry: RegionUnit,
66    pub violation: RegionUnit,
67    /// Alive supervision's per-SchedContext record, one per `sc`.
68    pub alive_slot: RegionUnit,
69    /// phase-476 W0 — the per-callback-slot lifetime tag (generation + owning
70    /// node), one per `cbs`. Placed after every older region so each keeps
71    /// its offset.
72    pub slot_tag: RegionUnit,
73    /// phase-382 W3' — one parameter-store slot (`nros_params::ParameterSlot`),
74    /// one per [`Counts::params`]. Placed LAST, for the reason `slot_tag` was:
75    /// an image that carves no store (`params == 0`) keeps every offset and
76    /// the total it had before the region existed.
77    pub parameter_slot: RegionUnit,
78    /// Issue 1812 — the parameter SERVICES' home (`nros_node`'s `ParamHome`:
79    /// their state, the shared request/reply buffer pair and the staging slot
80    /// a remote set decodes its value into), one per [`Counts::param_home`].
81    /// Placed after the store's slots for the same reason they are last: an
82    /// image with no store keeps every offset and its total.
83    pub param_home: RegionUnit,
84}
85
86/// How many of each region the image needs.
87///
88/// `remaps` and `violations` are fixed counts in `nros-node` rather than knobs;
89/// they are parameters here so this crate states no policy.
90#[derive(Clone, Copy, Debug, PartialEq, Eq)]
91pub struct Counts {
92    pub cbs: usize,
93    pub sc: usize,
94    pub nodes: usize,
95    pub arena: usize,
96    pub remaps: usize,
97    pub violations: usize,
98    /// `nros-node`'s `alloc` feature — it adds the sporadic-atomic table, so it
99    /// changes the SIZE and cannot be inferred from the counts.
100    pub alloc: bool,
101    /// phase-382 W3' — parameter-store slots carved from the backing. ZERO for
102    /// an image the build does not know to build a store, which is what keeps
103    /// the region off every image that merely links the declare path (issue
104    /// 1702's rejected `.bss` static was unconditional). Policy is
105    /// `nros-params`'; this crate only places the region.
106    pub params: usize,
107    /// Issue 1812 — parameter-service homes carved from the backing: one when
108    /// the image carves a store, zero otherwise. `nros-node` decides; this
109    /// crate only places the region.
110    pub param_home: usize,
111}
112
113/// Byte offset of each region, plus the total size and alignment.
114#[derive(Clone, Copy, Debug, PartialEq, Eq)]
115pub struct Offsets {
116    pub arena: usize,
117    pub entries: usize,
118    pub sched_contexts: usize,
119    pub sched_context_bindings: usize,
120    pub sporadic_states: usize,
121    pub sporadic_atomic_states: usize,
122    pub remaps: usize,
123    pub nodes: usize,
124    pub extra_sessions: usize,
125    pub extra_session_ids: usize,
126    pub node_sched_table: usize,
127    pub dispatch_slots: usize,
128    pub component_slots: usize,
129    pub active_groups: usize,
130    pub group_sched_table: usize,
131    pub monitor_violations: usize,
132    pub alive_slots: usize,
133    pub slot_tags: usize,
134    pub params: usize,
135    pub param_home: usize,
136    pub size: usize,
137    pub align: usize,
138}
139
140const fn align_up(off: usize, align: usize) -> usize {
141    off.div_ceil(align) * align
142}
143
144/// Place every region and report the offsets, total size and alignment.
145///
146/// Reproduces `#[repr(C)]` declaration-order layout: the arena first at offset
147/// 0, then each table aligned up to its own element alignment, and the total
148/// rounded up to the maximum alignment seen.
149pub const fn offsets(counts: Counts, units: RegionUnits) -> Offsets {
150    let Counts {
151        cbs,
152        sc,
153        nodes: node_slots,
154        arena,
155        remaps: remap_slots,
156        violations: violation_slots,
157        alloc,
158        params: param_slots,
159        param_home: home_slots,
160    } = counts;
161
162    let mut off = 0usize;
163    let mut max_align = 1usize;
164
165    // arena: [MaybeUninit<u8>; arena] — align 1, at offset 0.
166    let arena_off = 0usize;
167    off += arena;
168
169    macro_rules! place {
170        ($n:expr, $u:expr) => {{
171            let RegionUnit { size, align } = $u;
172            // A zero alignment is not a layout any compiler produces; treating
173            // it as 1 keeps this total rather than panicking in a `const fn`,
174            // and a caller that supplies one gets an answer it can compare.
175            let a = if align == 0 { 1 } else { align };
176            if a > max_align {
177                max_align = a;
178            }
179            off = align_up(off, a);
180            let at = off;
181            off += $n * size;
182            at
183        }};
184    }
185
186    let entries = place!(cbs, units.callback_meta);
187    let sched_contexts = place!(sc, units.sched_context);
188    let sched_context_bindings = place!(cbs, units.sched_context_id);
189    let sporadic_states = place!(sc, units.sporadic_state);
190    // Placed only under `alloc`, and the `else` must not move `off` — the
191    // no-alloc layout is the one every bare-metal image links.
192    let sporadic_atomic_states = if alloc {
193        place!(sc, units.sporadic_atomic)
194    } else {
195        off
196    };
197    let remaps = place!(remap_slots, units.remap_rule);
198    let nodes = place!(node_slots, units.node_record);
199    let extra_sessions = place!(node_slots, units.concrete_session);
200    let extra_session_ids = place!(node_slots, units.extra_session_id);
201    let node_sched_table = place!(node_slots, units.node_sched_entry);
202    let dispatch_slots = place!(node_slots, units.dispatch_slot);
203    let component_slots = place!(node_slots, units.component_slot);
204    let active_groups = place!(node_slots, units.group_name);
205    let group_sched_table = place!(cbs, units.group_sched_entry);
206    let monitor_violations = place!(violation_slots, units.violation);
207    let alive_slots = place!(sc, units.alive_slot);
208    let slot_tags = place!(cbs, units.slot_tag);
209    // Zero slots must not move `off` either: `place!` would still align it up
210    // to the slot's alignment, which is padding an image with no store would
211    // pay for nothing (and a total that moved for it). So an EMPTY region's
212    // offset may be misaligned for its type: a consumer must not build a slice
213    // from it (`nros_node`'s `carve` uses `region_slice`, issue 1771).
214    let params = if param_slots > 0 {
215        place!(param_slots, units.parameter_slot)
216    } else {
217        off
218    };
219    // Issue 1812 — same rule as the store: no home, no padding.
220    let param_home = if home_slots > 0 {
221        place!(home_slots, units.param_home)
222    } else {
223        off
224    };
225
226    Offsets {
227        arena: arena_off,
228        entries,
229        sched_contexts,
230        sched_context_bindings,
231        sporadic_states,
232        sporadic_atomic_states,
233        remaps,
234        nodes,
235        extra_sessions,
236        extra_session_ids,
237        node_sched_table,
238        dispatch_slots,
239        component_slots,
240        active_groups,
241        group_sched_table,
242        monitor_violations,
243        alive_slots,
244        slot_tags,
245        params,
246        param_home,
247        size: align_up(off, max_align),
248        align: max_align,
249    }
250}
251
252/// The backing's total size in bytes — what a reservation has to hold.
253pub const fn size_of_backing(counts: Counts, units: RegionUnits) -> usize {
254    offsets(counts, units).size
255}