天天看點

C#(99):C#互操作

“為什麼我們需要掌握互操作技術的呢?” 對于這個問題的解釋就是——掌握了.NET平台下的互操作性技術可以幫助我們在.NET中調用非托管的dll和COM元件。

。.NET 平台下提供了3種互操作性的技術:

Platform Invoke(P/Invoke),即平台調用,主要用于調用C庫函數和Windows API

C++ Introp, 主要用于Managed C++(托管C++)中調用C++類庫

COM Interop, 主要用于在.NET中調用COM元件和在COM中使用.NET程式集。

使用平台調用的技術可以在托管代碼中調用動态連結庫(Dll)中實作的非托管函數,如Win32 Dll和C/C++ 建立的dll。

(1).  獲得非托管函數的資訊,即dll的名稱,需要調用的非托管函數名等資訊

(2). 在托管代碼中對非托管函數進行聲明,并且附加平台調用所需要屬性

(3). 在托管代碼中直接調用第二步中聲明的托管函數

平台調用的過程可以通過下圖更好地了解:

C#(99):C#互操作

第一步就需要知道非托管函數聲明,為了找到需要需要調用的非托管函數,可以借助兩個工具——Visual Studio自帶的dumpbin.exe和depends.exe.

dumpbin.exe 是一個指令行工具,可以用于檢視從非托管DLL中導出的函數等資訊,可以通過打開Visual Studio 2010 Command Prompt(中文版為Visual Studio 指令提示(2010)),然後切換到DLL所在的目錄,輸入 dummbin.exe/exports dllName, 如 dummbin.exe/exports User32.dll 來檢視User32.dll中的函數聲明,關于更多指令的參數可以參看MSDN;

