2

I'm writing a proxy class that loads a shared library with dlopen() and forwards its member functions to the appropriate members of the proxied class instance inside the loaded shared object behind the scenes.

For example, the shared object has a Person class:

class Person
{
    ...
    void setName(std::string name);
};

I've added a wrapper file that includes the person.h header and defines the following symbol:

extern "C" {
    void Person_setName(void* instancePointer, std::string name);
}

This call just forwards to the person object which is the first argument, extern "C" to avoid the whole name mangling issue. On the client side I've written a Person class with the same members that holds a pointer to the wrapped class and forwards all calls.

Now some question arise:

  1. is there a better way to instantiate and use a class from a loaded shared object? I've found some other solution that can live without the wrappers, but it's discouraged on Usenet and highly dependent on GCC and its version, and undefined behavior, so I decided against that route.
  2. is there a way to automate the creation of those wrappers? They are all just adding a first argument that is a pointer to an instance to each method and forward to the real thing. There has to be some template magic for that, no? Currently I'm using some macros for the job but I'd be interested in a template solution.
  3. Maybe there is some tool that can do such a thing automatically, like SWIG? As far as I've seen SWIG is just for exporting a C++ interface to high level languages.
hochl
  • 12,524
  • 10
  • 53
  • 87
  • I tend to prefer to return objects directly from the shared library. [Are clang++ and g++ ABI compatible?](https://stackoverflow.com/a/32444174/7582247) – Ted Lyngmo Jul 08 '19 at 10:30
  • that is the solution i mentioned under `undefined behavior`. I've resolved mangled symbols and used a cast to `void (*setName)(Person*, std::string)`, but that seems to be discouraged. If you have a link to code that uses this route i'd still be interested. – hochl Jul 08 '19 at 10:38
  • If you return an instance of `Person` directly from the shared lib you wouln't need C wrappers and casting. – Ted Lyngmo Jul 08 '19 at 10:47
  • how will the client program that uses `dlopen()` to open the shared object find the correct symbol if it calls `setName` on such a returned instance? – hochl Jul 08 '19 at 10:51
  • 1
    I made an example to show how. – Ted Lyngmo Jul 10 '19 at 12:59

2 Answers2

2

is there a better way to instantiate and use a class from a loaded shared object?

If you want to be safe and support any shared object (i.e. compiled by any compiler/flags etc.), then no: you have to go through the C ABI.

Remember you should not use C++ objects in the interface either, e.g. like the std::string you are passing in Person_setName.

is there a way to automate the creation of those wrappers? There has to be some template magic for that, no? Currently I'm using some macros for the job but I'd be interested in a template solution.

No, you cannot create member functions on the fly (we do not have reflection, metaclasses and similar compile-time features yet).

They are all just adding a first argument that is a pointer to an instance to each method and forward to the real thing.

You can create a variadic template that forwards arguments to a given extern "C" function, of course, but you are not really getting anything useful from that compared to simply calling the C functions. In other words, don't do that: either create a proper C++ class or leave the users to call the C functions.

Maybe there is some tool that can do such a thing automatically, like SWIG? As far as I've seen SWIG is just for exporting a C++ interface to high level languages.

I have used Clang's tooling support in the past to perform similar tasks as a pre-build step, so it is indeed possible!

Acorn
  • 24,970
  • 5
  • 40
  • 69
  • can you provide some pointer on how you used Clang? I've seen SWIG exports XML so that could be used for a custom tool that generates the wrappers. The thing is there are possibly several rather large classes that need to get wrapped, but get changed by other developers constantly, so keeping the wraps up-to-date will mean a lot of headaches. – hochl Jul 08 '19 at 10:44
  • Done, I have put a link in the answer :-) Yes, I actually had that same situation in one project where external developers changed code at any time. We enforced a pre-build step on all of them, which took care of it. – Acorn Jul 08 '19 at 10:48
2

The original question has been answered, but there was this question in the comments that I think needs an answer too.

