在 C++Builder 工程裏使用 Visual C++ DLL——第1部分:C函數

在 C++Builder 工程裏使用 Visual C++ DLL——第1部分:C函數

譯者序:

  第一次讀這篇文章是在 2001 年 10 月,幫我解決了一點小問題。本來不好意思翻譯,因爲英語水平實在太差。最近發現不少網友在問在 C++Builder 的工程裏調用 Visual C++ DLL 的問題,也許是用 C++Builder 的人比以前多了吧。於是把心一橫,不就是板兒磚嘛?“拋磚引玉”,希望它能給你幫點小忙,也歡迎指出翻譯中的錯誤。

  shadowstar 2003-6-3

shadowstar's home: http://shadowstar.126.com/

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

  很可能有一天你的老闆問你是否能用 C++Builder 創建一個 GUI,調用現有的用 Microsoft Visual C++ 編譯的 32 位 DLL。經常地,原始 DLL 的源代碼不會提供給你,也許因爲 DLL 來自第三方供應商,也可能是 22 歲的實習生不小心從網絡上刪除了 /DLL/SRC 目錄。給你一個 DLL 和頭文件,這篇文章爲你示範如何在你的 C++Builder 工程裏調用這種 DLL。

在 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 函數的標識。




extern  {








FUNCTION int __stdcall   StdCallFunction(int Value);
FUNCTION int __cdecl     CdeclFunction  (int Value);
FUNCTION int             UnknownFunction(int Value);


}









FUNCTION int __stdcall StdCallFunction(int Value)
{
    return Value + ;
}

FUNCTION int __cdecl   CdeclFunction(int Value)
{
    return Value + ;
}

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 中每個函數的代碼。









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 這個實用工具,文件格式的不同得以克服。

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

  關於函數連接名字,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 連接器報告一個未解決的外部錯誤,意識是沒有找到函數。

  解決這三個問題的方法要依賴 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 步。

<script src="../../bottom.js"></script>
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章