Building Modular ROS Packages

Building Modular ROS Packages

Introduction

 The key power of the Catkin build tool is how it makes it easier to build modular software without having to keep track of the specific build products of each package. Modularity, in this case, comes in the form of building specific functionality into libraries which can be used by other packages. This tutorial is meant for someone with minimal to moderate CMake experience and minimal experience with Catkin.

 This tutorial begins by separating the executable code from the ROS C++ Hello World Tutorial into a library and building it with CMake and Catkin. If you are unfamilar with Catkin or CMake, this tutorial will make more sense after you have worked through the Gentle Introduction to Catkin.

 The next step involves creating a second package which depends on the first package and uses the functionality defined in our library. This inter-dependency then demonstrates how to use the catkin_package() CMake function to declare exported targets for a package.

Pre-Requisites

  • A computer running a recent Ubuntu Linix1 LTS (long-term support) installation
  • Minimal experience with the Linux and the command-line interface
  • Minimal experience with compiling C++ code

Tools Used

  • Ubuntu Linux
  • The bash shell
  • C++
  • CMake
  • Catkin
  • Any plain-text editor (I like vim).

ROS Packages Used

  • roscpp
  • roscpp
  • catkin

Create a Catkin Package

Create a new directory for your package:

1
mkdir src/modular_lib_pkg

Add bare-bones Catkin CMakeLists.txt and package.xml files to make your directory a valid package:
src/modular_lib_pkg/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
# Declare the version of the CMake API for forward-compatibility
cmake_minimum_required(VERSION 2.8)

# Declare the name of the CMake Project
project(modular_lib_pkg)

# Find Catkin
find_package(catkin REQUIRED)
# Declare this project as a catkin package
catkin_package()

src/modular_lib_pkg/package.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
<package>
<!-- Package Metadata -->
<name>modular_lib_pkg</name>
<maintainer email="you@example.com">Your Name</maintainer>
<description>
A ROS tutorial on modularity.
</description>
<version>0.0.0</version>
<license>BSD</license>

<!-- Required by Catkin -->
<buildtool_depend>catkin</buildtool_depend>
</package>

Separating Functionality into a Library

 The first step in making code available for use in other ROS packages is to encapsulate its functionality into a library.

 On most operating systems, including Linux8, there are two types of libraries: static libraries and dynamic libraries. Both of these types of libraries contain compiled binary code which can be executed directly by a computer.

 Static libraries (.a for “archive” on Linux) are linked into an executable when it is built and it becomes part of that executable. When the executable is loaded, the binary code that was copied from the static library is also loaded. Dynamic libraries (.so for “shared object” on Linux), however, are not copied into the executable, and instead are loaded at runtime.

 This means not only are dynamically-linked executables smaller, but also the libraries that they depend on chan change internally without necessitating recompilation of the executable.

 In the ROS community, dynamic libraries are most commonly used, and this is what will be built by default when using Catkin.

Create the Library Code

 The first step is to create the library. Our library will encapsulate the hello-world functionality used in the ROS C++ hello-world tutorial9 so that you can call a single function called say_hello() to broadcast “Hello, world!” over the /rosout topic.

 There’s nothing fundamentally different between putting C++ code in a library as opposed to an executable. What is required, however, is to split the code definition from the declaration. This involves creating two files: a header file and a source file.

 The header file should contain only what is needed by the compiler of anyone who uses the library. As such, it only needs to contain function and class delcarations, and does not need to contain function definitions.

 The header with the declaration of our say_hello() function is as follows:
modular_lib_pkg/include/modular_lib_pkg/hello_world.h

1
2
3
4
5
6
7
8
// Inclusion guard to prevent this header from being included multiple times
#ifndef __MODULAR_LIB_PKG_HELLO_WORLD_H
#define __MODULAR_LIB_PKG_HELLO_WORLD_H

//! Broadcast a hello-world message over ROS_INFO
void say_hello();

