# HG changeset patch # User David Demelier # Date 1464791180 -7200 # Node ID 78f296a7b2e5820f8d30c2baaca7e9bfd130b1a5 # Parent 00f1789a49fa53cb4731d8f36c3ace2a3707798b Dynlib: resurrection diff -r 00f1789a49fa -r 78f296a7b2e5 CMakeLists.txt --- a/CMakeLists.txt Wed Jun 01 16:19:49 2016 +0200 +++ b/CMakeLists.txt Wed Jun 01 16:26:20 2016 +0200 @@ -43,4 +43,5 @@ endif () add_subdirectory(modules/base64) +add_subdirectory(modules/dynlib) add_subdirectory(modules/options) diff -r 00f1789a49fa -r 78f296a7b2e5 modules/dynlib/CMakeLists.txt --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/dynlib/CMakeLists.txt Wed Jun 01 16:26:20 2016 +0200 @@ -0,0 +1,28 @@ +# +# CMakeLists.txt -- code building for common code +# +# Copyright (c) 2013-2016 David Demelier +# +# Permission to use, copy, modify, and/or distribute this software for any +# purpose with or without fee is hereby granted, provided that the above +# copyright notice and this permission notice appear in all copies. +# +# THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES +# WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF +# MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR +# ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES +# WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN +# ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF +# OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. +# + +add_library(dynlib-plugin MODULE ${CMAKE_CURRENT_SOURCE_DIR}/test/plugin.cpp) +target_include_directories(dynlib-plugin PRIVATE ${CMAKE_CURRENT_SOURCE_DIR}) + +code_define_module( + NAME dynlib + SOURCES dynlib.hpp + FLAGS PLUGIN=\"$\" +) + +add_dependencies(test-dynlib dynlib-plugin) \ No newline at end of file diff -r 00f1789a49fa -r 78f296a7b2e5 modules/dynlib/doc/mainpage.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/dynlib/doc/mainpage.cpp Wed Jun 01 16:26:20 2016 +0200 @@ -0,0 +1,63 @@ +/** + * \mainpage + * + * Welcome to the dynlib library. + * + * ## Introduction + * + * Opening shared libraries from C++ (e.g for plugins) is platform specific, with that class, you can open them very + * easily. + * + * ## Installation + * + * Just copy the file dynlib.hpp and add it to your project. + * + * ## Overview + * + * This one minute tutorial shows how to load a shared library named `plugin.so` and execute a C function `say_hello`. + * + * ### Main + * + * This is the main executable that loads the `plugin.so` file. + * + * ````cpp + * #include + * + * #include "dynlib.hpp" + * + * using HelloFunc = void (*)(const std::string &); + * + * int main() + * { + * try { + * Dynlib library("./plugin.so"); + * + * HelloFunc hello = library.sym("say_hello"); + * hello("Test"); + * } catch (const std::exception &error) { + * std::cerr << error.what() << std::endl; + * } + * + * return 0; + * } + * ```` + * + * ### Plugin + * + * This is the plugin that you compile as a shared library. + * + * For example with gcc: `gcc -fPIC -shared plugin.cpp -o plugin.so`. + * + * ````cpp + * #include + * + * #include "dynlib.hpp" + * + * extern "C" { + * + * DYNLIB_EXPORT void say_hello() + * { + * std::cout << "Hello from the plugin!" << std::endl; + * } + * ```` + */ diff -r 00f1789a49fa -r 78f296a7b2e5 modules/dynlib/dynlib.hpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/dynlib/dynlib.hpp Wed Jun 01 16:26:20 2016 +0200 @@ -0,0 +1,334 @@ +/* + * dynlib.hpp -- portable shared library loader + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#ifndef DYNLIB_HPP +#define DYNLIB_HPP + +/** + * \file dynlib.hpp + * \brief Portable shared library loader. + * \author David Demelier + */ + +/** + * \page Dynlib Dynlib + * \brief Portable shared library loader. + * + * The dynlib module let you open shared libraries dynamically at runtime. + * + * ## Operating system support + * + * | System | Support | Remarks | + * |---------|---------|--------------------| + * | Apple | Ok | | + * | FreeBSD | Ok | | + * | Linux | Ok | Needs -ldl library | + * | Windows | Ok | | + * + * ## How to export symbols + * + * When you want to dynamically load symbols from your shared library, make sure they are in a `extern "C"` block, if + * not they will be [mangled][name-mangling]. + * + * Note, this does not mean that you can't write C++ code, it just mean that you can't use namespaces and function + * overloading. + * + * Example of **plugin.cpp**: + * + * ````cpp + * #include + * + * #include "dynlib.hpp" + * + * extern "C" { + * + * DYNLIB_EXPORT void plugin_load() + * { + * std::cout << "Loading plugin" << std::endl; + * } + * + * DYNLIB_EXPORT void plugin_unload() + * { + * std::cout << "Unloading plugin" << std::endl; + * } + * + * } + * ```` + * + * The \ref DYNLIB_EXPORT macro is necessary on some platforms to be sure that symbol will be visible. Make sure you always + * add it before any function. + * + * To compile, see your compiler documentation or build system. For gcc you can use the following: + * + * ```` + * gcc -std=c++14 -shared plugin.cpp -o plugin.so + * ```` + * + * ## How to load the library + * + * The dynlib module will search for the library in various places, thus you can use relative paths names but be sure + * that the library can be found. Otherwise, just use an absolute path to the file. + * + * ````cpp + * #include + * + * #include "dynlib.hpp" + * + * int main() + * { + * try { + * Dynlib dso("./plugin" DYNLIB_SUFFIX); + * } catch (const std::exception &ex) { + * std::cerr << ex.what() << std::endl; + * } + * + * return 0; + * } + * ```` + * + * ## How to load symbol + * + * The last part is symbol loading, you muse use raw C function pointer and the Dynlib::sym function. + * + * ````cpp + * #include + * + * #include "dynlib.hpp" + * + * using PluginLoad = void (*)(); + * using PluginUnload = void (*)(); + * + * int main() + * { + * try { + * Dynlib dso("./plugin" DYNLIB_SUFFIX); + * + * dso.sym("plugin_load")(); + * dso.sym("plugin_unload")(); + * } catch (const std::exception &ex) { + * std::cerr << ex.what() << std::endl; + * } + * + * return 0; + * } + * ```` + * + * [name-mangling]: https://en.wikipedia.org/wiki/Name_mangling + */ + +#include +#include + +#if defined(_WIN32) +# include +#else +# include +#endif + +/** + * \brief Export the symbol. + * + * This is required on some platforms and you should put it before your function signature. + * + * \code{.cpp} + * extern "C" { + * + * DYNLIB_EXPORT void my_function() + * { + * } + * + * } + * \endcode + */ +#if defined(_WIN32) +# define DYNLIB_EXPORT __declspec(dllexport) +#else +# define DYNLIB_EXPORT +#endif + +/** + * \brief Usual suffix for the library. + * + * This macro expands to the suffix convention for this platform. + * + * \code{.cpp} + * Dynlib library("./myplugin" DYNLIB_SUFFIX); + * \endcode + * + * \note Don't use the suffix expanded value shown in Doxygen as it may be wrong. + */ +#if defined(_WIN32) +# define DYNLIB_SUFFIX ".dll" +#elif defined(__APPLE__) +# define DYNLIB_SUFFIX ".dylib" +#else +# define DYNLIB_SUFFIX ".so" +#endif + +/** + * \class Dynlib + * \brief Load a dynamic module. + * + * This class is a portable wrapper to load shared libraries on supported systems. + */ +class Dynlib { +private: +#if defined(_WIN32) + using Handle = HMODULE; + using Sym = FARPROC; +#else + using Handle = void *; + using Sym = void *; +#endif + +public: + /** + * \brief Policy for symbol resolution. + */ + enum Policy { + Immediately, //!< load symbols immediately + Lazy //!< load symbols when needed + }; + +private: + Handle m_handle; + + Dynlib(const Dynlib &) = delete; + Dynlib &operator=(const Dynlib &) = delete; + + Dynlib(Dynlib &&) = delete; + Dynlib &operator=(Dynlib &&) = delete; + +#if defined(_WIN32) + std::string error() + { + LPSTR error = nullptr; + std::string errmsg; + + FormatMessageA( + FORMAT_MESSAGE_ALLOCATE_BUFFER | FORMAT_MESSAGE_FROM_SYSTEM, + NULL, + GetLastError(), + MAKELANGID(LANG_NEUTRAL, SUBLANG_DEFAULT), + (LPSTR)&error, 0, NULL); + + if (error) { + errmsg = std::string(error); + LocalFree(error); + } + + return errmsg; + } +#endif + +public: + /** + * Constructor to load a shared module. + * + * \param path the absolute path + * \param policy the policy to load + * \throw std::runtime_error on error + */ + Dynlib(const std::string &path, Policy policy = Immediately); + + /** + * Close the library automatically. + */ + ~Dynlib(); + + /** + * Get a symbol from the library. + * + * On some platforms the symbol must be manually exported. + * + * \param name the symbol + * \return the symbol + * \throw std::runtime_error on error + * \see DYNLIB_EXPORT + */ + template + T sym(const std::string &name); +}; + +#if defined(_WIN32) + +/* + * Windows implementation + * ------------------------------------------------------------------ + */ + +inline Dynlib::Dynlib(const std::string &path, Policy) +{ + m_handle = LoadLibraryA(path.c_str()); + + if (m_handle == nullptr) + throw std::runtime_error(error()); +} + +inline Dynlib::~Dynlib() +{ + FreeLibrary(m_handle); + m_handle = nullptr; +} + +template +T Dynlib::sym(const std::string &name) +{ + Sym sym = GetProcAddress(m_handle, name.c_str()); + + if (sym == nullptr) + throw std::runtime_error(error()); + + return reinterpret_cast(sym); +} + +#else + +/* + * Unix implementation + * ------------------------------------------------------------------ + */ + +inline Dynlib::Dynlib(const std::string &path, Policy policy) +{ + m_handle = dlopen(path.c_str(), policy == Immediately ? RTLD_NOW : RTLD_LAZY); + + if (m_handle == nullptr) + throw std::runtime_error(dlerror()); +} + +inline Dynlib::~Dynlib() +{ + dlclose(m_handle); + m_handle = nullptr; +} + +template +T Dynlib::sym(const std::string &name) +{ + Sym sym = dlsym(m_handle, name.c_str()); + + if (sym == nullptr) + throw std::runtime_error(dlerror()); + + return reinterpret_cast(sym); +} + +#endif + +#endif // !DYNLIB_HPP diff -r 00f1789a49fa -r 78f296a7b2e5 modules/dynlib/test/main.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/dynlib/test/main.cpp Wed Jun 01 16:26:20 2016 +0200 @@ -0,0 +1,56 @@ +/* + * main.cpp -- test the dynamic library loader + * + * Copyright (c) 2013-2016 David Demelier + * + * Permission to use, copy, modify, and/or distribute this software for any + * purpose with or without fee is hereby granted, provided that the above + * copyright notice and this permission notice appear in all copies. + * + * THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES + * WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF + * MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR + * ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES + * WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN + * ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF + * OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE. + */ + +#include + +#include + +#include + +using Initialize = void (*)(std::string &s); + +TEST(Basic, initialize) +{ + try { + Dynlib library(PLUGIN); + Initialize init = library.sym("initialize"); + + std::string expected("Hello World"); + std::string result; + + init(result); + + ASSERT_EQ(expected, result); + } catch (const std::runtime_error &error) { + FAIL() << error.what(); + } +} + +TEST(Basic, absent) +{ + Dynlib library(PLUGIN); + + ASSERT_THROW(library.sym("initialize_typo"), std::runtime_error); +} + +int main(int argc, char **argv) +{ + testing::InitGoogleTest(&argc, argv); + + return RUN_ALL_TESTS(); +} diff -r 00f1789a49fa -r 78f296a7b2e5 modules/dynlib/test/plugin.cpp --- /dev/null Thu Jan 01 00:00:00 1970 +0000 +++ b/modules/dynlib/test/plugin.cpp Wed Jun 01 16:26:20 2016 +0200 @@ -0,0 +1,12 @@ +#include + +#include + +extern "C" { + +void DYNLIB_EXPORT initialize(std::string &result) +{ + result = "Hello World"; +} + +}