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

Static Linking

Statically linking to a shared library behaves the same way as linking to a static library on all platforms. Objects from the program and the shared libraries can contribute classes, methods and overriders to the default registry, or any registries.

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.

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. These variables must be truly global and unique for the library to operate correctly.

Under this condition, a shared library can dynamically 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.

Dynamic linking on Linux and POSIX-like platforms fulfills the unicity requirement with little effort from the programmer. On Linux, it is just a matter of tossing in -rdynamic, or adding ENABLE_EXPORTS ON to the library’s target properties if using cmake.

If a library only uses its own registries, for example, if using open-methods as an implementation detail, it has its own global data, and there is no need to call initialize.

Let’s look at an example. The following header is included by the program and the shared library:

// animals.hpp

#include <string>
#include <boost/openmethod.hpp>

struct Animal { virtual ~Animal() {} };
struct Herbivore : Animal {};
struct Carnivore : Animal {};

BOOST_OPENMETHOD(
    meet, (
        boost::openmethod::virtual_ptr<Animal>,
        boost::openmethod::virtual_ptr<Animal>),
    std::string);

The shared library contains an object that adds two overriders, a new class, Tiger, and a factory function:

// extensions.cpp
#include "animals.hpp"

using namespace boost::openmethod::aliases;

BOOST_OPENMETHOD_OVERRIDE(
    meet, (virtual_ptr<Herbivore>, virtual_ptr<Carnivore>), std::string) {
    return "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" {
#ifdef _WIN32
__declspec(dllexport)
#endif
auto make_tiger() -> Animal* {
    return new Tiger;
}
}

The main program adds a couple of classes then calls meet method. At this point, we only have the catch-call overrider:

// dynamic_main.cpp

#include "animals.hpp"

#include <boost/openmethod.hpp>
#include <boost/openmethod/initialize.hpp>
#include <boost/dll/shared_library.hpp>
#include <boost/dll/runtime_symbol_info.hpp>
#include <iostream>
#include <memory>

using namespace boost::openmethod::aliases;

struct Cow : Herbivore {};
struct Wolf : Carnivore {};

BOOST_OPENMETHOD_CLASSES(Animal, Herbivore, Cow, Wolf, Carnivore);

BOOST_OPENMETHOD_OVERRIDE(
    meet, (virtual_ptr<Animal>, virtual_ptr<Animal>), std::string) {
    return "greet";
}

int main() {
    std::cout << "Before loading the shared library.\n";

    boost::openmethod::initialize();

    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...

    return 0;
}

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 << "\nAfter loading the shared library.\n";

    boost::dll::shared_library lib(
        boost::dll::program_location().parent_path() / LIBRARY_NAME,
        boost::dll::load_mode::rtld_now);
    boost::openmethod::initialize();

    std::cout << "cow meets wolf -> "
              << meet(*std::make_unique<Cow>(), *std::make_unique<Wolf>())
              << "\n"; // 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"; // hunt

    return 0;
}

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();

    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

    return 0;
}

Windows

If we try the example on Windows, the result is disappointing:

Before loading the shared library.
cow meets wolf -> greet
wolf meets cow -> greet

After loading the shared library.
cow meets wolf -> greet
wolf meets cow -> greet
cow meets tiger -> unknown class struct Tiger

What happens here is that the program and the DLL have their own copies of "global" variables. When the DLL is loaded, its static constructors run, and they add overriders to their copy of the method (the method::fn static variable for the given name and signature). They are ignored when the main program calls initialize.

Likewise, BOOST_OPENMETHOD_CLASSES(Tiger, Carnivore) in the DLL adds Tiger to the DLL’s copy of the registry. For the perspective of the program’s registry, the class does not exist.

In theory, this can be fixed by adding declspec(dllimport) and declspec(dllexport) attributes where needed. However, this is not practical, because programs and DLLs can both import and export registries and methods. The underlying objects are instantiated from templates, which complicates the matter. Research is being done on this subject. However, as of now, dynamic loading is supported on Windows only if it does not attempt to share a registry across modules.

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. We can use it to override the default registry, for example using a compiler command-line switch (-DBOOST_OPENMETHOD_DEFAULT_REGISTRY=indirect_registry).

Here is an example of a program that carries virtual_ptrs across initialize calls:

// indirect_main.cpp

#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 <boost/dll/runtime_symbol_info.hpp>
#include <iostream>

using namespace boost::openmethod::aliases;

struct Cow : Herbivore {};
struct Wolf : Carnivore {};

BOOST_OPENMETHOD_CLASSES(Animal, Herbivore, Cow, Wolf, Carnivore);

BOOST_OPENMETHOD_OVERRIDE(
    meet, (virtual_ptr<Animal>, virtual_ptr<Animal>), std::string) {
    return "greet";
}

auto main() -> int {
    using namespace boost::openmethod::aliases;

    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 << "cow.vptr() = " << gracie.vptr() << "\n"; // 0x5d3121d22be8
    std::cout << "wolf.vptr() = " << willy.vptr() << "\n"; // 0x5d3121d22bd8

    std::cout << "\nAfter loading the shared library.\n";

    boost::dll::shared_library lib(
        boost::dll::program_location().parent_path() / LIBRARY_NAME,
        boost::dll::load_mode::rtld_now);

    boost::openmethod::initialize();

    std::cout << "cow meets wolf -> " << meet(*gracie, *willy) << "\n"; // run
    std::cout << "wolf meets cow -> " << meet(*willy, *gracie) << "\n"; // hunt
    std::cout << "cow.vptr() = " << gracie.vptr() << "\n"; // 0x5d3121d21998
    std::cout << "wolf.vptr() = " << willy.vptr() << "\n"; // 0x5d3121d21988

    return 0;
}

This program loads a shared library that is itself compiled with -DBOOST_OPENMETHOD_DEFAULT_REGISTRY=indirect_registry.