编译器之间的DLL兼容性

时间:2009-01-13 18:23:04

标签: c++ dll compiler-construction binary-compatibility

有没有办法让不同编译器构建的c ++ dll相互兼容?这些类可以有工厂方法来创建和销毁,因此每个编译器都可以使用自己的new / delete(因为不同的运行时有自己的堆)。

我尝试了以下代码但它在第一个成员方法上崩溃了:

interface.h

#pragma once

class IRefCounted
{
public:
    virtual ~IRefCounted(){}
    virtual void AddRef()=0;
    virtual void Release()=0;
};
class IClass : public IRefCounted
{
public:
    virtual ~IClass(){}
    virtual void PrintSomething()=0;
};

test.cpp用VC9编译,test.exe

#include "interface.h"

#include <iostream>
#include <windows.h>

int main()
{
    HMODULE dll;
    IClass* (*method)(void);
    IClass *dllclass;

    std::cout << "Loading a.dll\n";
    dll = LoadLibraryW(L"a.dll");
    method = (IClass* (*)(void))GetProcAddress(dll, "CreateClass");
    dllclass = method();//works
    dllclass->PrintSomething();//crash: Access violation writing location 0x00000004
    dllclass->Release();
    FreeLibrary(dll);

    std::cout << "Done, press enter to exit." << std::endl;
    std::cin.get();
    return 0;
}

用g ++编译的a.cpp g ++。exe -shared c.cpp -o c.dll

#include "interface.h"
#include <iostream>

class A : public IClass
{
    unsigned refCnt;
public:
    A():refCnt(1){}
    virtual ~A()
    {
        if(refCnt)throw "Object deleted while refCnt non-zero!";
        std::cout << "Bye from A.\n";
    }
    virtual void AddRef()
    {
        ++refCnt;
    }
    virtual void Release()
    {
        if(!--refCnt)
            delete this;
    }

    virtual void PrintSomething()
    {
        std::cout << "Hello World from A!" << std::endl;
    }
};

extern "C" __declspec(dllexport) IClass* CreateClass()
{
    return new A();
}

编辑: 我将以下行添加到GCC CreateClass方法中,文本被正确打印到控制台,因此它可以正确地调用它来杀死它。

std::cout << "C.DLL Create Class" << std::endl;

我想知道,即使跨语言,COM如何设法维护二进制兼容性,因为它基本上都是具有继承性的类(尽管只有单个),因此也是虚函数。如果我不能重载操作符/函数,只要我能维护基本的OOP内容(即类和单个继承),我就不会受到严重的打扰。

9 个答案:

答案 0 :(得分:8)

如果你降低了你的期望并坚持简单的功能,你应该能够混合使用不同编译器构建的模块。

类和虚函数的行为方式由C ++标准定义,但实现的方式取决于编译器。在这种情况下,我知道VC ++构建的对象具有虚拟函数,在对象的前4个字节中具有“vtable”指针(我假设为32位),并且指向指向方法条目的指针表分。

所以行:dllclass->PrintSomething(); 实际上相当于:

struct IClassVTable {
    void (*pfIClassDTOR)           (Class IClass * this) 
    void (*pfIRefCountedAddRef)    (Class IRefCounted * this);
    void (*pfIRefCountedRelease)   (Class IRefCounted * this);
    void (*pfIClassPrintSomething) (Class IClass * this);
    ...
};
struct IClass {
    IClassVTable * pVTab;
};
(((struct IClass *) dllclass)->pVTab->pfIClassPrintSomething) (dllclass);

如果g ++编译器以不同于MSFT VC ++的方式实现虚函数表 - 因为它可以自由地执行并且仍然符合C ++标准 - 这将会像您所证明的那样崩溃。 VC ++代码要求函数指针位于内存中的特定位置(相对于对象指针)。

通过继承变得更加复杂,而且真的,真的,复杂的多重继承和虚拟继承。

