电竞比分网-中国电竞赛事及体育赛事平台

分享

C++Builder和Visual C++之間互相用dll的方法

 louis.sun 2010-09-13

在 C++Builder 工程里使用 Visual C++ DLL——第1部分:C函數(shù)

 

譯者序:

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

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

  很可能有一天你的老板問你是否能用 C++Builder 創(chuàng)建一個 GUI,調(diào)用現(xiàn)有的用 Microsoft Visual C++ 編譯的 32 位 DLL。經(jīng)常地,原始 DLL 的源代碼不會提供給你,也許因為 DLL 來自第三方供應(yīng)商,也可能是 22 歲的實(shí)習(xí)生不小心從網(wǎng)絡(luò)上刪除了 \DLL\SRC 目錄。給你一個 DLL 和頭文件,這篇文章為你示范如何在你的 C++Builder 工程里調(diào)用這種 DLL。

    * 在 C++Builder 工程里調(diào)用 DLL 函數(shù)
    * Visual C++ DLL 帶來的問題
    * 第1步:識別在 Visual C++ DLL 里使用的調(diào)用習(xí)慣
    * 第2步:檢查 DLL 里的連接名字
    * 第3步:為 Visual C++ DLL 生成引入庫
    * 第4步:把引入庫添加到你的工程里
    * 結(jié)束語

在 C++Builder 工程里調(diào)用 DLL 函數(shù)

  調(diào)用 Visual C++ DLL 給 C++Builder 程序員提出了一些獨(dú)特的挑戰(zhàn)。在我們試圖解決 Visual C++ 生成的 DLL 之前,回顧一下如何調(diào)用一個 C++Builder 創(chuàng)建的 DLL 可能會有所幫助。調(diào)用 C++Builder 創(chuàng)建的 DLL 要比 Visual C++ 的少了許多障礙。

  為了在你的 C++Builder 工程里調(diào)用 DLL,你需要三種元素:DLL 本身,帶有函數(shù)原型的頭文件,和引入庫(你可以在運(yùn)行時載入 DLL,而不是使用引入庫,但為了簡單我們按引入庫的方法做)。調(diào)用 DLL 函數(shù),首先通過選擇菜單 Project | Add to Project 的方法,把引入庫添加到你的 C++Builder 工程里;其次,在需要調(diào)用 DLL 函數(shù)的 C++ 源文件里為 DLL 頭文件插入 #include 聲明;最后添加調(diào)用 DLL 函數(shù)的代碼。

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

//------------------------------------------
// 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 創(chuàng)建測試 DLL,打開 C++Builder,選擇菜單 File | New 調(diào)出 Object Repository。選擇 DLL 圖標(biāo),單擊 OK 按鈕。C++Builder 會創(chuàng)建一個新的工程,帶有一個源文件。這個文件包含一個 DLL 的入口函數(shù)和一些 include 聲明?,F(xiàn)在選擇 File | New Unit。保存新的單元為 DLL.CPP。從清單 A 拷貝粘貼文本插入頭文件 DLL.H。從清單 B 拷貝代碼,把它插入 DLL.CPP。確定 #define _BUILD_DLL_ 位于 #include "DLL.H" 聲明的上面。

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

  這時,你有了在 C++Builder 里調(diào)用 DLL 所需的三個元素:DLL 本身,帶有函數(shù)原型的頭文件,用來連接的引入庫。現(xiàn)在我們需要一個用來調(diào)用 DLL 函數(shù)的 C++Builder 工程。在 C++Builder 里創(chuàng)建一個新的工程,保存到你的硬盤上。從 DLL 工程目錄里拷貝 DLL、引入庫、DLL.H 頭文件到新的目錄。其次,在主單元里添加 #include 聲明,包含 DLL.H。最后,添加調(diào)用 DLL 函數(shù)的代碼。清單 C 列出了調(diào)用由清單 A 和 B 生成的 DLL 中每個函數(shù)的代碼。

//------------------------------------------
// Listing C: MAINFORM.CPP - DLLTest program
#include <vcl\vcl.h>
#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 帶來的問題

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

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

  關(guān)于函數(shù)連接名字,Borland 和 Microsoft 在下面兩點(diǎn)上不同:

    * 1- Visual C++ 有時修飾導(dǎo)出的 __stdcall 函數(shù)。
    * 2- Borland C++Builder 在引入這個被修飾的函數(shù)時,認(rèn)為是 __cdecl 函數(shù)。

  那么,這件事為什么這樣重要呢?拿分歧#1 __stdcall 調(diào)用習(xí)慣來說。如果你用 Visual C++ 創(chuàng)建了一個 DLL,它包含一個 __stdcall 修飾的函數(shù)叫做 MyFunction(),Visual C++ 將給函數(shù)一個連接名字,為 _MyFunction@4。當(dāng) Borland 連接器設(shè)法解決調(diào)用構(gòu)造這個函數(shù)的時候,它認(rèn)為要找一個名為 MyFunction 的函數(shù)。因為 Visual C++ DLL 引入庫不包含叫作 MyFunction 的函數(shù),Borland 連接器報告一個未解決的外部錯誤,意識是沒有找到函數(shù)。

  解決這三個問題的方法要依賴 Visual C++ DLL 的編譯方式。我把整個過程分為四步。
