2

C++ で記述されたライブラリを DLL に変換する必要があります。このライブラリは、別のコンパイラで変更および再コンパイルでき、引き続き機能するはずです。

__declspec(dllexport) を使用してすべてのクラスを直接エクスポートした場合、コンパイラ/バージョン間の完全なバイナリ互換性を達成する可能性はほとんどないことを読みました。

また、純粋な仮想インターフェイスを DLL から取得して、関数ポインターでいっぱいのテーブルを渡すだけで、名前マングリングの問題を解決できることも読みました。ただし、一部のコンパイラは、連続するリリース間で vtable 内の関数の順序を変更することさえあるため、これでも失敗する可能性があることを読みました。

最後に、私は自分の vtable を実装するだけでよいと考えました。

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);

テスト.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;
}

最初の 2 つの方法との適切な互換性を達成できないという上記の仮定は正しかったでしょうか?

3 番目のソリューションは移植可能で安全ですか?

- 具体的には、基底型 TestItf で TestImp からキャストされた関数ポインターを使用した場合の影響が心配です。この単純なテスト ケースでは機能しているように見えますが、配置やオブジェクト レイアウトの変更などによって、場合によっては安全でなくなる可能性があると思います。

編集
このメソッドは C# でも使用できます。上記のコードにいくつかのマイナーな変更が加えられています。

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();
    }
}
4

2 に答える 2