Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add option to use RTLD_NODELETE when loading a library #102

Merged
merged 6 commits into from
Aug 31, 2022
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions loader/include/gz/plugin/Loader.hh
Original file line number Diff line number Diff line change
Expand Up @@ -133,6 +133,18 @@ namespace gz
public: std::unordered_set<std::string> LoadLib(
const std::string &_pathToLibrary);


chapulina marked this conversation as resolved.
Show resolved Hide resolved
/// \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<std::string> LoadLib(
const std::string &_pathToLibrary, bool _noDelete);
chapulina marked this conversation as resolved.
Show resolved Hide resolved

/// \brief Instantiates a plugin for the given plugin name
///
/// \param[in] _pluginNameOrAlias
Expand Down
21 changes: 17 additions & 4 deletions loader/src/Loader.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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<void> 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.
Expand Down Expand Up @@ -139,12 +139,18 @@ namespace gz
/////////////////////////////////////////////////
std::unordered_set<std::string> Loader::LoadLib(
const std::string &_pathToLibrary)
{
return this->LoadLib(_pathToLibrary, false);
}
/////////////////////////////////////////////////
std::unordered_set<std::string> Loader::LoadLib(
const std::string &_pathToLibrary, bool _noDelete)
{
std::unordered_set<std::string> newPlugins;

// Attempt to load the library at this path
const std::shared_ptr<void> &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.
Expand Down Expand Up @@ -420,7 +426,7 @@ namespace gz

/////////////////////////////////////////////////
std::shared_ptr<void> Loader::Implementation::LoadLib(
const std::string &_full_path)
const std::string &_full_path, bool _noDelete)
{
std::shared_ptr<void> dlHandlePtr;

Expand All @@ -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);
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

we might need an ifdef for RTLD_NODELETE on windows

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Done in 644f613

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Apparently, the behavior of dlopen is different between Linux and macOS. On Linux, if you dlopen (with RTLD_NODELETE) , dlclose, and dlopen again, since the plugin is not deleted, dlopen will reuse the existing plugin. On macOS, however, even though the plugin is not deleted, dlopen will treat the second dlopen as though a new library is being loaded and end up loading it into a new memory location. Thus, the type of test I was using won't work. Since we're running out of time, I've decided to enable the test only on Linux. If there are other ways to test this, I'm open to ideas.

Should we add __APPLE__ to the ifdef?

#endif
void *dlHandle = dlopen(_full_path.c_str(), dlopenMode);

const char *loadError = dlerror();
if (nullptr == dlHandle || nullptr != loadError)
Expand Down
5 changes: 4 additions & 1 deletion test/integration/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,8 @@ foreach(test ${test_targets})
GzBadPluginSize
GzDummyPlugins
GzFactoryPlugins
GzTemplatedPlugins)
GzTemplatedPlugins
GzInstanceCounter)

target_compile_definitions(${test} PRIVATE
"${plugin_target}_LIB=\"$<TARGET_FILE:${plugin_target}>\"")
Expand All @@ -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})
Expand Down
63 changes: 63 additions & 0 deletions test/integration/plugin_unload.hh
Original file line number Diff line number Diff line change
@@ -0,0 +1,63 @@

chapulina marked this conversation as resolved.
Show resolved Hide resolved
/*
* 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 <gtest/gtest.h>

#include <iostream>
#include <string>
#include <unordered_set>

#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
chapulina marked this conversation as resolved.
Show resolved Hide resolved
gz::plugin::PluginPtr LoadInstanceCounter(bool _nodelete)
{
gz::plugin::Loader pl;

std::unordered_set<std::string> 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<test::util::InstanceCounterBase>();
ASSERT_NE(nullptr, instanceCounter);

EXPECT_EQ(_numExpectedInstances, instanceCounter->Instances());
}

#endif
30 changes: 30 additions & 0 deletions test/integration/plugin_unload_with_nodelete.cc
Original file line number Diff line number Diff line change
@@ -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 <gz/utils/ExtraTestMacros.hh>

#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);
}
30 changes: 30 additions & 0 deletions test/integration/plugin_unload_without_nodelete.cc
Original file line number Diff line number Diff line change
@@ -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 <gz/utils/ExtraTestMacros.hh>

#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);
}
3 changes: 3 additions & 0 deletions test/plugins/CMakeLists.txt
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand All @@ -23,6 +25,7 @@ foreach(plugin_target
GzDummyPlugins
GzFactoryPlugins
GzTemplatedPlugins
GzInstanceCounter
GzDummyStaticPlugin)

target_link_libraries(${plugin_target} PRIVATE
Expand Down
59 changes: 59 additions & 0 deletions test/plugins/InstanceCounter.cc
Original file line number Diff line number Diff line change
@@ -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 <gz/plugin/Register.hh>

#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
36 changes: 36 additions & 0 deletions test/plugins/InstanceCounter.hh
Original file line number Diff line number Diff line change
@@ -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