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

Message Binding Generation

nros uses generated Rust bindings for ROS 2 message types. nros build generates no_std compatible bindings from each package’s package.xml dependencies before it builds (nros configure does that step alone, and nros interface generate rust the codegen alone).

Overview

The binding generator is part of the nros CLI and provides:

  • Pure Rust, no_std compatible output using heapless types
  • Automatic dependency resolution through your ROS 2 install’s ament index
  • the [patch.crates-io] rows that point the nano-ros runtime crates at the SDK root, written into the image’s generated build settings

Prerequisites

  1. package.xml in each node package - Declares ROS interface dependencies

    <?xml version="1.0"?>
    <package format="3">
      <name>my_package</name>
      <version>0.1.0</version>
      <description>My nros package</description>
      <maintainer email="dev@example.com">Developer</maintainer>
      <license>Apache-2.0</license>
      <depend>std_msgs</depend>
      <depend>geometry_msgs</depend>
      <export>
        <build_type>ament_cargo</build_type>
      </export>
    </package>
    
  2. nros tool built + on PATH

    Install a release; install.sh puts nros in ~/.nros/bin:

    curl -fsSL https://raw.githubusercontent.com/NEWSLabNTU/nano-ros/main/scripts/install.sh | sh
    export PATH="$HOME/.nros/bin:$PATH"
    

    See Installation for the full walkthrough.

  3. A ROS 2 install, sourced for the first build

    The bindings are generated from the message definitions of your ROS 2 install, std_msgs included, so the generated layouts and type hashes match the ROS 2 nodes you talk to. Source it before the first nros build, nros run or nros configure of a project:

    source /opt/ros/humble/setup.bash
    

    That configure records the prefix (AMENT_PREFIX_PATH) in the project, and later configures reuse it, so later shells need not source ROS. With neither, configure refuses and names this step. A project whose packages depend on no message package at all needs no ROS 2.

Workflow

Step 1: Create package.xml

Declare your ROS interface dependencies in <depend> tags:

<depend>std_msgs</depend>      <!-- For std_msgs::msg::Int32, String, etc. -->
<depend>example_interfaces</depend>  <!-- For service types -->

Step 2: Build

cd my_ws
source /opt/ros/humble/setup.bash   # the first build of the workspace only
nros build

Before it builds, this will:

  1. Parse package.xml to find dependencies
  2. Resolve transitive dependencies through the ament index
  3. Filter to interface packages (those with msg/srv/action)
  4. Generate bindings into the workspace’s generated/ directory
  5. Resolve the bringup’s launch file into the system model
  6. Write each image’s build settings under build/, carrying the board’s triple and flags, the resolved pool budgets, and the [patch.crates-io] rows for the nano-ros runtime crates

Each step is redone only when its inputs changed. To run them without building (before opening the workspace in an IDE, for instance), use nros configure. nros interface generate rust is the codegen-only step underneath — steps 1–4 and nothing else — and is useful when you want bindings without a workspace.

Step 3: there is no step 3 — package.xml is the dependency list

A generated message crate is a path dependency on its own directory, and nros build writes that row for you, before it plans: one per <depend> / <build_depend> / <exec_depend> that names a message package, tagged # nros-managed:

[dependencies]
std_msgs = { path = "../../generated/std_msgs", default-features = false }  # nros-managed
example_interfaces = { path = "../../generated/example_interfaces", default-features = false }  # nros-managed

So adding a message package is one edit — the <depend> — exactly as on the C/C++ road, and deleting the <depend> deletes the row on the next build. A row without the tag is yours: the build never edits or removes it, so deleting the tag is how you take a row over. The nros row is not managed — it carries a feature choice that is yours to make.

Never name a message crate by registry version (std_msgs = "*"): those crates are generated per host from your ament install, they are not published, and a bare name resolves against the public crates.io instead.

The nano-ros runtime crates are the other case — nros, nros-board-<board>, the backends — and they are registry-named with version = "*", because the build writes the [patch.crates-io] rows that redirect them into the nano-ros SDK root.

Why msg crates are RMW-agnostic

Generated msg crates carry only the wire-format data type — no backend-specific code, no per-RMW Cargo feature. A user manifest is the plain pair:

[dependencies]
std_msgs = { path = "../../generated/std_msgs", default-features = false }  # nros-managed
nros     = { version = "*" }

Transport choice and message schema are orthogonal concerns and the manifest reflects that. (Which backend you actually get is [system] rmw in the bringup’s system.toml, not a feature you pick here — see Switching RMW in Config.) This matches upstream rclcpp + rclrs, which both ship msg packages RMW-agnostic and let the RMW pick which descriptor representation it wants at runtime.

