nros_platform/lib.rs
1//! Unified platform abstraction traits for nros.
2//!
3//! This crate defines the backend-agnostic interface that platform
4//! implementations (POSIX, Zephyr, FreeRTOS, bare-metal, etc.) must satisfy.
5//! RMW backends consume these traits via thin shim crates that translate
6//! RMW-specific C symbols (e.g., `z_clock_now`, `uxr_millis`) into calls
7//! on the active platform implementation.
8//!
9//! # Trait hierarchy
10//!
11//! Capabilities are split into independent sub-traits so each RMW backend
12//! can declare exactly what it needs:
13//!
14//! - [`PlatformClock`] — monotonic clock (required by all backends)
15//! - [`PlatformAlloc`] — heap allocation (zenoh-pico only)
16//! - [`PlatformSleep`] — sleep / delay (zenoh-pico only)
17//! - [`PlatformYield`] — cooperative yield (zenoh-pico `socket_wait_event`)
18//! - [`PlatformRandom`] — pseudo-random number generation (zenoh-pico only)
19//! - [`PlatformTime`] — wall-clock time (zenoh-pico only)
20//! - [`PlatformThreading`] — tasks, mutexes, condvars (multi-threaded platforms)
21//!
22//! # Compile-time resolution
23//!
24//! Exactly one platform feature must be enabled. The `ConcretePlatform`
25//! type alias (gated on any `platform-*` feature) resolves to the active
26//! backend, eliminating generic parameters.
27
28#![no_std]
29
30mod board;
31mod resolve;
32
33// Phase 212.N.1 — the Board trait family lives in `board/` (was a
34// flat `board.rs`); `BoardConfig` stays at
35// the crate root for back-compat. New 212.N consumers reach the
36// full surface (`Board`, `BoardInit`, `BoardEntry`, …) through
37// `nros_platform::board::*`.
38pub use board::{
39 Board, BoardConfig, BoardEntry, BoardExit, BoardInit, BoardPrint, DeployOverlay,
40 DispatchStrategy, EmbassyBoardEntry, LinkKind, NodeDispatchRuntime, NullNodeRuntime,
41 PriorityDirection, RticBoardEntry, RuntimeCtx, RuntimeError, SignaledCallback, TierSpec,
42 TierSpinGap, boot_tier_index, freertos_priority_for, posix_nice_for, threadx_priority_for,
43};
44// Phase 313 W1 (issue #0243) — the deprecated `NodeRuntime` crate-root alias is
45// removed; consumers use `NodeDispatchRuntime`.
46// Phase 212.N.2 — `NetworkError` is the return type any external
47// `NetworkWait` impl carries, so it needs to be reachable at the
48// crate root. The `board` module stays private; this re-export keeps
49// the boundary clean.
50pub use board::network::NetworkError;
51
52// Phase 129.C.3.b — `NET_*` constants exported unconditionally
53// (see `resolve.rs`). `ConcretePlatform` keeps its feature gate
54// because the type alias still needs a concrete platform crate
55// linked in.
56pub use resolve::{NET_ENDPOINT_ALIGN, NET_ENDPOINT_SIZE, NET_SOCKET_ALIGN, NET_SOCKET_SIZE};
57
58#[cfg(any(
59 feature = "platform-posix",
60 feature = "platform-cffi",
61 feature = "platform-mps2-an385",
62 feature = "platform-stm32f4",
63 feature = "platform-esp32-qemu",
64 feature = "platform-nuttx",
65 feature = "platform-freertos",
66 feature = "platform-threadx",
67 feature = "platform-zephyr",
68))]
69pub use resolve::ConcretePlatform;
70
71// issue 1123 — the `nros_log` sink list, reachable from a crate the ENTRY
72// already declares.
73//
74// `nros_platform_cffi::log::init_default()` is the one funnel that publishes a
75// sink list (issue 0710 moved it there because the ABI it speaks is a
76// DEPENDENCY, not a feature). Every board crate calls it in its own boot path —
77// but the Zephyr pure-Rust entry is a MACRO, not a board method, and neither
78// the Zephyr example leaves (`examples/zephyr/rust/*`) nor `nros` itself dep
79// `nros-platform-cffi`, so no expansion could name it. They all dep THIS crate
80// (`nros-platform = { features = ["platform-zephyr"] }`), and every
81// `platform-*` feature already pulls `nros-platform-cffi` in, so the re-export
82// costs nothing and adds no link-time requirement that the feature did not
83// already carry.
84//
85// Deliberately NOT giving the leaves a `nros-platform-cffi` dep of their own:
86// that pushes a link-time requirement onto every consumer, which is the shape
87// issue 0710 rejected.
88#[cfg(any(
89 feature = "platform-posix",
90 feature = "platform-cffi",
91 feature = "platform-mps2-an385",
92 feature = "platform-stm32f4",
93 feature = "platform-esp32-qemu",
94 feature = "platform-nuttx",
95 feature = "platform-freertos",
96 feature = "platform-threadx",
97 feature = "platform-zephyr",
98))]
99pub use nros_platform_cffi::log;
100
101// Re-export every trait from the split-out `nros-platform-api` crate so
102// existing `use nros_platform::PlatformClock;` imports keep working.
103pub use nros_platform_api::*;
104
105// Link-graph anchor — relays an in-rlib `#[used]` static to the
106// `_nros_force_link_cffi` symbol that lives in `nros-platform-cffi`.
107// Downstream crates (`nros-rmw-zenoh`, the C/C++ FFI) reference
108// `__FORCE_LINK_CFFI` from their own `#[used]` static, which chains
109// up through this crate to cffi and keeps the `libnros_platform_posix.a`
110// static lib in the final link. Without the chain, rustc elides the
111// cffi rlib and every `nros_platform_*` C symbol is unresolved.
112#[cfg(feature = "platform-posix")]
113#[doc(hidden)]
114#[used]
115pub static __FORCE_LINK_CFFI: extern "C" fn() = nros_platform_cffi::_nros_force_link_cffi;
116
117/// Issue 1732 — a termination signal ENDS a hosted image's spin, so the boot
118/// funnel can close the RMW session before the process exits.
119///
120/// The one spelling for every hosted funnel: the Rust board
121/// (`nros-board-linux`, both its single-executor and its tiered path), the
122/// `nros::main!` hosted spin loops, and the native C/C++ runners in
123/// `nros-cpp`. The guard itself is C, in the POSIX port
124/// (`nros_posix_install_termination_guard` in `nros-platform-posix`), because
125/// that is the one translation unit every hosted image already links.
126///
127/// The contract, all of it in the port:
128///
129/// * installed only over the DEFAULT disposition of SIGTERM / SIGINT — an
130/// application's own handler and `SIG_IGN` are left alone;
131/// * the handler only sets a flag; nothing is torn down in a signal frame;
132/// * a SECOND signal ends the image by the default action, so a wedged
133/// teardown is still stoppable without SIGKILL.
134///
135/// On a build without the POSIX port nothing is installed and
136/// [`requested`](termination::requested) is always `false`, so a loop that
137/// polls it costs a constant.
138pub mod termination {
139 #[cfg(feature = "platform-posix")]
140 unsafe extern "C" {
141 fn nros_posix_install_termination_guard() -> i32;
142 fn nros_posix_termination_requested() -> i32;
143 }
144
145 /// Install the guard; returns how many of SIGTERM / SIGINT carry it (0 when
146 /// the application already handles both, or on a non-POSIX build).
147 /// Idempotent. Call it before the session opens, so a signal that arrives
148 /// during registration is not lost.
149 pub fn install_guard() -> usize {
150 #[cfg(feature = "platform-posix")]
151 {
152 // SAFETY: plain C call; `sigaction` on the process's own handlers.
153 let n = unsafe { nros_posix_install_termination_guard() };
154 if n > 0 { n as usize } else { 0 }
155 }
156 #[cfg(not(feature = "platform-posix"))]
157 {
158 0
159 }
160 }
161
162 /// `true` once a guarded termination signal has arrived. A spin loop that
163 /// sees it returns, and its funnel closes the session.
164 pub fn requested() -> bool {
165 #[cfg(feature = "platform-posix")]
166 {
167 // SAFETY: reads a `sig_atomic_t`.
168 unsafe { nros_posix_termination_requested() != 0 }
169 }
170 #[cfg(not(feature = "platform-posix"))]
171 {
172 false
173 }
174 }
175}
176
177// ============================================================================
178// Phase 248 C7 — Zephyr platform helper (relocated from `nros::platform::zephyr`)
179// ============================================================================
180/// Zephyr-specific platform helpers.
181///
182/// On Zephyr's `native_sim`, the default network interface is assigned an IPv4
183/// address at boot, but the underlying TAP link reports `net_if_is_up() == false`
184/// for ~100–200 ms until the host side is ready. Opening a zenoh session before
185/// that returns `TransportError::ConnectionFailed`. Call [`zephyr::wait_network`]
186/// before `Executor::open`. Mirrors the `nros_platform_zephyr_wait_network()` C
187/// helper the C/C++ examples use; the symbol is RMW-independent (defined in
188/// `nros-platform-zephyr`, compiled in every RMW build). Equivalent to
189/// `nros-board-zephyr`'s `ZephyrBoard::wait_link_up`.
190// phase-391 W3 — the rlsf arena behind Zephyr's C allocation funnel.
191#[cfg(feature = "platform-zephyr")]
192pub mod zephyr_heap;
193
194#[cfg(feature = "platform-zephyr")]
195pub mod zephyr {
196 unsafe extern "C" {
197 fn nros_platform_zephyr_wait_network(timeout_ms: i32) -> i32;
198 }
199
200 /// The default network interface did not come up within the timeout.
201 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
202 pub struct NetworkTimeout;
203
204 /// Block until the default Zephyr network interface is operational, or the
205 /// timeout expires. `Ok(())` on link-up, `Err(NetworkTimeout)` on timeout.
206 pub fn wait_network(timeout_ms: i32) -> Result<(), NetworkTimeout> {
207 // SAFETY: `nros_platform_zephyr_wait_network` has no preconditions beyond
208 // being called from a Zephyr thread context — always true in a Zephyr app.
209 let ret = unsafe { nros_platform_zephyr_wait_network(timeout_ms) };
210 if ret == 0 {
211 Ok(())
212 } else {
213 Err(NetworkTimeout)
214 }
215 }
216}
217
218// ============================================================================
219// Phase 71.27 — opt-in `#[global_allocator]`
220// ============================================================================
221//
222// On bare-metal / RTOS targets DDS + heapless futures need a real
223// heap. Each `nros-platform-*` crate already implements `PlatformAlloc`
224// against its native heap (`pvPortMalloc` on FreeRTOS,
225// `tx_byte_allocate` on ThreadX, `kmm_malloc` on NuttX,
226// `k_malloc` on Zephyr, libc `malloc` on POSIX). This module promotes
227// that trait impl into a `#[global_allocator]` so application crates
228// don't have to write per-platform glue.
229//
230// Off by default — `platform-posix` users link against libstd's
231// allocator. Enable via `nros-platform/global-allocator` in the
232// example crate's `Cargo.toml` to wire it in.
233
234// phase-361 W8.c / issue 0594 — this is the ONE `#[global_allocator]` in the
235// tree. `nros-c` used to define a second one under an identical gate, reaching
236// the same heap by a different route (a direct `extern "C" nros_platform_alloc`
237// rather than the trait), and the two were kept apart only by a manifest
238// comment: `nros-c` deps `nros-platform` non-optionally, so any image that
239// enabled both features got a duplicate lang item. `nros-c/global-allocator`
240// now forwards here, which makes the duplication impossible rather than
241// merely discouraged — cargo unifies one crate's one feature into one unit.
242//
243// This adapter covers BOTH link shapes, which the `nros-c` copy did not:
244// every `platform-*` feature resolves `ConcretePlatform` to `CffiPlatform`
245// (see `resolve.rs`), whose `PlatformAlloc` impl IS `nros_platform_alloc`, and
246// the bare-metal Rust crates (mps2-an385, stm32f4, esp32-qemu) reach their own
247// arena through the same trait. One API, one arena, per RFC-0034 D6.
248#[cfg(all(feature = "global-allocator", not(feature = "std")))]
249mod global_allocator {
250 use core::{
251 alloc::{GlobalAlloc, Layout},
252 ffi::c_void,
253 };
254
255 use crate::ConcretePlatform;
256 use nros_platform_api::PlatformAlloc;
257
258 /// Alignment the platform ABI guarantees. `nros_platform_alloc` has no
259 /// alignment parameter, and every port behind it returns memory aligned
260 /// for the widest scalar — 8 bytes on every target nano-ros builds for.
261 const PLATFORM_ALIGN: usize = 8;
262
263 /// `GlobalAlloc` adapter over `<ConcretePlatform as PlatformAlloc>`.
264 pub struct PlatformGlobalAllocator;
265
266 unsafe impl GlobalAlloc for PlatformGlobalAllocator {
267 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
268 // phase-361 W8.c — an over-aligned request FAILS rather than
269 // silently returning under-aligned memory. The ABI cannot express
270 // alignment, so the honest answer to `align > 8` is null, which
271 // routes the caller into `handle_alloc_error`. The previous
272 // `let _ = layout.align();` produced UB no build could see;
273 // `zpico-alloc` already answered the same question with null.
274 if layout.align() > PLATFORM_ALIGN {
275 return core::ptr::null_mut();
276 }
277 let p = <ConcretePlatform as PlatformAlloc>::alloc(layout.size()) as *mut u8;
278 #[cfg(feature = "alloc-stats")]
279 if !p.is_null() {
280 super::heap_stats::STATS.on_alloc(layout.size());
281 }
282 p
283 }
284
285 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
286 <ConcretePlatform as PlatformAlloc>::dealloc(ptr as *mut c_void);
287 #[cfg(feature = "alloc-stats")]
288 super::heap_stats::STATS.on_dealloc(_layout.size());
289 }
290 }
291
292 #[global_allocator]
293 static ALLOCATOR: PlatformGlobalAllocator = PlatformGlobalAllocator;
294}
295
296// phase-361 W8.c — the Rust-footprint heap counter, moved here with the
297// allocator it instruments. It counts only what passes through the
298// `#[global_allocator]`; the C side's direct `nros_platform_alloc` traffic
299// (zenoh-pico's `z_malloc` etc.) is not seen, so it under-reports true heap
300// pressure. The *unified* figure is the platform's own
301// `nros_platform_heap_used_bytes` (RFC-0034 D7).
302//
303// No `#[no_mangle]` here: the C names (`rclc_heap_used_bytes` …) belong to the
304// C/C++ API surface and stay exported by `nros-c` / `nros-cpp`, which read
305// these accessors. A pure-Rust image gets the counter without gaining C
306// symbols it never asked for.
307#[cfg(feature = "alloc-stats")]
308pub mod heap_stats {
309 use core::sync::atomic::{AtomicUsize, Ordering};
310
311 /// Bytes outstanding through the Rust global allocator, and the high-water
312 /// mark since boot. `Relaxed` throughout — this is instrumentation, and no
313 /// other state is ordered against it.
314 pub struct HeapStats {
315 used_bytes: AtomicUsize,
316 peak_bytes: AtomicUsize,
317 }
318
319 impl HeapStats {
320 /// Create a zeroed counter. `const` so it can back a `static`.
321 pub const fn new() -> Self {
322 Self {
323 used_bytes: AtomicUsize::new(0),
324 peak_bytes: AtomicUsize::new(0),
325 }
326 }
327
328 /// Record a successful allocation of `size` bytes and update the peak.
329 #[inline]
330 pub fn on_alloc(&self, size: usize) {
331 let used = self.used_bytes.fetch_add(size, Ordering::Relaxed) + size;
332 let _ = self.peak_bytes.fetch_max(used, Ordering::Relaxed);
333 }
334
335 /// Record a deallocation of `size` bytes.
336 #[inline]
337 pub fn on_dealloc(&self, size: usize) {
338 self.used_bytes.fetch_sub(size, Ordering::Relaxed);
339 }
340
341 /// Bytes currently outstanding.
342 #[inline]
343 pub fn used(&self) -> usize {
344 self.used_bytes.load(Ordering::Relaxed)
345 }
346
347 /// Peak outstanding bytes since boot.
348 #[inline]
349 pub fn peak(&self) -> usize {
350 self.peak_bytes.load(Ordering::Relaxed)
351 }
352 }
353
354 impl Default for HeapStats {
355 fn default() -> Self {
356 Self::new()
357 }
358 }
359
360 pub static STATS: HeapStats = HeapStats::new();
361
362 /// Bytes currently outstanding through the Rust global allocator.
363 #[inline]
364 pub fn used() -> usize {
365 STATS.used()
366 }
367
368 /// Peak outstanding bytes through the Rust global allocator since boot.
369 #[inline]
370 pub fn peak() -> usize {
371 STATS.peak()
372 }
373}