Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

Platform API Design

The platform API (nros-platform) sits below the RMW backend. It exposes the OS or hardware primitives that zenoh-pico and XRCE-DDS need: a clock, optionally a heap, optionally threading, optionally networking. This page explains why the trait surface is grouped the way it is.

Canonical interface spec. The function-pointer signatures, parameter docs, ownership rules, blocking allowance, and failure modes for every platform entry live in the platform-cffi Doxygen reference. This doc is rationale, not spec — when the two disagree, the Doxygen wins. The Rust trait crate nros-platform-api mirrors the C signatures one-for-one.

For per-platform behaviour comparison, see Platform Differences. For writing a new port, see Custom Platform.

Trait groups and rationale

The traits cluster into seven concern groups. Each group is independent: a platform can provide some and stub others, and the RMW backend declares which it actually needs.

Time – PlatformClock

Every backend needs a monotonic clock. zenoh-pico uses milliseconds for socket timeouts and lease management; it uses microseconds for finer-grained protocol math (KeepAlive intervals, lease expiry calculations). We expose both clock_ms and clock_us rather than a single clock_ns because:

  • 32-bit MCUs without a hardware microsecond tick cannot serve clock_ns accurately. Synthesizing nanoseconds from a 1 ms tick is a lie that hides clock resolution from the caller.
  • u64 nanoseconds wraps after ~584 years, but on a Cortex-M0 every multiply-divide on a 64-bit value costs cycles that the lease task does not have to spare.
  • The two functions can share a single hardware source: clock_us returns the raw counter, clock_ms divides by 1000. Platforms with a slow tick (1 ms) implement clock_us as clock_ms * 1000 and accept the resolution loss.

The clock is monotonic and wraparound-free for system lifetime. There is no failure mode – if the platform cannot produce time, it cannot run nano-ros at all.

Memory – PlatformAlloc

Only zenoh-pico needs PlatformAlloc. XRCE-DDS does not allocate. The trait is a thin malloc/realloc/free shim because zenoh-pico’s internal buffer types expect that contract.

Bare-metal platforms back this with a bump allocator (linked-list-allocator or embedded-alloc). RTOS platforms back it with pvPortMalloc (FreeRTOS), tx_byte_allocate (ThreadX), k_malloc (Zephyr), or malloc (NuttX, POSIX). zenoh-pico’s working set is ~64 KB total; the allocator must have at least that much budget, ideally more.

Threading – PlatformThreading

Three sub-areas: tasks (spawn/join/exit), mutexes (regular + recursive), and condition variables. Single-threaded targets (bare-metal) provide stub implementations: task_init returns -1 (so zenoh-pico’s lease task spawn fails gracefully and the application drives lease-keepalive itself), and mutex_lock/condvar_wait are no-ops that always succeed.

The condvar API is the load-bearing one: zenoh-pico’s blocking z_get and the C++ Future::wait both block on a condvar that the receive callback signals. On single-threaded platforms there is no thread to block, so the blocking C++ wait paths are not used (the C++ action client status note covers the migration to non-blocking polling).

Sleep / Random / Wall-time

Three small zenoh-pico-only traits grouped for convenience:

  • PlatformSleep – delay APIs. On bare-metal with smoltcp, the implementation must call network_poll() while busy-waiting, otherwise packets queue and the lease times out.
  • PlatformRandom – a 32-bit xorshift PRNG seeded with hardware entropy (or a user-supplied seed). Used for session IDs and protocol nonces, not cryptography.
  • PlatformTime – wall-clock time for log timestamps. On bare-metal with no RTC, return monotonic time as a fallback.

Networking – PlatformTcp / PlatformUdp / PlatformSocketHelpers

zenoh-pico’s network layer is split into three traits because the original C interface (unix/network.c) has three concerns:

  • TCP and UDP each have their own open/read/send/close because the backend opens different socket types for each.
  • PlatformSocketHelpers carries the cross-cutting operations – set_non_blocking, accept, generic close, and wait_event – that apply to either socket family.

Sockets and endpoints are opaque *mut c_void pointers; their underlying types vary per platform (POSIX int, lwIP struct netconn*, Zephyr socket descriptor, smoltcp SocketHandle). The shim layer auto-detects the type sizes from C headers at build time so the FFI boundary stays type-erased.

