Writing a Composable Node (C++)

Starting Place

Let’s assume that you have a regular rclcpp::Node executable that you want to run in the same process as other nodes to enable more efficient communication.

We’ll start from having a class that directly inherits from Node, and that also has a main method defined.

namespace palomino
{
    class VincentDriver : public rclcpp::Node
    {
        // ...
    };
}

int main(int argc, char * argv[])
{
    rclcpp::init(argc, argv);
    rclcpp::spin(std::make_shared<palomino::VincentDriver>());
    rclcpp::shutdown();
    return 0;
}

This will typically be compiled as an executable in your Cmake.

# ...
add_executable(vincent_driver src/vincent_driver.cpp)
# ...
install(TARGETS vincent_driver
    DESTINATION lib/${PROJECT_NAME}
)

Code Updates

Add the Package Dependency

Your package.xml should have a dependency on rclcpp_components, a la

<depend>rclcpp_components</depend>

Alternatively, you can independently add a build_depend/exec_depend.

Class Definition

The only change to your class definition that you may have to do is ensure that the constructor for the class takes a NodeOptions argument.

VincentDriver(const rclcpp::NodeOptions & options) : Node("vincent_driver", options)
{
  // ...
}

No More Main Method

Replace your main method with a pluginlib-style macro invocation.

#include <rclcpp_components/register_node_macro.hpp>
RCLCPP_COMPONENTS_REGISTER_NODE(palomino::VincentDriver)

Caution

If the main method you are replacing contains a MultiThreadedExecutor, be sure to make note and ensure that your container node is multithreaded. See section below.

CMake Changes

First, add rclcpp_components as a dependency in your CMakeLists.txt with:

find_package(rclcpp_components REQUIRED)

Second, we’re going to replace our add_executable with a add_library with a new target name.

add_library(vincent_driver_component src/vincent_driver.cpp)

Third, replace other build commands that used the old target to act on the new target. i.e. ament_target_dependencies(vincent_driver ...) becomes ament_target_dependencies(vincent_driver_component ...)

Fourth, add a new command to declare your component.

rclcpp_components_register_node(
    vincent_driver_component
    PLUGIN "palomino::VincentDriver"
    EXECUTABLE vincent_driver
)

Fifth and finally, change any installation commands in the CMake that operated on the old target to install the library version instead. For instance, do not install either target into lib/${PROJECT_NAME}. Replace with the library installation.

ament_export_targets(export_vincent_driver_component)
install(TARGETS vincent_driver_component
        EXPORT export_vincent_driver_component
        ARCHIVE DESTINATION lib
        LIBRARY DESTINATION lib
        RUNTIME DESTINATION bin
)

Running Your Node

See the Composition tutorial for an in-depth look at composing nodes. The quick and dirty version is that if you had the following in your Python launch file,

from launch_ros.actions import Node

# ..

ld.add_action(Node(
    package='palomino',
    executable='vincent_driver',
    # ..
))

you can replace it with

from launch_ros.actions import ComposableNodeContainer
from launch_ros.descriptions import ComposableNode

# ..
ld.add_action(ComposableNodeContainer(
    name='a_buncha_nodes',
    namespace='',
    package='rclcpp_components',
    executable='component_container',
    composable_node_descriptions=[
        ComposableNode(
            package='palomino',
            plugin='palomino::VincentDriver',
            name='vincent_driver',
            # ..
            extra_arguments=[{'use_intra_process_comms': True}],
        ),
    ]
))

Caution

If you need multi-threading, instead of setting your executable to component_container, set it to component_container_mt