P/Invoke是什麼?

P/Invoke是什麼? 在受控代碼與非受控代碼進行交互時會產生一個事務(transition) ,這通常發生在使用平臺調用服務(Platform Invocation Services),即P/Invoke

如調用系統的 API 或與 COM 對象打交道,通過 System.Runtime.InteropServices 命名空間

雖然使用 Interop 非常方便,但據估計每次調用事務都要執行 10 到 40 條指令,算起來開銷也不少,所以我們要儘量少調用事務

如果非用不可,建議本着一次調用執行多個動作,而不是多次調用每次只執行少量動作的原則。

 

 

    在對託管代碼進行 P/Invoke 調用時,DllImportAttribute 類型扮演着重要的角色。DllImportAttribute 的主要作用是給 CLR 指示哪個 DLL 導出您想要調用的函數。相關 DLL 的名稱被作爲一個構造函數參數傳遞給 DllImportAttribute。下面是使用P/Invoke時的一個簡單例子:

Code
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW",  SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);

1,"KERNEL32.DLL", 爲必選屬性,用來指出相關 DLL 的名稱,The name of the DLL that contains the unmanaged method.
2,EntryPoint:指示要調用的 DLL 入口點的名稱或序號.
指定入口點名稱時,您可以提供一個字符串來指示包含入口點的 DLL 的名稱,或者也可以按序號來標識入口點。序號以 # 符號爲前綴,如 #1。如果省略此字段,則公共語言運行庫將使用以 DllImportAttribute 標記的 .NET 方法的名稱。
在不希望外部託管方法具有與 DLL 導出相同的名稱的情況下,可以設置該屬性來指示導出的 DLL 函數的入口點名稱。當您定義兩個調用相同非託管函數的外部方法時,這特別有用。
3,CharSet 指示如何向方法封送字符串參數,並控制名稱損壞。
通過一個 CharSet 枚舉的成員使用此字段指定字符串參數的封送處理行爲,並指定要調用的入口點名稱(給定的確切名稱或以“A”、“W”結尾的名稱)。用於 C# 和 Visual Basic 的默認枚舉成員爲 CharSet.Ansi,用於 C++ 的默認枚舉成員爲 CharSet.None,它與 CharSet.Ansi 等效。在 Visual Basic 中可以使用 Declare 語句指定 CharSet 字段。
    ExactSpelling字段會影響 CharSet 字段在確定要調用的入口點名稱時的行爲。
對於字符集,並非所有版本的 Windows 都是同樣創建的。Windows 9x 系列產品缺少重要的 Unicode 支持,而 Windows NT 和 Windows CE 系列則一開始就使用 Unicode。在這些操作系統上運行的 CLR 將Unicode 用於 String 和 Char 數據的內部表示。但也不必擔心 — 當調用 Windows 9x API 函數時,CLR 會自動進行必要的轉換,將其從 Unicode轉換爲 ANSI。


如果 DLL 函數不以任何方式處理文本,則可以忽略 DllImportAttribute 的 CharSet 屬性。然而,當 Char 或 String 數據是等式的一部分時,應該將 CharSet 屬性設置爲 CharSet.Auto。這樣可以使 CLR 根據宿主 OS 使用適當的字符集。如果沒有顯式地設置 CharSet 屬性,則其默認值爲 CharSet.Ansi。這個默認值是有缺點的,因爲對於在 Windows 2000、Windows XP 和 Windows NT® 上進行的 interop 調用,它會消極地影響文本參數封送處理的性能。

應該顯式地選擇 CharSet.Ansi 或 CharSet.Unicode 的 CharSet 值而不是使用 CharSet.Auto 的唯一情況是:您顯式地指定了一個導出函數,而該函數特定於這兩種 Win32 OS 中的某一種。ReadDirectoryChangesW API 函數就是這樣的一個例子,它只存在於基於 Windows NT 的操作系統中,並且只支持 Unicode;在這種情況下,您應該顯式地使用 CharSet.Unicode。

有時,Windows API 是否有字符集關係並不明顯。一種決不會有錯的確認方法是在 Platform SDK 中檢查該函數的 C 語言頭文件。(如果您無法肯定要看哪個頭文件,則可以查看 Platform SDK 文檔中列出的每個 API 函數的頭文件。)如果您發現該 API 函數確實定義爲一個映射到以 A 或 W 結尾的函數名的宏,則字符集與您嘗試調用的函數有關係。Windows API 函數的一個例子是在 WinUser.h 中聲明的 GetMessage API,您也許會驚訝地發現它有 A 和 W 兩種版本。

