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

Mixed-language workspace

nano-ros Node pkgs are linked through a C ABI register trampoline, so one image can compose Node pkgs written in different languages. The native reference shape is:

  • C Node pkgs for C code you want to keep in C.
  • C++ and Rust Node pkgs beside them, in the same workspace.
  • One Bringup pkg with the normal ROS 2 launch XML and the [image.*] rows.

The mixed workspace is in examples/workspaces/mixed/. For a pure-C workspace, use examples/workspaces/c/.

Layout

my_ws/
├── .colcon_workspace             # tracked marker; there is no root build file
└── src/
    ├── c_talker_pkg/             # C Node pkg
    ├── cpp_listener_pkg/         # C++ Node pkg
    ├── rust_heartbeat_pkg/       # Rust Node pkg
    └── demo_bringup/             # launch XML + system.toml (+ [image.*])

Mixed is not a special case. The driver is chosen by the board, and cmake wins whenever the package graph crosses languages — corrosion makes cargo consumable from cmake, and nothing makes cmake consumable from cargo (RFC-0024 §6.3). So a mixed workspace builds exactly like the C++ one:

$ cd examples/workspaces/mixed
$ nros sync
$ nros build linux

nros build writes a CMakeLists.txt — a nano_ros_workspace(…) call listing the discovered packages, plus one nano_ros_add_executable(…) per image on that coordinate — into build/<coordinate>/, configures it into build/<coordinate>/cmake/, and builds. That generated file is the cmake root; the workspace itself has no build file at all (RFC-0098 D9), and neither the root nor the entry is yours to write.

C Node pkg

A C Node pkg is a typed component (RFC-0043): no main(). It defines a state struct + a configure(node, executor, self) function and exports the C-ABI factory/configure seam via RCLC_C_COMPONENT(StateT, configure_fn) — the typed Entry creates the node and runs configure on the real executor. The component is written against the C API: configure receives an rcl_node_t * and an rclc_executor_t * over the entry’s own node and executor, so a C and a C++ component in one image share both.

# std_msgs is declared in package.xml (`<depend>std_msgs</depend>`), which
# generates its C bindings for this package.
find_package(std_msgs REQUIRED)
nano_ros_auto_add_library(talker_lib STATIC src/Talker.c)

nros_components_register_node(talker_lib
    PLUGIN c_talker_pkg::Talker
    EXECUTABLE talker
    SHAPE configure
    TYPED
    DEPLOY linux)
#include <stdint.h>
#include <rclc/rclc.h>
#include <std_msgs/msg/int32.h>

typedef struct {
    rcl_publisher_t pub;
    rcl_timer_t timer;
    int32_t count;
} c_talker_pkg_t;

static void on_tick(rcl_timer_t* timer, void* ctx) {
    (void)timer;
    c_talker_pkg_t* self = (c_talker_pkg_t*)ctx;
    std_msgs__msg__Int32 msg = {.data = self->count};
    (void)rcl_publish(&self->pub, &msg, NULL);
    self->count++;
}

static rcl_ret_t talker_configure(rcl_node_t* node, rclc_executor_t* executor,
                                  c_talker_pkg_t* self) {
    self->count = 0;
    self->pub = rcl_get_zero_initialized_publisher();
    rcl_ret_t rc = rclc_publisher_init_default(
        &self->pub, node, ROSIDL_GET_MSG_TYPE_SUPPORT(std_msgs, msg, Int32), "/chatter");
    if (rc != RCL_RET_OK) return rc;
    self->timer = rcl_get_zero_initialized_timer();
    rc = rclc_timer_init_with_context(&self->timer, 1000000000ULL, on_tick, self);
    if (rc != RCL_RET_OK) return rc;
    return rclc_executor_add_timer(executor, &self->timer);
}

RCLC_C_COMPONENT(c_talker_pkg_t, talker_configure)

nano_ros_auto_add_library injects NROS_PKG_NAME (language inferred from the sources), so RCLC_C_COMPONENT exports the __nros_c_component_<pkg>_{create,configure} seam the typed Entry calls — interoperable with C++ configure(Node&) and Rust nros::node! components in one launch graph.

The image

Nothing about the language mix reaches the image declaration. One row per program, in src/demo_bringup/system.toml:

[image_defaults]
rmw = "zenoh"

[image.linux]
board = "linux"

The launch file names the Node pkgs, whatever they are written in:

<launch>
  <node pkg="c_talker_pkg" exec="talker" name="talker"/>
  <node pkg="cpp_listener_pkg" exec="listener" name="listener"/>
</launch>

From that pair, nros build emits this into the generated root. LANG follows the workspace’s own sources; BOARD and DEPLOY come from the resolved image, so a generated entry cannot disagree with the board it was generated for. There is no SOURCES, because the verb generates the translation unit carrying main:

nano_ros_add_executable(linux_entry
    BOARD   native
    BRINGUP "${CMAKE_CURRENT_SOURCE_DIR}/../../src/demo_bringup"
    LAUNCH  default
    LANG    cpp
    TYPED
    DEPLOY  native)

The generated entry translation unit calls each package’s __nros_component_<pkg>_register symbol and the CMake sidecar links the matching static libraries. The symbol keeps the legacy component spelling for ABI compatibility; the user-facing package role is still Node pkg.

Note there is no MODEL here. The SystemModel is a build artifact resolved from the launch file by nros sync; naming it in a build file is the deprecated expert override, and a committed one is banned outright (gate check-no-tracked-models).

Scaffolding

# Current compatibility scaffold for Node pkgs:
nros new --component c_talker_pkg --lang c --use-case talker
nros new --component cpp_listener_pkg --lang cpp --use-case listener
nros new system demo_bringup --components c_talker_pkg,cpp_listener_pkg

For a complete working tree, copy the template instead of creating each package separately.