So one big question for a library is to decide if it will have any forwards/backwards compatibility. Also consider if a DLL actually gets you anything much over a static library. Choosing to use a DLL prevents many cases of inlining for example, that static libs / multiple c/cpp files will do ("link time code generation"), as well as potentially much bigger restrictions on the use of say templates (why bother having a DLL if most code must be in headers anyway?).
- In the simplest case, you say that the programs must be compiled against the exact version of the DLL used
- Otherwise you might say it must be the same toolchain, settings, etc., so using C++ classes and standard library stuff across the boundary is OK (malloc/free, new/delete, FILE, std::string, etc.), but versions of the DLL are otherwise backwards compatible.
- Rely on some platform conventions outside the C++ standard. e.g. Microsoft basically say how vtable's / virtual functions, will work on Windows, be that MSVC, GCC, CLANG, whatever to use the “C++ style” COM interfaces rather than the C ones (and who uses those now?).
- Otherwise your basically limited to more of a C library, and make sure each of your objects has an exported malloc/free style function pair. C++ doesn't have a standard ABI even for basic classes, and certainly not standard library objects / allocators.
Any types in your public interface like that `getLuaState` you would need to create a “wrapper type” to contain it with all the methods you wish to expose.
You can generally forward declare any pointer/reference types generally as mentioned, but often you will need to know object sizes, for example because they appear within your own struct/class definitions, on the stack, used in an array, etc.
You can give up some more performance, turn those fields into pointers, or turn your object into a pointer using virtual functions or PIMPL (point to implementation) to hide those details:
struct ExternalThing;
class FOO_EXPORT FooA
{
public:
void hello_world();
private:
ExternalThing x; // ERROR, forward declaration is not enough!
};
// make the problem field a pointer
class FooB
{
public:
~FooB(); // Don't forget the destructor, copy constructor, etc. as well as the default needs the full declaration
void hello_world();
private:
// NOTE: Any changes here will break compatibility if any program needs to know sizeof(FooB) (arrays,
// stack/auto object storage, new/delete, etc.), but is OK in case 1. once you recompile!
// If you don't recompile, you won't get a load error (like a missing DLL or symbol), but risk memory corruption.
std::unique_ptr<ExternalThing> x; // OK, size of pointer is always known
int y;
float z;
};
// Make all your private stuff a pointer
class FOO_EXPORT FooC // PIMPL
{
public:
FooC();
~FooC();
void hello_world();
private: // NOTE: Since this is just a pointer, Impl can change and maintain compatibility, working for case 2.
struct Impl;
std::unique_ptr<Impl> impl;
};
// in some cpp file
struct FooC::Impl // OK to make any changes to this and be backwards compatible!
{
ExternalThing x;
int y;
float z;
};
FooC::FooC() : impl(std::make_unique<Impl>()) {}; // extra cost of allocating somehow
void FooC::hello_world()
{
std::cout << impl->x << std::endl; // and extra level of indirection
}
class IFooC // interface
{
public:
// NOTE, be very careful with virtual function changes, they will break the vtable and not cause a load error like a missing symbol would.
// For example in COM you will see them make a IFooC2 or such, even if just adding a function in that version onwards.
virtual ~IFooC() {}
virtual void hello_world() = 0;
};
FOO_EXPORT IFooC *create_foo_c();
// in some cpp file
class FooC : public IFooC { ... }; // OK to make any chane to this and not recompile
FOO_EXPORT IFooC *create_foo_c()
{
return new FooC();
}
You can also just leave enough space for the private field manually, but I wouldn't really recommend it and be wary if the object size might change (32 vs 64bit, debug vs release, just new versions even). You could also possibly do similar hacks using `#ifdef` macros within the class declaration, but that is certainly getting pretty risky.
class FooD // put on the stack, arrays, anywhere! no extra pointers!
{
public:
FooD();
~FooD();
void hello_world();
private:
std::aligned_storage<64, 8>::type x;
};
// in cpp
FooD::FooD()
{
static_assert(sizeof(ExternalThing) == sizeof(x));
static_assert(alignof(ExternalThing) == alignof(x));
new(&amp;amp;x) ExternalThing("Hello World!"); // be wary of exception safety!
}
FooD::~FooD()
{
reinterpret_cast<ExternalThing*>(&amp;amp;x)->~ExternalThing(); // manually call the destructor
};
void FooD::hello_world()
{
std::cout << reinterpret_cast<ExternalThing&amp;amp;>(&amp;amp;x) << std::endl; // cast to the actual type as needed
};