在C++Builder裏創建可以被Visual C++使用的DLL

在C++Builder裏創建可以被Visual C++使用的DLL
shadowstar's home: http://shadowstar.126.com/

source:http://www.bcbdev.com/articles/bcbdll.htm

  在前兩篇文章裏,我們討論瞭如何在C++Builder工程裏調用用MS Visual C++創建的DLL。這篇文章討論相反的一種情形,舉例說明如何用C++Builder創建一個DLL,使它可以在Visual C++工程裏調用。

簡介:爲什麼這個這麼難

  如果你用BCB創建了一個DLL,它可以被BCB的可執行文件調用,你知道這種使用DLL的方式沒什麼難度。當你構造一個DLL,BCB生成一個帶“.LIB”擴展名的引入庫。把這個LIB文件添加到你的工程裏。連接器按引入庫決定DLL內部調用。當你運行你的程序時,DLL隱式的被載入,你不必去考慮DLL內部調用工作是。

  當EXE文件是由Microsoft Visual C++編譯的時候,情況會變得比較複雜。有3個主要的問題。首先,BCB和MSVC對DLL中的函數命名方式是不一致的。BCB使用一種習慣,MSVC使用另一種不同的習慣。當然,兩種習慣是不兼容的。命名問題在如何在C++Builder工程裏使用VC++編譯的DLL那篇文章裏已經討論過了。表1總結了各個編譯器在各自的調用習慣下,導出的MyFunction函數。注意Borland給__cdecl函數前加了一個下劃線,而MSVC沒有。另一方面,MSVC認爲導出的__stdcall函數前面帶有下劃線,後面還有一些垃圾。

表1: Visual C++ and C++Builder 命名習慣

調用習慣       VC++命名       VC++(使用了DEF)     C++Builder命名
-----------------------------------------------------------------------
__stdcall           _MyFunction@4   MyFunction          MyFunction
__cdecl             MyFunction      MyFunction          _MyFunction

  第2個問題是Borland引入庫與MSVC不是二進制兼容的。當你編譯DLL時,由BCB創建的引入庫不能被MSVC用來連接。如果你想使用隱式連接,那麼你需要創建一個MSVC格式的引入庫。另一種可選擇的辦法就是採用顯式連接(LoadLibrary和GetProcAddress)。

第3個問題是不能從DLL裏導出C++類和成員函數,如果你想讓MSVC的用戶也可以調用它。好吧,那不完全屬實。你的DLL能導出C++類,但是MSVC不能使用它們。原因就是C++成員函數名被編譯器改編(mangled)。這個改編的名字結果了DLL。爲了調用在DLL裏被改編的函數,你必需知道被改編的是哪個函數。Borland和Microsoft使用了不同的名字改編方案。結果是,MSVC不能恰好看到Borland編譯的DLL裏的C++類和成員函數。

Tip 注意:

Borland和Microsoft沒有采用相同的方式改編函數,因爲依照ANSI C++標準,C++編譯器不被假定追隨相同的指導方針。名字改編只是實現的細節。


  這三個問題使得Borland創建的DLL可以在MSVC裏被調用變得非常困難,但並非不可能的。這篇文章描述了一套指導方針,你可以跟着製作與Microsoft兼容的BCB DLL。我們討論四種不同的技術。三種採用引入庫隱式連接調用,一種在運行時利用顯式連接。

指導方針摘要

  你可以跟着下面的指導方針摘要列表建造你的DLL。第1個列表討論隱式連接;第2個列表描述顯式連接;第3種技術採用#define組裝隱式連接;最後一個例子利用假的MSVC DLL工程爲__stdcall函數創建引入庫。

技術1: 隱式連接
------------------------------------------------------------------------------
1- 使用__cdecl調用習慣代替__stdcall。
2- 導出簡單的"C"風格函數,沒有C++類或成員函數。
3- 確定你有一個 extern "C" {} 包圍你的函數原型。
4- 創建DEF文件,包含與Microsoft兼容的導出函數別名。別名也就是不包含前面的下劃線。
   DEF文件內容如下:

   EXPORTS
   ; MSVC name    = Borland name
     Foo          = _Foo
     Bar          = _Bar

