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
and after the registry class is defined - they expand to
registry_state<REGISTRY::registry_type>, which needs a complete type.
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. It
also registers the library’s classes a second time - see below:
// 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_CLASSES(Animal, Herbivore, Cow, Carnivore, Wolf);
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.
The program registers Animal, Herbivore, Cow, Carnivore and Wolf,
although the library has registered them already. That is the rule
use_classes states: a class must be registered in as many translation
units as it has type ids. On ELF and Mach-O it has one - the dynamic linker
merges the type_info objects, and the second registration is redundant but
harmless. Windows keeps one type_info per module, so the program’s Wolf and
the library’s Wolf are two different type ids, and the program creates its
Wolf with new. Without the second registration the program’s id is unknown
to the registry, and the first dispatch on it fails with unknown class Wolf.
Registering in both modules is what makes the example portable, and costs
nothing where it is not needed.
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.
int main() {
// ...
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
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.
|
Type Ids Across Modules
Everything above is about sharing the registry’s state. There is a second,
quieter question: where the type ids themselves come from, and how far apart
they end up. It decides how well the registry’s type_hash policy can do
its job, and it is the one place where dlopen behaves differently from
ordinary linking.
Under std_rtti, a type id is &typeid(X) - the address of a
std::type_info object. The Itanium ABI requires that identity to be pointer
identity across modules, and the linker delivers it for an implicitly linked
shared library with a copy relocation: the record is copied into the
executable’s image, and the library’s references are redirected to that copy.
So a program and the libraries it links against present one compact set of type
ids, however many modules there are.
dlopen does not get that. A plugin’s own classes are not named by the
executable, so nothing unifies them; their records stay in the plugin’s own
mapping, which the loader places wherever it likes - and with address-space
randomization, somewhere different on every run. The distance between a
program’s type ids and its plugin’s is routinely measured in terabytes, and it
moves from run to run.
RTTI has to keep default visibility for ids to unify at all. Under
-fvisibility=hidden one class can end up with a different type_info object
in each module; initialize copes - it treats them as several ids for the
same class - but they are extra ids for the hash to separate. This is why the
library’s own shared-library tests mark their classes BOOST_SYMBOL_VISIBLE.
|
What it costs
fast_perfect_hash, the default, searches for a multiplier M and a shift
S such that (M * x) >> S is collision-free over the registered type ids. It
is fast and compact when the ids are evenly spread, and degrades when they are
not - and a program plus a few dlopened modules is as uneven as it
gets: several tight clusters, very far apart. Two things follow:
-
the search gets dramatically more expensive, and on a large enough set it fails - it gives up after half a million attempts and the error handler is called with a
search_error, which by default terminates the program; -
vptr_vectorsizes its table from the hash’s range, so a hash that is working hard costs memory as well as time.
A program that loads plugins and registers more than a few hundred classes is the one most likely to meet both.
The alternatives
Three other type_hash policies trade that away, and a fourth option
removes the hash from the picture entirely. They are all drop-in: with
replaces a policy with the one of the same category, in place, so the new hash
still precedes vptr_vector in the list.
| policy | what it does |
|---|---|
One slot per type id, whatever the addresses are, and a search whose cost depends only on how many classes there are. The table size can be stated before seeing an address. Costs a second dependent load on every dispatch - a nanosecond or two per call. The one to reach for in a plugin host. |
|
The same idea with the final reduction replaced by a shift. Cheaper per call
than |
|
Indexes by the smallest set of bit positions that still separates the type ids. As fast per call as the default, and it finds its table deterministically in milliseconds. Needs BMI2, for every translation unit of the program - see its documentation before choosing it. |
|
Not a hash at all: a map keyed on the type id, so there is no table to size and no search to fail. Slower per dispatch than any of the above, and the only option that asks nothing of the type ids. |
Switching is one declaration. The registry is then a custom registry, so it needs the treatment in Custom Registries to be shared across modules:
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::minimal_perfect_hash<>> {};
vptr_map replaces the vptr policy rather than the hash, and the hash is then
dead weight, so drop it:
struct plugin_registry :
boost::openmethod::default_registry::with<
boost::openmethod::policies::vptr_map<>>::without<
boost::openmethod::policies::type_hash> {};
| none of this arises until a module registers classes of its own. A plugin that only adds overriders for classes the program already registered contributes no new type ids, and the default policies are as good there as anywhere. |
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. The library 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/core.hpp>, or any header that includes
it, like <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 that of
default_registry, 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
#ifndef MY_REGISTRY_DEFINED
#define MY_REGISTRY_DEFINED
struct my_registry;
#define BOOST_OPENMETHOD_DEFAULT_REGISTRY my_registry
#include <boost/openmethod.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
#endif
The #define makes my_registry the registry the BOOST_OPENMETHOD macros
use, exactly as indirect_vptr/animals.hpp does above; drop it if the methods
name their registry explicitly. The sharing macros come last, after the
definition.
// 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.
|