I believe MS breaks their C++ ABI with each major release of MSVC. I'm unsure about their minor releases. That said, it seems that if you release a binary build of your dll to the public, you would need to release several builds - one build for each major release of MSVC you wish to support. If a new minor release of MSVC comes out after you've distributed your library, can people safely use your library if their app is built with the new version of MSVC?
Wikipedia shows a table of MSVC versions https://en.wikipedia.org/wiki/Microsoft_Visual_C%2B%2B#cite_note-43
From _MSC_VER it appears that Visual Studio 2015 and Visual Studio 2017 have the same major version, 19, for the compiler. So a DLL built with Visual Studio 2015 should work with an application built with Visual Studio 2017, correct?
The main thing that changes across compiler releases is the C / C++ runtime. So, for example, passing a stream
or a FILE *
across your API is likely to cause trouble, so don't do that. Likewise, don't free
memory in your application that was allocated within your DLL and don't delete in the app an object that was instantiated in the DLL. Or vice-versa.
The other things that might change are the order of / alignment of / total size of the member variables in an object, the name mangling scheme used by different versions of the compiler, or the layout of the vtable(s) in the object (and perhaps the location of those vtables withing the object, especially when using multiple or virtual inheritance).
There is some light at the end of the tunnel though. If you are prepared to wrap a C++ class that you want to export across your API in something that looks in essense like a COM object then you can insure yourself against all of these issues. This is because Microsoft have effectively promised not to change the vtable layout for such an object because, if they did, COM would break.
This does impose some limitations on how such 'COM-like' objects can be used but I'll come to that in a minute. The good news is that you can avoid most of the heavy lifting that implementing a full-blown COM object involves by cherry-picking just the best bits. By way of example, you can do something like the following.
Firstly, a generic, public, abstract class which lets us provide a custom deleter for std::unique_ptr and std::shared_ptr:
// Generic public class
class GenericPublicClass
{
public:
// pseudo-destructor
virtual void Destroy () = 0;
protected:
// Protected, virtual destructor
virtual ~GenericPublicClass () { }
};
// Custom deleter for std::unique_ptr and std::shared_ptr
typedef void (* GPCDeleterFP) (GenericPublicClass *);
void GPCDeleter (GenericPublicClass *obj)
{
obj->Destroy ();
};
Now the public header file for a class (MyPublicClass
) to be exported by the DLL:
// Demo public class - interface
class MyPublicClass;
extern "C" MyPublicClass *MyPublicClass_Create (int initial_x);
class MyPublicClass : public GenericPublicClass
{
public:
virtual int Get_x () = 0;
// ...
private:
friend MyPublicClass *MyPublicClass_Create (int initial_x);
friend class MyPublicClassImplementation;
MyPublicClass () { }
~MyPublicClass () = 0 { }
};
Next, the implementation of MyPublicClass
, which is private to the DLL:
#include "stdio.h"
// Demo public class - implementation
class MyPublicClassImplementation : public MyPublicClass
{
public:
// Constructor
MyPublicClassImplementation (int initial_x)
{
m_x = initial_x;
}
// Destructor
~MyPublicClassImplementation ()
{
printf ("Destructor called\n");
// ...
}
// MyPublicClass pseudo-destructor
void Destroy () override
{
delete this;
}
// MyPublicClass public methods
int Get_x () override
{
return m_x;
}
// ...
protected:
// ...
private:
int m_x;
// ...
};
And finally, a simple test program:
#include "stdio.h"
#include <memory>
int main ()
{
std::unique_ptr <MyPublicClass, GPCDeleterFP> p1 (MyPublicClass_Create (42), GPCDeleter);
int x1 = p1->Get_x ();
printf ("%d\n", x1);
std::shared_ptr <MyPublicClass> p2 (MyPublicClass_Create (84), GPCDeleter);
int x2= p2->Get_x ();
printf ("%d\n", x2);
}
Output:
42
84
Destructor called
Destructor called
Things to note:
MyPublicClass
are declared private
, because they are off-limits to users of the DLL. This ensures that new and delete use the same version of the runtime library (i.e. the one used by the DLL).MyPublicClass
are instead created via factory function Create_MyPublicClass
. This is declared extern "C"
to avoid name-mangling issues.MyPublicClass
are declared virtual
, again to avoid name-mangling issues. MyPublicClassImplementation
can do whatever it likes, of course.MyPublicClass
has no data members. It can have (if they are declared private) but it doesn't need to.The costs of doing this are:
virtual
, and from forwarding them to the underlying implementation (if that's what you end up doing). For me, that would be the least of my worries.On the plus side:
Only you can judge if this approach is worth the effort. LMK.
Edit: I thought this over while clearing out some brambles and realised that it needs to work with std::unique_ptr and std::shared_ptr to be useful. It can also be improved by making the public class abstract (as COM does) and then implementing all the functionality in a derived class inside the DLL, as this gives you more flexibility when implementing the class. I have therefore reworked the code above to include these changes and changed the names of a few things to make the intention clearer. Hope it helps someone.