简体   繁体   English

充满函数指针的结构是否是C ++二进制兼容性的好解决方案?

[英]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. 我有一个用C ++编写的库,需要将其转换为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). 我已经读到,如果我直接使用__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. 我还读到可以从DLL中提取纯虚拟接口,以通过简单地传递一个充满函数指针的表来消除名称修改的问题。 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. 但是,我读到即使这样也会失败,因为某些编译器甚至可能在连续发行之间更改vtable中函数的顺序。

So finally, I figured I could just implement my own vtable, and this is where I am at: 所以最后,我想我可以实现自己的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);

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 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. -特别是,我担心在基本类型TestItf上使用来自TestImp的强制转换函数指针的影响。 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#. 此方法也可以与C#一起使用。 A few minor modifications have been made to the above code. 对上面的代码做了一些小的修改。

Test.cs 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 Program.cs

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

First of all: your TestItf destructor should be virtual because you return a descendant type as a base ancestor. 首先:TestItf析构函数应该是虚拟的,因为您返回后代类型作为基本祖先。 Without virtuality, there will be a memory leak on some compilers. 没有虚拟性,某些编译器将发生内存泄漏。

Now according to binary compatibility. 现在根据二进制兼容性。 There are following generic pitfalls: 有以下一般性陷阱:

  1. Calling conventions. 调用约定。 If both compilers (your and client one) are aware about the calling convention you chosen, it's OK (since then, plain classless stdcall convention like Win32 API does is a proven solution for years and multiple languages, not only C++) 如果两个编译器(您的和客户端的一个)都知道您选择的调用约定,那么就可以了(从那时起,像Win32 API这样的普通无类stdcall约定就是一种经过多年验证的解决方案,不仅是C ++,而且还提供了多种语言)
  2. Structures alignment. 结构对齐。 Pack your published structures with 1-byte alignment - most of compilers has appropriate setting via pragma or compilation keys. 以1字节对齐方式打包已发布的结构-大多数编译器通过编译指示或编译键进行适当的设置。

Keeping that two points in mind you'll play safe in mostly any platform. 牢记这两点,您几乎可以在任何平台上安全使用。

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. 尽管原始问题中使用的C#示例确实在Windows下运行,但在Mac OS X上却失败了。 The sizes of the vtables between C#/Mono and C++ do not match up due to different sizes of member function pointers. 由于成员函数指针的大小不同,因此C#/ Mono与C ++之间的vtable大小不匹配。 Mono expects a 4 byte function pointer, while the xcode/c++ compiler expects them to be 8 bytes. Mono希望有4个字节的函数指针,而xcode / c ++编译器希望它们是8个字节。

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. 将8个字节的成员函数指针截断为4个字节并将它们发送到Mono仍然有效。 This is probably because I am using a POD class type. 这可能是因为我使用的是POD类类型。 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. 考虑到所有问题,原始问题中建议的用于互操作的方法将比其应有的麻烦多得多,我选择了点子,然后使用C接口。

声明:本站的技术帖子网页,遵循CC BY-SA 4.0协议,如果您需要转载,请注明本站网址或者原文地址。任何问题请咨询:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM