Skip to main content

nros_platform_api/
lib.rs

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