在 C++Builder 工程裏調用 DLL 函數

調用 Visual C++ DLL 給 C++Builder 程序員提出了一些獨特的挑戰。在咱們試圖解決 Visual C++ 生成的 DLL 以前,回顧一下如何調用一個 C++Builder 建立的 DLL 可能會有所幫助。調用 C++Builder 建立的 DLL 要比 Visual C++ 的少了許多障礙。程序員

  爲了在你的 C++Builder 工程裏調用 DLL,你須要三種元素:DLL 自己,帶有函數原型的頭文件,和引入庫(你能夠在運行時載入 DLL,而不是使用引入庫,但爲了簡單咱們按引入庫的方法作)。調用 DLL 函數,首先經過選擇菜單 Project | Add to Project 的方法,把引入庫添加到你的 C++Builder 工程裏;其次,在須要調用 DLL 函數的 C++ 源文件裏爲 DLL 頭文件插入 #include 聲明;最後添加調用 DLL 函數的代碼。編輯器

  程序清單 A 和 B 包含了作爲測試 DLL 的源代碼。注意,測試代碼實現了兩種不一樣的調用習慣(__stdcall 和 __cdecl)。這樣幫是有充分的理由的。當你設法調用一個用 Visual C++ 編譯的 DLL 時,大多讓你頭疼的事情都是因爲處理不一樣的調用習慣產生的。還要注意一點,有一個函數,它沒有明確列出使用的調用習慣。這個未知函數做爲不列出調用習慣的 DLL 函數的標識。函數

//------------------------------------------ // Listing A: DLL.H #ifdef __cplusplus extern "C" { #endif #ifdef _BUILD_DLL_ #define FUNCTION __declspec(dllexport) #else #define FUNCTION __declspec(dllimport) #endif FUNCTION int __stdcall StdCallFunction(int Value); FUNCTION int __cdecl CdeclFunction (int Value); FUNCTION int UnknownFunction(int Value); #ifdef __cplusplus } #endif //------------------------------------------ //Listing B: DLL.C #define _BUILD_DLL_ #include "dll.h" FUNCTION int __stdcall StdCallFunction(int Value) { return Value + 1; } FUNCTION int __cdecl CdeclFunction(int Value) { return Value + 2;  }   FUNCTION int UnknownFunction(int Value)  {  return Value;  }  

  從清單 A 和 B 建立測試 DLL,打開 C++Builder,選擇菜單 File | New 調出 Object Repository。選擇 DLL 圖標,單擊 OK 按鈕。C++Builder 會建立一個新的工程,帶有一個源文件。這個文件包含一個 DLL 的入口函數和一些 include 聲明。如今選擇 File | New Unit。保存新的單元爲 DLL.CPP。從清單 A 拷貝粘貼文本插入頭文件 DLL.H。從清單 B 拷貝代碼,把它插入 DLL.CPP。肯定 #define _BUILD_DLL_ 位於 #include "DLL.H" 聲明的上面。工具

  保存工程爲 BCBDLL.BPR。接下來,編譯工程,看看生成的文件。C++Builder 生成了一個 DLL 和以 .LIB 爲擴展名的引入庫。測試

  這時,你有了在 C++Builder 裏調用 DLL 所需的三個元素:DLL 自己,帶有函數原型的頭文件,用來鏈接的引入庫。如今咱們須要一個用來調用 DLL 函數的 C++Builder 工程。在 C++Builder 裏建立一個新的工程,保存到你的硬盤上。從 DLL 工程目錄裏拷貝 DLL、引入庫、DLL.H 頭文件到新的目錄。其次,在主單元裏添加 #include 聲明,包含 DLL.H。最後,添加調用 DLL 函數的代碼。清單 C 列出了調用由清單 A 和 B 生成的 DLL 中每一個函數的代碼。ui

//------------------------------------------ // Listing C: MAINFORM.CPP - DLLTest program #include #pragma hdrstop #include "MAINFORM.h" #include "dll.h" //--------------------------------------------------------- #pragma resource "*.dfm" TForm1 *Form1; //--------------------------------------------------------- __fastcall TForm1::TForm1(TComponent* Owner) : TForm(Owner) { } //--------------------------------------------------------- void __fastcall TForm1::Button1Click(TObject *Sender) { int Value = StrToInt(Edit1->Text); int Result= StdCallFunction(Value); ResultLabel->Caption = IntToStr(Result); } //--------------------------------------------------------- void __fastcall TForm1::Button2Click(TObject *Sender) { int Value = StrToInt(Edit1->Text); int Result= CdeclFunction(Value); ResultLabel->Caption = IntToStr(Result); } //--------------------------------------------------------- void __fastcall TForm1::Button3Click(TObject *Sender)  {  int Value = StrToInt(Edit1->Text);  int Result= UnknownFunction(Value); ResultLabel->Caption = IntToStr(Result);  }  