第1步:識別在 Visual C++ DLL 里使用的調(diào)用習(xí)慣

  為了與命名習(xí)慣纏結(jié)交戰(zhàn),你必須首先確定在 DLL 里函數(shù)使用的調(diào)用習(xí)慣。你可以通過查看 DLL 的頭文件來確定。在 DLL 頭文件里的函數(shù)原型形式如下:

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

  CALLING_CONVENTION 應(yīng)該是 __stdcall 或 __cdecl(具體例子參見清單 A)。很多時候,調(diào)用習(xí)慣沒有被指定,在這種情況下默認(rèn)為 __cdecl。
第2步:檢查 DLL 里的連接名字

  如果在第 1 步中顯示 DLL 利用 __stdcall 調(diào)用習(xí)慣,你需要進(jìn)一步檢查 DLL,確定 Visual C++ 在創(chuàng)建它時采用的命名習(xí)慣。Visual C++ 默認(rèn)情況下要修飾 __stdcall 函數(shù),但如果寫這個 DLL 的程序員在他們的工程里增加一個 DEF 文件,可以阻止命名修飾。如果供應(yīng)商沒有使用 DEF 文件,你的工會稍微繁瑣一些。

  命令行工具 TDUMP 允許你檢查 DLL 導(dǎo)出函數(shù)的連接名字。下面向 DLL 調(diào)用 TDUMP 的命令。

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

  TDUMP 能報告許多關(guān)于 DLL 的信息。我們僅對 DLL 的導(dǎo)出函數(shù)感興趣。-ee 命令選項指示 TDUMP 僅列出導(dǎo)出信息。-m 開關(guān)告訴 TDUMP 按 DLL 函數(shù)的原始格式顯示。如果沒有 -m 開關(guān),TDUMP 將嘗試把修飾過的函數(shù)轉(zhuǎn)化為人們易讀的格式。如果 DLL 很大的話,你應(yīng)該重定向 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 函數(shù)上的前綴下劃線和后綴 @4。__cdecl 和未指定調(diào)用方式的函數(shù)沒有任何修飾符。如果 Visuall C++ DLL 編譯的時候帶 DEF 文件,在 __stdcall 函數(shù)上的修飾符將不會出現(xiàn)。
第3步:為 Visual C++ DLL 生成一個引入庫

  這是關(guān)鍵部分。由于 C++Builder 和 Visual C++ 的庫文件格式不同,你不能把 Visual C++ 創(chuàng)建的引入庫添加到你的 C++Builder 工程里。你必須用隨 C++Builder 一起發(fā)行的命令行工具創(chuàng)建一個 OMF 格式的引入庫。依靠上面兩步得出的結(jié)論,這一步或者很順利,或者需要一些時間。

  如前面所述,C++Builder 和 Visual C++ 在關(guān)于怎樣給 DLL 函數(shù)命名上是不一致的。由于命名習(xí)慣的不同,如果 C++Builder 和 Visual C++ 對 DLL 調(diào)用習(xí)慣的實(shí)現(xiàn)不一致,你需要創(chuàng)建一個帶有別名的引入庫。表 A 列出了不一致的地方。

表A:Visual C++和C++Builder命名習(xí)慣

調(diào)用習(xí)慣    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++ 創(chuàng)建的連接名字。注意,兩個產(chǎn)品僅在一種情況下一致:Visual C++ 工程包含 DEF 文件的 __stdcall 函數(shù)。下一關(guān),你需要創(chuàng)建一個帶有別名的引入庫,使 Visual C++ 命名與 C++Builder 命名相一致。

表 A 顯示出幾種你在創(chuàng)建引入庫時可能需要處理的組合。我把組合分成兩種情況。

第 1 種情況:DLL 只包含 __stdcall 函數(shù),DLL 供應(yīng)商利用了 DEF 文件

  表 A 顯示,僅當(dāng) DLL 使用了 __stdcall 函數(shù)時 VC++ 和 C++Builder 是一致的。而且,DLL 必須帶有 DEF 文件編譯,以防止 VC++ 修飾連接名字。頭文件會告訴你是否使用了 __stdcall 調(diào)用習(xí)慣(第 1 步),TDUMP 將顯示函數(shù)是否被修飾(第 2 步)。如果 DLL 包含沒有被修飾的 __stdcall 函數(shù),Visual C++ 和 C++Buidler 在給函數(shù)命名上保持一致。你可以運(yùn)行 IMPLIB 為 DLL 創(chuàng)建一個引入庫。不需要別名。