#endif

Next is the source or implementation file. This file should contain what is needed by the linker to connect function calls to binary code. As such, it needs to contain all of the definitions of the functions declared in the corresponding header.
 The source file with the definition of say_hello() is as follows:
modular_lib_pkg/src/hello_world.cpp
1
2
3
4
5
6
// Include the ROS C++ APIs
#include <ros/ros.h>

void say_hello() {
ROS_INFO_STREAM("Hello, world!");
}

Now that we’ve written the code for the library, we can add a rule to the CMakeLists.txt file to actually build it. Note that just like in the ROS C++ hello-world tutorial9, we need to add a dependency on roscpp in order to use ROS. This is just like adding an executable with the add_executable() CMake command: instead, we use add_library():
src/modular_lib_pkg/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
# Declare the version of the CMake API for forward-compatibility
cmake_minimum_required(VERSION 2.8)

# Declare the name of the CMake Project
project(modular_lib_pkg)

# Find and get all the information about the roscpp package
find_package(roscpp REQUIRED)

# Find Catkin
find_package(catkin REQUIRED)
# Declare this project as a catkin package
catkin_package()

# Add the headers from roscpp
include_directories(${roscpp_INCLUDE_DIRS})

# Define a library target called hello_world
add_library(hello_world src/hello_world.cpp)
target_link_libraries(hello_world ${roscpp_LIBRARIES})

 Also, now that we’re using the roscpp package, we need to list it as a build- and run-dependency of our package:
src/modular_lib_pkg/package.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
<package>
<!-- Package Metadata -->
<name>modular_lib_pkg</name>
<maintainer email="you@example.com">Your Name</maintainer>
<description>
A ROS tutorial on modularity.
</description>
<version>0.0.0</version>
<license>BSD</license>

<!-- Required by Catkin -->
<buildtool_depend>catkin</buildtool_depend>

<!-- Package Dependencies -->
<build_depend>roscpp</build_depend>
<run_depend>roscpp</run_depend>
</package>

 At this point you should be able to compile the library by running catkin_make from the root of your workspace and see the following output:
1
2
3
4
Scanning dependencies of target hello_world
[100%] Building CXX object modular_lib_pkg/CMakeFiles/hello_world.dir/src/hello_world.cpp.o
Linking CXX shared library /tmp/devel/lib/libhello_world.so
[100%] Built target hello_world

Notice that it built the hello_world target into a file called libhello_world.so. This is the standard naming convention for dynamic libraries on Linux. Also, it built the library into the lib subdirectory of the develspace, so when you source one of the setup files in the devel directory, it will make this library available for dynamic linking at runtime.

Create the Node

 Now that we have our hello_world library, we can write a simple program to call the say_hello() function in that library. This program is nearly identical to the one used in the ROS C++ hello-world Tutorial9, except we replace the call to ROS_INFO with a call to say_hello() and we include the header file in the previous section.
modular_lib_pkg/src/hello_world_node.cpp

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Include the ROS C++ APIs
#include <ros/ros.h>

// Include the declaration of our library function
#include <modular_lib_pkg/hello_world.h>

// Standard C++ entry point
int main(int argc, char** argv) {
// Initialize ROS
ros::init(argc, argv, "hello_world_node");
ros::NodeHandle nh;

// Call our library function
say_hello();

// Wait for SIGINT/Ctrl-C
ros::spin();
return 0;
}

To build this node, just add an appropriate add_executable() call to the bottom of the package’s CMakeLists.txt:
1
2
add_executable(hello_world_node src/hello_world_node.cpp)
target_link_libraries(hello_world_node ${roscpp_LIBRARIES})

Building the Node (and getting a compiler error)

 At this point, you can try to build hello_world_node with catkin_make, but you will see the following error:

