簡(jiǎn)介:為什么這個(gè)這么難
如果你用BCB創(chuàng)建了一個(gè)DLL,它可以被BCB的可執(zhí)行文件調(diào)用,你知道這種使用DLL的方式?jīng)]什么難度。當(dāng)你構(gòu)造一個(gè)DLL,BCB生成一個(gè)帶“.LIB”擴(kuò)展名的引入庫(kù)。把這個(gè)LIB文件添加到你的工程里。連接器按引入庫(kù)決定DLL內(nèi)部調(diào)用。當(dāng)你運(yùn)行你的程序時(shí),DLL隱式的被載入,你不必去考慮DLL內(nèi)部調(diào)用工作是。
當(dāng)EXE文件是由Microsoft Visual C++編譯的時(shí)候,情況會(huì)變得比較復(fù)雜。有3個(gè)主要的問題。首先,BCB和MSVC對(duì)DLL中的函數(shù)命名方式是不一致的。BCB使用一種習(xí)慣,MSVC使用另一種不同的習(xí)慣。當(dāng)然,兩種習(xí)慣是不兼容的。命名問題在如何在C++Builder工程里使用VC++編譯的DLL那篇文章里已經(jīng)討論過了。表1總結(jié)了各個(gè)編譯器在各自的調(diào)用習(xí)慣下,導(dǎo)出的MyFunction函數(shù)。注意Borland給__cdecl函數(shù)前加了一個(gè)下劃線,而MSVC沒有。另一方面,MSVC認(rèn)為導(dǎo)出的__stdcall函數(shù)前面帶有下劃線,后面還有一些垃圾。
表1: Visual C++ and C++Builder 命名習(xí)慣
調(diào)用習(xí)慣 VC++命名 VC++(使用了DEF) C++Builder命名
-----------------------------------------------------------------------
__stdcall _MyFunction@4 MyFunction MyFunction
__cdecl MyFunction MyFunction _MyFunction
第2個(gè)問題是Borland引入庫(kù)與MSVC不是二進(jìn)制兼容的。當(dāng)你編譯DLL時(shí),由BCB創(chuàng)建的引入庫(kù)不能被MSVC用來(lái)連接。如果你想使用隱式連接,那么你需要?jiǎng)?chuàng)建一個(gè)MSVC格式的引入庫(kù)。另一種可選擇的辦法就是采用顯式連接(LoadLibrary和GetProcAddress)。
第3個(gè)問題是不能從dll里導(dǎo)出c++類和成員函數(shù),如果你想讓msvc的用戶也可以調(diào)用它。好吧,那不完全屬實(shí)。你的dll能導(dǎo)出c++類,但是msvc不能使用它們。原因就是c++成員函數(shù)名被編譯器改編(mangled)。這個(gè)改編的名字結(jié)果了dll。為了調(diào)用在dll里被改編的函數(shù),你必需知道被改編的是哪個(gè)函數(shù)。borland和microsoft使用了不同的名字改編方案。結(jié)果是,msvc不能恰好看到borland編譯的dll里的c++類和成員函數(shù)。
|
注意:
borland和microsoft沒有采用相同的方式改編函數(shù),因?yàn)橐勒誥nsi C++標(biāo)準(zhǔn),C++編譯器不被假定追隨相同的指導(dǎo)方針。名字改編只是實(shí)現(xiàn)的細(xì)節(jié)。
|
這三個(gè)問題使得Borland創(chuàng)建的DLL可以在MSVC里被調(diào)用變得非常困難,但并非不可能的。這篇文章描述了一套指導(dǎo)方針,你可以跟著制作與Microsoft兼容的BCB DLL。我們討論四種不同的技術(shù)。三種采用引入庫(kù)隱式連接調(diào)用,一種在運(yùn)行時(shí)利用顯式連接。
你可以跟著下面的指導(dǎo)方針摘要列表建造你的DLL。第1個(gè)列表討論隱式連接;第2個(gè)列表描述顯式連接;第3種技術(shù)采用#define組裝隱式連接;最后一個(gè)例子利用假的MSVC DLL工程為__stdcall函數(shù)創(chuàng)建引入庫(kù)。
技術(shù)1: 隱式連接
------------------------------------------------------------------------------
1- 使用__cdecl調(diào)用習(xí)慣代替__stdcall。
2- 導(dǎo)出簡(jiǎn)單的"C"風(fēng)格函數(shù),沒有C++類或成員函數(shù)。
3- 確定你有一個(gè) extern "C" {} 包圍你的函數(shù)原型。
4- 創(chuàng)建DEF文件,包含與Microsoft兼容的導(dǎo)出函數(shù)別名。別名也就是不包含前面的下劃線。
DEF文件內(nèi)容如下:
EXPORTS
; MSVC name = Borland name
Foo = _Foo
Bar = _Bar
5- 把DEF文件加入到你的工程里重新編譯它。
6- 把DLL和DLL頭文件拷貝到你的MSVC工程目錄里。
7- 運(yùn)行impdef為DLL創(chuàng)建第2個(gè)DEF文件。這個(gè)DEF文件用來(lái)創(chuàng)建引入庫(kù)。
> impdef mydll.def mydll.dll
8- 運(yùn)行Microsoft的LIB工具,用上一步創(chuàng)建的DEF文件創(chuàng)建COFF引入庫(kù)。調(diào)用格式為:
> lib /DEF mydll.def
9- 把用LIB.EXE創(chuàng)建的LIB文件添加到你的MSVC工程里。
技術(shù)2: 顯式連接
------------------------------------------------------------------------------
1- 使用__cdecl或__stdcall,如果你使用__stdcall可以跳過第4,5步。
2- 導(dǎo)出簡(jiǎn)單的"C"風(fēng)格函數(shù),沒有C++類或成員函數(shù)。
3- 確定你有一個(gè)extern "C" {}包圍你的函數(shù)原型。
4- 如果你使用__cdecl,那么你可能想去掉導(dǎo)出函數(shù)前面的下劃線,但你不必這么做。你可以用例1的第4,5步去掉下劃線。如果你沒有去掉下載線,在調(diào)用GetProcAddress函數(shù)時(shí)函數(shù)名必須前面的下劃線。
5- 把DLL拷貝到MSVC工程目錄里。
6- 在MSVC應(yīng)用程序中,使用LoadLibrary API函數(shù)載入DLL。
7- 調(diào)用GetProcAddress API在DLL里查找你想要的調(diào)用函數(shù),保存GetProcAddress函數(shù)返回的函數(shù)指針。當(dāng)你想調(diào)用函數(shù)的時(shí)候,提取函數(shù)指針。
8- 當(dāng)你用完DLL時(shí)調(diào)用FreeLibrary。
技術(shù)3: 用#define組裝隱式連接
------------------------------------------------------------------------------
1- 用__cdecl調(diào)用習(xí)慣代替__stdcall。
2- 導(dǎo)出簡(jiǎn)單的"C"風(fēng)格函數(shù),沒有C++類或成員函數(shù)。
3- 確定你有一個(gè)extern "C" {}包圍你的函數(shù)原型。
4- 在你的DLL頭文件里,為每一個(gè)導(dǎo)出函數(shù)名創(chuàng)建一個(gè)#define。
#define會(huì)調(diào)用預(yù)編譯器在每一個(gè)函數(shù)名前加上下劃線。因?yàn)槲覀冎幌霝镸SVC創(chuàng)建別名,所以代碼檢查_MSC_VER。
#ifdef _MSC_VER
#define Foo _Foo
#define Bar _Bar
#endif
5- 把DLL和DLL頭文件拷貝到MSVC工程目錄里。
6- 運(yùn)行impdef為DLL函數(shù)DEF文件。
> impdef mydll.def mydll.dll
7- 使用Microsoft的LIB工具為DEF文件創(chuàng)建COFF格式的引入庫(kù)。
>lib /def mydll.def
8- 把LIB.EXE創(chuàng)建的LIB文件添加到MSVC工程里。
技術(shù)4: 用__stdcall函數(shù)隱式連接
------------------------------------------------------------------------------
1- 當(dāng)建造你的DLL時(shí)使用__stdcall調(diào)用習(xí)慣。
2- 導(dǎo)出簡(jiǎn)單的"C"風(fēng)格函數(shù),沒有C++類或成員函數(shù)。
3- 確定你有一個(gè)extern "C" {}包圍你的函數(shù)原型。
4- 為MSVC創(chuàng)建一個(gè)引入庫(kù)。這一部分比較困難。你不能用LIB.EXE為__stdcall函數(shù)創(chuàng)建引入庫(kù)。你必須創(chuàng)建一個(gè)由MSVC編譯的的假的DLL。這樣做,按這些步驟:
4a- 用MSVC創(chuàng)建一個(gè)不使用MFC的DLL
4b- 從BCB里拷貝覆蓋DLL頭文件和DLL源代碼
4c- 編輯你的DLL源代碼,拋開每一個(gè)例程的函數(shù)體部分,使用一個(gè)假的返回值返回
4d- 配置MSVC工程生成的DLL,采用和BCB DLL同的的名字
4e- 把DEF文件添加到MSVC工程,禁止它對(duì)__stdcall命名進(jìn)行修飾(_Foo@4)
5- 編譯第4步得到的虛假DLL工程。這將會(huì)生成一個(gè)DLL(你可以把它丟到垃圾筒里)和一個(gè)LIB文件(這是你需要的)。
6- 把從第5步得到的LIB文件添加到你需要調(diào)用這個(gè)BCB DLL的MSVC工程里。LIB文件會(huì)確保連接。為MSVC可執(zhí)行文件配置BCB DLL(不是虛假DLL)。
|
注意:
一般情況下,隱式連接比顯式連接要優(yōu)先考慮,因?yàn)閷?duì)程序員來(lái)說(shuō)隱式連接更簡(jiǎn)單,而且它是類型安全的(錯(cuò)誤發(fā)生在連接時(shí)而不是運(yùn)行時(shí))。不管用哪種方法,當(dāng)你在編譯器間共享DLL時(shí),如果你選擇堅(jiān)持使用隱式連接,就必須為每一個(gè)編譯器創(chuàng)建兼容的引入庫(kù)。創(chuàng)建兼容的引入庫(kù)比用顯式連增加的負(fù)擔(dān)就是要注意更多的要求。
|
|
注意:
如果你想使你的DLL可以被Visual Basic的開發(fā)者使用,顯式連接的指導(dǎo)方針同樣適用。如果你想把你的DLL給VC開發(fā)者,按顯式連接的指導(dǎo)方針,采用__stdcall調(diào)用習(xí)慣。
|
下面4個(gè)部分詳細(xì)描述每一種技術(shù)。
這個(gè)例子詳細(xì)描述了上一部分技術(shù)1的指導(dǎo)方針。技術(shù)1的指針方針可以分為兩組。1-5項(xiàng)處理在BCB這邊編譯DLL;6-9項(xiàng)處理在MSVC這邊使用DLL。我們將沿這條主線分別進(jìn)行討論。
在這個(gè)例子里,我們將用BCB建造一個(gè)DLL,它導(dǎo)出兩個(gè)函數(shù): Foo和Bar。兩個(gè)函數(shù)都返回一個(gè)整型值。函數(shù)原型為:
int Foo (int Value);
int Bar (void);
然后我們?cè)趍svc里建造一個(gè)測(cè)試exe,用來(lái)調(diào)用borland DLL。
用BCB編譯DLL
下面兩個(gè)程序清單包含我們的DLL源代碼。清單1要在BCB和MSVC之間共享的頭文件;清單2包含我們的DLL函數(shù)實(shí)現(xiàn)部分。創(chuàng)建一個(gè)BCB DLL工程,從清單1和2中拷貝代碼粘貼到工程里。或者你可以下載這篇文章的源代碼以節(jié)省時(shí)間。BCB DLL工程已經(jīng)為你設(shè)置好了。(參見最下面的下載部分)
// ----------------------------------------------
// Listing 1- DLL header file
#ifndef BCBDLL_H
#define BCBDLL_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef BUILD_DLL
#define IMPORT_EXPORT __declspec(dllexport)
#else
#define IMPORT_EXPORT __declspec(dllimport)
#endif
IMPORT_EXPORT int __cdecl Foo (int Value);
IMPORT_EXPORT int __cdecl Bar (void);
#ifdef __cplusplus
}
#endif
#endif
// ----------------------------------------------
// ----------------------------------------------
// Listing 2- DLL source code
#include <windows.h>
#pragma hdrstop
#define BUILD_DLL
#include "bcbdll.h"
int __cdecl Foo (int Value)
{
return Value + 1;
}
int __cdecl Bar (void)
{
static int ret = 0;
return ret++;
}
// ----------------------------------------------
關(guān)于頭文件有兩個(gè)要注意的地方。首先,觀察我們用 extern "C" 的方法確保函數(shù)名不會(huì)被C++編譯器改編;其次,注意到在我們建造DLL時(shí),導(dǎo)出函數(shù)有一個(gè)特殊指示的前綴__declspec(dllexport)。當(dāng)我們從MSVC里使用DLL時(shí),函數(shù)前綴變?yōu)開_declspec(dllimport)。這個(gè)指示的改變是通過IMPORT_EXPORT宏定義實(shí)現(xiàn)的。
最后,注意我們顯式聲明了__cdecl為調(diào)用習(xí)慣。技術(shù)上,我們可以省略__cdecl關(guān)鍵字,因?yàn)開_cdecl已經(jīng)是默認(rèn)的。但是,我想不管怎樣把它列出來(lái)是一個(gè)好習(xí)慣。通過列出調(diào)用習(xí)慣,你顯式的告訴人們你選擇了__cdecl作為一個(gè)前提。同樣,默認(rèn)的調(diào)用習(xí)慣在兩個(gè)編譯器里可以通過編譯開關(guān)改變。你肯定不想這些編譯器開關(guān)影響到你DLL的可用性。
頭文件本身滿足了指導(dǎo)方針中的1-3項(xiàng) 。我們需要做的下一件事情是處理第4項(xiàng): 給導(dǎo)出函數(shù)建立別名。
首先,按現(xiàn)在的情況建造DLL代碼。其次,運(yùn)行TDUMP工具檢查函數(shù)的函數(shù)名確實(shí)包含前面的下劃線。
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'
|
注意:
使用TDUMP時(shí)別忘了用 -m 開關(guān)。TDUMP嘗試反改編(unmangle)被修飾的名字,使他們更容易閱讀。但是,當(dāng)你查看一個(gè)DLL的時(shí)候,明智的選擇是查看函數(shù)的原始格式。-m 開關(guān)告訴TDUMP顯示原始函數(shù)名。
|
像你看到的那樣,F(xiàn)oo和Bar都包含前端下劃線。至于__CPPdebugHook,你可以不理它,它是幕后操縱的,當(dāng)它不存在好了。它對(duì)你沒什么意義,你也不能讓它走開,因此就不要把它放在心上了。
為了用別名去掉下劃線,我們需要做三件事:首先創(chuàng)建DLL的DEF文件;然后調(diào)整DEF文件,為Borland名字創(chuàng)建MSVC的別名;最后,把DEF文件添加到你的BCB工程里,重建DLL。
要?jiǎng)?chuàng)建DEF文件,對(duì)DLL運(yùn)行Borland的IMPDEF工具。
C:> impdef bcbdllx.def bcbdll.dll
我選擇bcbdllx.def為文件名,因?yàn)樯院?在我們創(chuàng)建MSVC引入庫(kù)之前)我們將使用其它DEF文件。我想避免兩者混淆。bcbdllx.def內(nèi)容如下:
LIBRARY BCBDLL.DLL
EXPORTS
_Bar @1 ; _Bar
_Foo @2 ; _Foo
___CPPdebugHook @3 ; ___CPPdebugHook
注意到在Foo和Boo前端的下劃線。如果DLL把Foo和Bar導(dǎo)出為_Foo和_Bar,當(dāng)MSVC用戶設(shè)法建造他們的工程的時(shí)候,將看到連接錯(cuò)誤。我們需要?jiǎng)內(nèi)ハ聞澗€。我們用在DEF文件里給函數(shù)別名的方法實(shí)現(xiàn)。
DEF文件別名允許我們?yōu)檎鎸?shí)的函數(shù)導(dǎo)出擔(dān)當(dāng)代理或占位符的函數(shù)名。在DLL里的真實(shí)的函數(shù)仍然是_Foo和_Bar。代理名將是Foo和Bar(注意沒有了下劃線)。當(dāng)我們給兩個(gè)函數(shù)別名的時(shí)候,DLL將導(dǎo)出兩個(gè)將的符號(hào),它們歸諸于原來(lái)的函數(shù)。
完成別名, 編輯DEF文件,改變成下面的樣子:
LIBRARY BCBDLL.DLL
EXPORTS
Bar = _Bar
Foo = _Foo
這個(gè)DEF文件創(chuàng)建兩個(gè)新的出口,F(xiàn)oo和Bar,它們分別擔(dān)當(dāng)_Foo和_Bar的點(diǎn)位符。把這個(gè)DEF文件保存到你的硬盤上。一旦你完成了這些工作,便可以把DEF文件添加到你的BCB工程里,使用Project-Add菜單項(xiàng)。添加后,BCB會(huì)在工程管理器(Project Manager)的樹狀結(jié)構(gòu)里顯示出DEF文件。
一旦你把DEF文件加入到工程里,做一次完全的重建。工程連接好之后,再次對(duì)DLL運(yùn)行TDUMP,檢查從DLL里導(dǎo)出的帶下劃線函數(shù)。
>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'
對(duì)TDUMP的輸出有兩點(diǎn)要注意的事情要注意。首先,觀察Foo和Bar到場(chǎng)了(沒有前端下劃線)。現(xiàn)在DLL導(dǎo)出函數(shù)名與MSVC的一致了。還注意到原來(lái)的函數(shù),_Foo和_Bar,還在那兒。被修飾過的函數(shù)仍就從DLL里導(dǎo)出。使用DEF文件別名并不隱藏原來(lái)的函數(shù)。
你可能會(huì)想把這原來(lái)的兩個(gè)函數(shù)用什么辦法隱藏起來(lái)。但是,這么做將會(huì)危害到從BCB工程里使用DLL的人們。記得BCB的連接器期望在那兒有一個(gè)前端下劃線。如果你真的用了什么方法從DLL里把_Foo和_Bar隱藏了(以我的知識(shí)是不可能實(shí)現(xiàn)的),那么你的DLL從BCB里調(diào)用將變得非常困難。
如果TDUMP的輸出沒有列出代理函數(shù)(不帶下劃線的函數(shù)),那么返回上一步,檢查你的DEF文件。在你可以繼續(xù)之前,你需要得到別名的出現(xiàn)。如果DLL看起來(lái)OK了,那么該是轉(zhuǎn)到MSVC這邊的時(shí)間了。
從MSVC里調(diào)用DLL
一旦你擁有了一個(gè)被反改編__cdecl函數(shù)出口的DLL模型,下一步就是要為MSVC用戶生成一個(gè)引入庫(kù)。為這,你將需要?jiǎng)倓倓?chuàng)建的DLL,使用Borland的IMPDEF實(shí)用工具(再一次),和來(lái)自MSVC的LIB.EXE工具。第一步是創(chuàng)建DLL的DEF文件。為這,我建議你拷貝DLL和DLL頭文件到你的MSVC工程目錄里,在那兒工作。
C:> impdef bcbdll.def bcbdll.dll
impdef將創(chuàng)建一個(gè)def文件,內(nèi)容如下:
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文件,改變它的內(nèi)容為:
LIBRARY BCBDLL.DLL
IMPORTS
Bar @4 ; Bar
Foo @5 ; Foo
注意到我們移除了包含下劃線的函數(shù),和調(diào)試鉤掛(debug hook)函數(shù)。我們還把EXPORT改成了IMPORTS,因?yàn)槲覀儸F(xiàn)在是在引入函數(shù),而不是導(dǎo)出它們(我懷疑它對(duì)MSVC LIB.EXE來(lái)說(shuō)會(huì)產(chǎn)生不同)。
下一步,我們用Microsoft LIB.EXE,從DEF文件那兒創(chuàng)建一個(gè)COFF格式的庫(kù)。語(yǔ)法為:
lib /DEF:bcbdll.def /out:bcbdll_msvc.lib
|
注意:
MSVC命令行實(shí)用工具在默認(rèn)情況下不在你的配置的路徑里。你可能需要運(yùn)行一個(gè)MSVC帶的批處理文件,使得LIB.EXE可以被直接調(diào)用。批處理文件叫做VCVARS32.BAT,它位于DevStudio安裝路徑的\VC\BIN子目錄下。
|
這里,所有艱苦的工作都做完了。現(xiàn)在你需要做就是把你的DLL,MSVC LIB文件,和DLL文件件加入到你的MSVC客戶端。要使用DLL,需要添加LIB文件到MSVC工程里,并且在源代碼內(nèi)#include DLL頭文件。
我準(zhǔn)備了一個(gè)MSVC的簡(jiǎn)單工程來(lái)證明上面的概念。清單3給出客戶端DLL的源代碼。沒什么特別的地方,就是一個(gè)main函數(shù),一個(gè)DLL頭文件的#include,和對(duì)DLL的幾個(gè)函數(shù)調(diào)用。主要是你正確的添加了引入庫(kù),由LIB.EXE生成的那個(gè),添加到MSVC工程里。
// ----------------------------------------------
// Listing 3- MSVC DLL client code
#include <iostream>
#include <windows.h>
using namespace std;
#include "bcbdll.h"
int main()
{
cout << "Foo(10) = " << Foo(10) << endl;
cout << "Bar() = " << Bar() << endl;
cout << "Bar() = " << Bar() << endl;
cout << "Bar() = " << Bar() << endl;
return 0;
}
// ----------------------------------------------
這個(gè)例子向你展示了如何從MSVC里使用顯式連接調(diào)用BCB編譯的DLL。用顯式連接,你不必?cái)[弄?jiǎng)?chuàng)建一個(gè)MSVC兼容的引入庫(kù)。顯示連接不利的是它需要在用戶端做更多的工作,它不及隱式連接類型安全,錯(cuò)誤被延期到運(yùn)行時(shí)而不是連接時(shí)。雖然顯式連接有許多不利因素,但在某些情況下它還是十分有用的。
在這個(gè)例子里,我們將創(chuàng)建一個(gè)DLL,它導(dǎo)出兩個(gè)函數(shù):Foo和Bar。函數(shù)的原型同上一個(gè)例子一樣。
int Foo (int Value);
int Bar (void);
這一顯式連接的指導(dǎo)方針與隱式連接的相仿。我們需要導(dǎo)出簡(jiǎn)單的C函數(shù),需要防止C++名字改編。如果我們用__cdecl調(diào)用習(xí)慣,那么我們可能想要為BCB導(dǎo)出的函數(shù)建立別名,以去掉它們前端的下劃線。如果我們選擇不用別名去掉下劃線的方法,那么當(dāng)按名字載入函數(shù)時(shí),我們必須包含下劃線。換句話說(shuō),當(dāng)你對(duì)__cdecl函數(shù)起作用時(shí),你必須在某幾點(diǎn)上處理下劃線。你也可以在BCB建造DLL的時(shí)候處理下劃線,或者在運(yùn)行時(shí)調(diào)用DLL時(shí)處理它。我們利用__stdcall代替__cdecl以回避整個(gè)討論的下劃線問題。這是我們?cè)谶@個(gè)例子里要做的。清單4和5給出的我們DLL的源代碼。
|
注意:
如果你導(dǎo)出__stdcall函數(shù),至關(guān)緊要的是要讓客戶端應(yīng)用程序知道。一些人容易犯一個(gè)錯(cuò)誤,認(rèn)為使用__stdcall只不過是去掉了__cdecl函數(shù)前面的下劃線。別掉進(jìn)這個(gè)陷井。__stdcall函數(shù)處理堆棧方式也__cdecl是不同的。如果客戶端應(yīng)用程序把__stdcall當(dāng)作__cdecl函數(shù)調(diào)用(也就是,堆棧將被破壞,客戶端程序會(huì)死得很難看),將要發(fā)生一些錯(cuò)誤。
|
// ----------------------------------------------
// Listing 4- DLL header file
#ifndef BCBDLL_H
#define BCBDLL_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef BUILD_DLL
#define IMPORT_EXPORT __declspec(dllexport)
#else
#define IMPORT_EXPORT __declspec(dllimport)
#endif
IMPORT_EXPORT int __stdcall Foo (int Value);
IMPORT_EXPORT int __stdcall Bar (void);
#ifdef __cplusplus
}
#endif
#endif
// ----------------------------------------------
// ----------------------------------------------
// Listing 5- DLL source code
#include <windows.h>
#define BUILD_DLL
#include "bcbdll.h"
int __stdcall Foo (int Value)
{
return Value + 1;
}
int __stdcall Bar (void)
{
static int ret = 0;
return ret++;
}
// ----------------------------------------------
注意這段代碼幾乎與隱式連接的一模一樣。唯一不同的地方就是把Foo和Bar的調(diào)用習(xí)慣改成__stdcall代替__cdecl。
現(xiàn)在讓我們看一下調(diào)用DLL的MSVC程序代碼。代碼如清單6所示。
// ----------------------------------------------
// Listing 6- MSVC client code
#include <iostream>
#include <windows.h>
using namespace std;
HINSTANCE hDll = 0;
typedef int (__stdcall *foo_type) (int Value);
typedef int (__stdcall *bar_type) ();
foo_type Foo=0;
bar_type Bar=0;
void DLLInit()
{
hDll = LoadLibrary("bcbdll.dll");
Foo = (foo_type)GetProcAddress(hDll, "Foo");
Bar = (bar_type)GetProcAddress(hDll, "Bar");
}
void DLLFree()
{
FreeLibrary(hDll);
}
int main()
{
DLLInit();
cout << "Foo() = " << Foo(10) << endl;
cout << "Bar() = " << Bar() << endl;
cout << "Bar() = " << Bar() << endl;
cout << "Bar() = " << Bar() << endl;
DLLFree();
return 0;
}
// ----------------------------------------------
這段代碼片段里有許多需要消化的地方。首先也是最重要的,觀察代碼本身是編譯器中立的。你可以在BCB或MSVC里編譯它。我首先在BCB里編譯它,確信它可以按我所想的工作。
第二,注意到代碼沒有為#include bcbdll.h操心。有一個(gè)重要的原因。bcbdll.h為Foo和Bar函數(shù)定義的原型。但是,我們不把我們的代碼同任何預(yù)先定義的那些原型連接。通常,這些原型的存根來(lái)自引入庫(kù)。但是這個(gè)例子示范的是顯式連接,當(dāng)你顯示地連接時(shí),是不使用引入庫(kù)的,在頭文件里的Foo和Bar原型對(duì)我們來(lái)說(shuō)沒多大意義。
第三件要注意的事情是關(guān)于這段代碼里出現(xiàn)的typedef和函數(shù)指針,位于源文件的頂部附近。晃式連接需要你在運(yùn)行時(shí)用API GetProcAddrress得到DLL函數(shù)的地址。你必須把GetProcAddress返回的結(jié)果存儲(chǔ)到某個(gè)地方。最好的地點(diǎn)是把結(jié)果存儲(chǔ)到函數(shù)指針里。通過把函數(shù)地址存儲(chǔ)到函數(shù)指針里,你可以使用正常的函數(shù)調(diào)用語(yǔ)法調(diào)用函數(shù)(如 Foo(10))。
typedef聲明創(chuàng)建了兩個(gè)新的類型: foo_type和bar_type。它們都是函數(shù)指針類型。foo_type聲明了一個(gè)指向__stdcall函數(shù)的類型,這個(gè)函數(shù)打官腔一個(gè)整型參數(shù),返回一個(gè)整型值。bar_type定義了一個(gè)指向__stdcall類型的、沒有參數(shù)、有一個(gè)整型返回值的函數(shù)。這些typedef產(chǎn)生了兩個(gè)效果。第一,它們提供了清晰的方式來(lái)聲明函數(shù)指針變量Foo和Bar。第二,它們使我們可以很方便的轉(zhuǎn)換GetProcAddress返回的結(jié)果。從GetProcAddress返回的結(jié)果是一個(gè)指向__stdcall類型的、沒有參數(shù)、有一個(gè)整型返回值的函數(shù)。除非你的函數(shù)與這個(gè)格式相同,否則你需要轉(zhuǎn)換GetProcAddress的結(jié)果(這個(gè)轉(zhuǎn)換是顯式連接比隱式連接缺管類型安全的原因)。
在typedef的下面有兩個(gè)變量Foo和Bar。這兩個(gè)是函數(shù)指針變量。它們會(huì)保存我們想要調(diào)用的兩個(gè)函數(shù)的地址。注意這些變量的名字是任意的。我選擇Foo和Bar是為了使代碼像隱式連接。不要犯這樣的錯(cuò)誤,F(xiàn)oo和Bar變量名沒有與DLL里的真實(shí)函數(shù)建立連接。我們可以把變量命名為Guido和Bjarne,如果你想的話。
在函數(shù)指針聲明下面,你會(huì)看到兩個(gè)叫DllInit和DllFree的函數(shù)實(shí)體。這兩個(gè)實(shí)體處理載入DLL,查找導(dǎo)出函數(shù),和在我們使用賽后釋放程序庫(kù)。用這種方法,其余的代碼不知道DLL是顯式連接的。它可以像往常一樣調(diào)用Foo和Bar(或者Guido和Bjarne,如果你改變了名字)。唯一要協(xié)調(diào)的是你必須在調(diào)用任何DLL程序之前調(diào)用DllInit。我們也應(yīng)當(dāng)細(xì)致的,調(diào)用DllFree釋放程序庫(kù)。
|
注意:
當(dāng)在命名總題上Borland編譯器和Microsoft編譯器之間大戰(zhàn)之時(shí),GetProcAddress是你的最后一道防線。這包括Borland __cdecl命名帶一個(gè)前端下劃線(如 _Foo)。也包括改編C++名字。如果有人支持你用改編函數(shù)名字的DLL,你可以永遠(yuǎn)傳遞這些難看的參數(shù),把改編名字給GetProcAddress。不管你實(shí)際上你能調(diào)用函數(shù)而沒碰到其它的什么問題,但是至少你將會(huì)有一個(gè)機(jī)會(huì)。
|
這就是全部。在MSVC里編譯代碼,你就完成了。你不必?cái)[弄DEF文件或是引入庫(kù)。但是在你這邊的代碼里有些瑣碎的工作要處理。
這個(gè)例子展示了可能是從MSVC工程里調(diào)用BCB DLL最簡(jiǎn)單的一種方法,但它也可能是最沒有吸引力的一種方法。代碼使用一個(gè)狡詐的#define,當(dāng)檢查到是Microsoft編譯器時(shí)給__cdecl函數(shù)前加上下劃線。也就是說(shuō),我們簡(jiǎn)單的#define了Foo為_Foo。
這種技術(shù)的優(yōu)勢(shì)在于我們不必實(shí)行任何別名。 我們能直接導(dǎo)出包含下劃線的__cdecl函數(shù)。但是,我們?nèi)跃捅仨氂肕icrosoft的LIB.EXE創(chuàng)建一個(gè)MSVC兼容的引入庫(kù)。
這種技術(shù)是關(guān)鍵是MSVC不期望__cdecl函數(shù)有任何的修飾(見表1)。它們應(yīng)當(dāng)和看起來(lái)一樣。 如果MSVC應(yīng)用程序試圖執(zhí)行一個(gè)__cdecl函數(shù)Foo,它期望在DLL里查找一個(gè)沒有下劃線的函數(shù)Foo。如果我們改變MSVC的代碼,讓它調(diào)用_Foo,那么它將試圖在DLL里查找一個(gè)叫做_Foo的函數(shù)。
Borland給__cdecl函數(shù)前加上了下劃線。我們可以哄騙MSVC,讓它在調(diào)用函數(shù)的時(shí)候在函數(shù)名的前面加一個(gè)下劃線。緊記我們只想在MSVC這邊添加一個(gè)下劃線,而不是Borland這邊。
#define組裝的DLL代碼與例1里清單2的代碼完全一樣。唯一不同的是DLL頭文件。當(dāng)檢測(cè)到是MSVC時(shí),DLL頭文件為每一個(gè)函數(shù)原型加一個(gè)下劃線。清單7展示了修改后的頭文件。
// ----------------------------------------------
// Listing 7- DLL header file
#ifndef BCBDLL_H
#define BCBDLL_H
#ifdef __cplusplus
extern "C" {
#endif
#ifdef BUILD_DLL
#define IMPORT_EXPORT __declspec(dllexport)
#else
#define IMPORT_EXPORT __declspec(dllimport)
#endif
// #define kludge. If we are being compiled with MSVC, then just tack on a
// leading underscore because Borland C++ will export Foo and Bar as _Foo
// and _Bar respectively
#ifdef _MSC_VER
#define Foo _Foo
#define Bar _Bar
#endif
IMPORT_EXPORT int __cdecl Foo (int Value);
IMPORT_EXPORT int __cdecl Bar (void);
#ifdef __cplusplus
}
#endif
#endif
// ----------------------------------------------
在頭文件里,除#define組裝之外,你還必須創(chuàng)建一個(gè)MSVC兼容的引入庫(kù)。你可以按前面的步驟完成。對(duì)編譯好的DLL運(yùn)行IMPDEF,得到一個(gè)DEF文件。然后運(yùn)行Microsoft LIB.EXE工具創(chuàng)建一個(gè)COFF格式的引入庫(kù)。這時(shí),你不必考慮去編輯DEF文件。最后,拷貝DLL,COFF引入庫(kù),和DLL文件件到你的MSVC工程里。把LIB文件添加到你的MSVC工程里,重建。
這是創(chuàng)建MSVC引入庫(kù)的命令行例子。注意我們不必編輯DEF文件。我們剛好可以把它傳遞給LIB.EXE。
// Create def file
> impdef bcbdll.def bcbdll.dll
// create COFF import library using MS lib.exe
>lib /def bcbdll.def
在我們進(jìn)行之前,讓我們調(diào)查一下為什么我們需要單獨(dú)論述__stdcall函數(shù)。MSVC沒有提供與Borland的IMPLIB相當(dāng)?shù)墓ぞ摺D悴荒芫鹑LL,生成一個(gè)MSVC可用的引入庫(kù)。最接近的工具是LIB.EXE,它可以通過一個(gè)DEF文件創(chuàng)建一個(gè)引入庫(kù)。DEF文件必須是手動(dòng)創(chuàng)建,或利用Borland的IMPDEF工具生成的。
沒什么大不了的啊?你仍能創(chuàng)建MSVC引入庫(kù),只是必須通過中間步驟創(chuàng)建一個(gè)DEF文件,然后把它傳遞給LIB.EXE工具。正確的,在你采用__cdecl函數(shù)的時(shí)候。當(dāng)你轉(zhuǎn)到__stdcall的時(shí)候,問題就發(fā)生了。問題是Microsoft的LIB.EXE工具為導(dǎo)出__stdcall函數(shù)的DLL生成引入庫(kù)顯得無(wú)能為力。
因?yàn)檫@個(gè)原因,我把用__stdcall隱式連接分離出來(lái)作為它自己的一部分。我們需要跟著一個(gè)不同步驟的次序來(lái)創(chuàng)建Microsoft兼容的引入庫(kù)。(同樣注意到我把這部分放到最后的好理由,至少這些步驟是冗長(zhǎng)乏味的)。
既然我們不能用LIB.EXE為用__stdcall的BCB DLL生成引入庫(kù),那我們需要提出一種不同的策略。有一種生成引入庫(kù)的方法(可能是唯一的方法),依靠只要你建造一個(gè)DLL的,MSVC就可以生成一個(gè)引入庫(kù)這一事實(shí)。如果你建造一個(gè)包含__stdcall函數(shù)的MSVC DLL,編譯器和連接器會(huì)正確的分解導(dǎo)出的__stdcall函數(shù),生成引入庫(kù)。
那么你會(huì)問它會(huì)怎么幫助我們呢?畢竟,我們正在用Borland C++編譯DLL。在MSVC里創(chuàng)建一個(gè)DLL工程有什么好處?我們想讓EXE用MSVC編譯,但是DLL應(yīng)當(dāng)保持在BCB這邊。這個(gè)問題的答案是我們?cè)贛SVC里編譯虛假DLL工程,唯一的目的是生成一個(gè)__stdcall的引入庫(kù)。由MSVC創(chuàng)建的DLL可以被丟到垃圾筒里。我們不需要它。
這種技術(shù)是建立在虛假DLL工程的基礎(chǔ)之上的。我們?cè)贛SVC里創(chuàng)建一個(gè)虛假DLL工程,就是得到生成Microsoft兼容的引入庫(kù)的好處。于是我們可以把這個(gè)引入庫(kù)和BCB生成的DLL相結(jié)合,再提供給MSVC用戶,使得他們可以調(diào)用我們的帶有__stdcall函數(shù)的Borland DLL。
這是這種技術(shù)所必須的幾步。首先,用BCB編譯你的DLL。用__stdcall調(diào)用習(xí)慣,導(dǎo)出簡(jiǎn)單的C函數(shù),用extern "C"包裝所有的聲明。DLL的代碼與例2中清單4和5的代碼相同,因此我不把它們?cè)倭谐鰜?lái)了。第二步是在MSVC里創(chuàng)建虛假DLL工程。編譯虛假DLL工程,盜取生成的引入庫(kù)。最后一步是把這個(gè)引入庫(kù)添加到任一想要調(diào)用Borland DLL的MSVC工程里。
這一技術(shù)最有挑戰(zhàn)興趣的是圍繞虛假DLL工程和引入庫(kù)的基因。建造一個(gè)虛假DLL工程,用MSVC創(chuàng)建一個(gè)non-MFC DLL工作區(qū)。編輯MSVC工程設(shè)置,以便使生成DLL的函數(shù)與BCB DLL的名字相匹配(在我們的例子里是bcbdll.dll)。這個(gè)設(shè)置可以在project-settings-link下找到。從Borland工程目錄里拷貝你的DLL頭文件源代碼到虛假DLL工程目錄。如果你的工程由多個(gè)CPP文件組成,那么只需拷貝包含導(dǎo)出聲明的頭文件。把CPP源代碼文件添加到虛假工作區(qū)。
下一步,進(jìn)入每一個(gè)導(dǎo)出函數(shù)的定義,刪除每個(gè)函數(shù)實(shí)體的代碼。以一堆空函數(shù)告終。如果函數(shù)有返回值,在適當(dāng)?shù)奈恢帽A舴祷卣Z(yǔ)句。只是一些虛假的返回值(比如0)。除丟棄函數(shù)體之外,移除任何不必要的#include語(yǔ)句(你應(yīng)當(dāng)可以移大部分#include,因?yàn)樗械暮瘮?shù)體都是空的)。
我們的BCB DLL與例2的清單4和5包含同樣的代碼。 清單8展示了同樣的代碼被修整下來(lái)后的版本。這個(gè)修整下來(lái)后的版被添加到虛假DLL工作區(qū)。
// ----------------------------------------------
// Listing 8- dummy DLL source code
#define BUILD_DLL
#include "bcbdll.h"
int __stdcall Foo (int Value)
{
return 0;
}
int __stdcall Bar (void)
{
return 0;
}
// ----------------------------------------------
這時(shí),我們應(yīng)當(dāng)可以在MSVC里編譯虛假DLL工作。但是在我們編譯之前,我們必須再實(shí)行一步,以抗擊Microsoft編譯器的一些特性。我們的虛假DLL導(dǎo)出__stdcall函數(shù)。當(dāng)Microsoft DLL導(dǎo)出__stdcall函數(shù)時(shí),通常都給函數(shù)名做了修飾,添加了前端下劃線,附加了'@'符號(hào)和一個(gè)數(shù)字的結(jié)尾(見文章開始處的表1)。例如,F(xiàn)oo將被導(dǎo)出為_Foo@4。這不是我們想要的行為。虛假DLL的全部的目的就是為我們的BCB DLL生成MSVC引入庫(kù)。我們的BCB DLL包含簡(jiǎn)單的、沒有下劃線的、__stdcall函數(shù)(Foo和Bar)。它沒有給生成引入庫(kù)帶來(lái)任何好處,因?yàn)榕c修飾的名字(_Foo@4和_Bar@0)不匹配 DLL包含簡(jiǎn)單的、沒有下劃線的、__stdcall函數(shù)(Foo和Bar)。它沒有給生成引入庫(kù)帶來(lái)任何好處,因?yàn)榕c修飾的名字(_Foo@4和_Bar@0)不匹配。
幸運(yùn)地,我們可以防止MSVC修飾虛假__stdcall函數(shù),方法是添加一個(gè)DEF文件到虛假DLL工程里。DEF文件簡(jiǎn)單的列出每一個(gè)要導(dǎo)出的函數(shù)。內(nèi)容如下:
LIBRARY BCBDLL.DLL
EXPORTS
Bar
Foo
|
注意:
在DEF文件里的程序庫(kù)名應(yīng)當(dāng)與由MSVC生成的虛假DLL名字相匹配,而它轉(zhuǎn)而應(yīng)當(dāng)與用BCB創(chuàng)建的DLL名字相匹配。如果這三項(xiàng)不匹配,那么你會(huì)運(yùn)行出各種不同的錯(cuò)誤(通常是未解決的連接器錯(cuò)誤)。
|
把DEF文件添加到虛假DLL工程里,建造虛假DLL。當(dāng)MSVC建造DLL工程的時(shí)候,它會(huì)創(chuàng)建一個(gè)引入庫(kù)。這個(gè)引入庫(kù)是讓MSVC用戶可以用隱式連接的方式調(diào)用導(dǎo)出__stdcall函數(shù)的BCB DLL的關(guān)鍵因素,把它連同你的DLL一起提供給MSVC用戶。你的用戶應(yīng)當(dāng)把這個(gè)引入庫(kù)添加到任何調(diào)用你的BCB DLL的MSVC工程里。