OpenMethod interoperates with shared libraries on Linux, other POSIX-like platforms, and Windows.

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

BOOST_OPENMETHOD_IMPORT_REGISTRY

header; every translation unit of a client module

BOOST_OPENMETHOD_EXPORT_REGISTRY

header; every translation unit of the owning module

BOOST_OPENMETHOD_INSTANTIATE_REGISTRY

exactly one .cpp of the owning module

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) and extern are 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. EXPORT expands to nothing and INSTANTIATE carries the dllexport.

  • 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 INSTANTIATE carries 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.