2

我有一个用 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;
    }
}

主文件

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

    return 0;
}

关于无法与前两种方法实现适当兼容性的上述假设是否正确?

我的第三个解决方案便携且安全吗?

- 具体来说,我担心在基本类型 TestItf 上使用来自 TestImp 的强制转换函数指针的效果。它似乎在这个简单的测试用例中有效,但我想像对齐或改变对象布局这样的事情在某些情况下可能会使这不安全。

编辑
此方法也可用于 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();
    }
}
4

2 回答 2

0

首先:您的 TestItf 析构函数应该是虚拟的,因为您返回一个后代类型作为基本祖先。如果没有虚拟性,某些编译器会出现内存泄漏。

现在根据二进制兼容性。有以下一般陷阱:

  1. 调用约定。如果两个编译器(您的和客户端的)都知道您选择的调用约定,那没关系(从那时起,像 Win32 API 这样的普通无类 stdcall 约定是多年来和多种语言的经过验证的解决方案,而不仅仅是 C++)
  2. 结构对齐。使用 1 字节对齐打包您发布的结构 - 大多数编译器通过编译指示或编译键进行适当的设置。

牢记这两点,您几乎可以在任何平台上玩得安全。

于 2013-09-25T21:08:46.413 回答
0

不。

以方便的面向对象方式实现语言之间的互操作性是我探索这个想法的最初动机的重要组成部分。

虽然原始问题中使用的 C# 示例在 windows 下确实有效,但在 mac osx 上却失败了。由于成员函数指针的大小不同,C#/Mono 和 C++ 之间的 vtable 大小不匹配。Mono 期望 4 字节函数指针,而 xcode/c++ 编译器期望它们为 8 字节。

显然,成员函数指针不仅仅是指针。有时,它们可以指向包含额外数据的结构来处理某些继承情况。

将 8 字节成员函数指针截断为 4 字节并将它们发送到单声道实际上是可行的。这可能是因为我使用的是 POD 类类型。不过,我不想依赖这样的黑客。

考虑到所有因素,原始问题中建议的用于互操作的方法比它的价值要麻烦得多,而且我选择了字节,并使用 C 接口。

于 2013-10-01T03:21:31.810 回答