天天看點

如何在C#中使用 Win32和其他庫

這一次我們将深入探讨如何在 C# 中使用 Win32 和其他現有庫。 

C# 使用者經常提出兩個問題:“我為什麼要另外編寫代碼來使用内置于 Windows 中的功能?在架構中為什麼沒有相應的内容可以為我完成這一任務?”當架構小組建構他們的 .NET 部分時,他們評估了為使 .NET 程式員可以使用 Win32 而需要完成的工作,結果發現 Win32 API 集非常龐大。他們沒有足夠的資源為所有 Win32 API 編寫托管接口、加以測試并編寫文檔,是以隻能優先處理最重要的部分。許多常用操作都有托管接口,但是還有許多完整的 Win32 部分沒有托管接口。 

平台調用 (P/Invoke) 是完成這一任務的最常用方法。要使用 P/Invoke,您可以編寫一個描述如何調用函數的原型,然後運作時将使用此資訊進行調用。另一種方法是使用 Managed Extensions to C++ 來包裝函數,這部分内容将在以後的專欄中介紹。 

要了解如何完成這一任務,最好的辦法是通過示例。在某些示例中,我隻給出了部分代碼;完整的代碼可以通過下載下傳獲得。 

簡單示例

在第一個示例中,我們将調用 Beep() API 來發出聲音。首先,我需要為 Beep() 編寫适當的定義。檢視 MSDN 中的定義,我發現它具有以下原型: 

BOOL Beep(

DWORD dwFreq,   // 聲音頻率

DWORD dwDuration  // 聲音持續時間

);

要用 C# 來編寫這一原型,需要将 Win32 類型轉換成相應的 C# 類型。由于 DWORD 是 4 位元組的整數,是以我們可以使用 int 或 uint 作為 C# 對應類型。由于 int 是 CLS 相容類型(可以用于所有 .NET 語言),以此比 uint 更常用,并且在多數情況下,它們之間的差別并不重要。bool 類型與 BOOL 對應。現在我們可以用 C# 編寫以下原型: 

public static extern bool Beep(int frequency, int duration); 

這是相當标準的定義,隻不過我們使用了 extern 來指明該函數的實際代碼在别處。此原型将告訴運作時如何調用函數;現在我們需要告訴它在何處找到該函數。 

我們需要回顧一下 MSDN 中的代碼。在參考資訊中,我們發現 Beep() 是在 kernel32.lib 中定義的。這意味着運作時代碼包含在 kernel32.dll 中。我們在原型中添加 DllImport 屬性将這一資訊告訴運作時: 

[DllImport("kernel32.dll")] 

這就是我們要做的全部工作。下面是一個完整的示例,它生成的随機聲音在二十世紀六十年代的科幻電影中很常見。 

using System;

using System.Runtime.InteropServices; 

namespace Beep

{

class Class1

[DllImport("kernel32.dll")]

static void Main(string[] args)

Random random = new Random(); 

for (int i = 0; i < 10000; i++)

Beep(random.Next(10000), 100);

}

它的聲響足以刺激任何聽者!由于 DllImport 允許您調用 Win32 中的任何代碼,是以就有可能調用惡意代碼。是以您必須是完全受信任的使用者,運作時才能進行 P/Invoke 調用。 

枚舉和常量

Beep() 可用于發出任意聲音,但有時我們希望發出特定類型的聲音,是以我們改用 MessageBeep()。MSDN 給出了以下原型: 