how will the client program that uses dlopen() to open the shared object find the correct symbol if it calls setName on such a returned instance?

You make the methods virtual. Here's an example in which libfoobar.so is created. It provides one factory function (make_Foo) to create a Foo. A Bar can be created from the Foo instance. All methods can be used via the instance pointers. I've mixed using raw pointers and unique_ptrs to show some options.

First, foobar.hpp and foobar.cpp that will be put into libfoobar.so:

foobar.hpp

#pragma once

#include <string>
#include <memory>

class Bar;

class Foo {
public:
    Foo();
    virtual ~Foo();

    virtual void set_name(const std::string& name);
    virtual std::string const& get_name() const;

    virtual Bar* get_Bar() const;

private:
    std::string m_name;
};

class Bar {
public:
    Bar(const Foo&);
    virtual ~Bar();

    virtual std::string const& get_value() const;
private:
    std::string m_value;
};

// a Foo factory
extern "C" {
std::unique_ptr<Foo> make_Foo();
}

foobar.cpp

#include "foobar.hpp"

// You can also use the library constructor and destructor
// void __attribute__((constructor)) init(void) {}
// void __attribute__((destructor)) finalize(void) {}

// Foo - impl

Foo::Foo() : m_name{} {}

Foo::~Foo() {}

void Foo::set_name(const std::string& name) {
    m_name = name;
}

std::string const& Foo::get_name() const {
    return m_name;
}

Bar* Foo::get_Bar() const {
    return new Bar(*this);
}

// Bar - impl

Bar::Bar(const Foo& f) :  m_value(f.get_name()) {}
Bar::~Bar() {}

std::string const& Bar::get_value() const { return m_value; }

// a factory function that can be loaded with dlsym()
extern "C" {
std::unique_ptr<Foo> make_Foo() {
    return std::make_unique<Foo>();
}
}

Then a generic dynamic library helper:

dynlib.hpp

#pragma once

#include <dlfcn.h> // dlload, dlsym, dlclose
#include <stdexcept>

using dynlib_error = std::runtime_error;

class dynlib {
public:
    dynlib(const char* filename);
    dynlib(const dynlib&) = delete;
    dynlib(dynlib&&);
    dynlib& operator=(const dynlib&) = delete;
    dynlib& operator=(dynlib&&);
    virtual ~dynlib();

protected:
    template<typename T>
    T load(const char* symbol) const {
        static_cast<void>(dlerror()); // clear errors
        return reinterpret_cast<T>(dlsym(handle, symbol));
    }

private:
    void* handle;
};

dynlib.cpp

#include "dynlib.hpp"
#include <utility>

dynlib::dynlib(const char* filename) : handle(dlopen(filename, RTLD_NOW | RTLD_LOCAL)) {
    if(handle == nullptr) throw dynlib_error(std::string(dlerror()));
}

dynlib::dynlib(dynlib&& o) : handle(std::exchange(o.handle, nullptr)) {}

dynlib& dynlib::operator=(dynlib&& o) {
    if(handle) dlclose(handle);
    handle = std::exchange(o.handle, nullptr);
    return *this;
}

dynlib::~dynlib() {
    if(handle) dlclose(handle);
}

And a dynlib descendant to load libfoobar.so:

foobarloader.hpp

#pragma once

#include "foobar.hpp"
#include "dynlib.hpp"

#include <memory>

class foobarloader : public dynlib {
public:
    using foo_t = std::unique_ptr<Foo> (*)();
    const foo_t make_Foo; // a factory function to load
                          // add more if needed

    foobarloader();
};

foobarloader.cpp

#include "foobarloader.hpp"

foobarloader::foobarloader() :
    dynlib("./libfoobar.so"),
    make_Foo(load<foo_t>("make_Foo")) // load function
{
    if(make_Foo == NULL) throw dynlib_error(std::string(dlerror()));
}

And finally an application that is not linked with libfoobar.so in any way:

test_app.cpp

#include "foobarloader.hpp"

