After our series of post about ROS 2 CLI tools (1, 2), we continue exploring the ROS 2 realm taking a look at ROS 2 components and more specifically, how they compare to plugins.
Long story short, components are plugins.
Short story long? Is that a thing?
Well plugins and components are indeed essentially the same thing. Down the road, both are built into respective shared libraries, neither have a main function, they both are loaded at runtime and used by a third party.
We’ll note here that while plugins come straight outta ROS 1, components on the other hand are the ROS 2 evolution of ROS 1 nodelets (what’s that?) after being exposed to a Fire Stone on a full moon. Same idea, different beasts.
Plugins vs. Components
So what are the actual differences? To put it simply, a component is a plugin which derives from a ROS 2 node.
This assertion is backed by the fact the both rely on the class_loaderpackage, a ROS-independent library for dynamic class introspection and loading from runtime libraries. Their respective internal plugin-related plumbing (factory, registration, library path finding, loading etc.) is managed underneath by class_loader. Both offer a macro-based helper for registration, and both macros resolve to class_loader‘s CLASS_LOADER_REGISTER_CLASS macro. Yeah they immediately resolve to it, I mean, they don’t even try to hide it. On the other hand, why would they?
For a traditional plugin, the base class can be anything, user defined or not. The only constraint is that the derived class has to be default constructible (therefore, by the law of C++, the base class too). In the case of components, the plugin class commonly derives from the rclcpp::Node class, but it is not required. Indeed, the requirements for a class to be exported as a component are,
Have a constructor that takes a single argument that is a rclcpp::NodeOptions instance.
which includes rclcpp::Node of course, but also e.g. rclcpp_lifecycle::LifecycleNode. It means that a component can inherit from any class meeting those requirements, but it can also implement them itself and skip inheritance altogether.
That last point is an important difference between plugins and components.
Components are special plugins
Indeed, traditionally plugins must declare their base classes when registering to class_loader factory scheme. This allows the factory to instantiate the derived object to a smart-pointer of base type which in turn can be delivered to the user, the developer.
// Registering a plugin
// Instantiating a plugin
// We have access to all of BaseClass API
std::shared_ptr<BaseClass> base_ptr = plugin_loader.createSharedInstance("DerivedClass");
Components’ base class registration on the other hand was developed relying on void (smart) pointer-based type-erasure. Thus allowing for a simple wrapper class as a common base class to all components. This design implies that the developer is not expected to query the factory for a new instance by himself. What would you do with a void pointer anyway? Instead the factory serves intended agents such as the rclcpp_components::ComponentManager and rclcpp_components::ComponentContainer.
// Registering a component
// NodeInstanceWrapper is a wrapper around both a
// std::shared_ptr<void> and a
// Not much to do with those!
rclcpp_components::NodeInstanceWrapper component_wrapper = component_loader->create_node_instance(options);
In most cases, components inherit from rclcpp::Node as it is the easiest way to fulfill the above requirements. Therefore, we’ll assume that a component is a ROS 2 node, with everything it involves, possibly parameters, listeners/publishers, services, action, et tutti quanti.
We can therefore make an important distinction here: components are plugins with a ROS 2 interface. This draws an important line as when and where to use plugins or components.
Plugins or components, that is the question
The answer to this question obviously depends on your project. But first, let me assume that you do have a need for plugins and spare you asking
‘Do you really need to use plugins?’
(see what I did there).
As a general rule, I’d recommend for you to use components over plugins whenever possible. The reason is simple, you, as a components developer, do not have to deal with the plugin aspect of things beside the registration macro. No plugin name as a ROS parameter, no plugin loader, no exception handling etc. Then from a user perspective it is clearer I believe to launch a specific node, say my_plugin_b_node, rather than launching an interface node given a plugin’s name as a ROS parameter my_node plugin_name:='PluginB'. It also spares the frustration of seeing the node crash or being stall because the plugin is not found or misspelled.
Note that this recommendation does not prevent you from enforcing some common API the same manner it would arise from using plugins. Your components can inherit from a base class of your making which enforce this API. The base class may also implement the ROS interface, abstracting it away, leaving to the derived class a simple virtual doMath(Arg) -like function to define.
But my stuff is so modular that I’m using several plugins
Can you possibly rethink the design? Can you break it down in several small nodes that communicate through ROS interfaces? I’d bet that some of those intermediate data could be useful somewhere else in your system too.
If not, or if you find yourself with a different use case which does require plugin, you may still declare your node class as a component! Components and plugins are not mutually exclusive. Moreover,
While doing the legwork for this post, I wrote a small ROS 2 package which has no other purpose than being a goto example on how to write a component or a plugin in ROS 2. You may find it on github: demo_plugin_component. This example showcase the use of plugins in conjunction with components, look for one and get both. After compiling, one may run it in either way,
Like a plain node,
$ ros2 run demo_plugin_component talker_node __params:=demo_plugin_component/cfg/params.yaml