std and alloc Requirements
This document maps which parts of the nano-ros API require the std or alloc
Cargo features. All core crates are #![no_std] by default and gate
std/alloc-dependent code behind feature flags.
Feature Hierarchy
std (default)
└─ alloc
└─ (base no_std)
Enabling std automatically enables alloc. Enabling alloc does not enable
std. With --no-default-features, the entire library compiles without
std or alloc.
Summary by Crate
| Crate | no_std base | alloc additions | std additions |
|---|---|---|---|
| nros-serdes | CDR ser/de for primitives, heapless types, &str, [T; N] | String and Vec<T> ser/de | (none) |
| nros-core | Time, Duration, Clock (atomic fallback), lifecycle, logger, error types, action types | (none) | Clock::now() via SystemTime, std::error::Error impls |
| nros-rmw | All traits, QoS, sync primitives, safety/E2E protocol | handle_request_boxed() (Box<Reply>) | (none) |
| nros-params | ParameterServer, ParameterValue, all parameter types (heapless) | (none) | ParameterVariant impls for std::string::String, std::vec::Vec |
| nros-node | Executor::open(), create_node(), spin_once(), Promise, pub/sub/service/action, timers (fn pointer callbacks) | Boxed timer callbacks, handle_request_boxed(), parameter services | spin(), spin_period(), ExecutorConfig::from_env(), halt flag |
| nros | Re-exports from above | (same as above) | SpinPeriodResult re-export |
RMW Backend Crates
| Crate | no_std base | alloc additions | std additions |
|---|---|---|---|
| nros-rmw-zenoh | Zenoh-pico RMW implementation, all pub/sub/service/action ops | (none) | (none) |
| zpico-sys | FFI bindings to zenoh-pico C library | (none) | (none) |
| nros-rmw-xrce | XRCE-DDS RMW implementation, all pub/sub/service/action ops | (none) | (none) |
| xrce-sys | FFI bindings to Micro-XRCE-DDS-Client C library | (none) | (none) |
All four backend crates are unconditionally #![no_std] and do not use alloc. The std
feature gates only extern crate std (for macro availability in transport modules) and
is propagated through the feature chain but does not add any API surface.
Detailed API Availability
Always Available (no_std, no alloc)
Executor and Node:
Executor::open(&config)– open RMW sessionExecutorConfig::new(locator)– manual configurationexecutor.create_node(name)– create a nodeexecutor.spin_once(timeout_ms)– single spin iterationexecutor.spin_period(period)– periodic spin withoutstd::thread::sleep
Two-layer API. Every entity kind follows one verb discipline:
- Layer 1 (caller polls) –
Node::create_*returns an owned handle. Caller drivestake/call/try_accept_goal/take_request_rawitself. Good for tight RT loops and task-per-entity FreeRTOS. - Layer 2 (executor dispatches) –
Executor::register_*takes a closure;spin_oncefires it on rx / reply / timer. Good for callback-shaped applications.
Both layers share the same session; mix per entity.
Publish/Subscribe:
- L1 —
node.create_publisher::<M>(topic),node.create_subscription::<M>(topic)(poll withtake()) - L2 —
executor.register_timer(period, || publisher.publish(...)),executor.register_subscription::<M, _>(topic, |msg| { ... }) publisher.publish(&msg)/publish_serialized(&bytes)— publish messages
Services:
- L1 —
node.create_service::<S>(name)(poll withhandle_request()),node.create_client::<S>(name)+client.call(&request)→Promise<Reply>(poll withpromise.take(), or block on it withpromise.wait(&mut executor, timeout)). - L2 —
executor.register_service::<S, _>(name, |req| reply). Service clients keep the L1Promiseshape; the typed callback API isn’t surfaced (onlyregister_service_client_rawexists for byte-level use).
Actions:
- L1 —
node.create_action_server::<A>(name)+try_accept_goal/complete_goal, ornode.create_action_client::<A>(name)+send_goal→Promise<GoalId>/get_result→Promise<(GoalStatus, Result)>. - L2 —
executor.register_action_server::<A, _, _>(name, goal_cb, cancel_cb)returns a handle for publishing feedback and completing goals. Action clients keep the L1Promiseshape for the same reason as service clients.
Promises:
Promise<'a, T, Cli>– allocation-free promise, borrows client’s reply slotPromise::take()– non-blocking poll for reply, while the executor spins- No async runtime drives the executor:
spin_asyncis retired, because a foreign scheduler, not nano-ros, would decide when callbacks run
Timers:
TimerHandlewith function pointer callbacks (fn())TimerDuration,TimerMode,TimerState
Serialization:
CdrWriter/CdrReader– CDR serialization to/from byte buffersSerialize/Deserializetraits- Implementations for:
bool,u8-u64,i8-i64,f32,f64,char,&str,[T; N],heapless::String<N>,heapless::Vec<T, N>
Time:
Time::new(),Time::from_nanos(),Time::to_nanos()Time::from_secs_f64(),Time::to_secs_f64()Duration::new(),Duration::from_nanos(),Duration::to_nanos()Duration::from_secs_f64(),Duration::to_secs_f64()Clockwith atomic counter fallback (no wall-clock time)
Parameters (local only):
Node::declare_parameter/get_parameter/set_parameter– the node’s parameters, in the executor’s one storeParameterServer– the store itself; its operations are behindnros_params::ParameterServerOps, for a caller that holds a standalone storeParameterValueenum with heapless collectionsParameterDescriptor,ParameterType,ParameterParameterBuilderfor declaring parameters with constraints
Other:
LifecycleState,LifecycleTransition,LifecyclePollingNodeLogger(usescore::sync::atomic)GoalId,GoalStatus,GoalResponse,CancelResponse,CancelReturnCodeQoSProfile,TopicInfo,ServiceInfoSafetyValidator,IntegrityStatus(withsafety-e2efeature)- Sync primitives:
spin::Mutexorcritical-section(feature-selected)
Requires alloc
| API | Location | Why |
|---|---|---|
Serialize/Deserialize for String, Vec<T> | nros-serdes | Heap-allocated containers |
TimerCallback (Box<dyn FnMut() + Send>) | nros-node/timer.rs | Boxed closure for timer callbacks |
Timer::new_with_box(), set_callback_box() | nros-node/timer.rs | Construct/update boxed timer callbacks |
EmbeddedServiceServer::handle_request_boxed() | nros-node/handles.rs | Returns Box<Reply> for large response types |
param-services heap fallback | nros-node/executor/spin.rs | Store + service state when the backing carves none |
Argv parameter overrides (-p, --params-file) | nros-node/ros_args.rs | Owned override strings from the hosted edge |
Parameter services detail: the param-services feature
does not need alloc. The six handlers stream requests and replies, and the
parameter store, the service state, the shared request/reply buffers and the
slot a remote set decodes its value into are all carved from the executor
backing whenever the build knows the image builds a store
(NROS_PARAM_STORE=1, params: in the contract, or param_services in the
bringup; capacity NROS_MAX_PARAMETERS). Without alloc, a build that carves
no store is refused at compile time and the message names NROS_PARAM_STORE.
With alloc, such a build falls back to the heap as before.
The core ParameterServer API works without alloc (its operations are
reached through nros_params::ParameterServerOps).
Requires std
| API | Location | Why |
|---|---|---|
Clock::now() (system/steady clock) | nros-core/clock.rs | Uses std::time::SystemTime / UNIX_EPOCH |
std::error::Error for NanoRosError, RclReturnCode | nros-core/error.rs | Trait requires std |
ExecutorConfig::from_env() | nros-node/types.rs | Uses std::env::var() + Box::leak() |
Executor::spin(options) | nros-node/spin.rs | Uses std::thread::sleep(), Arc<AtomicBool> |
Executor::spin_period(duration) | nros-node/spin.rs | Uses std::time::Instant, std::thread::sleep() |
Executor::halt_flag() | nros-node/spin.rs | Returns Arc<AtomicBool> for cross-thread cancellation |
SpinPeriodResult | nros-node/types.rs | Contains std::time::Duration |
ParameterVariant for std::string::String, std::vec::Vec | nros-params/types.rs | Convenience conversions for std types |
Typical Configurations
The nros umbrella does not depend on concrete RMW crates.
A consuming Cargo.toml lists three path deps: nros (with
rmw-cffi + a platform-* feature), the chosen backend crate
(nros-rmw-zenoh / nros-rmw-xrce-cffi), and —
on POSIX — nros-platform-cffi with posix-c-port so the C
nros_platform_* symbols link into a pure-cargo build. The backend
crate’s #[ctor] registers its vtable before main.
Bare-metal / RTOS (no allocator):
nros = { path = "…/nros", default-features = false, features = ["rmw-cffi", "platform-bare-metal"] }
nros-rmw-zenoh = { path = "…/nros-rmw-zenoh", features = ["platform-bare-metal"] }
Full pub/sub, services, actions, timers (fn pointers), parameters (local).
Promises (Promise, take()) are available without std or alloc.
Use spin_once() or spin_period() in your main loop.
Embedded with allocator (e.g., Zephyr with heap):
nros = { path = "…/nros", default-features = false, features = ["alloc", "rmw-cffi", "platform-zephyr"] }
nros-rmw-zenoh = { path = "…/nros-rmw-zenoh", features = ["platform-zephyr"] }
Adds boxed timer callbacks and handle_request_boxed() for large service replies.
Desktop / Linux:
nros = { path = "…/nros", default-features = false, features = ["alloc", "env", "log", "rmw-cffi"] }
nros-rmw-zenoh = { path = "…/nros-rmw-zenoh", features = ["platform-posix", "link-tcp", "ros-humble"] }
nros-platform-cffi = { path = "…/nros-platform-cffi", features = ["posix-c-port"] }
Full API including spin(), spin_period(), from_env(), system clock.
nros has no std feature: your program is a hosted binary and has std
itself, env reads the process environment (through the C library’s
getenv), and log routes nano-ros’s own diagnostics to whatever log
logger you install.
For async, use an external runtime (tokio current_thread + spawn_local for background spin).
Desktop with parameter services: add param-services to the
nros feature list above. Adds ~/get_parameters,
~/set_parameters, etc. for ros2 param CLI interop.
C-Level Allocation
Both RMW backends compile and link C libraries that perform heap allocation
independently of Rust’s alloc feature. Disabling the Rust alloc feature
eliminates Rust-side heap usage (Box, Vec, String) but does not
eliminate allocation in the C transport layer.
| Backend crate | C Library | Allocator by Platform |
|---|---|---|
nros-rmw-zenoh | zenoh-pico 1.10.1 | POSIX: malloc; Zephyr: k_malloc; FreeRTOS: pvPortMalloc; bare-metal: custom bump allocator |
nros-rmw-xrce-cffi | Micro-XRCE-DDS 3.0.1 | POSIX: malloc; Zephyr: k_malloc; FreeRTOS: pvPortMalloc; NuttX: malloc |
This is by design: the C libraries manage their own session state, stream
buffers, and protocol metadata using platform-provided allocators. The Rust
alloc feature controls only the Rust API surface (boxed callbacks, heap
containers, etc.) and is orthogonal to C-level memory management.
Checking your own image
Because of the row above, a feature list is evidence that an image is heap-free and not proof of it: a vendored C dependency or a weak-symbol fallback can link an allocator without any Cargo feature changing. Ask the linker instead:
scripts/check-no-alloc-image.py path/to/your.elf
It reads the image’s symbol table and fails on any allocation symbol — libc’s
malloc family, Rust’s __rust_alloc family, C++ operator new, and the RTOS
heaps (k_malloc, pvPortMalloc, tx_byte_allocate, kmm_malloc,
heap_caps_malloc) — reporting for each whether the image calls it or
contains it. --tier unified takes --objects instead and enforces the
weaker rule that allocation may happen only inside the nros_platform_*
backend.
None of the examples shipped in this repo are built this way today — they all
enable alloc — so run it against your own image, not against one of ours.