Creating C++ Libraries (for ament_cmake)

The C++ library block for ament_cmake

TL;DR

When your project exports a library, you might benefit from using the following template. Note that there is, in general, no reason to define multiple libraries. A single shared library can hold all the content that you want to export from a package, hence the library named ${PROJECT_NAME}.

Remember to

  1. Add all exported headers to include/<PACKAGE_NAME> otherwise other packages cannot see it.

  2. Add all source files of the library to add_library.

  3. Add all ROS2 dependencies of the library to ament_target_dependencies.

  4. Add ALL dependencies for which you used find_package to ament_export_dependencies, otherwise dependencies might become complex for projects that use your library.

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

####################################
# CPP Shared Library Block [BEGIN] #
# vvvvvvvvvvvvvvvvvvvvvvvvvvvvvvvv #
# https://ros2-tutorial.readthedocs.io/en/latest/
# The most common use case is to merge everything you need to export
# into the same shared library called ${PROJECT_NAME}.
add_library(${PROJECT_NAME} SHARED
    src/sample_class.cpp

    )

ament_target_dependencies(${PROJECT_NAME}
    rclcpp

    )

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

ament_export_targets(export_${PROJECT_NAME} HAS_LIBRARY_TARGET)
ament_export_dependencies(
    rclcpp
    Eigen3
    Qt5Core

    )

target_link_libraries(${PROJECT_NAME}
    Qt5::Core

    )

install(
    DIRECTORY include/
    DESTINATION include
    )

install(
    TARGETS ${PROJECT_NAME}
    EXPORT export_${PROJECT_NAME}
    LIBRARY DESTINATION lib
    ARCHIVE DESTINATION lib
    RUNTIME DESTINATION bin
    INCLUDES DESTINATION include
    )
# ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ #
# CPP Shared Library Block [END] #
##################################

The base package can be created with

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

resulting in the following output

ros2 pkg create output
ros2 pkg create cpp_package_with_a_library \
--build-type ament_cmake \
--dependencies rclcpp
going to create a new package
package name: cpp_package_with_a_library
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_library
creating ./cpp_package_with_a_library/package.xml
creating source and include folder
creating folder ./cpp_package_with_a_library/src
creating folder ./cpp_package_with_a_library/include/cpp_package_with_a_library
creating ./cpp_package_with_a_library/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

Library sources

In this step, we’ll work on these.

cpp_package_with_a_library
├── CMakeLists.txt
├── include
│   └── cpp_package_with_a_library
│       └── sample_class.hpp
├── package.xml
└── src
    ├── sample_class.cpp
    ├── sample_class_local_node.cpp
    ├── sample_class_local_node.hpp
    └── sample_class_local_node_main.cpp

A class that does a bunch of nothing, but that depends on Eigen3 and Qt, as an example.

sample_class.hpp

 1#pragma once
 2
 3#include <ostream>
 4
 5#include <QString>
 6#include <eigen3/Eigen/Dense>
 7
 8class SampleClass
 9{
10private:
11    int a_private_member_;
12    const QString a_private_qt_string_;
13    const Eigen::MatrixXd a_private_eigen3_matrix_;
14
15public:
16    SampleClass();
17
18    int get_a_private_member() const;
19    void set_a_private_member(int value);
20    std::string to_string() const;
21
22    static double sum_of_squares(const double&a, const double& b);
23};
24
25std::ostream& operator<<(std::ostream& os, const SampleClass& sc);

sample_class.cpp

 1#include <cpp_package_with_a_library/sample_class.hpp>
 2
 3
 4
 5/**
 6 * @brief SampleClass::SampleClass the default constructor.
 7 */
 8SampleClass::SampleClass():
 9    a_private_qt_string_("I am a QString"),