BOOL MessageBeep(

UINT uType // 聲音類型

這看起來很簡單,但是從注釋中可以發現兩個有趣的事實。 

首先,uType 參數實際上接受一組預先定義的常量。 

其次,可能的參數值包括 -1,這意味着盡管它被定義為 uint 類型,但 int 會更加适合。 

對于 uType 參數,使用 enum 類型是合乎情理的。MSDN 列出了已命名的常量,但沒有就具體值給出任何提示。由于這一點,我們需要檢視實際的 API。 

如果您安裝了 Visual Studio? 和 C++,則 Platform SDK 位于 \Program Files\Microsoft Visual Studio .NET\Vc7\PlatformSDK\Include 下。 

為查找這些常量,我在該目錄中執行了一個 findstr。 

findstr "MB_ICONHAND" *.h 

它确定了常量位于 winuser.h 中,然後我使用這些常量來建立我的 enum 和原型: 

public enum BeepType

SimpleBeep = -1,

IconAsterisk = 0x00000040,

IconExclamation = 0x00000030,

IconHand = 0x00000010,

IconQuestion = 0x00000020,

Ok = 0x00000000,

[DllImport("user32.dll")]

public static extern bool MessageBeep(BeepType beepType);

現在我可以用下面的語句來調用它: MessageBeep(BeepType.IconQuestion); 

處理結構 

有時我需要确定我筆記本的電池狀況。Win32 為此提供了電源管理函數。 

搜尋 MSDN 可以找到 GetSystemPowerStatus() 函數。 

BOOL GetSystemPowerStatus(

LPSYSTEM_POWER_STATUS lpSystemPowerStatus

此函數包含指向某個結構的指針,我們尚未對此進行過處理。要處理結構,我們需要用 C# 定義結構。我們從非托管的定義開始: 

typedef struct _SYSTEM_POWER_STATUS {

BYTE  ACLineStatus; 

BYTE  BatteryFlag; 

BYTE  BatteryLifePercent; 

BYTE  Reserved1; 

DWORD BatteryLifeTime; 

DWORD BatteryFullLifeTime; 

} SYSTEM_POWER_STATUS, *LPSYSTEM_POWER_STATUS;

然後,通過用 C# 類型代替 C 類型來得到 C# 版本。 

struct SystemPowerStatus

byte ACLineStatus;

byte batteryFlag;

byte batteryLifePercent;

byte reserved1;

int batteryLifeTime;

int batteryFullLifeTime;

這樣,就可以友善地編寫出 C# 原型: 

public static extern bool GetSystemPowerStatus(

ref SystemPowerStatus systemPowerStatus);

在此原型中,我們用“ref”指明将傳遞結構指針而不是結構值。這是處理通過指針傳遞的結構的一般方法。 

此函數運作良好,但是最好将 ACLineStatus 和 batteryFlag 字段定義為 enum: 

  enum ACLineStatus: byte 

Offline = 0,

Online = 1,

Unknown = 255,

enum BatteryFlag: byte

High = 1,

Low = 2,

Critical = 4,

Charging = 8,

NoSystemBattery = 128,

請注意,由于結構的字段是一些位元組,是以我們使用 byte 作為該 enum 的基本類型。 

字元串

雖然隻有一種 .NET 字元串類型,但這種字元串類型在非托管應用中卻有幾項獨特之處。可以使用具有内嵌字元數組的字元指針和結構,其中每個數組都需要正确的封送處理。 

在 Win32 中還有兩種不同的字元串表示: 

ANSI 

Unicode

最初的 Windows 使用單位元組字元,這樣可以節省存儲空間,但在處理很多語言時都需要複雜的多位元組編碼。Windows NT? 出現後,它使用雙位元組的 Unicode 編碼。為解決這一差别,Win32 API 采用了非常聰明的做法。它定義了 TCHAR 類型,該類型在 Win9x 平台上是單位元組字元,在 WinNT 平台上是雙位元組 Unicode 字元。對于每個接受字元串或結構(其中包含字元資料)的函數,Win32 API 均定義了該結構的兩種版本,用 A 字尾指明 Ansi 編碼,用 W 指明 wide 編碼(即 Unicode)。如果您将 C++ 程式編譯為單位元組,會獲得 A 變體,如果編譯為 Unicode,則獲得 W 變體。Win9x 平台包含 Ansi 版本,而 WinNT 平台則包含 W 版本。 

由于 P/Invoke 的設計者不想讓您為所在的平台操心,是以他們提供了内置的支援來自動使用 A 或 W 版本。如果您調用的函數不存在,互操作層将為您查找并使用 A 或 W 版本。 

通過示例能夠很好地說明字元串支援的一些精妙之處。 

簡單字元串

下面是一個接受字元串參數的函數的簡單示例: 

BOOL GetDiskFreeSpace(

LPCTSTR lpRootPathName,     // 根路徑

LPDWORD lpSectorsPerCluster,  // 每個簇的扇區數

LPDWORD lpBytesPerSector,    // 每個扇區的位元組數

LPDWORD lpNumberOfFreeClusters, // 可用的扇區數

LPDWORD lpTotalNumberOfClusters // 扇區總數

根路徑定義為 LPCTSTR。這是獨立于平台的字元串指針。 

由于不存在名為 GetDiskFreeSpace() 的函數,封送拆收器将自動查找“A”或“W”變體,并調用相應的函數。我們使用一個屬性來告訴封送拆收器,API 所要求的字元串類型。 

以下是該函數的完整定義,就象我開始定義的那樣: 

static extern bool GetDiskFreeSpace(

[MarshalAs(UnmanagedType.LPTStr)]

string rootPathName,

ref int sectorsPerCluster,

ref int bytesPerSector,

ref int numberOfFreeClusters,

ref int totalNumberOfClusters);

不幸的是,當我試圖運作時,該函數不能執行。問題在于,無論我們在哪個平台上,封送拆收器在預設情況下都試圖查找 API 的 Ansi 版本,由于 LPTStr 意味着在 Windows NT 平台上會使用 Unicode 字元串,是以試圖用 Unicode 字元串來調用 Ansi 函數就會失敗。 

有兩種方法可以解決這個問題:一種簡單的方法是删除 MarshalAs 屬性。如果這樣做,将始終調用該函數的 A 版本,如果在您所涉及的所有平台上都有這種版本,這是個很好的方法。但是,這會降低代碼的執行速度,因為封送拆收器要将 .NET 字元串從 Unicode 轉換為多位元組,然後調用函數的 A 版本(将字元串轉換回 Unicode),最後調用函數的 W 版本。 

要避免出現這種情況,您需要告訴封送拆收器,要它在 Win9x 平台上時查找 A 版本,而在 NT 平台上時查找 W 版本。要實作這一目的,可以将 CharSet 設定為 DllImport 屬性的一部分: 

[DllImport("kernel32.dll", CharSet = CharSet.Auto)] 

在我的非正式計時測試中,我發現這一做法比前一種方法快了大約百分之五。 

對于大多數 Win32 API,都可以對字元串類型設定 CharSet 屬性并使用 LPTStr。但是,還有一些不采用 A/W 機制的函數,對于這些函數必須采取不同的方法。 

字元串緩沖區

.NET 中的字元串類型是不可改變的類型,這意味着它的值将永遠保持不變。對于要将字元串值複制到字元串緩沖區的函數,字元串将無效。這樣做至少會破壞由封送拆收器在轉換字元串時建立的臨時緩沖區;嚴重時會破壞托管堆,而這通常會導緻錯誤的發生。無論哪種情況都不可能獲得正确的傳回值。 

要解決此問題,我們需要使用其他類型。StringBuilder 類型就是被設計為用作緩沖區的,我們将使用它來代替字元串。下面是一個示例: 

[DllImport("kernel32.dll", CharSet = CharSet.Auto)]

public static extern int GetShortPathName(

string path,

StringBuilder shortPath,

int shortPathLength);

使用此函數很簡單: 

StringBuilder shortPath = new StringBuilder(80);

int result = GetShortPathName(

@"d:\test.jpg", shortPath, shortPath.Capacity);

string s = shortPath.ToString();

請注意,StringBuilder 的 Capacity 傳遞的是緩沖區大小。 

具有内嵌字元數組的結構

某些函數接受具有内嵌字元數組的結構。例如,GetTimeZoneInformation() 函數接受指向以下結構的指針: 

typedef struct _TIME_ZONE_INFORMATION { 

LONG    Bias; 

WCHAR   StandardName[ 32 ]; 

SYSTEMTIME StandardDate; 

LONG    StandardBias; 

WCHAR   DaylightName[ 32 ]; 

SYSTEMTIME DaylightDate; 

LONG    DaylightBias; 

} TIME_ZONE_INFORMATION, *PTIME_ZONE_INFORMATION;

在 C# 中使用它需要有兩種結構。一種是 SYSTEMTIME,它的設定很簡單: 

  struct SystemTime

public short wYear;

public short wMonth;

public short wDayOfWeek;

public short wDay;

public short wHour;

public short wMinute;

public short wSecond;

public short wMilliseconds;

這裡沒有什麼特别之處;另一種是 TimeZoneInformation,它的定義要複雜一些: 

[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]

struct TimeZoneInformation

public int bias;

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]

public string standardName;

SystemTime standardDate;

public int standardBias;

public string daylightName;

SystemTime daylightDate;

public int daylightBias;

此定義有兩個重要的細節。第一個是 MarshalAs 屬性: 

[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)] 

檢視 ByValTStr 的文檔,我們發現該屬性用于内嵌的字元數組;另一個是 SizeConst,它用于設定數組的大小。 

我在第一次編寫這段代碼時,遇到了執行引擎錯誤。通常這意味着部分互操作覆寫了某些記憶體,表明結構的大小存在錯誤。我使用 Marshal.SizeOf() 來擷取所使用的封送拆收器的大小,結果是 108 位元組。我進一步進行了調查,很快回憶起用于互操作的預設字元類型是 Ansi 或單位元組。而函數定義中的字元類型為 WCHAR,是雙位元組,是以導緻了這一問題。 

我通過添加 StructLayout 屬性進行了更正。結構在預設情況下按順序布局,這意味着所有字段都将以它們列出的順序排列。CharSet 的值被設定為 Unicode,以便始終使用正确的字元類型。 

經過這樣處理後,該函數一切正常。您可能想知道我為什麼不在此函數中使用 CharSet.Auto。這是因為,它也沒有 A 和 W 變體,而始終使用 Unicode 字元串,是以我采用了上述方法編碼。

具有回調的函數

當 Win32 函數需要傳回多項資料時,通常都是通過回調機制來實作的。開發人員将函數指針傳遞給函數,然後針對每一項調用開發人員的函數。 

在 C# 中沒有函數指針,而是使用“委托”,在調用 Win32 函數時使用委托來代替函數指針。 

EnumDesktops() 函數就是這類函數的一個示例: 

BOOL EnumDesktops(

HWINSTA hwinsta,       // 視窗執行個體的句柄

DESKTOPENUMPROC lpEnumFunc, // 回調函數

LPARAM lParam        // 用于回調函數的值

HWINSTA 類型由 IntPtr 代替,而 LPARAM 由 int 代替。DESKTOPENUMPROC 所需的工作要多一些。下面是 MSDN 中的定義: 

BOOL CALLBACK EnumDesktopProc(

LPTSTR lpszDesktop, // 桌面名稱

LPARAM lParam    // 使用者定義的值

我們可以将它轉換為以下委托: 

delegate bool EnumDesktopProc(

string desktopName,

int lParam);

完成該定義後,我們可以為 EnumDesktops() 編寫以下定義: 

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

static extern bool EnumDesktops(

IntPtr windowStation,

EnumDesktopProc callback,

這樣該函數就可以正常運作了。 

在互操作中使用委托時有個很重要的技巧:封送拆收器建立了指向委托的函數指針,該函數指針被傳遞給非托管函數。但是,封送拆收器無法确定非托管函數要使用函數指針做些什麼,是以它假定函數指針隻需在調用該函數時有效即可。 

結果是如果您調用諸如 SetConsoleCtrlHandler() 這樣的函數,其中的函數指針将被儲存以便将來使用,您就需要確定在您的代碼中引用委托。如果不這樣做,函數可能表面上能執行,但在将來的記憶體回收進行中會删除委托,并且會出現錯誤。 

其他進階函數

迄今為止我列出的示例都比較簡單,但是還有很多更複雜的 Win32 函數。下面是一個示例: 

DWORD SetEntriesInAcl(

ULONG cCountOfExplicitEntries,      // 項數

PEXPLICIT_ACCESS pListOfExplicitEntries, // 緩沖區

PACL OldAcl,               // 原始 ACL

PACL *NewAcl               // 新 ACL

前兩個參數的處理比較簡單:ulong 很簡單,并且可以使用 UnmanagedType.LPArray 來封送緩沖區。 

但第三和第四個參數有一些問題。問題在于定義 ACL 的方式。ACL 結構僅定義了 ACL 标頭,而緩沖區的其餘部分由 ACE 組成。ACE 可以具有多種不同類型,并且這些不同類型的 ACE 的長度也不同。 

如果您願意為所有緩沖區配置設定空間,并且願意使用不太安全的代碼,則可以用 C# 進行處理。但工作量很大,并且程式非常難調試。而使用 C++ 處理此 API 就容易得多。 

屬性的其他選項

DLLImport 和 StructLayout 屬性具有一些非常有用的選項,有助于 P/Invoke 的使用。下面列出了所有這些選項: 

DLLImport

CallingConvention

您可以用它來告訴封送拆收器,函數使用了哪些調用約定。您可以将它設定為您的函數的調用約定。通常,如果此設定錯誤,代碼将不能執行。但是,如果您的函數是 Cdecl 函數,并且使用 StdCall(預設)來調用該函數,那麼函數能夠執行,但函數參數不會從堆棧中删除,這會導緻堆棧被填滿。 

CharSet

控制調用 A 變體還是調用 W 變體。 

EntryPoint

此屬性用于設定封送拆收器在 DLL 中查找的名稱。設定此屬性後,您可以将 C# 函數重新命名為任何名稱。 

ExactSpelling

将此屬性設定為 true,封送拆收器将關閉 A 和 W 的查找特性。 

PreserveSig

COM 互操作使得具有最終輸出參數的函數看起來是由它傳回的該值。此屬性用于關閉這一特性。 

SetLastError

確定調用 Win32 API SetLastError(),以便您找出發生的錯誤。 

StructLayout

LayoutKind

結構在預設情況下按順序布局,并且在多數情況下都适用。如果需要完全控制結構成員所放置的位置,可以使用 LayoutKind.Explicit,然後為每個結構成員添加 FieldOffset 屬性。當您需要建立 union 時,通常需要這樣做。 

控制 ByValTStr 成員的預設字元類型。 

Pack

設定結構的壓縮大小。它控制結構的排列方式。如果 C 結構采用了其他壓縮方式,您可能需要設定此屬性。 

Size

設定結構大小。不常用;但是如果需要在結構末尾配置設定額外的空間,則可能會用到此屬性。 

從不同位置加載

您無法指定希望 DLLImport 在運作時從何處查找檔案,但是可以利用一個技巧來達到這一目的。 

DllImport 調用 LoadLibrary() 來完成它的工作。如果程序中已經加載了特定的 DLL,那麼即使指定的加載路徑不同,LoadLibrary() 也會成功。 

這意味着如果直接調用 LoadLibrary(),您就可以從任何位置加載 DLL,然後 DllImport LoadLibrary() 将使用該 DLL。 

由于這種行為,我們可以提前調用 LoadLibrary(),進而将您的調用指向其他 DLL。如果您在編寫庫,可以通過調用 GetModuleHandle() 來防止出現這種情況,以確定在首次調用 P/Invoke 之前沒有加載該庫。 

P/Invoke 疑難解答

如果您的 P/Invoke 調用失敗,通常是因為某些類型的定義不正确。以下是幾個常見問題: 

1.long != long。在 C++ 中,long 是 4 位元組的整數,但在 C# 中,它是 8 位元組的整數。

2.字元串類型設定不正确。

本文轉自夜&楓部落格園部落格,原文連結:http://www.cnblogs.com/newstart/archive/2012/10/11/2719597.html,如需轉載請自行聯系原作者