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, NodeDispatchRuntime, NullNodeRuntime, PriorityDirection,
41 RticBoardEntry, RuntimeCtx, RuntimeError, SignaledCallback, TierSpec, TierSpinGap,
42 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// Re-export every trait from the split-out `nros-platform-api` crate so
72// existing `use nros_platform::PlatformClock;` imports keep working.
73pub use nros_platform_api::*;
74
75// Link-graph anchor — relays an in-rlib `#[used]` static to the
76// `_nros_force_link_cffi` symbol that lives in `nros-platform-cffi`.
77// Downstream crates (`nros-rmw-zenoh`, the C/C++ FFI) reference
78// `__FORCE_LINK_CFFI` from their own `#[used]` static, which chains
79// up through this crate to cffi and keeps the `libnros_platform_posix.a`
80// static lib in the final link. Without the chain, rustc elides the
81// cffi rlib and every `nros_platform_*` C symbol is unresolved.
82#[cfg(feature = "platform-posix")]
83#[doc(hidden)]
84#[used]
85pub static __FORCE_LINK_CFFI: extern "C" fn() = nros_platform_cffi::_nros_force_link_cffi;
86
87// ============================================================================
88// Phase 248 C7 — Zephyr platform helper (relocated from `nros::platform::zephyr`)
89// ============================================================================
90/// Zephyr-specific platform helpers.
91///
92/// On Zephyr's `native_sim`, the default network interface is assigned an IPv4
93/// address at boot, but the underlying TAP link reports `net_if_is_up() == false`
94/// for ~100–200 ms until the host side is ready. Opening a zenoh session before
95/// that returns `TransportError::ConnectionFailed`. Call [`zephyr::wait_network`]
96/// before `Executor::open`. Mirrors the `nros_platform_zephyr_wait_network()` C
97/// helper the C/C++ examples use; the symbol is RMW-independent (defined in
98/// `nros-platform-zephyr`, compiled in every RMW build). Equivalent to
99/// `nros-board-zephyr`'s `ZephyrBoard::wait_link_up`.
100// phase-391 W3 — the rlsf arena behind Zephyr's C allocation funnel.
101#[cfg(feature = "platform-zephyr")]
102pub mod zephyr_heap;
103
104#[cfg(feature = "platform-zephyr")]
105pub mod zephyr {
106 unsafe extern "C" {
107 fn nros_platform_zephyr_wait_network(timeout_ms: i32) -> i32;
108 }
109
110 /// Block until the default Zephyr network interface is operational, or the
111 /// timeout expires. `Ok(())` on link-up, `Err(())` on timeout.
112 pub fn wait_network(timeout_ms: i32) -> Result<(), ()> {
113 // SAFETY: `nros_platform_zephyr_wait_network` has no preconditions beyond
114 // being called from a Zephyr thread context — always true in a Zephyr app.
115 let ret = unsafe { nros_platform_zephyr_wait_network(timeout_ms) };
116 if ret == 0 { Ok(()) } else { Err(()) }
117 }
118}
119
120// ============================================================================
121// Phase 71.27 — opt-in `#[global_allocator]`
122// ============================================================================
123//
124// On bare-metal / RTOS targets DDS + heapless futures need a real
125// heap. Each `nros-platform-*` crate already implements `PlatformAlloc`
126// against its native heap (`pvPortMalloc` on FreeRTOS,
127// `tx_byte_allocate` on ThreadX, `kmm_malloc` on NuttX,
128// `k_malloc` on Zephyr, libc `malloc` on POSIX). This module promotes
129// that trait impl into a `#[global_allocator]` so application crates
130// don't have to write per-platform glue.
131//
132// Off by default — `platform-posix` users link against libstd's
133// allocator. Enable via `nros-platform/global-allocator` in the
134// example crate's `Cargo.toml` to wire it in.
135
136// phase-361 W8.c / issue 0594 — this is the ONE `#[global_allocator]` in the
137// tree. `nros-c` used to define a second one under an identical gate, reaching
138// the same heap by a different route (a direct `extern "C" nros_platform_alloc`
139// rather than the trait), and the two were kept apart only by a manifest
140// comment: `nros-c` deps `nros-platform` non-optionally, so any image that
141// enabled both features got a duplicate lang item. `nros-c/global-allocator`
142// now forwards here, which makes the duplication impossible rather than
143// merely discouraged — cargo unifies one crate's one feature into one unit.
144//
145// This adapter covers BOTH link shapes, which the `nros-c` copy did not:
146// every `platform-*` feature resolves `ConcretePlatform` to `CffiPlatform`
147// (see `resolve.rs`), whose `PlatformAlloc` impl IS `nros_platform_alloc`, and
148// the bare-metal Rust crates (mps2-an385, stm32f4, esp32-qemu) reach their own
149// arena through the same trait. One API, one arena, per RFC-0034 D6.
150#[cfg(all(feature = "global-allocator", not(feature = "std")))]
151mod global_allocator {
152 use core::{
153 alloc::{GlobalAlloc, Layout},
154 ffi::c_void,
155 };
156
157 use crate::ConcretePlatform;
158 use nros_platform_api::PlatformAlloc;
159
160 /// Alignment the platform ABI guarantees. `nros_platform_alloc` has no
161 /// alignment parameter, and every port behind it returns memory aligned
162 /// for the widest scalar — 8 bytes on every target nano-ros builds for.
163 const PLATFORM_ALIGN: usize = 8;
164
165 /// `GlobalAlloc` adapter over `<ConcretePlatform as PlatformAlloc>`.
166 pub struct PlatformGlobalAllocator;
167
168 unsafe impl GlobalAlloc for PlatformGlobalAllocator {
169 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
170 // phase-361 W8.c — an over-aligned request FAILS rather than
171 // silently returning under-aligned memory. The ABI cannot express
172 // alignment, so the honest answer to `align > 8` is null, which
173 // routes the caller into `handle_alloc_error`. The previous
174 // `let _ = layout.align();` produced UB no build could see;
175 // `zpico-alloc` already answered the same question with null.
176 if layout.align() > PLATFORM_ALIGN {
177 return core::ptr::null_mut();
178 }
179 let p = <ConcretePlatform as PlatformAlloc>::alloc(layout.size()) as *mut u8;
180 #[cfg(feature = "alloc-stats")]
181 if !p.is_null() {
182 super::heap_stats::STATS.on_alloc(layout.size());
183 }
184 p
185 }
186
187 unsafe fn dealloc(&self, ptr: *mut u8, _layout: Layout) {
188 <ConcretePlatform as PlatformAlloc>::dealloc(ptr as *mut c_void);
189 #[cfg(feature = "alloc-stats")]
190 super::heap_stats::STATS.on_dealloc(_layout.size());
191 }
192 }
193
194 #[global_allocator]
195 static ALLOCATOR: PlatformGlobalAllocator = PlatformGlobalAllocator;
196}
197
198// phase-361 W8.c — the Rust-footprint heap counter, moved here with the
199// allocator it instruments. It counts only what passes through the
200// `#[global_allocator]`; the C side's direct `nros_platform_alloc` traffic
201// (zenoh-pico's `z_malloc` etc.) is not seen, so it under-reports true heap
202// pressure. The *unified* figure is the platform's own
203// `nros_platform_heap_used_bytes` (RFC-0034 D7).
204//
205// No `#[no_mangle]` here: the C names (`nros_heap_used_bytes` …) belong to the
206// C/C++ API surface and stay exported by `nros-c` / `nros-cpp`, which read
207// these accessors. A pure-Rust image gets the counter without gaining C
208// symbols it never asked for.
209#[cfg(feature = "alloc-stats")]
210pub mod heap_stats {
211 use core::sync::atomic::{AtomicUsize, Ordering};
212
213 /// Bytes outstanding through the Rust global allocator, and the high-water
214 /// mark since boot. `Relaxed` throughout — this is instrumentation, and no
215 /// other state is ordered against it.
216 pub struct HeapStats {
217 used_bytes: AtomicUsize,
218 peak_bytes: AtomicUsize,
219 }
220
221 impl HeapStats {
222 /// Create a zeroed counter. `const` so it can back a `static`.
223 pub const fn new() -> Self {
224 Self {
225 used_bytes: AtomicUsize::new(0),
226 peak_bytes: AtomicUsize::new(0),
227 }
228 }
229
230 /// Record a successful allocation of `size` bytes and update the peak.
231 #[inline]
232 pub fn on_alloc(&self, size: usize) {
233 let used = self.used_bytes.fetch_add(size, Ordering::Relaxed) + size;
234 let _ = self.peak_bytes.fetch_max(used, Ordering::Relaxed);
235 }
236
237 /// Record a deallocation of `size` bytes.
238 #[inline]
239 pub fn on_dealloc(&self, size: usize) {
240 self.used_bytes.fetch_sub(size, Ordering::Relaxed);
241 }
242
243 /// Bytes currently outstanding.
244 #[inline]
245 pub fn used(&self) -> usize {
246 self.used_bytes.load(Ordering::Relaxed)
247 }
248
249 /// Peak outstanding bytes since boot.
250 #[inline]
251 pub fn peak(&self) -> usize {
252 self.peak_bytes.load(Ordering::Relaxed)
253 }
254 }
255
256 impl Default for HeapStats {
257 fn default() -> Self {
258 Self::new()
259 }
260 }
261
262 pub static STATS: HeapStats = HeapStats::new();
263
264 /// Bytes currently outstanding through the Rust global allocator.
265 #[inline]
266 pub fn used() -> usize {
267 STATS.used()
268 }
269
270 /// Peak outstanding bytes through the Rust global allocator since boot.
271 #[inline]
272 pub fn peak() -> usize {
273 STATS.peak()
274 }
275}