4,SetLastError 指示被調用方在從屬性化方法返回之前是否調用 SetLastError Win32 API 函數。
true 指示被調用方將調用 SetLastError;否則爲 false。默認值爲 false,但在 Visual Basic 中除外。
運行時封送拆收器將調用 GetLastError 並緩存返回的值,以防其被其他 API 調用重寫。可通過調用 GetLastWin32Error來檢索錯誤代碼。
錯誤處理非常重要,但在編程時經常被遺忘。當您進行 P/Invoke 調用時,也會面臨其他的挑戰 — 處理託管代碼中 Windows API 錯誤處理和異常之間的區別。我可以給您一點建議。如果您正在使用 P/Invoke 調用 Windows API 函數,而對於該函數,您使用 GetLastError 來查找擴展的錯誤信息,則應該在外部方法的 DllImportAttribute 中將 SetLastError 屬性設置爲 true。這適用於大多數外部方法。
這會導致 CLR 在每次調用外部方法之後緩存由 API 函數設置的錯誤。然後,在包裝方法中,可以通過調用類庫的 System.Runtime.InteropServices.Marshal 類型中定義的 Marshal.GetLastWin32Error 方法來獲取緩存的錯誤值。我的建議是檢查這些期望來自 API 函數的錯誤值,併爲這些值引發一個可感知的異常。對於其他所有失敗情況(包括根本就沒意料到的失敗情況),則引發在 System.ComponentModel 命名空間中定義的 Win32Exception,並將 Marshal.GetLastWin32Error 返回的值傳遞給它。看下面的例子:


Code
namespace Wintellect.Interop.Sound{
   using System;
   using System.Runtime.InteropServices;
   using System.ComponentModel;

   sealed class Sound{
      public static void MessageBeep(BeepTypes type){
         if(!MessageBeep((UInt32) type)){
            Int32 err = Marshal.GetLastWin32Error();
            throw new Win32Exception(err);
         }
      }

      [DllImport("User32.dll", SetLastError=true)]
      static extern Boolean MessageBeep(UInt32 beepType);

      private Sound(){}
   }

   enum BeepTypes{
      Simple = -1,
      Ok                = 0x00000000,
      IconHand          = 0x00000010,
      IconQuestion      = 0x00000020,
      IconExclamation   = 0x00000030,
      IconAsterisk      = 0x00000040
   }
}
 

5,CallingConvention 指示入口點的調用約定。
將此字段設置爲 CallingConvention 枚舉成員之一。CallingConvention 字段的默認值爲 WinAPI,而後者又默認爲 StdCall 約定。
我將在此介紹的最後也可能是最不重要的一個 DllImportAttribute 屬性是 CallingConvention。通過此屬性,可以給 CLR 指示應該將哪種函數調用約定用於堆棧中的參數。CallingConvention.Winapi 的默認值是最好的選擇,它在大多數情況下都可行。然而,如果該調用不起作用,則可以檢查 Platform SDK 中的聲明頭文件,看看您調用的 API 函數是否是一個不符合調用約定標準的異常 API。

通常,本機函數(例如 Windows API 函數或 C- 運行時 DLL 函數)的調用約定描述瞭如何將參數推入線程堆棧或從線程堆棧中清除。大多數 Windows API 函數都是首先將函數的最後一個參數推入堆棧,然後由被調用的函數負責清理該堆棧。相反,許多 C-運行時 DLL 函數都被定義爲按照方法參數在方法簽名中出現的順序將其推入堆棧,將堆棧清理工作交給調用者。

幸運的是,要讓 P/Invoke 調用工作只需要讓外圍設備理解調用約定即可。通常,從默認值 CallingConvention.Winapi 開始是最好的選擇。然後,在 C 運行時 DLL 函數和少數函數中,可能需要將約定更改爲 CallingConvention.Cdecl。

6,ExactSpelling:控制 DllImportAttribute.CharSet 字段是否使公共語言運行庫在非託管 DLL 中搜索入口點名稱,而不使用指定的入口點名稱。
如果爲 false,則當 DllImportAttribute.CharSet 字段設置爲 CharSet.Ansi 時,將調用附加有字母“A”的入口點名稱;當 DllImportAttribute.CharSet 字段設置爲 CharSet.Unicode 時,將調用附加有字母“W”的入口點名稱。此字段通常由託管編譯器設置。下表根據編程語言設置的默認值,說明了 CharSet 字段和 ExactSpelling 字段之間的關係。您可以重寫默認設置,但須謹慎。


Language ANSI Unicode Auto
Visual Basic ExactSpelling:=True ExactSpelling:=True ExactSpelling:=False
C# ExactSpelling=false ExactSpelling=false ExactSpelling=false
C++ ExactSpelling=false ExactSpelling=false ExactSpelling=false

 

詳細看下面的例子:


Code
using System.Runtime.InteropServices;
public class Win32 {
    [DllImport("user32.dll", CharSet=CharSet.Unicode,
               ExactSpelling=true)]
    public static extern int MessageBoxW(int hWnd, String text, String
                                          caption, uint type);
}

 

發佈了94 篇原創文章 · 獲贊 13 · 訪問量 68萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章