IMPLIB 的命令格式如下:

  IMPLIB (destination lib name) (source dll)

例如:

  IMPLIB mydll.lib mydll.dll

第 2 種情況:DLL 包含 __cdecl 函數(shù)或者被修飾的 __stdcall 函數(shù)

  如果你的 DLL 供營商堅持創(chuàng)建于編譯器無關(guān)的 DLL,你很幸運(yùn)地可以把它歸入第 1 種情況。不幸地,有幾種可能使你不能把它歸入第 1 種情況。第一,如果 DLL 供應(yīng)商在函數(shù)聲明的時候省略了調(diào)用習(xí)慣,則默認(rèn)為 __cdecl,__cdecl 強(qiáng)迫你進(jìn)入情況 2。第二,即使你的供應(yīng)商利用了 __stdcall 調(diào)用習(xí)慣,他們可能忽視了利用 DEF 文件去掉 Visual C++ 的修飾符。

  然而你找到了這里,Good Day,歡迎來到第 2 種情況。你被用一個函數(shù)名與 C++Builder 不同的 DLL 困住。擺脫這個麻煩的唯一辦法就是創(chuàng)建一個引入庫,為 Visual C++ 的函數(shù)名定義一個和 C++Builder 的格式兼容的別名。幸運(yùn)地,C++Builder 命令行工具允許你創(chuàng)建一個帶有別名的引入庫。

  第一步,用 C++Builder 帶的 IMPDEF 程序給 Visual C++ DLL 創(chuàng)建一個 DEF 文件。IMPDEF 創(chuàng)建的 DEF 文件可以列出 DLL 導(dǎo)出的所有函數(shù)。你可以這樣調(diào)用IMPDEF:

  IMPDEF (Destination DEF file) (source DLL file)

例如:

  IMPDEF mydll.def mydll.dll

  運(yùn)行 IMPDEF 之后,選擇一個編輯器打開產(chǎn)生的 DEF 文件。對用 Visual C++ 編譯源程序清單 A 和 B 生成 DLL,IMPDEF 創(chuàng)建的 DEF 文件如下:

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

  下一步將修改 DEF 文件,讓 DLL 函數(shù)的別名看起來和 C++Builder 的函數(shù)一樣。你可以這樣創(chuàng)建一個 DLL 函數(shù)的別名,列出一個 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

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

  最后一步將從別名 DEF 文件創(chuàng)建一個別名引入庫。你又要靠 IMPLIB 實(shí)用程序了,只是這一次,用別名 DEF 文件做為源文件代替它原來的 DLL。格式為:

  IMPLIB (dest lib file) (source def file)

例如:

  IMPLIB mydll.lib mydll.def

  創(chuàng)建了引入庫,還要繼續(xù)進(jìn)行到第四步。你首先應(yīng)該檢查引入庫,以保證每一個 DLL 函數(shù)與 C++Builder 具有一致的命名格式。你可以用 TLIB 實(shí)用程序檢查引入庫。

  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 創(chuàng)建了一個引入庫,你可以用菜單 Project | Add to Project 把它添加到你的 C++Builder 工程里。你使用引入庫的時候不必考慮它是否包含有別名。把這個引入庫添加到你的工程里的之后,建造(build)你的工程,看看是不是可以成功的連接。
結(jié)束語:

  這篇文章為你示范了如何在 C++Builder 工程里調(diào)用 Visual C++ DLL 的函數(shù)。這些技巧對 C++Builder 1 和 C++Builder 3,Visual C++ 4.x 或 Visual C++ 5 創(chuàng)建的 DLL 生效(我還沒有測試 Visual C++ 6)。

  你可能注意到,這篇文章僅討論了如何調(diào)用 DLL 里 C 風(fēng)格的函數(shù)。沒有嘗試去做調(diào)用 Visual C++ DLL 對象的方法。因為對于成員函數(shù)的連接名字被改編(mangled),C++ DLL 表現(xiàn)出更加困難的問題。編譯器要使用一種名字改編(name mangling)方案,以支持函數(shù)重載。不幸地,C++ 標(biāo)準(zhǔn)沒有指定編譯器應(yīng)當(dāng)如何改編類的方法。由于沒有一個嚴(yán)格的標(biāo)準(zhǔn)到位,Borland 和 Microsoft 各自為名字改編發(fā)展了他們自己的技術(shù),并且兩者的習(xí)慣是不兼容的。在理論上,你可以用同樣的別名技術(shù)調(diào)用位于 DLL 里的一個類的成員函數(shù)。但你應(yīng)該考慮創(chuàng)建一個 COM 對象來代替。COM 帶來了許多它自己的問題,但它強(qiáng)制執(zhí)行以一種標(biāo)準(zhǔn)方式調(diào)用對象的方法。由 Visual C++ 創(chuàng)建的 COM 對象可以在任一開發(fā)環(huán)境里被調(diào)用,包括 Delphi 和 C++Builder。

  C++Builder 3.0 引入了一個新的命令行實(shí)用程序叫做 COFF2OMF.EXE。這個實(shí)用程序可以把 Visual C++ 引入庫轉(zhuǎn)化為 C++Builder 的引入庫。此外,對 __cdecl 函數(shù),這個程序還會自動的產(chǎn)生從 Visual C++ 格式到 C++Builder 格式的別名。如果 DLL 專用 __cdecl 調(diào)用習(xí)慣,自動別名可以簡化第 3 步。

 