5- 把DEF文件加入到你的工程裏重新編譯它。
6- 把DLL和DLL頭文件拷貝到你的MSVC工程目錄裏。
7- 運行impdef爲DLL創建第2個DEF文件。這個DEF文件用來創建引入庫。
     > impdef mydll.def mydll.dll
8- 運行Microsoft的LIB工具,用上一步創建的DEF文件創建COFF引入庫。調用格式爲:
     > lib /DEF mydll.def
9- 把用LIB.EXE創建的LIB文件添加到你的MSVC工程裏。

技術2: 顯式連接
------------------------------------------------------------------------------
1- 使用__cdecl或__stdcall,如果你使用__stdcall可以跳過第4,5步。
2- 導出簡單的"C"風格函數,沒有C++類或成員函數。
3- 確定你有一個extern "C" {}包圍你的函數原型。
4- 如果你使用__cdecl,那麼你可能想去掉導出函數前面的下劃線,但你不必這麼做。你可以用例1的第4,5步去掉下劃線。如果你沒有去掉下載線,在調用GetProcAddress函數時函數名必須前面的下劃線。
5- 把DLL拷貝到MSVC工程目錄裏。
6- 在MSVC應用程序中,使用LoadLibrary API函數載入DLL。
7- 調用GetProcAddress API在DLL裏查找你想要的調用函數,保存GetProcAddress函數返回的函數指針。當你想調用函數的時候,提取函數指針。
8- 當你用完DLL時調用FreeLibrary。

技術3: 用#define組裝隱式連接
------------------------------------------------------------------------------
1- 用__cdecl調用習慣代替__stdcall。
2- 導出簡單的"C"風格函數,沒有C++類或成員函數。
3- 確定你有一個extern "C" {}包圍你的函數原型。
4- 在你的DLL頭文件裏,爲每一個導出函數名創建一個#define。
   #define會調用預編譯器在每一個函數名前加上下劃線。因爲我們只想爲MSVC創建別名,所以代碼檢查_MSC_VER。

     #ifdef _MSC_VER
     #define Foo _Foo
     #define Bar _Bar
     #endif

5- 把DLL和DLL頭文件拷貝到MSVC工程目錄裏。
6- 運行impdef爲DLL函數DEF文件。
     > impdef mydll.def mydll.dll
7- 使用Microsoft的LIB工具爲DEF文件創建COFF格式的引入庫。
     >lib /def mydll.def
8- 把LIB.EXE創建的LIB文件添加到MSVC工程裏。

技術4: 用__stdcall函數隱式連接
------------------------------------------------------------------------------
1- 當建造你的DLL時使用__stdcall調用習慣。
2- 導出簡單的"C"風格函數,沒有C++類或成員函數。
3- 確定你有一個extern "C" {}包圍你的函數原型。
4- 爲MSVC創建一個引入庫。這一部分比較困難。你不能用LIB.EXE爲__stdcall函數創建引入庫。你必須創建一個由MSVC編譯的的假的DLL。這樣做,按這些步驟:
     4a- 用MSVC創建一個不使用MFC的DLL
     4b- 從BCB裏拷貝覆蓋DLL頭文件和DLL源代碼
     4c- 編輯你的DLL源代碼,拋開每一個例程的函數體部分,使用一個假的返回值返回
     4d- 配置MSVC工程生成的DLL,採用和BCB DLL同的的名字
     4e- 把DEF文件添加到MSVC工程,禁止它對__stdcall命名進行修飾(_Foo@4)
5- 編譯第4步得到的虛假DLL工程。這將會生成一個DLL(你可以把它丟到垃圾筒裏)和一個LIB文件(這是你需要的)。
6- 把從第5步得到的LIB文件添加到你需要調用這個BCB DLL的MSVC工程裏。LIB文件會確保連接。爲MSVC可執行文件配置BCB DLL(不是虛假DLL)。

Tip 注意:

  一般情況下,隱式連接比顯式連接要優先考慮,因爲對程序員來說隱式連接更簡單,而且它是類型安全的(錯誤發生在連接時而不是運行時)。不管用哪種方法,當你在編譯器間共享DLL時,如果你選擇堅持使用隱式連接,就必須爲每一個編譯器創建兼容的引入庫。創建兼容的引入庫比用顯式連增加的負擔就是要注意更多的要求。


 

Tip 注意:

  如果你想使你的DLL可以被Visual Basic的開發者使用,顯式連接的指導方針同樣適用。如果你想把你的DLL給VC開發者,按顯式連接的指導方針,採用__stdcall調用習慣。


 

下面4個部分詳細描述每一種技術。

例1: 顯式連接

  這個例子詳細描述了上一部分技術1的指導方針。技術1的指針方針可以分爲兩組。1-5項處理在BCB這邊編譯DLL;6-9項處理在MSVC這邊使用DLL。我們將沿這條主線分別進行討論。

  在這個例子裏,我們將用BCB建造一個DLL,它導出兩個函數: Foo和Bar。兩個函數都返回一個整型值。函數原型爲:

int Foo (int Value);
int Bar (void);

然後我們在MSVC裏建造一個測試EXE,用來調用Borland DLL。

用BCB編譯DLL

  下面兩個程序清單包含我們的DLL源代碼。清單1要在BCB和MSVC之間共享的頭文件;清單2包含我們的DLL函數實現部分。創建一個BCB DLL工程,從清單1和2中拷貝代碼粘貼到工程裏。或者你可以下載這篇文章的源代碼以節省時間。BCB DLL工程已經爲你設置好了。(參見最下面的下載部分)






extern  {








IMPORT_EXPORT int __cdecl  Foo  (int Value);
IMPORT_EXPORT int __cdecl  Bar  (void);


}











int __cdecl  Foo  (int Value)
{
    return Value + ;
}

int __cdecl  Bar  (void)
{
    static int ret = ;
    return ret++;
}

  關於頭文件有兩個要注意的地方。首先,觀察我們用 extern "C" 的方法確保函數名不會被C++編譯器改編;其次,注意到在我們建造DLL時,導出函數有一個特殊指示的前綴__declspec(dllexport)。當我們從MSVC裏使用DLL時,函數前綴變爲__declspec(dllimport)。這個指示的改變是通過IMPORT_EXPORT宏定義實現的。

    最後,注意我們顯式聲明瞭__cdecl爲調用習慣。技術上,我們可以省略__cdecl關鍵字,因爲__cdecl已經是默認的。但是,我想不管怎樣把它列出來是一個好習慣。通過列出調用習慣,你顯式的告訴人們你選擇了__cdecl作爲一個前提。同樣,默認的調用習慣在兩個編譯器裏可以通過編譯開關改變。你肯定不想這些編譯器開關影響到你DLL的可用性。

    頭文件本身滿足了指導方針中的1-3項 。我們需要做的下一件事情是處理第4項: 給導出函數建立別名。

    首先,按現在的情況建造DLL代碼。其次,運行TDUMP工具檢查函數的函數名確實包含前面的下劃線。

c:> tdump -m -ee bcbdll.dll
Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation
                    Display of File BCBDLL.DLL

EXPORT ord:0001='_Bar'
EXPORT ord:0002='_Foo'
EXPORT ord:0003='___CPPdebugHook'

 

Tip 注意:

    使用TDUMP時別忘了用 -m 開關。TDUMP嘗試反改編(unmangle)被修飾的名字,使他們更容易閱讀。但是,當你查看一個DLL的時候,明智的選擇是查看函數的原始格式。-m 開關告訴TDUMP顯示原始函數名。


    像你看到的那樣,Foo和Bar都包含前端下劃線。至於__CPPdebugHook,你可以不理它,它是幕後操縱的,當它不存在好了。它對你沒什麼意義,你也不能讓它走開,因此就不要把它放在心上了。

    爲了用別名去掉下劃線,我們需要做三件事:首先創建DLL的DEF文件;然後調整DEF文件,爲Borland名字創建MSVC的別名;最後,把DEF文件添加到你的BCB工程裏,重建DLL。

    要創建DEF文件,對DLL運行Borland的IMPDEF工具。

C:> impdef bcbdllx.def bcbdll.dll

    我選擇bcbdllx.def爲文件名,因爲稍後(在我們創建MSVC引入庫之前)我們將使用其它DEF文件。我想避免兩者混淆。bcbdllx.def內容如下:

LIBRARY     BCBDLL.DLL

EXPORTS
    _Bar                           @1   ; _Bar
    _Foo                           @2   ; _Foo
    ___CPPdebugHook                @3   ; ___CPPdebugHook

    注意到在Foo和Boo前端的下劃線。如果DLL把Foo和Bar導出爲_Foo和_Bar,當MSVC用戶設法建造他們的工程的時候,將看到連接錯誤。我們需要剝去下劃線。我們用在DEF文件裏給函數別名的方法實現。

    DEF文件別名允許我們爲真實的函數導出擔當代理或佔位符的函數名。在DLL裏的真實的函數仍然是_Foo和_Bar。代理名將是Foo和Bar(注意沒有了下劃線)。當我們給兩個函數別名的時候,DLL將導出兩個將的符號,它們歸諸於原來的函數。

    完成別名, 編輯DEF文件,改變成下面的樣子:

LIBRARY     BCBDLL.DLL

EXPORTS
    Bar = _Bar
    Foo = _Foo

    這個DEF文件創建兩個新的出口,Foo和Bar,它們分別擔當_Foo和_Bar的點位符。把這個DEF文件保存到你的硬盤上。一旦你完成了這些工作,便可以把DEF文件添加到你的BCB工程裏,使用Project-Add菜單項。添加後,BCB會在工程管理器(Project Manager)的樹狀結構裏顯示出DEF文件。

    一旦你把DEF文件加入到工程裏,做一次完全的重建。工程連接好之後,再次對DLL運行TDUMP,檢查從DLL裏導出的帶下劃線函數。

>tdump -m -ee bcbdll.dll
Turbo Dump  Version 5.0.16.12 Copyright (c) 1988, 2000 Inprise Corporation
                    Display of File BCBDLL.DLL

EXPORT ord:0004='Bar'
EXPORT ord:0005='Foo'
EXPORT ord:0002='_Bar'
EXPORT ord:0001='_Foo'
EXPORT ord:0003='___CPPdebugHook'

    對TDUMP的輸出有兩點要注意的事情要注意。首先,觀察Foo和Bar到場了(沒有前端下劃線)。現在DLL導出函數名與MSVC的一致了。還注意到原來的函數,_Foo和_Bar,還在那兒。被修飾過的函數仍就從DLL裏導出。使用DEF文件別名並不隱藏原來的函數。

    你可能會想把這原來的兩個函數用什麼辦法隱藏起來。但是,這麼做將會危害到從BCB工程裏使用DLL的人們。記得BCB的連接器期望在那兒有一個前端下劃線。如果你真的用了什麼方法從DLL裏把_Foo和_Bar隱藏了(以我的知識是不可能實現的),那麼你的DLL從BCB裏調用將變得非常困難。

    如果TDUMP的輸出沒有列出代理函數(不帶下劃線的函數),那麼返回上一步,檢查你的DEF文件。在你可以繼續之前,你需要得到別名的出現。如果DLL看起來OK了,那麼該是轉到MSVC這邊的時間了。

從MSVC裏調用DLL

    一旦你擁有了一個被反改編__cdecl函數出口的DLL模型,下一步就是要爲MSVC用戶生成一個引入庫。爲這,你將需要剛剛創建的DLL,使用Borland的IMPDEF實用工具(再一次),和來自MSVC的LIB.EXE工具。第一步是創建DLL的DEF文件。爲這,我建議你拷貝DLL和DLL頭文件到你的MSVC工程目錄裏,在那兒工作。

C:> impdef bcbdll.def  bcbdll.dll

IMPDEF將創建一個DEF文件,內容如下:

C:> impdef bcbdll.def  bcbdll.dll

LIBRARY     BCBDLL.DLL

EXPORTS
    Bar                            @4   ; Bar
    Foo                            @5   ; Foo
    _Bar                           @2   ; _Bar
    _Foo                           @1   ; _Foo
    ___CPPdebugHook                @3   ; ___CPPdebugHook

打開DEF文件,改變它的內容爲:

LIBRARY     BCBDLL.DLL

IMPORTS
    Bar                            @4   ; Bar
    Foo                            @5   ; Foo

    注意到我們移除了包含下劃線的函數,和調試鉤掛(debug hook)函數。我們還把EXPORT改成了IMPORTS,因爲我們現在是在引入函數,而不是導出它們(我懷疑它對MSVC LIB.EXE來說會產生不同)。

    下一步,我們用Microsoft LIB.EXE,從DEF文件那兒創建一個COFF格式的庫。語法爲:

