A C++ toolset for loading plugins with C ABI.
Provides:
- an interface to work with plugins whatever they are - dynamically-loaded or statically-linked libraries;
- code-generation tools to produce fasade for statically-linked plugins, so symbols redefinition conflicts are no longer a problem;
- a CMake script to produce statically-linked fasade in a single function call.
This means that for various plugins, which implement, for example,
get_full_name()
, you can, respectively:
- access all found plugins via:
CxxPlug<...> plug(...); auto lst = plug.get_available(); auto loaded_1 = plug.load(lst[0]), loaded_2 = plug.load(lst[1]); cout << loaded_1.impl.get_full_name() << '\n'; cout << loaded_2.impl.get_full_name() << '\n';
- link different implementations of the same interface statically, hiding conflictable symbols using provided command-line tools;
- decide, which plugins will be built-in and which are to be loaded dynamically when requested - using a single CMake variable with list of sources.
Requires:
nm
,objcopy
,ld
executables - implemented by GNU or LLVM, so can work even on Windows, despite Microsoft not providing anything similar toobjcopy --localize-symbol
andld --relocatable
.
On POSIX-compatible systems the dlopen
requirements for dynamically-loaded
libraries location are added, so the following strategies are possible when
plugin is searched by:
- relative path - requires the resulting executable being launched always from the same corresponding path (not very convenient, but portable);
- absolute path - requires plugins to be placed by the same absolute paths (not portable or requires adoption for every distributive);
- executable path - when using
RPATH
method, all plugin implementations must be placed in the same directory as executable's, so all implementations of all interfaces must have unique names (portable, but messy).
Conventions:
- plugins should implement
get_implementation_uuid
, that returns pointer to a valid uuid; - plugins should implement
get_implementation_name
that takes pointer to iso369-3 string and returns plugin name in the requested language, or default.
Dependencies (available as git submodules):
- ecstrings - its call is written by
code generation toos (when producing
*_interface.hpp
) to return plugin interface name - default or user-provided; - libload - optional,
cxxplug
provides an example of dynamic library load provider based on this library.
Build:
git clone https://github.com/makaleks/cxxplug.git --recursive
cd cxxplug
cmake -B build
cmake --build build
Run an example:
cd build
ctest .
or
cd build/cxxplug/tests/overview
./overview
the output of the last one will be similar to
0: [Oh Plugin](Ivan Susanin)
1: [Oh Plugin](Gregor Eisenhorn)
2: [Oh Plugin](plugin_human/vasiliy_pupkin.so)
0: PluginHuman {
src: <built_in>
name: "Ivan Susanin"
uuid: cf75898-a81-47fb-afe-93ccb9c8dc8c
first_name: "Ivan"
last_name: "Susanin"
age: 45
work_with_int: 10 => 0
}
1: PluginHuman {
src: <built_in>
name: "Gregor Eisenhorn"
uuid: 241bee6f-5a6d-47d9-a98e-952e954c037
first_name: "Gregor"
last_name: "Eisenhorn"
age: 42
work_with_int: 10 => 100
}
2: PluginHuman {
src: "plugin_human/vasiliy_pupkin.so"
name: "Vasiliy Puplik"
uuid: 704599c7-1e21-4d1c-9491-a89b598a097
first_name: "Vasiliy"
last_name: "Pupkin"
age: 30
work_with_int: 10 => 20
}
Done
The following is based on plugin_human of overview example.
-
Define plugin sources:
list( APPEND vasiliy_pupkin "vasiliy_pupkin/vasiliy_pupkin.c" ) list( APPEND ivan_susanin "ivan_susanin/ivan_susanin.c" ) list( APPEND gregor_eisenhorn "gregor_eisenhorn/gregor_eisenhorn.c" )
-
Choose which plugins should be loaded in runtime and which - must be linked:
list( APPEND plugins_dynamic "vasiliy_pupkin" ) list( APPEND plugins_integrated "ivan_susanin" "gregor_eisenhorn" )
-
Build dynamic libraries as usual:
foreach( plugin_name ${plugins_dynamic} ) add_library("${plugin_name}" MODULE "${${plugin_name}}") # So the output will be like "ivan_susanin_proxy.a" set_target_properties("${plugin_name}" PROPERTIES PREFIX "") target_compile_definitions("${plugin_name}" PRIVATE -DTEST_SHARED_BUILD) endforeach( plugin_name ${plugins_dynamic} )
-
Call cxxplug_gen_integrated() to build statically-linked fasade.
cxxplug_gen_integrated( LISTS_INSIDE INTERFACE_CONFIG "cxxplug_interface.conf" INTERFACE_NAME "plugin_human" IMPLEMENTATIONS ${plugins_integrated} ) # Library target object returned, must be linked with final executable set(plugin_human_builtin ${CXXPLUG_INTERFACE_BUILTIN} PARENT_SCOPE) # Include directory with generated builtin-collection and interface headers # returned include_directories("${CXXPLUG_GENERATED_INCLUDE_DIR}") target_link_libraries(overview ${plugin_human_builtin})
#include <cxxplug.hpp>
#include <cxxplug/some_source_providers/filesystem_dynlib_provider.hpp>
#include <cxxplug/some_load_providers/libload_provider.hpp>
#include "cxxplug_gen/builtin/plugin_human_builtin.hpp"
template <typename PlugLoaded>
void print_plugin_human (PlugLoaded &loaded) {
char lang[] = "eng";
int value = 10;
cout << "\
PluginHuman {\n\
src: " <<
(
loaded.is_builtin
? "<built_in>"
: (string("\"") + loaded.source_name + "\"")
) << "\n\
\n\
name: \"" << loaded.impl.get_implementation_name(lang) << "\"\n\
uuid: " << *(Uuid*)loaded.impl.get_implementation_id() << "\n\
\n\
first_name: \"" << loaded.impl.get_first_name() << "\"\n\
last_name: \"" << loaded.impl.get_last_name() << "\"\n\
age: " << std::dec << (unsigned)loaded.impl.get_age() << "\n\
work_with_int: " << value;
loaded.impl.work_with_int(&value);
cout << " => " << value << "\n\
}\n";
}
int main () {
// Yes, you are free to define and set your own LoadProvider, SourceProvider
// and even collection of builtin plugins - cxxplug is flexible enough
CxxPlug<
PluginHumanInterface,
SourceProviderFilesystemDynLib<PluginHumanInterface>,
LoadProviderLibload
> plug(
"plugin_human",
// This array was generated using `cxxplug-gen-collected-proxies` tool
span<PluginHumanInterface>(begin(plugin_human),end(plugin_human))
);
auto lst = plug.get_available();
for (size_t i = 0; i < lst.size(); i++) {
cout << i << ": " << lst[i].string() << "\n";
}
if (0 != lst.size()) {
cout << "\n";
}
// In fact, it is
// `vector<CxxPlugLoaded<PluginHumanInterface,LoadProviderLibload>> loaded;`
// but if no wish to change template arguments someday, use
vector<result_of<
decltype(&decltype(plug)::load)(decltype(plug), decltype(lst[0]))
>::type> loaded;
for (size_t i = 0; i < lst.size(); i++) {
loaded.emplace_back(plug.load(lst[i]));
cout << i << ": ";
print_plugin_human(loaded[i]);
}
cout << "\nDone\n";
return 0;
}
cxxplug_gen_integrated() was made for CMake, but
you may write your of code for any build system,
because cxxplug_gen_integrated()
only:
- adds plugin as static-library target,
forivan_susanin
producesivan_susanin.a
, - calls cxxplug-gen-interface tool to
produce interface header from plugin interface description
file,
example of produced -
plugin_human_interface.hpp
; - calls cxxplug-gen-proxy tool to
produce interface variable (whose type is defined in interface header,
produced above) from plugin's C-implementation whith unique name,
forivan_susanin
producesivan_susanin_proxy_proxy.hpp
,ivan_susanin_proxy.cpp
and asks CMake to build them asivan_susanin_proxy_impl.a
with a setting to be linked withivan_susanin.a
, built above; - calls
ld --whole-archive --relocatable
to force immediate linkage ofivan_susanin_proxy_impl.a
andivan_susanin.a
to a single file -ivan_susanin_proxy_merged.a
- calls cxxplug-hide-linked-symbols
to hide (localize) symbols, defined in some library, in other library,
here symbols redefinition conflict goes away,
forivan_susanin
producesivan_susanin_proxy.a
, - calls
cxxplug-gen-collected-proxies
to generate source files for an array, including all produced proxies,
then adds CMake target to build it,
forplugin_human
interface produceslibplugin_human_builtin.a
, which is returned as${CXXPLUG_INTERFACE_BUILTIN}
, (in addition,${CXXPLUG_GENERATED_INCLUDE_DIR}
with a path tolibplugin_human_builtin.a
header is returned)