1
2
3
[100%] Building CXX object modular_lib_pkg/CMakeFiles/hello_world_node.dir/src/hello_world_node.cpp.o
/tmp/src/modular_lib_pkg/src/hello_world_node.cpp:5:42: fatal error: modular_lib_pkg/hello_world.h: No such file or directory
compilation terminated.

 The compiler is complaining about modular_lib_pkg/hello_world.h not existing, but we know it exists! The problem isn’t that the file doesn’t exist, but rather that we haven’t told the compiler where to look for it.

 In the same way that we added the header search paths for roscpp, we also need to add our own local include directory where we put our own headers. To do so, just add the relative path to src/modular_lib_pkg/include to the existing include_directories() command in CMakeLists.txt:

1
include_directories(include ${roscpp_INCLUDE_DIRS})

Building the Node (and getting a linker error)

At this point, you can try to build hello_world_node with catkin_make again, but you will see another error:

1
2
3
4
[100%] Building CXX object modular_lib_pkg/CMakeFiles/hello_world_node.dir/src/hello_world_node.cpp.o
Linking CXX executable /tmp/foo/devel/lib/modular_lib_pkg/hello_world_node
CMakeFiles/hello_world_node.dir/src/hello_world_node.cpp.o:hello_world_node.cpp:function main: error: undefined reference to 'say_hello()'
collect2: ld returned 1 exit status

 This time, hello_world_node.cpp is compiled successfully, but the linker reports an error that the say_hello() function is undefined. The declaration was found in the hello_world.h header file, otherwise it wouldn’t have compiled, still the definition from hello_world.cpp was missing.

 In order to resolve this, in addition to linking against ${roscpp_LIBRARIES}, we also link hello_world_node against the hello_world target so that its symbols are defined for the linker. This is done by adding hello_world to the existing target_link_libraries() command like the following:

1
target_link_libraries(hello_world_node ${roscpp_LIBRARIES} hello_world)

The following CMakeLists.txt file contains both this and the previous modifications:
src/modular_lib_pkg/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
# Declare the version of the CMake API for forward-compatibility
cmake_minimum_required(VERSION 2.8)

# Declare the name of the CMake Project
project(modular_lib_pkg)

# Find and get all the information about the roscpp package
find_package(roscpp REQUIRED)

# Find Catkin
find_package(catkin REQUIRED)
# Declare this project as a catkin package
catkin_package()

# Add the local headers and the headers from roscpp
include_directories(include ${roscpp_INCLUDE_DIRS})

# Define a library target called hello_world
add_library(hello_world src/hello_world.cpp)
target_link_libraries(hello_world ${roscpp_LIBRARIES})

# Define an executable target called hello_world_node
add_executable(hello_world_node src/hello_world_node.cpp)
target_link_libraries(hello_world_node ${roscpp_LIBRARIES} hello_world)

Building the Node (and succeeding)

Now you should be able to compile hello_world_node succesfully and then (assuming you sourced one of your workspace’s setup files) you can run it with rosrun:

1
rosrun modular_lib_pkg hello_world_node

This node does the same thing as before, except now, the core functionality is implemented in a separate library, which could more easily be used by other packages.

Using Libraries from Other Packages

Now that we’ve created a single package with its functionality built into a library, we can create another package which also uses that functionality. In this case, we’ll create another hello_world_node in another package which also links against libhello_world.so from modular_lib_pkg.

Create the Second Package and Node

First, create a package for the new node called modular_node_pkg:

1
mkdir src/modular_node_pkg

Next, add the source code for our node. This code is exactly the same as the hello_world_node.cpp in the modular_lib_pkg:
modular_node_pkg/hello_world_node.cpp
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// Include the ROS C++ APIs
#include <ros/ros.h>

// Include the declaration of our library function
#include <modular_lib_pkg/hello_world.h>

// Standard C++ entry point
int main(int argc, char** argv) {
// Initialize ROS
ros::init(argc, argv, "hello_world_node");
ros::NodeHandle nh;

// Call our library function
say_hello();

// Wait for SIGINT/Ctrl-C
ros::spin();
return 0;
}

