changeset 518:78f296a7b2e5

Dynlib: resurrection
author David Demelier <markand@malikania.fr>
date Wed, 01 Jun 2016 16:26:20 +0200
parents 00f1789a49fa
children d6dad57e9e6b
files CMakeLists.txt modules/dynlib/CMakeLists.txt modules/dynlib/doc/mainpage.cpp modules/dynlib/dynlib.hpp modules/dynlib/test/main.cpp modules/dynlib/test/plugin.cpp
diffstat 6 files changed, 494 insertions(+), 0 deletions(-) [+]
line wrap: on
line diff
--- 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)
--- /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 <markand@malikania.fr>
+#
+# 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=\"$<TARGET_FILE:dynlib-plugin>\"
+)
+
+add_dependencies(test-dynlib dynlib-plugin)
\ No newline at end of file
--- /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 <iostream>
+ * 
+ * #include "dynlib.hpp"
+ * 
+ * using HelloFunc = void (*)(const std::string &);
+ * 
+ * int main()
+ * {
+ *	try {
+ *		Dynlib library("./plugin.so");
+ *
+ *		HelloFunc hello = library.sym<HelloFunc>("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 <iostream>
+ *
+ * #include "dynlib.hpp"
+ *
+ * extern "C" {
+ *
+ * DYNLIB_EXPORT void say_hello()
+ * {
+ *	std::cout << "Hello from the plugin!" << std::endl;
+ * }
+ * ````
+ */
--- /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 <markand@malikania.fr>
+ *
+ * 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 <markand@malikania.fr>
+ */
+
+/**
+ * \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 <iostream>
+ *
+ * #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 <iostream>
+ *
+ * #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 <iostream>
+ *
+ * #include "dynlib.hpp"
+ *
+ * using PluginLoad = void (*)();
+ * using PluginUnload = void (*)();
+ *
+ * int main()
+ * {
+ *	try {
+ *		Dynlib dso("./plugin" DYNLIB_SUFFIX);
+ *
+ *		dso.sym<PluginLoad>("plugin_load")();
+ *		dso.sym<PluginUnload>("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 <stdexcept>
+#include <string>
+
+#if defined(_WIN32)
+#  include <Windows.h>
+#else
+#  include <dlfcn.h>
+#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 <typename T>
+	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 <typename T>
+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<T>(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 <typename T>
+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<T>(sym);
+}
+
+#endif
+
+#endif // !DYNLIB_HPP
--- /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 <markand@malikania.fr>
+ *
+ * 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 <iostream>
+
+#include <gtest/gtest.h>
+
+#include <dynlib.hpp>
+
+using Initialize = void (*)(std::string &s);
+
+TEST(Basic, initialize)
+{
+	try {
+		Dynlib library(PLUGIN);
+		Initialize init = library.sym<Initialize>("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>("initialize_typo"), std::runtime_error);
+}
+
+int main(int argc, char **argv)
+{
+	testing::InitGoogleTest(&argc, argv);
+
+	return RUN_ALL_TESTS();
+}
--- /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 <string>
+
+#include <dynlib.hpp>
+
+extern "C" {
+
+void DYNLIB_EXPORT initialize(std::string &result)
+{
+	result = "Hello World";
+}
+
+}