For DDS-based backends that need a per-type descriptor on the wire (Cyclone DDS today), the nros-rmw-cyclonedds shim builds those descriptors lazily on first pub/sub for a given message type, walks the static field schema exposed by nros-serdes (the Message trait with const TYPE_NAME + const FIELDS), and caches the result in a bounded no_std registry. No per-msg-pkg backend code is required.

The registry is bounded by the NROS_CYCLONEDDS_MAX_TYPES sizing knob.

Where the nano-ros sources come from

A project uses the SDK root the nros binary belongs to — a release’s own share/nano-ros — so a plain nros build works anywhere. To name a different SDK root, pass it:

nros build --nano-ros-path <sdk-root>

The build then writes the [patch.crates-io] rows into each image’s generated settings, pointing each nros* requirement at a directory in the SDK root. Nothing about the location is baked into a tracked file, so moving the workspace, or installing another release, needs no edit: the next build re-points it.

Use in code

#![allow(unused)]
fn main() {
use std_msgs::msg::Int32;
use example_interfaces::srv::{AddTwoInts, AddTwoIntsRequest, AddTwoIntsResponse};

let msg = Int32 { data: 42 };
}

Command Options

nros interface generate rust is the codegen step alone — it writes bindings and nothing else:

nros interface generate rust [OPTIONS]

Options:
      --manifest <PATH>         Path to package.xml [default: package.xml]
  -o, --output <DIR>            Output directory [default: generated]
      --ros-edition <EDITION>   humble | iron | jazzy [default: humble]
      --codegen-config <PATH>   Explicit per-field capacity config (nros-codegen.toml)
      --rename <OLD=NEW>        Rename a generated package
      --force                   Overwrite existing bindings
  -v, --verbose                 Enable verbose output

It also still accepts --generate-config / --nano-ros-path / --nano-ros-git, which write a .cargo/config.toml beside a package by hand. Those are the old shape and are not the path to use: nros build writes the patches into the generated settings instead, where they are regenerated rather than committed.

Generated Output Structure

my_ws/
├── src/
│   ├── my_node/
│   │   ├── package.xml      # Your dependency declarations
│   │   ├── Cargo.toml       # Your package manifest (a library)
│   │   └── src/lib.rs       # Your code using generated types
│   └── my_node_bringup/
│       ├── package.xml
│       └── system.toml      # Board, RMW, components
├── generated/               # Generated bindings (do not edit)
│   ├── std_msgs/
│   │   ├── Cargo.toml
│   │   └── src/
│   │       ├── lib.rs       # #![no_std]
│   │       └── msg/
│   │           ├── mod.rs
│   │           └── int32.rs
│   └── builtin_interfaces/  # Transitive dependency
│       └── ...
└── build/                   # Build output (do not commit)
    └── …/<image>_entry/     # each image's generated entry and its cargo settings, incl. the patches

Generated Code Features

no_std by default:

#![allow(unused)]
#![no_std]

fn main() {
pub mod msg;
}

std feature for optional std support:

[features]
default = []
std = ["nros-core/std", "nros-serdes/std"]

heapless types for embedded:

#![allow(unused)]
fn main() {
pub struct String {
    pub data: heapless::String<256>,
}

pub struct Arrays {
    pub data: heapless::Vec<i32, 64>,
}
}

Service types with Request/Response:

#![allow(unused)]
fn main() {
pub struct AddTwoInts;
pub struct AddTwoIntsRequest { pub a: i64, pub b: i64 }
pub struct AddTwoIntsResponse { pub sum: i64 }

impl RosService for AddTwoInts {
    type Request = AddTwoIntsRequest;
    type Reply = AddTwoIntsResponse;
}
}

A timestamp message gets SecNanosecMsg, so Time::to_ros_msg() can return it:

#![allow(unused)]
fn main() {
impl nros_core::SecNanosecMsg for Time {
    fn from_sec_nanosec(sec: i32, nanosec: u32) -> Self { Self { sec, nanosec } }
}
}

Emitted for every message whose field set is exactly { int32 sec, uint32 nanosec } — builtin_interfaces/msg/Time, its Duration twin, and any message of yours with that shape. It is what makes the call a publisher writes work:

#![allow(unused)]
fn main() {
msg.header.stamp = node.now().to_ros_msg();
}

