├── .gitignore ├── CMakeLists.txt ├── README.md ├── TODO.md ├── include └── scy │ └── pluga │ └── pluga.h ├── src └── pluga.cpp └── tests ├── CMakeLists.txt ├── plugatestplugin ├── CMakeLists.txt ├── testplugin.cpp ├── testplugin.h └── testpluginapi.h └── plugatests.cpp /.gitignore: -------------------------------------------------------------------------------- 1 | *~ 2 | ~* 3 | -------------------------------------------------------------------------------- /CMakeLists.txt: -------------------------------------------------------------------------------- 1 | ask_build_sourcey_module(pluga) 2 | if(BUILD_MODULES OR (NOT BUILD_MODULES AND BUILD_MODULE_pluga)) 3 | define_sourcey_module(pluga base) 4 | endif() 5 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Pluga 2 | 3 | > Simple C++ plugin system 4 | 5 | [![Circle CI](https://circleci.com/gh/sourcey/libsourcey.svg?style=shield&circle-token=ab142562b19bb857de796d729aab28fa9df7682d)](https://circleci.com/gh/sourcey/libsourcey) 6 | [![Doxygen](http://sourcey.com/images/doxygen.svg)](http://sourcey.com/libsourcey/api-pacm/) 7 | 8 | **Homepage**: [http://sourcey.com/pluga](http://sourcey.com/pluga/) 9 | **Documentation**: [http://sourcey.com/libsourcey/api-pluga/](http://sourcey.com/libsourcey/api-pluga/) 10 | **Dependencies**: [LibSourcey (base, uv)](http://sourcey.com/libsourcey/) 11 | **Licence**: LGPL 12 | 13 | Pluga is a simple C++ plugin system that you can drop into your own projects. It currently supports: 14 | 15 | * Loading cross-platform plugins and shared libraries 16 | * Simple and elegant API for defining plugins 17 | * Strict API versioning for ABI safety 18 | 19 | For more information take a look at [this blog post](http://sourcey.com/building-a-simple-cpp-cross-platform-plugin-system) which talks about the concepts and mothodology behind Pluga. 20 | 21 | ## Installing 22 | 23 | * Install [LibSourcey](http://sourcey.com/libsourcey/#installation) 24 | * Clone the Pluga repository in the LibSourcey `src` folder: 25 | ~~~ bash 26 | cd libsourcey/src 27 | git clone https://github.com/sourcey/pluga.git 28 | ~~~ 29 | * Ensure the `BUILD_MODULE_pluga` build variables are enabled in CMake and regenerate the LibSourcey project files 30 | * To build Pluga tests also enable the `BUILD_TESTS` and `BUILD_TEST_pluga` variables in CMake 31 | 32 | ## Contributing 33 | 34 | Contributions in the form of pull requests are always welcome. 35 | 36 | 1. [Fork Pluga on Github](https://github.com/sourcey/pluga) 37 | 2. Create your feature branch (`git checkout -b my-new-feature`) 38 | 3. Commit your changes (`git commit -am 'Add some feature'`) 39 | 4. Push to the branch (`git push origin my-new-feature`) 40 | 5. Create new Pull Request 41 | 42 | ## Issues 43 | 44 | If you find any bugs or issues please use the [Github issue tracker](https://github.com/sourcey/pluga/issues). 45 | -------------------------------------------------------------------------------- /TODO.md: -------------------------------------------------------------------------------- 1 | * Implement a PluginManager class aka [Spot](https://anionu.com/spot) 2 | * Recursively scans for plugin.json manifests 3 | * Enables loading and unloading of plugins -------------------------------------------------------------------------------- /include/scy/pluga/pluga.h: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | // LibSourcey 4 | // Copyright (c) 2005, Sourcey 5 | // 6 | // SPDX-License-Identifier: LGPL-2.1+ 7 | // 8 | /// @addtogroup pluga 9 | /// @{ 10 | 11 | 12 | #ifndef SCY_Pluga_H 13 | #define SCY_Pluga_H 14 | 15 | 16 | #include "scy/base.h" 17 | 18 | 19 | // Shared library exports 20 | #if defined(SCY_WIN) && defined(SCY_SHARED_LIBRARY) 21 | #if defined(Pluga_EXPORTS) 22 | #define Pluga_API __declspec(dllexport) 23 | #else 24 | #define Pluga_API __declspec(dllimport) 25 | #endif 26 | #else 27 | #define Pluga_API // nothing 28 | #endif 29 | 30 | 31 | namespace scy { 32 | namespace pluga { 33 | 34 | 35 | // Forward declare the plugin class which must be defined externally. 36 | class Pluga_API IPlugin; 37 | 38 | // Define the API version. 39 | // This value is incremented whenever there are ABI breaking changes. 40 | #define SCY_PLUGIN_API_VERSION 1 41 | 42 | #ifdef SCY_WIN 43 | #define SCY_PLUGIN_EXPORT __declspec(dllexport) 44 | #else 45 | #define SCY_PLUGIN_EXPORT // empty 46 | #endif 47 | 48 | // Define a type for the static function pointer. 49 | Pluga_API typedef IPlugin* (*GetPluginFunc)(); 50 | 51 | // Plugin details structure that's exposed to the application. 52 | struct PluginDetails 53 | { 54 | int apiVersion; 55 | const char* fileName; 56 | const char* className; 57 | const char* pluginName; 58 | const char* pluginVersion; 59 | GetPluginFunc initializeFunc; 60 | }; 61 | 62 | #define SCY_STANDARD_PLUGIN_STUFF SCY_PLUGIN_API_VERSION, __FILE__ 63 | 64 | #define SCY_PLUGIN(classType, pluginName, pluginVersion) \ 65 | extern "C" { \ 66 | SCY_PLUGIN_EXPORT scy::pluga::IPlugin* getPlugin() \ 67 | { \ 68 | static classType singleton; \ 69 | return &singleton; \ 70 | } \ 71 | SCY_PLUGIN_EXPORT scy::pluga::PluginDetails exports = { \ 72 | SCY_STANDARD_PLUGIN_STUFF, \ 73 | #classType, \ 74 | pluginName, \ 75 | pluginVersion, \ 76 | getPlugin, \ 77 | }; \ 78 | } 79 | 80 | 81 | } // namespace pluga 82 | } // namespace scy 83 | 84 | 85 | #endif // SCY_Pluga_H 86 | 87 | 88 | /// @\} 89 | -------------------------------------------------------------------------------- /src/pluga.cpp: -------------------------------------------------------------------------------- 1 | /// 2 | // 3 | // LibSourcey 4 | // Copyright (c) 2005, Sourcey 5 | // 6 | // SPDX-License-Identifier: LGPL-2.1+ 7 | // 8 | /// @addtogroup pluga 9 | /// @{ 10 | 11 | 12 | #include "scy/pluga/pluga.h" 13 | 14 | 15 | // just to keep compiler happy :) 16 | 17 | 18 | /// @\} 19 | -------------------------------------------------------------------------------- /tests/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | define_libsourcey_test(plugatests base pluga) 2 | add_subdirectory("plugatestplugin") 3 | -------------------------------------------------------------------------------- /tests/plugatestplugin/CMakeLists.txt: -------------------------------------------------------------------------------- 1 | define_libsourcey_library(plugatestplugin base pluga) 2 | -------------------------------------------------------------------------------- /tests/plugatestplugin/testplugin.cpp: -------------------------------------------------------------------------------- 1 | #include "testplugin.h" 2 | #include 3 | #include 4 | #include 5 | #include 6 | 7 | 8 | // 9 | // Test Plugin 10 | // 11 | 12 | 13 | SCY_PLUGIN(TestPlugin, "Test Plugin", "0.1.1") 14 | 15 | 16 | TestPlugin::TestPlugin() 17 | { 18 | std::cout << "TestPlugin: Create" << std::endl; 19 | } 20 | 21 | 22 | TestPlugin::~TestPlugin() 23 | { 24 | std::cout << "TestPlugin: Destroy" << std::endl; 25 | } 26 | 27 | 28 | bool TestPlugin::onCommand(const char* node, const char* data, 29 | unsigned int size) 30 | { 31 | std::cout << "TestPlugin: Command: " << node << ": " << data << std::endl; 32 | 33 | try { 34 | // Handle a JSON encoded options hash 35 | if (strcmp(node, "options:set") == 0) { 36 | #if 0 37 | json::value root; 38 | json::Reader reader; 39 | if (!reader.parse(data, size, root)) 40 | throw std::runtime_error("Invalid JSON format: " + reader.getFormattedErrorMessages()); 41 | 42 | // Do something with JSON data here... 43 | #endif 44 | } 45 | 46 | // Handle raw file data 47 | else if (strcmp(node, "file:write") == 0) { 48 | std::string path("test.bin"); 49 | std::ofstream ofs(path, std::ios::out | std::ios::binary); 50 | if (!ofs.is_open()) 51 | throw std::runtime_error("Cannot write to output file: " + 52 | path); 53 | ofs.write(data, size); 54 | ofs.close(); 55 | } 56 | 57 | // Handle unknown commands 58 | else 59 | throw std::runtime_error("Unknown command"); 60 | } 61 | 62 | // Catch exceptions to set the internal error message and return false 63 | catch (std::exception& exc) { 64 | _error.assign(exc.what()); 65 | std::cerr << "Command error: " << _error << std::endl; 66 | return false; 67 | } 68 | 69 | return true; 70 | } 71 | 72 | 73 | void TestPlugin::setValue(const char* value) 74 | { 75 | std::cout << "TestPlugin: Set value: " << value << std::endl; 76 | _value = value; 77 | } 78 | 79 | 80 | std::string TestPlugin::sValue() const 81 | { 82 | return _value; 83 | } 84 | 85 | 86 | const char* TestPlugin::cValue() const 87 | { 88 | return _value.c_str(); 89 | } 90 | 91 | 92 | const char* TestPlugin::lastError() const 93 | { 94 | return _error.empty() ? nullptr : _error.c_str(); 95 | } 96 | 97 | 98 | // 99 | // Static Methods 100 | // 101 | 102 | 103 | extern "C" SCY_PLUGIN_EXPORT int gimmeFive(); 104 | 105 | 106 | int gimmeFive() 107 | { 108 | return 5; 109 | } 110 | -------------------------------------------------------------------------------- /tests/plugatestplugin/testplugin.h: -------------------------------------------------------------------------------- 1 | #ifndef SCY_TestPlugin_H 2 | #define SCY_TestPlugin_H 3 | 4 | 5 | #include "testpluginapi.h" 6 | 7 | 8 | /// Test plugin implementation 9 | class TestPlugin : public scy::pluga::IPlugin 10 | { 11 | public: 12 | TestPlugin(); 13 | virtual ~TestPlugin(); 14 | 15 | // 16 | /// Commands 17 | 18 | /// Handle a command from the application. 19 | virtual bool onCommand(const char* node, const char* data, unsigned int size); 20 | 21 | // Return the last error message as a char pointer. 22 | /// If no error is set a nullptr will be returned. 23 | virtual const char* lastError() const; 24 | 25 | 26 | // 27 | /// String accessors 28 | 29 | /// Sets the internal string from the given value. 30 | /// The given value must be null terminated. 31 | virtual void setValue(const char* value); 32 | 33 | // Return the internal string value as a char pointer. 34 | /// Since we are returning a POD type plugins will be ABI agnostic. 35 | virtual const char* cValue() const; 36 | 37 | 38 | #if PLUGA_ENABLE_STL 39 | /// Return the internal string value as an STL string. 40 | /// This method breaks ABI agnosticity. 41 | /// See the PLUGU_ENABLE_STL definition above. 42 | virtual std::string sValue() const; 43 | #endif 44 | 45 | protected: 46 | std::string _error; 47 | std::string _value; 48 | }; 49 | 50 | 51 | #endif 52 | -------------------------------------------------------------------------------- /tests/plugatestplugin/testpluginapi.h: -------------------------------------------------------------------------------- 1 | #ifndef SCY_TestPluginAPI_H 2 | #define SCY_TestPluginAPI_H 3 | 4 | 5 | #include "scy/pluga/pluga.h" 6 | #include 7 | 8 | 9 | namespace scy { 10 | namespace pluga { 11 | 12 | 13 | // NOTE: When using STL containers and other complex types you 14 | // will need to ensure plugins are always built using the same 15 | // compiler, since system libraries aren't ABI compatible. 16 | #define PLUGA_ENABLE_STL 1 17 | 18 | 19 | /// Virtual base class for plugins 20 | class IPlugin 21 | { 22 | public: 23 | IPlugin(){}; 24 | virtual ~IPlugin(){}; 25 | 26 | // 27 | /// Commands 28 | 29 | /// Handle a command from the application. 30 | virtual bool onCommand(const char* node, const char* data, 31 | unsigned int size) = 0; 32 | 33 | /// Return the last error message as a char pointer. 34 | /// If no error is set a nullptr will be returned. 35 | virtual const char* lastError() const = 0; 36 | 37 | 38 | // 39 | /// String accessors 40 | 41 | // Sets the internal string from the given value. 42 | /// The given value must be null terminated. 43 | virtual void setValue(const char* value) = 0; 44 | 45 | // Return the internal string value as a char pointer. 46 | /// Since we are returning a POD type plugins will be ABI agnostic. 47 | virtual const char* cValue() const = 0; 48 | 49 | 50 | #if PLUGA_ENABLE_STL 51 | /// Return the internal string value as an STL string. 52 | /// This method breaks ABI agnosticity. 53 | /// See the PLUGU_ENABLE_STL definition above. 54 | virtual std::string sValue() const = 0; 55 | #endif 56 | }; 57 | 58 | 59 | typedef int (*GimmeFiveFunc)(); /// Static function which returns, you guessed 60 | /// it, the number 5! 61 | 62 | 63 | } // namespace pluga 64 | } // namespace scy 65 | 66 | 67 | #endif 68 | -------------------------------------------------------------------------------- /tests/plugatests.cpp: -------------------------------------------------------------------------------- 1 | #include "scy/pluga/pluga.h" 2 | #include "scy/base.h" 3 | #include "scy/logger.h" 4 | #include "scy/sharedlibrary.h" 5 | #include "scy/test.h" 6 | 7 | #include "plugatestplugin/testpluginapi.h" 8 | 9 | 10 | using std::cout; 11 | using std::cerr; 12 | using std::endl; 13 | 14 | 15 | #define PLUGA_ENABLE_STL 1 16 | 17 | 18 | namespace scy { 19 | namespace pluga { 20 | 21 | 22 | class Tests 23 | { 24 | public: 25 | Tests() { runPluginTest(); } 26 | 27 | void runPluginTest() 28 | { 29 | // Set the plugin shared library location 30 | std::string path(SCY_BUILD_DIR); 31 | path += "/src/pluga/tests/plugatestplugin/"; 32 | #if WIN32 33 | #ifdef _DEBUG 34 | path += "plugatestplugind.dll"; 35 | #else 36 | path += "plugatestplugin.dll"; 37 | #endif 38 | #else 39 | #ifdef _DEBUG 40 | path += "libplugatestplugind.so"; 41 | #else 42 | path += "libplugatestplugin.so"; 43 | #endif 44 | #endif 45 | 46 | try { 47 | // Load the shared library 48 | cout << "Loading: " << path << endl; 49 | SharedLibrary lib; 50 | lib.open(path); 51 | 52 | // Get plugin descriptor and exports 53 | PluginDetails* info; 54 | lib.sym("exports", reinterpret_cast(&info)); 55 | cout << "Plugin Info: " 56 | << "\n\tAPI Version: " << info->apiVersion 57 | << "\n\tFile Name: " << info->fileName 58 | << "\n\tClass Name: " << info->className 59 | << "\n\tPlugin Name: " << info->pluginName 60 | << "\n\tPlugin Version: " << info->pluginVersion 61 | << endl; 62 | 63 | // API version checking 64 | if (info->apiVersion != SCY_PLUGIN_API_VERSION) 65 | throw std::runtime_error(util::format( 66 | "Plugin version mismatch. Expected %s, got %s.", 67 | SCY_PLUGIN_API_VERSION, info->apiVersion)); 68 | 69 | // Instantiate the plugin 70 | auto plugin = reinterpret_cast(info->initializeFunc()); 71 | 72 | // Call string accessor methods 73 | plugin->setValue("abracadabra"); 74 | expect(strcmp(plugin->cValue(), "abracadabra") == 0); 75 | #if PLUGA_ENABLE_STL 76 | expect(plugin->sValue() == "abracadabra"); 77 | #endif 78 | 79 | // Call command methods 80 | expect(plugin->onCommand("options:set", "rendomdata", 10)); 81 | expect(plugin->lastError() == nullptr); 82 | expect(plugin->onCommand("unknown:command", "rendomdata", 10) == false); 83 | expect(strcmp(plugin->lastError(), "Unknown command") == 0); 84 | 85 | // Call a C function 86 | GimmeFiveFunc gimmeFive; 87 | lib.sym("gimmeFive", reinterpret_cast(&gimmeFive)); 88 | expect(gimmeFive() == 5); 89 | 90 | // Close the plugin and free memory 91 | lib.close(); 92 | } catch (std::exception& exc) { 93 | cerr << "Error: " << exc.what() << endl; 94 | expect(false); 95 | } 96 | 97 | cout << "Ending" << endl; 98 | } 99 | }; 100 | 101 | 102 | } // namespace pluga 103 | } // namespace scy 104 | 105 | 106 | int main(int argc, char** argv) 107 | { 108 | #ifdef _MSC_VER 109 | _CrtSetDbgFlag(_CRTDBG_ALLOC_MEM_DF | _CRTDBG_LEAK_CHECK_DF); 110 | #endif 111 | 112 | // Run tests 113 | { 114 | scy::pluga::Tests run; 115 | } 116 | 117 | return 0; 118 | } 119 | --------------------------------------------------------------------------------