然而 depends.exe是一個可視化界面工具,大家可以從 “VS安裝目錄\Program Files (x86)\Microsoft Visual Studio 10.0\Common7\Tools\Bin\”  這個路徑找到,然後輕按兩下  depends.exe 就可以出來一個可視化界面(如果某些人安裝的VS沒有附帶這個工具,也可以從官方網站下載下傳:http://www.dependencywalker.com/),如下圖:

C#(99):C#互操作

上圖中 用紅色标示出 MessageBox 有兩個版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代筆的就是Unicode版本,這也是上面所說的依據。下面就看看 MessageBox的C++聲明的(更多的函數的定義大家可以從MSDN中找到,這裡提供MessageBox的定義在MSDN中的連結:http://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx ):

現在已經知道了需要調用的Win32 API 函數的定義聲明,下面就依據平台調用的步驟,在.NET 中實作對該非托管函數的調用,下面就看看.NET中的代碼的:

(1). DllImport屬性的ExactSpelling字段如果設定為true時,則在托管代碼中聲明的函數名必須與要調用的非托管函數名完全一緻,因為從ExactSpelling字面意思可以看出為 "準确拼寫"的意思,當ExactSpelling設定為true時,此時會改變平台調用的行為,此時平台調用隻會根據根函數名進行搜尋,而找不到的時候不會添加 A或者W來進行再搜尋,.

C#(99):C#互操作

(2). 如果采用設定CharSet的值來控制調用函數的版本時,則需要在托管代碼中聲明的函數名必須與根函數名一緻,否則也會調用出錯

C#(99):C#互操作

(3). 如果通過指定DllImport屬性的EntryPoint字段的方式來調用函數版本時,此時必須相應地指定與之比對的CharSet設定,意思就是——如果指定EntryPoint為 MessageBoxW,那麼必須将CharSet指定為CharSet.Unicode,如果指定EntryPoint為 MessageBoxA,那麼必須将CharSet指定為CharSet.Ansi或者不指定,因為 CharSet預設值就是Ansi。

C#(99):C#互操作

(4). CharSet還有一個可選字段為——CharSet.Auto, 如果把CharSet字段設定為CharSet.Auto,則平台調用會針對目标作業系統适當地自動封送字元串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,預設值為 Unicode;在 Windows 98 和 Windows Me 上,預設值為 Ansi。

C#(99):C#互操作

捕捉由托管定義導緻的異常示範代碼:

捕獲由Win32函數本身傳回異常的示範代碼如下:要想獲得在調用Win32函數過程中出現的錯誤資訊,首先必須将DllImport屬性的SetLastError字段設定為true,隻有這樣,平台調用才會将最後一次調用Win32産生的錯誤碼儲存起來,然後會在托管代碼調用Win32失敗後,通過Marshal類的靜态方法GetLastWin32Error獲得由平台調用儲存的錯誤碼,進而對錯誤進行相應的分析和處理。

資料封送是——在托管代碼中對非托管函數進行互操作時,需要通過方法的參數和傳回值在托管記憶體和非托管記憶體之間傳遞資料的過程,資料封送處理的過程是由CLR(公共語言運作時)的封送處理服務(即封送拆送器)完成的。

封送時需要處理的資料類型分為兩種——可直接複制到本機結構中的類型(blittable)和非直接複制到本機結構中的類型(non-bittable)。

把在托管記憶體和非托管記憶體中有相同表現形式的資料類型稱為——可直接複制到本機結構中的類型,這些資料類型不需要封送拆送器進行任何特殊的處理就可以在托管和非托管代碼之間傳遞,

下面列出一些課直接複制到本機結構中的簡單資料類型:

 Windows 資料類型

非托管資料類型

托管資料類型

托管資料類型解釋

 BYTE/Uchar/UInt8

unsigned char

System.Byte

無符号8位整型

 Sbyte/Char/Int8

char

System.SByte

有符号8位整型

 Short/Int16

short

System.Int16

有符号16位整型

 USHORT/WORD/UInt16/WCHAR

unsigned short

System.UInt16

無符号16位整型

 Bool/HResult/Int/Long

long/int

System.Int32

有符号32位整型

 DWORD/ULONG/UINT

unsigned long/unsigned int

System.UInt32

無符号32位整型

 INT64/LONGLONG

_int64

System.Int64

有符号64位整型

 UINT64/DWORDLONG/ULONGLONG

_uint64

System.UInt64

無符号64位整型

 INT_PTR/hANDLE/wPARAM

void*/int或_int64

System.IntPtr

有符号指針類型

 HANDLE

void*

System.UIntPtr

無符号指針類型

 FLOAT

float

System.Single

單精度浮點數

 DOUBLE

double

System.Double

雙精度浮點數

除了上表列出來的簡單類型之外,還有一些複制類型也屬于可直接複制到本機結構中的資料類型:

(1) 資料元素都是可直接複制到本機結構中的一進制數組,如整數數組,浮點數組等

(2)隻包含可直接複制到本機結構中的格式化值類型

(3)成員變量全部都是可複制到本機結構中的類型且作為格式化類型封送的類

上面提到的格式化指的是——在類型定義時,成員的記憶體布局在聲明時就明确指定的類型。在代碼中用StructLayout屬性修飾被指定的類型,并将StructLayout的LayoutKind屬性設定為Sequential或Explicit,例如:

對于這種類型,封送器需要對它們進行相應的類型轉換之後再複制到被調用的函數中,下面列出一些非直接複制到本機結構中的資料類型:

 Bool

bool

System.Boolean

布爾類型

 WCHAR/TCHAR

char/ wchar_t

System.Char

ANSI字元/Unicode字元

 LPCSTR/LPCWSTR/LPCTSTR/LPSTR/LPWSTR/LPTSTR

const char*/const wchar_t*/char*/wchar_t*

System.String

ANSI字元串/Unicode字元串,如果非托管代碼不需要更新此字元串時,此時用String類型在托管代碼中聲明字元串類型

 LPSTR/LPWSTR/LPTSTR

Char*/wchar_t*

System.StringBuilder

ANSI字元串/Unicode字元串,如果非托管代碼需要更新此字元串,然後把更新的字元串傳回托管代碼中,此時用StringBuilder類型在托管代碼中聲明字元串

除了上表中列出的類型之外,還有很多其他類型屬于非直接複制到本機結構中的類型,例如其他指針類型和句柄類型等。

封送作為傳回值的字元串,下面是一段示範代碼,代碼中主要是調用Win32 GetTempPath函數來獲得傳回臨時路徑,此時拆送器就需要把傳回的字元串封送回托管代碼中。使用System.StringBuilder托管資料類型。

在我們實際調用Win32 API函數時,經常需要封送結構體和類等複制類型,下面就以Win32 函數GetVersionEx為例子來示範如何對作為參數的結構體進行封送處理。

下面是GetVersionEx非托管定義(更多關于該函數的資訊可以參看MSDN連結:http://msdn.microsoft.com/en-us/library/ms885648.aspx ):

參數lpVersionInformation是一個指向 OSVERSIONINFO結構體的指針類型,是以我們在托管代碼中為函數GetVersionEx函數之前,必須知道 OSVERSIONINFO結構體的非托管定義,然後再在托管代碼中定義一個等價的結構體類型作為參數。以下是OSVERSIONINFO結構體的非托管定義:

知道了OSVERSIONINFO結構體在非托管代碼中的定義之後, 現在我們就需要在托管代碼中定義一個等價的結構,并且要保證兩個結構體在記憶體中的布局相同。托管代碼中的結構體定義如下:

從上面的定義可以看出, 托管代碼中定義的結構體有以下三個方面與非托管代碼中的結構體是相同的:

字段聲明的順序

字段的類型

字段在記憶體中的大小

并且在上面結構體的定義中,我們使用到了 StructLayout 屬性,該屬性屬于System.Runtime.InteropServices命名空間(是以在使用平台調用技術必須添加這個額外的命名空間)。這個類的作用就是允許開發人員顯式指定結構體或類中資料字段的記憶體布局,為了保證結構體中的資料字段在記憶體中的順序與定義時一緻,是以指定為 LayoutKind.Sequential(該枚舉也是預設值)。

下面就具體看看在托管代碼中調用的代碼:

下面直接通過GetVersionEx函數進行封送類的處理的例子,具體代碼如下:

為了解決在.NET中的托管代碼能夠調用COM元件的問題,.NET 平台下提供了COM Interop,即COM互操作技術。

在.NET中使用COM對象,主要方法:使用TlbImp工具為COM元件建立一個互操作程式集來綁定早期的COM對象,這樣就可以在程式中添加互操作程式集來調用COM對象。

找到要使用的COM 元件并注冊它。使用 regsvr32.exe 注冊或登出 COM DLL。                

在項目中添加對 COM 元件或類型庫的引用。                

添加引用時,Visual Studio 會用到Tlbimp.exe(類型庫導入程式),Tlbimp.exe程式将生成一個 .NET Framework 互操作程式集。該程式集又稱為運作時可調用包裝 (RCW),其中包含了包裝COM元件中的類和接口。Visual Studio 将生成元件的引用添加至項目。

建立RCW中類的執行個體,這樣就可以使用托管對象一樣來使用COM對象。

在.NET中使用COM元件的過程:

C#(99):C#互操作

在建立的控制台程式裡添加”Microsoft.Office.Interop.Word 14.0.0.0 “ 這個引用

Microsoft.Office.Interop.Word.dll 确實是一個.NET程式集,并且它也叫做COM元件的互操作程式集,這個程式集中包含了COM元件中定義的類型的中繼資料, 托管代碼通過調用互操作程式集中公開的接口或對象來間接地調用COM對象和接口的。

關于通過Tlblmp.exe工具來生成互操作程式集步驟,這裡我就不多詳細訴說了,大家可以參考MSDN中這個工具詳細使用說明 :http://msdn.microsoft.com/zh-cn/library/tt0cf3sx(v=VS.80).aspx 。

  然而我們也可以使用Visual Studio中内置的支援來完成為COM類型庫建立互操作程式集的工作,我們隻需要在VS中為.NET 項目添加對應的COM元件的引用,此時VS就會自動将COM類型庫中的COM類型庫轉化為程式集中的中繼資料,并在項目的Bin目錄下生成對于的互操作程式集,是以在VS中添加COM引用,其實最後程式中引用的是互操作程式集,然後通過RCW來對COM元件進行調用。 然而對于Office中的Microsoft.Office.Interop.Wordd.dll,這個程式集也是互操作程式集,但是它又是主互操作程式集,即PIA(Primary Interop Assemblies)。主互操作程式集是一個由供應商提供的唯一的程式集,為了生成主互操作程式集,可以在使用TlbImp指令是打開 /primary 選項。

C#(99):C#互操作

從上面的結果我們看到了一個 HRESULT值,這個值真是COM代碼中傳回傳回的。在COM中,COM方法通過傳回 HRESULT 來報告錯誤;.NET 方法則通過引發異常來報告錯誤,為了友善地在托管代碼中獲得COM代碼中出現的錯誤和異常資訊,CLR提供了兩者之間的轉換,每一個代表錯誤發生的HRESULT都會被映射到.NET Framework中的一個異常類.

繼續閱讀