在 C++Builder 工程里使用 Visual C++ DLL——第2部分:C++ 類

 

 

source:http://www./articles/vcdll2.htm

注意:這篇文章描述如何把 C++ 類從 Visual C++ DLL 引入到 BCB 的工程中。在我們開始之前,我覺得必須給出一點(diǎn)警告。這篇文章不是真的準(zhǔn)備大量發(fā)布的。如果“文章”跌宕起伏,難以閱讀,或包含錯誤,我道賺!我沒有時間去改良它。我決定繼續(xù)并發(fā)布的唯一原因是因為很多開發(fā)者問到怎么處理這個問題。我認(rèn)為,一篇寫的很爛的文章總比什么都沒有好。我希望這個不連貫概念的搜集品會給你帶來幫助。

在上一篇文章如何“在 C++Builder 工程里使用 Visual C++ DLL”中,我描述了如何為 MSVC DLL 創(chuàng)建一個 Borland 兼容的引入庫。主要的難點(diǎn)在于 MSVC 和 Borland 使用的函數(shù)命名格式不同。舉例來說,Borland 認(rèn)為 __cdecl 函數(shù)在它們的開頭有一個下劃線,但 MSVC 沒有。幸運(yùn)的是,你可以用 Borland 命令行實(shí)用工具克服名稱的不同,這些工具有 TDUMP、IMPLIB、IMPDEF、和 COFF2OMF。方法是用這些命令行工具創(chuàng)建一個帶有 Borland 兼容函數(shù)名的 Borland 兼容引入庫。一旦你擁有了 Borland 兼容引入庫,你便可以開始工作了。你可以簡單的連接引入庫來使用 MSVC DLL。

不幸地,這種策略不能完全帶你走出這片森林。在上一篇 DLL 文章的結(jié)尾,我丟下了一個小炸彈。你只能調(diào)用 MSVC DLL 里簡單的 C 函數(shù),而不能引入類或類成員函數(shù)。Doh!

那么如果你需要從 MSVC DLL 引入 C++ 類要做些什么呢?啊……這個,如果是那樣的話,你就被關(guān)到角落里了,沒有多少可選擇的余地(通常在你退到角落里的時候,你的選項都不是令人滿意的)。這篇文描述了三種可以帶你走出角落的方法。

壞消息:當(dāng)你準(zhǔn)備花點(diǎn)時間研究這篇垃圾的時候,我覺得,再次,被迫發(fā)出警告。所有三種技術(shù)需要你有 Microsoft Visual C++。你不需要有要調(diào)用的 DLL 的源代碼,但你需要有可以調(diào)它的工具。三種技術(shù)都或多或少使用包裝技術(shù),我們用 MSVC 把 DLL 包裝成可以在 BCB 里使用的某種形式。

    * 三種技術(shù)摘要
    * 技術(shù) 1: 把 C++ 類包裹到 C 庫里
    * 技術(shù) 2: 創(chuàng)建 COM 包裝
    * 技術(shù) 3: 使用帶虛函數(shù)的抽象基類(pseudo-COM)
    * 結(jié)論
    * 下載

三種技術(shù)摘要

Ok, 現(xiàn)丑了。這就是那三種技術(shù)。

   1. 用 MSVC 創(chuàng)建一個 DLL,把 C++ 類包裹成簡單的 C 函數(shù)。簡單的 C 函數(shù)是可以在 BCB 里引入的。
   2. 用 MSVC 創(chuàng)建一個 COM 對象,把 C++ 類經(jīng)過限制包裝。BCB 可以作為 COM 客戶端來調(diào)用 VC++ COM 對象。
   3. 把 C++ 類用抽象類包裝起來,這個抽象類只帶有一些沒有實(shí)體的虛函數(shù)。這從本質(zhì)上說還是 COM,只是沒有了難看的部分。

