ROS 2 pluginlib Guide 2026
Master ROS 2's C++ plugin system: define base classes, register plugins with PLUGINLIB_EXPORT_CLASS, write XML manifests, register with pluginlib_export_plugin_description_file, and load at runtime via ClassLoader. Includes a complete Nav2 custom costmap layer example.
Why pluginlib?
Nav2 uses pluginlib for every swappable algorithm — controllers, planners, costmap layers, recovery behaviors. Understanding pluginlib lets you write custom Nav2 plugins, swap algorithms via YAML parameters, and build your own extensible ROS 2 frameworks. The pattern is: define interface → implement plugin → register XML → load with ClassLoader.
What Is pluginlib?
pluginlib is the ROS 2 plugin system for dynamically loading C++ classes at runtime. It lets you swap algorithms (costmap layers, controller plugins, planner plugins) without recompiling the host package.
pluginlib: Runtime C++ Plugin Loading
═══════════════════════════════════════════════════════
The pattern:
1. Define an abstract BASE CLASS in a shared library (the "interface")
2. Implement PLUGIN CLASSES that inherit from the base
3. Register plugins via XML manifest + PLUGINLIB_EXPORT_PLUGIN macro
4. Host package loads plugins at runtime via ClassLoader<Base>
Use cases in ROS 2:
Nav2 → costmap_2d::Layer (obstacle, inflation, static map layers)
Nav2 → nav2_core::Controller (DWB, MPPI, TEB controllers)
Nav2 → nav2_core::GlobalPlanner (NavFn, Smac Planner)
MoveIt → kinematics::KinematicsBase (KDL, IK-Fast solvers)
rviz2 → rviz_common::Display (sensor visualizers as plugins)
PCL → pcl_ros filter/feature plugins
Benefits:
✅ Users swap algorithms without changing host code
✅ New implementations ship as separate packages
✅ ClassLoader::createSharedInstance() returns a shared_ptr
✅ createUnmanagedInstance() for manual lifecycle control
✅ Available plugins discoverable via ros2 pkg xml⚡ pluginlib is NOT pluginlib for Python — it's C++ only. For Python algorithm swapping, use node parameters + importlib.import_module() or Python's ABC + factory pattern.
Step 1 — Define the Base Class (Interface)
The base class defines the interface that all plugins must implement. It lives in its own header and shared library. Pure virtual methods enforce the contract.
// include/my_costmap_pkg/layer_base.hpp
#pragma once
#include <string>
#include <memory>
namespace my_costmap_pkg {
class LayerBase {
public:
// Virtual destructor — required for polymorphism
virtual ~LayerBase() = default;
// Called once when the layer is first loaded
// Use instead of constructor — plugins have a default constructor
virtual void initialize(const std::string & name, double resolution) = 0;
// Called each costmap update cycle
// bounds = the region to update
virtual void updateCosts(
double robot_x, double robot_y, double robot_yaw,
double min_x, double min_y, double max_x, double max_y
) = 0;
// Must be true before updateCosts is called
virtual bool isCurrent() const = 0;
// Human-readable name (set in initialize)
std::string name_;
protected:
LayerBase() = default;
};
} // namespace my_costmap_pkg⚡ Plugins MUST have a no-argument constructor (or no constructor at all — the compiler generates one). pluginlib instantiates them with new T() and calls initialize() separately. Never put required setup in the constructor.
Step 2 — Implement a Plugin Class
The plugin inherits from LayerBase and provides concrete implementations of all pure virtual methods. It registers itself with the PLUGINLIB_EXPORT_PLUGIN macro.
// include/my_costmap_pkg/static_layer.hpp
#pragma once
#include "my_costmap_pkg/layer_base.hpp"
namespace my_costmap_pkg {
class StaticLayer : public LayerBase {
public:
StaticLayer() = default; // no-arg constructor required
void initialize(const std::string & name, double resolution) override;
void updateCosts(double, double, double, double, double, double, double) override;
bool isCurrent() const override { return is_current_; }
private:
bool is_current_ = false;
double resolution_ = 0.05;
};
} // namespace my_costmap_pkg
// ──────────────────────────────────────────────────────────
// src/static_layer.cpp
#include "my_costmap_pkg/static_layer.hpp"
#include <pluginlib/class_list_macros.hpp>
namespace my_costmap_pkg {
void StaticLayer::initialize(const std::string & name, double resolution) {
name_ = name;
resolution_ = resolution;
is_current_ = true;
RCLCPP_INFO(
rclcpp::get_logger("static_layer"),
"StaticLayer '%s' initialized at %.2f m/cell", name.c_str(), resolution
);
}
void StaticLayer::updateCosts(
double, double, double, // robot pose (unused here)
double, double, double, double // bounds (unused here)
) {
// Static layer: cost never changes — nothing to do
}
} // namespace my_costmap_pkg
// ── REGISTER the plugin ──────────────────────────────────
// First arg: fully-qualified plugin class name
// Second arg: fully-qualified base class name
PLUGINLIB_EXPORT_CLASS(
my_costmap_pkg::StaticLayer,
my_costmap_pkg::LayerBase
)Step 3 — Plugin XML Manifest
The XML manifest tells pluginlib which classes are in which shared library. It must be registered in CMakeLists.txt and package.xml to be discoverable.
<!-- plugins/costmap_plugins.xml -->
<library path="static_layer">
<!-- path = the library target name from CMakeLists.txt
(without lib prefix or .so suffix) -->
<class
type="my_costmap_pkg::StaticLayer"
base_class_type="my_costmap_pkg::LayerBase"
>
<description>
Static costmap layer — loads a pre-defined obstacle map.
</description>
</class>
</library>
<!-- Multiple plugins in the same library: -->
<!--
<library path="my_costmap_layers">
<class type="my_costmap_pkg::StaticLayer"
base_class_type="my_costmap_pkg::LayerBase">
<description>Static map layer</description>
</class>
<class type="my_costmap_pkg::InflationLayer"
base_class_type="my_costmap_pkg::LayerBase">
<description>Inflation layer</description>
</class>
<class type="my_costmap_pkg::ObstacleLayer"
base_class_type="my_costmap_pkg::LayerBase">
<description>Dynamic obstacle layer</description>
</class>
</library>
-->⚡ The path attribute is the CMake target name (what you put in add_library(NAME …)) — NOT the file path. pluginlib appends lib prefix and .so suffix automatically.
Step 4 — CMakeLists.txt Registration
Register the plugin XML manifest so other packages can discover it via pluginlib::ClassLoader. Export the base class header so plugins can include it.
cmake_minimum_required(VERSION 3.8)
project(my_costmap_pkg)
find_package(ament_cmake REQUIRED)
find_package(rclcpp REQUIRED)
find_package(pluginlib REQUIRED)
# ── Plugin library (implements LayerBase) ────────────────
add_library(static_layer SHARED src/static_layer.cpp)
target_include_directories(static_layer PUBLIC
$<BUILD_INTERFACE:${CMAKE_CURRENT_SOURCE_DIR}/include>
$<INSTALL_INTERFACE:include>
)
ament_target_dependencies(static_layer rclcpp pluginlib)
# ── Register plugin XML manifest ─────────────────────────
# This macro installs the XML and registers it in ament index
# so ClassLoader can find it across packages
pluginlib_export_plugin_description_file(
my_costmap_pkg # base class package
plugins/costmap_plugins.xml
)
# ── Export headers (other packages need LayerBase) ───────
ament_export_include_directories(include)
ament_export_libraries(static_layer)
ament_export_dependencies(pluginlib)
install(DIRECTORY include/ DESTINATION include)
install(TARGETS static_layer
ARCHIVE DESTINATION lib
LIBRARY DESTINATION lib
RUNTIME DESTINATION bin
)
ament_package()⚡ pluginlib_export_plugin_description_file() takes the BASE CLASS PACKAGE name (where LayerBase is defined), not the plugin package name. If the base and plugin are in the same package, use the same name twice.
Step 5 — Loading Plugins at Runtime
ClassLoader<Base> loads the shared library and instantiates plugins. createSharedInstance() returns a shared_ptr managed by the loader.
#include <pluginlib/class_loader.hpp>
#include "my_costmap_pkg/layer_base.hpp"
#include <rclcpp/rclcpp.hpp>
int main(int argc, char ** argv) {
rclcpp::init(argc, argv);
auto logger = rclcpp::get_logger("plugin_demo");
// ── Create ClassLoader ────────────────────────────────
// Template: base class type
// First arg: package that exports the base class (package.xml)
// Second arg: fully-qualified base class name
pluginlib::ClassLoader<my_costmap_pkg::LayerBase> loader(
"my_costmap_pkg",
"my_costmap_pkg::LayerBase"
);
// ── List available plugins ────────────────────────────
auto classes = loader.getDeclaredClasses();
for (auto & cls : classes) {
RCLCPP_INFO(logger, "Found plugin: %s", cls.c_str());
}
// ── Instantiate from config string ───────────────────
std::string plugin_name = "my_costmap_pkg::StaticLayer";
try {
// Returns shared_ptr<LayerBase>
auto layer = loader.createSharedInstance(plugin_name);
layer->initialize("static_map", 0.05);
if (layer->isCurrent()) {
RCLCPP_INFO(logger, "Layer '%s' is current", layer->name_.c_str());
}
layer->updateCosts(0.0, 0.0, 0.0, -5.0, -5.0, 5.0, 5.0);
} catch (pluginlib::PluginlibException & ex) {
RCLCPP_ERROR(logger, "Plugin load failed: %s", ex.what());
}
rclcpp::shutdown();
return 0;
}Discovering Available Plugins
Use ros2 pkg commands and the ClassLoader API to discover which plugins are installed and registered.
# List all plugin manifests exported by a package
ros2 pkg xml my_costmap_pkg | grep pluginlib
# Show raw XML manifest content
ros2 pkg xml my_costmap_pkg
# ── In a Nav2 context: list available costmap layers ──────
# Nav2 uses pluginlib for all its algorithm plugins
ros2 param list /local_costmap/local_costmap | grep plugin
# Current plugin loaded in Nav2 controller
ros2 param get /controller_server FollowPath.plugin
# List all Nav2 plugins of a specific type
# (look in the source: nav2_core package defines base classes)
# ── C++ API: list at runtime ──────────────────────────────
# auto classes = loader.getDeclaredClasses();
# → "nav2_costmap_2d::StaticLayer"
# → "nav2_costmap_2d::ObstacleLayer"
# → "nav2_costmap_2d::InflationLayer"
# ── Check if plugin is registered ─────────────────────────
# loader.isClassAvailable("my_costmap_pkg::StaticLayer")
# → true if installed + sourced
# ── Common Nav2 plugin base classes ────────────────────────
# nav2_core::Controller → controller_server plugins
# nav2_core::GlobalPlanner → planner_server plugins
# nav2_core::CostmapLayer → costmap_2d plugins
# nav2_core::Smoother → smoother_server plugins
# nav2_core::RecoveryBehavior → behavior_server pluginsQuick Reference
| Concept | Details |
|---|---|
| Base class requirement | Pure virtual methods + virtual destructor + no-arg constructor |
| Plugin registration macro | PLUGINLIB_EXPORT_CLASS(PluginType, BaseType) in .cpp file |
| XML manifest path attr | CMake target name (no lib prefix, no .so suffix) |
| CMake registration | pluginlib_export_plugin_description_file(base_pkg plugins.xml) |
| Load plugin | ClassLoader<Base>("base_pkg", "base::Class").createSharedInstance(name) |
| List available | loader.getDeclaredClasses() — all installed + sourced plugins |
| Nav2 controllers | nav2_core::Controller base class — DWB, MPPI, TEB are plugins |
| No-arg constructor | pluginlib creates with new T() — init logic goes in initialize() |
| Discovery | ros2 pkg xml <pkg> — shows exported plugin manifests |
| Source after install | Re-source install/setup.bash after installing a new plugin package |