ROS 2 - Plugins

Introduction

pluginlib is a C++ library for loading and unloading plugins (classes) from within a ROS package. They are dynamically loadable classes that are loaded from a runtime library (.so or .dll) without requiring compile-time linking.

How it Works

Advantages

Use Cases

Methodology - from ROS 2 Docs

Base Class Package

From ws/src, run:

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

This will generate a polygon_base. Open up ws/src/polygon_base/include/polygon_base/regular_polygon.hpp and past:

#ifndef POLYGON_BASE_REGULAR_POLYGON_HPP
#define POLYGON_BASE_REGULAR_POLYGON_HPP

namespace polygon_base {
class RegularPolygon {
public:
    /* This is a **pure virtual** function by set = 0. Meaning
     * every derived class needs to provide its own implementation.
     */
    virtual void initialize(double side_length) = 0;
    virtual double area() = 0;
    /* This is important to release resources at deletion
       */
    virtual ~RegularPolygon(){}

protected:
    /* This is to ensure that only the derived classes can instantiate the 
     * base class.
     */
    RegularPolygon(){}
};
}  // namespace polygon_base

#endif  // POLYGON_BASE_REGULAR_POLYGON_HPP

This is the abstract RegularPolygon class.

Important

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.

This is because the plugin needs to be loaded automatically without needing specific construction instructions. Otherwise it cannot be swapped easily.

Make header available to other classes. So in ws/src/polygon_base/CMakeLists.txt

ament_target_dependencies(...)
// After that add:

install(
DIRECTORY include/
DESTINATION include
)

// ...

ament_export_include_directories(
include
)
// Before this add:
ament_package()

Plugin Package

From ws/src, run:

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

Open up ws/src/polygon_plugins/src/polygon_plugins.cpp and paste:

#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_CLASSSquare, polygon_base::RegularPolygon
PLUGINLIB_EXPORT_CLASSTriangle, polygon_base::RegularPolygon

The PLUGINLIB_EXPORT_CLASS macro register the classes as actual plugins:

Then, in ws/src/polygon_plugins/plugins.xml, add:

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

Finally, need to export the plugin via the ws/src/polygon_plugins/CMakeLists.txt:

pluginlib_export_plugin_description_file(polygon_base plugins.xml)

Using the Plugins

In the base package (or any other package) add ws/src/polygon_base/src/area_node.cpp with 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.createSharedInstanceTriangle";
        triangle->initialize(10.0);

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

        printf("Triangle area: %.2f\n", triangle->area());
        printf("Square area: %.2f\n", square->area());
    } catchPluginlibException& 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:

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.

Build and Run

colcon build --packages-select polygon_base polygon_plugins
ros2 run polygon_base area_node