下面描述各種技術(shù)的更多詳細(xì)內(nèi)容。在每一個例子中,我們將假定 MSVC DLL 導(dǎo)出的類形式如下:

class CFoo
{
public:
    CFoo(int x);
    ~CFoo();

    int DoSomething(int y);
};

技術(shù) 1: 把 C++ 類包裹到 C 庫里

在前一篇有關(guān) VC++ DLL 的文章里,我們知道在一個 Borland 工程里調(diào)用從一個 MSVC DLL 導(dǎo)出的簡單的 C 函數(shù)是可能的。利用這條信息可知,我們可以在 MSVC 里創(chuàng)建一個 DLL 工程,來導(dǎo)出簡單的 C 函數(shù)給 BCB 用。這個 MSVC 包裹的 DLL 會作為 C++ DLL 的客戶端。包裹 DLL 將導(dǎo)出簡單的 C 函數(shù),以創(chuàng)建的 CFoo 對象調(diào),調(diào)用 CFoo 成員函數(shù),和銷毀 CFoo 對象。

CFoo 類包含三個我們關(guān)心的函數(shù):構(gòu)造函數(shù),析構(gòu)函數(shù),和所有重要的 DoSomething 函數(shù)。我們需要把每一個函數(shù)包裹成與其等價的 C 函數(shù)。

// original class
class CFoo
{
public:
    CFoo(int x);
    ~CFoo();

    int DoSomething(int y);
};

// flattened C code
void* __stdcall new_CFoo(int x)
{
    return new CFoo(x);
}

int __stdcall CFoo_DoSomething(void* handle, int y)
{
    CFoo *foo = reinterpret_cast<CFoo *>(handle);
    return foo->DoSomething(y);
}

void __stdcall delete_CFoo(void *handle)
{
    CFoo *foo = reinterpret_cast<CFoo *>(handle);
    delete foo;
}

這里有幾個比較重要的地方要注意。首先,注意每一個 C++ 成員函數(shù)被映射為一個簡單的 C 函數(shù)。其次,觀察到我們?yōu)?C 函數(shù)明確地使用 __stdcall 調(diào)用習(xí)慣。在前一篇 DLL 文章里,我們知道簡單的調(diào)用在 MSVC DLL 里的無格式 C 函數(shù),真是很麻煩。如果我們放棄越過種種艱難困苦去用它,我們可以使這個努力稍微容易一點(diǎn)。讓 Borland 調(diào)用 Microsoft DLL 最簡單的辦法是 DLL 導(dǎo)出無格式,無修飾,__stdcall 調(diào)用習(xí)慣的 C 函數(shù)。Borland 和 Microsoft 對 __cdecl 函數(shù)的處理上是不同的。通常,他們對 __stdcall 函數(shù)也不同,因為 MSVC 修飾 __stdcall 函數(shù),但我們可以通過添加一個 DEF 文件到 MSVC 工程里來阻止這種行為。參見下載部分的例子有 DEF 文件的例子。

其它關(guān)于代碼要注意的事情是 new_CFoo 函數(shù)返回一個指向 CFoo 對象的指針。BCB 調(diào)用者必須在本地保存這個指針。這可能看起來和這篇文章的主題有點(diǎn)矛盾。畢竟,我想 BCB 不能使用來自 MSVC DLL 的 C++ 對象?如果那是正確的,那么為什么我們還要返回一個 CFoo 對象指針呢?

答案是 BCB 不能調(diào)用 MSVC DLL 導(dǎo)出類的成員函數(shù)。但是,這并不意味著它不能存儲這樣對象的地址。new_CFoo 返回的是一個 CFoo 對象的指針。BCB 客戶端可以存儲這個指針,但不能用。BCB 不能廢棄它(不應(yīng)當(dāng)嘗試這么做)。讓這個觀點(diǎn)更容易理解一點(diǎn),new_CFoo 返回一個空指針(總之它不能返回別的什么東西)。在 BCB 這邊,除了存儲它,然后把它傳回給 DLL,沒有什么可以安全地處理這個空指針的方法。

Ok,在我們繼續(xù)前進(jìn)之前,還有另外兩個要十分注意的地方。首先,注意 CFoo_DoSomething 把空指針作為它的第一個參數(shù)。這個空指針與 new_CFoo 返回的是同一個空指針。空指針用 reinterpret_cast 被追溯到 CFoo 對象(你知道,當(dāng)你看到一個 reinterpret_cast 的時候,你正在處理是難看的代碼)。DoSomething 成員函數(shù)在轉(zhuǎn)換之后被調(diào)用。最后,注意空指針也是 delete_CFoo 函數(shù)的參數(shù)。包裝 DLL 刪除對象是至關(guān)緊要的。你不應(yīng)當(dāng)在 BCB 里對空指針調(diào)用 delete。顯然它不會按你想的去做。