Visual C++ DLL 帶來的問題

  在理想世界裏,調用 Visual C++ 建立的 DLL 不會比調用 C++Builder 建造的 DLL 難。不幸地,Borland 和 Microsoft 有幾點不一致的地方。首先,Borland 和 Microsoft 在 OBJ 和引入庫的文件格式上不一樣(Visual C++ 使用 COFF 庫格式,而 Borland 使用 OMF 格式)。這就意味着你不能把一個 Microsoft 生成的引入庫添加到C++Builder 的工程裏。感謝 Borland IMPLIB 這個實用工具,文件格式的不一樣得以克服。this

  兩個產品在鏈接名字(linker name)習慣上也不一樣。這是 C++Builder 調用 Visual C++ DLL 的主要障礙。在 DLL 或 OBJ 裏的每個函數有一個鏈接名字。鏈接器用鏈接名字在鏈接期間解決(resolve)聲明瞭原型的函數。若是鏈接器不能找到它認爲是程序須要的鏈接名字的函數,它將產生一個未解決的外部錯誤(unresolved external error)。spa

  關於函數鏈接名字,Borland 和 Microsoft 在下面兩點上不一樣:命令行

  • 1- Visual C++ 有時修飾導出的 __stdcall 函數。
  • 2- Borland C++Builder 在引入這個被修飾的函數時,認爲是 __cdecl 函數。

  那麼,這件事爲何這樣重要呢?拿分歧#1 __stdcall 調用習慣來講。若是你用 Visual C++ 建立了一個 DLL,它包含一個 __stdcall 修飾的函數叫作 MyFunction(),Visual C++ 將給函數一個鏈接名字,爲 _MyFunction@4。當 Borland 鏈接器設法解決調用構造這個函數的時候,它認爲要找一個名爲 MyFunction 的函數。由於 Visual C++ DLL 引入庫不包含叫做 MyFunction 的函數,Borland 鏈接器報告一個未解決的外部錯誤,意識是沒有找到函數。orm

  解決這三個問題的方法要依賴 Visual C++ DLL 的編譯方式。我把整個過程分爲四步。

第1步:識別在 Visual C++ DLL 裏使用的調用習慣

  爲了與命名習慣纏結交戰,你必須首先肯定在 DLL 裏函數使用的調用習慣。你能夠經過查看 DLL 的頭文件來肯定。在 DLL 頭文件裏的函數原型形式以下:

  __declspec(dllimport) void CALLING_CONVENTION MyFunction(int nArg);

  CALLING_CONVENTION 應該是 __stdcall 或 __cdecl(具體例子參見清單 A)。不少時候,調用習慣沒有被指定,在這種狀況下默認爲 __cdecl。

第2步:檢查 DLL 裏的鏈接名字

  若是在第 1 步中顯示 DLL 利用 __stdcall 調用習慣,你須要進一步檢查 DLL,肯定 Visual C++ 在建立它時採用的命名習慣。Visual C++ 默認狀況下要修飾 __stdcall 函數,但若是寫這個 DLL 的程序員在他們的工程裏增長一個 DEF 文件,能夠阻止命名修飾。若是供應商沒有使用 DEF 文件,你的工會稍微繁瑣一些。

  命令行工具 TDUMP 容許你檢查 DLL 導出函數的鏈接名字。下面向 DLL 調用 TDUMP 的命令。

  TDUMP -ee -m MYDLL.DLL > MYDLL.LST

  TDUMP 能報告許多關於 DLL 的信息。咱們僅對 DLL 的導出函數感興趣。-ee 命令選項指示 TDUMP 僅列出導出信息。-m 開關告訴 TDUMP 按 DLL 函數的原始格式顯示。若是沒有 -m 開關,TDUMP 將嘗試把修飾過的函數轉化爲人們易讀的格式。若是 DLL 很大的話,你應該重定向 TDUMP 的輸出到一個文件裏(經過附加的 > MYDLL.LST)。

  TDUMP 爲源程序清單 A 和 B 的測試 DLL 輸出以下:

  Turbo Dump Version 5.0.16.4 Copyright (c) 1988, 1998 Borland International   Display of File DLL.DLL

  EXPORT ord:0000='CdeclFunction'   EXPORT ord:0002='UnknownFunction'   EXPORT ord:0001='_StdCallFunction@4'

  注意在 __stdcall 函數上的前綴下劃線和後綴 @4。__cdecl 和未指定調用方式的函數沒有任何修飾符。若是 Visuall C++ DLL 編譯的時候帶 DEF 文件,在 __stdcall 函數上的修飾符將不會出現。

