Skip to main content

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}