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}