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, 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}