#include <iostream>
#include <stdexcept>
#include <utility>
#include <memory>

int main() {
    try {
        foobarloader fbl;

        auto f = fbl.make_Foo(); // std::unique_ptr<Foo> example
        f->set_name("Howdy");
        std::cout << "Foo name: " << f->get_name() << "\n";

        Bar* b = f->get_Bar();   // raw Bar* example
        std::cout << "Bar value: " << b->get_value() << "\n";
        delete b;

    } catch(const std::exception& ex) {
        std::clog << "Exception: " << ex.what() << "\n";
        return 1;
    }
}

building

If you use clang++ add -Wno-return-type-c-linkage to suppress warnings about the factory method. I've used -DNDEBUG -std=c++14 -O3 -Wall -Wextra -Wshadow -Weffc++ -pedantic -pedantic-errors but excluded it below for brevity.

g++ -fPIC -c -o foobar.o foobar.cpp
g++ -shared -o libfoobar.so foobar.o

g++ -c -o dynlib.o dynlib.cpp
g++ -c -o foobarloader.o foobarloader.cpp
g++ -c -o test_app.o test_app.cpp

g++ -rdynamic -o test_app test_app.o foobarloader.o dynlib.o -ldl

No libfoobar.so linkage:

% ldd test_app
    linux-vdso.so.1 (0x00007ffcee58c000)
    libdl.so.2 => /lib64/libdl.so.2 (0x00007fb264cb3000)
    libstdc++.so.6 => /lib64/libstdc++.so.6 (0x00007fb264aba000)
    libm.so.6 => /lib64/libm.so.6 (0x00007fb264974000)
    libgcc_s.so.1 => /lib64/libgcc_s.so.1 (0x00007fb26495a000)
    libc.so.6 => /lib64/libc.so.6 (0x00007fb264794000)
    /lib64/ld-linux-x86-64.so.2 (0x00007fb264cfd000)

But class member functions work as expected:

% ./test_app
Foo name: Howdy
Bar value: Howdy
Ted Lyngmo
  • 93,841
  • 5
  • 60
  • 108
  • that's kind of a cool solution, a pity i can only upvote once just for the effort you invested ... `You make the methods virtual.` could you explain why the `virtual` methods help with loading in this scenario? – hochl Jul 11 '19 at 11:40
  • @hochl Thanks! Well, I'm not an expert on the subject but using `virtual` for [late binding](https://en.wikipedia.org/wiki/Late_binding) makes the linker "happy" with the promise that there "will be" a binding later. I'm not sure my own interpretation is correct actually, but that's how I've used it in both Linux and Windows :-) – Ted Lyngmo Jul 11 '19 at 12:05
  • I've changed my implementation accordingly and it seems to work. Now I would accept both answers but unfortunately that's not possible ;-) – hochl Jul 11 '19 at 12:11
  • @hochl That's fine :-) There are somethings to consider: It's pretty fragile as-is. In my example, the `.so` is doing `new` - but it's the application side that does `delete`. The `unique_ptr` returned from the `.so` must also match the one you use when compiling your application. When building a DLL in Windows (where I wasn't sure what toolchain would use it) I overloaded `operator delete` for the classes in the interface (`#ifndef BUILDING_LIB`) which deferred `delete` to a `Destroy` member function compiled into the `.so` to assure that the correct `delete` would be used. – Ted Lyngmo Jul 11 '19 at 12:37
  • Found the link where Remy helped me to set that up: https://newsgroups.embarcadero.com/thread.jspa?threadID=172448&tstart=465 – Ted Lyngmo Jul 11 '19 at 12:39
  • Ah ok. I assumed that the delete is found with late binding because the destructor is virtual also? – hochl Jul 11 '19 at 12:43
  • 1
    The destructor function will be found dynamically, but not the actual `free` used to release the memory. If there's a difference between the one used by the `.so` and the one used by the application side, it could get ugly. – Ted Lyngmo Jul 11 '19 at 12:45