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, LinkKind, 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 error a board's link-up wait
47// reports, 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-nuttx",
64 feature = "platform-freertos",
65 feature = "platform-threadx",
66 feature = "platform-zephyr",
67))]
68pub use resolve::ConcretePlatform;
69
70// issue 1123 — the `nros_log` sink list, reachable from a crate the ENTRY
71// already declares.
72//
73// `nros_platform_cffi::log::init_default()` is the one funnel that publishes a
74// sink list (issue 0710 moved it there because the ABI it speaks is a
75// DEPENDENCY, not a feature). Every board crate calls it in its own boot path —
76// but the Zephyr pure-Rust entry is a MACRO, not a board method, and neither
77// the Zephyr example leaves (`examples/zephyr/rust/*`) nor `nros` itself dep
78// `nros-platform-cffi`, so no expansion could name it. They all dep THIS crate
79// (`nros-platform = { features = ["platform-zephyr"] }`), and every
80// `platform-*` feature already pulls `nros-platform-cffi` in, so the re-export
81// costs nothing and adds no link-time requirement that the feature did not
82// already carry.
83//
84// Deliberately NOT giving the leaves a `nros-platform-cffi` dep of their own:
85// that pushes a link-time requirement onto every consumer, which is the shape
86// issue 0710 rejected.
87#[cfg(any(
88 feature = "platform-posix",
89 feature = "platform-cffi",
90 feature = "platform-mps2-an385",
91 feature = "platform-stm32f4",
92 feature = "platform-nuttx",
93 feature = "platform-freertos",
94 feature = "platform-threadx",
95 feature = "platform-zephyr",
96))]
97pub use nros_platform_cffi::log;
98
99// Re-export every trait from the split-out `nros-platform-api` crate so
100// existing `use nros_platform::PlatformClock;` imports keep working.
101pub use nros_platform_api::*;
102
103// Link-graph anchor — relays an in-rlib `#[used]` static to the
104// `_nros_force_link_cffi` symbol that lives in `nros-platform-cffi`.
105// Downstream crates (`nros-rmw-zenoh`, the C/C++ FFI) reference
106// `__FORCE_LINK_CFFI` from their own `#[used]` static, which chains
107// up through this crate to cffi and keeps the `libnros_platform_posix.a`
108// static lib in the final link. Without the chain, rustc elides the
109// cffi rlib and every `nros_platform_*` C symbol is unresolved.
110#[cfg(feature = "platform-posix")]
111#[doc(hidden)]
112#[used]
113pub static __FORCE_LINK_CFFI: extern "C" fn() = nros_platform_cffi::_nros_force_link_cffi;
114
115/// Issue 1732 — a termination signal ENDS a hosted image's spin, so the boot
116/// funnel can close the RMW session before the process exits.
117///
118/// The one spelling for every hosted funnel: the Rust board
119/// (`nros-board-linux`, both its single-executor and its tiered path), the
120/// `nros::main!` hosted spin loops, and the native C/C++ runners in
121/// `nros-cpp`. The guard itself is C, in the POSIX port
122/// (`nros_posix_install_termination_guard` in `nros-platform-posix`), because
123/// that is the one translation unit every hosted image already links.
124///
125/// The contract, all of it in the port:
126///
127/// * installed only over the DEFAULT disposition of SIGTERM / SIGINT — an
128/// application's own handler and `SIG_IGN` are left alone;
129/// * the handler only sets a flag; nothing is torn down in a signal frame;
130/// * a SECOND signal ends the image by the default action, so a wedged
131/// teardown is still stoppable without SIGKILL.
132///
133/// On a build without the POSIX port nothing is installed and
134/// [`requested`](termination::requested) is always `false`, so a loop that
135/// polls it costs a constant.
136pub mod termination {
137 #[cfg(feature = "platform-posix")]
138 unsafe extern "C" {
139 fn nros_posix_install_termination_guard() -> i32;
140 fn nros_posix_termination_requested() -> i32;
141 }
142
143 /// Install the guard; returns how many of SIGTERM / SIGINT carry it (0 when
144 /// the application already handles both, or on a non-POSIX build).
145 /// Idempotent. Call it before the session opens, so a signal that arrives
146 /// during registration is not lost.
147 pub fn install_guard() -> usize {
148 #[cfg(feature = "platform-posix")]
149 {
150 // SAFETY: plain C call; `sigaction` on the process's own handlers.
151 let n = unsafe { nros_posix_install_termination_guard() };
152 if n > 0 { n as usize } else { 0 }
153 }
154 #[cfg(not(feature = "platform-posix"))]
155 {
156 0
157 }
158 }
159
160 /// `true` once a guarded termination signal has arrived. A spin loop that
161 /// sees it returns, and its funnel closes the session.
162 pub fn requested() -> bool {
163 #[cfg(feature = "platform-posix")]
164 {
165 // SAFETY: reads a `sig_atomic_t`.
166 unsafe { nros_posix_termination_requested() != 0 }
167 }
168 #[cfg(not(feature = "platform-posix"))]
169 {
170 false
171 }
172 }
173}
174
175/// The kernel's deadline reservation on the calling thread (issue 2018).
176///
177/// The reservation is C, in the POSIX port
178/// (`nros_posix_apply_current_deadline` in `nros-platform-posix`), for the
179/// reason [`termination`] gives. It is reached only on a build that selected
180/// that port. A `cfg(unix)` gate is not enough: Zephyr `native_sim`, and the
181/// host tiers of ThreadX and FreeRTOS, target a Linux triple without the POSIX
182/// port. Their port defines no `nros_posix_*` symbol, so a reference made the
183/// linker take the POSIX `platform.o` from whichever archive carried one, and
184/// every other symbol in it then collided with the image's own port
185/// (issue 2032; issue 1776 had the same shape).
186pub mod sched {
187 use core::ffi::c_char;
188
189 #[cfg(feature = "platform-posix")]
190 unsafe extern "C" {
191 fn nros_posix_apply_current_deadline(
192 name: *const c_char,
193 tier_class: *const c_char,
194 budget_us: u64,
195 period_us: u64,
196 deadline_us: u64,
197 ) -> i32;
198 }
199
200 /// Ask the kernel to reserve `budget_us` every `period_us` for the calling
201 /// thread. Returns `true` when the kernel took it, and `false` otherwise.
202 /// That covers a tier class that asks for no reservation, a refusal (no
203 /// `CAP_SYS_NICE`), and a build whose port has no such kernel facility.
204 /// On `false` the caller enforces the budget itself.
205 ///
206 /// # Safety
207 ///
208 /// `name` and `tier_class` are NUL-terminated strings or null.
209 pub unsafe fn apply_current_deadline(
210 name: *const c_char,
211 tier_class: *const c_char,
212 budget_us: u64,
213 period_us: u64,
214 deadline_us: u64,
215 ) -> bool {
216 #[cfg(feature = "platform-posix")]
217 {
218 // SAFETY: the caller's contract on the two strings.
219 unsafe {
220 nros_posix_apply_current_deadline(
221 name,
222 tier_class,
223 budget_us,
224 period_us,
225 deadline_us,
226 ) == 1
227 }
228 }
229 #[cfg(not(feature = "platform-posix"))]
230 {
231 let _ = (name, tier_class, budget_us, period_us, deadline_us);
232 false
233 }
234 }
235}
236
237// ============================================================================
238// Phase 248 C7 — Zephyr platform helper (relocated from `nros::platform::zephyr`)
239// ============================================================================
240/// Zephyr-specific platform helpers.
241///
242/// On Zephyr's `native_sim`, the default network interface is assigned an IPv4
243/// address at boot, but the underlying TAP link reports `net_if_is_up() == false`
244/// for ~100–200 ms until the host side is ready. Opening a zenoh session before
245/// that returns `TransportError::ConnectionFailed`. Call [`zephyr::wait_network`]
246/// before `Executor::open`. Mirrors the `nros_platform_zephyr_wait_network()` C
247/// helper the C/C++ examples use; the symbol is RMW-independent (defined in
248/// `nros-platform-zephyr`, compiled in every RMW build). Equivalent to
249/// `nros-board-zephyr`'s `ZephyrBoard::wait_link_up`.
250// phase-391 W3 — the rlsf arena behind Zephyr's C allocation funnel.
251#[cfg(feature = "platform-zephyr")]
252pub mod zephyr_heap;
253
254#[cfg(feature = "platform-zephyr")]
255pub mod zephyr {
256 unsafe extern "C" {
257 fn nros_platform_zephyr_wait_network(timeout_ms: i32) -> i32;
258 }
259
260 /// The default network interface did not come up within the timeout.
261 #[derive(Debug, Clone, Copy, PartialEq, Eq)]
262 pub struct NetworkTimeout;
263
264 /// Block until the default Zephyr network interface is operational, or the
265 /// timeout expires. `Ok(())` on link-up, `Err(NetworkTimeout)` on timeout.
266 pub fn wait_network(timeout_ms: i32) -> Result<(), NetworkTimeout> {
267 // SAFETY: `nros_platform_zephyr_wait_network` has no preconditions beyond
268 // being called from a Zephyr thread context — always true in a Zephyr app.
269 let ret = unsafe { nros_platform_zephyr_wait_network(timeout_ms) };
270 if ret == 0 {
271 Ok(())
272 } else {
273 Err(NetworkTimeout)
274 }
275 }
276}
277
278// ============================================================================
279// Phase 71.27 — opt-in `#[global_allocator]`
280// ============================================================================
281//
282// On bare-metal / RTOS targets DDS + heapless futures need a real
283// heap. Each `nros-platform-*` crate already implements `PlatformAlloc`
284// against its native heap (`pvPortMalloc` on FreeRTOS,
285// `tx_byte_allocate` on ThreadX, `kmm_malloc` on NuttX,
286// `k_malloc` on Zephyr, libc `malloc` on POSIX). This module promotes
287// that trait impl into a `#[global_allocator]` so application crates
288// don't have to write per-platform glue.
289//
290// Off by default — `platform-posix` users link against libstd's
291// allocator. Enable via `nros-platform/global-allocator` in the
292// example crate's `Cargo.toml` to wire it in.
293
294// phase-361 W8.c / issue 0594 — this is the ONE `#[global_allocator]` in the
295// tree. `nros-c` used to define a second one under an identical gate, reaching
296// the same heap by a different route (a direct `extern "C" nros_platform_alloc`
297// rather than the trait), and the two were kept apart only by a manifest
298// comment: `nros-c` deps `nros-platform` non-optionally, so any image that
299// enabled both features got a duplicate lang item. `nros-c/global-allocator`
300// now forwards here, which makes the duplication impossible rather than
301// merely discouraged — cargo unifies one crate's one feature into one unit.
302//
303// This adapter covers BOTH link shapes, which the `nros-c` copy did not:
304// every `platform-*` feature resolves `ConcretePlatform` to `CffiPlatform`
305// (see `resolve.rs`), whose `PlatformAlloc` impl IS `nros_platform_alloc`, and
306// the bare-metal Rust crates (mps2-an385, stm32f4, esp32-qemu) reach their own
307// arena through the same trait. One API, one arena, per RFC-0034 D6.
308#[cfg(all(feature = "global-allocator", not(feature = "std")))]
309mod global_allocator {
310 use core::{
311 alloc::{GlobalAlloc, Layout},
312 ffi::c_void,
313 };
314
315 use crate::ConcretePlatform;
316 use nros_platform_api::PlatformAlloc;
317
318 /// Alignment the platform ABI guarantees. `nros_platform_alloc` has no
319 /// alignment parameter, and every port behind it returns memory aligned
320 /// for the widest scalar — 8 bytes on every target nano-ros builds for.
321 const PLATFORM_ALIGN: usize = 8;
322
323 /// `GlobalAlloc` adapter over `<ConcretePlatform as PlatformAlloc>`.
324 pub struct PlatformGlobalAllocator;
325
326 unsafe impl GlobalAlloc for PlatformGlobalAllocator {
327 unsafe fn alloc(&self, layout: Layout) -> *mut u8 {
328 // phase-361 W8.c — never return under-aligned memory: the previous
329 // `let _ = layout.align();` produced UB no build could see.
330 //
331 // An over-aligned request used to FAIL here (null, so
332 // `handle_alloc_error`). phase-397 W2 made the executor's
333 // `WakeCtx` `repr(align(64))` and put it in an `Arc`, so every
334 // embedded image with an executor panicked at boot with "memory
335 // allocation of 128 bytes failed" before registering a node
336 // (S32K344, Zephyr 4.4). The ABI still cannot express alignment,
337 // so an over-aligned block is carved out of a larger one instead
338 // (`over_align`).
339 let p = if layout.align() > PLATFORM_ALIGN {
340 match super::over_align::raw_size(layout) {
341 // SAFETY: `raw_size` sized the raw block for `place`.
342 Some(raw_size) => unsafe {
343 super::over_align::place(
344 <ConcretePlatform as PlatformAlloc>::alloc(raw_size) as *mut u8,
345 layout,
346 )
347 },
348 None => core::ptr::null_mut(),
349 }
350 } else {
351 <ConcretePlatform as PlatformAlloc>::alloc(layout.size()) as *mut u8
352 };
353 #[cfg(feature = "alloc-stats")]
354 if !p.is_null() {
355 super::heap_stats::STATS.on_alloc(layout.size());
356 }
357 p
358 }
359
360 unsafe fn dealloc(&self, ptr: *mut u8, layout: Layout) {
361 let raw = if layout.align() > PLATFORM_ALIGN {
362 // SAFETY: `ptr` came from the over-aligned branch of `alloc`
363 // (same layout), which stored the raw block below it.
364 unsafe { super::over_align::raw_of(ptr) }
365 } else {
366 ptr
367 };
368 <ConcretePlatform as PlatformAlloc>::dealloc(raw as *mut c_void);
369 #[cfg(feature = "alloc-stats")]
370 super::heap_stats::STATS.on_dealloc(layout.size());
371 }
372 }
373
374 #[global_allocator]
375 static ALLOCATOR: PlatformGlobalAllocator = PlatformGlobalAllocator;
376}
377
378/// An over-aligned block inside an 8-aligned one (the global allocator's
379/// `align > 8` path). The raw block is `size + align` bytes; the returned
380/// pointer is the first `align` boundary at least one `usize` past its start,
381/// and that `usize` just below it keeps the raw pointer for `dealloc`.
382///
383/// Room: the raw pointer is 8-aligned and `align` is a power of two above 8,
384/// so the gap to the boundary is a multiple of 8 in `8..=align`: the header
385/// fits in it and `gap + size <= align + size`.
386#[cfg_attr(
387 not(all(feature = "global-allocator", not(feature = "std"))),
388 allow(dead_code)
389)]
390mod over_align {
391 use core::alloc::Layout;
392
393 const HEADER: usize = core::mem::size_of::<usize>();
394
395 /// The raw block's size for `layout`, or `None` on overflow.
396 pub(crate) fn raw_size(layout: Layout) -> Option<usize> {
397 layout.size().checked_add(layout.align())
398 }
399
400 /// The aligned pointer inside `raw` (null stays null).
401 ///
402 /// # Safety
403 /// `raw` is null or an 8-aligned block of `raw_size(layout)` bytes, and
404 /// `layout.align() > 8`.
405 pub(crate) unsafe fn place(raw: *mut u8, layout: Layout) -> *mut u8 {
406 if raw.is_null() {
407 return raw;
408 }
409 let align = layout.align();
410 let start = raw as usize;
411 let aligned = (start + HEADER + align - 1) & !(align - 1);
412 let p = raw.wrapping_add(aligned - start);
413 // SAFETY: `aligned - HEADER >= start`, and it is 8-aligned (both
414 // `aligned` and `start` are), inside the raw block.
415 unsafe { (p.sub(HEADER) as *mut usize).write(start) };
416 p
417 }
418
419 /// The raw block `place` carved `p` out of.
420 ///
421 /// # Safety
422 /// `p` came from `place`.
423 pub(crate) unsafe fn raw_of(p: *mut u8) -> *mut u8 {
424 // SAFETY: `place` wrote the raw pointer there.
425 let start = unsafe { (p.sub(HEADER) as *const usize).read() };
426 p.wrapping_sub(p as usize - start)
427 }
428
429 #[cfg(test)]
430 mod tests {
431 extern crate std;
432 use super::*;
433 use std::{vec, vec::Vec};
434
435 #[test]
436 fn every_offset_fits_and_round_trips() {
437 for align in [16usize, 32, 64, 128] {
438 for size in [1usize, 8, 63, 64, 128, 200] {
439 let layout = Layout::from_size_align(size, align).unwrap();
440 let n = raw_size(layout).unwrap();
441 // Every 8-aligned start an align-period can present.
442 let mut buf: Vec<u64> = vec![0; (n + 2 * align) / 8 + 1];
443 let base = buf.as_mut_ptr() as *mut u8;
444 for off in (0..align).step_by(8) {
445 let raw = base.wrapping_add(off);
446 let p = unsafe { place(raw, layout) };
447 assert_eq!(p as usize % align, 0);
448 assert!(p as usize - raw as usize >= HEADER);
449 assert!(p as usize + size <= raw as usize + n);
450 assert_eq!(unsafe { raw_of(p) }, raw);
451 }
452 }
453 }
454 }
455
456 #[test]
457 fn null_stays_null() {
458 let layout = Layout::from_size_align(128, 64).unwrap();
459 assert!(unsafe { place(core::ptr::null_mut(), layout) }.is_null());
460 }
461 }
462}
463
464// phase-361 W8.c — the Rust-footprint heap counter, moved here with the
465// allocator it instruments. It counts only what passes through the
466// `#[global_allocator]`; the C side's direct `nros_platform_alloc` traffic
467// (zenoh-pico's `z_malloc` etc.) is not seen, so it under-reports true heap
468// pressure. The *unified* figure is the platform's own
469// `nros_platform_heap_used_bytes` (RFC-0034 D7).
470//
471// No `#[no_mangle]` here: the C names (`rclc_heap_used_bytes` …) belong to the
472// C/C++ API surface and stay exported by `nros-c` / `nros-cpp`, which read
473// these accessors. A pure-Rust image gets the counter without gaining C
474// symbols it never asked for.
475#[cfg(feature = "alloc-stats")]
476pub mod heap_stats {
477 use core::sync::atomic::{AtomicUsize, Ordering};
478
479 /// Bytes outstanding through the Rust global allocator, and the high-water
480 /// mark since boot. `Relaxed` throughout — this is instrumentation, and no
481 /// other state is ordered against it.
482 pub struct HeapStats {
483 used_bytes: AtomicUsize,
484 peak_bytes: AtomicUsize,
485 }
486
487 impl HeapStats {
488 /// Create a zeroed counter. `const` so it can back a `static`.
489 pub const fn new() -> Self {
490 Self {
491 used_bytes: AtomicUsize::new(0),
492 peak_bytes: AtomicUsize::new(0),
493 }
494 }
495
496 /// Record a successful allocation of `size` bytes and update the peak.
497 #[inline]
498 pub fn on_alloc(&self, size: usize) {
499 let used = self.used_bytes.fetch_add(size, Ordering::Relaxed) + size;
500 let _ = self.peak_bytes.fetch_max(used, Ordering::Relaxed);
501 }
502
503 /// Record a deallocation of `size` bytes.
504 #[inline]
505 pub fn on_dealloc(&self, size: usize) {
506 self.used_bytes.fetch_sub(size, Ordering::Relaxed);
507 }
508
509 /// Bytes currently outstanding.
510 #[inline]
511 pub fn used(&self) -> usize {
512 self.used_bytes.load(Ordering::Relaxed)
513 }
514
515 /// Peak outstanding bytes since boot.
516 #[inline]
517 pub fn peak(&self) -> usize {
518 self.peak_bytes.load(Ordering::Relaxed)
519 }
520 }
521
522 impl Default for HeapStats {
523 fn default() -> Self {
524 Self::new()
525 }
526 }
527
528 pub static STATS: HeapStats = HeapStats::new();
529
530 /// Bytes currently outstanding through the Rust global allocator.
531 #[inline]
532 pub fn used() -> usize {
533 STATS.used()
534 }
535
536 /// Peak outstanding bytes through the Rust global allocator since boot.
537 #[inline]
538 pub fn peak() -> usize {
539 STATS.peak()
540 }
541}