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 component_slot: RegionUnit,
63    pub group_name: RegionUnit,
64    pub group_sched_entry: RegionUnit,
65    pub violation: RegionUnit,
66    /// Alive supervision's per-SchedContext record, one per `sc`.
67    pub alive_slot: RegionUnit,
68    /// phase-476 W0 — the per-callback-slot lifetime tag (generation + owning
69    /// node), one per `cbs`. Placed after every older region so each keeps
70    /// its offset.
71    pub slot_tag: RegionUnit,
72    /// phase-382 W3' — one parameter-store slot (`nros_params::ParameterSlot`),
73    /// one per [`Counts::params`]. Placed LAST, for the reason `slot_tag` was:
74    /// an image that carves no store (`params == 0`) keeps every offset and
75    /// the total it had before the region existed.
76    pub parameter_slot: RegionUnit,
77    /// Issue 1812 — the parameter SERVICES' home (`nros_node`'s `ParamHome`:
78    /// their state, the shared request/reply buffer pair and the staging slot
79    /// a remote set decodes its value into), one per [`Counts::param_home`].
80    /// Placed after the store's slots for the same reason they are last: an
81    /// image with no store keeps every offset and its total.
82    pub param_home: RegionUnit,
83}
84
85/// How many of each region the image needs.
86///
87/// `remaps` and `violations` are fixed counts in `nros-node` rather than knobs;
88/// they are parameters here so this crate states no policy.
89#[derive(Clone, Copy, Debug, PartialEq, Eq)]
90pub struct Counts {
91    pub cbs: usize,
92    pub sc: usize,
93    pub nodes: usize,
94    /// Issue 1982 -- extra-session slots (and their ids): sessions a node
95    /// opens on an RMW backend other than the primary's. At most `nodes`;
96    /// `nros-node` sizes it from the image's backend count (0 when it links
97    /// one), this crate only places the region.
98    pub extra_sessions: usize,
99    pub arena: usize,
100    pub remaps: usize,
101    pub violations: usize,
102    /// `nros-node`'s `alloc` feature — it adds the sporadic-atomic table, so it
103    /// changes the SIZE and cannot be inferred from the counts.
104    pub alloc: bool,
105    /// phase-382 W3' — parameter-store slots carved from the backing. ZERO for
106    /// an image the build does not know to build a store, which is what keeps
107    /// the region off every image that merely links the declare path (issue
108    /// 1702's rejected `.bss` static was unconditional). Policy is
109    /// `nros-params`'; this crate only places the region.
110    pub params: usize,
111    /// Issue 1812 — parameter-service homes carved from the backing: one when
112    /// the image carves a store, zero otherwise. `nros-node` decides; this
113    /// crate only places the region.
114    pub param_home: usize,
115}
116
117/// Byte offset of each region, plus the total size and alignment.
118#[derive(Clone, Copy, Debug, PartialEq, Eq)]
119pub struct Offsets {
120    pub arena: usize,
121    pub entries: usize,
122    pub sched_contexts: usize,
123    pub sched_context_bindings: usize,
124    pub sporadic_states: usize,
125    pub sporadic_atomic_states: usize,
126    pub remaps: usize,
127    pub nodes: usize,
128    pub extra_sessions: usize,
129    pub extra_session_ids: usize,
130    pub node_sched_table: usize,
131    pub component_slots: usize,
132    pub active_groups: usize,
133    pub group_sched_table: usize,
134    pub monitor_violations: usize,
135    pub alive_slots: usize,
136    pub slot_tags: usize,
137    pub params: usize,
138    pub param_home: usize,
139    pub size: usize,
140    pub align: usize,
141}
142
143const fn align_up(off: usize, align: usize) -> usize {
144    off.div_ceil(align) * align
145}
146
147/// Place every region and report the offsets, total size and alignment.
148///
149/// Reproduces `#[repr(C)]` declaration-order layout: the arena first at offset
150/// 0, then each table aligned up to its own element alignment, and the total
151/// rounded up to the maximum alignment seen.
152pub const fn offsets(counts: Counts, units: RegionUnits) -> Offsets {
153    let Counts {
154        cbs,
155        sc,
156        nodes: node_slots,
157        extra_sessions: extra_slots,
158        arena,
159        remaps: remap_slots,
160        violations: violation_slots,
161        alloc,
162        params: param_slots,
163        param_home: home_slots,
164    } = counts;
165
166    let mut off = 0usize;
167    let mut max_align = 1usize;
168
169    // arena: [MaybeUninit<u8>; arena] — align 1, at offset 0.
170    let arena_off = 0usize;
171    off += arena;
172
173    macro_rules! place {
174        ($n:expr, $u:expr) => {{
175            let RegionUnit { size, align } = $u;
176            // A zero alignment is not a layout any compiler produces; treating
177            // it as 1 keeps this total rather than panicking in a `const fn`,
178            // and a caller that supplies one gets an answer it can compare.
179            let a = if align == 0 { 1 } else { align };
180            if a > max_align {
181                max_align = a;
182            }
183            off = align_up(off, a);
184            let at = off;
185            off += $n * size;
186            at
187        }};
188    }
189
190    let entries = place!(cbs, units.callback_meta);
191    let sched_contexts = place!(sc, units.sched_context);
192    let sched_context_bindings = place!(cbs, units.sched_context_id);
193    let sporadic_states = place!(sc, units.sporadic_state);
194    // Placed only under `alloc`, and the `else` must not move `off` — the
195    // no-alloc layout is the one every bare-metal image links.
196    let sporadic_atomic_states = if alloc {
197        place!(sc, units.sporadic_atomic)
198    } else {
199        off
200    };
201    let remaps = place!(remap_slots, units.remap_rule);
202    let nodes = place!(node_slots, units.node_record);
203    let extra_sessions = place!(extra_slots, units.concrete_session);
204    let extra_session_ids = place!(extra_slots, units.extra_session_id);
205    let node_sched_table = place!(node_slots, units.node_sched_entry);
206    let component_slots = place!(node_slots, units.component_slot);
207    let active_groups = place!(node_slots, units.group_name);
208    let group_sched_table = place!(cbs, units.group_sched_entry);
209    let monitor_violations = place!(violation_slots, units.violation);
210    let alive_slots = place!(sc, units.alive_slot);
211    let slot_tags = place!(cbs, units.slot_tag);
212    // Zero slots must not move `off` either: `place!` would still align it up
213    // to the slot's alignment, which is padding an image with no store would
214    // pay for nothing (and a total that moved for it). So an EMPTY region's
215    // offset may be misaligned for its type: a consumer must not build a slice
216    // from it (`nros_node`'s `carve` uses `region_slice`, issue 1771).
217    let params = if param_slots > 0 {
218        place!(param_slots, units.parameter_slot)
219    } else {
220        off
221    };
222    // Issue 1812 — same rule as the store: no home, no padding.
223    let param_home = if home_slots > 0 {
224        place!(home_slots, units.param_home)
225    } else {
226        off
227    };
228
229    Offsets {
230        arena: arena_off,
231        entries,
232        sched_contexts,
233        sched_context_bindings,
234        sporadic_states,
235        sporadic_atomic_states,
236        remaps,
237        nodes,
238        extra_sessions,
239        extra_session_ids,
240        node_sched_table,
241        component_slots,
242        active_groups,
243        group_sched_table,
244        monitor_violations,
245        alive_slots,
246        slot_tags,
247        params,
248        param_home,
249        size: align_up(off, max_align),
250        align: max_align,
251    }
252}
253
254/// The backing's total size in bytes — what a reservation has to hold.
255pub const fn size_of_backing(counts: Counts, units: RegionUnits) -> usize {
256    offsets(counts, units).size
257}