Search code examples
c++visual-c++dllshared-librariesabi

What is the best practice for supporting different ABIs with a shared library release?


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?


Solution

  • 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:

    • The constructor and destructor of 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).
    • Objects of class MyPublicClass are instead created via factory function Create_MyPublicClass. This is declared extern "C" to avoid name-mangling issues.
    • All public methods of 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:

    • You might have to do a lot of wrapping.
    • Applications using the DLL can't derive from classes exported by the DLL.
    • There will be some (minor) performance penalty from making all method calls 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.
    • You can't put these objects on the stack.

    On the plus side:

    • You can change your implementation in future releases in pretty much any way you like.
    • You can probably mix and match compiler vendors, if those compilers claim to support COM. Users of your DLL might like this.

    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.