Skip to main content
🔌 pluginlibROS 2 · June 2026

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.

1

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.

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

2

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.

cpp
// 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.

3

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.

cpp
// 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
)
4

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.

xml
<!-- 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.

5

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

6

Step 5 — Loading Plugins at Runtime

ClassLoader<Base> loads the shared library and instantiates plugins. createSharedInstance() returns a shared_ptr managed by the loader.

cpp
#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;
}
7

Discovering Available Plugins

Use ros2 pkg commands and the ClassLoader API to discover which plugins are installed and registered.

bash
# 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 plugins

Quick Reference

ConceptDetails
Base class requirementPure virtual methods + virtual destructor + no-arg constructor
Plugin registration macroPLUGINLIB_EXPORT_CLASS(PluginType, BaseType) in .cpp file
XML manifest path attrCMake target name (no lib prefix, no .so suffix)
CMake registrationpluginlib_export_plugin_description_file(base_pkg plugins.xml)
Load pluginClassLoader<Base>("base_pkg", "base::Class").createSharedInstance(name)
List availableloader.getDeclaredClasses() — all installed + sourced plugins
Nav2 controllersnav2_core::Controller base class — DWB, MPPI, TEB are plugins
No-arg constructorpluginlib creates with new T() — init logic goes in initialize()
Discoveryros2 pkg xml <pkg> — shows exported plugin manifests
Source after installRe-source install/setup.bash after installing a new plugin package