PlatformUdpMulticast is split out as a fourth networking trait because embedded targets that connect to a fixed tcp/host:port locator never multicast and should not pay the code-size cost of multicast plumbing.

NetworkPoll – PlatformNetworkPoll

Bare-metal only. network_poll() advances the smoltcp state machine, processing pending RX/TX. Platforms with kernel-level networking (Linux, lwIP-on-FreeRTOS, NetX-on-ThreadX, Zephyr sockets) drive their own NIC and don’t need this.

PlatformSleep and the wait_event helper both call network_poll() while waiting, so packets keep flowing during otherwise-idle time. Without this hook, smoltcp would only receive when the application explicitly asked for it – a recipe for dropped TCP segments.

Libc – PlatformLibc

zenoh-pico uses strlen, memcpy, errno, etc. directly. Bare-metal targets that link picolibc or newlib-nano get these for free. Targets without a C runtime (some no_std builds) provide the trait, which forwards to Rust implementations of the same functions.

This trait exists because the alternative – patching zenoh-pico to call platform shims for every libc function – would require modifying upstream sources we don’t control.

Why clock_ms and clock_us, not clock_ns

Summarized from above:

APIProsConsVerdict
clock_ns onlySingle function, finest resolution64-bit math on every call; lies on 1 ms-tick MCUsRejected
clock_ms onlyCheap, fits zenoh-pico’s lease mathInsufficient resolution for sub-millisecond protocol timingInsufficient
Both clock_ms and clock_usEach call is cheap and honest about its resolutionTwo functions to implementChosen

Behavior contracts

Each trait below has a contract table. Columns:

  • Method – name (matches the trait definition).
  • Blocking? – whether the method may suspend the caller.
  • May fail? – whether the method has a meaningful failure mode.
  • Unsupported fallback – what to do when the platform cannot provide the capability.
  • Notes – extra constraints (monotonicity, reentrancy, side effects).

PlatformClock

MethodBlocking?May fail?Unsupported fallbackNotes
clock_msNoNoRequired for any backendMonotonic; wraparound-free for system lifetime
clock_usNoNoRequired for any backendSame monotonic base as clock_ms

PlatformAlloc

MethodBlocking?May fail?Unsupported fallbackNotes
allocNoYes (null)Required for zenoh-picoCaller checks for null and propagates as RMW error
reallocNoYes (null)Required for zenoh-picoExisting block must be preserved on failure
deallocNoNoRequired for zenoh-picodealloc(null) is a no-op

PlatformSleep

MethodBlocking?May fail?Unsupported fallbackNotes
sleep_usYesNoRequired for zenoh-picoBare-metal must call network_poll() during busy-wait
sleep_msYesNoRequired for zenoh-picoSame
sleep_sYesNoRequired for zenoh-picoSame; typically implemented as sleep_ms(s * 1000)

PlatformRandom

MethodBlocking?May fail?Unsupported fallbackNotes
random_u8 / _u16 / _u32 / _u64NoNoRequired for zenoh-picoxorshift32 is sufficient; not cryptographic
random_fillNoNoRequired for zenoh-picoFills len bytes; no upper bound check

PlatformTime (wall-clock)

MethodBlocking?May fail?Unsupported fallbackNotes
time_now_msNoNoRequired for zenoh-picoReturn clock_ms if no RTC
time_since_epochNoNoRequired for zenoh-picoReturn (monotonic_s, 0) if no RTC

PlatformThreading – tasks

MethodBlocking?May fail?Unsupported fallbackNotes
task_initNoYes (-1)Return -1 on single-threadedzenoh-pico’s lease task must degrade gracefully
task_joinYesYesReturn 0 (success)Single-threaded never spawned a task to join
task_detachNoYesReturn 0Same
task_cancelNoYesReturn 0Same
task_exitNoNoNo-opCaller is the only thread
task_freeNoNoNo-opNo allocation to free

PlatformThreading – mutexes (regular and recursive)