下面的程序清單展示了 C 函數(shù)的 DLL 頭文件。這個頭文件可以在 MSVC 和 BCB 之間共享。

// DLL header file
#ifndef DLL_H
#define DLL_H

#ifdef BUILD_DLL
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#else

#ifdef __cplusplus
extern "C" {
#endif

DLLAPI void* __stdcall new_CFoo(int x);
DLLAPI int   __stdcall CFoo_DoSomething(void* handle, int y);
DLLAPI void  __stdcall delete_CFoo(void *handle);

#ifdef __cplusplus
}
#endif

#endif

這是一個典型的 DLL 頭文件。注意到一個令人好奇的事情,在頭文件里看不到 CFoo 類。頭文件僅包含用以包裝 CFoo 的無格式 C 函數(shù)。

下面的程序清單展示了如何在 BCB 里調(diào)用 DLL。

#include "dll.h"

void bar()
{
    int x = 10;
    int y = 20;
    int z;

    void * foo = new_CFoo(x);
    z = CFoo_DoSomething(foo, y);
    delete_CFoo(foo);
}

這樣就可以了。盡管不太漂亮,但還能用。事實(shí)上,不管這個技術(shù)多么奇異,在其它一些不能調(diào)用 DLL 的情形,同樣可以用這種方法。舉例來說,Delphi 程序員使用相同的技術(shù),因為 Delphi 不能調(diào)用 C++ 成員函數(shù)。Delphi 程序員必須把 C++ 類包裹成 C 代碼,并連接成 C OBJ 文件。開源工具 SWIG (swig.org) 被設(shè)計用來生成象這樣的包裝函數(shù),在那里允許你使用類似 Python 的角本語言調(diào)用 C++ 對象。
技術(shù) 2: 創(chuàng)建 COM 包裝

不幸地,我還沒有這種技術(shù)的例子(嗨,我說過這篇文章不是為黃金時段準(zhǔn)備的)。但這個主意是這樣工作的。在 MSVC 里創(chuàng)建一個 COM 對象?;蛟S你可以運(yùn)行向?qū)?。?chuàng)建一個進(jìn)程內(nèi)服務(wù)器(如 DLL,不是 EXE)。同樣,確認(rèn)你創(chuàng)建了一個 COM 對象,而不是自動控制對象。自動控制只會是使每一件事更困難。除非你也需要在 VB 或 ASP 頁面用 C++ 類,那也可以用無格式 COM,而不用自動控制。

在 COM 工程內(nèi)部,創(chuàng)建一個新的 COM 對象。MSVC 大概想讓你創(chuàng)建一個 COM 接口。既然我們正在包裝一個稱做 CFoo 的類,一個好的接口名應(yīng)當(dāng)是 IFoo。MSVC 也會讓你為執(zhí)行類的 COM 對象命名。CFooImpl 是一個不錯的候選者。

COM 對象應(yīng)當(dāng)用聚合包裝 C++ DLL 類。換句話說,COM 對象包含 CFoo 成員變量。不要設(shè)法從 CFoo 繼承你的 COM 類。對每一個 C++ DLL 類(CFoo)的成員函數(shù),在你的 COM 對象里創(chuàng)建一個類似的函數(shù)。如果可能的話,用相同的名字,傳遞相同的參數(shù),返回相同類型的值。你需要調(diào)整一些事情。比如,字符串在 COM 里通常被傳遞為 BSTR。同樣,返回值被特別地傳遞為輸出參數(shù),因為 COM 方法應(yīng)當(dāng)返回一個錯誤代碼。當(dāng)你做完這些,C++ 類的每一個成員函數(shù)在 COM 包裝里應(yīng)當(dāng)有一個相應(yīng)的函數(shù)。

在你 build COM 包裝之后,用 regsrv32.exe 注冊它。一旦注冊,你應(yīng)當(dāng)能例示這個 COM 對象,并且用 BCB 代碼調(diào)用它包裝的成員函數(shù)。

再一次,我為上面介紹的這種技術(shù)沒有可運(yùn)行的演示道歉。
技術(shù) 3: 使用帶虛函數(shù)的抽象基類(pseudo-COM)

技術(shù) 3 是一種 pseudo-COM 方法。COM 是一個二進(jìn)制對象規(guī)范。COM 對象可以被 BCB 和 MSVC 調(diào)用,而不管 COM 對象是用什么編譯器編譯的。因此,這個二進(jìn)制用什么魔法工作的呢?答案就是基于要講的這種技術(shù)。