第3步:爲 Visual C++ DLL 生成一個引入庫

  這是關鍵部分。因爲 C++Builder 和 Visual C++ 的庫文件格式不一樣,你不能把 Visual C++ 建立的引入庫添加到你的 C++Builder 工程裏。你必須用隨 C++Builder 一塊兒發行的命令行工具建立一個 OMF 格式的引入庫。依靠上面兩步得出的結論,這一步或者很順利,或者須要一些時間。

  如前面所述,C++Builder 和 Visual C++ 在關於怎樣給 DLL 函數命名上是不一致的。因爲命名習慣的不一樣,若是 C++Builder 和 Visual C++ 對 DLL 調用習慣的實現不一致,你須要建立一個帶有別名的引入庫。表 A 列出了不一致的地方。

表A:Visual C++和C++Builder命名習慣

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

  C++Builder 欄列出 Borland 鏈接器想要找的鏈接名字。第一個 Visual C++ 欄列出 Visual C++ 工程裏沒有使用 DEF 文件時的鏈接名字。第二個 Visual C++ 欄包含了使用 DEF 文件時 Visual C++ 建立的鏈接名字。注意,兩個產品僅在一種狀況下一致:Visual C++ 工程包含 DEF 文件的 __stdcall 函數。下一關,你須要建立一個帶有別名的引入庫,使 Visual C++ 命名與 C++Builder 命名相一致。

表 A 顯示出幾種你在建立引入庫時可能須要處理的組合。我把組合分紅兩種狀況。

第 1 種狀況:DLL 只包含 __stdcall 函數,DLL 供應商利用了 DEF 文件

  表 A 顯示,僅當 DLL 使用了 __stdcall 函數時 VC++ 和 C++Builder 是一致的。並且,DLL 必須帶有 DEF 文件編譯,以防止 VC++ 修飾鏈接名字。頭文件會告訴你是否使用了 __stdcall 調用習慣(第 1 步),TDUMP 將顯示函數是否被修飾(第 2 步)。若是 DLL 包含沒有被修飾的 __stdcall 函數,Visual C++ 和 C++Buidler 在給函數命名上保持一致。你能夠運行 IMPLIB 爲 DLL 建立一個引入庫。不須要別名。

IMPLIB 的命令格式以下:

  IMPLIB (destination lib name) (source dll)

例如:

  IMPLIB mydll.lib mydll.dll

第 2 種狀況:DLL 包含 __cdecl 函數或者被修飾的 __stdcall 函數

  若是你的 DLL 供營商堅持建立於編譯器無關的 DLL,你很幸運地能夠把它納入第 1 種狀況。不幸地,有幾種可能使你不能把它納入第 1 種狀況。第一,若是 DLL 供應商在函數聲明的時候省略了調用習慣,則默認爲 __cdecl,__cdecl 強迫你進入狀況 2。第二,即便你的供應商利用了 __stdcall 調用習慣,他們可能忽視了利用 DEF 文件去掉 Visual C++ 的修飾符。

  然而你找到了這裏,Good Day,歡迎來到第 2 種狀況。你被用一個函數名與 C++Builder 不一樣的 DLL 困住。擺脫這個麻煩的惟一辦法就是建立一個引入庫,爲 Visual C++ 的函數名定義一個和 C++Builder 的格式兼容的別名。幸運地,C++Builder 命令行工具容許你建立一個帶有別名的引入庫。

  第一步,用 C++Builder 帶的 IMPDEF 程序給 Visual C++ DLL 建立一個 DEF 文件。IMPDEF 建立的 DEF 文件能夠列出 DLL 導出的全部函數。你能夠這樣調用IMPDEF:

  IMPDEF (Destination DEF file) (source DLL file)