lib /DEF:bcbdll.def /out:bcbdll_msvc.lib

 

Tip 注意:

    MSVC命令行實用工具在默認情況下不在你的配置的路徑裏。你可能需要運行一個MSVC帶的批處理文件,使得LIB.EXE可以被直接調用。批處理文件叫做VCVARS32.BAT,它位於DevStudio安裝路徑的/VC/BIN子目錄下。


    這裏,所有艱苦的工作都做完了。現在你需要做就是把你的DLL,MSVC LIB文件,和DLL文件件加入到你的MSVC客戶端。要使用DLL,需要添加LIB文件到MSVC工程裏,並且在源代碼內#include DLL頭文件。

    我準備了一個MSVC的簡單工程來證明上面的概念。清單3給出客戶端DLL的源代碼。沒什麼特別的地方,就是一個main函數,一個DLL頭文件的#include,和對DLL的幾個函數調用。主要是你正確的添加了引入庫,由LIB.EXE生成的那個,添加到MSVC工程裏。




using namespace std;



int main()
{
    cout <<  << Foo() << endl;
    cout <<  << Bar() << endl;
    cout <<  << Bar() << endl;
    cout <<  << Bar() << endl;
    return ;
}

例2:顯式連接

    這個例子向你展示瞭如何從MSVC裏使用顯式連接調用BCB編譯的DLL。用顯式連接,你不必擺弄創建一個MSVC兼容的引入庫。顯示連接不利的是它需要在用戶端做更多的工作,它不及隱式連接類型安全,錯誤被延期到運行時而不是連接時。雖然顯式連接有許多不利因素,但在某些情況下它還是十分有用的。

    在這個例子裏,我們將創建一個DLL,它導出兩個函數:Foo和Bar。函數的原型同上一個例子一樣。

int Foo (int Value);
int Bar (void);

    這一顯式連接的指導方針與隱式連接的相仿。我們需要導出簡單的C函數,需要防止C++名字改編。如果我們用__cdecl調用習慣,那麼我們可能想要爲BCB導出的函數建立別名,以去掉它們前端的下劃線。如果我們選擇不用別名去掉下劃線的方法,那麼當按名字載入函數時,我們必須包含下劃線。換句話說,當你對__cdecl函數起作用時,你必須在某幾點上處理下劃線。你也可以在BCB建造DLL的時候處理下劃線,或者在運行時調用DLL時處理它。我們利用__stdcall代替__cdecl以迴避整個討論的下劃線問題。這是我們在這個例子裏要做的。清單4和5給出的我們DLL的源代碼。

Tip 注意:

    如果你導出__stdcall函數,至關緊要的是要讓客戶端應用程序知道。一些人容易犯一個錯誤,認爲使用__stdcall只不過是去掉了__cdecl函數前面的下劃線。別掉進這個陷井。__stdcall函數處理堆棧方式也__cdecl是不同的。如果客戶端應用程序把__stdcall當作__cdecl函數調用(也就是,堆棧將被破壞,客戶端程序會死得很難看),將要發生一些錯誤。







extern  {








IMPORT_EXPORT int __stdcall Foo  (int Value);
IMPORT_EXPORT int __stdcall Bar  (void);


}










int __stdcall  Foo  (int Value)
{
    return Value + ;
}

int __stdcall Bar  (void)
{
    static int ret = ;
    return ret++;
}

    注意這段代碼幾乎與隱式連接的一模一樣。唯一不同的地方就是把Foo和Bar的調用習慣改成__stdcall代替__cdecl。

    現在讓我們看一下調用DLL的MSVC程序代碼。代碼如清單6所示。




using namespace std;

HINSTANCE hDll = ;
typedef int (__stdcall *foo_type)  (int Value);
typedef int (__stdcall *bar_type)  ();
foo_type Foo=;
bar_type Bar=;

void DLLInit()
{
    hDll = LoadLibrary();
    Foo = (foo_type)GetProcAddress(hDll, );
    Bar = (bar_type)GetProcAddress(hDll, );
}

void DLLFree()
{
    FreeLibrary(hDll);
}

