From b587d66bde7007f360c9c162f7b0ed2b7e052f2e Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 4 Aug 2025 14:17:44 -0500 Subject: [PATCH 1/5] Added internal storage for loaded plugins to keep them in scope for the lifetime of the plugin loader --- README.md | 14 ++++++++------ include/boost_plugin_loader/plugin_loader.h | 5 +++++ include/boost_plugin_loader/plugin_loader.hpp | 9 ++++++++- 3 files changed, 21 insertions(+), 7 deletions(-) diff --git a/README.md b/README.md index f313d4d..20c975c 100644 --- a/README.md +++ b/README.md @@ -53,20 +53,22 @@ See the [`ShapeFactory` plugin for an example implementation](examples/shape/sha ## Keep plugins in scope during use -Once the plugin object goes out of scope, the library providing it will be unloaded, resulting in undefined behavior and potential segfaults. -Thus, the plugin object must be kept in scope for as long as it (and objects created by it) are being used. +The plugin loader internally retains a pointer to all plugins that it has created. +Therefore, the plugin loader must be kept in scope for the lifetime of any plugin that it creates (or any object that the plugin creates). + Here is an example of what **not** to do: ```c++ -boost_plugin_loader::PluginLoader plugin_loader; - Shape::Ptr shape; + { + boost_plugin_loader::PluginLoader plugin_loader; + ShapeFactory::Ptr square_factory = plugin_loader.createInstance("Square"); shape = square_factory.create(2.0); - // Library providing "Square" plugin is unloaded here when `square_factory` goes out of scope + // The library providing the definition of the `square_factory` plugin and `shape` objects is unloaded here when `plugin_looader` goes out of scope } -std::cout << "Square area: " << shape->area() << std::endl; // <-- segfault because the library providing plugin factory (and the object generated by it) was unloaded +std::cout << "Square area: " << shape->area() << std::endl; // <-- segfault because the library providing plugin factory (and the object generated by it) was unloaded when `plugin_loader` went out of scope ``` diff --git a/include/boost_plugin_loader/plugin_loader.h b/include/boost_plugin_loader/plugin_loader.h index e017972..5353d11 100644 --- a/include/boost_plugin_loader/plugin_loader.h +++ b/include/boost_plugin_loader/plugin_loader.h @@ -176,6 +176,11 @@ class PluginLoader template typename std::enable_if::value, bool>::type hasSymbol(const boost::dll::shared_library& lib, const std::string& symbol_name) const; + + /** + * @brief Container for loaded plugins to keep them in scope as long as the plugin loader is alive + */ + mutable std::vector> loaded_plugins_; }; } // namespace boost_plugin_loader diff --git a/include/boost_plugin_loader/plugin_loader.hpp b/include/boost_plugin_loader/plugin_loader.hpp index 8421979..2c6a916 100644 --- a/include/boost_plugin_loader/plugin_loader.hpp +++ b/include/boost_plugin_loader/plugin_loader.hpp @@ -217,7 +217,14 @@ std::shared_ptr PluginLoader::createInstance(const std::string& plug for (const auto& lib : libraries) { if (hasSymbol(lib, plugin_name)) - return createSharedInstance(lib, plugin_name); + { + auto plugin = createSharedInstance(lib, plugin_name); + + // Add the plugin to the internal cache to keep it in scope for the lifetime of the plugin loader + loaded_plugins_.push_back(std::dynamic_pointer_cast(plugin)); + + return plugin; + } } std::stringstream msg; From fe165bfeaf7118fc7151ae76b56be893db2e7458 Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 4 Aug 2025 14:17:53 -0500 Subject: [PATCH 2/5] Updated example plugins to use compile definition for section --- examples/CMakeLists.txt | 6 ++++++ examples/example.cpp | 7 ++++--- examples/plugin.cpp | 8 ++++++-- examples/printer/printer.h | 2 +- examples/shape/shape.h | 2 +- 5 files changed, 18 insertions(+), 7 deletions(-) diff --git a/examples/CMakeLists.txt b/examples/CMakeLists.txt index d72bf69..3a4453b 100644 --- a/examples/CMakeLists.txt +++ b/examples/CMakeLists.txt @@ -1,6 +1,7 @@ # Example plugin definition and explict template instantiation of plugin loader for test plugin class add_library(${PROJECT_NAME}_example_plugin plugin.cpp) target_link_libraries(${PROJECT_NAME}_example_plugin PUBLIC ${PROJECT_NAME}) +target_compile_definitions(${PROJECT_NAME}_example_plugin PUBLIC SECTION_PRINTER=printer SECTION_SHAPE=shape) target_cxx_version(${PROJECT_NAME}_example_plugin PUBLIC VERSION 17) target_clang_tidy(${PROJECT_NAME}_example_plugin ENABLE ${ENABLE_CLANG_TIDY}) @@ -15,6 +16,11 @@ target_link_libraries(${PROJECT_NAME}_example_plugin_impl PUBLIC ${PROJECT_NAME} target_cxx_version(${PROJECT_NAME}_example_plugin_impl PUBLIC VERSION 17) target_clang_tidy(${PROJECT_NAME}_example_plugin_impl ENABLE ${ENABLE_CLANG_TIDY}) +# Add a target compile definition to the plugin base library denoting the name of the library in which the plugins are +# implemented +target_compile_definitions(${PROJECT_NAME}_example_plugin + PUBLIC PLUGIN_LIBRARY_EXAMPLES="${PROJECT_NAME}_example_plugin_impl") + # Example add_executable(${PROJECT_NAME}_example example.cpp) target_link_libraries(${PROJECT_NAME}_example PRIVATE ${PROJECT_NAME}_example_plugin) diff --git a/examples/example.cpp b/examples/example.cpp index bcb373b..2f90e30 100644 --- a/examples/example.cpp +++ b/examples/example.cpp @@ -48,7 +48,8 @@ void demoPrinterPlugins(const PluginLoader& loader) const Printer::Ptr plugin = loader.createInstance(plugin_name); assert(plugin != nullptr); plugin->operator()(); - // Note: `plugin` goes out of scope here, and the library providing its definition will be unloaded + // Note: `plugin` goes out of scope here. However, the plugin loader maintains a reference to it, so the library + // providing its definition will not be unload until the plugin loader goes out of scope. } } @@ -60,8 +61,8 @@ void demoShapePlugins(const PluginLoader& loader) assert(shape_plugins.size() == 2); // Create the square and triangle factory plugins - // Note: these factories must stay in scope as long as they and any object they create are being used. Otherwise, the - // library providing them will be unloaded, resulting in undefined behavior and potential segfaults + // Note: the plugin loader must stay in scope as long as the plugins and any object the plugins create are being used. + // Otherwise, the library providing them will be unloaded, resulting in undefined behavior and potential segfaults auto square_factory = loader.createInstance("Square"); auto triangle_factory = loader.createInstance("Triangle"); diff --git a/examples/plugin.cpp b/examples/plugin.cpp index c554ab5..9eb7bba 100644 --- a/examples/plugin.cpp +++ b/examples/plugin.cpp @@ -26,13 +26,17 @@ #include #include // NOLINT(misc-include-cleaner) +// Macros for converting a non-string target compile definition into a string +#define STRINGIFY_HELPER(x) #x +#define STRINGIFY(x) STRINGIFY_HELPER(x) + namespace boost_plugin_loader { // Define the section name for loading plugins of base class `Printer` // This should match the section name specified in the plugin export macro for this class std::string Printer::getSection() { - return "printer"; + return STRINGIFY(SECTION_PRINTER); } INSTANTIATE_PLUGIN_LOADER(Printer) @@ -40,7 +44,7 @@ INSTANTIATE_PLUGIN_LOADER(Printer) // This should match the section name specified in the plugin export macro for this class std::string ShapeFactory::getSection() { - return "shape"; + return STRINGIFY(SECTION_SHAPE); } INSTANTIATE_PLUGIN_LOADER(ShapeFactory) diff --git a/examples/printer/printer.h b/examples/printer/printer.h index 6c73585..97a2852 100644 --- a/examples/printer/printer.h +++ b/examples/printer/printer.h @@ -47,4 +47,4 @@ class Printer #include // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -#define EXPORT_PRINTER_PLUGIN(DERIVED_CLASS, ALIAS) EXPORT_CLASS_SECTIONED(DERIVED_CLASS, ALIAS, printer) +#define EXPORT_PRINTER_PLUGIN(DERIVED_CLASS, ALIAS) EXPORT_CLASS_SECTIONED(DERIVED_CLASS, ALIAS, SECTION_PRINTER) diff --git a/examples/shape/shape.h b/examples/shape/shape.h index 7d19393..6dfcc72 100644 --- a/examples/shape/shape.h +++ b/examples/shape/shape.h @@ -68,4 +68,4 @@ class ShapeFactory #include // NOLINTNEXTLINE(cppcoreguidelines-avoid-non-const-global-variables) -#define EXPORT_SHAPE_PLUGIN(DERIVED_CLASS, ALIAS) EXPORT_CLASS_SECTIONED(DERIVED_CLASS, ALIAS, shape) +#define EXPORT_SHAPE_PLUGIN(DERIVED_CLASS, ALIAS) EXPORT_CLASS_SECTIONED(DERIVED_CLASS, ALIAS, SECTION_SHAPE) From 3bb13412c5836914dd6aceddd16214bbdfe1322c Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 4 Aug 2025 14:20:19 -0500 Subject: [PATCH 3/5] Updated unit test compile definition names --- test/CMakeLists.txt | 4 ++-- test/plugin_loader_unit.cpp | 25 +++++++++++++------------ 2 files changed, 15 insertions(+), 14 deletions(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 73de109..3b1ad30 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -27,8 +27,8 @@ target_link_libraries( target_compile_definitions(${PROJECT_NAME}_plugin_loader_unit PRIVATE ${COMPILE_DEFINITIONS}) target_compile_definitions( ${PROJECT_NAME}_plugin_loader_unit - PRIVATE PLUGIN_DIR="${CMAKE_CURRENT_BINARY_DIR}" PLUGINS_MULTIPLY="${PROJECT_NAME}_test_plugin_multiply" - PLUGINS_ADD="${PROJECT_NAME}_test_plugin_add") + PRIVATE PLUGIN_DIR="${CMAKE_CURRENT_BINARY_DIR}" PLUGIN_LIBRARY_MULTIPLY="${PROJECT_NAME}_test_plugin_multiply" + PLUGIN_LIBRARY_ADD="${PROJECT_NAME}_test_plugin_add") target_clang_tidy(${PROJECT_NAME}_plugin_loader_unit ENABLE ${ENABLE_CLANG_TIDY}) target_cxx_version(${PROJECT_NAME}_plugin_loader_unit PUBLIC VERSION 17) target_code_coverage( diff --git a/test/plugin_loader_unit.cpp b/test/plugin_loader_unit.cpp index d738c2a..d7425c7 100644 --- a/test/plugin_loader_unit.cpp +++ b/test/plugin_loader_unit.cpp @@ -41,7 +41,7 @@ TEST(BoostPluginLoaderUnit, Utils) // NOLINT { using namespace boost_plugin_loader; - const std::string lib_name = std::string(PLUGINS_MULTIPLY); + const std::string lib_name = std::string(PLUGIN_LIBRARY_MULTIPLY); const std::string lib_dir = std::string(PLUGIN_DIR); { @@ -172,7 +172,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT { PluginLoader plugin_loader; plugin_loader.search_paths.insert(std::string(PLUGIN_DIR)); - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); EXPECT_TRUE(plugin_loader.isPluginAvailable(getSymbolName())); auto plugin = plugin_loader.createInstance(getSymbolName()); @@ -197,7 +197,8 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT { // Use full path PluginLoader plugin_loader; - const std::string full_path = boost_plugin_loader::decorate(std::string(PLUGINS_MULTIPLY), std::string(PLUGIN_DIR)); + const std::string full_path = + boost_plugin_loader::decorate(std::string(PLUGIN_LIBRARY_MULTIPLY), std::string(PLUGIN_DIR)); plugin_loader.search_libraries.insert(full_path); EXPECT_TRUE(plugin_loader.isPluginAvailable(getSymbolName())); @@ -227,7 +228,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT PluginLoader plugin_loader; EXPECT_EQ(plugin_loader.count(), 0); EXPECT_TRUE(plugin_loader.empty()); - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); EXPECT_EQ(plugin_loader.count(), 1); EXPECT_FALSE(plugin_loader.empty()); @@ -243,7 +244,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT PluginLoader plugin_loader; plugin_loader.search_system_folders = false; plugin_loader.search_paths.insert("does_not_exist"); - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); EXPECT_FALSE(plugin_loader.isPluginAvailable(getSymbolName())); // Behavior change: used to return nullptr but now throws exception @@ -255,7 +256,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT { PluginLoader plugin_loader; plugin_loader.search_system_folders = false; - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); { EXPECT_FALSE(plugin_loader.isPluginAvailable("does_not_exist")); @@ -299,7 +300,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT { PluginLoader plugin_loader; plugin_loader.search_system_folders = false; - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) const std::vector plugins = plugin_loader.getAvailablePlugins(TestPluginMultiply::getSection()); @@ -309,7 +310,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPlugin) // NOLINT { PluginLoader plugin_loader; plugin_loader.search_system_folders = true; - plugin_loader.search_libraries.insert(std::string(PLUGINS_MULTIPLY)); + plugin_loader.search_libraries.insert(std::string(PLUGIN_LIBRARY_MULTIPLY)); const std::vector plugins = plugin_loader.getAvailablePlugins(TestPluginMultiply::getSection()); EXPECT_EQ(plugins.size(), 1); @@ -362,7 +363,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPluginsSameSymbolDifferentSections) // NOLI // Try to load an instance of `TestPluginAddImpl` from the library in which `TestPluginMultiplyImpl` was defined { PluginLoader plugin_loader; - plugin_loader.search_libraries.insert(PLUGINS_MULTIPLY); + plugin_loader.search_libraries.insert(PLUGIN_LIBRARY_MULTIPLY); plugin_loader.search_paths.insert(PLUGIN_DIR); // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) @@ -372,7 +373,7 @@ TEST(BoostPluginLoaderUnit, LoadTestPluginsSameSymbolDifferentSections) // NOLI // Try to load an instance of `TestPluginMultiplyImpl` from the library in which `TestPluginAddImpl` was defined { PluginLoader plugin_loader; - plugin_loader.search_libraries.insert(PLUGINS_ADD); + plugin_loader.search_libraries.insert(PLUGIN_LIBRARY_ADD); plugin_loader.search_paths.insert(PLUGIN_DIR); // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) @@ -382,8 +383,8 @@ TEST(BoostPluginLoaderUnit, LoadTestPluginsSameSymbolDifferentSections) // NOLI // Given both libraries, correctly load and use each plugin, even though they share the same symbol name { PluginLoader plugin_loader; - plugin_loader.search_libraries.insert(PLUGINS_ADD); - plugin_loader.search_libraries.insert(PLUGINS_MULTIPLY); + plugin_loader.search_libraries.insert(PLUGIN_LIBRARY_ADD); + plugin_loader.search_libraries.insert(PLUGIN_LIBRARY_MULTIPLY); plugin_loader.search_paths.insert(PLUGIN_DIR); // Load and use a multiply plugin From 02fe44abe569483a8dc232b3af5d2d7a99f239af Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 4 Aug 2025 14:50:03 -0500 Subject: [PATCH 4/5] Added unit test to test plugin scope --- test/CMakeLists.txt | 3 ++- test/plugin_loader_unit.cpp | 36 ++++++++++++++++++++++++++++++++++++ 2 files changed, 38 insertions(+), 1 deletion(-) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index 3b1ad30..6d59531 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -23,7 +23,8 @@ target_link_libraries( PRIVATE GTest::GTest GTest::Main ${PROJECT_NAME} - ${PROJECT_NAME}_test_plugin) + ${PROJECT_NAME}_test_plugin + ${PROJECT_NAME}_example_plugin) target_compile_definitions(${PROJECT_NAME}_plugin_loader_unit PRIVATE ${COMPILE_DEFINITIONS}) target_compile_definitions( ${PROJECT_NAME}_plugin_loader_unit diff --git a/test/plugin_loader_unit.cpp b/test/plugin_loader_unit.cpp index d7425c7..bfdd2a2 100644 --- a/test/plugin_loader_unit.cpp +++ b/test/plugin_loader_unit.cpp @@ -37,6 +37,7 @@ #include #include #include "test_plugin.h" +#include "../examples/shape/shape.h" TEST(BoostPluginLoaderUnit, Utils) // NOLINT { @@ -407,6 +408,41 @@ TEST(BoostPluginLoaderUnit, LoadTestPluginsSameSymbolDifferentSections) // NOLI } } +TEST(BoostPluginLoaderUnit, TestPluginLoaderScope) // NOLINT +{ + using boost_plugin_loader::PluginLoader; + using boost_plugin_loader::Shape; + using boost_plugin_loader::ShapeFactory; + + // Create a plugin loader + PluginLoader plugin_loader; + + // Configure the plugin loader to be able to find the shape plugins from the example subdirectory + plugin_loader.search_libraries.insert(PLUGIN_LIBRARY_EXAMPLES); + plugin_loader.search_paths.insert(PLUGIN_DIR); + + // Create an instance of a triangle shape using a plugin that goes out of scope + Shape::Ptr triangle; + { + ShapeFactory::Ptr triangle_factory; + // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) + ASSERT_NO_THROW(triangle_factory = plugin_loader.createInstance("Triangle")); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) + ASSERT_NO_THROW(triangle = triangle_factory->create(std::make_tuple(2.0, 4.0))); + + // The plugin factory goes out of scope here, but the plugin loader maintains a pointer to it internally, keeping + // the plugin library loaded + } + + // Use an object created by a plugin whose local instance has gone out of scope but is still maintained in-scope by + // the plugin loader + // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) + double area; + EXPECT_NO_THROW(area = triangle->area()); + // NOLINTNEXTLINE(cppcoreguidelines-avoid-goto) + EXPECT_NEAR(area, 4.0, 1.0e-6); +} + int main(int argc, char** argv) { testing::InitGoogleTest(&argc, argv); From 544333e80f8ca9920222d45073df0f17675a7e4b Mon Sep 17 00:00:00 2001 From: Michael Ripperger Date: Mon, 15 Sep 2025 13:29:39 -0500 Subject: [PATCH 5/5] Added mutex to make access to plugin cache threadsafe --- include/boost_plugin_loader/plugin_loader.h | 2 ++ include/boost_plugin_loader/plugin_loader.hpp | 1 + 2 files changed, 3 insertions(+) diff --git a/include/boost_plugin_loader/plugin_loader.h b/include/boost_plugin_loader/plugin_loader.h index 5353d11..5957be8 100644 --- a/include/boost_plugin_loader/plugin_loader.h +++ b/include/boost_plugin_loader/plugin_loader.h @@ -24,6 +24,7 @@ #include #include #include +#include /** @brief Macro for explicitly template instantiating a plugin loader for a given base class */ #define INSTANTIATE_PLUGIN_LOADER(PluginBase) \ @@ -181,6 +182,7 @@ class PluginLoader * @brief Container for loaded plugins to keep them in scope as long as the plugin loader is alive */ mutable std::vector> loaded_plugins_; + mutable std::mutex plugin_cache_mutex_; }; } // namespace boost_plugin_loader diff --git a/include/boost_plugin_loader/plugin_loader.hpp b/include/boost_plugin_loader/plugin_loader.hpp index 2c6a916..9fb33a1 100644 --- a/include/boost_plugin_loader/plugin_loader.hpp +++ b/include/boost_plugin_loader/plugin_loader.hpp @@ -221,6 +221,7 @@ std::shared_ptr PluginLoader::createInstance(const std::string& plug auto plugin = createSharedInstance(lib, plugin_name); // Add the plugin to the internal cache to keep it in scope for the lifetime of the plugin loader + std::lock_guard lock{ plugin_cache_mutex_ }; loaded_plugins_.push_back(std::dynamic_pointer_cast(plugin)); return plugin;