例如:

  IMPDEF mydll.def mydll.dll

  運行 IMPDEF 以後,選擇一個編輯器打開產生的 DEF 文件。對用 Visual C++ 編譯源程序清單 A 和 B 生成 DLL,IMPDEF 建立的 DEF 文件以下:

  EXPORTS    ; use this type of aliasing    ; (Borland name) = (Name exported by Visual C++)    _CdeclFunction = CdeclFunction    _UnknownFunction = UnknownFunction    StdCallFunction = _StdCallFunction@4

  下一步將修改 DEF 文件,讓 DLL 函數的別名看起來和 C++Builder 的函數同樣。你能夠這樣建立一個 DLL 函數的別名,列出一個 C++Builder 兼容的名字,後面接原始的 Visual C++ 鏈接名字。對於程序清單 A 和 B 的測試 DLL 來講,帶別名的 DEF 以下:

  EXPORTS    ; use this type of aliasing    ; (Borland name) = (Name exported by Visual C++)    _CdeclFunction = CdeclFunction    _UnknownFunction = UnknownFunction    StdCallFunction = _StdCallFunction@4

  注意,在左邊的函數名與表 A 中 Borland 兼容的名字相匹配。在右邊的函數名是真實的 Visual C++ DLL 函數的鏈接名字。

  最後一步將從別名 DEF 文件建立一個別名引入庫。你又要靠 IMPLIB 實用程序了,只是這一次,用別名 DEF 文件作爲源文件代替它原來的 DLL。格式爲:

  IMPLIB (dest lib file) (source def file)

例如:

  IMPLIB mydll.lib mydll.def

  建立了引入庫,還要繼續進行到第四步。你首先應該檢查引入庫,以保證每個 DLL 函數與 C++Builder 具備一致的命名格式。你能夠用 TLIB 實用程序檢查引入庫。

  TLIB mydll.lib, mydll.lst

爲測試 DLL 生成的列表文件以下:

 Publics by module   StdCallFunction size = 0 StdCallFunction   _CdeclFunction size = 0 _CdeclFunction   _UnknownFunction size = 0 _UnknownFunction  

第 4 步:把引入庫添加到你的工程裏

  一旦你爲 Visual C++ DLL 建立了一個引入庫,你能夠用菜單 Project | Add to Project 把它添加到你的 C++Builder 工程裏。你使用引入庫的時候沒必要考慮它是否包含有別名。把這個引入庫添加到你的工程裏的以後,建造(build)你的工程,看看是否是能夠成功的鏈接。

結束語:

  這篇文章爲你示範瞭如何在 C++Builder 工程裏調用 Visual C++ DLL 的函數。這些技巧對 C++Builder 1 和 C++Builder 3,Visual C++ 4.x 或 Visual C++ 5 建立的 DLL 生效(我尚未測試 Visual C++ 6)。

  你可能注意到,這篇文章僅討論瞭如何調用 DLL 裏 C 風格的函數。沒有嘗試去作調用 Visual C++ DLL 對象的方法。由於對於成員函數的鏈接名字被改編(mangled),C++ DLL 表現出更加困難的問題。編譯器要使用一種名字改編(name mangling)方案,以支持函數重載。不幸地,C++ 標準沒有指定編譯器應當如何改編類的方法。因爲沒有一個嚴格的標準到位,Borland 和 Microsoft 各自爲名字改編髮展了他們本身的技術,而且二者的習慣是不兼容的。在理論上,你能夠用一樣的別名技術調用位於 DLL 裏的一個類的成員函數。但你應該考慮建立一個 COM 對象來代替。COM 帶來了許多它本身的問題,但它強制執行以一種標準方式調用對象的方法。由 Visual C++ 建立的 COM 對象能夠在任一開發環境裏被調用,包括 Delphi 和 C++Builder。

  C++Builder 3.0 引入了一個新的命令行實用程序叫作 COFF2OMF.EXE。這個實用程序能夠把 Visual C++ 引入庫轉化爲 C++Builder 的引入庫。此外,對 __cdecl 函數,這個程序還會自動的產生從 Visual C++ 格式到 C++Builder 格式的別名。若是 DLL 專用 __cdecl 調用習慣,自動別名能夠簡化第 3 步。

相關文章
相關標籤/搜索