COM 函數(shù)調(diào)用通過函數(shù)查找表來分派。神奇地是這個函數(shù)查找表與 C++ 虛函數(shù)表用同樣的方法正確地工作。事實(shí)上,他們就是相同的。COM 不過是虛函數(shù)和虛函數(shù)表的一種美稱的形式。

COM 可以工作,是因為 BCB 和 MSVC 真正使用相同的虛分派系統(tǒng)。COM 依賴于大多數(shù) Win32 C++ 編譯器都用相同的方法生成和使用 vtable 的這個事實(shí)。因為兩個編譯器用相同的虛分派系統(tǒng),我們就能在 MSVC 里用虛函數(shù)創(chuàng)建一個包裝類,它可以被 BCB 調(diào)用。這正是 COM 所做的。

這是 pseudo-COM 包裝類的 DLL 頭文件。它包括一個抽象基類,IFoo,它服務(wù)于 pseudo-COM 接口。它還包括兩個 C 函數(shù),用來創(chuàng)建和刪除 IFoo 對象。這個頭文件在 MSVC 和 BCB 之間共享。

#ifndef DLL_H
#define DLL_H

#ifdef BUILD_DLL
#define DLLAPI __declspec(dllexport)
#else
#define DLLAPI __declspec(dllimport)
#endif

// psuedo COM interface
class IFoo
{
public:
    virtual int __stdcall DoSomething(int x) = 0;
    virtual __stdcall ~IFoo() = 0;
};

#ifdef __cplusplus
extern "C" {
#endif

DLLAPI IFoo*  __stdcall new_IFoo(int x);
DLLAPI void   __stdcall delete_IFoo(IFoo *f);

#ifdef __cplusplus
}
#endif

#endif

注意到兩個 C 函數(shù)類似技術(shù) 1 的函數(shù),除了現(xiàn)在它們與 IFoo 合作,而不是空指針。這種技術(shù)比第一種提供了更多的類型安全。

這里是 MSVC 包裝的源代碼。它包括一個從 IFoo 繼承而來的稱作 CFooImpl 的類。CFooImpl 是 IFoo 接口的實(shí)現(xiàn)。

#define BUILD_DLL

#include "dll.h"

IFoo::~IFoo()
{
 // must implement base class destructor
 // even if its abstract
}

// Note: we declare the class here because no one outside needs to be concerned
//       with it.
class CFooImpl : public IFoo
{
private:
    CFoo  m_Foo; // the real C++ class from the existing MSVC C++ DLL
public:
    CFooImpl(int x);
    virtual ~CFooImpl();
    virtual int __stdcall DoSomething(int x);
};

CFooImpl::CFooImpl(int x)
    : m_Foo(x)
{
}

int __stdcall CFooImpl::DoSomething(int x)
{
    return m_Foo.DoSomething(x);
}

CFooImpl::~CFooImpl()
{
}

IFoo * __stdcall new_IFoo(int x)
{
    return new CFooImpl(x);
}

void __stdcall delete_IFoo(IFoo *f)
{
    delete f;
}

這兒有許多好的素材資料。首先,注意到現(xiàn)在我們有一個類在 BCB 和 MSVC 之間共享的頭文件。好象是一件好事。更重要的是,注意到 BCB 工程將只與 IFoo 類打交道。真正的 IFoo 實(shí)現(xiàn)由叫做 CFooImpl 的派生類提供,那是在 MSVC 工程內(nèi)部。

BCB 客戶端代碼將與 IFoo 對象以多態(tài)性合作。要得到一個包裝實(shí)例,BCB 代碼可以調(diào)用 new_IFoo 函數(shù)。new_IFoo 的工作像一個函數(shù)工廠,提供新的 IFoo 實(shí)例。new_Foo 返回一個指向 IFoo 實(shí)例的指針。然而,指針是多態(tài)的。指針的靜態(tài)類型是 IFoo,但它實(shí)際的動態(tài)類型將被指向 CFooImpl(BCB 代碼是不知道真相的)。

這是 BCB 客戶端的代碼。

#include "dll.h"

void bar()
{
    int x = 10;
    int y = 20;
    int z;


    IFoo *foo = new_IFoo(x);
    z = foo->DoSomething(y);
    delete_IFoo(foo);
}

現(xiàn)在給出在技術(shù) 3 上某些部分的注釋。第一,至關(guān)緊要的是你從 MSVC DLL 里刪除 IFoo 指針。這個由調(diào)用 delete_IFoo 函數(shù)傳遞 IFoo 指針完成。不要嘗試從 BCB 里刪除對象。

void bar()
{
    IFoo *foo = new_IFoo(x);
    delete foo;               // BOOM!!!
}

