2017年8月17日 星期四

[C#] 直接調用Win32 API DLL

  1. [DllImport("kernel32.dll")]是什麼意思?

這叫引入kernel32.dll這個動態連接庫。
這個動態連接庫裡麵包含了很多WindowsAPI函數,如果你想使用這面的函數,就需要這麼引入。舉個例子:
[DllImport("kernel32.dll")]
private static extern void函數名(參數,[參數]);
函數名就是一個屬於kernel32.dll裡的一個函數。完了你就可以用那個函數了。

kernel32.dll調用kernel32.dll這個DLL裡面的API接口!

系統API
例如
[DllImport("user32.dll")]//--引入API
public static extern ReturnType FunctionName(type arg1,type arg2,...);//--聲明方法

調用該方法是和調用普通方法沒區別
DLL Import屬性

現在是更深入地進行探討的時候了。在對託管代碼進行P/Invoke調用時,DllImportAttribute類型扮演著重要的角色。DllImportAttribute的主要作用是給CLR指示哪個DLL導出您想要調用的函數。相關DLL的名稱被作為一個構造函數參數傳遞給DllImportAttribute。

如果您無法肯定哪個DLL定義了您要使用的Windows API函數,Platform SDK文檔將為您提供最好的幫助資源。在Windows API函數主題文字臨近結尾的位置,SDK文檔指定了C應用程序要使用該函數必須鏈接的.lib文件。在幾乎所有的情況下,該.lib文件具有與定義該函數的系統DLL文件相同的名稱。例如,如果該函數需要C應用程序鏈接到Kernel32.lib,則該函數就定義在Kernel32.dll中。您可以在MessageBeep中找到有關MessageBeep的Platform SDK文檔主題。在該主題結尾處,您會注意到它指出庫文件是User32.lib;這表明MessageBeep是從User32.dll中導出的。

可選的DllImportAttribute屬性

除了指出宿主DLL外,DllImportAttribute還包含了一些可選屬性,其中四個特別有趣:EntryPoint、CharSet、SetLastError和CallingConvention。

EntryPoint在不希望外部託管方法具有與DLL導出相同的名稱的情況下,可以設置該屬性來指示導出的DLL函數的入口點名稱。當您定義兩個調用相同非託管函數的外部方法時,這特別有用。另外,在Windows中還可以通過它們的序號值綁定到導出的DLL函數。如果您需要這樣做,則諸如“#1”或“#129”的EntryPoint值指示DLL中非託管函數的序號值而不是函數名。

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兩種版本。

SetLastError錯誤處理非常重要,但在編程時經常被遺忘。當您進行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返回的值傳遞給它。如果您回頭看一下圖1中的代碼,您會看到我在extern MessageBeep方法的公共包裝中就採用了這種方法。

CallingConvention我將在此介紹的最後也可能是最不重要的一個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。
  1. [C#] 直接調用Win32 API DLL

若要宣告直接調用DLL方法,請按下列方法操作:

第一步: 引用System.Runtime.InteropServices
using System.Runtime.InteropServices;

第二步: 直接從 C# 調用 DLL,使用 C# 關鍵字 static 和 extern 聲明方法。
使用DllImportAttribute 類別來使用API
[DllImport("user32.dll")] //替換成所需的DLL檔
public static extern ReturnType FunctionName(type arg1,type arg2,...);//替換成所需的方法及參數

===================================================================
直接從 C# 調用 DLL 導出

若要聲明一個方法使其具有來自 DLL 導出的實現,請執行下列操作:

使用 C# 關鍵字 static 和 extern 聲明方法。

將 DllImport 屬性附加到該方法。DllImport 屬性允許您指定包含該方法的 DLL 的名稱。通常的做法是用與導出的方法相同的名稱命名 C# 方法,但也可以對 C# 方法使用不同的名稱。

還可以為方法的參數和返回值指定自定義封送處理信息,這將重寫 .NET Framework 的默認封送處理。

示例 1

本示例顯示如何使用 DllImport 屬性通過調用 msvcrt.dll 中的 puts 輸出消息。

// PInvokeTest.cs
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(string c);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();

public static void Main()
{
puts("Test");
_flushall();
}
}

輸出

Test

代碼討論

前面的示例顯示了聲明在非託管 DLL 中實現的 C# 方法的最低要求。PlatformInvokeTest.puts 方法用 static 和 extern 修飾符聲明並且具有 DllImport 屬性,該屬性使用默認名稱 puts 通知編譯器此實現來自 msvcrt.dll。若要對 C# 方法使用不同的名稱(如 putstring),則必須在 DllImport 屬性中使用 EntryPoint 選項,如下所示:

[DllImport("msvcrt.dll", EntryPoint="puts")]

有關 DllImport 屬性的語法的更多信息,請參見 DllImportAttribute 類。

默認封送處理和為非託管方法的參數指定自定義封送處理

當從 C# 代碼中調用非託管函數時,公共語言運行庫必須封送參數和返回值。

對於每個 .NET Framework 類型均有一個默認非託管類型,公共語言運行庫將使用此非託管類型在託管到非託管的函數調用中封送數據。例如,C# 字符串值的默認封送處理是封送為 LPTSTR(指向 TCHAR 字符緩衝區的指針)類型。可以在非託管函數的 C# 聲明中使用 MarshalAs 屬性重寫默認封送處理。
===================================================================
示例 2

本示例使用 DllImport 屬性輸出一個字符串。它還顯示如何通過使用 MarshalAs 屬性重寫函數參數的默認封送處理。

// Marshal.cs
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
[DllImport("msvcrt.dll")]
public static extern int puts(
[MarshalAs(UnmanagedType.LPStr)]
string m);
[DllImport("msvcrt.dll")]
internal static extern int _flushall();


public static void Main()
{
puts("Hello World!");
_flushall();
}
}

