Skip to main content

nros/
sizes.rs

1//! Single source of truth for FFI opaque-storage sizes.
2//!
3//! Each `export_size!` invocation produces two artefacts:
4//!
5//! * `pub const FOO_SIZE: usize = core::mem::size_of::<T>();` — a normal
6//!   Rust const suitable for in-crate `const _: () = assert!(...)` checks and
7//!   direct use by `no_std` consumers.
8//! * `pub static __NROS_SIZE_FOO: [u8; FOO_SIZE] = [0; FOO_SIZE];` — an
9//!   array-sized static whose *symbol storage size* in the compiled rlib
10//!   equals FOO_SIZE. `nros-c`/`nros-cpp` build scripts read the sizes out
11//!   via [`nros_sizes_build::extract_sizes`](../../../nros-sizes-build/index.html)
12//!   to derive opaque-storage macros for the generated C/C++ headers.
13//!
14//! # The markers exist ONLY in the size probe's own build (issue 1765)
15//!
16//! The statics, and the `__NROS_SIZE_FN_*` fn-pointer markers beside them, are
17//! compiled only under `feature = "ffi-size-markers"`. Exactly one party
18//! requests that feature: the nested probe cargo behind
19//! `nros-build-helpers::shared::probe_nros_sizes`, which builds `nros` in its
20//! OWN target directory. No shipped dep-site enables it, and `build.rs` makes
21//! enabling it outside the probe a compile error.
22//!
23//! It used to be requested on `nros-c`'s and `nros-cpp`'s `nros` dependency,
24//! so the markers were part of the linked staticlib: `#[no_mangle]` immutable
25//! statics, which a staticlib exports and the board linker scripts place in
26//! `.text`. Every C/C++ image paid the SUM of the probed sizes in flash, as
27//! zero bytes (111,252 B of a 587,256 B FreeRTOS C zenoh talker), and once
28//! phase-382 W3' carved the parameter store into `EXECUTOR_SIZE`, a second
29//! 280 KB copy of the store. The `pub const`s are unaffected: they are what
30//! in-crate asserts and `no_std` consumers read, and a const occupies nothing.
31//!
32//! Feature gating follows the rest of the crate: the statics only exist when
33//! an RMW backend (`rmw-zenoh` / `rmw-xrce` / `rmw-cyclonedds` / `rmw-cffi`) is
34//! active, which is exactly the condition under which the `Rmw*` type
35//! aliases resolve. Workspace-level `cargo check` without any RMW feature
36//! sees this module as empty.
37
38#[cfg(feature = "rmw-cffi")]
39mod rmw_sizes {
40    use crate::internals::{
41        RmwPublisher, RmwServiceClient, RmwServiceServer, RmwSession, RmwSubscriber,
42    };
43
44    // Phase 77.25: per-name v0-mangled markers so the probe works
45    // under fat LTO. Each call to `export_size!(NAME = Ty)` expands to
46    // a distinct generic fn `__nros_size_NAME<const N: usize>` plus a
47    // monomorphised fn-pointer static. The monomorphisation's v0
48    // mangled symbol name contains both the name ("NAME") *and* the
49    // const-generic value (the size) — e.g. demangles as
50    // nros::sizes::rmw_sizes::__nros_size_PUBLISHER_SIZE::<48>.
51    // Symbol names survive LTO because the linker still needs them,
52    // even when the object file is LLVM bitcode and `object::parse`
53    // can't read symbol byte sizes. The original `__NROS_SIZE_<NAME>`
54    // static kept for backwards-compat is also emitted for consumers
55    // that still walk the legacy path.
56    #[doc(hidden)]
57    pub mod _size_markers {}
58
59    macro_rules! export_size {
60        ($vis:vis $name:ident = $ty:ty) => {
61            export_bytes!($vis $name = core::mem::size_of::<$ty>());
62        };
63    }
64
65    // Issue 1970 -- the same marker for a byte count that is not one type's
66    // size (a tier's executor backing).
67    macro_rules! export_bytes {
68        ($vis:vis $name:ident = $bytes:expr) => {
69            $vis const $name: usize = $bytes;
70            // issue 1765 — every marker is `cfg`'d on the probe-only feature,
71            // not merely `cfg_attr(.., used)`. A `#[no_mangle]` static is
72            // EXPORTED from a staticlib whether or not it is `#[used]`, so the
73            // attribute alone never kept one out of an image.
74            // `check-size-markers-unlinked` holds every item here to this.
75            pastey::paste! {
76                #[cfg(feature = "ffi-size-markers")]
77                #[used]
78                #[unsafe(no_mangle)]
79                #[doc(hidden)]
80                pub static [<__NROS_SIZE_ $name>]: [u8; $name] = [0u8; $name];
81
82                #[cfg(feature = "ffi-size-markers")]
83                #[doc(hidden)]
84                #[allow(non_snake_case)]
85                #[inline(never)]
86                pub fn [<__nros_size_ $name>]<const N: usize>() -> usize { N }
87
88                #[cfg(feature = "ffi-size-markers")]
89                #[used]
90                #[doc(hidden)]
91                pub static [<__NROS_SIZE_FN_ $name>]: fn() -> usize =
92                    [<__nros_size_ $name>]::<{ $name }>;
93            }
94        };
95    }
96
97    export_size!(pub SESSION_SIZE        = RmwSession);
98    export_size!(pub PUBLISHER_SIZE      = RmwPublisher);
99    export_size!(pub SUBSCRIBER_SIZE     = RmwSubscriber);
100    export_size!(pub SERVICE_CLIENT_SIZE = RmwServiceClient);
101    export_size!(pub SERVICE_SERVER_SIZE = RmwServiceServer);
102
103    // Phase 122.3.c.3 — L1 polling-mode Raw* handles. Defaults to
104    // `DEFAULT_RX_BUF_SIZE` on each const-generic slot, which is
105    // exactly the value `nros-c::config::MESSAGE_BUFFER_SIZE` resolves
106    // to (both derive from `NROS_SUBSCRIPTION_BUFFER_SIZE` via
107    // `nros-node`'s build.rs). nros-c's build.rs reads these probe
108    // values and emits the matching `*_OPAQUE_U64S` macros into the
109    // per-build variant header so C struct `_opaque` storage agrees
110    // with the Rust struct layout. Without this probe, cbindgen
111    // ships a `_OPAQUE_U64S = 1` placeholder which silently
112    // truncates the C-side `_opaque` slot.
113    export_size!(pub RAW_SUBSCRIPTION_SIZE   = nros_node::RawSubscription);
114    export_size!(pub RAW_SERVICE_SERVER_SIZE = nros_node::RawServiceServer);
115    export_size!(pub RAW_SERVICE_CLIENT_SIZE = nros_node::RawServiceClient);
116
117    // Phase 122.3.c.6 — typeless action `*Core` types are the raw
118    // L1 polling-mode entities (typed `ActionServer<A>` / `ActionClient<A>`
119    // wrap them with serdes glue). Default const generics resolve to
120    // `DEFAULT_RX_BUF_SIZE` (= `MESSAGE_BUFFER_SIZE` in nros-c) and
121    // `MAX_GOALS = 4`, matching the L2 callback path.
122    export_size!(pub RAW_ACTION_SERVER_SIZE = nros_node::ActionServerCore);
123    export_size!(pub RAW_ACTION_CLIENT_SIZE = nros_node::ActionClientCore);
124
125    // phase-271 — the C/C++ `_opaque` executor buffer holds the `Executor<'static>`
126    // header AND the per-entry storage backing carved from the same buffer, so it
127    // must be sized for the combined `#[repr(C)]` layout, not bare `Executor`.
128    export_size!(pub EXECUTOR_SIZE       = nros_node::ExecutorInlineStorage);
129
130    // Issue 1970 -- each executor tier's own backing, in the tier table's order
131    // (`nros_node::config::TIER_SIZINGS`), zero past the last tier and on an
132    // image sized as one executor. Eight is the most a per-tier image carries:
133    // the CLI's descriptor states no tier beyond it. The C/C++ header publishes
134    // them as `NROS_CPP_TIER_BACKING_SIZE_<i>`, which size the generated
135    // entry's per-tier backing statics.
136    export_bytes!(pub TIER_BACKING_SIZE_0 = nros_node::tier_backing_bytes(0));
137    export_bytes!(pub TIER_BACKING_SIZE_1 = nros_node::tier_backing_bytes(1));
138    export_bytes!(pub TIER_BACKING_SIZE_2 = nros_node::tier_backing_bytes(2));
139    export_bytes!(pub TIER_BACKING_SIZE_3 = nros_node::tier_backing_bytes(3));
140    export_bytes!(pub TIER_BACKING_SIZE_4 = nros_node::tier_backing_bytes(4));
141    export_bytes!(pub TIER_BACKING_SIZE_5 = nros_node::tier_backing_bytes(5));
142    export_bytes!(pub TIER_BACKING_SIZE_6 = nros_node::tier_backing_bytes(6));
143    export_bytes!(pub TIER_BACKING_SIZE_7 = nros_node::tier_backing_bytes(7));
144
145    // phase-509 P9b item 4 (RFC-0108 "As built in P9b") -- the ONE executor
146    // backing a level runner opens per core: every tier's (level's) members,
147    // summed. Zero on an image sized as one executor. Published as
148    // `NROS_CPP_LEVELS_BACKING_SIZE`, which sizes the generated entry's
149    // levels backing.
150    export_bytes!(pub LEVELS_BACKING_SIZE = nros_node::levels_backing_bytes());
151
152    // issue 0961 — the STACK requirement is a different number from the one
153    // above, and reusing that one would overstate it. `EXECUTOR_SIZE` is the
154    // executor value PLUS its carved backing, which is what the caller's
155    // `_opaque` buffer must hold; the backing lives wherever the caller put
156    // that buffer (`.bss` for a static holder) and never lands on the stack.
157    // What lands on the stack is the bare value: `open_in` builds one in its
158    // frame and returns it, and `nros_cpp_init` holds the returned value
159    // before writing it into the caller's storage — the same value, moved
160    // twice, which is why the check derived from this is `2 x`.
161    //
162    // On a 320 KiB part the overstatement would be a build error refusing an
163    // image that fits, so the two numbers stay separate rather than one being
164    // reused for both jobs.
165    export_size!(pub EXECUTOR_VALUE_SIZE = nros_node::Executor<'static>);
166    export_size!(pub GUARD_CONDITION_SIZE = nros_node::GuardCondition);
167    export_size!(pub LIFECYCLE_CTX_SIZE  = nros_node::lifecycle::LifecyclePollingNodeCtx);
168    // Phase 91.C: nros-c's `ActionServerInternal` embeds this nros-node
169    // type as a typed field. cbindgen (which can't recurse into deps)
170    // emits the field as `ActionServerRawHandle handle;` referencing a
171    // type it cannot define. nros-c's build.rs reads this size and emits
172    // an opaque type-compatible declaration into nros_config_generated.h
173    // so the cbindgen output is self-contained.
174    export_size!(pub ACTION_SERVER_RAW_HANDLE_SIZE = nros_node::ActionServerRawHandle);
175
176    // Layout-mirror struct for `nros_c::action::ActionServerInternal`.
177    // ActionServerInternal lives in the `nros-c` crate, so it can't be
178    // referenced from `nros` directly. This mirror has the same `#[repr(C)]`
179    // field shape and therefore the same byte size. nros-c asserts at compile
180    // time that `size_of::<ActionServerInternal>() ==
181    // size_of::<ActionServerInternalLayout>()`.
182    //
183    // phase-487 W4 removed five fields from both: a second copy of the
184    // server's callback record (~~`c_goal_callback`~~, ~~`c_cancel_callback`~~,
185    // ~~`c_accepted_callback`~~, `c_context`) and `server_ptr`, which pointed back
186    // to the struct containing it. The trampolines read the server's own
187    // record now.
188    use core::ffi::c_void;
189
190    #[repr(C)]
191    #[doc(hidden)]
192    pub struct ActionServerInternalLayout {
193        pub handle: nros_node::ActionServerRawHandle,
194        pub executor_ptr: *mut c_void,
195    }
196    export_size!(pub ACTION_SERVER_INTERNAL_SIZE = ActionServerInternalLayout);
197
198    // Layout-mirrors for nros-cpp's `CppActionServer` and `CppActionClient`.
199    //
200    // Same approach as `ActionServerInternalLayout` above: nros-cpp's
201    // wrapper structs live in a downstream crate but their byte sizes can
202    // be reconstructed from the field shape. This eliminates the
203    // hand-math in `nros-cpp/build.rs` (was Phase 87.11).
204    //
205    // The C++-side `nros::ActionServer<A>` / `nros::ActionClient<A>`
206    // classes hold opaque storage sized to these probe values. nros-cpp
207    // asserts `size_of::<CppActionServer>() == size_of::<CppActionServerLayout>()`
208    // (and the same for CppActionClient) so any field-shape drift in the
209    // real wrapper trips the build immediately.
210
211    type CppGoalCallbackLayout =
212        unsafe extern "C" fn(*const [u8; 16], *const u8, usize, *mut c_void) -> i32;
213    type CppCancelCallbackLayout = unsafe extern "C" fn(*const [u8; 16], *mut c_void) -> i32;
214    // issue 0796 — the C++ callback tier's accepted-goal hook (`CppAcceptedCallback`).
215    type CppAcceptedCallbackLayout = unsafe extern "C" fn(*const [u8; 16], *mut c_void);
216
217    // Byte-shape mirror of one of `nros-cpp`'s `nros_cpp_qos_t` policy enums
218    // (`nros_cpp_qos_reliability_t` et al). These are `#[repr(C)]` fieldless
219    // enums, so their width follows the *target C ABI*: `c_int` (4 bytes) on
220    // x86_64, but **1 byte on ARM EABI** (`armv7a-nuttx-eabihf` defaults to
221    // `-fshort-enums`). Mirroring them as `c_int` (the pre-fix shape) over-sized
222    // the qos block by 12 bytes on ARM and tripped the `CppActionServer`
223    // layout assert. A `#[repr(C)]` enum here tracks the same short-enum width
224    // on every target. Variant count is irrelevant to width while ≤ 255 (all
225    // four real enums have ≤ 4 variants), so one placeholder mirrors all four.
226    #[repr(C)]
227    #[doc(hidden)]
228    pub enum CppQosEnumLayout {
229        A = 0,
230        B = 1,
231    }
232
233    // Phase 193.4b: byte-shape mirror of `nros-cpp`'s `nros_cpp_qos_t`
234    // (4 C-ABI enums + `c_int` depth + 3×u32 + u8). The action server now
235    // stores the create-time QoS until registration; the real
236    // `CppActionServer` field-shape assert keeps this in sync.
237    #[repr(C)]
238    #[doc(hidden)]
239    pub struct CppQosLayout {
240        pub reliability: CppQosEnumLayout,
241        pub durability: CppQosEnumLayout,
242        pub history: CppQosEnumLayout,
243        pub liveliness_kind: CppQosEnumLayout,
244        pub depth: core::ffi::c_int,
245        pub deadline_ms: u32,
246        pub lifespan_ms: u32,
247        pub liveliness_lease_ms: u32,
248        pub avoid_ros_namespace_conventions: u8,
249    }
250
251    // Phase 87.6 thin-wrapper: `action_name` / `type_name` / `type_hash`
252    // buffers moved to the C++ `nros::ActionServer<A>` class.
253    // Phase 104.C.9.b: `node_id` field added so action create→register
254    // can route through the per-Node session.
255    // Phase 193.4b: `qos` field added so create-time QoS reaches the three
256    // underlying service servers at register time.
257    #[repr(C)]
258    #[doc(hidden)]
259    pub struct CppActionServerLayout {
260        pub handle: Option<nros_node::ActionServerRawHandle>,
261        pub goal_cb: Option<CppGoalCallbackLayout>,
262        pub cancel_cb: Option<CppCancelCallbackLayout>,
263        pub accepted_cb: Option<CppAcceptedCallbackLayout>,
264        pub cb_ctx: *mut c_void,
265        pub node_id: u8,
266        pub _reserved: [u8; 7],
267        pub qos: CppQosLayout,
268    }
269    export_size!(pub CPP_ACTION_SERVER_SIZE = CppActionServerLayout);
270
271    type CppActionGoalResponseCb = Option<unsafe extern "C" fn(bool, *const [u8; 16], *mut c_void)>;
272    type CppActionFeedbackCb =
273        Option<unsafe extern "C" fn(*const [u8; 16], *const u8, usize, *mut c_void)>;
274    type CppActionResultCb =
275        Option<unsafe extern "C" fn(*const [u8; 16], i32, *const u8, usize, *mut c_void)>;
276    type CppActionCancelReplyCb =
277        Option<unsafe extern "C" fn(*const [u8; 16], i64, *const u8, usize, *mut c_void)>;
278
279    #[repr(C)]
280    #[doc(hidden)]
281    pub struct CppActionClientCallbacksLayout {
282        pub goal_response: CppActionGoalResponseCb,
283        pub feedback: CppActionFeedbackCb,
284        pub result: CppActionResultCb,
285        /// Issue 2034 — the cancel-reply callback.
286        pub cancel_reply: CppActionCancelReplyCb,
287        pub context: *mut c_void,
288    }
289
290    // Phase 87.6 thin-wrapper: `action_name` buffer moved to the C++
291    // `nros::ActionClient<A>` class.
292    #[repr(C)]
293    #[doc(hidden)]
294    pub struct CppActionClientLayout {
295        pub callbacks: CppActionClientCallbacksLayout,
296        pub arena_entry_index: i32,
297        pub executor_ptr: *mut c_void,
298    }
299    export_size!(pub CPP_ACTION_CLIENT_SIZE = CppActionClientLayout);
300}
301
302#[cfg(feature = "rmw-cffi")]
303pub use rmw_sizes::*;