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}