Creating C++ Nodes (for ament_cmake)

The C++ binary block for ament_cmake

TL;DR

When adding a new Node in an existing CMakeLists.txt, you might benefit from using the following template.

Remember to:

  1. Add ALL dependencies (including ROS2 ones) with find_package, if applicable.

    # find dependencies
    find_package(ament_cmake REQUIRED)
    find_package(rclcpp REQUIRED)
    
  2. Change print_forever_node to the name of your Node.

  3. Add all source files to add_executable.

  4. Add all ROS2 dependencies of this binary to ament_target_dependencies.

  5. Add any other (NOT ROS2) libraries to target_link_libraries.

############################
# CPP Binary Block [BEGIN] #
# vvvvvvvvvvvvvvvvvvvvvvvv #
# https://ros2-tutorial.readthedocs.io/en/latest/
# While we cant use blocks https://cmake.org/cmake/help/latest/command/block.html#command:block
# we use set--unset
set(RCLCPP_LOCAL_BINARY_NAME print_forever_node)

add_executable(${RCLCPP_LOCAL_BINARY_NAME}
    src/print_forever_node_main.cpp
    src/print_forever_node.cpp

    )

ament_target_dependencies(${RCLCPP_LOCAL_BINARY_NAME}
    rclcpp

    )

target_link_libraries(${RCLCPP_LOCAL_BINARY_NAME}

    )

target_include_directories(${RCLCPP_LOCAL_BINARY_NAME} PUBLIC
    $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
    $<INSTALL_INTERFACE:include>)

target_compile_features(${RCLCPP_LOCAL_BINARY_NAME} PUBLIC c_std_99 cxx_std_17)

install(TARGETS ${RCLCPP_LOCAL_BINARY_NAME}
    DESTINATION lib/${PROJECT_NAME})

unset(RCLCPP_LOCAL_BINARY_NAME)
# ^^^^^^^^^^^^^^^^^^^^^^ #
# CPP Binary Block [END] #
##########################

Create the package

Warning

We’ll skip using the --node-name option to create the Node template, because, currently, it generates a Node and a CMakeLists.txt different from my advice.

cd ~/ros2_tutorial_workspace/src
ros2 pkg create cpp_package_with_a_node \
--build-type ament_cmake \
--dependencies rclcpp

which outputs

ros2 pkg create output
going to create a new package
package name: cpp_package_with_a_node
destination directory: /home/murilo/ROS2_Tutorial/ros2_tutorial_workspace/src
package format: 3
version: 0.0.0
description: TODO: Package description
maintainer: ['murilo <murilomarinho@ieee.org>']
licenses: ['TODO: License declaration']
build type: ament_cmake
dependencies: ['rclcpp']
creating folder ./cpp_package_with_a_node
creating ./cpp_package_with_a_node/package.xml
creating source and include folder
creating folder ./cpp_package_with_a_node/src
creating folder ./cpp_package_with_a_node/include/cpp_package_with_a_node
creating ./cpp_package_with_a_node/CMakeLists.txt

[WARNING]: Unknown license 'TODO: License declaration'.  This has been set in the package.xml, but no LICENSE file has been created.
It is recommended to use one of the ament license identitifers:
Apache-2.0
BSL-1.0
BSD-2.0
BSD-2-Clause
BSD-3-Clause
GPL-3.0-only
LGPL-3.0-only
MIT
MIT-0

Making C++ ROS2 Nodes

(Murilo’s) rclcpp best practices

For each new C++ Node, we make three files following the style below.

For a Node called print_forever_node we have

  1. src/print_forever_node.hpp with the Node’s class definition. In general, this is not exported to other packages, so it should not be in the package’s include folder.

  2. src/print_forever_node.cpp with the Node’s class implementation.

  3. src/print_forever_node_main.cpp with the Node’s main function implementation.

In this step, we’ll work on these.

cpp_package_with_a_node
├── CMakeLists.txt
├── include
│   └── cpp_package_with_a_node
│       └── .placeholder
├── package.xml
└── src
    ├── print_forever_node.cpp
    ├── print_forever_node.hpp
    └── print_forever_node_main.cpp

These files do not exists, so we’ll create them.

cd ~/ros2_tutorial_workspace/src/cpp_package_with_a_node
mkdir src

Similar to what we did in Python, we inherit from rclcpp::Node. Whatever is different is owing to differences in languages.

