diff --git a/loader/include/gz/plugin/Loader.hh b/loader/include/gz/plugin/Loader.hh index 4be3be9a..56d79c19 100644 --- a/loader/include/gz/plugin/Loader.hh +++ b/loader/include/gz/plugin/Loader.hh @@ -133,6 +133,17 @@ namespace gz public: std::unordered_set LoadLib( const std::string &_pathToLibrary); + /// \brief Load a library at the given path + /// + /// \param[in] _pathToLibrary + /// The path to a library + /// \param[in] _noDelete + /// If true, RTLD_NODELETE will be used when loading the library. + /// + /// \returns The set of plugins that have been loaded from the library + public: std::unordered_set LoadLib( + const std::string &_pathToLibrary, bool _noDelete); + /// \brief Instantiates a plugin for the given plugin name /// /// \param[in] _pluginNameOrAlias diff --git a/loader/src/Loader.cc b/loader/src/Loader.cc index e35b80e3..9296d454 100644 --- a/loader/src/Loader.cc +++ b/loader/src/Loader.cc @@ -46,7 +46,7 @@ namespace gz /// \return If a library exists at the given path, get a point to its dl /// handle. If the library does not exist, get a nullptr. public: std::shared_ptr LoadLib( - const std::string &_pathToLibrary); + const std::string &_pathToLibrary, bool _noDelete); /// \brief Using a dl handle produced by LoadLib, extract the /// Info from the loaded library. @@ -139,12 +139,18 @@ namespace gz ///////////////////////////////////////////////// std::unordered_set Loader::LoadLib( const std::string &_pathToLibrary) + { + return this->LoadLib(_pathToLibrary, false); + } + ///////////////////////////////////////////////// + std::unordered_set Loader::LoadLib( + const std::string &_pathToLibrary, bool _noDelete) { std::unordered_set newPlugins; // Attempt to load the library at this path const std::shared_ptr &dlHandle = - this->dataPtr->LoadLib(_pathToLibrary); + this->dataPtr->LoadLib(_pathToLibrary, _noDelete); // Quit early and return an empty set of plugin names if we did not // actually get a valid dlHandle. @@ -420,7 +426,7 @@ namespace gz ///////////////////////////////////////////////// std::shared_ptr Loader::Implementation::LoadLib( - const std::string &_full_path) + const std::string &_full_path, bool _noDelete) { std::shared_ptr dlHandlePtr; @@ -431,7 +437,14 @@ namespace gz // NOTE: We open using RTLD_LOCAL instead of RTLD_GLOBAL to prevent the // symbols of different libraries from writing over each other. - void *dlHandle = dlopen(_full_path.c_str(), RTLD_LAZY | RTLD_LOCAL); +#ifdef _WIN32 + // RTLD_NODELETE is not defined in dlfcn-32. + (void) _noDelete; + int dlopenMode = RTLD_LAZY | RTLD_LOCAL; +#else + int dlopenMode = RTLD_LAZY | RTLD_LOCAL | (_noDelete ? RTLD_NODELETE : 0); +#endif + void *dlHandle = dlopen(_full_path.c_str(), dlopenMode); const char *loadError = dlerror(); if (nullptr == dlHandle || nullptr != loadError) diff --git a/test/integration/CMakeLists.txt b/test/integration/CMakeLists.txt index cea2931c..881d2f13 100644 --- a/test/integration/CMakeLists.txt +++ b/test/integration/CMakeLists.txt @@ -20,7 +20,8 @@ foreach(test ${test_targets}) GzBadPluginSize GzDummyPlugins GzFactoryPlugins - GzTemplatedPlugins) + GzTemplatedPlugins + GzInstanceCounter) target_compile_definitions(${test} PRIVATE "${plugin_target}_LIB=\"$\"") @@ -33,6 +34,8 @@ foreach(test INTEGRATION_EnablePluginFromThis_TEST INTEGRATION_factory INTEGRATION_plugin + INTEGRATION_plugin_unload_with_nodelete + INTEGRATION_plugin_unload_without_nodelete INTEGRATION_WeakPluginPtr) if(TARGET ${test}) diff --git a/test/integration/plugin_unload.hh b/test/integration/plugin_unload.hh new file mode 100644 index 00000000..4c198c6c --- /dev/null +++ b/test/integration/plugin_unload.hh @@ -0,0 +1,63 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef GZ_PLUGIN_TEST_INTEGRATION_PLUGIN_UNLOAD_HH +#define GZ_PLUGIN_TEST_INTEGRATION_PLUGIN_UNLOAD_HH + +#include + +#include +#include +#include + +#include "../plugins/InstanceCounter.hh" +#include "gz/plugin/Loader.hh" +#include "gz/plugin/PluginPtr.hh" +#include "gz/plugin/SpecializedPluginPtr.hh" + +///////////////////////////////////////////////// +/// \brief Load the InstanceCounter plugin +/// \param[in] _nodelete True if RTLD_NODELETE should be used when loading the +/// \return Pointer to the plugin +gz::plugin::PluginPtr LoadInstanceCounter(bool _nodelete) +{ + gz::plugin::Loader pl; + + std::unordered_set pluginNames = + pl.LoadLib(GzInstanceCounter_LIB, _nodelete); + + return pl.Instantiate("test::util::InstanceCounter"); +} + +///////////////////////////////////////////////// +/// \brief Load plugin, Instantiate the InstanceCounter, check the number of +/// instances, and finally unload the plugin. Note, the plugin is unloaded when +/// `instanceCounterPlugin` goes out of scope. +/// \param[in] _nodelete True if RTLD_NODELETE should be used when loading the +/// library. +/// \param[in] _numExpectedInstances Expected number of instances of the plugin. +void LoadAndTestInstanceCounter(bool _nodelete, int _numExpectedInstances) +{ + gz::plugin::PluginPtr instanceCounterPlugin = LoadInstanceCounter(_nodelete); + test::util::InstanceCounterBase *instanceCounter = + instanceCounterPlugin->QueryInterface(); + ASSERT_NE(nullptr, instanceCounter); + + EXPECT_EQ(_numExpectedInstances, instanceCounter->Instances()); +} + +#endif diff --git a/test/integration/plugin_unload_with_nodelete.cc b/test/integration/plugin_unload_with_nodelete.cc new file mode 100644 index 00000000..e1f02ea4 --- /dev/null +++ b/test/integration/plugin_unload_with_nodelete.cc @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include "plugin_unload.hh" + +///////////////////////////////////////////////// +TEST(PluginsWithNoDelete, + GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(AreNotDeletedOnUnload)) +{ + LoadAndTestInstanceCounter(true, 1); + // Since the plugin is not deleted on unload, the second time we load the + // plugin, the instance count is incremenetd. + LoadAndTestInstanceCounter(true, 2); +} diff --git a/test/integration/plugin_unload_without_nodelete.cc b/test/integration/plugin_unload_without_nodelete.cc new file mode 100644 index 00000000..60c36bbc --- /dev/null +++ b/test/integration/plugin_unload_without_nodelete.cc @@ -0,0 +1,30 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include "plugin_unload.hh" + +///////////////////////////////////////////////// +TEST(PluginsWithoutNoDelete, + GZ_UTILS_TEST_ENABLED_ONLY_ON_LINUX(AreDeletedOnUnload)) +{ + LoadAndTestInstanceCounter(false, 1); + // Unlike the test in *with* nodelete, the number of instances will remain 1 + // after loading the plugin a second time. + LoadAndTestInstanceCounter(false, 1); +} diff --git a/test/plugins/CMakeLists.txt b/test/plugins/CMakeLists.txt index df8e4792..43604b6d 100644 --- a/test/plugins/CMakeLists.txt +++ b/test/plugins/CMakeLists.txt @@ -10,6 +10,8 @@ add_library(GzDummyPlugins SHARED DummyPlugins.cc DummyPluginsOtherTranslationUnit.cc) +add_library(GzInstanceCounter SHARED InstanceCounter.cc) + add_library(GzDummyStaticPlugin STATIC DummyStaticPlugin.cc) # Create a variable for the name of the header which will contain the dummy plugin path. @@ -23,6 +25,7 @@ foreach(plugin_target GzDummyPlugins GzFactoryPlugins GzTemplatedPlugins + GzInstanceCounter GzDummyStaticPlugin) target_link_libraries(${plugin_target} PRIVATE diff --git a/test/plugins/InstanceCounter.cc b/test/plugins/InstanceCounter.cc new file mode 100644 index 00000000..516c3329 --- /dev/null +++ b/test/plugins/InstanceCounter.cc @@ -0,0 +1,59 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#include + +#include "InstanceCounter.hh" + +namespace test +{ +namespace util +{ + +namespace { + +///////////////////////////////////////////////// +int numInstancesImpl() { + // Use a static variable that never gets deleted as a way to demonstrate + // that plugins with RTLD_NODELETE are not deleted when unloaded. + static int *instances = new int(0); + ++(*instances); + return *instances; +} +} + +/// \brief A class that counts the number of times a plugin has been +/// instantiated. +class InstanceCounter : public InstanceCounterBase +{ + public: InstanceCounter() : instances(numInstancesImpl()) + { + } + + public: ~InstanceCounter() override = default; + + public: int Instances() override + { + return this->instances; + } + + private: int instances; +}; + +GZ_ADD_PLUGIN(InstanceCounter, InstanceCounterBase) +} // namespace util +} // namespace test diff --git a/test/plugins/InstanceCounter.hh b/test/plugins/InstanceCounter.hh new file mode 100644 index 00000000..c58415e9 --- /dev/null +++ b/test/plugins/InstanceCounter.hh @@ -0,0 +1,36 @@ +/* + * Copyright (C) 2022 Open Source Robotics Foundation + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + * + */ + +#ifndef GZ_PLUGIN_TEST_PLUGINS_INSTANCECOUNTER_HH_ +#define GZ_PLUGIN_TEST_PLUGINS_INSTANCECOUNTER_HH_ + +namespace test +{ +namespace util +{ +/// \brief Get the instance count of this plugin, that's the number of times +/// this plugin has been instantiated in this process. +class InstanceCounterBase +{ + public: virtual ~InstanceCounterBase() = default; + + public: virtual int Instances() = 0; +}; + +} // namespace util +} // namespace test +#endif