10

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

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

接口.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;
};

用VC9编译的test.cpp,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;
}

a.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 方法中添加了以下行,文本被正确打印到控制台,所以它的函数调用就是杀死它。

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

我想知道,COM 是如何设法保持跨语言的二进制兼容性的,因为它基本上所有类都具有继承(尽管只有单个),因此是虚函数。只要我可以维护基本的 OOP 内容(即类和单一继承),如果我不能重载运算符/函数,我就不会感到非常困扰。

4

9 回答 9

10

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

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

所以行: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 结构(Plain Old Data;即,没有虚函数)和指向不透明对象的指针。

于 2009-01-13T20:17:27.303 回答
6

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

于 2009-01-13T21:35:23.247 回答
5

只要你只使用extern "C"函数就可以。

这是因为“C” ABI定义良好,而 C++ ABI 故意没有定义。因此,每个编译器都可以定义自己的。

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

于 2009-01-13T20:43:53.243 回答
5

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

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

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

于 2009-01-13T22:08:29.857 回答
3

我想你会发现这篇 MSDN 文章很有用

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

于 2009-01-13T21:18:01.067 回答
2

您确实非常依赖 VC 和 GCC 之间兼容的 v-table 布局。这有点可能没问题。确保调用约定匹配是您应该检查的内容(COM:__stdcall,您:__thiscall)。

重要的是,您在写作方面获得了 AV。当您自己调用方法时没有写入任何内容,因此很可能 operator<< 正在执行轰炸。当使用 LoadLibrary() 加载 DLL 时,std::cout 是否可能被 GCC 运行时初始化?调试器应该告诉。

于 2009-01-13T20:08:44.000 回答
1

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

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

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

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

于 2016-02-19T04:26:47.330 回答
0

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

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

于 2009-01-13T18:58:45.433 回答
0

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

除了 COM,还有其他(最古老的)方法可以解决 ABI,例如使用普通旧数据和不透明指针。看看Qt/KDE 库开发者解决 ABI 的方法。

于 2012-05-11T21:40:26.053 回答