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.