编译器之间的Dll兼容性

问题描述:

有什么方法可以使用不同编译器构建的C++ dll兼容吗?这些类可以有创建和销毁的工厂方法,因此每个编译器都可以使用它自己的新/删除(因为不同的运行时有自己的堆)。编译器之间的Dll兼容性

我尝试以下的代码,但它碰撞在所述第一构件的方法:

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

一个.cpp用g ++编译 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方法中添加了下面一行,文本被正确地打印到控制台,所以它的defenatly函数调用多数民众赞成在杀死它。

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

我想知道,如何COM勉强维持二进制兼容性,甚至跨越语言,因为它的basicly与所有的继承类(虽然只是单),因此虚函数。只要我能够维持基本的面向对象的东西(即类和单一继承),如果我不能重载操作符/函数,我就不会受到大规模的困扰。

+0

COM如何做到这一点?使用轻量级RPC调用 - 您可以使用dce-rpc构建您的应用程序,并获得相同的结果。在任何情况下,COM都不会提供指向外部dll内存的指针,它会对该dll进行函数调用。 – gbjbaanb 2009-01-14 13:43:07

+0

以下文章[此处](http://eli.thegreenplace.net/2011/09/16/exporting-c-classes-from-a-dll/),[here](http://www.codeproject。 com/Articles/28969/HowTo-Export-C-classes-from-a-DLL#CppMatureApproach)和[here](http://chadaustin.me/cppinterface.html)可能会有所帮助。除了内联的虚拟析构函数之外,你的代码示例几乎就在那里。 AFAIK,抽象接口中的所有方法都必须是纯虚拟的'= 0'。 – greatwolf 2013-03-12 22:46:11

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

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

所以行:如果G ++编译器不同于MSFT VC++实现任何方式的虚函数表

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

- 因为它是免费做的,:dllclass->PrintSomething(); 实际上相当于类似仍然符合C++标准 - 这只会在您演示时崩溃。VC++代码期望函数指针位于内存中的特定位置(相对于对象指针)。

它通过继承变得更加复杂,而且确实真的很复杂,具有多重继承和虚拟继承。

微软一直非常公开VC++实现类的方式,所以你可以编写依赖它的代码。例如,由MSFT分发的许多COM对象头在头中都具有C和C++绑定。 C绑定暴露了他们的vtable结构,就像我上面的代码一样。另一方面,GNU-IIRC在不同版本中保留了使用不同实现的选项,并且只保证使用它的编译器构建的程序(仅限于!)将符合标准行为

简短的答案是坚持简单的C风格的功能,POD结构(普通的旧数据,即没有虚拟功能)和指向不透明对象的指针。

+0

我不想在每个地方都使用普通的函数,但是我只是告诉大家他们必须先用VC9编译dll,然后才能采取这一步... – 2009-01-13 20:33:05

有趣..如果你在VC++中编译DLL,会发生什么情况,以及如果你在CreateClass()中放置了一些调试语句会怎么样?

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

+0

不,VC调试器显示指针的可重构值,据我所知(我从来没有尝试用VC调试过一个非VC二进制文件),它从来没有真正进入PrintSomething方法,至少堆栈帧指示它从来没有进入这个DLL。 – 2009-01-13 20:28:40

+0

当您调试不是用VC构建的代码时 - 或者甚至是当您没有调试符号时 - 您无法完全相信调试器告诉您有关调用堆栈的内容。 – 2009-01-13 23:30:17

你确实严重依赖VC和GCC兼容的v-table布局。这有点可能是好的。确保调用约定匹配是你应该检查的东西(COM:__stdcall,你:__thiscall)。

重要的是,你正在写一个AV。当您调用本方法时,没有任何内容正在写入,因此操作员很可能正在进行轰炸。当使用LoadLibrary()加载DLL时,std :: cout是否可能由GCC运行时初始化?调试器应该告诉。

+0

如何检查cout是否使用VC在GCC dll中正确创建?还有COM的工作方式究竟是通过__stdcall工作的,因为即使是基本的类都必须通过VC下的__thiscall工作呢? – 2009-01-13 20:31:30

只要您只使用extern "C"函数即可。

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

在某些编译器中,不同版本的编译器之间的C++ ABI甚至不同标志都会生成不兼容的ABI。

我想你会在你的代码中找到this MSDN article useful

无论如何,从快速浏览,我可以告诉你,你不应该在接口声明虚析构函数。相反,当引用计数下降到零时,您需要在A :: Release()中执行delete this

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

你可能能够组织代码的一种方式是在应用程序和dll中使用类,但保留两者之间的接口作为外部“C”函数。这是我用C#程序集使用的C++ dll完成的。导出的DLL功能用于操纵实例通过静态类访问*实例()方法是这样的:

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

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

您的问题是维护ABI。虽然相同的编译器,但不同的版本,你仍然想维护ABI。 COM是解决它的一种方法。如果你真的想了解COM如何解决这个问题,那么看看这篇描述COM本质的文章CPP to COM in msdn

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

与您的代码,导致飞机坠毁在接口定义虚析构函数的问题:

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

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

看看COM接口。他们没有任何构造函数/析构函数。永远不要在界面中定义任何析构函数,它会没事的。