int main()
{
    DLLInit();

    cout <<  << Foo() << endl;
    cout <<  << Bar() << endl;
    cout <<  << Bar() << endl;
    cout <<  << Bar() << endl;
    DLLFree();
    return ;
}

    這段代碼片段裏有許多需要消化的地方。首先也是最重要的,觀察代碼本身是編譯器中立的。你可以在BCB或MSVC裏編譯它。我首先在BCB裏編譯它,確信它可以按我所想的工作。

    第二,注意到代碼沒有爲#include bcbdll.h操心。有一個重要的原因。bcbdll.h爲Foo和Bar函數定義的原型。但是,我們不把我們的代碼同任何預先定義的那些原型連接。通常,這些原型的存根來自引入庫。但是這個例子示範的是顯式連接,當你顯示地連接時,是不使用引入庫的,在頭文件裏的Foo和Bar原型對我們來說沒多大意義。

    第三件要注意的事情是關於這段代碼裏出現的typedef和函數指針,位於源文件的頂部附近。晃式連接需要你在運行時用API GetProcAddrress得到DLL函數的地址。你必須把GetProcAddress返回的結果存儲到某個地方。最好的地點是把結果存儲到函數指針裏。通過把函數地址存儲到函數指針裏,你可以使用正常的函數調用語法調用函數(如 Foo(10))。

    typedef聲明創建了兩個新的類型: foo_type和bar_type。它們都是函數指針類型。foo_type聲明瞭一個指向__stdcall函數的類型,這個函數打官腔一個整型參數,返回一個整型值。bar_type定義了一個指向__stdcall類型的、沒有參數、有一個整型返回值的函數。這些typedef產生了兩個效果。第一,它們提供了清晰的方式來聲明函數指針變量Foo和Bar。第二,它們使我們可以很方便的轉換GetProcAddress返回的結果。從GetProcAddress返回的結果是一個指向__stdcall類型的、沒有參數、有一個整型返回值的函數。除非你的函數與這個格式相同,否則你需要轉換GetProcAddress的結果(這個轉換是顯式連接比隱式連接缺管類型安全的原因)。

    在typedef的下面有兩個變量Foo和Bar。這兩個是函數指針變量。它們會保存我們想要調用的兩個函數的地址。注意這些變量的名字是任意的。我選擇Foo和Bar是爲了使代碼像隱式連接。不要犯這樣的錯誤,Foo和Bar變量名沒有與DLL裏的真實函數建立連接。我們可以把變量命名爲Guido和Bjarne,如果你想的話。

    在函數指針聲明下面,你會看到兩個叫DllInit和DllFree的函數實體。這兩個實體處理載入DLL,查找導出函數,和在我們使用賽後釋放程序庫。用這種方法,其餘的代碼不知道DLL是顯式連接的。它可以像往常一樣調用Foo和Bar(或者Guido和Bjarne,如果你改變了名字)。唯一要協調的是你必須在調用任何DLL程序之前調用DllInit。我們也應當細緻的,調用DllFree釋放程序庫。

Tip 注意:

    當在命名總題上Borland編譯器和Microsoft編譯器之間大戰之時,GetProcAddress是你的最後一道防線。這包括Borland __cdecl命名帶一個前端下劃線(如 _Foo)。也包括改編C++名字。如果有人支持你用改編函數名字的DLL,你可以永遠傳遞這些難看的參數,把改編名字給GetProcAddress。不管你實際上你能調用函數而沒碰到其它的什麼問題,但是至少你將會有一個機會。


    這就是全部。在MSVC裏編譯代碼,你就完成了。你不必擺弄DEF文件或是引入庫。但是在你這邊的代碼裏有些瑣碎的工作要處理。

例3: 用#define組裝隱式連接

    這個例子展示了可能是從MSVC工程裏調用BCB DLL最簡單的一種方法,但它也可能是最沒有吸引力的一種方法。代碼使用一個狡詐的#define,當檢查到是Microsoft編譯器時給__cdecl函數前加上下劃線。也就是說,我們簡單的#define了Foo爲_Foo。

    這種技術的優勢在於我們不必實行任何別名。 我們能直接導出包含下劃線的__cdecl函數。但是,我們仍就必須用Microsoft的LIB.EXE創建一個MSVC兼容的引入庫。

    這種技術是關鍵是MSVC不期望__cdecl函數有任何的修飾(見表1)。它們應當和看起來一樣。 如果MSVC應用程序試圖執行一個__cdecl函數Foo,它期望在DLL裏查找一個沒有下劃線的函數Foo。如果我們改變MSVC的代碼,讓它調用_Foo,那麼它將試圖在DLL裏查找一個叫做_Foo的函數。

    Borland給__cdecl函數前加上了下劃線。我們可以哄騙MSVC,讓它在調用函數的時候在函數名的前面加一個下劃線。緊記我們只想在MSVC這邊添加一個下劃線,而不是Borland這邊。

    #define組裝的DLL代碼與例1裏清單2的代碼完全一樣。唯一不同的是DLL頭文件。當檢測到是MSVC時,DLL頭文件爲每一個函數原型加一個下劃線。清單7展示了修改後的頭文件。






