C#互操作系列文章:
- C#互操作性入門系列(一):C#中互操作性介紹
- C#互操作性入門系列(二):使用平台調用調用Win32 函數
- C#互操作性入門系列(三):平台調用中的數據封送處理
- C#互操作性入門系列(四):在C# 中調用COM組件
本專題概要:
- 引言
- 如何使用平台調用Win32 函數——從實例開始
- 當調用Win32函數出錯時怎么辦?——獲得Win32函數的錯誤信息
- 小結
一、引言
上一專題對.NET 互操作性做了一個全面的概括,其中講到.NET平台下實現互操作性有三種技術——平台調用,C++ Interop和COM Interop,今天在這個專題中將會大家介紹第一種技術,即平台調用。然而朋友們應該會有這樣的疑問,平台調用到底有什么用呢? 為什么我們要用平台調用的技術了?對於這兩個問題的答案就是——平台調用可以幫助我們實現在.NET平台下(也就是指用C#、VB.net語言寫的應用程序下)可以調用非托管函數(指定的是C/C++語言寫的函數)。這樣如果我們在.NET平台下實現的功能有現有的C/C++ 函數實現了這樣的功能,這時候我們完全沒必要自己再用托管語言(如C#、vb.net)去實現一個這樣的功能,這時候我們應該想到 “拿來主義”,直接使用平台調用技術調用C/C++ 實現的函數。然而在實際應用中,使用平台調用技術來調用Win32 API較為普遍,所以在這個專題中將為大家具體介紹了如何使用平台調用來調用Win32函數以及調用過程中應該注意的問題,下面就從一個具體的實例開始本專題的介紹。
二、如何使用平台調用Win32 函數——從實例開始
在前一個專題中已經介紹了使用平台調用來調用非托管函數的步驟:
(1). 獲得非托管函數的信息,即dll的名稱,需要調用的非托管函數名等信息
(2). 在托管代碼中對非托管函數進行聲明,並且附加平台調用所需要屬性
(3). 在托管代碼中直接調用第二步中聲明的托管函數
然而調用Win32 API函數還有一些問題需要注意的地方, 首先, 因為很多Win32 API函數都有ANSI和Unicode兩個版本,所以在托管代碼聲明時需要指定調用調用函數的版本。 然而很多Win32 API函數有ANSI和Unicode兩個版本並不是隨便說說的,而是有根據的。大家從調用步驟中可以看出,第一步就需要知道非托管函數聲明,為了找到需要需要調用的非托管函數,可以借助兩個工具——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/),如下圖:
上圖中 我用紅色標示出 MessageBox 有兩個版本,而MessageBoxA 代表的就是ANSI版本,而MessageBoxW 代筆的就是Unicode版本,這也是上面所說的依據。下面就看看 MessageBox的C++聲明的(更多的函數的定義大家可以從MSDN中找到,這里提供MessageBox的定義在MSDN中的鏈接:http://msdn.microsoft.com/en-us/library/windows/desktop/ms645505(v=vs.85).aspx ):
int WINAPI MessageBox( _In_opt_ HWND hWnd, _In_opt_ LPCTSTR lpText, _In_opt_ LPCTSTR lpCaption, _In_ UINT uType );
現在已經知道了需要調用的Win32 API 函數的定義聲明,下面就依據平台調用的步驟,在.NET 中實現對該非托管函數的調用,下面就看看.NET中的代碼的:
using System; // 使用平台調用技術進行互操作性之前,首先需要添加這個命名空間 using System.Runtime.InteropServices; namespace 平台調用Demo { class Program { // 在托管代碼中對非托管函數進行聲明,並且附加平台調用所需要屬性 // 在默認情況下,CharSet為CharSet.Ansi // 指定調用哪個版本的方法有兩種——通過DllImport屬性的CharSet字段和通過EntryPoint字段指定 // 在托管函數中聲明注意一定要加上 static 和extern 這兩個關鍵字
[DllImport("user32.dll")] public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type); // 在默認情況下,CharSet為CharSet.Ansi [DllImport("user32.dll")] public static extern int MessageBoxA(IntPtr hWnd, String text, String caption, uint type); // 在默認情況下,CharSet為CharSet.Ansi [DllImport("user32.dll")] public static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); // 第一種指定方式,通過CharSet字段指定 [DllImport("user32.dll", CharSet = CharSet.Unicode)] public static extern int MessageBox2(IntPtr hWnd, String text, String caption, uint type); // 通過EntryPoint字段指定 [DllImport("user32.dll", EntryPoint="MessageBoxA")] public static extern int MessageBox3(IntPtr hWnd, String text, String caption, uint type); [DllImport("user32.dll", EntryPoint = "MessageBoxW")] public static extern int MessageBox4(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { // 在托管代碼中直接調用聲明的托管函數 // 使用CharSet字段指定的方式,要求在托管代碼中聲明的函數名必須與非托管函數名一樣 // 否則就會出現找不到入口點的運行時錯誤 //MessageBox1(new IntPtr(0), "Learning Hard", "歡迎", 0); // 下面的調用都可以運行正確 //MessageBoxA(new IntPtr(0), "Learning Hard", "歡迎", 0); //MessageBox(new IntPtr(0), "Learning Hard", "歡迎", 0); // 使用指定函數入口點的方式調用 //MessageBox3(new IntPtr(0), "Learning Hard", "歡迎", 0); // 調用Unicode版本的會出現亂碼 MessageBox4(new IntPtr(0), "Learning Hard", "歡迎", 0); } } }
運行正確的結果為:
從代碼的注釋中可以看出,第一個調用MessageBox1會出現運行時錯誤,然而為什么改調用會出現 “無法在 DLL“user32.dll”中找到名為“MessageBox1”的入口點。”的錯誤呢? 為了知道為什么,這里就需要明白通過CharSet字段指定的這種方式的內部執行行為了。之所以會出現這個錯誤,是因為當指定CharSet為Ansi時,P/Invoke首先會通過根函數名在User32.dll中搜索,即不帶后綴A的函數名MessageBox1 進行搜索,如果找到與跟函數一樣名稱的函數,就調用該函數;
如果沒有找到則使用帶后綴為A的函數MessageBox1A進行搜索,如果找到,則使用該函數,如果還是沒有找到,則會出現 “無法在 DLL“user32.dll”中找到名為“MessageBox1”的入口點。”的錯誤。把CharSet指定為Unicode時,搜索方式是一樣的,只是沒找到根函數時會加W后綴進行搜索的。 從上面的搜索調用函數的過程中可以發現,因為user32.dll中既不存在MessageBox1函數也不存在MessageBox1A函數,所以才會出現調用錯誤。(朋友看到出現錯誤時,應該會有這樣的疑問——我們如何捕捉錯誤來顯示錯誤信息呢?這個疑問將會在下一部分解釋。)
然而使用平台調用技術中,還需要注意下面4點:
(1). DllImport屬性的ExactSpelling字段如果設置為true時,則在托管代碼中聲明的函數名必須與要調用的非托管函數名完全一致,因為從ExactSpelling字面意思可以看出為 "准確拼寫"的意思,當ExactSpelling設置為true時,此時會改變平台調用的行為,此時平台調用只會根據根函數名進行搜索,而找不到的時候不會添加 A或者W來進行再搜索,. 例如,如果指定 MessageBox,則平台調用將搜索 MessageBox,如果它找不到完全相同的拼寫則會出現找不到入口函數的錯誤。 從前面的代碼中可以看出,我們在代碼中並沒有指定 ExactSpelling 字段,然而代碼中卻沒有出現調用錯誤,這就說明在C#和托管C++語言中, ExactSpelling 默認值就是false的,然而在VB。NET中,ExactSpelling的默認值就是true, 所以以上代碼如果轉化為Vb.NET時,就需要顯式指定ExactSpelling 字段為false,不然就會出現 “找不到函數入口的錯誤”。 為了讓大家更加容易理解上面的理論,相信大家看到下面一張圖會更加理解 ExactSpelling字段的含義的:
(2). 如果采用設置CharSet的值來控制調用函數的版本時,則需要在托管代碼中聲明的函數名必須與根函數名一致,否則也會調用出錯,這點從平台調用過程中可以很好地理解,如果需要調用非托管函數名為 MessageBoxA,而你在托管代碼聲明為 MessageBox1,這樣在搜索過程中明顯就會提示找不到函數名的錯誤, 也就是上面代碼中第一個調用出錯的原因。
(3). 如果通過指定DllImport屬性的EntryPoint字段的方式來調用函數版本時,此時必須相應地指定與之匹配的CharSet設置,意思就是——如果指定EntryPoint為 MessageBoxW,那么必須將CharSet指定為CharSet.Unicode,如果指定EntryPoint為 MessageBoxA,那么必須將CharSet指定為CharSet.Ansi或者不指定,因為 CharSet默認值就是Ansi。上面代碼MessageBox4的調用之所以會出現亂碼,是因為CharSet指定為Ansi(也是默認值)時, 平台調用將字符串按照ANSI編碼方式封送到非托管內存中(在.NET 中,字符串的編碼方式默認為Unicode的),即每個字符僅占一個字節,(而對於Unicode編碼的字符串來說,字符串中的每個字符都是使用兩個字節進行編碼的),當非托管函數MessageBoxW開始執行時,它會把該內存中的數據按照Unicode編碼處理,即每兩個字節當做是一個Unicode字符,知道遇到雙字節的‘\0’ 字符結束。所以非托管函數返回的結果也就出現亂碼了。 如果指定EntryPoint 字段的值為MessageBoxA,卻把CharSet字段設置為CharSet.Unicode的情況下,也會出現同樣的亂碼問題,如下圖所示:
(4). CharSet還有一個可選字段為——CharSet.Auto, 如果把CharSet字段設置為CharSet.Auto,則平台調用會 針對目標操作系統適當地自動封送字符串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上,默認值為 Unicode;在 Windows 98 和 Windows Me 上,默認值為 Ansi。盡管公共語言運行時默認值為 Auto,但使用語言可重寫此默認值。例如,默認情況下,C# 將所有方法和類型都標記為 Ansi。所以下面的調用一樣也會出現亂碼,原因在第三點中已經解釋了,下面直接附上測試例子和結果:
class Program { [DllImport("user32.dll", EntryPoint = "MessageBoxA", CharSet = CharSet.Auto)] public static extern int MessageBox5(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { MessageBox5(new IntPtr(0), "Learning Hard", "歡迎", 0); } }
運行結果為:
三、當調用Win32函數出錯時怎么辦?——獲得Win32函數的錯誤信息
前面部分為大家演示了平台調用的使用以及使用過程需要注意的問題, 當大家了解了這些之后,肯定會有這樣的一個疑問,當調用Win32函數過程中遇到由Win32函數返回的錯誤要怎樣去處理呢? 或者由非托管函數的托管定義導致的錯誤或異常怎么捕捉,就如上面代碼中調用MessageBox1出現異常時,如何捕捉並給用於一個友好的提示信息呢?對於這個兩個問題,下面通過兩個具體的例子來演示。
捕捉由托管定義導致的異常演示代碼:
class Program { // 在托管代碼中對非托管函數進行聲明,並且附加平台調用所需要屬性 // 在默認情況下,CharSet為CharSet.Ansi // 指定調用哪個版本的方法有兩種——通過DllImport屬性的CharSet字段和通過EntryPoint字段指定 [DllImport("user32.dll")] public static extern int MessageBox1(IntPtr hWnd, String text, String caption, uint type); static void Main(string[] args) { try { MessageBox1(new IntPtr(0), "Learning Hard", "歡迎", 0); } catch (DllNotFoundException dllNotFoundExc) { Console.WriteLine("DllNotFoundException 異常發生,異常信息為: " + dllNotFoundExc.Message); } catch (EntryPointNotFoundException entryPointExc) { Console.WriteLine("EntryPointNotFoundException 異常發生,異常信息為: " + entryPointExc.Message); } Console.Read(); } }
運行結果為:
捕獲由Win32函數本身返回異常的演示代碼如下:
using System; using System.ComponentModel; // 使用平台調用技術進行互操作性之前,首先需要添加這個命名空間 using System.Runtime.InteropServices; namespace 處理Win32函數返回的錯誤 { class Program { // Win32 API // DWORD WINAPI GetFileAttributes( // _In_ LPCTSTR lpFileName //); // 在托管代碼中對非托管函數進行聲明 [DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)] public static extern uint GetFileAttributes(string filename); static void Main(string[] args) { // 試圖獲得一個不存在文件的屬性 // 此時調用Win32函數會發生錯誤 GetFileAttributes("FileNotexist.txt"); // 在應用程序的Bin目錄下存在一個test.txt文件,此時調用會成功 //GetFileAttributes("test.txt"); // 獲得最后一次獲得的錯誤 int lastErrorCode = Marshal.GetLastWin32Error(); // 將Win32的錯誤碼轉換為托管異常 //Win32Exception win32exception = new Win32Exception(); Win32Exception win32exception = new Win32Exception(lastErrorCode); if (lastErrorCode != 0) { Console.WriteLine("調用Win32函數發生錯誤,錯誤信息為 : {0}", win32exception.Message); } else { Console.WriteLine("調用Win32函數成功,返回的信息為: {0}", win32exception.Message); } Console.Read(); } } }
運行結果為:
要想獲得在調用Win32函數過程中出現的錯誤信息,首先必須將DllImport屬性的SetLastError字段設置為true,只有這樣,平台調用才會將最后一次調用Win32產生的錯誤碼保存起來,然后會在托管代碼調用Win32失敗后,通過Marshal類的靜態方法GetLastWin32Error獲得由平台調用保存的錯誤碼,從而對錯誤進行相應的分析和處理。這樣就可以獲得Win32中的錯誤信息了。
上面代碼簡單地演示了如何在托管代碼中獲得最后一次發生的Win32錯誤信息,然而還可以通過調用Win32 API 提供的FormatMessage函數的方式來獲得錯誤信息,然而這種方式有一個很顯然的弊端(所以這里就不演示了),當對FormatMessage函數調用失敗時,這時候就有可能獲得不正確的錯誤信息,所以,推薦采用.NET提供的Win32Exception異常類來獲得具體的錯誤信息。關於更多的FormatMessage函數可以參考MSDN: http://msdn.microsoft.com/en-us/library/ms679351(v=vs.85).aspx
四、小結
講到這里,本專題的內容也就介紹完了,本專題只是簡單介紹了使用平台調用技術來調用Win32函數,然而實際的操作遠遠不是這么簡單的,要掌握平台調用的技術,還需要大家在工作過程多多實踐。因為在本專題中涉及了一些數據封送一些知識,為了幫助大家更好掌握數據封送處理,在一個專題將為大家帶來平台調用中的數據封送處理專題。