C#呼叫Windows API詳解(上)

weixin_34262482發表於2008-07-17
以前我寫過通過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傳遞輸入或者輸出引數。
DllImportDemo1.JPG
當呼叫非託管API函式時,它將依次執行以下操作:
1.查詢包含該函式的 DLL。
2.將該 DLL 載入到記憶體中。
3.查詢函式在記憶體中的地址並將其引數推到堆疊上,以封送所需的資料(注意:只在第一次呼叫函式時,才會查詢和載入 DLL 並查詢函式在記憶體中的地址。)。
4.將控制權轉移給非託管函式。
5.對非託管 DLL 函式的“平臺呼叫”呼叫
平臺呼叫會向託管呼叫方引發由非託管函式生成的異常。
DLL 函式的標識包括以下元素:
函式的名稱或序號
實現所在的 DLL 檔案的名稱
例如,如果指定 User32.dll 中的 MessageBox 函式,需要標識該函式 (MessageBox) 及其位置(User32.dll、User32 或 user32)。Microsoft Windows 應用程式程式設計介面 (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 樣式函式中使用的資料型別。許多非託管庫包含將這些資料型別作為引數傳遞並返回值的函式。第三列列出了在託管程式碼中使用的相應的 .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 執行庫自動為非託管記憶體中的物件的成員選擇適當的佈局。使用此列舉成員定義的物件不能在託管程式碼的外部公開。嘗試這樣做將引發異常。
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 位元組字元形式封送字串。
待續.....

相關文章