extern  {
















IMPORT_EXPORT int __cdecl  Foo  (int Value);
IMPORT_EXPORT int __cdecl  Bar  (void);


}



    在頭文件裏,除#define組裝之外,你還必須創建一個MSVC兼容的引入庫。你可以按前面的步驟完成。對編譯好的DLL運行IMPDEF,得到一個DEF文件。然後運行Microsoft LIB.EXE工具創建一個COFF格式的引入庫。這時,你不必考慮去編輯DEF文件。最後,拷貝DLL,COFF引入庫,和DLL文件件到你的MSVC工程裏。把LIB文件添加到你的MSVC工程裏,重建。

    這是創建MSVC引入庫的命令行例子。注意我們不必編輯DEF文件。我們剛好可以把它傳遞給LIB.EXE。

// Create def file
> impdef bcbdll.def bcbdll.dll

// create COFF import library using MS lib.exe
>lib /def bcbdll.def

例4: 用__stdcall函數隱式連接

    在我們進行之前,讓我們調查一下爲什麼我們需要單獨論述__stdcall函數。MSVC沒有提供與Borland的IMPLIB相當的工具。你不能攫取DLL,生成一個MSVC可用的引入庫。最接近的工具是LIB.EXE,它可以通過一個DEF文件創建一個引入庫。DEF文件必須是手動創建,或利用Borland的IMPDEF工具生成的。

    沒什麼大不了的啊?你仍能創建MSVC引入庫,只是必須通過中間步驟創建一個DEF文件,然後把它傳遞給LIB.EXE工具。正確的,在你採用__cdecl函數的時候。當你轉到__stdcall的時候,問題就發生了。問題是Microsoft的LIB.EXE工具爲導出__stdcall函數的DLL生成引入庫顯得無能爲力。

    因爲這個原因,我把用__stdcall隱式連接分離出來作爲它自己的一部分。我們需要跟着一個不同步驟的次序來創建Microsoft兼容的引入庫。(同樣注意到我把這部分放到最後的好理由,至少這些步驟是冗長乏味的)。

    既然我們不能用LIB.EXE爲用__stdcall的BCB DLL生成引入庫,那我們需要提出一種不同的策略。有一種生成引入庫的方法(可能是唯一的方法),依靠只要你建造一個DLL的,MSVC就可以生成一個引入庫這一事實。如果你建造一個包含__stdcall函數的MSVC DLL,編譯器和連接器會正確的分解導出的__stdcall函數,生成引入庫。

     那麼你會問它會怎麼幫助我們呢?畢竟,我們正在用Borland C++編譯DLL。在MSVC裏創建一個DLL工程有什麼好處?我們想讓EXE用MSVC編譯,但是DLL應當保持在BCB這邊。這個問題的答案是我們在MSVC裏編譯虛假DLL工程,唯一的目的是生成一個__stdcall的引入庫。由MSVC創建的DLL可以被丟到垃圾筒裏。我們不需要它。

    這種技術是建立在虛假DLL工程的基礎之上的。我們在MSVC裏創建一個虛假DLL工程,就是得到生成Microsoft兼容的引入庫的好處。於是我們可以把這個引入庫和BCB生成的DLL相結合,再提供給MSVC用戶,使得他們可以調用我們的帶有__stdcall函數的Borland DLL。

    這是這種技術所必須的幾步。首先,用BCB編譯你的DLL。用__stdcall調用習慣,導出簡單的C函數,用extern "C"包裝所有的聲明。DLL的代碼與例2中清單4和5的代碼相同,因此我不把它們再列出來了。第二步是在MSVC裏創建虛假DLL工程。編譯虛假DLL工程,盜取生成的引入庫。最後一步是把這個引入庫添加到任一想要調用Borland DLL的MSVC工程裏。

    這一技術最有挑戰興趣的是圍繞虛假DLL工程和引入庫的基因。建造一個虛假DLL工程,用MSVC創建一個non-MFC DLL工作區。編輯MSVC工程設置,以便使生成DLL的函數與BCB DLL的名字相匹配(在我們的例子裏是bcbdll.dll)。這個設置可以在Project-Settings-Link下找到。從Borland工程目錄裏拷貝你的DLL頭文件源代碼到虛假DLL工程目錄。如果你的工程由多個CPP文件組成,那麼只需拷貝包含導出聲明的頭文件。把CPP源代碼文件添加到虛假工作區。

    下一步,進入每一個導出函數的定義,刪除每個函數實體的代碼。以一堆空函數告終。如果函數有返回值,在適當的位置保留返回語句。只是一些虛假的返回值(比如0)。除丟棄函數體之外,移除任何不必要的#include語句(你應當可以移大部分#include,因爲所有的函數體都是空的)。

    我們的BCB DLL與例2的清單4和5包含同樣的代碼。 清單8展示了同樣的代碼被修整下來後的版本。這個修整下來後的版被添加到虛假DLL工作區。