10    a_private_eigen3_matrix_((Eigen::Matrix2d() << 1,2,3,4).finished())
11{
12
13}
14
15/**
16 * @brief SampleClass::get_a_private_member.
17 * @return an int with the value of a_private_member_.
18 */
19int SampleClass::get_a_private_member() const
20{
21    return a_private_member_;
22}
23
24/**
25 * @brief SampleClass::set_a_private_member.
26 * @param value The new value for a_private_member_.
27 */
28void SampleClass::set_a_private_member(int value)
29{
30    a_private_member_ = value;
31}
32
33/**
34 * @brief SampleClass::sum_of_squares.
35 * @param a The first number.
36 * @param b The second number.
37 * @return a*a + 2*a*b + b*b.
38 */
39double SampleClass::sum_of_squares(const double &a, const double &b)
40{
41    return a*a + 2*a*b + b*b;
42}
43
44/**
45 * @brief SampleClass::to_string converts a SampleClass to a std::string representation.
46 * @return a pretty(-ish) std::string representation of the object.
47 */
48std::string SampleClass::to_string() const
49{
50    std::stringstream ss;
51    ss << "Sample_Class:: " << std::endl <<
52          "a_private_member_ = "        << std::to_string(a_private_member_) << std::endl <<
53          "a_private_qt_string_ = "     << a_private_qt_string_.toStdString() << std::endl <<
54          "a_private_eigen3_matrix_ = " << a_private_eigen3_matrix_ << std::endl;
55    return ss.str();
56}
57
58/**
59 * @brief operator << the stream operator for SampleClass objects.
60 * @param [in/out] the std::ostream to be modified.
61 * @param [in] sc the SampleClass whose representation is to be streamed.
62 * @return the modified os with the added SampleClass string representation.
63 * @see SampleClass::to_string().
64 */
65std::ostream &operator<<(std::ostream &os, const SampleClass &sc)
66{
67    return os << sc.to_string();
68}

Sources for a local node that uses the library

In this step, we’ll work on these.

cpp_package_with_a_library
├── CMakeLists.txt
├── include
│   └── cpp_package_with_a_library
│       └── sample_class.hpp
├── package.xml
└── src
    ├── sample_class.cpp
    ├── sample_class_local_node.cpp
    ├── sample_class_local_node.hpp
    └── sample_class_local_node_main.cpp

Just in case you need to have a node, in the same package, that also uses the library exported by this package. Nothing too far from what we have already done.

sample_class.cpp

 1#include "sample_class_local_node.hpp"
 2
 3/**
 4 * @brief SampleClassLocalNode::SampleClassLocalNode Default constructor.
 5 */
 6SampleClassLocalNode::SampleClassLocalNode():
 7    rclcpp::Node("sample_class_local_node"),
 8    timer_period_(0.5),
 9    print_count_(0)
10{
11    timer_ = create_wall_timer(
12                std::chrono::milliseconds(long(timer_period_*1e3)),
13                std::bind(&SampleClassLocalNode::_timer_callback, this)
14                );
15}
16
17/**
18 * @brief SampleClassLocalNode::_timer_callback periodically prints class info using RCLCPP_INFO.
19 */
20void SampleClassLocalNode::_timer_callback()
21{
22    RCLCPP_INFO_STREAM(get_logger(),
23                       std::string("sum_of_squares = ") +
24                       std::to_string(SampleClass::sum_of_squares(print_count_,print_count_-5))
25                       );
26
27    RCLCPP_INFO_STREAM(get_logger(),
28                       sample_class_.to_string() +
29                       std::to_string(print_count_) +
30                       std::string(" times.")
31                       );
32    print_count_++;
33}

sample_class_local_node.cpp

 1#pragma once
 2
 3#include <rclcpp/rclcpp.hpp>
 4#include <cpp_package_with_a_library/sample_class.hpp>
 5
 6/**
 7 * @brief A ROS2 Node that uses the SampleClass within the same package.
 8 */
 9class SampleClassLocalNode: public rclcpp::Node
10{
11private:
12    SampleClass sample_class_;
13
14    double timer_period_;
15    int print_count_;
16    rclcpp::TimerBase::SharedPtr timer_;
17
18    void _timer_callback();
19public:
20    SampleClassLocalNode();
21
22};

sample_class.cpp

 1#include <rclcpp/rclcpp.hpp>
 2
 3#include "sample_class_local_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<SampleClassLocalNode>();
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}