Reputation: 57
I have a class with a virtual method foo
:
class SimpleClass {
public:
SimpleClass() = default;
virtual ~SimpleClass() {
std::cout << "in virtual destructor\n";
}
virtual int foo() {
std::cout << "in virtual method\n";
x = 42;
return x;
}
private:
int x;
};
I want to dynamically load it and create instances. Here's how I do it:
interfaces.h
:
#include <string>
class AbstractClass
{
friend class ClassLoader;
public:
explicit AbstractClass();
~AbstractClass();
protected:
void* newInstanceWithSize(size_t sizeofClass);
struct ClassImpl* pImpl;
};
template <class T>
class Class
: public AbstractClass
{
public:
T* newInstance()
{
size_t classSize = sizeof(T);
void* rawPtr = newInstanceWithSize(classSize);
return reinterpret_cast<T*>(rawPtr);
}
};
enum class ClassLoaderError {
NoError = 0,
FileNotFound,
LibraryLoadError,
NoClassInLibrary
};
class ClassLoader
{
public:
explicit ClassLoader();
AbstractClass* loadClass(const std::string &fullyQualifiedName);
ClassLoaderError lastError() const;
~ClassLoader();
private:
struct ClassLoaderImpl* pImpl;
};
#include <algorithm>
#include <cstdlib>
#include <cstring>
#include <new>
#include <string>
#include <dlfcn.h>
#include "interfaces.h"
typedef void* (*constructor_t)();
struct ClassImpl {
void* lib = nullptr;
std::string class_name;
};
AbstractClass::AbstractClass(): pImpl(new ClassImpl()) {}
AbstractClass::~AbstractClass() {
if (pImpl->lib) {
dlclose(pImpl->lib);
}
delete pImpl;
}
std::string CreateSymbolicName(const std::string& class_name) {
size_t pos = 0;
size_t new_pos = 0;
std::string sym_name = "_ZN";
while ((new_pos = class_name.find("::", pos)) != std::string::npos) {
sym_name += std::to_string(new_pos - pos);
sym_name += class_name.substr(pos, new_pos - pos);
pos = new_pos + 2;
}
sym_name += std::to_string(class_name.size() - pos);
sym_name += class_name.substr(pos);
sym_name += "C1Ev";
return sym_name;
}
void* AbstractClass::newInstanceWithSize(size_t size_of_class) {
std::string sym_name = CreateSymbolicName(pImpl->class_name);
constructor_t constructor =
reinterpret_cast<constructor_t>(dlsym(pImpl->lib, sym_name.c_str()));
void* obj = constructor();
void* memory = new char[size_of_class];
memcpy(memory, obj, size_of_class);
return memory;
}
struct AbstractClassList {
AbstractClass* abstract_class = nullptr;
AbstractClassList* next = nullptr;
};
struct ClassLoaderImpl {
AbstractClassList* head = nullptr;
AbstractClassList* cur = nullptr;
};
ClassLoader::ClassLoader(): pImpl(new ClassLoaderImpl()) {}
ClassLoader::~ClassLoader() {
while (!pImpl->head) {
pImpl->cur = pImpl->head;
pImpl->head = pImpl->head->next;
delete pImpl->cur;
}
delete pImpl;
}
std::string parsePath(const std::string& fully_qualified_name) {
std::string result = fully_qualified_name;
std::replace(result.begin(), result.end(), ':', '/');
return std::string(std::getenv("CLASSPATH")) + "/" + result + ".so";
}
AbstractClass* ClassLoader::loadClass(const std::string& fully_qualified_name) {
auto path = parsePath(fully_qualified_name);
void* lib = dlopen(path.c_str(), RTLD_NOW|RTLD_LOCAL);
if (!lib) {
return nullptr;
}
if (!pImpl->head) {
pImpl->head = new AbstractClassList();
pImpl->cur = pImpl->head;
} else {
pImpl->cur->next = new AbstractClassList();
pImpl->cur = pImpl->cur->next;
}
pImpl->cur->abstract_class = new AbstractClass();
pImpl->cur->abstract_class->pImpl->lib = lib;
pImpl->cur->abstract_class->pImpl->class_name = fully_qualified_name;
return pImpl->cur->abstract_class;
}
It works Ok until foo
is virtual. If foo
is virtual I receive segfault when try to call it.
What's wrong?
Upvotes: 0
Views: 323
Reputation: 67743
It's possible to load virtual subtypes dynamically, although there are a load of platform-specific details.
Sam's exactly right that none of this behaviour is defined by the standard, because of course dlopen
is a platform-specific extension. Name mangling and the location and linkage of RTTI symbols will also be platform-specific. That doesn't mean it can't be done, just that it's neither portable nor described in the language standard.
You should:
have an abstract base class for the things you want to create.
Your AbstractClass
seems to be a combination of the abstract base class for your objects, and an abstract factory. Don't confuse these things.
have a separate standard factory interface for returning pointers to this base.
This can just be an extern "C"
function pointer with a standard name, to avoid mangling. Obviously you can wrap this with a pretty object and map it to a string ID for ease of use, but keep the actual dynamic library interface as simple as possible.
use dlsym
to get this factory function pointer for each loaded library
make sure the base-class RTTI is either exported from the executable (typically by linking with -rdynamic
) or is emitted into a separate shared object which you explicitly load with RTLD_GLOBAL
Note that rustyx' comment:
The compiler generates RTTI bits with weak linkage, then the linker decides whether or not to include them in the program
doesn't really mean this is impossible. It does however mean it's up to you to figure out how to force your compiler & linker to produce the result you need.
If you get this bit wrong, you'll find that RTTI functionality like dynamic_cast
misbehaves in horrible ways. You can use objdump
or nm
to find out exactly where each symbol ended up.
Note also that just calling a factory function to return a pointer-to-abstract-base is fine, so long as every factory uses the same allocation scheme (ie, new
, without fancy allocators, placement new or overridden operator new
). If you do anything more complex, you need to support properly destroying these objects as well.
Upvotes: 1
Reputation: 118340
What's wrong is that this is undefined behavior, C++ simply doesn't work this way.
std::string sym_name = CreateSymbolicName(pImpl->class_name);
constructor_t constructor =
reinterpret_cast<constructor_t>(dlsym(pImpl->lib, sym_name.c_str()));
void* obj = constructor();
This appears to be looking up the mangled name for a class's constructor, and calling it as constructor
.
I'm almost positive that this is undefined behavior, there is no defined way, in the C++ standard, to invoke C++ constructors in this manner. Even if it's not, for a particular C++ implementation, this is undefined behavior anyway:
void* obj = constructor();
void* memory = new char[size_of_class];
memcpy(memory, obj, size_of_class);
memcpy
is only valid for PODs. It is undefined behavior for non-POD objects. An observed crash, when a class has a virtual method, is the obvious manifestation of undefined behavior.
There are several other problems here, but this is the main reason for the crash. The only defined mechanism for copying class instances is by using their copy constructor.
Upvotes: 0