這段代碼將在痛苦中死去。問題是 IFoo 是被在 MSVC 包裝 DLL 里的 new_IFoo 函數(shù)創(chuàng)建的。同樣地,IFoo 對象占的內(nèi)存是被 MSVC 內(nèi)存管理器分配的。當(dāng)你刪除一個對象時,只有權(quán)刪除和它用同一個內(nèi)存管理器創(chuàng)建的對象。如果你在 BCB 這邊對指針調(diào)用 delete,那么你是用 Borland 內(nèi)存管理器刪除。現(xiàn)在,我可能錯了,但是我愿意拿我的房子和一個生殖器打賭,要么二個,不能企圖讓 Microsoft 內(nèi)存管理器和 Borland 內(nèi)存管理器一起工作。當(dāng)你用 Borland 內(nèi)存管理器刪除指針的時候,難道它會嘗試聯(lián)系 Microsoft 內(nèi)存管理器,讓它知道它應(yīng)當(dāng)釋放的哪些內(nèi)存?

另外解釋一項,BCB 代碼完全按照 IFoo 虛函數(shù)接口工作。在 BCB 這邊你看不到任何 CFooImpl 類的事件。CFooImpl 在 MSVC 包裝工程的內(nèi)存。當(dāng)你從 BCB 這邊調(diào)用 DoSomething 的時候,調(diào)用通過虛函數(shù)表被分派到 CFooImpl。

如果你在這個概念上理解有困難的話,不要著急。我或許沒有把它描述的很好。下面的內(nèi)容可以幫助理解,在 BCB 這邊,你可以用 CPU viewer 單步跟蹤代碼。它允許你單步跟蹤每一條匯編指令,看看 vtable 是怎么進(jìn)行查找工作的。
Tip  注意:

如果你使用這種 pseudo-COM 技術(shù),確定你沒有嘗試重載虛函數(shù)。換句話說,不要創(chuàng)建象這樣的接口:

class IFoo
{
public:
    virtual int __stdcall DoSomething(int x) = 0;
    virtual int __stdcall DoSomething(float x) = 0;
    virtual int __stdcall DoSomething(const char *x) = 0;
};

不應(yīng)當(dāng)重載虛接口函數(shù)的原因是 MSVC 和 BCB 在 vtable 上不可能(或許不會)制定相同的方法。當(dāng)我試驗重載時,在 BCB 這邊調(diào)用 DoSomething(int),在 MSVC 那邊象是分派到 DoSomething(float)。Borland 和 Microsoft 在 vtable 格式上不重載的時候是一致的。這可能解釋了為什么 COM 對象不使用重載函數(shù)。

If you need to wrap a C++ class with overloaded functions, then you should create a distinct function name for each one.

class IFoo
{
public:
    virtual int __stdcall DoSomething_int  (int x) = 0;
    virtual int __stdcall DoSomething_float(float x) = 0;
    virtual int __stdcall DoSomething_str  (const char *x) = 0;
};


結(jié)論:

Ok, 我們到哪兒了?啊,在文章開始,我們講了關(guān)于為什么 BCB 不能調(diào)用 DLL 里的 C++ 成員函數(shù),如果 DLL 是被 MSVC 編譯的。原因就是兩種編譯器在成員函數(shù)命名上不一致。我們討論了三種(有點(diǎn)討厭)工作方法。每一種工作方法由一個為 C++ DLL 而建立的 MSVC 包裝 DLL。包裝 DLL 用一些 BCB 可以理解的格式揭露 C++ 類。第一種技術(shù)把每一個 C++ 類的成員函數(shù)包裹成無格式的 C 函數(shù)。第二種技術(shù)把每一個成員函數(shù)映射成 COM 對象的成員。最后一種技術(shù)依賴虛函數(shù)是按查找表分派而不是名稱的事實(shí)。在這種策略里,每一個 C++ 成員函數(shù)被映射成一個抽象類的虛函數(shù)。

下載部分包括這篇文章的例子代碼。第一個下載包含原始的 MSVC C++ DLL,我們設(shè)法與它合作。三種技術(shù)的每一個例程使用相同的 DLL。仍就沒有為技術(shù) 2 準(zhǔn)備例子。
下載

    本站是提供個人知識管理的網(wǎng)絡(luò)存儲空間,所有內(nèi)容均由用戶發(fā)布,不代表本站觀點(diǎn)。請注意甄別內(nèi)容中的聯(lián)系方式、誘導(dǎo)購買等信息,謹(jǐn)防詐騙。如發(fā)現(xiàn)有害或侵權(quán)內(nèi)容,請點(diǎn)擊一鍵舉報。
    轉(zhuǎn)藏 分享 獻(xiàn)花(0

    0條評論

    發(fā)表

    請遵守用戶 評論公約

    類似文章 更多