2009-01-13 91 views
7

有什麼方法可以使用不同編譯器構建的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

回答

8

如果您降低了期望值並堅持簡單的功能,那麼您應該能夠混合使用不同編譯器構建的模塊。

類和虛函數的行爲方式由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

0

有趣..如果你在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

2

你確實嚴重依賴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

2

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

這是因爲「C」ABI定義明確,而C++ ABI故意沒有定義。因此每個編譯器都被允許定義它自己的。

在某些編譯器中,不同版本的編譯器之間的C++ ABI甚至不同標誌都會生成不兼容的ABI。

3

我想你會在你的代碼中找到this MSDN article useful

無論如何,從快速瀏覽,我可以告訴你,你不應該在接口聲明虛析構函數。相反,當引用計數下降到零時,您需要在A :: Release()中執行delete this

5

如果你這樣做,你幾乎肯定會遇到麻煩 - 而其他評論者是正確的,在某些情況下C++ ABI可能是相同的,這兩個庫使用不同的CRT,不同版本的STL,不同的異常拋出語義,不同的優化......你正走向瘋狂的道路。

3

你可能能夠組織代碼的一種方式是在應用程序和dll中使用類,但保留兩者之間的接口作爲外部「C」函數。這是我用C#程序集使用的C++ dll完成的。導出的DLL功能用於操縱實例通過靜態類訪問*實例()方法是這樣的:

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

對於對象的多個實例,已經DLL函數創建實例並返回標識符,然後可以傳遞給Instance()方法以使用所需的特定對象。如果您需要應用程序和dll之間的繼承,請在應用程序端創建一個包裝導出的dll函數的類並從中派生其他類。像這樣組織代碼將使DLL接口在編譯器和語言間保持簡單和可移植性。

0

您的問題是維護ABI。雖然相同的編譯器,但不同的版本,你仍然想維護ABI。 COM是解決它的一種方法。如果你真的想了解COM如何解決這個問題,那麼看看這篇描述COM本質的文章CPP to COM in msdn

除了COM之外,還有其他的(最古老的)解決ABI問題的方法就像使用Plain舊數據和不透明指針一樣。 Look在Qt/KDE庫開發人員解決ABI的方法。

0

與您的代碼,導致飛機墜毀在接口定義虛析構函數的問題:

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

刪除它們,一切都會好起來。問題是由虛擬功能表的組織方式引起的。 MSVC編譯器忽略析構函數,但GCC將它作爲表中的第一個函數添加。

看看COM接口。他們沒有任何構造函數/析構函數。永遠不要在界面中定義任何析構函數,它會沒事的。