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
- Dynamic Loading: Plugins are typically compiled as shared libraries that can be loaded into the application at runtime. This is done using a class loader mechanism provided by the ROS 2 ecosystem (often through the
pluginlibpackage). - Interface Definition: Developers define a common interface for a particular type of functionality (e.g., sensor drivers, planners, controllers). The plugins then implement these interfaces.
- Registration and Discovery: Each plugin is registered using an XML manifest file that describes the available classes and their corresponding shared libraries. When the system starts up, the plugin loader scans for these manifest files, allowing the system to know what plugins are available.
- Runtime Integration: Once loaded, these plugins can be instantiated and used as needed. This enables swapping out or upgrading components without recompiling the entire system.
Advantages
- Modularity: Encourages a clean separation of concerns by allowing different functionalities to reside in separate modules.
- Extensibility: New features or alternative implementations can be added easily without altering the base application.
- Flexibility: Enables runtime configuration changes, making it possible to choose different implementations based on the current operating conditions.
- Reduced Dependencies: By decoupling components, plugins help in minimizing the dependencies between various parts of the system, which can simplify both development and testing.
Use Cases
- Sensor Integration: Loading different sensor drivers as plugins allows a robot to support multiple sensor types without a need for system-wide changes.
- Algorithm Selection: For tasks like path planning or object recognition, different algorithms can be implemented as plugins, enabling the system to switch between them based on context or performance needs.
- Simulation and Visualization: Simulation environments can load various plugins to simulate different robot components or environmental factors, making it easier to test new features.
- Controller Customization: Control systems can be designed with plugin-based architectures, where each plugin represents a different control strategy, allowing for easy testing and replacement.
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.
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:
polygon_plugins::Squareis the qualified plugin class.polygon_plugins::RegularPolygonis the qualified base class.
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>
- The
librarytag 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. - The
classtag 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'spolygon_plugins::Square.base_class: The fully qualified base class type for the plugin. For us, that'spolygon_base::RegularPolygon.description: A description of the plugin and what it does.
Finally, need to export the plugin via the ws/src/polygon_plugins/CMakeLists.txt:
pluginlib_export_plugin_description_file(polygon_base plugins.xml)
- The package with the base class, i.e.
polygon_base. - The relative path to the Plugin Declaration xml, i.e.
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:
- 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.
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