nros-core cannot name a message type — every generated crate depends on it, and your message crates are generated from your packages — so the conversion is implemented on the generated side and the trait is the seam. The C++ side spells the same thing as a template (node.now().to_msg(msg.header.stamp)); Rust has no structural bound, so it needs the named trait. There is no Result: nros’s Time already is { sec, nanosec }, so nothing can fail. (rclrs returns Result<_, TryFromIntError> because its Time is i64 nanoseconds; a ported t.to_ros_msg()? will not compile here, and deleting the ? is the fix.)

Examples

Each example on GitHub is a workspace that carries its own bringup and resolves on its own, so it builds wherever it lives. Build each from its own root:

cd my_talker_ws && nros build
cd my_service_client_ws && nros build

Regenerating Bindings

To regenerate after ROS package updates or dependency changes:

nros interface generate rust --force

Where the definitions come from

nros build reads message definitions from the ROS 2 install you sourced (or the one the project’s first configure recorded), through AMENT_PREFIX_PATH. The release carries copies of a few standard packages, but they only fill gaps under a ROS 2 install, never stand in for one: with no prefix set and none recorded, configure refuses rather than build from them, because they would silently fix a ROS 2 distro and the types would stop matching a peer on another one. A message package in your own workspace shadows the installed one of the same name.

Troubleshooting

“AMENT_PREFIX_PATH is unset and no configure of … has recorded one to reuse”

  • This shell has not sourced ROS 2, and this project has never been configured from one that had. Run source /opt/ros/humble/setup.bash (or your overlay’s install/setup.bash), then build again.

“Package ‘X’ not found in ament index or bundled interfaces”

  • Source the ROS 2 environment: source /opt/ros/humble/setup.bash
  • Check package is installed: ros2 pkg list | grep X
  • Install if missing: sudo apt install ros-humble-X

Build errors with generated code

  • Regenerate with --force flag
  • Check nros crate compatibility

Which CMake spelling (one table)

Five codegen entry points exist in the CMake surface. They are NOT interchangeable spellings of one thing — each serves one audience, and two are back-compat only. Pick by what you are doing:

You are…UseNotes
Consuming an existing msg package from an app/node pkgfind_package(<pkg> REQUIRED)The recommended default. The smart Find-stub resolves the package through the 3-layer search (workspace → ament → bundled) and generates bindings on the fly. Zero codegen calls in your file.
Defining msgs in a message packagerosidl_generate_interfaces(${PROJECT_NAME} msg/… DEPENDENCIES …)Recommended — the stock ROS shape, intercepted by nano-ros. Your msg pkg carries zero nano-ros-specific lines and cross-builds under colcon unchanged.
Defining msgs and you want the nano-ros verb explicitly (or C bindings)nano_ros_generate_interfaces(<name> <files…> [LANGUAGE C|CPP] [DEPENDENCIES …])The ament-shape alias. Defaults to LANGUAGE CPP like rosidl — pass LANGUAGE C for C.
Building a workspace root with several msg pkgsnros_workspace_interfaces() after set(NROS_INTERFACE_SEARCH_PATH …/src)Bulk-builds every workspace msg pkg in topological order; one line, no per-pkg add_subdirectory.
Generating this pkg’s package.xml dependencies (no find_package flow)nros_find_interfaces([LANGUAGE C|CPP] [SKIP_INSTALL])Consume-by-manifest: resolves the transitive interface closure of your own package.xml. Used by the workspace node pkgs. Defaults LANGUAGE CPP.
—nros_generate_interfaces(<target> <files…>)Deprecated for new code. The low-level generator everything above routes through; direct calls are back-compat only.

C Code Generation (CMake)

nano_ros_generate_interfaces(… LANGUAGE C) generates C bindings for .msg, .srv, and .action files (the function defaults to C++ — the LANGUAGE C argument is what makes it the C generator). It uses a bundled codegen library — no external nros binary needed.

Prerequisites

nano_ros_generate_interfaces() becomes available automatically once the consumer’s CMakeLists.txt invokes add_subdirectory(nano-ros). The codegen tool ships inside the in-tree nros CLI binary (packages/cli/target/release/nros; the old nros-codegen submodule is retired); cmake auto-resolves it from PATH / packages/cli/target/release/ / the transitional ${NROS_HOME:-~/.nros}/bin/. No separate build step.

Usage

For a complete example, see the features workspace, examples/workspaces/features: its custom_msgs package defines a message, and c_reading_talker_pkg and c_reading_listener_pkg use the generated C bindings. The custom-message feature demo is a node package there, with its own image row.

Design background