Skip to main content

nros_platform_api/
lib.rs

1//! Platform capability sub-traits for nros.
2//!
3//! This crate exists to break the dependency cycle between
4//! `nros-platform` (which depends on every platform crate via its
5//! feature-gated `ConcretePlatform` resolver) and the platform crates
6//! themselves (which need to implement these traits on their ZSTs).
7//! It contains only trait definitions — no implementations, no
8//! dependencies — so platform crates can take a build-time dep on it
9//! without creating a cycle back through `nros-platform`.
10//!
11//! `nros-platform` re-exports everything from this crate, so downstream
12//! code that writes `use nros_platform::PlatformClock;` continues to
13//! work unchanged.
14//!
15//! Each trait covers an independent system capability. Platform
16//! implementations pick which traits to implement based on what the
17//! hardware/RTOS provides. RMW shim crates declare trait bounds for
18//! the capabilities they need.
19
20#![no_std]
21//!
22//! # Naming convention
23//!
24//! Method names drop redundant prefixes when the trait name already
25//! supplies the namespace — e.g., `PlatformTcp::open` rather than
26//! `PlatformTcp::tcp_open`. Dispatch is always through a qualified
27//! path (`<ConcretePlatform as PlatformTcp>::open(...)`), so
28//! trait-to-trait collisions (PlatformTcp::open vs PlatformUdp::open)
29//! are disambiguated at the call site without needing a prefix on the
30//! trait method itself.
31//!
32//! Three categories still keep sub-namespace prefixes internally:
33//!
34//! * `PlatformThreading` — `mutex_*`, `condvar_*`, `task_*` because the
35//!   trait bundles three independent primitive families and unprefixed
36//!   `init` / `drop` would be ambiguous *within* the trait itself.
37//! * `PlatformUdpMulticast` — `mcast_*` because these methods have
38//!   different signatures from `PlatformUdp`'s same-name methods; keeping
39//!   the prefix makes call sites that use both traits self-documenting.
40//! * The `close` method appears on both `PlatformTcp` and
41//!   `PlatformSocketHelpers` — the first is TCP teardown, the second is
42//!   zenoh-pico's generic "shutdown + close" helper. Both live unprefixed
43//!   in their respective traits; call sites disambiguate via the
44//!   qualified path.
45//!
46//! # Status (Phase 84.F4)
47//!
48//! The platform ZSTs (`PosixPlatform`, `ZephyrPlatform`, etc.) do **not**
49//! currently implement these traits — every platform exposes its methods
50//! as *inherent* `impl Platform { fn foo() {} }` blocks, and shim crates
51//! dispatch by name match. 84.F4 migrates each platform to `impl
52//! PlatformX for Platform { fn foo() {} }` one trait at a time, with the
53//! shims switching to `<P as PlatformX>::foo()`. Until that work is
54//! complete the traits here are a target specification.
55
56use core::ffi::{c_int, c_void};
57
58pub mod boot_config;
59pub mod wake;
60pub mod xorshift32;
61
62pub use boot_config::{
63    BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BakedBootConfig,
64    NROS_BOOT_CONFIG_MAGIC, NROS_BOOT_CONFIG_VERSION,
65};
66pub use wake::{WAKE_STORAGE_ALIGN, WAKE_STORAGE_BYTES, Wake, WakeInitError, WakeReason};
67
68// ============================================================================
69// Clock (required by all RMW backends)
70// ============================================================================
71
72/// Monotonic clock.
73///
74/// The most critical platform primitive. Must be backed by a hardware timer
75/// or OS tick — never by a software counter that only advances when polled.
76pub trait PlatformClock {
77    /// Returns monotonic time in milliseconds.
78    fn clock_ms() -> u64;
79
80    /// Returns monotonic time in microseconds.
81    fn clock_us() -> u64;
82}
83
84// ============================================================================
85// Heap allocation (zenoh-pico requires ~64 KB heap)
86// ============================================================================
87
88/// Heap memory allocation.
89pub trait PlatformAlloc {
90    /// Allocate `size` bytes. Returns null on failure.
91    fn alloc(size: usize) -> *mut c_void;
92
93    /// Reallocate a previously allocated block. Returns null on failure.
94    fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
95
96    /// Free a previously allocated block.
97    fn dealloc(ptr: *mut c_void);
98
99    /// Phase 230 Z5 / RFC-0034 D7 — bytes currently allocated from the
100    /// platform heap. The true unified figure where the platform owns one
101    /// kernel heap shared by the C side (`alloc`) and the Rust
102    /// `#[global_allocator]`. Default `0` = "unknown / not instrumented".
103    fn heap_used_bytes() -> usize {
104        0
105    }
106
107    /// Total managed heap size in bytes (used + free), or `0` if unknown.
108    fn heap_total_bytes() -> usize {
109        0
110    }
111}
112
113// ============================================================================
114// Sleep / delay
115// ============================================================================
116
117/// Sleep primitives.
118///
119/// On bare-metal with smoltcp, implementations should poll the network
120/// stack during busy-wait sleep to avoid missing packets.
121pub trait PlatformSleep {
122    /// Sleep for the given number of microseconds.
123    fn sleep_us(us: usize);
124
125    /// Sleep for the given number of milliseconds.
126    fn sleep_ms(ms: usize);
127
128    /// Sleep for the given number of seconds.
129    fn sleep_s(s: usize);
130}
131
132// ============================================================================
133// Cooperative yield
134// ============================================================================
135
136/// Scheduler yield primitive.
137///
138/// Used inside `socket_wait_event` and similar "let another task make
139/// progress" points where the caller isn't actually waiting for I/O
140/// readability — the background read task already owns that — it just
141/// needs to relinquish the CPU so the real waiter can run.
142///
143/// Prior to Phase 77.22 each backend hand-rolled its own 1-ms busy
144/// sleep (`libc::usleep(1000)`, `vTaskDelay(1)`, `tx_thread_sleep(1)`,
145/// `k_usleep(1000)`, `select(.., 1 ms)`) — all with slightly different
146/// units and no common home.
147///
148/// **ISR-safety**: on the hosted-RTOS backends (FreeRTOS / NuttX /
149/// Zephyr / ThreadX) the underlying primitives panic or error when
150/// invoked from an ISR. Don't call `yield_now()` from an interrupt
151/// handler.
152///
153/// **Bare-metal has no real yield**: there's nothing to yield *to*.
154/// The default bare-metal impl is `core::hint::spin_loop()` (a pure
155/// CPU hint: emits `YIELD` / `PAUSE` / `WFE` depending on the arch,
156/// safe everywhere). Board crates that have armed an IRQ source may
157/// opt in to deep idle (`wfi`) via a separate `BoardIdle` hook — not
158/// part of this trait because calling `wfi` without an IRQ source
159/// deadlocks.
160pub trait PlatformYield {
161    /// Relinquish the CPU long enough for another task / thread to run.
162    ///
163    /// Non-blocking in the sleep sense — returns as soon as the
164    /// scheduler has had an opportunity to pick a different runnable.
165    fn yield_now();
166}
167
168// ============================================================================
169// Phase 110.D — `PlatformScheduler` (per-thread OS scheduling policy)
170// ============================================================================
171
172/// Errors returned by [`PlatformScheduler`] entry points.
173#[derive(Debug, Clone, Copy, PartialEq, Eq)]
174pub enum SchedError {
175    /// The active platform doesn't expose this control surface
176    /// (e.g. bare-metal with no scheduler, or an RTOS without an
177    /// affinity API).
178    Unsupported,
179    /// The requested policy is valid for this platform but the
180    /// numeric arguments fall outside the platform's accepted range.
181    OutOfRange,
182    /// A platform-specific syscall / kernel call failed; check
183    /// `errno` (POSIX) or the RTOS error code for details.
184    KernelError,
185}
186
187/// Per-thread OS scheduling policy.
188///
189/// `Fifo` / `RoundRobin` / `Deadline` / `Sporadic` map to platform-
190/// native scheduling classes when available. `Platform(...)` is the
191/// escape hatch for RTOS-specific knobs (e.g. ThreadX preempt-
192/// threshold) that don't map cleanly into the portable variants.
193///
194/// User-facing API (`Executor::open_threaded`) takes the abstract
195/// [`Priority`-like](crate) values and `PlatformScheduler` translates
196/// them into platform-native numerics — direction-flipped priority
197/// (Zephyr / ThreadX low-numeric = high-priority vs POSIX / FreeRTOS
198/// high-numeric = high-priority) handled internally. Phase 110.D.
199#[derive(Debug, Clone, Copy, PartialEq, Eq)]
200pub enum SchedPolicy {
201    /// POSIX `SCHED_FIFO`. `os_pri` is the platform-native numeric
202    /// priority; out-of-range values surface `SchedError::OutOfRange`.
203    Fifo { os_pri: u8 },
204    /// POSIX `SCHED_RR` with `quantum_ms` time slice.
205    RoundRobin { os_pri: u8, quantum_ms: u32 },
206    /// Linux `SCHED_DEADLINE` (sched_setattr). All values nanoseconds.
207    Deadline {
208        runtime_ns: u64,
209        period_ns: u64,
210        deadline_ns: u64,
211    },
212    /// NuttX `SCHED_SPORADIC` server. Phase 110.E.
213    Sporadic {
214        budget_us: u32,
215        period_us: u32,
216        hi_pri: u8,
217        lo_pri: u8,
218    },
219}
220
221// Per-thread OS-scheduling control surface.
222//
223// Each platform implements as much of this trait as its kernel
224// supports. `bare-metal` returns [`SchedError::Unsupported`] from
225// every entry point — there is no scheduler to talk to.
226//
227// Phase 110.D wires this for Linux + NuttX (via POSIX) at v1; Zephyr
228// / FreeRTOS / ThreadX impls land alongside per-RTOS bring-up.
229
230// ============================================================================
231// Phase 110.E.b — `PlatformTimer` (ISR-driven periodic refill)
232// ============================================================================
233
234/// Errors returned by [`PlatformTimer`] entry points.
235#[derive(Debug, Clone, Copy, PartialEq, Eq)]
236pub enum TimerError {
237    /// The active platform doesn't expose a periodic-timer surface
238    /// (e.g. bare-metal without a board-side `SysTickHook`).
239    Unsupported,
240    /// `period_us` is below the platform's minimum timer resolution
241    /// or above its maximum representable interval.
242    OutOfRange,
243    /// A platform-specific syscall / kernel call failed; check
244    /// `errno` (POSIX) or the RTOS error code for details.
245    KernelError,
246}
247
248/// Periodic timer for ISR-driven sporadic-server budget refill
249/// (Phase 110.E.b). The trait factors out the per-platform timer
250/// surface so the executor (`nros-node`) can register an atomic
251/// refill callback without becoming generic over the platform —
252/// see `docs/design/0017-platform-timer.md`.
253///
254/// Default `create_periodic` returns [`TimerError::Unsupported`] so
255/// platforms without a timer surface inherit safe behavior; `destroy`
256/// is a no-op default for the same reason.
257pub trait PlatformTimer {
258    /// Opaque per-platform handle (FreeRTOS `TimerHandle_t`,
259    /// Zephyr `*mut k_timer`, ThreadX `*mut TX_TIMER`, POSIX
260    /// `timer_t`). Must be `Send + Sync + 'static` so the executor
261    /// can stash it across thread boundaries.
262    type TimerHandle: Send + Sync + 'static;
263
264    /// Register a periodic timer that fires `callback(user_data)`
265    /// every `period_us` microseconds. Returns the platform-native
266    /// handle for later destruction.
267    ///
268    /// # Safety
269    ///
270    /// `user_data` must outlive the returned handle. The callback is
271    /// invoked from a platform-defined timer context (direct ISR on
272    /// Zephyr / bare-metal, deferred via `xTimerPendFunctionCall` on
273    /// FreeRTOS, signal handler on POSIX) — bodies should be
274    /// short and use atomic ops only.
275    fn create_periodic(
276        _period_us: u32,
277        _callback: extern "C" fn(*mut c_void),
278        _user_data: *mut c_void,
279    ) -> Result<Self::TimerHandle, TimerError> {
280        Err(TimerError::Unsupported)
281    }
282
283    /// Cancel + free the timer. Idempotent on already-destroyed
284    /// handles. Must drain in-flight callback invocations before
285    /// returning so the user_data pointer is no longer accessed.
286    fn destroy(_handle: Self::TimerHandle) {}
287
288    /// Phase 110.E.b follow-up — register a one-shot timer that
289    /// fires `callback(user_data)` exactly once after `timeout_us`
290    /// microseconds. Used by per-callback runtime measurement: arm
291    /// just before dispatch with `timeout_us = budget_us`; on
292    /// callback completion call `cancel`. If the timer fires first
293    /// the callback overran its budget.
294    ///
295    /// Default returns `Unsupported` so platforms without a oneshot
296    /// surface inherit safe behavior.
297    fn create_oneshot(
298        _timeout_us: u32,
299        _callback: extern "C" fn(*mut c_void),
300        _user_data: *mut c_void,
301    ) -> Result<Self::TimerHandle, TimerError> {
302        Err(TimerError::Unsupported)
303    }
304
305    /// Phase 110.E.b follow-up — cancel a previously-armed oneshot
306    /// timer. Returns `true` when the cancellation prevented the
307    /// callback from firing, `false` when the callback already
308    /// fired (or the timer was already cancelled). Default is a
309    /// no-op returning `false`.
310    fn cancel(_handle: &mut Self::TimerHandle) -> bool {
311        false
312    }
313}
314
315pub trait PlatformScheduler {
316    /// Apply the requested policy to the calling thread.
317    ///
318    /// Default returns [`SchedError::Unsupported`] so single-core bare-
319    /// metal targets and RTOS impls without per-thread scheduling
320    /// pickup the no-op behavior automatically. Platforms with a
321    /// real scheduler override this.
322    fn set_current_thread_policy(_p: SchedPolicy) -> Result<(), SchedError> {
323        Err(SchedError::Unsupported)
324    }
325
326    /// Cooperative yield. Same semantics as
327    /// [`PlatformYield::yield_now`]; mirrored here so consumers don't
328    /// need to import both traits when only the scheduler control
329    /// surface is in scope. Default is a `core::hint::spin_loop()`.
330    fn yield_now() {
331        core::hint::spin_loop();
332    }
333
334    /// Pin the calling thread to the CPUs whose bit is set in
335    /// `cpu_mask`. Default returns [`SchedError::Unsupported`] —
336    /// platforms with affinity APIs override.
337    fn set_affinity(_cpu_mask: u32) -> Result<(), SchedError> {
338        Err(SchedError::Unsupported)
339    }
340}
341
342// ============================================================================
343// Random number generation
344// ============================================================================
345
346/// Pseudo-random number generation.
347///
348/// A simple xorshift32 PRNG is sufficient. Seed with hardware entropy
349/// (RNG peripheral, ADC noise, wall-clock time) during platform init.
350pub trait PlatformRandom {
351    fn random_u8() -> u8;
352    fn random_u16() -> u16;
353    fn random_u32() -> u32;
354    fn random_u64() -> u64;
355
356    /// Fill buffer with random bytes.
357    fn random_fill(buf: *mut c_void, len: usize);
358}
359
360// ============================================================================
361// Wall-clock time (for logging, not timing-critical)
362// ============================================================================
363
364/// Wall-clock / system time.
365///
366/// Used for logging timestamps and `z_time_now_as_str()`.
367/// On bare-metal without an RTC, return monotonic time or zeros.
368///
369/// The two-function `time_since_epoch_*` split (instead of returning a
370/// struct) was chosen to match the shape that zenoh-pico's C headers
371/// want across the FFI boundary — zpico-platform-shim forwards each
372/// of these directly to a `_z_time_*` symbol, so collapsing them into
373/// a Rust struct would require the shim to decompose the struct on
374/// every call.
375pub trait PlatformTime {
376    /// Returns system time in milliseconds.
377    fn time_now_ms() -> u64;
378
379    /// Seconds component of wall-clock time since the Unix epoch.
380    fn time_since_epoch_secs() -> u32;
381
382    /// Sub-second nanoseconds component of wall-clock time since the
383    /// Unix epoch (i.e. the nanosecond remainder after the seconds are
384    /// stripped; always in `0..1_000_000_000`).
385    fn time_since_epoch_nanos() -> u32;
386}
387
388// ============================================================================
389// Threading (multi-threaded platforms only)
390// ============================================================================
391//
392// Handle types are opaque `*mut c_void` to match the shape zenoh-pico
393// passes across the FFI boundary. The original draft had typed wrappers
394// (`TaskHandle`, `MutexHandle`, ...) but every shipped platform used
395// `*mut c_void` internally and the shim never materialised the typed
396// forms — keeping them in the trait would have required a pointless
397// cast at every impl site. (F4.1 / F4.5 decision, 2026-04-24.)
398//
399// Mutex / condvar / task method names keep their sub-namespace prefix
400// (`mutex_*`, `condvar_*`, `task_*`) because the trait bundles three
401// independent primitive families and unprefixed `init` / `drop` would
402// be ambiguous *within* the trait itself.
403
404/// Threading primitives: tasks, mutexes, and condition variables.
405///
406/// # Threading
407///
408/// Mutex / condvar operations must be safe under concurrent callers.
409/// **Recursive mutex (`mutex_rec_*`) must support same-thread
410/// re-entrancy** — zenoh-pico relies on this; a non-recursive mutex
411/// backing `mutex_rec_*` deadlocks under load.
412///
413/// # ISR-safety
414///
415/// **None of these methods are ISR-safe** on hosted RTOSes
416/// (FreeRTOS / NuttX / Zephyr / ThreadX) — the underlying primitives
417/// panic or error when invoked from an ISR. Only `core::hint::spin_loop()`
418/// in [`PlatformYield::yield_now`] is.
419///
420/// For single-threaded platforms (bare-metal), all operations should
421/// be no-ops returning `0`, except [`task_init`](Self::task_init)
422/// which should return `-1`.
423pub trait PlatformThreading {
424    // -- Tasks --
425
426    /// Spawn a new task. `task` is opaque caller-provided storage;
427    /// `attr` carries scheduling hints (priority, stack size) or
428    /// `null` for defaults; `entry` is the task entry point; `arg`
429    /// is forwarded to `entry`. Returns `0` on success, non-zero
430    /// on failure.
431    fn task_init(
432        task: *mut c_void,
433        attr: *mut c_void,
434        entry: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
435        arg: *mut c_void,
436    ) -> i8;
437
438    /// Block until `task` exits. Cleans up the task storage on
439    /// success.
440    fn task_join(task: *mut c_void) -> i8;
441    /// Mark `task` as detached — its storage is reclaimed on exit
442    /// without a join.
443    fn task_detach(task: *mut c_void) -> i8;
444    /// Request `task` to terminate at the next cancellation point.
445    /// Cooperative.
446    fn task_cancel(task: *mut c_void) -> i8;
447    /// Terminate the calling task immediately. Does not return.
448    fn task_exit();
449    /// Free the task storage allocated by `task_init`.
450    fn task_free(task: *mut *mut c_void);
451
452    // -- Mutex --
453
454    /// Initialise a non-recursive mutex in caller-provided storage.
455    fn mutex_init(m: *mut c_void) -> i8;
456    /// Tear down a non-recursive mutex.
457    fn mutex_drop(m: *mut c_void) -> i8;
458    /// Lock; block if held.
459    fn mutex_lock(m: *mut c_void) -> i8;
460    /// Try to lock; non-zero return immediately if held.
461    fn mutex_try_lock(m: *mut c_void) -> i8;
462    /// Unlock; only the owning thread may call this.
463    fn mutex_unlock(m: *mut c_void) -> i8;
464
465    // -- Recursive mutex --
466
467    /// Initialise a *recursive* mutex (same-thread re-entrancy
468    /// permitted). Required by zenoh-pico.
469    fn mutex_rec_init(m: *mut c_void) -> i8;
470    /// Tear down a recursive mutex.
471    fn mutex_rec_drop(m: *mut c_void) -> i8;
472    /// Lock; re-entry from the owning thread must succeed.
473    fn mutex_rec_lock(m: *mut c_void) -> i8;
474    /// Try to lock; same re-entry semantics as `mutex_rec_lock`.
475    fn mutex_rec_try_lock(m: *mut c_void) -> i8;
476    /// Unlock; releases when the lock count returns to zero.
477    fn mutex_rec_unlock(m: *mut c_void) -> i8;
478
479    // -- Condition variables --
480
481    /// Initialise a condition variable in caller-provided storage.
482    fn condvar_init(cv: *mut c_void) -> i8;
483    /// Tear down a condition variable.
484    fn condvar_drop(cv: *mut c_void) -> i8;
485    /// Wake one waiter on the condition variable.
486    fn condvar_signal(cv: *mut c_void) -> i8;
487    /// Wake all waiters on the condition variable.
488    fn condvar_signal_all(cv: *mut c_void) -> i8;
489    /// Phase 124.B.7.a — ISR-safe variant of [`Self::condvar_signal`].
490    /// Callable from interrupt / signal-handler context. Backends
491    /// implement via async-signal-safe primitives (POSIX:
492    /// `eventfd` write forwarded by a worker thread; RTOS:
493    /// `xSemaphoreGiveFromISR`, `tx_event_flags_set` from ISR,
494    /// `k_sem_give` from ISR). Returns non-zero when the backend
495    /// has no ISR-safe path — caller can fall back to
496    /// `condvar_signal` (with the obvious latency cost).
497    ///
498    /// Default body: forward to `condvar_signal`. POSIX and other
499    /// backends override to use their async-signal-safe primitive.
500    fn condvar_signal_from_isr(cv: *mut c_void) -> i8 {
501        Self::condvar_signal(cv)
502    }
503    /// Atomically release `m` and block on `cv`. The mutex is
504    /// re-acquired before this function returns.
505    fn condvar_wait(cv: *mut c_void, m: *mut c_void) -> i8;
506
507    /// Wait with absolute monotonic deadline (milliseconds since
508    /// the [`PlatformClock::clock_ms`] epoch). Returns non-zero on
509    /// timeout.
510    fn condvar_wait_until(cv: *mut c_void, m: *mut c_void, abstime: u64) -> i8;
511
512    // -- Wake primitive (Phase 130) --
513    //
514    // Binary-semaphore-shaped primitive for the executor's wake_flag
515    // / spin_once cv-wait pair. Default bodies return "unsupported"
516    // (`-1`, size 0) so existing single-thread bare-metal platforms
517    // don't need to override; platforms that want event-driven wake
518    // (POSIX, Zephyr, FreeRTOS, NuttX, ThreadX) override with their
519    // native binary semaphore.
520
521    /// Initialise a binary-semaphore-shaped wake primitive in
522    /// caller-provided storage. See `<nros/platform.h>` for the
523    /// per-platform backing primitive (POSIX `sem_t`, Zephyr
524    /// `k_sem`, FreeRTOS `xSemaphoreBinary`, …). Default returns
525    /// `-1` (unsupported).
526    fn wake_init(_w: *mut c_void) -> i8 {
527        -1
528    }
529    /// Tear down a wake primitive. Default returns `-1`
530    /// (unsupported).
531    fn wake_drop(_w: *mut c_void) -> i8 {
532        -1
533    }
534    /// Block until signaled or `timeout_ms` elapses. Returns `0` on
535    /// signal, `1` on timeout, `-1` on error. Default returns `-1`
536    /// (unsupported).
537    fn wake_wait_ms(_w: *mut c_void, _timeout_ms: u32) -> i8 {
538        -1
539    }
540    /// Wake one waiter. Idempotent — a signal pending when another
541    /// arrives is coalesced (the primitive stays at value 1).
542    /// Default returns `-1` (unsupported).
543    fn wake_signal(_w: *mut c_void) -> i8 {
544        -1
545    }
546    /// ISR-safe signal. Returns `-1` when the backend has no ISR
547    /// path; callers may fall back to `wake_signal` (with the
548    /// obvious latency cost). Default forwards to `wake_signal`.
549    fn wake_signal_from_isr(w: *mut c_void) -> i8 {
550        Self::wake_signal(w)
551    }
552    /// Caller-storage size requirement (bytes). Default `0` —
553    /// signals "no wake primitive available". May be called before
554    /// `wake_init`.
555    fn wake_storage_size() -> usize {
556        0
557    }
558    /// Caller-storage alignment requirement (bytes). Default `1`.
559    fn wake_storage_align() -> usize {
560        1
561    }
562}
563
564/// Network poll callback for bare-metal platforms using smoltcp.
565///
566/// Not required for platforms with OS-level networking (POSIX, Zephyr, NuttX).
567///
568/// **Dispatch model**: bare-metal smoltcp platforms route this hook to
569/// `SmoltcpBridge::poll_network()` so CFFI callers, RMW shims, and direct
570/// platform users all share one network pump.
571pub trait PlatformNetworkPoll {
572    /// Poll the network stack to process pending I/O.
573    ///
574    /// Default no-op — platforms with OS-level networking don't need a
575    /// pump (kernel TCP/IP stack runs in the background). Bare-metal
576    /// smoltcp providers override this.
577    fn network_poll() {}
578}
579
580/// Global mutual exclusion against preemption + ISR delivery
581/// (Phase 121.9).
582///
583/// Backs the Rust `critical_section::Impl` registration used by
584/// DDS, nros-rmw-{xrce,zenoh}, and any other no_std consumer of
585/// `critical_section::with()`. The token returned by `acquire` is
586/// passed back to `release`; it holds whatever bookkeeping the
587/// platform needs to restore the prior posture (Cortex-M PRIMASK bit,
588/// Cortex-R CPSR I-bit, RISC-V `mstatus.MIE` snapshot, pthread
589/// recursion depth, etc.) and is opaque to callers.
590///
591/// Reentrant by contract: nested `acquire` / `release` pairs must
592/// stack — the platform impl is responsible for nesting (PRIMASK
593/// already stacks; pthread side uses a recursive mutex).
594pub trait PlatformCriticalSection {
595    /// Enter a critical section. Returns an opaque token to pass to
596    /// [`Self::release`].
597    fn acquire() -> u32;
598
599    /// Leave a critical section, restoring the posture captured at
600    /// [`Self::acquire`].
601    fn release(token: u32);
602}
603
604// ============================================================================
605// Networking — TCP
606// ============================================================================
607
608/// TCP networking.
609///
610/// Socket and endpoint parameters are opaque `*mut c_void` pointers to
611/// platform-specific types (`_z_sys_net_socket_t`, `_z_sys_net_endpoint_t`).
612/// The shim provides correctly-sized `#[repr(C)]` wrappers whose sizes are
613/// auto-detected from C headers at build time (see Phase 80 design).
614///
615/// Read functions return `usize::MAX` on error. Send returns `usize::MAX` on error.
616///
617/// Method names are unprefixed — the trait already namespaces them. Shims
618/// dispatch via `<ConcretePlatform as PlatformTcp>::open(...)` etc.
619pub trait PlatformTcp {
620    /// Resolve address + port strings into an endpoint handle.
621    fn create_endpoint(ep: *mut c_void, address: *const u8, port: *const u8) -> i8;
622    /// Free endpoint resources.
623    fn free_endpoint(ep: *mut c_void);
624    /// Open a TCP client connection. `endpoint` is by-value (opaque bytes on stack).
625    fn open(sock: *mut c_void, endpoint: *const c_void, timeout_ms: u32) -> i8;
626    /// Open a TCP listening socket.
627    fn listen(sock: *mut c_void, endpoint: *const c_void) -> i8;
628    /// Close a TCP socket.
629    fn close(sock: *mut c_void);
630    /// Read up to `len` bytes. Returns bytes read, or `usize::MAX` on error.
631    fn read(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
632    /// Read exactly `len` bytes. Returns `len` on success, `usize::MAX` on error.
633    fn read_exact(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
634    /// Send `len` bytes. Returns bytes sent, or `usize::MAX` on error.
635    fn send(sock: *const c_void, buf: *const u8, len: usize) -> usize;
636}
637
638// ============================================================================
639// Networking — UDP unicast
640// ============================================================================
641
642/// UDP unicast networking.
643pub trait PlatformUdp {
644    fn create_endpoint(ep: *mut c_void, address: *const u8, port: *const u8) -> i8;
645    fn free_endpoint(ep: *mut c_void);
646    fn open(sock: *mut c_void, endpoint: *const c_void, timeout_ms: u32) -> i8;
647    fn close(sock: *mut c_void);
648    fn read(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
649    fn read_exact(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
650    fn send(sock: *const c_void, buf: *const u8, len: usize, endpoint: *const c_void) -> usize;
651    /// Set the receive timeout on a UDP socket (milliseconds).
652    /// 0 means block indefinitely (no timeout).
653    fn set_recv_timeout(sock: *const c_void, timeout_ms: u32);
654
655    /// Open a UDP socket in listen (server) mode, bound to the given
656    /// endpoint. Returns 0 on success, negative on failure.
657    ///
658    /// Optional — the default returns `-1`, which the shim forwards to
659    /// `_z_listen_udp_unicast` as "not implemented". Platforms that
660    /// need UDP server sockets (e.g. for running an XRCE-DDS agent
661    /// locally) should override this. Once Phase 84.F4 lands (the
662    /// "platform traits become a real contract" refactor), the shim
663    /// will dispatch through this trait method automatically.
664    fn listen(_sock: *mut c_void, _endpoint: *const c_void, _timeout_ms: u32) -> i8 {
665        -1
666    }
667}
668
669// ============================================================================
670// Networking — socket helpers
671// ============================================================================
672
673/// Socket helper operations called by zenoh-pico's transport layer.
674///
675/// Unprefixed method names: dispatch via
676/// `<ConcretePlatform as PlatformSocketHelpers>::set_non_blocking(...)`.
677/// Note that the `close` method here is the socket-layer close (shutdown +
678/// close) used by zenoh-pico's generic helpers; `PlatformTcp::close` is the
679/// TCP-specific close. Both exist because zenoh-pico's C surface has both.
680pub trait PlatformSocketHelpers {
681    /// Set socket to non-blocking mode.
682    fn set_non_blocking(sock: *const c_void) -> i8;
683    /// Accept a pending connection.
684    fn accept(sock_in: *const c_void, sock_out: *mut c_void) -> i8;
685    /// Close a socket (shutdown + close).
686    fn close(sock: *mut c_void);
687    /// Wait for socket events (multi-threaded platforms).
688    fn wait_event(peers: *mut c_void, mutex: *mut c_void) -> i8;
689}
690
691// ============================================================================
692// libc stubs (bare-metal only)
693// ============================================================================
694
695/// Standard C library functions needed by zenoh-pico on bare-metal targets.
696///
697/// Platforms with a C runtime (RTOS, POSIX) do NOT need to implement this.
698///
699/// # Dispatch model (Phase 84.F4.6)
700///
701/// This trait is **documentary only** — it is NOT dispatched through by
702/// `zpico-platform-shim` or `xrce-platform-shim`. The C libraries resolve
703/// these symbols (`strlen`, `memcpy`, `errno`, ...) at link time directly
704/// from `#[unsafe(no_mangle)] extern "C" fn` definitions in
705/// `nros-baremetal-common`, which bare-metal platform crates pull in
706/// via the `libc-stubs` feature:
707///
708/// ```text
709///   nros-baremetal-common = { ..., features = ["libc-stubs"] }
710/// ```
711///
712/// The trait is retained in this API surface so that a future shim
713/// refactor could route libc through typed Rust methods without
714/// changing consumers. Today, implementing `PlatformLibc` on a platform
715/// ZST would be pure documentation; the actual contract — "the linker
716/// can resolve `strlen` etc." — is enforced at link time, not at
717/// compile time. No platform crate implements this trait in the
718/// current tree.
719pub trait PlatformLibc {
720    fn strlen(s: *const u8) -> usize;
721    fn strcmp(s1: *const u8, s2: *const u8) -> c_int;
722    fn strncmp(s1: *const u8, s2: *const u8, n: usize) -> c_int;
723    fn strchr(s: *const u8, c: c_int) -> *mut u8;
724    fn strncpy(dest: *mut u8, src: *const u8, n: usize) -> *mut u8;
725    fn memcpy(dest: *mut c_void, src: *const c_void, n: usize) -> *mut c_void;
726    fn memmove(dest: *mut c_void, src: *const c_void, n: usize) -> *mut c_void;
727    fn memset(dest: *mut c_void, c: c_int, n: usize) -> *mut c_void;
728    fn memcmp(s1: *const c_void, s2: *const c_void, n: usize) -> c_int;
729    fn memchr(s: *const c_void, c: c_int, n: usize) -> *mut c_void;
730    fn strtoul(nptr: *const u8, endptr: *mut *mut u8, base: c_int) -> core::ffi::c_ulong;
731    fn errno_ptr() -> *mut c_int;
732}
733
734// ============================================================================
735// Logging — Phase 88 leveled log delivery
736// ============================================================================
737
738/// Per-platform leveled log delivery.
739///
740/// Matches the post-Phase-129 platform-ABI pattern: the portable
741/// facade (`nros-log`) formats messages into a fixed buffer; each
742/// `nros-platform-<rtos>` carries the actual delivery (stderr,
743/// `printk`, `esp_log_write`, `syslog`, board-registered UART writer,
744/// etc.).
745///
746/// `severity` is the `u8` discriminant of `nros_log::Severity`
747/// (0 = Trace .. 5 = Fatal). Implementors should map onto the
748/// platform's nearest native level.
749///
750/// `name` is the logger name (UTF-8, NOT null-terminated, may be
751/// empty); `message` is the already-formatted body (UTF-8, NOT
752/// null-terminated). Delivery is infallible from the caller's POV;
753/// platforms that fill an internal buffer (RTT, syslog) silently
754/// drop on overflow.
755///
756/// Thread / ISR safety is per-platform — see the table in
757/// `docs/roadmap/phase-88-nros-log.md`. The ABI itself is synchronous
758/// and the caller-side facade carries a recursion guard so a sink
759/// that triggers `log()` from inside `write` is short-circuited.
760pub trait PlatformLog {
761    /// Deliver one record. Severity is a stable `u8` matching
762    /// `nros_log::Severity::as_u8()`.
763    fn write(severity: u8, name: &[u8], message: &[u8]);
764
765    /// Best-effort drain of any internal buffer. Default = no-op.
766    fn flush() {}
767}
768
769// ============================================================================
770// Networking — UDP multicast
771// ============================================================================
772
773/// UDP multicast networking (used for zenoh scouting on desktop platforms).
774pub trait PlatformUdpMulticast {
775    fn mcast_open(
776        sock: *mut c_void,
777        endpoint: *const c_void,
778        lep: *mut c_void,
779        timeout_ms: u32,
780        iface: *const u8,
781    ) -> i8;
782    fn mcast_listen(
783        sock: *mut c_void,
784        endpoint: *const c_void,
785        timeout_ms: u32,
786        iface: *const u8,
787        join: *const u8,
788    ) -> i8;
789    fn mcast_close(
790        sockrecv: *mut c_void,
791        socksend: *mut c_void,
792        rep: *const c_void,
793        lep: *const c_void,
794    );
795    fn mcast_read(
796        sock: *const c_void,
797        buf: *mut u8,
798        len: usize,
799        lep: *const c_void,
800        addr: *mut c_void,
801    ) -> usize;
802    fn mcast_read_exact(
803        sock: *const c_void,
804        buf: *mut u8,
805        len: usize,
806        lep: *const c_void,
807        addr: *mut c_void,
808    ) -> usize;
809    fn mcast_send(
810        sock: *const c_void,
811        buf: *const u8,
812        len: usize,
813        endpoint: *const c_void,
814    ) -> usize;
815}
816
817// ============================================================================
818// Inter-VM / mailbox transport (NVIDIA IVC and similar)
819// ============================================================================
820
821/// Inter-processor mailbox transport, modelled after NVIDIA Tegra IVC.
822///
823/// IVC (Inter-VM Communication on Tegra; in practice CCPLEX↔SPE on AGX
824/// Orin) is a header-prefixed lock-free SPSC ring in shared DRAM, paired
825/// with a hardware doorbell for wake. One channel is one peer — there
826/// is no discovery, naming, QoS, or fanout — which is why it's a *link*
827/// transport (peer to TCP/UDP/Serial/RawEth inside zenoh-pico) rather
828/// than a new RMW backend.
829///
830/// Channel handles are opaque `*mut c_void` to match the shape zenoh-pico
831/// passes across its FFI boundary. The driver crate (`nvidia-ivc`)
832/// translates between this opaque handle and either NVIDIA's FSP
833/// `tegra_ivc_*` API (`fsp` feature) or a Unix-socket pair
834/// (`unix-mock` feature, host dev + CI).
835///
836/// **Zero-copy contract** (Phase 11.3.A). NVIDIA's FSP IVC API is
837/// fundamentally a "borrow ring slot, fill, commit / release"
838/// pattern. Both backends mirror that here so consumers don't branch
839/// on backend:
840///
841/// - [`Self::rx_get`] returns a pointer into the channel's RX slot
842///   (and writes the frame length to `*len_out`); [`Self::rx_release`]
843///   advances the producer-visible cursor. Returns null +
844///   `*len_out = 0` if the ring is empty.
845/// - [`Self::tx_get`] returns a writable pointer to the next free TX
846///   slot (and writes the slot capacity to `*cap_out`);
847///   [`Self::tx_commit`] makes the slot visible to the peer (and
848///   rings the per-frame doorbell on FSP — see `notify` for batching).
849///   [`Self::tx_abandon`] frees the slot without sending. Returns
850///   null + `*cap_out = 0` if the ring is full.
851///
852/// Single outstanding RX slot and single outstanding TX slot per
853/// channel — borrow, finish, repeat. Multi-frame batching is the
854/// caller's job (loop `tx_get` / fill / `tx_commit`, then one
855/// `notify`).
856///
857/// `frame_size` is the fixed per-channel frame size negotiated at
858/// carveout setup (typical NVIDIA IVC: 64 bytes per frame, 16 frames
859/// per channel). The link layer uses it to pick its reassembly buffer.
860pub trait PlatformIvc {
861    /// Resolve a channel ID into an opaque handle. Returns null on
862    /// failure. The numeric ID matches the NVIDIA channel index
863    /// (`channel 2 = aon_echo`).
864    fn channel_get(id: u32) -> *mut c_void;
865
866    /// Fixed frame size negotiated for this channel, in bytes.
867    fn frame_size(ch: *mut c_void) -> u32;
868
869    /// Borrow the next-available RX frame. Writes the frame length to
870    /// `*len_out` and returns a pointer into the ring. Returns null +
871    /// `*len_out = 0` if no frame is available.
872    fn rx_get(ch: *mut c_void, len_out: *mut usize) -> *const u8;
873
874    /// Release the most recently `rx_get`'d frame back to the
875    /// producer. Pair 1:1 with `rx_get` calls that returned non-null.
876    fn rx_release(ch: *mut c_void);
877
878    /// Borrow the next free TX slot. Writes the slot capacity to
879    /// `*cap_out` and returns a writable pointer. Returns null +
880    /// `*cap_out = 0` if the ring is full.
881    fn tx_get(ch: *mut c_void, cap_out: *mut usize) -> *mut u8;
882
883    /// Commit `len` bytes from the most recently `tx_get`'d slot.
884    /// Slot is then visible to the peer; per-frame doorbell may also
885    /// fire on FSP. Pair 1:1 with `tx_get` calls that returned
886    /// non-null.
887    fn tx_commit(ch: *mut c_void, len: usize);
888
889    /// Abandon the most recently `tx_get`'d slot without sending.
890    /// Pair 1:1 with `tx_get` calls that returned non-null.
891    fn tx_abandon(ch: *mut c_void);
892
893    /// Ring the doorbell. On FSP this is redundant (commit/release
894    /// already invoke `ch->notify_remote`); on unix-mock it's a no-op
895    /// (SOCK_DGRAM wakes the peer naturally). Provided for symmetry
896    /// so callers can batch-commit then notify once.
897    fn notify(ch: *mut c_void);
898}
899
900// ============================================================================
901// Serial (UART / PTY)
902// ============================================================================
903
904/// Serial (byte-stream) transport.
905///
906/// Used by XRCE-DDS's HDLC-framed serial transport and (once the
907/// bare-metal migration lands) by zenoh-pico's serial link layer.
908///
909/// # Handle model
910///
911/// `open()` returns a platform-defined [`Handle`](PlatformSerial::Handle)
912/// — an FD on POSIX, a port-table index on bare-metal, whatever the
913/// impl wants. Every other method takes the handle back. This lets a
914/// single platform impl service multiple concurrent devices (e.g.,
915/// `zpico-serial`'s two-port table) without the trait constraining
916/// the impl to a single active device.
917///
918/// Single-device platforms return the same handle forever and ignore
919/// it internally; `INVALID` gives a well-defined "no live handle"
920/// sentinel for shims that stash the current handle in a `static`.
921///
922/// # Path conventions
923///
924/// `path` in `open()` is platform-specific: a null-terminated UTF-8
925/// device path on POSIX (e.g., `/dev/ttyUSB0` or a PTY), or a
926/// board-defined port identifier on bare-metal (typically parsed by
927/// the platform's internal handler). Callers pass the locator string
928/// from their config unchanged; interpretation is the platform's job.
929///
930/// # I/O conventions
931///
932/// Read / write return `usize::MAX` on hard error. Read with
933/// `timeout_ms == 0` should block for a platform-chosen default;
934/// positive values are the poll/select deadline in milliseconds.
935/// Returning `0` from `read()` indicates "no data within timeout" and
936/// is **not** an error — both XRCE and zenoh-pico tolerate
937/// timeout-zero reads.
938pub trait PlatformSerial {
939    /// Platform-specific handle type. POSIX returns the FD (`i32`);
940    /// bare-metal returns a port-table index (`u8`). Must be `Copy`
941    /// so shims can stash it in a `static`.
942    type Handle: Copy;
943
944    /// Sentinel handle meaning "not a live device". Shims initialise
945    /// their cached handle to this and compare against it to detect
946    /// "transport not yet opened" states.
947    const INVALID: Self::Handle;
948
949    /// Returns `true` if `h` is a live handle (not [`INVALID`](Self::INVALID)
950    /// and points at a device that was opened and not yet closed).
951    fn is_valid(h: Self::Handle) -> bool;
952
953    /// Open the serial device identified by `path`. Returns a live
954    /// handle on success, [`INVALID`](Self::INVALID) on failure.
955    fn open(path: *const u8) -> Self::Handle;
956
957    /// Close the given handle. No-op if already closed or invalid.
958    fn close(h: Self::Handle);
959
960    /// Configure baud rate (in bits per second). Returns 0 on success,
961    /// -1 on error. Called after `open()`; implementations may choose
962    /// to apply the baud rate during `open()` instead and make this a
963    /// no-op.
964    fn configure(h: Self::Handle, baudrate: u32) -> i8;
965
966    /// Read up to `len` bytes into `buf`. Returns the number of bytes
967    /// read, `0` on timeout, or `usize::MAX` on hard error.
968    fn read(h: Self::Handle, buf: *mut u8, len: usize, timeout_ms: u32) -> usize;
969
970    /// Write `len` bytes from `buf`. Returns bytes written, or
971    /// `usize::MAX` on error.
972    fn write(h: Self::Handle, buf: *const u8, len: usize) -> usize;
973}