輸出

運行此示例時,字符串

Hello World!

將顯示在控制台上。

代碼討論

在前面的示例中,puts 函數的參數的默認封送處理已從默認值 LPTSTR 重寫為 LPSTR。

MarshalAs 屬性可以放置在方法參數、方法返回值以及結構和類的字段上。若要設置方法返回值的封送處理,請將 MarshalAs 屬性與返回屬性位置重寫一起放置在方法上的屬性塊中。例如,若要顯式設置 puts 方法返回值的封送處理:

...
[DllImport("msvcrt.dll")]
[return : MarshalAs(UnmanagedType.I4)]
public static extern int puts(
...

有關 MarshalAs 屬性的語法的更多信息,請參見 MarshalAsAttribute 類。

注意   In 和 Out 屬性可用於批註非託管方法的參數。它們與 MIDL 源文件中的 in 和 out 修飾符的工作方式類似。請注意,Out 屬性與 C# 參數修飾符 out 不同。有關 In 和 Out 屬性的更多信息,請參見 InAttribute 類和 OutAttribute 類。

為用戶定義的結構指定自定義封送處理

可以為傳遞到非託管函數或從非託管函數返回的結構和類的字段指定自定義封送處理屬性。通過向結構或類的字段中添加 MarshalAs 屬性可以做到這一點。還必須使用 StructLayout 屬性設置結構的佈局,還可以控制字符串成員的默認封送處理,並設置默認封裝大小。
===================================================================
示例 3

本示例說明如何為結構指定自定義封送處理屬性。

請考慮下面的 C 結構:

typedef struct tagLOGFONT
{
LONG lfHeight;
LONG lfWidth;
LONG lfEscapement;
LONG lfOrientation;
LONG lfWeight;
BYTE lfItalic;
BYTE lfUnderline;
BYTE lfStrikeOut;
BYTE lfCharSet;
BYTE lfOutPrecision;
BYTE lfClipPrecision;
BYTE lfQuality;
BYTE lfPitchAndFamily;
TCHAR lfFaceName[LF_FACESIZE];
} LOGFONT;

在 C# 中,可以使用 StructLayout 和 MarshalAs 屬性描述前面的結構,如下所示:

// logfont.cs
// compile with: /target:module
using System;
using System.Runtime.InteropServices;

[StructLayout(LayoutKind.Sequential)]
public class LOGFONT
{
public const int LF_FACESIZE = 32;
public int lfHeight;
public int lfWidth;
public int lfEscapement;
public int lfOrientation;
public int lfWeight;
public byte lfItalic;
public byte lfUnderline;
public byte lfStrikeOut;
public byte lfCharSet;
public byte lfOutPrecision;
public byte lfClipPrecision;
public byte lfQuality;
public byte lfPitchAndFamily;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst=LF_FACESIZE)]
public string lfFaceName;
}

有關 StructLayout 屬性的語法的更多信息,請參見 StructLayoutAttribute 類。

然後即可將該結構用在 C# 代碼中,如下所示:

// pinvoke.cs
// compile with: /addmodule:logfont.netmodule
using System;
using System.Runtime.InteropServices;

class PlatformInvokeTest
{
[DllImport("gdi32.dll", CharSet=CharSet.Auto)]
public static extern IntPtr CreateFontIndirect(
[In, MarshalAs(UnmanagedType.LPStruct)]
LOGFONT lplf // characteristics
);

[DllImport("gdi32.dll")]
public static extern bool DeleteObject(
IntPtr handle
);

public static void Main()
{
LOGFONT lf = new LOGFONT();
lf.lfHeight = 9;
lf.lfFaceName = "Arial";
IntPtr handle = CreateFontIndirect(lf);

if (IntPtr.Zero == handle)
{
Console.WriteLine("Can't creates a logical font.");
}
else
{

if (IntPtr.Size == 4)
Console.WriteLine("{0:X}", handle.ToInt32());
else
Console.WriteLine("{0:X}", handle.ToInt64());

// Delete the logical font created.
if (!DeleteObject(handle))
Console.WriteLine("Can't delete the logical font");
}
}
}

運行示例

C30A0AE5

代碼討論

在前面的示例中,CreateFontIndirect 方法使用了一個 LOGFONT 類型的參數。MarshalAs 和 In 屬性用於限定此參數。程序將由此方法返回的數值顯示為十六進制大寫字符串。

註冊回調方法

若要註冊調用非託管函數的託管回調,請用相同的參數列表聲明一個委託並通過 PInvoke 傳遞它的一個實例。在非託管端,它將顯示為一個函數指針。有關 PInvoke 和回調的更多信息,請參見平台調用詳解。

例如,考慮以下非託管函數 MyFunction,此函數要求 callback 作為其參數之一:

typedef void (__stdcall *PFN_MYCALLBACK)();
int __stdcall MyFunction(PFN_ MYCALLBACK callback);

若要從託管代碼調用 MyFunction,請聲明該委託,將 DllImport 附加到函數聲明,並根據需要封送任何參數或返回值:

public delegate void MyCallback();
[DllImport("MYDLL.DLL")]
public static extern void MyFunction(MyCallback callback);

同時,請確保委託實例的生存期覆蓋非託管代碼的生存期;否則,委託在經過垃圾回收後將不再可用。

沒有留言:

張貼留言