Microsoft一直非常公开VC ++实现类的方式,因此您可以编写依赖于它的代码。例如,MSFT分发的许多COM对象头在头中都有C和C ++绑定。 C绑定暴露了他们的vtable结构,就像我上面的代码一样。

另一方面,GNU-IIRC已经开放了在不同版本中使用不同实现的选项,并且只保证用它的编译器构建的程序(仅!)将符合标准行为,

简短的回答是坚持使用简单的C风格函数,POD结构(普通旧数据;即没有虚函数),以及指向不透明对象的指针。

答案 1 :(得分:5)

如果你这样做,你几乎肯定会遇到麻烦 - 而其他评论者认为C ++ ABI在某些情况下可能是相同的,两个库使用不同的CRT,不同版本的STL,不同的异常抛出语义学,不同的优化......你正走向疯狂的道路。

答案 2 :(得分:3)

我想你会找到this MSDN article useful

无论如何,通过快速浏览一下代码,我可以告诉你,你不应该在界面中声明虚拟析构函数。相反,当引用计数降至零时,您需要在A :: Release()中执行delete this

答案 3 :(得分:3)

您可以组织代码的一种方法是在app和dll中使用类,但保持两者之间的接口作为extern“C”函数。这是我用C#程序集使用的C ++ dll完成它的方式。导出的DLL函数用于操作可通过静态类* Instance()方法访问的实例,如下所示:

__declspec(dllexport) void PrintSomething()
{
    (A::Instance())->PrintSometing();
}

对于多个对象实例,使用dll函数创建实例并返回标识符,然后可以将其传递给Instance()方法以使用所需的特定对象。如果您需要在app和dll之间继承,请在app端创建一个包装导出的dll函数的类,并从中派生出其他类。像这样组织你的代码将使DLL接口在编译器和语言之间保持简单和可移植。

答案 4 :(得分:2)

您严格依赖VC和GCC之间兼容的v-table布局。这有点可能。确保调用约定匹配是你应该检查的(COM:__ stdcall,you:__ thishisall)。

值得注意的是,你正在写一部AV。当您自己进行方法调用时,没有任何内容被写入,因此很可能是运算符&lt;&lt;正在进行轰炸。当DLL加载LoadLibrary()时,std :: cout是否可能由GCC运行时初始化?调试器应该告诉。

答案 5 :(得分:2)

只要您只使用extern "C"个功能,就可以。

这是因为“C”ABI定义明确,而C ++ ABI故意没有定义。因此,允许每个编译器定义自己的。

在某些编译器中,不同版本的编译器之间的C ++ ABI甚至是不同的标志都会产生无法匹配的ABI。

答案 6 :(得分:0)

有趣..如果你在VC ++中编译dll会发生什么,如果你在CreateClass()中放入一些调试语句怎么办?

我说可能你的2个不同运行时'版本'的cout是冲突而不是你的方法调用 - 但我相信返回的函数指针/ dllclass不是0x00000004?

答案 7 :(得分:0)

你的问题是维持ABI。虽然编译器相同但版本不同,但您仍希望维护ABI。 COM是解决它的一种方式。如果您真的想了解COM如何解决这个问题,请查看本文CPP to COM in msdn,其中介绍了COM的本质。

除了COM之外,还有其他(最古老的方法之一)解决ABI的方法,例如使用普通旧数据和不透明指针。 Look在Qt / KDE库开发人员解决ABI的方式。

答案 8 :(得分:0)

导致崩溃的代码问题是接口定义中的虚拟析构函数:

virtual ~IRefCounted(){}
    ...
virtual ~IClass(){}

删除它们,一切都会好的。问题是由虚函数表的组织方式引起的。 MSVC编译器忽略析构函数,但GCC将其添加为表中的第一个函数。

查看COM接口。他们没有任何构造函数/析构函数。永远不要在界面中定义任何析构函数,它就可以了。