[轉]C# 互操做性入門系列(二):使用平臺調用調用Win32 函數

傳送門html

C#互操做系列文章:windows

  1. C# 互操做性入門系列(一):C#中互操做性介紹
  2. C# 互操做性入門系列(二):使用平臺調用調用Win32 函數
  3. C# 互操做性入門系列(三):平臺調用中的數據封送處理
  4. C# 互操做性入門系列(四):在C#中調用COM組件

本專題概要:函數

  • 引言
  • 如何使用平臺調用Win32 函數——從實例開始
  • 當調用Win32函數出錯時怎麼辦?——得到Win32函數的錯誤信息
  • 小結

1、引言工具

上一專題對.NET 互操做性作了一個全面的歸納,其中講到.NET平臺下實現互操做性有三種技術——平臺調用,C++ Interop和COM Interop,今天在這個專題中將會你們介紹第一種技術,即平臺調用。然而朋友們應該會有這樣的疑問,平臺調用到底有什麼用呢? 爲何咱們要用平臺調用的技術了?對於這兩個問題的答案就是——平臺調用能夠幫助咱們實如今.NET平臺下(也就是指用C#、VB.net語言寫的應用程序下)能夠調用非託管函數(指定的是C/C++語言寫的函數)。這樣若是咱們在.NET平臺下實現的功能有現有的C/C++ 函數實現了這樣的功能,這時候咱們徹底不必本身再用託管語言(如C#、vb.net)去實現一個這樣的功能,這時候咱們應該想到 「拿來主義」,直接使用平臺調用技術調用C/C++ 實現的函數。然而在實際應用中,使用平臺調用技術來調用Win32 API較爲廣泛,因此在這個專題中將爲你們具體介紹瞭如何使用平臺調用來調用Win32函數以及調用過程當中應該注意的問題,下面就從一個具體的實例開始本專題的介紹。測試

2、如何使用平臺調用Win32 函數——從實例開始網站

在前一個專題中已經介紹了使用平臺調用來調用非託管函數的步驟:ui

(1).  得到非託管函數的信息,即dll的名稱,須要調用的非託管函數名等信息編碼

(2). 在託管代碼中對非託管函數進行聲明,而且附加平臺調用所須要屬性spa

(3). 在託管代碼中直接調用第二步中聲明的託管函數操作系統

然而調用Win32 API函數還有一些問題須要注意的地方, 首先, 由於不少Win32 API函數都有ANSI和Unicode兩個版本,因此在託管代碼聲明時須要指定調用調用函數的版本。  然而不少Win32 API函數有ANSI和Unicode兩個版本並非隨便說說的,而是有根據的。你們從調用步驟中能夠看出,第一步就須要知道非託管函數聲明,爲了找到須要須要調用的非託管函數,能夠藉助兩個工具——Visual Studio自帶的dumpbin.exedepends.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);
         }
     }
運行結果爲:

3、當調用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

4、小結

講到這裏,本專題的內容也就介紹完了,本專題只是簡單介紹了使用平臺調用技術來調用Win32函數,然而實際的操做遠遠不是這麼簡單的,要掌握平臺調用的技術,還須要你們在工做過程多多實踐。由於在本專題中涉及了一些數據封送一些知識,爲了幫助你們更好掌握數據封送處理,在一個專題將爲你們帶來平臺調用中的數據封送處理專題。

相關文章
相關標籤/搜索