Then add the following CMakeLists.txt and package.xml files to the new package. Note that now that we’re using the modular_lib_pkg just like we’re using the roscpp package, we need to find its headers and libraries just like we do with roscpp:
src/modular_node_pkg/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
# Declare the version of the CMake API for forward-compatibility
cmake_minimum_required(VERSION 2.8)

# Declare the name of the CMake Project
project(modular_node_pkg)

# Find and get all the information about the roscpp package
find_package(roscpp REQUIRED)

# Find and get all the information about the modular_lib_pkg package
find_package(modular_lib_pkg REQUIRED)

# Find Catkin
find_package(catkin REQUIRED)
# Declare this project as a catkin package
catkin_package()

# Add the headers from roscpp
include_directories(${roscpp_INCLUDE_DIRS} ${modular_lib_pkg_INCLUDE_DIRS})

# Define an executable target called hello_world_node
add_executable(hello_world_node2 hello_world_node.cpp)
target_link_libraries(hello_world_node2 ${roscpp_LIBRARIES} ${modular_lib_pkg_LIBRARIES})

NOTE: Goofy or not, the way that Catkin works, it combines all of your packages into a single CMake project. This means that each package must have unique target names. Otherwise the world will implode and unhappiness will descend upon the land. If you don’t want to have this constraint, you can use catkin_make_isolated which will build each package in isolation, but will be slower.
src/modular_lib_pkg/package.xml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
<package>
<!-- Package Metadata -->
<name>modular_node_pkg</name>
<maintainer email="you@example.com">Your Name</maintainer>
<description>
A ROS tutorial on modularity.
</description>
<version>0.0.0</version>
<license>BSD</license>

<!-- Required by Catkin -->
<buildtool_depend>catkin</buildtool_depend>

<!-- Package Dependencies -->
<build_depend>roscpp</build_depend>
<build_depend>modular_lib_pkg</build_depend>

<run_depend>roscpp</run_depend>
<run_depend>modular_lib_pkg</run_depend>
</package>

After creating these files, your workspace should look like the following:
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
.
├── build
│ └── ...
├── devel
│ └── ...
└── src
├── CMakeLists.txt -> /opt/ros/hydro/share/catkin/cmake/toplevel.cmake
├── modular_lib_pkg
│ ├── CMakeLists.txt
│ ├── include
│ │ └── modular_lib_pkg
│ │ └── hello_world.h
│ ├── package.xml
│ └── src
│ ├── hello_world.cpp
│ └── hello_world_node.cpp
└── modular_node_pkg
├── CMakeLists.txt
├── hello_world_node.cpp
└── package.xml

Building the Node (and getting a compiler error again)

If you try to build yor workspace by running catkin_make at this point, you will get the same compiler error as before, but this time with the new node!

1
2
3
[100%] Building CXX object modular_node_pkg/CMakeFiles/hello_world_node2.dir/hello_world_node.cpp.o
/tmp/foo/src/modular_node_pkg/hello_world_node.cpp:5:41: fatal error: modular_lib_pkg/hello_world.h: No such file or directory
compilation terminated.

Despite the fact that you included ${modular_lib_pkg_INCLUDE_DIRS} in the include_directories() CMake function, it still couldn’t find the header. This is because this sort of information needs to be exported by the other package.

With the current workspace, not only will ${modular_lib_pkg_INCLUDE_DIRS} be empty, but also ${modular_lib_pkg_LIBRARIES} will also be empty.

Exporting Package Flags to Other Packages

In the previous secion, our second package, modular_node_pkg, was unable to get the compilation or linker flags from the first package, modular_lib_pkg. This is because the flags weren’t exported by modular_lib_pkg. With Catkin, exporting such information is done with the catkin_package() command in the CMakeLists.txt file, and in the case of modular_lib_pkg, we didn’t pass it any arguments:

