以前我寫過通過WMI來擷取有關係統資訊的系列文章,確實通過WMI能夠恨輕易地實現很多我們想實現的功能,不過有些情況下我們很難利用WMI來實現一些複雜的功能,比如最近我做的一個項目,其中有一個功能就是要更改系統目前時間,利用WMI就很難實現(我沒有找到相關的方法),還有一些其它方面的功能,也比較難以通過WMI來實現,也許是WMI需要較高的許可權才能執行的原因吧。所以,儘管我們不願意,但是又不得不通過調用Windows 的API來實現。本文的目的就是講述如何在C#中調用Windows的系統API。
本文將按照下面的步驟分別講解:
API簡介
C#中的單一資料型別與API中的資料類型對應關係
如何在調用API時傳遞複雜參數:封裝類、結構和聯合
如何調用API
如何確保成功調用API
API簡介
Windows API(Application Programming Interface,應用編程介面)是微軟為了方便廣大Windows開發人員調用系統底層功能而公開的一系列函數介面。.net中的函數很多就是對系統底層API的一些封裝,但是在.net中並沒有包含Windows所有的API函數。所幸的是,在.net中允許我們調用系統的API函數,並且還可以根據需要向系統API傳遞輸入或者輸出參數。
當調用非託管API函數時,它將依次執行以下操作:
1.尋找包含該函數的 DLL。
2.將該 DLL 載入到記憶體中。
3.尋找函數在記憶體中的地址並將其參數推到堆棧上,以封送所需的資料(注意:只在第一次調用函數時,才會尋找和載入 DLL 並尋找函數在記憶體中的地址。)。
4.將控制權轉移給非託管函數。
5.對非託管 DLL 函數的“平台叫用”調用
平台叫用會向託管調用方引發由非託管函數產生的異常。
DLL 函數的標識包括以下元素:
函數的名稱或序號
實現所在的 DLL 檔案的名稱
例如,如果指定 User32.dll 中的 MessageBox 函數,需要標識該函數 (MessageBox) 及其位置(User32.dll、User32 或 user32)。Microsoft Windows API (Win32 API) 可以包含每個字元和字串處理函數的兩個版本:單位元組字元 ANSI 版本和雙位元組字元 Unicode 版本。如果不進行指定,CharSet 欄位所表示的字元集將預設為 ANSI。某些函數可以有兩個以上的版本。
MessageBoxA 是 MessageBox 函數的 ANSI 進入點;而 MessageBoxW 是 Unicode 版本。可以通過運行各種命令列工具,為特定 DLL(例如 user32.dll)列出函數名。例如,可以使用 dumpbin /exports user32.dll 或 link /dump /exports user32.dll 來擷取函數名。
您可以在代碼中將非託管函數重新命名為任何所需的名稱,但是要將該新名稱映射到 DLL 中的初始進入點。有關在託管原始碼中重新命名非託管 DLL 函數的說明,請參見指定進入點。
利用平台叫用,可以通過調用 Win32 API 和其他 DLL 中的函數來控制作業系統中相當大的一部分。除了 Win32 API 之外,還有許多其他的 API 和 DLL 可通過平台叫用來調用。
下面將說明 Win32 API 中幾個常用的 DLL。
GDI32.dll:用於裝置輸出的圖形裝置介面 (GDI) 函數,例如用於繪圖和字型管理的函數。
Kernel32.dll:用於記憶體管理和資源處理的低層級作業系統函數。
User32.dll:用於訊息處理、計時器、菜單和通訊的 Windows 管理函數。
涉及到函數調用,自然免不了要向系統API提供參數或者擷取調用系統API之後的傳回值,由於Windows採用了C/C++開發的,而我們調用的程式語言是C#,二者的資料類型自然會存在一些不一致的情況,下面的表列出了二者之間的一個對應關係。
下表列出了在 Win32 API(在 Wtypes.h 中列出)和 C 樣式函數中使用的資料類型。許多非託管庫包含將這些資料類型作為參數傳遞並傳回值的函數。第三列列出了在Managed 程式碼中使用的相應的 .NET Framework 內建實值型別或類。某些情況下,您可以用大小相同的類型替換此表中列出的類型。
| Wtypes.h 中的非託管類型 |
非託管 C 語言類型 |
託管類名 |
說明 |
HANDLE |
void* |
System.IntPtr |
在 32 位 Windows 作業系統上為 32 位,在 64 位元 Windows 作業系統上為 64 位元。 |
BYTE |
unsigned char |
System.Byte |
8 位 |
SHORT |
short |
System.Int16 |
16 位 |
WORD |
unsigned short |
System.UInt16 |
16 位 |
INT |
int |
System.Int32 |
32 位 |
UINT |
unsigned int |
System.UInt32 |
32 位 |
LONG |
long |
System.Int32 |
32 位 |
BOOL |
long |
System.Int32 |
32 位 |
DWORD |
unsigned long |
System.UInt32 |
32 位 |
ULONG |
unsigned long |
System.UInt32 |
32 位 |
CHAR |
char |
System.Char |
用 ANSI 修飾。 |
LPSTR |
char* |
System.String 或 System.Text.StringBuilder |
用 ANSI 修飾。 |
LPCSTR |
Const char* |
System.String 或 System.Text.StringBuilder |
用 ANSI 修飾。 |
LPWSTR |
wchar_t* |
System.String 或 System.Text.StringBuilder |
用 Unicode 修飾。 |
LPCWSTR |
Const wchar_t* |
System.String 或 System.Text.StringBuilder |
用 Unicode 修飾。 |
FLOAT |
Float |
System.Single |
32 位 |
DOUBLE |
Double |
System.Double |
64 位元 |
如何在調用API時傳遞複雜參數:封裝類、結構和聯合
類和結構在 .NET Framework 中是類似的。它們都可以具有欄位、屬性和事件。它們也有靜態和非靜態方法。一個顯著區別是結構屬於實值型別而類屬於參考型別。
結構:
比如一個常用函數,用於擷取日期時間的,原始聲明如下:
VOID GetSystemTime(LPSYSTEMTIME lpSystemTime);
這個方法位於Kernel32.dll類庫中,這個方法需要一個SYSTEMTIME的結構,其原始聲明如下:
typedef struct _SYSTEMTIME {
WORD wYear;
WORD wMonth;
WORD wDayOfWeek;
WORD wDay;
WORD wHour;
WORD wMinute;
WORD wSecond;
WORD wMilliseconds;
} SYSTEMTIME, *PSYSTEMTIME;
根據C#中單一資料型別與C/C++中資料類型的對應關係,我們可以完成如下代碼:
public struct SystemTime
{
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
對上面的API方法的調用聲明如下:
[DllImport("kernel32.dll", EntryPoint = "SetSystemTime")]
public static extern void GetSystemTime(
SystemTime systemTime
);
在預設情況下,上面的方法將SystemTime類In/Out 參數進行傳遞。必須用 InAttribute 和 OutAttribute 屬性聲明該參數,因為作為參考型別的類在預設情況下將作為輸入參數進行傳遞。為使調用方接收結果,必須顯式應用這些方向屬性,如ref或者out。
另外,我們還需要指定結構在記憶體中的布局,這個我們可以在聲明結構時加以StructLayout屬性來指明。而StructLayout屬性需要一個layoutKind的枚舉值。它有如下幾個值:
| 成員名稱 |
說明 |
| Auto |
運行庫自動為非託管記憶體中的對象的成員選擇適當的布局。使用此枚舉成員定義的對象不能在Managed 程式碼的外部公開。嘗試這樣做將引發異常。 |
| Explicit |
對象的各個成員在非託管記憶體中的精確位置被顯式控制。每個成員必須使用 FieldOffsetAttribute 指示該欄位在類型中的位置。 |
| Sequential |
對象的成員按照它們在被匯出到非託管記憶體時出現的順序依次布局。這些成員根據在 StructLayoutAttribute.Pack 中指定的封裝進行布局,並且可以是不連續的。 |
在本例中,使用Sequential就行了。上面的C#結構描述修正如下:
[StructLayout(LayoutKind.Sequential)]
public struct SystemTime
{
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
當然如果想聲明成Explicit也是可以的,如下:
[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)]
public struct MySystemTime
{ [FieldOffset(0)]
public ushort wYear;
[FieldOffset(2)]
public ushort wMonth;
[FieldOffset(4)]
public ushort wDayOfWeek;
[FieldOffset(6)]
public ushort wDay;
[FieldOffset(8)]
public ushort wHour;
[FieldOffset(10)]
public ushort wMinute;
[FieldOffset(12)]
public ushort wSecond;
[FieldOffset(14)]public ushort wMilliseconds;
}
每個欄位的FieldOffset依次遞增為2位元組,因為嚴格ushort佔用的記憶體大小也正好是2位元組。總共8個欄位,因此總共16位元組。在這裡又多用了一個CharSet屬性聲明,它是用來規定封送字串應使用何種字元集。它也是一個枚舉類型,對可能值和對應描述如下:
| 成員名稱 |
說明 |
| Auto |
針對目標作業系統適當地自動封送字串。在 Windows NT、Windows 2000、Windows XP 和 Windows Server 2003 系列上預設值為 Unicode;在 Windows 98 和 Windows Me 上預設值為 Ansi。儘管公用語言運行庫預設值為 Auto,使用語言可重寫此預設值。例如,預設情況下,C# 將所有方法和類型都標記為 Ansi。 |
| Ansi |
以多位元組字串的形式封送字串。 |
| None |
此值已淘汰,它與 CharSet.Ansi 具有相同的行為 |
| Unicode |
以 Unicode 2 位元組字元形式封送字串。 |
待續.....