Node packages
This page is part of the Multi-Node Projects group. Previous: Project layout — Next: Bringup packages
A Node pkg is the unit of reusable behaviour in a multi-node workspace.
It is a Rust library — a [lib] crate — that implements one node and
registers it with nros::node!(T).
The Entry pkg is what boots the binary; the Node pkg is what runs inside it.
Prereqs
Pick one path from a fresh checkout — just is NOT a prereq.
A. Front door (bare machine OK — no Rust, no just):
./scripts/bootstrap.sh
Installs rustup if needed and builds the in-tree nros CLI from
source at packages/cli/target/release/nros, leaving it on PATH for
this shell (nano-ros is a source distribution — no prebuilt nros).
B. Already have cargo (equivalent — same build, same binary):
git submodule update --init packages/cli/third-party/ros-launch-manifest
cargo build --release --manifest-path packages/cli/Cargo.toml --bin nros
export PATH="$PWD/packages/cli/target/release:$PATH"
Every subsequent shell sources the workspace env via one of:
direnv allow # if you use direnv
source ./activate.sh # bash / zsh
source ./activate.fish # fish
Then provision the native host:
nros setup native --rmw zenoh
Scaffolding a Node pkg
Use nros new to create a skeleton:
nros new talker --platform native --lang rust
nros new creates a project skeleton.
For a workspace, move (or create) the result under src/talker_pkg/ so the
workspace root’s Cargo.toml can include it as a member:
# workspace Cargo.toml
[workspace]
resolver = "2"
members = ["src/talker_pkg", "src/listener_pkg", "src/native_entry"]
Node pkg anatomy
A Node pkg has three files:
src/talker_pkg/
├── package.xml # ROS 2 manifest — <exec_depend> per message package
├── Cargo.toml # [lib] + [package.metadata.nros.node] metadata
└── src/lib.rs # impl Node + ExecutableNode; ends with nros::node!(Talker);
No fn main() here — a Node pkg is a library linked into an Entry pkg.
The Entry pkg’s macro-generated runtime owns nros::init, executor open,
RMW registration, and the spin/yield loop.
Cargo.toml — the [package.metadata.nros.node] block
The metadata block is what the nros CLI reads to discover, name, and
wire this node into a topology.
From examples/stm32f4/rust/talker_pkg/Cargo.toml:
[lib]
crate-type = ["rlib"]
[dependencies]
nros = { path = "../../../../packages/core/nros", default-features = false,
features = ["alloc", "rmw-cffi", "platform-bare-metal", "ros-humble"] }
[package.metadata.nros.node]
class = "stm32f4_talker_pkg::Talker"
name = "talker"
default_namespace = "/"
The three fields in [package.metadata.nros.node]:
| Field | Purpose |
|---|---|
class | Fully-qualified Rust path to the type that impls Node + ExecutableNode |
name | Default ROS 2 node name (remappable at launch) |
default_namespace | Default namespace (remappable at launch) |
For a native workspace the nros dep would use features = ["std", "rmw-cffi", "platform-posix", "ros-humble"] instead of platform-bare-metal. The RMW feature (rmw-zenoh, rmw-xrce, rmw-cyclonedds) is chosen at build time — it is not baked into the Node pkg itself.
src/lib.rs — the node implementation
A Node pkg implements two traits: Node (declarative registration) and
ExecutableNode (per-callback body), then calls nros::node! to export the
trampolines the Entry macro expects.
Here is the essential shape, drawn from
examples/stm32f4/rust/talker_pkg/src/lib.rs
(see that file for the full worked version):
#![allow(unused)]
fn main() {
use nros::{
CallbackCtx, ExecutableNode, Node, NodeContext, NodeOptions, NodeResult,
TimerDuration,
};
pub struct Talker;
impl Node for Talker {
const NAME: &'static str = "talker";
fn register(ctx: &mut NodeContext<'_>) -> NodeResult<()> {
let mut node = ctx.create_node(NodeOptions::new("talker"))?;
let chatter = node.create_publisher_for_topic::<MyMsg>("/chatter")?;
let _timer =
node.create_timer_for_callback_name("on_tick", TimerDuration::from_millis(1000))?;
node.callback_for_name("on_tick")
.publishes_entity(&chatter)?;
Ok(())
}
}
impl ExecutableNode for Talker {
type State = i32;
fn init() -> Self::State { 0 }
// See the full example for the callback body.
}
nros::node!(Talker); // <-- exports the trampolines; this is the last line
}
Key points:
Node::registeris declarative — it runs once at startup to declare publishers, subscriptions, timers, and callback edges. No message bytes flow here.ExecutableNode::on_callbackis the body — called by the Entry pkg’s executor each time a callback fires.stateis your per-node mutable storage.nros::node!(Talker)must be the last public API call in the file. It generates theextern "C"trampolines the Entry macro imports.- There is no
fn main()in a Node pkg.
package.xml — the ROS 2 manifest
A Node pkg that uses generated message types lists them as <exec_depend>
entries. Minimal example:
<?xml version="1.0"?>
<package format="3">
<name>talker_pkg</name>
<version>0.1.0</version>
<description>Talker node</description>
<maintainer email="dev@example.com">Developer</maintainer>
<license>MIT OR Apache-2.0</license>
<depend>std_msgs</depend>
<export>
<build_type>ament_cargo</build_type>
</export>
</package>
If your node uses no external message packages, the <depend> line can be
omitted.
Services & clients
Server (responds to requests)
Declare the service in Node::register and handle requests in the node body.
-
Rust — declare a service edge in
register; requests dispatch intoExecutableNode::on_callbacklike any other callback (read the request and write the reply through theCallbackCtx). -
C++/C (the
configure(Node&)component shape) — bind a typed member withnros::bind_service:// Response on_request(const Request&) — no hand-rolled CDR. AddTwoInts::Response on_add(const AddTwoInts::Request& req) { AddTwoInts::Response r; r.sum = req.a + req.b; return r; } ::nros::Result configure(::nros::Node& node) { return ::nros::bind_service<AddTwoInts, MyServer, &MyServer::on_add>( node, "/add_two_ints", this); // service-type name from AddTwoInts::TYPE_NAME }bind_servicedeserializes the request into the generatedSvc::Request, calls your typed member, and serializes the returnedSvc::Response— no manual byte offsets. (bind_service_raw<&method>is still available for the rare hand-rolled case.)
Client (issues requests) — call from tick, not on_callback
A node cannot make a blocking service/action client call from
on_callback: that runs while the executor is mid-dispatch, so a blocking call
would deadlock the spin. Client calls go on the separate per-spin hook
ExecutableNode::tick(&mut TickCtx), which the executor runs once per
spin_once between callback dispatch:
#![allow(unused)]
fn main() {
fn on_callback(state: &mut State, cb: Callback<'_>, ctx: &mut CallbackCtx<'_>) {
// ...decide a request is needed; just arm a flag / stash args:
state.want_call = true;
}
fn tick(state: &mut State, ctx: &mut TickCtx<'_>) {
if state.want_call {
state.want_call = false;
let reply = ctx.call_for_name("add_two_ints", &request); // blocking, safe here
// ...use reply
}
}
}
The two-surface pattern (arm in on_callback, call in tick) is the supported
way to drive a client from a declarative node. See
examples/workspaces/rust/.../service_client_pkg for the reference.
Building
From the workspace root, sync generated interfaces first, then let Cargo compile the Node pkgs and Entry pkg:
# From examples/workspaces/rust/ (or your workspace root):
nros sync
nros codegen-system --bringup demo_bringup
cargo build -p native_entry
No per-Node-pkg invocation is needed — the workspace resolver handles dependency ordering.
To cross-compile for an embedded target, pass --target and ensure
.cargo/config.toml in the workspace root sets the right linker and target:
cargo build --target thumbv7em-none-eabihf
Next steps
- Bringup packages — wire the Node pkgs together into a topology.
- Entry packages — build the binary that boots the topology on real hardware or a host process.
- Role reference —
the full reference for all three roles, metadata fields, and the
nros::main!()four forms.