Creating and using plugins (C++)

Goal: Learn to create and load a simple plugin using pluginlib

Tutorial level: Beginner

Time: 20 minutes

Background

This tutorial is derived from http://wiki.ros.org/pluginlib and Writing and Using a Simple Plugin Tutorial.

pluginlib is a C++ library for loading and unloading plugins from within a ROS package. Plugins are dynamically loadable classes that are loaded from a runtime library (i.e. shared object, dynamically linked library). With pluginlib, one does not have to explicitly link their application against the library containing the classes – instead pluginlib can open a library containing exported classes at any point without the application having any prior awareness of the library or the header file containing the class definition. Plugins are useful for extending/modifying application behavior without needing the application source code.

Prerequisites

This tutorial assumes basic C++ knowledge and that you have pluginlib installed.

sudo apt-get install ros-humble-pluginlib

Tasks

In this tutorial, you will create a two new packages, one that defines the base class, and the other that provides the plugins. The base class will define a generic polygon class, and then our plugins will define specific shapes.

1 Create the Base Class Package

Create a new empty package in your ros2_ws/src folder with the following terminal command.

ros2 pkg create --build-type ament_cmake polygon_base --dependencies pluginlib --node-name area_node

Open your favorite editor, edit ros2_ws/src/polygon_base/include/polygon_base/regular_polygon.hpp, and paste the following inside of it:

#ifndef POLYGON_BASE_REGULAR_POLYGON_HPP
#define POLYGON_BASE_REGULAR_POLYGON_HPP

namespace polygon_base
{
  class RegularPolygon
  {
    public:
      virtual void initialize(double side_length) = 0;
      virtual double area() = 0;
      virtual ~RegularPolygon(){}

    protected:
      RegularPolygon(){}
  };
}  // namespace polygon_base

#endif  // POLYGON_BASE_REGULAR_POLYGON_HPP

This code above should be pretty self explanatory… we’re creating an abstract class called RegularPolygon. One thing to notice is the presence of the initialize method. With pluginlib, a constructor without parameters is required for classes so, if any parameters are required, we use the initialize method to initialize the object.

We need to make this header available to other classes, so open ros2_ws/src/polygon_base/CMakeLists.txt for editing. Add the following lines after the ament_target_dependencies command.

install(
  DIRECTORY include/
  DESTINATION include
)

And add this command before the ament_package command

ament_export_include_directories(
  include
)

We will return to this package later to write our test node.

2 Create the Plugin Package

Now we’re going to write two non-virtual implementations of our abstract class. Create a second empty package in your ros2_ws/src folder with the following terminal command.

ros2 pkg create --build-type ament_cmake polygon_plugins --dependencies polygon_base pluginlib --library-name polygon_plugins

2.1 Source code for the plugins

Open ros2_ws/src/polygon_plugins/src/polygon_plugins.cpp for editing, and paste the following inside of it:

#include <polygon_base/regular_polygon.hpp>
#include <cmath>

namespace polygon_plugins
{
  class Square : public polygon_base::RegularPolygon
  {
    public:
      void initialize(double side_length) override
      {
        side_length_ = side_length;
      }

      double area() override
      {
        return side_length_ * side_length_;
      }

    protected:
      double side_length_;
  };

  class Triangle : public polygon_base::RegularPolygon
  {
    public:
      void initialize(double side_length) override
      {
        side_length_ = side_length;
      }

      double area() override
      {
        return 0.5 * side_length_ * getHeight();
      }

      double getHeight()
      {
        return sqrt((side_length_ * side_length_) - ((side_length_ / 2) * (side_length_ / 2)));
      }

    protected:
      double side_length_;
  };
}

#include <pluginlib/class_list_macros.hpp>

PLUGINLIB_EXPORT_CLASS(polygon_plugins::Square, polygon_base::RegularPolygon)
PLUGINLIB_EXPORT_CLASS(polygon_plugins::Triangle, polygon_base::RegularPolygon)

The implementation of the Square and Triangle classes should be fairly straightforward: save the side length, and use it to calculate the area. The only piece that is pluginlib specific is the last three lines, which invokes some magical macros that register the classes as actual plugins. Let’s go through the arguments to the PLUGINLIB_EXPORT_CLASS macro:

  1. The fully-qualified type of the plugin class, in this case, polygon_plugins::Square.

  2. The fully-qualified type of the base class, in this case, polygon_base::RegularPolygon.

2.2 Plugin Declaration XML

The steps above make it so that instances of our plugins can be created once the library they exist in is loaded, but the plugin loader still needs a way to find that library and to know what to reference within that library. To this end, we’ll also create an XML file that, along with a special export line in the package manifest, makes all the necessary information about our plugins available to the ROS toolchain.

