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