问题
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:
- 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.
- 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.
- 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.
回答1:
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!
回答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 callssetName
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_ptr
s 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
来源:https://stackoverflow.com/questions/56932785/automatically-creating-wrappers-for-classes-loaded-with-dlopen