OpenMethod uses global data to keep track of methods, overriders and classes,
all managed by static constructors and destructors. initialize uses that
information to set up dispatch tables. For a program and its shared libraries
to contribute classes, methods and overriders to the same registry, that data
must be truly global: a single copy, shared by all the modules.
All of a registry’s mutable state lives in a single variable. Sharing a
registry across modules therefore means sharing that one symbol: the owning
module exports it, and every client module imports it. Three macros do this.
Each takes the registry as an argument, so they can be used to manage
default_registry, indirect_registry and any custom registry:
| Macro | Where |
|---|---|
header; every translation unit of a client module |
|
header; every translation unit of the owning module |
|
exactly one |
The owning module uses two of them: EXPORT in the header its translation
units share, and INSTANTIATE in exactly one of them.
// header, every translation unit of a client module
BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry);
// header, every translation unit of the owning module
BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry);
// exactly one .cpp of the owning module
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry);
Use them at namespace scope, after <boost/openmethod.hpp> has been included.
Everything they emit is fully qualified, so nothing need be added to
namespace boost::openmethod.
The macros exist because the underlying explicit instantiations are not portable - the two ABIs want opposite things, and getting it wrong fails on one platform while compiling silently on the other:
-
On Windows, Cygwin and MinGW,
__declspec(dllexport)andexternare incompatible on an explicit instantiation, and MSVC rejects the combination outright. Nor is such a declaration needed there: visibility is not a PE concept, so the owning module’s other translation units may instantiate the state implicitly.EXPORTexpands to nothing andINSTANTIATEcarries thedllexport. -
On ELF and Mach-O the attribute must be on the declaration, so that every translation unit of the owning module pins the symbol to default visibility. Repeating it on the definition is an error on GCC, so
INSTANTIATEcarries no attribute there.
on ELF, EXPORT is not decoration. A translation unit of the owning
module that has neither it nor the instantiation instantiates the state
implicitly, and under -fvisibility=hidden that copy is module-local. Because
ELF merges COMDATs at the most restrictive visibility, the merged symbol then
becomes local: the module builds, exports nothing, and clients fail to link with
an undefined reference to registry_state<…>::st.
|
Nothing else needs decoration. In particular, methods declared with
BOOST_OPENMETHOD require no export or import: method objects are consolidated
across modules when initialize runs (see Implementation Notes).
If a library uses open-methods as an implementation detail, and does not make them part of its public API, it is not necessary to export the registry state.
Implicit Linking
When a program links against a shared library at build time, all the modules
are loaded before main starts, their static constructors run, and a single
call to initialize sets up the dispatch tables. The only requirement is the
one stated above: one module owns the registry state, the others import it.
The natural arrangement is for the shared library to own the registry: the
program already links against the library, so, on Windows, the import library
dependency runs in the usual direction. The implicit_linking example
demonstrates this arrangement.
The following header is shared between the program and the library. It defines
the class hierarchy and the meet method, and it is also where each module says
how it relates to the registry state:
// animals.hpp
#include <string>
#include <boost/openmethod.hpp>
#ifdef OWNS_REGISTRY_STATE
BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::default_registry);
#else
BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::default_registry);
#endif
struct Animal { virtual ~Animal() {} };
struct Herbivore : Animal {};
struct Carnivore : Animal {};
struct Cow : Herbivore {};
struct Wolf : Carnivore {};
BOOST_OPENMETHOD(
meet, (
boost::openmethod::virtual_ptr<Animal>,
boost::openmethod::virtual_ptr<Animal>),
std::string);
The module that owns the state defines OWNS_REGISTRY_STATE before including
this header, which selects EXPORT; every other module gets IMPORT. The owner
also uses INSTANTIATE, in one of its .cpp files.
each module here has a single translation unit, so on ELF the owning module
could get away with INSTANTIATE alone. EXPORT is shown because it becomes
required the moment that module gains a second translation unit - see the
warning above. Putting it in the shared header from the start makes the module
safe to grow.
|
Here the library owns the state. It registers the classes, provides the default behaviour, and emits the definition:
// extensions.cpp
#define OWNS_REGISTRY_STATE
#include "animals.hpp"
using namespace boost::openmethod;
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry);
BOOST_OPENMETHOD_CLASSES(Animal, Herbivore, Cow, Carnivore, Wolf);
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Animal>, virtual_ptr<Animal>), std::string) {
return "greet";
}
The program extends it. It adds a class the library has never heard of, two
overriders that specialise meet, and imports the state through the header:
// main.cpp
#include "animals.hpp"
#include <boost/openmethod/initialize.hpp>
#include <iostream>
#include <memory>
using namespace boost::openmethod::aliases;
struct Tiger : Carnivore {};
BOOST_OPENMETHOD_CLASSES(Tiger, Carnivore);
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Herbivore> a, virtual_ptr<Carnivore> b), std::string) {
auto base = next(a, b);
return "do not " + base + ", run";
}
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Carnivore>, virtual_ptr<Herbivore>), std::string) {
return "hunt";
}
auto main() -> int {
boost::openmethod::initialize();
std::unique_ptr<Animal> gracie(new Cow());
std::unique_ptr<Animal> willy(new Wolf());
std::unique_ptr<Animal> hobbes(new Tiger());
std::cout << "cow meets wolf -> " << meet(*gracie, *willy)
<< "\n"; // do not greet, run
std::cout << "wolf meets cow -> " << meet(*willy, *gracie) << "\n"; // hunt
std::cout << "cow meets tiger -> " << meet(*gracie, *hobbes)
<< "\n"; // do not greet, run
return 0;
}
Note what next does here: the program’s Herbivore, Carnivore overrider calls
the library’s Animal, Animal overrider, so control crosses the module boundary
in the middle of a single dispatch, and cow meets wolf prints
do not greet, run.
Dynamic Linking
By "dynamic linking", we mean a program loading a shared library after it has started, and accessing its content. A common application of dynamic linking is to implement plugin architectures.
A dynamically loaded library can add classes, methods and overriders to an
existing registry. initialize must be called to rebuild the dispatch tables
after loading or unloading a shared library.
Here the program is the natural owner of the registry state: it must be able to run before the library is loaded, and regardless of whether it is loaded at all.
Let’s look at an example. It shares the same animals.hpp as the implicit-linking
example above - same classes, same meet method, same OWNS_REGISTRY_STATE
convention - but ownership is reversed.
Here the main program owns the registry state, so it is main.cpp that defines
OWNS_REGISTRY_STATE and emits the EXPORT definition, in its only translation
unit, satisfying the exactly-one rule. It provides a catch-all overrider, then
calls the meet method:
// main.cpp
#define OWNS_REGISTRY_STATE
#include "animals.hpp"
#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <boost/dll/shared_library.hpp>
#include <iostream>
#include <memory>
using namespace boost::openmethod;
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::default_registry);
BOOST_OPENMETHOD_CLASSES(Herbivore, Cow, Carnivore, Wolf);
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Animal>, virtual_ptr<Animal>), std::string) {
return "greet";
}
int main() {
try {
std::cout << "Before loading the shared library.\n";
boost::openmethod::initialize(trace::from_env());
std::cout << "cow meets wolf -> "
<< meet(*std::make_unique<Cow>(), *std::make_unique<Wolf>())
<< "\n"; // greet
std::cout << "wolf meets cow -> "
<< meet(*std::make_unique<Wolf>(), *std::make_unique<Cow>())
<< "\n"; // greet
// to be continued...
The shared library does not define OWNS_REGISTRY_STATE, so the header
imported the state for it. It adds two overriders, a new class, Tiger, and a
factory function:
// extensions.cpp
#include "animals.hpp"
using namespace boost::openmethod;
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Herbivore> a, virtual_ptr<Carnivore> b), std::string) {
auto base = next(a, b);
return "do not " + base + ", run";
}
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Carnivore>, virtual_ptr<Herbivore>), std::string) {
return "hunt";
}
struct Tiger : Carnivore {};
BOOST_OPENMETHOD_CLASSES(Tiger, Carnivore);
extern "C" {
BOOST_SYMBOL_EXPORT auto make_tiger() -> Animal* {
return new Tiger;
}
}
We load the shared library using Boost.DLL. After calling initialize, the new
overriders are installed. The main program can also use Tiger objects, even
though it has no knowledge of that class at compile time.
int main() {
// ...
std::cout << "\nLoading shared object / DLL.\n";
boost::dll::shared_library lib(
"boost_openmethod-shared",
boost::dll::load_mode::rtld_global |
boost::dll::load_mode::append_decorations);
boost::openmethod::initialize(trace::from_env());
std::cout << "cow meets wolf -> "
<< meet(*std::make_unique<Cow>(), *std::make_unique<Wolf>())
<< "\n"; // do not greet, run
std::cout << "wolf meets cow -> "
<< meet(*std::make_unique<Wolf>(), *std::make_unique<Cow>())
<< "\n"; // hunt
auto make_tiger = lib.get<Animal*()>("make_tiger");
std::cout << "cow meets tiger -> "
<< meet(
*std::make_unique<Cow>(),
*std::unique_ptr<Animal>(make_tiger()))
<< "\n"; // do not greet, run
Finally, we unload the shared library and call initialize again. The
overriders provided by the shared library are removed from the method.
// ...
std::cout << "\nAfter unloading the shared library.\n";
lib.unload();
boost::openmethod::initialize(trace::from_env());
std::cout << "cow meets wolf -> "
<< meet(*std::make_unique<Cow>(), *std::make_unique<Wolf>())
<< "\n"; // greet
std::cout << "wolf meets cow -> "
<< meet(*std::make_unique<Wolf>(), *std::make_unique<Cow>())
<< "\n"; // greet
} catch (const std::exception& ex) {
std::cerr << "Exception: " << ex.what() << '\n';
return 1;
}
return 0;
}
dlclose does not necessarily unload the library when using gcc’s
-rdynamic option. The library may remain loaded until the program exits, so
the output of the last meet call may still be the same as the previous one.
|
if a shared library installs an error handler (see
error_handling.adoc) via the registry’s error_handler::set, reset it
(e.g. to the previous handler) before the library is unloaded. The handler is
part of the registry’s shared state, so it stays installed after dlclose;
calling it afterwards invokes code in the unloaded library.
|
CMake Setup
Since the executable owns the registry state, the shared library must link against the executable to resolve the imported symbol. This is the reverse of the typical linking direction:
add_executable(my_app main.cpp)
set_target_properties(my_app PROPERTIES ENABLE_EXPORTS ON)
target_link_libraries(my_app Boost::openmethod Boost::dll)
add_library(my_plugin SHARED extensions.cpp)
target_link_libraries(my_plugin PRIVATE Boost::openmethod my_app)
On Windows, ENABLE_EXPORTS ON on the executable tells CMake to generate an
import library (.lib on MSVC) that the DLL links against. On POSIX,
ENABLE_EXPORTS adds -rdynamic (or the platform’s equivalent), which makes
the executable’s symbols visible to the libraries it loads.
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY produces an explicit instantiation
definition, which may appear only once in the program. Use it in exactly one
translation unit of the owning module; that module’s other translation units use
EXPORT, and client modules IMPORT.
|
a translation unit that uses neither macro compiles cleanly - it just
silently gets its own private copy of the registry state instead of an error.
That module then stops sharing classes, methods and overriders with the rest of
the program (symptoms: missing_class or unexpected bad_call errors). Guard
against this by putting the applicable macro in a project header that every
translation unit includes, as in the examples, rather than repeating it in
individual .cpp files.
|
Indirect Vptrs
initialize rebuilds the v-tables in the registry. This invalidates all the
virtual_ptrs, and also the v-table pointers stored in objects by
inplace_vptr_base, related to that registry. This is seldom an issue, as
most programs that dynamically load shared libraries do so at the very beginning
of their execution.
Otherwise, indirect v-table pointers must be used. This is achieved by using a
registry that contains the indirect_vptr policy.
<boost/openmethod/default_registry.hpp> provides an indirect_registry
that has the same policies as default_registry, plus indirect_vptr. Make it
the registry the BOOST_OPENMETHOD macros use by defining
BOOST_OPENMETHOD_DEFAULT_REGISTRY
before including <boost/openmethod.hpp>.
The indirect_vptr example does that in the header both modules share, rather
than passing a compiler switch from the build system. One place then settles
both questions - which registry the program uses, and how its state is shared -
and no translation unit can accidentally disagree:
// animals.hpp
#include <string>
#define BOOST_OPENMETHOD_DEFAULT_REGISTRY boost::openmethod::indirect_registry
#include <boost/openmethod.hpp>
#ifdef OWNS_REGISTRY_STATE
BOOST_OPENMETHOD_EXPORT_REGISTRY(boost::openmethod::indirect_registry);
#else
BOOST_OPENMETHOD_IMPORT_REGISTRY(boost::openmethod::indirect_registry);
#endif
struct Animal { virtual ~Animal() {} };
struct Herbivore : Animal {};
struct Carnivore : Animal {};
struct Cow : Herbivore {};
struct Wolf : Carnivore {};
BOOST_OPENMETHOD_CLASSES(Animal, Herbivore, Cow, Carnivore, Wolf);
BOOST_OPENMETHOD(
meet, (
boost::openmethod::virtual_ptr<Animal>,
boost::openmethod::virtual_ptr<Animal>),
std::string);
indirect_registry has its own state, separate from default_registry’s,
shared with the same three macros: just name `indirect_registry, as above.
Here is a program that carries virtual_ptrs across initialize calls.
It owns the state, so it defines OWNS_REGISTRY_STATE and emits the definition:
// main.cpp
#define OWNS_REGISTRY_STATE
#include "animals.hpp"
#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <boost/openmethod/interop/std_unique_ptr.hpp>
#include <boost/dll/shared_library.hpp>
#include <iostream>
using namespace boost::openmethod::aliases;
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(boost::openmethod::indirect_registry);
BOOST_OPENMETHOD_OVERRIDE(
meet, (virtual_ptr<Animal>, virtual_ptr<Animal>), std::string) {
return "greet";
}
auto main() -> int {
std::cout << "Before loading the shared library.\n";
boost::openmethod::initialize();
auto gracie = make_unique_virtual<Cow>();
auto willy = make_unique_virtual<Wolf>();
std::cout << "cow meets wolf -> " << meet(*gracie, *willy) << "\n"; // greet
std::cout << "wolf meets cow -> " << meet(*willy, *gracie) << "\n"; // greet
std::cout << "\nAfter loading the shared library.\n";
boost::dll::shared_library lib(
"boost_openmethod-indirect_shared",
boost::dll::load_mode::rtld_global |
boost::dll::load_mode::append_decorations);
boost::openmethod::initialize();
// The virtual_ptrs made before the reload still dispatch correctly:
std::cout << "cow meets wolf -> " << meet(*gracie, *willy)
<< "\n"; // do not greet, run
std::cout << "wolf meets cow -> " << meet(*willy, *gracie) << "\n"; // hunt
return 0;
}
The shared library it loads includes the same header, so it uses
indirect_registry too and imports the state. The complete example is in the
indirect_vptr directory.
Custom Registries
A custom registry is shared exactly the same way - name it instead of
default_registry. Nothing above is specific to the predefined registries:
// my_registry.hpp
struct my_registry : boost::openmethod::registry</* policies... */> {};
#ifdef OWNS_REGISTRY_STATE
BOOST_OPENMETHOD_EXPORT_REGISTRY(my_registry);
#else
BOOST_OPENMETHOD_IMPORT_REGISTRY(my_registry);
#endif
// registry.cpp - exactly one translation unit of the owning module
#define OWNS_REGISTRY_STATE
#include "my_registry.hpp"
BOOST_OPENMETHOD_INSTANTIATE_REGISTRY(my_registry);
Implementation Notes
One state variable per registry
Everything mutable in a registry is agglomerated into a single variable. The
registry’s class and method lists, the dispatch tables, and the state of every
stateful policy (for example, the hash function parameters of
fast_perfect_hash, or the vector of v-table pointers of
vptr_vector) are all members of one object, of which there is exactly
one instance per registry. Policies do not keep their own globals: their state
is gathered, by template machinery, into a tuple inside that same object.
This is what makes sharing a registry across modules tractable: there is
exactly one symbol to export and import, whatever the registry’s policies.
The variable is the sole member of registry_state, a deliberately thin,
function-free class template. That form is dictated by MSVC:
dllexport/dllimport are only honored on a whole-class explicit
instantiation - not on a variable template, nor on the instantiation of a
lone static data member - and exporting a class wholesale also decorates its
member functions and nested types, which the state-holding class itself could
not tolerate. A one-member wrapper class with no functions is the one form
that can be exported and imported on every platform.
Registrars are consolidated, not shared
The export/import mechanism is deliberately limited to that single state
variable. No attempt is made to share the registrars - the static objects
generated by BOOST_OPENMETHOD, BOOST_OPENMETHOD_OVERRIDE and
BOOST_OPENMETHOD_CLASSES - across modules. Each module keeps its own copies
of the method objects, of the registration records, and of the per-class
v-table pointers used for dispatch.
Instead, the copies are consolidated at runtime. When a module is loaded, its
static constructors append its class and method records to the lists in the
shared registry state, each record pointing back into the module that
registered it. When initialize runs, records that describe the same method
or class - typically, one per module, keyed by type - are grouped together.
The dispatch tables are computed once, from the consolidated information; then
the resulting slots and strides are copied back into every module’s copy of
each method object, and every module’s v-table pointers are updated through
the pointers captured at registration time. Each module thus dispatches
through its own statics, at full speed, but all the copies agree because they
were derived from the same computation.
This is why method declarations need no dllexport/dllimport arguments, and
why modules do not need to agree on who "owns" a method: ownership only exists
for the registry state.
Platform Details
On Windows, by default, each module (executable or DLL) receives its own copy
of global variables. Without special measures, the registry state, dispatch
tables, and method function pointers are duplicated: when a DLL’s static
constructors register classes and overriders, they populate the DLL’s own copy
of the registry, invisible to the main program. The EXPORT and IMPORT
macros expand to __declspec(dllexport) and __declspec(dllimport)
decorations on an explicit instantiation of the registry state, making the
owner’s copy the only one.
On Linux and other ELF platforms, the macros can sometimes be omitted. Without
them, the registry state is an implicitly instantiated template variable with
ordinary external linkage, and the dynamic linker collapses all the modules'
copies into one - provided the symbol is visible, which is what -rdynamic
(ENABLE_EXPORTS ON in CMake) ensures for an executable. However, if the
program is built with -fvisibility=hidden, each module’s copy is internalized
to a module-local symbol, and the modules end up with separate registries, just
like on Windows. The macros handle this case: EXPORT emits an explicit
instantiation with default visibility; IMPORT emits a plain extern template
declaration, which suppresses the client’s own instantiation, forcing it to
reference the owner’s symbol. Since the macros are harmless when they are not
needed, portable code should simply always use them.
The platforms also differ in when the imported symbol must be resolved. On ELF, a shared library may be linked with unresolved references, which the dynamic linker satisfies at load time; a plugin can thus import the registry state from the executable that will load it. Mach-O (macOS) and PE (Windows) require every reference to be satisfied at link time: a client library must link against the owning module - hence the reverse link in the CMake setup above.
on Windows, every module that shares a registry must be linked
against the same dynamic C++ runtime (the /MD or /MDd family). The
registry state contains standard containers (std::vector, std::function,
etc.) allocated by whichever module runs initialize; if a module links the
static runtime (/MT) instead, or links a different runtime version, that
module’s CRT frees memory it did not allocate when the state is destroyed at
exit, corrupting the heap.
|