5.3.2.13. Creating and using plugins (C++)

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

Tutorial level: Beginner

Time: 20 minutes

5.3.2.13.1. 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, you do not have to explicitly link your 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.

5.3.2.13.2. Prerequisites

This tutorial assumes basic C++ knowledge and that you have successfully installed ROS 2.

5.3.2.13.3. Tasks

In this tutorial, you will create two new packages, one that defines the base class, and another 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 command:

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

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

The code above creates an abstract class called RegularPolygon. One thing to notice is the presence of the initialize method. With pluginlib, a constructor without parameters is required, so if any parameters to the class are needed, we use the initialize method to pass them to 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 command:

ros2 pkg create --build-type ament_cmake --license Apache-2.0 --dependencies polygon_base pluginlib --library-name polygon_plugins 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 is 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 enable plugin instances to be created when the containing library 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.

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 line to your ros2_ws/src/polygon_plugins/CMakeLists.txt after the line reading find_package(pluginlib REQUIRED):

pluginlib_export_plugin_description_file(polygon_base plugins.xml)

The arguments to the pluginlib_export_plugin_description_file command are:

  1. The package with 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 it’s 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 file:

  • 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:

source install/setup.bash

Now run the node:

ros2 run polygon_base area_node

It should print:

Triangle area: 43.30
Square area: 100.00

5.3.2.13.4. Summary

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