天天看點

C#互操作性入門系列(二):使用平台調用調用Win32 函數

C#互操作系列文章:

<a href="http://learninghard.blog.51cto.com/6146675/1117730" target="_blank">C#互操作性入門系列(一):C#中互操作性介紹</a>

<a href="http://www.cnblogs.com/zhili/archive/2013/01/21/PInvoke.html">C#互操作性入門系列(二):使用平台調用調用Win32 函數</a>

<a href="http://learninghard.blog.51cto.com/6146675/1124530">C#互操作性入門系列(三):平台調用中的資料封送處理</a>

<a href="http://learninghard.blog.51cto.com/6146675/1127124">C#互操作性入門系列(四):在C# 中調用COM元件</a>

本專題概要:

引言

如何使用平台調用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). 在托管代碼中直接調用第二步中聲明的托管函數

C#互操作性入門系列(二):使用平台調用調用Win32 函數

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")]  

publicstaticexternint MessageBox1(IntPtr hWnd, String text, String caption, uint type);  

publicstaticexternint MessageBoxA(IntPtr hWnd, String text, String caption, uint type);  

publicstaticexternint MessageBox(IntPtr hWnd, String text, String caption, uint type);  

// 第一種指定方式,通過CharSet字段指定

       [DllImport("user32.dll", CharSet = CharSet.Unicode)]  

publicstaticexternint MessageBox2(IntPtr hWnd, String text, String caption, uint type);  

// 通過EntryPoint字段指定

       [DllImport("user32.dll", EntryPoint="MessageBoxA")]  

publicstaticexternint MessageBox3(IntPtr hWnd, String text, String caption, uint type);  

       [DllImport("user32.dll", EntryPoint = "MessageBoxW")]  

publicstaticexternint MessageBox4(IntPtr hWnd, String text, String caption, uint type);  

staticvoid 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);  

       }  

   }  

}

運作正确的結果為:

C#互操作性入門系列(二):使用平台調用調用Win32 函數

  從代碼的注釋中可以看出,第一個調用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點:

C#互操作性入門系列(二):使用平台調用調用Win32 函數

(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的情況下,也會出現同樣的亂碼問題,如下圖所示:

C#互操作性入門系列(二):使用平台調用調用Win32 函數

(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。是以下面的調用一樣也會出現亂碼,原因在第三點中已經解釋了,下面直接附上測試例子和結果:

   {    

       [DllImport("user32.dll", EntryPoint = "MessageBoxA", CharSet =  CharSet.Auto)]  

publicstaticexternint MessageBox5(IntPtr hWnd, String text, String caption, uint type);  

           MessageBox5(new IntPtr(0), "Learning Hard", "歡迎", 0);  

        }  

    }

運作結果為:

C#互操作性入門系列(二):使用平台調用調用Win32 函數

捕獲由Win32函數本身傳回異常的示範代碼如下:

using System.ComponentModel;  

namespace 處理Win32函數傳回的錯誤  

// Win32 API  

//  DWORD WINAPI GetFileAttributes(

//  _In_  LPCTSTR lpFileName

//);

// 在托管代碼中對非托管函數進行聲明

       [DllImport("Kernel32.dll",SetLastError=true,CharSet=CharSet.Unicode)]  

publicstaticexternuint GetFileAttributes(string filename);  

// 試圖獲得一個不存在檔案的屬性

// 此時調用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();  

運作結果為:

C#互操作性入門系列(二):使用平台調用調用Win32 函數

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

四、小結

   講到這裡,本專題的内容也就介紹完了,本專題隻是簡單介紹了使用平台調用技術來調用Win32函數,然而實際的操作遠遠不是這麼簡單的,要掌握平台調用的技術,還需要大家在工作過程多多實踐。因為在本專題中涉及了一些資料封送一些知識,為了幫助大家更好掌握資料封送處理,在一個專題将為大家帶來平台調用中的資料封送處理專題。

C#互操作性入門系列(二):使用平台調用調用Win32 函數

三、當調用Win32函數出錯時怎麼辦?——獲得Win32函數的錯誤資訊

  前面部分為大家示範了平台調用的使用以及使用過程需要注意的問題, 當大家了解了這些之後,肯定會有這樣的一個疑問,當調用Win32函數過程中遇到由Win32函數傳回的錯誤要怎樣去處理呢? 或者由非托管函數的托管定義導緻的錯誤或異常怎麼捕捉,就如上面代碼中調用MessageBox1出現異常時,如何捕捉并給用于一個友好的提示資訊呢?對于這個兩個問題,下面通過兩個具體的例子來示範。

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

try

               MessageBox1(new IntPtr(0), "Learning Hard", "歡迎", 0);  

catch (DllNotFoundException dllNotFoundExc)  

               Console.WriteLine("DllNotFoundException 異常發生,異常資訊為: " + dllNotFoundExc.Message);  

catch (EntryPointNotFoundException entryPointExc)  

               Console.WriteLine("EntryPointNotFoundException 異常發生,異常資訊為: " + entryPointExc.Message);  

      }  

C#互操作性入門系列(二):使用平台調用調用Win32 函數

  講到這裡,本專題的内容也就介紹完了,本專題隻是簡單介紹了使用平台調用技術來調用Win32函數,然而實際的操作遠遠不是這麼簡單的,要掌握平台調用的技術,還需要大家在工作過程多多實踐。因為在本專題中涉及了一些資料封送一些知識,為了幫助大家更好掌握資料封送處理,在一個專題将為大家帶來平台調用中的資料封送處理專題。

     本文轉自LearningHard 51CTO部落格,原文連結:http://blog.51cto.com/learninghard/1123130,如需轉載請自行聯系原作者