1
catkin_package()

This function can be left empty if we don’t need to export anything, but if we do, there are several optional arguments10 and the following are most commonly used:

  • INCLUDE_DIRS One or more header directories that should be made available to other packages. These directories are relative to the path of the given CMakeLists.txt file.
  • LIBRARIES One or more libraries that should be made available to other packages. These are the target names of the libraries.
  • CATKIN_DEPENDS One or more names of Catkin packages whose build flags should be passed transitively to any package which depends on this one. This will cause dependent packages to automatically call find_package() on each of these names.
  • DEPENDS One or more names of packages whose build flags should be passed transitively to any package which depends on this one. If a name like foo is given here, then Catkin will add whatever the contents of the ${foo_INCLUDE_DIRS} and ${foo_LIBRARIES} variables will be exported as part of this package’s include directories and libraries, respectively.
    In our case, we want to export both a local include directory and a library, so we modify the catkin_package() call in the modular_lib_pkg CMakeLists.txt to export the flags for our include directory and library.

Additionally, we should declare that anyone depending on this package should also use build flags from the roscpp package. This is important either if we link our library against libraries from the roscpp package or if any of our exported header files #include headers from roscpp.

1
2
3
4
5
catkin_package(
INCLUDE_DIRS include
LIBRARIES hello_world
CATKIN_DEPENDS roscpp
)

NOTE: In this specific case, leaving out the CATKIN_DEPENDS on roscpp won’t cause any problems, but this is only because it is unlikely that someone would try to build a ROS C++ node without depending on roscpp directly. A motivating example will be shown in the next section.
The complete CMakeLists.txt for modular_lib_pkg is as follows:
src/modular_lib_pkg/CMakeLists.txt
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
# Declare the version of the CMake API for forward-compatibility
cmake_minimum_required(VERSION 2.8)

# Declare the name of the CMake Project
project(modular_lib_pkg)

# Find and get all the information about the roscpp package
find_package(roscpp REQUIRED)

# Find Catkin
find_package(catkin REQUIRED)
# Declare this project as a catkin package and export the necessary build flags
catkin_package(
INCLUDE_DIRS include
LIBRARIES hello_world
CATKIN_DEPENDS roscpp
)

# Add the local headers and the headers from roscpp
include_directories(include ${roscpp_INCLUDE_DIRS})

# Define a library target called hello_world
add_library(hello_world src/hello_world.cpp)
target_link_libraries(hello_world ${roscpp_LIBRARIES})

# Define an executable target called hello_world_node
add_executable(hello_world_node src/hello_world_node.cpp)
target_link_libraries(hello_world_node ${roscpp_LIBRARIES} hello_world)

You can now build the workspace again with catkin_make, but this time it should succeed:
1
2
3
4
5
6
7
8
9
[ 33%] Building CXX object modular_lib_pkg/CMakeFiles/hello_world.dir/src/hello_world.cpp.o
Linking CXX shared library /tmp/devel/lib/libhello_world.so
[ 33%] Built target hello_world
[ 66%] Building CXX object modular_lib_pkg/CMakeFiles/hello_world_node.dir/src/hello_world_node.cpp.o
Linking CXX executable /tmp/devel/lib/modular_lib_pkg/hello_world_node
[ 66%] Built target hello_world_node
[100%] Building CXX object modular_node_pkg/CMakeFiles/hello_world_node2.dir/hello_world_node.cpp.o
Linking CXX executable /tmp/devel/lib/modular_node_pkg/hello_world_node2
[100%] Built target hello_world_node2

And finally, (assuming you still have your workspace environment set up), you can run hello_world_node2:
1
rosrun modular_node_pkg hello_world_node2

Reference:
original article


Building Modular ROS Packages
https://qiangsun89.github.io/2023/03/10/Building-Modular-ROS-Packages/
作者
Qiang Sun
发布于
2023年3月10日
许可协议