Migrating a C++ Package Example

This example shows how to migrate an example C++ package from ROS 1 to ROS 2.

Prerequisites

You need a working ROS 2 installation, such as ROS jazzy.

The ROS 1 code

Say you have a ROS 1 package called talker that uses roscpp in one node, called talker. This package is in a catkin workspace, located at ~/ros1_talker.

Your ROS 1 workspace has the following directory layout:

$ cd ~/ros1_talker
$ find .
.
./src
./src/talker
./src/talker/package.xml
./src/talker/CMakeLists.txt
./src/talker/talker.cpp

The files have the following content:

src/talker/package.xml:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
  <name>talker</name>
  <version>0.0.0</version>
  <description>talker</description>
  <maintainer email="gerkey@example.com">Brian Gerkey</maintainer>
  <license>Apache-2.0</license>
  <buildtool_depend>catkin</buildtool_depend>
  <depend>roscpp</depend>
  <depend>std_msgs</depend>
</package>

src/talker/CMakeLists.txt:

cmake_minimum_required(VERSION 2.8.3)
project(talker)
find_package(catkin REQUIRED COMPONENTS roscpp std_msgs)
catkin_package()
include_directories(${catkin_INCLUDE_DIRS})
add_executable(talker talker.cpp)
target_link_libraries(talker ${catkin_LIBRARIES})
install(TARGETS talker
  RUNTIME DESTINATION ${CATKIN_PACKAGE_BIN_DESTINATION})

src/talker/talker.cpp:

#include <sstream>
#include "ros/ros.h"
#include "std_msgs/String.h"
int main(int argc, char **argv)
{
  ros::init(argc, argv, "talker");
  ros::NodeHandle n;
  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
  ros::Rate loop_rate(10);
  int count = 0;
  std_msgs::String msg;
  while (ros::ok())
  {
    std::stringstream ss;
    ss << "hello world " << count++;
    msg.data = ss.str();
    ROS_INFO("%s", msg.data.c_str());
    chatter_pub.publish(msg);
    ros::spinOnce();
    loop_rate.sleep();
  }
  return 0;
}

Migrating to ROS 2

Let’s start by creating a new workspace in which to work:

mkdir ~/ros2_talker
cd ~/ros2_talker

We’ll copy the source tree from our ROS 1 package into that workspace, where we can modify it:

mkdir src
cp -a ~/ros1_talker/src/talker src

Now we’ll modify the C++ code in the node. The ROS 2 C++ library, called rclcpp, provides a different API from that provided by roscpp. The concepts are very similar between the two libraries, which makes the changes reasonably straightforward to make.

Included headers

In place of ros/ros.h, which gave us access to the roscpp library API, we need to include rclcpp/rclcpp.hpp, which gives us access to the rclcpp library API:

//#include "ros/ros.h"
#include "rclcpp/rclcpp.hpp"

To get the std_msgs/String message definition, in place of std_msgs/String.h, we need to include std_msgs/msg/string.hpp:

//#include "std_msgs/String.h"
#include "std_msgs/msg/string.hpp"

Changing C++ library calls

Instead of passing the node’s name to the library initialization call, we do the initialization, then pass the node name to the creation of the node object:

//  ros::init(argc, argv, "talker");
//  ros::NodeHandle n;
    rclcpp::init(argc, argv);
    auto node = rclcpp::Node::make_shared("talker");

The creation of the publisher and rate objects looks pretty similar, with some changes to the names of namespace and methods.

//  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
//  ros::Rate loop_rate(10);
  auto chatter_pub = node->create_publisher<std_msgs::msg::String>("chatter",
    1000);
  rclcpp::Rate loop_rate(10);

To further control how message delivery is handled, a quality of service (QoS) profile could be passed in. The default profile is rmw_qos_profile_default. For more details, see the design document and concept overview.

The creation of the outgoing message is different in the namespace:

//  std_msgs::String msg;
  std_msgs::msg::String msg;

In place of ros::ok(), we call rclcpp::ok():

//  while (ros::ok())
  while (rclcpp::ok())

Inside the publishing loop, we access the data field as before:

msg.data = ss.str();

To print a console message, instead of using ROS_INFO(), we use RCLCPP_INFO() and its various cousins. The key difference is that RCLCPP_INFO() takes a Logger object as the first argument.

//    ROS_INFO("%s", msg.data.c_str());
    RCLCPP_INFO(node->get_logger(), "%s\n", msg.data.c_str());

Change the publish call to use the -> operator instead of ..

//    chatter_pub.publish(msg);
    chatter_pub->publish(msg);

Spinning (i.e., letting the communications system process any pending incoming/outgoing messages) is different in that the call now takes the node as an argument:

//    ros::spinOnce();
    rclcpp::spin_some(node);

Sleeping using the rate object is unchanged.

Putting it all together, the new talker.cpp looks like this:

