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//! # Who implements these traits
47//!
48//! The bare-metal Rust ports (`Mps2An385Platform`,
49//! `Stm32f4Platform`) implement them, and `nros_platform_export!` exports
50//! those impls over the C platform ABI; `CffiPlatform` implements them in
51//! the other direction, over that ABI. The hosted and RTOS ports (POSIX,
52//! Zephyr, FreeRTOS, NuttX, ThreadX) are C (`nros-platform-<rtos>/src/
53//! platform.c`). Their Rust ZSTs, ~~`PosixPlatform`~~ among them, are gone,
54//! and with them the Phase 84.F4 plan that would have moved each one from
55//! inherent methods onto these traits.
56
57// phase-359 W10 — the ONE Rust-side spawn helper for the task ABI this crate
58// defines. It lived in `nros-node::executor::platform_task`, private, which
59// meant the second Rust caller (`nros-cpp`'s native tier runtime) could only
60// reach it by exposing a core crate's internals or by writing a third copy of
61// allocate-spawn-join — `nros-board-nuttx` already holds a second. A helper for
62// an ABI belongs with the ABI.
63#[cfg(feature = "alloc")]
64extern crate alloc;
65// The module is NOT `alloc`-gated; `PlatformTask` inside it is. Gating the whole
66// module put `stack_unused_bytes` — a bare `extern` query that allocates nothing
67// — behind `alloc`, so `Executor::check_stack_headroom_rule` failed to compile
68// in every no-alloc image. That is the build where stack headroom matters most,
69// and the rule was unreachable there (phase-425 sweep; the lane that catches it,
70// `check workspace-embedded`, runs in no merge-gating event).
71pub mod task;
72
73use core::ffi::{c_int, c_void};
74
75pub mod boot_config;
76pub mod wake;
77pub mod xorshift32;
78
79pub use boot_config::{
80    BOOT_SET_DOMAIN, BOOT_SET_LOCATOR, BOOT_SET_NAMESPACE, BOOT_SET_NODE_NAME, BOOT_SET_RMW,
81    BakedBootConfig, NROS_BOOT_CONFIG_MAGIC, NROS_BOOT_CONFIG_VERSION,
82};
83pub use wake::{WAKE_STORAGE_ALIGN, WAKE_STORAGE_BYTES, Wake, WakeInitError, WakeReason};
84
85// ============================================================================
86// Clock (required by all RMW backends)
87// ============================================================================
88
89/// Monotonic clock.
90///
91/// The most critical platform primitive. Must be backed by a hardware timer
92/// or OS tick — never by a software counter that only advances when polled.
93pub trait PlatformClock {
94    /// Monotonic nanoseconds since a platform-defined epoch. Never
95    /// decreases. See `<nros/platform.h>` for the full contract
96    /// (RFC-0073).
97    fn clock_ns() -> u64;
98
99    /// Granularity of [`Self::clock_ns`] in nanoseconds: the smallest
100    /// non-zero difference two successive reads can report. Non-zero,
101    /// and constant after platform init.
102    fn clock_resolution_ns() -> u64;
103
104    /// Microseconds since the UNIX epoch, or `0` when this platform has no
105    /// wall-clock source. See `<nros/platform.h>` for the full contract
106    /// (issue 0758).
107    ///
108    /// DEFAULTED to `0` deliberately. Every existing port is correct
109    /// without changing a line: none of them has an epoch source today, and
110    /// `0` is precisely the honest answer. Making this a required method
111    /// would have forced eleven ports to write `0` by hand, which is the
112    /// same information spelled eleven times plus eleven chances to write
113    /// something else.
114    ///
115    /// It is also the difference between an ABI addition that is a
116    /// no-op for ports and one that breaks every image that has not caught
117    /// up — and the C side has no such default, so a port implemented in C
118    /// DOES say `0` explicitly.
119    ///
120    /// Unlike [`Self::clock_ns`] this is not monotonic: a port that
121    /// acquires its epoch after boot jumps when it does.
122    fn epoch_us() -> u64 {
123        0
124    }
125}
126
127// ============================================================================
128// Heap allocation (zenoh-pico requires ~64 KB heap)
129// ============================================================================
130
131/// Heap memory allocation.
132pub trait PlatformAlloc {
133    /// Allocate `size` bytes. Returns null on failure.
134    fn alloc(size: usize) -> *mut c_void;
135
136    /// Reallocate a previously allocated block. Returns null on failure.
137    fn realloc(ptr: *mut c_void, size: usize) -> *mut c_void;
138
139    /// Free a previously allocated block.
140    fn dealloc(ptr: *mut c_void);
141
142    /// Phase 230 Z5 / RFC-0034 D7 — bytes currently allocated from the
143    /// platform heap. The true unified figure where the platform owns one
144    /// kernel heap shared by the C side (`alloc`) and the Rust
145    /// `#[global_allocator]`. Default `0` = "unknown / not instrumented".
146    fn heap_used_bytes() -> usize {
147        0
148    }
149
150    /// Total managed heap size in bytes (used + free), or `0` if unknown.
151    fn heap_total_bytes() -> usize {
152        0
153    }
154}
155
156// ============================================================================
157// Fatal error
158// ============================================================================
159
160/// The image's one ending — phase-366 / RFC-0077.
161///
162/// Mirrors `nros_platform_panic` in `<nros/platform.h>`. Rust's
163/// `#[panic_handler]`, a C precondition failure and a C++ terminate all arrive
164/// here, so an image has one fatal path instead of one per language.
165///
166/// The ABI could express ALLOCATION but not fatality, so each language invented
167/// its own ending and four libraries each hardcoded a different one. This is the
168/// missing half.
169pub trait PlatformPanic {
170    /// Terminate the image. Never returns.
171    ///
172    /// `msg` is a DIAGNOSTIC, not a C string: `len` bytes, no NUL required,
173    /// possibly empty, and not necessarily UTF-8 — a port forwarding to a text
174    /// sink should treat it as bytes.
175    ///
176    /// Implementors must tolerate being called from ANY context: interrupt,
177    /// scheduler locked, or before the kernel starts. That rules out anything
178    /// needing a scheduler, which is why this is not "log at Fatal, then halt".
179    ///
180    /// Implementors SHOULD emit `msg` where a human can read it first. A silent
181    /// halt is the one behaviour no RTOS in this tree chose for itself.
182    fn panic(msg: *const u8, len: usize) -> !;
183}
184
185// ============================================================================
186// Sleep / delay
187// ============================================================================
188
189/// Sleep primitives.
190///
191/// On bare-metal with smoltcp, implementations should poll the network
192/// stack during busy-wait sleep to avoid missing packets.
193pub trait PlatformSleep {
194    /// Sleep for the given number of microseconds.
195    fn sleep_us(us: usize);
196
197    /// Sleep for the given number of milliseconds.
198    fn sleep_ms(ms: usize);
199
200    /// Sleep for the given number of seconds.
201    fn sleep_s(s: usize);
202}
203
204// ============================================================================
205// Cooperative yield
206// ============================================================================
207
208/// Scheduler yield primitive.
209///
210/// Used inside `socket_wait_event` and similar "let another task make
211/// progress" points where the caller isn't actually waiting for I/O
212/// readability — the background read task already owns that — it just
213/// needs to relinquish the CPU so the real waiter can run.
214///
215/// Prior to Phase 77.22 each backend hand-rolled its own 1-ms busy
216/// sleep (`libc::usleep(1000)`, `vTaskDelay(1)`, `tx_thread_sleep(1)`,
217/// `k_usleep(1000)`, `select(.., 1 ms)`) — all with slightly different
218/// units and no common home.
219///
220/// **ISR-safety**: on the hosted-RTOS backends (FreeRTOS / NuttX /
221/// Zephyr / ThreadX) the underlying primitives panic or error when
222/// invoked from an ISR. Don't call `yield_now()` from an interrupt
223/// handler.
224///
225/// **Bare-metal has no real yield**: there's nothing to yield *to*.
226/// The default bare-metal impl is `core::hint::spin_loop()` (a pure
227/// CPU hint: emits `YIELD` / `PAUSE` / `WFE` depending on the arch,
228/// safe everywhere). Board crates that have armed an IRQ source may
229/// opt in to deep idle (`wfi`) via a separate BoardIdle hook — not
230/// part of this trait because calling `wfi` without an IRQ source
231/// deadlocks.
232pub trait PlatformYield {
233    /// Relinquish the CPU long enough for another task / thread to run.
234    ///
235    /// Non-blocking in the sleep sense — returns as soon as the
236    /// scheduler has had an opportunity to pick a different runnable.
237    fn yield_now();
238}
239
240// ============================================================================
241// Phase 110.D — `PlatformScheduler` (per-thread OS scheduling policy)
242// ============================================================================
243
244/// Errors returned by [`PlatformScheduler`] entry points.
245#[derive(Debug, Clone, Copy, PartialEq, Eq)]
246pub enum SchedError {
247    /// The active platform doesn't expose this control surface
248    /// (e.g. bare-metal with no scheduler, or an RTOS without an
249    /// affinity API).
250    Unsupported,
251    /// The requested policy is valid for this platform but the
252    /// numeric arguments fall outside the platform's accepted range.
253    OutOfRange,
254    /// A platform-specific syscall / kernel call failed; check
255    /// `errno` (POSIX) or the RTOS error code for details.
256    KernelError,
257}
258
259/// Per-thread OS scheduling policy.
260///
261/// `Fifo` / `RoundRobin` / `Deadline` / `Sporadic` map to platform-
262/// native scheduling classes when available. `Platform(...)` is the
263/// escape hatch for RTOS-specific knobs (e.g. ThreadX preempt-
264/// threshold) that don't map cleanly into the portable variants.
265///
266/// User-facing API (`Executor::open_threaded`) takes the abstract
267/// [`Priority`-like](crate) values and `PlatformScheduler` translates
268/// them into platform-native numerics — direction-flipped priority
269/// (Zephyr / ThreadX low-numeric = high-priority vs POSIX / FreeRTOS
270/// high-numeric = high-priority) handled internally. Phase 110.D.
271#[derive(Debug, Clone, Copy, PartialEq, Eq)]
272pub enum SchedPolicy {
273    /// POSIX `SCHED_FIFO`. `os_pri` is the platform-native numeric
274    /// priority; out-of-range values surface `SchedError::OutOfRange`.
275    Fifo { os_pri: u8 },
276    /// POSIX `SCHED_RR` with `quantum_ms` time slice.
277    RoundRobin { os_pri: u8, quantum_ms: u32 },
278    /// Linux `SCHED_DEADLINE` (sched_setattr). All values nanoseconds.
279    Deadline {
280        runtime_ns: u64,
281        period_ns: u64,
282        deadline_ns: u64,
283    },
284    /// NuttX `SCHED_SPORADIC` server. Phase 110.E.
285    Sporadic {
286        budget_us: u32,
287        period_us: u32,
288        hi_pri: u8,
289        lo_pri: u8,
290    },
291}
292
293// Per-thread OS-scheduling control surface.
294//
295// Each platform implements as much of this trait as its kernel
296// supports. `bare-metal` returns [`SchedError::Unsupported`] from
297// every entry point — there is no scheduler to talk to.
298//
299// Phase 110.D wires this for Linux + NuttX (via POSIX) at v1; Zephyr
300// / FreeRTOS / ThreadX impls land alongside per-RTOS bring-up.
301
302// ============================================================================
303// Phase 110.E.b — `PlatformTimer` (ISR-driven periodic refill)
304// ============================================================================
305
306/// Errors returned by [`PlatformTimer`] entry points.
307#[derive(Debug, Clone, Copy, PartialEq, Eq)]
308pub enum TimerError {
309    /// The active platform doesn't expose a periodic-timer surface
310    /// (e.g. bare-metal without a board-side SysTickHook).
311    Unsupported,
312    /// `period_us` is below the platform's minimum timer resolution
313    /// or above its maximum representable interval.
314    OutOfRange,
315    /// A platform-specific syscall / kernel call failed; check
316    /// `errno` (POSIX) or the RTOS error code for details.
317    KernelError,
318}
319
320/// Periodic timer for ISR-driven sporadic-server budget refill
321/// (Phase 110.E.b). The trait factors out the per-platform timer
322/// surface so the executor (`nros-node`) can register an atomic
323/// refill callback without becoming generic over the platform —
324/// see `docs/design/0017-platform-timer.md`.
325///
326/// Default `create_periodic` returns [`TimerError::Unsupported`] so
327/// platforms without a timer surface inherit safe behavior; `destroy`
328/// is a no-op default for the same reason.
329pub trait PlatformTimer {
330    /// Opaque per-platform handle (FreeRTOS `TimerHandle_t`,
331    /// Zephyr `*mut k_timer`, ThreadX `*mut TX_TIMER`, POSIX
332    /// `timer_t`). Must be `Send + Sync + 'static` so the executor
333    /// can stash it across thread boundaries.
334    type TimerHandle: Send + Sync + 'static;
335
336    /// Register a periodic timer that fires `callback(user_data)`
337    /// every `period_us` microseconds. Returns the platform-native
338    /// handle for later destruction.
339    ///
340    /// # Safety
341    ///
342    /// `user_data` must outlive the returned handle. The callback is
343    /// invoked from a platform-defined timer context (direct ISR on
344    /// Zephyr / bare-metal, deferred via `xTimerPendFunctionCall` on
345    /// FreeRTOS, signal handler on POSIX) — bodies should be
346    /// short and use atomic ops only.
347    fn create_periodic(
348        _period_us: u32,
349        _callback: extern "C" fn(*mut c_void),
350        _user_data: *mut c_void,
351    ) -> Result<Self::TimerHandle, TimerError> {
352        Err(TimerError::Unsupported)
353    }
354
355    /// Cancel + free the timer. Idempotent on already-destroyed
356    /// handles. Must drain in-flight callback invocations before
357    /// returning so the user_data pointer is no longer accessed.
358    fn destroy(_handle: Self::TimerHandle) {}
359
360    /// Phase 110.E.b follow-up — register a one-shot timer that
361    /// fires `callback(user_data)` exactly once after `timeout_us`
362    /// microseconds. Used by per-callback runtime measurement: arm
363    /// just before dispatch with `timeout_us = budget_us`; on
364    /// callback completion call `cancel`. If the timer fires first
365    /// the callback overran its budget.
366    ///
367    /// Default returns `Unsupported` so platforms without a oneshot
368    /// surface inherit safe behavior.
369    fn create_oneshot(
370        _timeout_us: u32,
371        _callback: extern "C" fn(*mut c_void),
372        _user_data: *mut c_void,
373    ) -> Result<Self::TimerHandle, TimerError> {
374        Err(TimerError::Unsupported)
375    }
376
377    /// Phase 110.E.b follow-up — cancel a previously-armed oneshot
378    /// timer. Returns `true` when the cancellation prevented the
379    /// callback from firing, `false` when the callback already
380    /// fired (or the timer was already cancelled). Default is a
381    /// no-op returning `false`.
382    fn cancel(_handle: &mut Self::TimerHandle) -> bool {
383        false
384    }
385}
386
387pub trait PlatformScheduler {
388    /// Apply the requested policy to the calling thread.
389    ///
390    /// Default returns [`SchedError::Unsupported`] so single-core bare-
391    /// metal targets and RTOS impls without per-thread scheduling
392    /// pickup the no-op behavior automatically. Platforms with a
393    /// real scheduler override this.
394    fn set_current_thread_policy(_p: SchedPolicy) -> Result<(), SchedError> {
395        Err(SchedError::Unsupported)
396    }
397
398    /// Cooperative yield. Same semantics as
399    /// [`PlatformYield::yield_now`]; mirrored here so consumers don't
400    /// need to import both traits when only the scheduler control
401    /// surface is in scope. Default is a `core::hint::spin_loop()`.
402    fn yield_now() {
403        core::hint::spin_loop();
404    }
405
406    /// Pin the calling thread to the CPUs whose bit is set in
407    /// `_cpu_mask`. Default returns [`SchedError::Unsupported`] —
408    /// platforms with affinity APIs override.
409    fn set_affinity(_cpu_mask: u32) -> Result<(), SchedError> {
410        Err(SchedError::Unsupported)
411    }
412}
413
414// ============================================================================
415// Random number generation
416// ============================================================================
417
418/// Pseudo-random number generation.
419///
420/// A simple xorshift32 PRNG is sufficient. Seed with hardware entropy
421/// (RNG peripheral, ADC noise, wall-clock time) during platform init.
422pub trait PlatformRandom {
423    fn random_u8() -> u8;
424    fn random_u16() -> u16;
425    fn random_u32() -> u32;
426    fn random_u64() -> u64;
427
428    /// Fill buffer with random bytes.
429    fn random_fill(buf: *mut c_void, len: usize);
430}
431
432// ============================================================================
433// Wall-clock time (for logging, not timing-critical)
434// ============================================================================
435
436/// Wall-clock / system time.
437///
438/// Used for logging timestamps and `z_time_now_as_str()`.
439/// On bare-metal without an RTC, return 0.
440///
441/// ONE method for one fact, matching `nros_platform_time_now_ns` in the C ABI
442/// (issue 0532 item 5). It replaced `time_now_ms` + `time_since_epoch_secs` +
443/// `time_since_epoch_nanos`.
444///
445/// The old doc justified the split by zenoh-pico's FFI shape: "zpico-platform-
446/// shim forwards each of these directly to a `_z_time_*` symbol, so collapsing
447/// them into a Rust struct would require the shim to decompose the struct on
448/// every call." That argument was against a STRUCT, and it still holds — but a
449/// single `u64` is not a struct. The shim now takes one value and derives the
450/// pair with a divide and a remainder, which is both cheaper than two FFI calls
451/// and, unlike them, cannot tear across a second boundary.
452pub trait PlatformTime {
453    /// Wall-clock nanoseconds since the Unix epoch, or 0 if this platform has
454    /// no real-time clock.
455    fn time_now_ns() -> u64;
456}
457
458// ============================================================================
459// Threading (multi-threaded platforms only)
460// ============================================================================
461//
462// Handle types are opaque `*mut c_void` to match the shape zenoh-pico
463// passes across the FFI boundary. The original draft had typed wrappers
464// (~~`TaskHandle`~~, ~~`MutexHandle`~~, ...) but every shipped platform used
465// `*mut c_void` internally and the shim never materialised the typed
466// forms — keeping them in the trait would have required a pointless
467// cast at every impl site. (F4.1 / F4.5 decision, 2026-04-24.)
468//
469// Mutex / condvar / task method names keep their sub-namespace prefix
470// (`mutex_*`, `condvar_*`, `task_*`) because the trait bundles three
471// independent primitive families and unprefixed `init` / `drop` would
472// be ambiguous *within* the trait itself.
473
474/// Threading primitives: tasks, mutexes, and condition variables.
475///
476/// # Threading
477///
478/// Mutex / condvar operations must be safe under concurrent callers.
479/// **Recursive mutex (`mutex_rec_*`) must support same-thread
480/// re-entrancy** — zenoh-pico relies on this; a non-recursive mutex
481/// backing `mutex_rec_*` deadlocks under load.
482///
483/// # ISR-safety
484///
485/// **None of these methods are ISR-safe** on hosted RTOSes
486/// (FreeRTOS / NuttX / Zephyr / ThreadX) — the underlying primitives
487/// panic or error when invoked from an ISR. Only `core::hint::spin_loop()`
488/// in [`PlatformYield::yield_now`] is.
489///
490/// For single-threaded platforms (bare-metal), all operations should
491/// be no-ops returning `0`, except [`task_init`](Self::task_init)
492/// which should return `-1`.
493pub trait PlatformThreading {
494    /// Smallest number of bytes ever left unused on the CALLING task's
495    /// stack, or `0` if this platform does not instrument it.
496    ///
497    /// Defaulted so every existing impl keeps compiling: a port that says
498    /// nothing reports `0`, which the ABI documents as "not instrumented"
499    /// rather than "no headroom left".
500    fn task_stack_unused_bytes() -> usize {
501        0
502    }
503
504    /// [`task_stack_unused_bytes`](Self::task_stack_unused_bytes), capped at
505    /// `limit`, reading at most `limit` bytes of the stack (phase-436 E5).
506    ///
507    /// The default is the full query, capped, which is right for a port
508    /// whose measurement is already cheap or absent. A port that SCANS for
509    /// paint overrides it to read only the `limit` bytes at the deep end; see
510    /// `nros_platform_task_stack_unused_bytes_upto` in `nros/platform.h`.
511    fn task_stack_unused_bytes_upto(limit: usize) -> usize {
512        Self::task_stack_unused_bytes().min(limit)
513    }
514    // -- Tasks --
515
516    /// Spawn a new task. `task` is opaque caller-provided storage;
517    /// `attr` carries scheduling hints (priority, stack size) or
518    /// `null` for defaults; `entry` is the task entry point; `arg`
519    /// is forwarded to `entry`. Returns `0` on success, non-zero
520    /// on failure.
521    fn task_init(
522        task: *mut c_void,
523        attr: *mut c_void,
524        entry: Option<unsafe extern "C" fn(*mut c_void) -> *mut c_void>,
525        arg: *mut c_void,
526    ) -> i8;
527
528    /// Block until `task` exits. Cleans up the task storage on
529    /// success.
530    fn task_join(task: *mut c_void) -> i8;
531    /// Mark `task` as detached — its storage is reclaimed on exit
532    /// without a join.
533    fn task_detach(task: *mut c_void) -> i8;
534    /// Request `task` to terminate at the next cancellation point.
535    /// Cooperative.
536    fn task_cancel(task: *mut c_void) -> i8;
537    /// Terminate the calling task immediately. Does not return.
538    fn task_exit();
539    /// Free the task storage allocated by `task_init`.
540    fn task_free(task: *mut *mut c_void);
541
542    // -- Mutex --
543
544    /// Initialise a non-recursive mutex in caller-provided storage.
545    fn mutex_init(m: *mut c_void) -> i8;
546    /// Tear down a non-recursive mutex.
547    fn mutex_drop(m: *mut c_void) -> i8;
548    /// Lock; block if held.
549    fn mutex_lock(m: *mut c_void) -> i8;
550    /// Try to lock; non-zero return immediately if held.
551    fn mutex_try_lock(m: *mut c_void) -> i8;
552    /// Unlock; only the owning thread may call this.
553    fn mutex_unlock(m: *mut c_void) -> i8;
554
555    // -- Recursive mutex --
556
557    /// Initialise a *recursive* mutex (same-thread re-entrancy
558    /// permitted). Required by zenoh-pico.
559    fn mutex_rec_init(m: *mut c_void) -> i8;
560    /// Tear down a recursive mutex.
561    fn mutex_rec_drop(m: *mut c_void) -> i8;
562    /// Lock; re-entry from the owning thread must succeed.
563    fn mutex_rec_lock(m: *mut c_void) -> i8;
564    /// Try to lock; same re-entry semantics as `mutex_rec_lock`.
565    fn mutex_rec_try_lock(m: *mut c_void) -> i8;
566    /// Unlock; releases when the lock count returns to zero.
567    fn mutex_rec_unlock(m: *mut c_void) -> i8;
568
569    // -- Condition variables --
570
571    /// Initialise a condition variable in caller-provided storage.
572    fn condvar_init(cv: *mut c_void) -> i8;
573    /// Tear down a condition variable.
574    fn condvar_drop(cv: *mut c_void) -> i8;
575    /// Wake one waiter on the condition variable.
576    fn condvar_signal(cv: *mut c_void) -> i8;
577    /// Wake all waiters on the condition variable.
578    fn condvar_signal_all(cv: *mut c_void) -> i8;
579    /// Phase 124.B.7.a — ISR-safe variant of [`Self::condvar_signal`].
580    /// Callable from interrupt / signal-handler context. Backends
581    /// implement via async-signal-safe primitives (POSIX: `pipe`
582    /// write forwarded by a worker thread — Linux may use `eventfd`,
583    /// which is a Linux syscall and not POSIX; RTOS:
584    /// `xSemaphoreGiveFromISR`, `tx_event_flags_set` from ISR,
585    /// `k_sem_give` from ISR). Returns non-zero when the backend
586    /// has no ISR-safe path — caller can fall back to
587    /// `condvar_signal` (with the obvious latency cost).
588    ///
589    /// Default body: forward to `condvar_signal`. POSIX and other
590    /// backends override to use their async-signal-safe primitive.
591    fn condvar_signal_from_isr(cv: *mut c_void) -> i8 {
592        Self::condvar_signal(cv)
593    }
594    /// Atomically release `m` and block on `cv` until signalled or until
595    /// the absolute monotonic deadline `abstime` (milliseconds since the
596    /// [`PlatformClock::clock_ns`] epoch); the mutex is re-acquired before
597    /// this returns. Returns 0 when signalled, 1 on timeout and -1 on error
598    /// (issue 1906). The remaining time rounds UP to the port's tick.
599    ///
600    /// The ONLY condvar wait: the unbounded ~~`condvar_wait`~~ was removed from
601    /// the platform ABI (issue 1196, phase-436 D2). A caller whose contract
602    /// is unbounded loops over bounded waits, re-testing its predicate.
603    fn condvar_wait_until(cv: *mut c_void, m: *mut c_void, abstime: u64) -> i8;
604
605    // -- Wake primitive (Phase 130) --
606    //
607    // Binary-semaphore-shaped primitive for the executor's wake_flag
608    // / spin_once cv-wait pair. Default bodies return "unsupported"
609    // (`-1`, size 0) so existing single-thread bare-metal platforms
610    // don't need to override; platforms that want event-driven wake
611    // (POSIX, Zephyr, FreeRTOS, NuttX, ThreadX) override with their
612    // native binary semaphore.
613
614    /// Initialise a binary-semaphore-shaped wake primitive in
615    /// caller-provided storage. See `<nros/platform.h>` for the
616    /// per-platform backing primitive (POSIX `sem_t`, Zephyr
617    /// `k_sem`, FreeRTOS `xSemaphoreBinary`, …). Default returns
618    /// `-1` (unsupported).
619    fn wake_init(_w: *mut c_void) -> i8 {
620        -1
621    }
622    /// Tear down a wake primitive. Default returns `-1`
623    /// (unsupported).
624    fn wake_drop(_w: *mut c_void) -> i8 {
625        -1
626    }
627    /// Block until signaled or `timeout_ms` elapses. Returns `0` on
628    /// signal, `1` on timeout, `-1` on error. Default returns `-1`
629    /// (unsupported).
630    fn wake_wait_ms(_w: *mut c_void, _timeout_ms: u32) -> i8 {
631        -1
632    }
633    /// Wake one waiter. Idempotent — a signal pending when another
634    /// arrives is coalesced (the primitive stays at value 1).
635    /// Default returns `-1` (unsupported).
636    fn wake_signal(_w: *mut c_void) -> i8 {
637        -1
638    }
639    /// ISR-safe signal. Returns `-1` when the backend has no ISR
640    /// path; callers may fall back to `wake_signal` (with the
641    /// obvious latency cost). Default forwards to `wake_signal`.
642    fn wake_signal_from_isr(w: *mut c_void) -> i8 {
643        Self::wake_signal(w)
644    }
645    /// phase-436 W7 — OPTIONAL microsecond park.
646    ///
647    /// `wake_wait_ms` is the required primitive and floors every wait at a
648    /// millisecond through its own signature; this is the same wait without
649    /// that floor, for kernels whose timeout type is finer (issue 1242).
650    ///
651    /// `w` MUST be the executor's own wake object: the backend's listener
652    /// signals that one, and a park on anything else cannot be broken by data
653    /// arriving.
654    ///
655    /// Default `-1` — "cannot park", which leaves the executor on the
656    /// millisecond path it already had. A port that can do better overrides.
657    fn wake_park_until_us(_w: *mut c_void, _deadline_us: u64) -> i8 {
658        -1
659    }
660    /// The finest park `wake_park_until_us` can express, in microseconds.
661    ///
662    /// Default `0` — "no finer park offered". An RTOS tick is a coarser limit
663    /// than the ABI signature (ThreadX ticks at 100 Hz) and a timespec
664    /// primitive a finer one, so only the port can answer (issue 1242).
665    fn wake_park_granularity_us() -> u64 {
666        0
667    }
668    /// phase-521 W3 (RFC-0113 D3) — park on the wake object and on OS
669    /// handles: the same park as [`wake_park_until_us`](Self::wake_park_until_us),
670    /// ended also when one of `n` handles at `handles` becomes readable.
671    ///
672    /// Default `-1` — "this port cannot wait on OS handles", which is the
673    /// honest answer on every port without file descriptors. The executor then
674    /// keeps the wait it had.
675    fn wake_park_handles_us(
676        _w: *mut c_void,
677        _handles: *const i32,
678        _n: usize,
679        _deadline_us: u64,
680    ) -> i8 {
681        -1
682    }
683    /// Caller-storage size requirement (bytes). Default `0` —
684    /// signals "no wake primitive available". May be called before
685    /// `wake_init`.
686    fn wake_storage_size() -> usize {
687        0
688    }
689    /// Caller-storage alignment requirement (bytes). Default `1`.
690    fn wake_storage_align() -> usize {
691        1
692    }
693    /// Caller-storage size requirement (bytes) for a task handle.
694    ///
695    /// phase-359 W10 added these probes to `platform.h` and to the C ports but
696    /// not to this trait or the export macro, so `check-platform-abi-mirror`
697    /// saw a header symbol with no Rust emission. Same contract as the wake
698    /// pair above: default `0` means "this platform cannot start a task", which
699    /// is the honest answer for a port whose `task_init` always fails, and it
700    /// is safe to call before `task_init`.
701    fn task_storage_size() -> usize {
702        0
703    }
704    /// Caller-storage alignment requirement (bytes) for a task handle.
705    /// Default `1`.
706    fn task_storage_align() -> usize {
707        1
708    }
709    /// Caller-storage size requirement (bytes) for a plain mutex.
710    ///
711    /// phase-364 W2 declared the lock family's probes in `platform.h` without
712    /// the trait or macro halves — the same shape phase-359 W10 left for
713    /// `task`, one commit later. Defaults match the pair above: `0` size means
714    /// "this platform offers no such primitive", `1` alignment is the
715    /// no-constraint answer, and both are safe to call before any `*_init`.
716    fn mutex_storage_size() -> usize {
717        0
718    }
719    /// Caller-storage alignment requirement (bytes) for a plain mutex.
720    fn mutex_storage_align() -> usize {
721        1
722    }
723    /// Caller-storage size requirement (bytes) for a RECURSIVE mutex.
724    fn mutex_rec_storage_size() -> usize {
725        0
726    }
727    /// Caller-storage alignment requirement (bytes) for a recursive mutex.
728    fn mutex_rec_storage_align() -> usize {
729        1
730    }
731    /// Caller-storage size requirement (bytes) for a condition variable.
732    fn condvar_storage_size() -> usize {
733        0
734    }
735    /// Caller-storage alignment requirement (bytes) for a condition variable.
736    fn condvar_storage_align() -> usize {
737        1
738    }
739}
740
741/// Network poll callback for bare-metal platforms using smoltcp.
742///
743/// Not required for platforms with OS-level networking (POSIX, Zephyr, NuttX).
744///
745/// **Dispatch model**: bare-metal smoltcp platforms route this hook to
746/// `SmoltcpBridge::poll_network()` so CFFI callers, RMW shims, and direct
747/// platform users all share one network pump.
748pub trait PlatformNetworkPoll {
749    /// Poll the network stack to process pending I/O.
750    ///
751    /// Default no-op — platforms with OS-level networking don't need a
752    /// pump (kernel TCP/IP stack runs in the background). Bare-metal
753    /// smoltcp providers override this.
754    fn network_poll() {}
755}
756
757/// Global mutual exclusion against preemption + ISR delivery
758/// (Phase 121.9).
759///
760/// Backs the Rust `critical_section::Impl` registration used by
761/// DDS, nros-rmw-{xrce,zenoh}, and any other no_std consumer of
762/// `critical_section::with()`. The token returned by `acquire` is
763/// passed back to `release`; it holds whatever bookkeeping the
764/// platform needs to restore the prior posture (Cortex-M PRIMASK bit,
765/// Cortex-R CPSR I-bit, RISC-V `mstatus.MIE` snapshot, pthread
766/// recursion depth, etc.) and is opaque to callers.
767///
768/// Reentrant by contract: nested `acquire` / `release` pairs must
769/// stack — the platform impl is responsible for nesting (PRIMASK
770/// already stacks; pthread side uses a recursive mutex).
771pub trait PlatformCriticalSection {
772    /// Enter a critical section. Returns an opaque token to pass to
773    /// [`Self::release`].
774    fn acquire() -> u32;
775
776    /// Leave a critical section, restoring the posture captured at
777    /// [`Self::acquire`].
778    fn release(token: u32);
779}
780
781// ============================================================================
782// Networking — TCP
783// ============================================================================
784
785/// TCP networking.
786///
787/// Socket and endpoint parameters are opaque `*mut c_void` pointers to
788/// platform-specific types (`_z_sys_net_socket_t`, `_z_sys_net_endpoint_t`).
789/// The shim provides correctly-sized `#[repr(C)]` wrappers whose sizes are
790/// auto-detected from C headers at build time (see Phase 80 design).
791///
792/// Read functions return `usize::MAX` on error. Send returns `usize::MAX` on error.
793///
794/// Method names are unprefixed — the trait already namespaces them. Shims
795/// dispatch via `<ConcretePlatform as PlatformTcp>::open(...)` etc.
796pub trait PlatformTcp {
797    /// Resolve address + port strings into an endpoint handle.
798    fn create_endpoint(ep: *mut c_void, address: *const u8, port: *const u8) -> i8;
799    /// Free endpoint resources.
800    fn free_endpoint(ep: *mut c_void);
801    /// Open a TCP client connection. `endpoint` is by-value (opaque bytes on stack).
802    fn open(sock: *mut c_void, endpoint: *const c_void, timeout_ms: u32) -> i8;
803    /// Open a TCP listening socket.
804    fn listen(sock: *mut c_void, endpoint: *const c_void) -> i8;
805    /// Close a TCP socket.
806    fn close(sock: *mut c_void);
807    /// Read up to `len` bytes. Returns bytes read, or `usize::MAX` on error.
808    fn read(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
809    /// Read exactly `len` bytes. Returns `len` on success, `usize::MAX` on error.
810    fn read_exact(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
811    /// Send `len` bytes. Returns bytes sent, or `usize::MAX` on error.
812    fn send(sock: *const c_void, buf: *const u8, len: usize) -> usize;
813
814    /// phase-521 W6 (RFC-0113 D4) — install (`Some`) or remove (`None`) the
815    /// connected socket's receive notification, called from the stack's own
816    /// context each time data is queued, so it must only signal.
817    ///
818    /// Default `-1`: this stack has no such hook.
819    fn set_rx_notify(
820        _sock: *const c_void,
821        _notify: Option<unsafe extern "C" fn(*mut c_void)>,
822        _ctx: *mut c_void,
823    ) -> i8 {
824        -1
825    }
826}
827
828// ============================================================================
829// Networking — UDP unicast
830// ============================================================================
831
832/// UDP unicast networking.
833pub trait PlatformUdp {
834    fn create_endpoint(ep: *mut c_void, address: *const u8, port: *const u8) -> i8;
835    fn free_endpoint(ep: *mut c_void);
836    fn open(sock: *mut c_void, endpoint: *const c_void, timeout_ms: u32) -> i8;
837    fn close(sock: *mut c_void);
838    fn read(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
839    fn read_exact(sock: *const c_void, buf: *mut u8, len: usize) -> usize;
840    fn send(sock: *const c_void, buf: *const u8, len: usize, endpoint: *const c_void) -> usize;
841    /// Set the receive timeout on a UDP socket (milliseconds).
842    /// 0 means block indefinitely (no timeout).
843    fn set_recv_timeout(sock: *const c_void, timeout_ms: u32);
844
845    /// phase-521 W4 (RFC-0113 D4) — the OS handle that becomes readable when
846    /// a datagram arrives: a file descriptor on POSIX and NuttX.
847    ///
848    /// Default `-1`: this stack has no such handle, and its sessions report
849    /// readiness as a Notify instead.
850    fn readiness_handle(_sock: *const c_void) -> i32 {
851        -1
852    }
853
854    /// phase-521 W4 (RFC-0113 D4) — install (`Some`) or remove (`None`) the
855    /// socket's receive notification, called from the stack's own context
856    /// each time a datagram is queued, so it must only signal.
857    ///
858    /// Default `-1`: this stack has no such hook.
859    fn set_rx_notify(
860        _sock: *const c_void,
861        _notify: Option<unsafe extern "C" fn(*mut c_void)>,
862        _ctx: *mut c_void,
863    ) -> i8 {
864        -1
865    }
866
867    /// Open a UDP socket in listen (server) mode, bound to the given
868    /// endpoint. Returns 0 on success, negative on failure.
869    ///
870    /// Optional — the default returns `-1`, which the shim forwards to
871    /// `_z_listen_udp_unicast` as "not implemented". Platforms that
872    /// need UDP server sockets (e.g. for running an XRCE-DDS agent
873    /// locally) should override this. Once Phase 84.F4 lands (the
874    /// "platform traits become a real contract" refactor), the shim
875    /// will dispatch through this trait method automatically.
876    fn listen(_sock: *mut c_void, _endpoint: *const c_void, _timeout_ms: u32) -> i8 {
877        -1
878    }
879}
880
881// ============================================================================
882// Networking — socket helpers
883// ============================================================================
884
885/// Socket helper operations called by zenoh-pico's transport layer.
886///
887/// Unprefixed method names: dispatch via
888/// `<ConcretePlatform as PlatformSocketHelpers>::set_non_blocking(...)`.
889/// Note that the `close` method here is the socket-layer close (shutdown +
890/// close) used by zenoh-pico's generic helpers; `PlatformTcp::close` is the
891/// TCP-specific close. Both exist because zenoh-pico's C surface has both.
892pub trait PlatformSocketHelpers {
893    /// Set socket to non-blocking mode.
894    fn set_non_blocking(sock: *const c_void) -> i8;
895    /// Accept a pending connection.
896    fn accept(sock_in: *const c_void, sock_out: *mut c_void) -> i8;
897    /// Close a socket (shutdown + close).
898    fn close(sock: *mut c_void);
899    /// Wait for socket events (multi-threaded platforms).
900    fn wait_event(peers: *mut c_void, mutex: *mut c_void) -> i8;
901}
902
903// ============================================================================
904// libc stubs (bare-metal only)
905// ============================================================================
906
907/// Standard C library functions needed by zenoh-pico on bare-metal targets.
908///
909/// Platforms with a C runtime (RTOS, POSIX) do NOT need to implement this.
910///
911/// # Dispatch model (Phase 84.F4.6)
912///
913/// This trait is **documentary only** — it is NOT dispatched through by
914/// `zpico-platform-shim` or `xrce-platform-shim`. The C libraries resolve
915/// these symbols (`strlen`, `memcpy`, `errno`, ...) at link time directly
916/// from `#[unsafe(no_mangle)] extern "C" fn` definitions in
917/// `nros-baremetal-common`, which bare-metal platform crates pull in
918/// via the `libc-stubs` feature:
919///
920/// ```text
921///   nros-baremetal-common = { ..., features = ["libc-stubs"] }
922/// ```
923///
924/// The trait is retained in this API surface so that a future shim
925/// refactor could route libc through typed Rust methods without
926/// changing consumers. Today, implementing `PlatformLibc` on a platform
927/// ZST would be pure documentation; the actual contract — "the linker
928/// can resolve `strlen` etc." — is enforced at link time, not at
929/// compile time. No platform crate implements this trait in the
930/// current tree.
931pub trait PlatformLibc {
932    fn strlen(s: *const u8) -> usize;
933    fn strcmp(s1: *const u8, s2: *const u8) -> c_int;
934    fn strncmp(s1: *const u8, s2: *const u8, n: usize) -> c_int;
935    fn strchr(s: *const u8, c: c_int) -> *mut u8;
936    fn strncpy(dest: *mut u8, src: *const u8, n: usize) -> *mut u8;
937    fn memcpy(dest: *mut c_void, src: *const c_void, n: usize) -> *mut c_void;
938    fn memmove(dest: *mut c_void, src: *const c_void, n: usize) -> *mut c_void;
939    fn memset(dest: *mut c_void, c: c_int, n: usize) -> *mut c_void;
940    fn memcmp(s1: *const c_void, s2: *const c_void, n: usize) -> c_int;
941    fn memchr(s: *const c_void, c: c_int, n: usize) -> *mut c_void;
942    fn strtoul(nptr: *const u8, endptr: *mut *mut u8, base: c_int) -> core::ffi::c_ulong;
943    fn errno_ptr() -> *mut c_int;
944}
945
946// ============================================================================
947// Logging — Phase 88 leveled log delivery
948// ============================================================================
949
950/// Per-platform leveled log delivery.
951///
952/// Matches the post-Phase-129 platform-ABI pattern: the portable
953/// facade (`nros-log`) formats messages into a fixed buffer; each
954/// `nros-platform-<rtos>` carries the actual delivery (stderr,
955/// `printk`, `esp_log_write`, `syslog`, board-registered UART writer,
956/// etc.).
957///
958/// `severity` is the `u8` discriminant of `nros_log::Severity`
959/// (0 = Trace .. 5 = Fatal). Implementors should map onto the
960/// platform's nearest native level.
961///
962/// `name` is the logger name (UTF-8, NOT null-terminated, may be
963/// empty); `message` is the already-formatted body (UTF-8, NOT
964/// null-terminated). Delivery is infallible from the caller's POV;
965/// platforms that fill an internal buffer (RTT, syslog) silently
966/// drop on overflow.
967///
968/// Thread / ISR safety is per-platform — see the table in
969/// `docs/roadmap/phase-88-nros-log.md`. The ABI itself is synchronous
970/// and the caller-side facade carries a recursion guard so a sink
971/// that triggers `log()` from inside `write` is short-circuited.
972pub trait PlatformLog {
973    /// Deliver one record. Severity is a stable `u8` matching
974    /// `nros_log::Severity::as_u8()`.
975    fn write(severity: u8, name: &[u8], message: &[u8]);
976
977    /// Best-effort drain of any internal buffer. Default = no-op.
978    fn flush() {}
979}
980
981// ============================================================================
982// Networking — UDP multicast
983// ============================================================================
984
985/// UDP multicast networking (used for zenoh scouting on desktop platforms).
986pub trait PlatformUdpMulticast {
987    fn mcast_open(
988        sock: *mut c_void,
989        endpoint: *const c_void,
990        lep: *mut c_void,
991        timeout_ms: u32,
992        iface: *const u8,
993    ) -> i8;
994    fn mcast_listen(
995        sock: *mut c_void,
996        endpoint: *const c_void,
997        timeout_ms: u32,
998        iface: *const u8,
999        join: *const u8,
1000    ) -> i8;
1001    fn mcast_close(
1002        sockrecv: *mut c_void,
1003        socksend: *mut c_void,
1004        rep: *const c_void,
1005        lep: *const c_void,
1006    );
1007    fn mcast_read(
1008        sock: *const c_void,
1009        buf: *mut u8,
1010        len: usize,
1011        lep: *const c_void,
1012        addr: *mut c_void,
1013    ) -> usize;
1014    fn mcast_read_exact(
1015        sock: *const c_void,
1016        buf: *mut u8,
1017        len: usize,
1018        lep: *const c_void,
1019        addr: *mut c_void,
1020    ) -> usize;
1021    fn mcast_send(
1022        sock: *const c_void,
1023        buf: *const u8,
1024        len: usize,
1025        endpoint: *const c_void,
1026    ) -> usize;
1027}
1028
1029// ============================================================================
1030// Inter-VM / mailbox transport (NVIDIA IVC and similar)
1031// ============================================================================
1032
1033/// Inter-processor mailbox transport, modelled after NVIDIA Tegra IVC.
1034///
1035/// IVC (Inter-VM Communication on Tegra; in practice CCPLEX↔SPE on AGX
1036/// Orin) is a header-prefixed lock-free SPSC ring in shared DRAM, paired
1037/// with a hardware doorbell for wake. One channel is one peer — there
1038/// is no discovery, naming, QoS, or fanout — which is why it's a *link*
1039/// transport (peer to TCP/UDP/Serial/RawEth inside zenoh-pico) rather
1040/// than a new RMW backend.
1041///
1042/// Channel handles are opaque `*mut c_void` to match the shape zenoh-pico
1043/// passes across its FFI boundary. The driver crate (`nvidia-ivc`)
1044/// translates between this opaque handle and either NVIDIA's FSP
1045/// `tegra_ivc_*` API (`fsp` feature) or a Unix-socket pair
1046/// (`unix-mock` feature, host dev + CI).
1047///
1048/// **Zero-copy contract** (Phase 11.3.A). NVIDIA's FSP IVC API is
1049/// fundamentally a "borrow ring slot, fill, commit / release"
1050/// pattern. Both backends mirror that here so consumers don't branch
1051/// on backend:
1052///
1053/// - [`Self::rx_get`] returns a pointer into the channel's RX slot
1054///   (and writes the frame length to `*len_out`); [`Self::rx_release`]
1055///   advances the producer-visible cursor. Returns null +
1056///   `*len_out = 0` if the ring is empty.
1057/// - [`Self::tx_get`] returns a writable pointer to the next free TX
1058///   slot (and writes the slot capacity to `*cap_out`);
1059///   [`Self::tx_commit`] makes the slot visible to the peer (and
1060///   rings the per-frame doorbell on FSP — see `notify` for batching).
1061///   [`Self::tx_abandon`] frees the slot without sending. Returns
1062///   null + `*cap_out = 0` if the ring is full.
1063///
1064/// Single outstanding RX slot and single outstanding TX slot per
1065/// channel — borrow, finish, repeat. Multi-frame batching is the
1066/// caller's job (loop `tx_get` / fill / `tx_commit`, then one
1067/// `notify`).
1068///
1069/// `frame_size` is the fixed per-channel frame size negotiated at
1070/// carveout setup (typical NVIDIA IVC: 64 bytes per frame, 16 frames
1071/// per channel). The link layer uses it to pick its reassembly buffer.
1072pub trait PlatformIvc {
1073    /// Resolve a channel ID into an opaque handle. Returns null on
1074    /// failure. The numeric ID matches the NVIDIA channel index
1075    /// (`channel 2 = aon_echo`).
1076    fn channel_get(id: u32) -> *mut c_void;
1077
1078    /// Fixed frame size negotiated for this channel, in bytes.
1079    fn frame_size(ch: *mut c_void) -> u32;
1080
1081    /// Borrow the next-available RX frame. Writes the frame length to
1082    /// `*len_out` and returns a pointer into the ring. Returns null +
1083    /// `*len_out = 0` if no frame is available.
1084    fn rx_get(ch: *mut c_void, len_out: *mut usize) -> *const u8;
1085
1086    /// Release the most recently `rx_get`'d frame back to the
1087    /// producer. Pair 1:1 with `rx_get` calls that returned non-null.
1088    fn rx_release(ch: *mut c_void);
1089
1090    /// Borrow the next free TX slot. Writes the slot capacity to
1091    /// `*cap_out` and returns a writable pointer. Returns null +
1092    /// `*cap_out = 0` if the ring is full.
1093    fn tx_get(ch: *mut c_void, cap_out: *mut usize) -> *mut u8;
1094
1095    /// Commit `len` bytes from the most recently `tx_get`'d slot.
1096    /// Slot is then visible to the peer; per-frame doorbell may also
1097    /// fire on FSP. Pair 1:1 with `tx_get` calls that returned
1098    /// non-null.
1099    fn tx_commit(ch: *mut c_void, len: usize);
1100
1101    /// Abandon the most recently `tx_get`'d slot without sending.
1102    /// Pair 1:1 with `tx_get` calls that returned non-null.
1103    fn tx_abandon(ch: *mut c_void);
1104
1105    /// Ring the doorbell. On FSP this is redundant (commit/release
1106    /// already invoke `ch->notify_remote`); on unix-mock it's a no-op
1107    /// (SOCK_DGRAM wakes the peer naturally). Provided for symmetry
1108    /// so callers can batch-commit then notify once.
1109    fn notify(ch: *mut c_void);
1110}
1111
1112// ============================================================================
1113// Serial (UART / PTY)
1114// ============================================================================
1115
1116/// Serial (byte-stream) transport.
1117///
1118/// Used by XRCE-DDS's HDLC-framed serial transport and (once the
1119/// bare-metal migration lands) by zenoh-pico's serial link layer.
1120///
1121/// # Handle model
1122///
1123/// `open()` returns a platform-defined [`Handle`](PlatformSerial::Handle)
1124/// — an FD on POSIX, a port-table index on bare-metal, whatever the
1125/// impl wants. Every other method takes the handle back. This lets a
1126/// single platform impl service multiple concurrent devices (e.g.,
1127/// `zpico-serial`'s two-port table) without the trait constraining
1128/// the impl to a single active device.
1129///
1130/// Single-device platforms return the same handle forever and ignore
1131/// it internally; `INVALID` gives a well-defined "no live handle"
1132/// sentinel for shims that stash the current handle in a `static`.
1133///
1134/// # Path conventions
1135///
1136/// `path` in `open()` is platform-specific: a null-terminated UTF-8
1137/// device path on POSIX (e.g., `/dev/ttyUSB0` or a PTY), or a
1138/// board-defined port identifier on bare-metal (typically parsed by
1139/// the platform's internal handler). Callers pass the locator string
1140/// from their config unchanged; interpretation is the platform's job.
1141///
1142/// # I/O conventions
1143///
1144/// Read / write return `usize::MAX` on hard error. Read with
1145/// `timeout_ms == 0` should block for a platform-chosen default;
1146/// positive values are the poll/select deadline in milliseconds.
1147/// Returning `0` from `read()` indicates "no data within timeout" and
1148/// is **not** an error — both XRCE and zenoh-pico tolerate
1149/// timeout-zero reads.
1150pub trait PlatformSerial {
1151    /// Platform-specific handle type. POSIX returns the FD (`i32`);
1152    /// bare-metal returns a port-table index (`u8`). Must be `Copy`
1153    /// so shims can stash it in a `static`.
1154    type Handle: Copy;
1155
1156    /// Sentinel handle meaning "not a live device". Shims initialise
1157    /// their cached handle to this and compare against it to detect
1158    /// "transport not yet opened" states.
1159    const INVALID: Self::Handle;
1160
1161    /// Returns `true` if `h` is a live handle (not [`INVALID`](Self::INVALID)
1162    /// and points at a device that was opened and not yet closed).
1163    fn is_valid(h: Self::Handle) -> bool;
1164
1165    /// Open the serial device identified by `path`. Returns a live
1166    /// handle on success, [`INVALID`](Self::INVALID) on failure.
1167    fn open(path: *const u8) -> Self::Handle;
1168
1169    /// Close the given handle. No-op if already closed or invalid.
1170    fn close(h: Self::Handle);
1171
1172    /// Configure baud rate (in bits per second). Returns 0 on success,
1173    /// -1 on error. Called after `open()`; implementations may choose
1174    /// to apply the baud rate during `open()` instead and make this a
1175    /// no-op.
1176    fn configure(h: Self::Handle, baudrate: u32) -> i8;
1177
1178    /// Read up to `len` bytes into `buf`. Returns the number of bytes
1179    /// read, `0` on timeout, or `usize::MAX` on hard error.
1180    fn read(h: Self::Handle, buf: *mut u8, len: usize, timeout_ms: u32) -> usize;
1181
1182    /// Write `len` bytes from `buf`. Returns bytes written, or
1183    /// `usize::MAX` on error.
1184    fn write(h: Self::Handle, buf: *const u8, len: usize) -> usize;
1185}
1186
1187/// Smallest number of bytes ever left unused on the CALLING task's stack, or
1188/// `0` if this port does not instrument it.
1189///
1190/// Lives at the crate root, NOT in [`task`], because that module is
1191/// `#[cfg(feature = "alloc")]` -- `PlatformTask` allocates its own storage --
1192/// while this is a bare `extern "C"` query returning a `usize`. Gating it
1193/// behind `alloc` made it unavailable on exactly the no_std targets whose
1194/// stacks are worth watching, and broke the executor's stack-headroom rule
1195/// on the Zephyr build.
1196///
1197/// The heap has had `heap_used_bytes` since RFC-0034 D7; this is the stack's
1198/// counterpart. HEADROOM, not usage, and for the CALLING task only -- both
1199/// kernels answer for self with no handle, and answering for an arbitrary
1200/// task needs a native handle this ABI does not carry.
1201///
1202/// `0` means "not instrumented", not "no headroom".
1203pub fn stack_unused_bytes() -> usize {
1204    unsafe extern "C" {
1205        fn nros_platform_task_stack_unused_bytes() -> usize;
1206    }
1207    // SAFETY: a plain query with no arguments and no state; every port either
1208    // answers or returns 0.
1209    unsafe { nros_platform_task_stack_unused_bytes() }
1210}
1211
1212/// Give up the processor to another ready task, through the port's
1213/// `nros_platform_yield_now`. For a thread that must wait for another to
1214/// finish a short critical step (phase-436's wake fan-out waits out a backend
1215/// callback this way) without spinning a single core it shares with that
1216/// thread.
1217pub fn yield_now() {
1218    unsafe extern "C" {
1219        fn nros_platform_yield_now();
1220    }
1221    // SAFETY: no arguments, no return value; every port defines it.
1222    unsafe { nros_platform_yield_now() }
1223}
1224
1225/// [`stack_unused_bytes`], capped at `limit`: `min(unused, limit)`, reading at
1226/// most `limit` bytes of the calling task's stack (phase-436 E5).
1227///
1228/// What the `stack-headroom-runtime` rule needs: whether at least `limit`
1229/// bytes are unused. Below `limit` the answer is the exact headroom, so a
1230/// breach is reported without a second, full scan. `0` means "not
1231/// instrumented", as for [`stack_unused_bytes`].
1232pub fn stack_unused_bytes_upto(limit: usize) -> usize {
1233    unsafe extern "C" {
1234        fn nros_platform_task_stack_unused_bytes_upto(limit: usize) -> usize;
1235    }
1236    // SAFETY: a plain query; `limit` is a value, and every port either
1237    // answers or returns 0.
1238    unsafe { nros_platform_task_stack_unused_bytes_upto(limit) }
1239}