print_forever_node.hpp

 1#pragma once
 2
 3#include <memory>
 4#include <rclcpp/rclcpp.hpp>
 5
 6/**
 7 * @brief A ROS2 Node that prints to the console periodically, but in C++.
 8 */
 9class PrintForeverNode: public rclcpp::Node
10{
11private:
12    double timer_period_;
13    int print_count_;
14    //also equivalent to rclcpp::TimerBase::SharedPtr
15    std::shared_ptr<rclcpp::TimerBase> timer_;
16
17    void _timer_callback();
18public:
19    PrintForeverNode();
20
21};

The implementation has nothing special, just don’t forget to initialize the parent class, rclcpp::Node, with the name of the node.

print_forever_node.cpp

 1#include "print_forever_node.hpp"
 2
 3/**
 4 * @brief PrintForeverNode::PrintForeverNode Default constructor.
 5 */
 6PrintForeverNode::PrintForeverNode():
 7    rclcpp::Node("print_forever_cpp"),
 8    timer_period_(0.5),
 9    print_count_(0)
10{
11    //(Smart) pointers at the one thing that it doesn't matter much if they are not initialized in the member initializer list
12    //and this is a bit more readable.
13    timer_ = create_wall_timer(
14                std::chrono::milliseconds(long(timer_period_*1e3)),
15                std::bind(&PrintForeverNode::_timer_callback, this) //Note here the use of std::bind to build a single argument
16                );
17}
18
19/**
20 * @brief PrintForeverNode::_timer_callback periodically prints class info using RCLCPP_INFO.
21 */
22void PrintForeverNode::_timer_callback()
23{
24    RCLCPP_INFO_STREAM(get_logger(),
25                       std::string("Printed ") +
26                       std::to_string(print_count_) +
27                       std::string(" times.")
28                       );
29    print_count_++;
30}

Given that we are using rclcpp::spin(), there is nothing special here either. Just remember to not mess up the std::make_shared and always use perfect forwarding. See Perfect forwarding. The rclcpp::spin() handles the SIGINT when we, for example, press CTRL+C on the terminal. It is not perfect, but it does the trick for simple nodes like this one.

print_forever_node_main.cpp

 1#include <rclcpp/rclcpp.hpp>
 2
 3#include "print_forever_node.hpp"
 4
 5int main(int argc, char** argv)
 6{
 7    rclcpp::init(argc,argv);
 8
 9    try
10    {
11        auto node = std::make_shared<PrintForeverNode>();
12
13        rclcpp::spin(node);
14    }
15    catch (const std::exception& e)
16    {
17        std::cerr << std::string("::Exception::") << e.what();
18    }
19
20    return 0;
21}

Add a .placeholder if your include/<PACKAGE_NAME> is empty

Warning

If you don’t do this and add this package as a git repository without any files on the include/, CMake might return with an error when trying to compile your package.

cpp_package_with_a_node
├── CMakeLists.txt
├── include
│   └── cpp_package_with_a_node
│       └── .placeholder
├── package.xml
└── src
    ├── print_forever_node.cpp
    ├── print_forever_node.hpp
    └── print_forever_node_main.cpp

Empty directories will not be tracked by git. A file has to be added to the index. We can create an empty file in the include folder as follows

cd ~/ros2_tutorial_workspace/src/cpp_package_with_a_node/src
touch include/cpp_package_with_a_node/.placeholder

Running a C++ Node

As simple as it has always been, see Running a node (ros2 run).

ros2 run cpp_package_with_a_node print_forever_node

which returns

[INFO] [1688620414.406930812] [print_forever_node]: Printed 0 times.
[INFO] [1688620414.906890884] [print_forever_node]: Printed 1 times.
[INFO] [1688620415.406907619] [print_forever_node]: Printed 2 times.
[INFO] [1688620415.906881003] [print_forever_node]: Printed 3 times.
[INFO] [1688620416.406900108] [print_forever_node]: Printed 4 times.
[INFO] [1688620416.906886691] [print_forever_node]: Printed 5 times.
[INFO] [1688620417.406881803] [print_forever_node]: Printed 6 times.
[INFO] [1688620417.906858551] [print_forever_node]: Printed 7 times.
[INFO] [1688620418.406894922] [print_forever_node]: Printed 8 times.

and we’ll use CTRL+C to stop the node, resulting in

[INFO] [1688620418.725674401] [rclcpp]: signal_handler(signum=2)