#include <sstream>
// #include "ros/ros.h"
#include "rclcpp/rclcpp.hpp"
// #include "std_msgs/String.h"
#include "std_msgs/msg/string.hpp"
int main(int argc, char **argv)
{
//  ros::init(argc, argv, "talker");
//  ros::NodeHandle n;
  rclcpp::init(argc, argv);
  auto node = rclcpp::Node::make_shared("talker");
//  ros::Publisher chatter_pub = n.advertise<std_msgs::String>("chatter", 1000);
//  ros::Rate loop_rate(10);
  auto chatter_pub = node->create_publisher<std_msgs::msg::String>("chatter", 1000);
  rclcpp::Rate loop_rate(10);
  int count = 0;
//  std_msgs::String msg;
  std_msgs::msg::String msg;
//  while (ros::ok())
  while (rclcpp::ok())
  {
    std::stringstream ss;
    ss << "hello world " << count++;
    msg.data = ss.str();
//    ROS_INFO("%s", msg.data.c_str());
    RCLCPP_INFO(node->get_logger(), "%s\n", msg.data.c_str());
//    chatter_pub.publish(msg);
    chatter_pub->publish(msg);
//    ros::spinOnce();
    rclcpp::spin_some(node);
    loop_rate.sleep();
  }
  return 0;
}

Change the package.xml

ROS 2 packages use CMake functions and macros from ament_cmake_ros instead of catkin. Delete the dependency on catkin:

<!-- delete this -->
<buildtool_depend>catkin</buildtool_depend>`

Add a new dependency on ament_cmake_ros:

<buildtool_depend>ament_cmake_ros</buildtool_depend>

ROS 2 C++ libraries use rclcpp instead of roscpp.

Delete the dependency on roscpp:

<!-- delete this -->
<depend>roscpp</depend>

Add a dependency on rclcpp:

<depend>rclcpp</depend>

Add an <export> section to tell colcon the package is an ament_cmake package instead of a catkin package.

<export>
  <build_type>ament_cmake</build_type>
</export>

Your package.xml now looks like this:

<?xml version="1.0"?>
<?xml-model href="http://download.ros.org/schema/package_format2.xsd" schematypens="http://www.w3.org/2001/XMLSchema"?>
<package format="2">
  <name>talker</name>
  <version>0.0.0</version>
  <description>talker</description>
  <maintainer email="gerkey@example.com">Brian Gerkey</maintainer>
  <license>Apache-2.0</license>
  <buildtool_depend>ament_cmake</buildtool_depend>
  <depend>rclcpp</depend>
  <depend>std_msgs</depend>
  <export>
    <build_type>ament_cmake</build_type>
  </export>
</package>

Changing the CMake code

Require a newer version of CMake so that ament_cmake functions work correctly.

cmake_minimum_required(VERSION 3.14.4)

Use a newer C++ standard matching the version used by your target ROS distro in REP 2000. If you are using C++17, then set that version with the following snippet after the project(talker) call. Add extra compiler checks too because it is a good practice.

if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()

Replace the find_package(catkin ...) call with individual calls for each dependency.

find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)

Delete the call to catkin_package(). Add a call to ament_package() at the bottom of the CMakeLists.txt.

ament_package()

Make the target_link_libraries call modern CMake targets provided by rclcpp and std_msgs.

target_link_libraries(talker PUBLIC
  rclcpp::rclcpp
  ${std_msgs_TARGETS})

Delete the call to include_directories(). Add a call to target_include_directories() below add_executable(talker talker.cpp). Don’t pass variables like rclcpp_INCLUDE_DIRS into target_include_directories(). The include directories are already handled by calling target_link_libraries() with modern CMake targets.

target_include_directories(talker PUBLIC
   "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
   "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")

Change the call to install() so that the talker executable is installed into a project specific directory.

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

The new CMakeLists.txt looks like this:

cmake_minimum_required(VERSION 3.14.4)
project(talker)
if(NOT CMAKE_CXX_STANDARD)
  set(CMAKE_CXX_STANDARD 17)
endif()
if(CMAKE_COMPILER_IS_GNUCXX OR CMAKE_CXX_COMPILER_ID MATCHES "Clang")
  add_compile_options(-Wall -Wextra -Wpedantic)
endif()
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(std_msgs REQUIRED)
add_executable(talker talker.cpp)
target_include_directories(talker PUBLIC
   "$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>"
   "$<INSTALL_INTERFACE:include/${PROJECT_NAME}>")
target_link_libraries(talker PUBLIC
  rclcpp::rclcpp
  ${std_msgs_TARGETS})
install(TARGETS talker
  DESTINATION lib/${PROJECT_NAME})
ament_package()

Building the ROS 2 code

We source an environment setup file (in this case the one generated by following the ROS 2 installation tutorial, which builds in ~/ros2_ws, then we build our package using colcon build:

. ~/ros2_ws/install/setup.bash
cd ~/ros2_talker
colcon build

Running the ROS 2 node

Because we installed the talker executable into the correct directory, after sourcing the setup file, from our install tree, we can invoke it by running:

. ~/ros2_ws/install/setup.bash
ros2 run talker talker

Conclusion

You have learned how to migrate an example C++ ROS 1 package to ROS 2. Use the Migrating C++ Packages reference page to help you migrate your own C++ packages from ROS 1 to ROS 2.