Create ros2_ws/src/polygon_plugins/plugins.xml with the following code:

<library path="polygon_plugins">
  <class type="polygon_plugins::Square" base_class_type="polygon_base::RegularPolygon">
    <description>This is a square plugin.</description>
  </class>
  <class type="polygon_plugins::Triangle" base_class_type="polygon_base::RegularPolygon">
    <description>This is a triangle plugin.</description>
  </class>
</library>

A couple things to note:

  1. The library tag gives the relative path to a library that contains the plugins that we want to export. In ROS 2, that is just the name of the library. In ROS 1 it contained the prefix lib or sometimes lib/lib (i.e. lib/libpolygon_plugins) but here it is simpler.

  2. The class tag declares a plugin that we want to export from our library. Let’s go through its parameters:

  • type: The fully qualified type of the plugin. For us, that’s polygon_plugins::Square.

  • base_class: The fully qualified base class type for the plugin. For us, that’s polygon_base::RegularPolygon.

  • description: A description of the plugin and what it does.

  • name: There used to be a name attribute, but it is no longer required.

2.3 CMake Plugin Declaration

The last step is to export your plugins via CMakeLists.txt. This is a change from ROS 1, where the exporting was done via package.xml. Add the following block to your ros2_ws/src/polygon_plugins/CMakeLists.txt after the line reading find_package(pluginlib REQUIRED)

add_library(polygon_plugins src/polygon_plugins.cpp)
target_include_directories(polygon_plugins PUBLIC
  $<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
  $<INSTALL_INTERFACE:include>)
ament_target_dependencies(
  polygon_plugins
  polygon_base
  pluginlib
)

pluginlib_export_plugin_description_file(polygon_base plugins.xml)

install(
  TARGETS polygon_plugins
  EXPORT export_${PROJECT_NAME}
  ARCHIVE DESTINATION lib
  LIBRARY DESTINATION lib
  RUNTIME DESTINATION bin
)

And before the ament_package command, add

ament_export_libraries(
  polygon_plugins
)
ament_export_targets(
  export_${PROJECT_NAME}
)

The arguments to this CMake command are

  1. The package for the base class, i.e. polygon_base

  2. The relative path to the Plugin Declaration xml, i.e. plugins.xml

3 Use the Plugins

Now its time to use the plugins. This can be done in any package, but here we’re going to do it in the base package. Edit ros2_ws/src/polygon_base/src/area_node.cpp to contain the following:

#include <pluginlib/class_loader.hpp>
#include <polygon_base/regular_polygon.hpp>

int main(int argc, char** argv)
{
  // To avoid unused parameter warnings
  (void) argc;
  (void) argv;

  pluginlib::ClassLoader<polygon_base::RegularPolygon> poly_loader("polygon_base", "polygon_base::RegularPolygon");

  try
  {
    std::shared_ptr<polygon_base::RegularPolygon> triangle = poly_loader.createSharedInstance("polygon_plugins::Triangle");
    triangle->initialize(10.0);

    std::shared_ptr<polygon_base::RegularPolygon> square = poly_loader.createSharedInstance("polygon_plugins::Square");
    square->initialize(10.0);

    printf("Triangle area: %.2f\n", triangle->area());
    printf("Square area: %.2f\n", square->area());
  }
  catch(pluginlib::PluginlibException& ex)
  {
    printf("The plugin failed to load for some reason. Error: %s\n", ex.what());
  }

  return 0;
}

The ClassLoader is the key class to understand, defined in the class_loader.hpp header.

  • It is templated with the base class, i.e. polygon_base::RegularPolygon

  • The first argument is a string for the package name of the base class, i.e. polygon_base

  • The second argument is a string with the fully qualified base class type for the plugin, i.e. polygon_base::RegularPolygon

There are a number of ways to instantiate an instance of the class. In this example, we’re using shared pointers. We just need to call createSharedInstance with the fully-qualified type of the plugin class, in this case, polygon_plugins::Square.

Important note: the polygon_base package in which this node is defined does NOT depend on the polygon_plugins class. The plugins will be loaded dynamically without any dependency needing to be declared. Furthermore, we’re instantiating the classes with hardcoded plugin names, but you can also do so dynamically with parameters, etc.

4 Build and run

Navigate back to the root of your workspace, ros2_ws, and build your new packages:

colcon build --packages-select polygon_base polygon_plugins

From ros2_ws, be sure to source the setup files:

. install/setup.bash

Now run the node:

ros2 run polygon_base area_node

It should print

Triangle area: 43.30
Square area: 100.00

Summary

Congratulations! You’ve just written and used your first plugins.