Search code examples
c++dllbinary-compatibility

Is a struct full of function pointers a good solution for C++ binary compatibility?


I have a library written in C++ that I need to turn into a DLL. This library should be able to be modified and recompiled with different compilers and still work.

I have read that it's very unlikely that I will achieve full binary compatibility between compilers/version if I export all my classes directly using __declspec(dllexport).

I have also read that pure virtual interfaces can be pulled from the DLL to remove the issue of name mangling by simply passing a table full of function pointers. However, I have read that even this can fail, because some compilers may even change the order of the functions in the vtable between successive releases.

So finally, I figured I could just implement my own vtable, and this is where I am at:

Test.h

#pragma once
#include <iostream>
using namespace std;

class TestItf;
extern "C" __declspec(dllexport) TestItf* __cdecl CreateTest();

class TestItf {
public:
    static TestItf* Create() {
        return CreateTest();
    }
    void Destroy() {
        (this->*vptr->Destroy)();
    }
    void Print(const char *something) {
        (this->*vptr->Print)(something);
    }
    ~TestItf() {
        cout << "TestItf dtor" << endl;
    }
    typedef void(TestItf::*pfnDestroy)();
    typedef void(TestItf::*pfnPrint)(const char *something);

    struct vtable {
        pfnDestroy Destroy;
        pfnPrint Print;
    };    
protected:
    const vtable *const vptr;
    TestItf(vtable *vptr) : vptr(vptr){}
};

extern "C"__declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable);

Test.cpp

#include "Test.h"

class TestImp : public TestItf {
public:
    static TestItf::vtable TestImp_vptr;
    TestImp() : TestItf(&TestImp_vptr) {

    }
    ~TestImp() {
        cout << "TestImp dtor" << endl;
    }
    void Destroy() {
        delete this;
    }
    void Print(const char *something) {
        cout << something << endl;
    }
};

TestItf::vtable TestImp::TestImp_vptr =  {
    (TestItf::pfnDestroy)&TestImp::Destroy,
    (TestItf::pfnPrint)&TestImp::Print,
};

extern "C" {
    __declspec(dllexport) void __cdecl GetTestVTable(TestItf::vtable *vtable) {
        memcpy(vtable, &TestImp::TestImp_vptr, sizeof(TestItf::vtable));
    }
    __declspec(dllexport) TestItf* __cdecl CreateTest() {
        return new TestImp;
    }
}

main.cpp

int main(int argc, char *argv[])
{
    TestItf *itf = TestItf::Create();
    itf->Print("Hello World!");
    itf->Destroy();

    return 0;
}

Were my above assumptions correct about not being able to achieve proper compatibility with the first two methods?

Is my 3rd solution portable and safe?

-Specifically, I am worried about the effect of using the casted function pointers from TestImp on the base type TestItf. It does seem to work in this simple test case, but I imagine things like alignment or varying object layout might make this unsafe in some cases.

Edit
This method can also be used with C#. A few minor modifications have been made to the above code.

Test.cs

struct TestItf {
    [StructLayout(LayoutKind.Sequential, CharSet = CharSet.Ansi)]
    public struct VTable {
        [UnmanagedFunctionPointer(CallingConvention.ThisCall)]
        public delegate void pfnDestroy(IntPtr itf);

        [UnmanagedFunctionPointer(CallingConvention.ThisCall, CharSet = CharSet.Ansi)]
        public delegate void pfnPrint(IntPtr itf, string something);

        [MarshalAs(UnmanagedType.FunctionPtr)]
        public pfnDestroy Destroy;

        [MarshalAs(UnmanagedType.FunctionPtr)]
        public pfnPrint Print;
    }

    [DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
    private static extern void GetTestVTable(out VTable vtable);

    [DllImport("cppInteropTest", CallingConvention = CallingConvention.Cdecl)]
    private static extern IntPtr CreateTest();

    private static VTable vptr;
    static TestItf() {
        vptr = new VTable();
        GetTestVTable(out vptr);
    }

    private IntPtr itf;
    private TestItf(IntPtr itf) {
        this.itf = itf;
    }

    public static TestItf Create() {
        return new TestItf( CreateTest() );
    }

    public void Destroy() {
        vptr.Destroy(itf);
        itf = IntPtr.Zero;
    }

    public void Print(string something) {
        vptr.Print(itf, something);
    }
}

Program.cs

static class Program
{
    [STAThread]
    static void Main()
    {
        TestItf test = TestItf.Create();
        test.Print("Hello World!");
        test.Destroy();
    }
}

Solution

  • No.

    Interoperability between languages in a convenient object-oriented way was a big part of my original motivation for exploring this idea.

    While the C# example used in the original question does work under windows, it fails on mac osx. The sizes of the vtables between C#/Mono and C++ do not match up due to different sizes of member function pointers. Mono expects a 4 byte function pointer, while the xcode/c++ compiler expects them to be 8 bytes.

    Apparently, member function pointers are more than just pointers. Sometimes, they can point to structures that contain extra data to deal with certain inheritance situations.

    Truncating the 8 byte member function pointers to 4 bytes and sending them to mono anyways actually works. This is probably because I am using a POD class type. I wouldn't want to rely on a hack like this though.

    All things considered, the method used for interop suggested in the original question will be much more trouble than it's worth, and I've chosen to byte the bullet, and go with a C interface.