MethodBlocking?May fail?Unsupported fallbackNotes
mutex_init / mutex_rec_initNoYesReturn 0Single-threaded has no mutex state
mutex_drop / mutex_rec_dropNoYesReturn 0Same
mutex_lock / mutex_rec_lockYesYesReturn 0 (success)Single-threaded: no contention possible
mutex_try_lock / mutex_rec_try_lockNoYesReturn 0Always “succeeds” on single-threaded
mutex_unlock / mutex_rec_unlockNoYesReturn 0Same

PlatformThreading – condition variables

MethodBlocking?May fail?Unsupported fallbackNotes
condvar_initNoYesReturn 0No state on single-threaded
condvar_dropNoYesReturn 0Same
condvar_signalNoYesReturn 0No waiter to wake
condvar_signal_allNoYesReturn 0Same
condvar_waitYesYesReturn 0Single-threaded must use polling instead – avoid this path
condvar_wait_untilYesYes (timeout)Return 0 immediatelySame; blocking C++ Future::wait deadlocks on single-threaded (use non-blocking polling instead)

PlatformTcp

MethodBlocking?May fail?Unsupported fallbackNotes
create_endpointYes (DNS)YesRequired for zenoh-picoBacked by getaddrinfo or platform equivalent
free_endpointNoNoRequiredMirrors freeaddrinfo
openYesYesRequiredConnect with timeout in ms
listenNoYesOptional (server mode)Bare-metal client typically returns -1
closeNoNoRequiredShutdown + close
readNo (after set_non_blocking)Yes (usize::MAX)RequiredReturns 0 if no data; must be non-blocking for zenoh-pico’s poll loop
read_exactYesYesRequiredUsed for length-prefixed framing
sendYesYesRequiredMay block on socket buffer full

PlatformUdp

MethodBlocking?May fail?Unsupported fallbackNotes
create_endpointYes (DNS)YesRequired for zenoh-picoSame as TCP but SOCK_DGRAM
free_endpointNoNoRequired
openNoYesRequiredUDP socket open is non-blocking
closeNoNoRequired
readNoYesRequiredrecvfrom; returns 0 if no datagram
read_exactYesYesRequiredRarely used (UDP is message-oriented)
sendYesYesRequiredsendto

PlatformSocketHelpers

MethodBlocking?May fail?Unsupported fallbackNotes
set_non_blockingNoYesRequiredCritical: enables non-blocking read
acceptNo (after set_non_blocking)YesOptionalServer-mode only
closeNoNoRequiredGeneric socket close
wait_eventYesYesRequiredMulti-threaded: yields to scheduler. Single-threaded: spins + network_poll

PlatformUdpMulticast (optional)

MethodBlocking?May fail?Unsupported fallbackNotes
mcast_openYesYesStub returns -1Skipped on embedded targets without scouting
mcast_listenYesYesStub returns -1Same
mcast_closeNoNoNo-op
mcast_read / mcast_read_exactVariesYesStub returns usize::MAX
mcast_sendYesYesStub returns usize::MAX

PlatformNetworkPoll (bare-metal only)

MethodBlocking?May fail?Unsupported fallbackNotes
network_pollNoNoOS-driven platforms: no-opAdvances smoltcp once; called by sleep_* and wait_event

PlatformLibc (bare-metal without libc)

MethodBlocking?May fail?Unsupported fallbackNotes
strlen, strcmp, strncmp, strchr, strncpyNoNoLinker provides if libc presentSame semantics as the C standard
memcpy, memmove, memset, memcmp, memchrNoNoSameSame
strtoulNoYes (errno)SameUsed by zenoh-pico to parse locator strings
errno_ptrNoNoSameReturns pointer to thread-local (or static) errno

Cross-cutting rules

Two contract rules apply to every trait method:

  1. Reentrancy. Methods may be called from any context the executor enters: a publisher callback, a service handler, or directly from user code. Implementations must not assume a particular calling thread or critical-section state. Single-threaded platforms get this for free; RTOS platforms must use reentrant primitives.

  2. No panics across the FFI boundary. All trait methods are exposed to C through the shim crates. Panicking through C is undefined behavior. Implementations return error codes (or usize::MAX for byte counts) instead of panicking. The exception is PlatformClock – if the clock cannot be read, the system is fundamentally broken and there is nothing useful to return.