int __stdcall  Foo  (int Value)
{
    return ;
}

int __stdcall Bar  (void)
{
    return ;
}

    這時,我們應當可以在MSVC裏編譯虛假DLL工作。但是在我們編譯之前,我們必須再實行一步,以抗擊Microsoft編譯器的一些特性。我們的虛假DLL導出__stdcall函數。當Microsoft DLL導出__stdcall函數時,通常都給函數名做了修飾,添加了前端下劃線,附加了'@'符號和一個數字的結尾(見文章開始處的表1)。例如,Foo將被導出爲_Foo@4。這不是我們想要的行爲。虛假DLL的全部的目的就是爲我們的BCB DLL生成MSVC引入庫。我們的BCB DLL包含簡單的、沒有下劃線的、__stdcall函數(Foo和Bar)。它沒有給生成引入庫帶來任何好處,因爲與修飾的名字(_Foo@4和_Bar@0)不匹配 DLL包含簡單的、沒有下劃線的、__stdcall函數(Foo和Bar)。它沒有給生成引入庫帶來任何好處,因爲與修飾的名字(_Foo@4和_Bar@0)不匹配。

    幸運地,我們可以防止MSVC修飾虛假__stdcall函數,方法是添加一個DEF文件到虛假DLL工程裏。DEF文件簡單的列出每一個要導出的函數。內容如下:

LIBRARY     BCBDLL.DLL

EXPORTS
    Bar
    Foo
 
Tip 注意:

    在DEF文件裏的程序庫名應當與由MSVC生成的虛假DLL名字相匹配,而它轉而應當與用BCB創建的DLL名字相匹配。如果這三項不匹配,那麼你會運行出各種不同的錯誤(通常是未解決的連接器錯誤)。


    把DEF文件添加到虛假DLL工程裏,建造虛假DLL。當MSVC建造DLL工程的時候,它會創建一個引入庫。這個引入庫是讓MSVC用戶可以用隱式連接的方式調用導出__stdcall函數的BCB DLL的關鍵因素,把它連同你的DLL一起提供給MSVC用戶。你的用戶應當把這個引入庫添加到任何調用你的BCB DLL的MSVC工程裏。

結論

    這篇文章提供了四種技術,讓Microsoft Visual C++可以調用由BCB編譯的DLL。我希望這篇文章把每一種技術描述的很充分(這些內容有的不太容易理解)。爲了幫助你理解每一種技術,我爲每一種可代利用的技術製作了例子代碼,可以從這兒下載。zip文件解壓出四個子目錄,一個對應一種技術。每一個子目錄包含一個用BCB5 DLL工程的DLL目錄,和一個用來調用BCB DLL的MSVC工程的EXE目錄。MSVC工程是VC++ 5工程,但它們應當可以在MSVC 6下正常的工作。

下載

這篇文章的下載
bcbdll.zip 所有4種技術的源代碼 (130 kB).

 
發佈了33 篇原創文章 · 獲贊 3 · 訪問量 8萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章