Является ли структура, полная указателей на функции, хорошим решением для бинарной совместимости C++?
У меня есть библиотека, написанная на C++ , которую мне нужно превратить в DLL. Эта библиотека должна быть в состоянии быть изменена и перекомпилирована с различными компиляторами и все еще работать.
Я читал, что очень маловероятно, что я достигну полной бинарной совместимости между компиляторами / версиями, если я экспортирую все мои классы непосредственно с помощью _ _ declspec (dllexport).
Я также читал, что чистые виртуальные интерфейсы могут быть извлечены из DLL, чтобы устранить проблему искажения имен, просто передав таблица указателей на функции. Однако я читал, что даже это может не сработать, потому что некоторые компиляторы могут даже изменить порядок функций в vtable между последовательными выпусками.
Итак, наконец, я решил, что могу просто реализовать свою собственную vtable, и вот где я нахожусь:
Тест.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);
Тест.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;
    }
}
Главная.cpp
int main(int argc, char *argv[])
{
    TestItf *itf = TestItf::Create();
    itf->Print("Hello World!");
    itf->Destroy();
    return 0;
}
Были ли мои вышеописанные предположения правильными о невозможности достичь надлежащей совместимости с первыми двумя методы?
Является ли мое 3-е решение портативным и безопасным?
- в частности, меня беспокоит эффект использования приведенных указателей функций из TestImp для базового типа TestItf. Это действительно работает в этом простом тестовом случае, но я думаю, что такие вещи, как выравнивание или изменение расположения объектов, могут сделать это небезопасным в некоторых случаях.
Edit  
Этот метод также можно использовать с C#. Несколько незначительных изменений были внесены в вышеизложенное код.
Тест.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);
    }
}
Программа.cs
static class Program
{
    [STAThread]
    static void Main()
    {
        TestItf test = TestItf.Create();
        test.Print("Hello World!");
        test.Destroy();
    }
}
2 ответа:
Прежде всего: ваш деструктор TestItf должен быть виртуальным, потому что вы возвращаете тип потомка в качестве базового предка. Без виртуальности в некоторых компиляторах произойдет утечка памяти.
Теперь в соответствии с бинарной совместимостью. Существуют следующие общие подводные камни:
- Соглашения о вызовах. Если оба компилятора (ваш и клиентский) знают о выбранном вами соглашении о вызовах, это нормально (с тех пор простое бесклассовое соглашение stdcall, как и Win32 API, является проверенным решение для многих лет и нескольких языков, а не только C++)
- выравнивание структур. Упакуйте опубликованные структуры с 1-байтовым выравниванием-большинство компиляторов имеет соответствующие настройки через pragma или ключи компиляции.
Имея в виду эти два момента, вы будете играть безопасно в основном на любой платформе.
Нет.
Взаимодействие между языками удобным объектно-ориентированным способом было большой частью моей первоначальной мотивации для изучения этой идеи.В то время как пример C#, используемый в исходном вопросе, работает под windows, он терпит неудачу на mac osx. Размеры vtables между C# / Mono и C++ не совпадают из-за различных размеров указателей функций-членов. Mono ожидает 4-байтовый указатель на функцию, в то время как компилятор xcode/c++ ожидает, что их будет 8 байты.
Очевидно, указатели на функции-члены - это нечто большее, чем просто указатели. Иногда они могут указывать на структуры, содержащие дополнительные данные для решения определенных ситуаций наследования.Усечение указателей на 8-байтовые функции-члены до 4 байт и отправка их в mono в любом случае действительно работает. Это, вероятно, потому, что я использую тип класса POD. Хотя я бы не хотел полагаться на такой хак, как этот.
Учитывая все обстоятельства, метод, используемый для взаимодействия, предложенный в оригинальный вопрос будет намного сложнее, чем он того стоит, и я выбрал байт пули и пошел с интерфейсом C.