當然,使用 MessageBox 你不必通過 P/Invoke,因為 .NET 框架已經具備一個 MessageBox 類,但是大量的 API 函數框架是不直接支持的,調用這些函數 時需要 P/Invoke。並且,你還可以用 P/Invoke 調用自己 DLL中輸出的 C 函數。盡管在例子中我用的是 C#,但 P/Invoke 支持任何基於 .NET 的語言,如:Visual Basic .NET 或 JScript.Net。函數名稱都相同,只是語法有差別。
注意我用 IntPtr 來聲明 HWND。盡管使 用 int 也可能行,但對於任何象 HWND,HANDLE 或 HDC 這樣的句柄,你應該始終用 IntPtr,根據平台的不同,IntPtr 會默認為 32 位或 64 位,所以你根本不用擔心句柄的大小。
DllImport 具備各種修飾符,你可以用來說明有關引入函數的細節。在上面的例子中, CharSet=CharSet.Auto 告訴框架根據目標操作系統的具體情況,將 String 作為 Unicode 或 Ansi 來傳遞。另一個鮮為人知的修飾符是 CallingConvention,回想一下在 C 語言中,我們會有不同的調用規范,通過這些規范來說明編譯器如何在函數間通過堆棧傳遞參數以及返回 值的規則。DllImport 默認的 CallingConvention 是 CallingConvention.Winapi。實際上,這是一個偽規范,對於目標平台來說,它用默認 規范;例如, Windows 平台上的 StdCall(被調用者負責清除堆棧)以及 Windows CE .Net 上的 CDecl(調用者負責清除堆棧)。CDecl 還 可以用於帶有可變參數的函數,如:printf。
Giuseppe 碰到的就是調用規范的問題。C++ 還使用第三種調用規范:即 thiscall。用這 種調用規范,編譯器借助硬件的 ECX 寄存器來向不帶可變參數的類成員函數傳遞“this”指針。我們對 Giuseppe 程序的細節並不 了解,從出錯信息來分析,他企圖從使用 StdCall 規范的 C# 程序中調用使用 thiscall 規范的 C++ 函數——這樣當然不行啦!
除了調用規范,另一個從框架調用 C++ 方法時存在的互用性問題是鏈接:C 和 C++ 使用不同形式的鏈接,因為 C++ 需要名字修飾來 支持函數重載。這就是為什麼當在 C++ 程序中聲明 C 函數時,你得用 extern "C":這樣編譯器才不會修飾函數名。在 Windows 裡,整個 Windows.h 文件(現在是 winuser.h)都包含在 extern "C" 裡。
雖然使用 P/Invoke 和 DllImport 以及完全修 飾過的名稱和 CallingConvention=ThisCall 也有辦法直接調用某個 DLL 中的 C++ 成員函數,但如果你是一個正常的人,不要去這麼做。從 托管代碼中 調用 C++ 類的正確方法——第二種選擇——是在托管包裝器中包裝你的 C++ 類。如果你的類很多,包裝可 能很繁瑣,但別無選擇。假設你有一個 C++ 類 CWidget 並想包裝它,以便 .Net 客戶端能使用它,其基本套路如下:
public __gc class Widget
{
private:
CWidget* m_pObj; // ptr to native object
public:
Widget() { m_pObj = new CWidget; }
~Widget() { delete m_pObj; }
int Method(int n) { return m_pObj->Method(n); }
// etc.
};
任何類都是這種模式:
寫一個托管類(__gc)保存一個指向本地類的指針;
編寫構造函數和 析構函數分配和銷毀對象實例;
編寫對應於 C++ 成員函數的包裝器方法;
你不必包裝所有的成員函數,僅僅包裝那些打算暴露 給托管環境的函數即可。
Figure 2 所示的是一個簡單完整而具體的例子。CPerson 是一個本地 C++ 類,包含人名,有兩個成員函數: GetName 和 SetName,後者用於修改人名。Figure 3 所示的是 CPerson 的托管包裝器。在這個例子中,我將 Get/SetName 轉換為屬性,這樣 一來,基於 .Net 的程序員就可以用屬性語法。在 C# 中是這樣用的:
// C# clIEnt
MPerson.Person p = new MPerson.Person("Fred");
String name = p.Name;
p.Name = "FreddIE";
用不用屬性純粹是編程 風格問題,我完全可以照搬本地 C++ 類的做法也輸出兩個方法:GetName 和 SetName。但屬性給人的感覺更像 .Net。包裝器類就是一個程序 集,只不過與本地 DLL 鏈接。這是托管擴展一個很酷的特性之一:你可以直接與本地 C/C++ 代碼鏈接。如果你下載並編譯我的 CPerson 例子 源代碼,你會發現 makefile 產生兩個單獨的 DLLs:person.dll 和 mperson.dll,前者實現常規的本地 DLL,後者是包裝前者的托管程序集 。還有兩個測試程序:testcpp.exe,此為調用 person.dll 的本地 C++ 程序;testcs.exe,此為用 C# 編寫的程序,它調用托管包裝器 mperson.dll(它又調用本地 person.dll)。
以上我用非常簡單的例子著重說明了托管和本地之間跨邊界通訊的僅有的幾種方法。如 Figure 4 所示:
Figure 4 互用性途徑