標籤:
C#調用API
API(application programming interface)應用編程介面,這個是Windows編程人員用來操縱Windows系統的工具,其中包含了大量的方法供編程人員來使用。.NET平台同樣提供了Framework類庫,其實這個類庫就是將大量的API函數進行了重寫和封裝,但是有時光使用類庫的方法完成不了或很麻煩才可以完成我們的項目,這時就可以考慮使用Windows API函數來操作。
當調用非託管API函數時,它將依次執行以下操作:
1.尋找包含該函數的 DLL。
2.將該 DLL 載入到記憶體中。
3.尋找函數在記憶體中的地址並將其參數推到堆棧上,以封送所需的資料(注意:只在第一次調用函數時,才會尋找和載入 DLL 並尋找函數在記憶體中的地址。)。
4.將控制權轉移給非託管函數。
5.對非託管 DLL 函數的“平台叫用”調用
平台叫用會向託管調用方引發由非託管函數產生的異常。
Win16 和 Wint32 API:
Win16 是為十六位處理器開發的,早期的作業系統都支援
Wint32 是為三十二位處理器開發的,它的移植性比較強,被大部分處理器所支援。當前我們主要使用的都是這個。
Windows 32 API主要有三個類庫:(它們的詳細內容可以自己去查閱資料)
Kernel32.dll
User32.dll
GDI32.dll
首先,有一個問題:Win32 API函數放在哪裡?
Win32 API函數是Windows的核心,比如我們看到的表單、按鈕、對話方塊什麼的,都是依靠Win32函數“畫”在螢幕上的,由於這些控制項(有時也稱組件)都用於使用者與Windows進行互動,所以控制這些控制項的Win32 API函數稱為“使用者介面”函數(User Interface Win32 API),簡稱UI函數;還有一些函數,並不用於互動,比如管理當前系統正在啟動並執行進程、硬體系統狀態的監視等等……這些函數只有一套,但是可以被所有的Windows程式調用(只要這個程式的許可權足夠高),簡而言之,API是為程式所共用的。為了達到所有程式能共用一套API的目的,Windows採用了“動態連結程式庫”的辦法。之所以叫“動態連結程式庫”,是因為這樣的函數庫的調用方式是“隨用隨取”而不是像靜態連結庫那樣“用不用都要帶上”。
這裡不太好理解,不要緊,我們舉個小例子。我們把Windows比做一個遊樂場,而把在遊樂場裡玩兒的小孩比做一個一個程式。小孩在玩的過程中可能要喝水。我們有兩個辦法讓小傢伙們想喝水的時候就有水喝:1.給每個小傢伙配一個水壺,小傢伙們喝了的話就喝自己帶的水;2.給遊樂場配一個飲水機,誰渴了誰來喝。顯然,第二個方法要好得多,這體現在三個地方。第一,帶著水壺,小傢伙身體不靈活、玩不爽(影響程式的速度),況且這隻是帶了一個水壺,要是再帶上飯盒呢?還有輪滑、頭盔、創可貼、紗布……AK-47 My God,如果帶全了就趕上美國大兵了。所以遊樂園裡還是有個公用“倉庫”要來的方便,讓大家隨用隨取(動態連結)。第二,小傢伙們帶了那麼多東西,佔了遊樂場很多地方,讓遊樂場擁擠不堪,別的小朋友就進不來了(程式體積大,影響程式和系統的效能)。第三,如果某件物品升級了,比如水壺從一升的改為二升的,那麼每個小傢伙就必須go home去換新的(重新編譯器,由編譯器把新的靜態庫連結進程式主體裡),而第二種情況裡,只要遊樂場把自己倉庫裡的水壺換個型號,那麼所有小傢伙就都在同一時間擁有了大容量的水壺。Win32 API函數是放在Windows系統的核心庫檔案中的,這些庫在硬碟裡的儲存形式是.dll檔案。我們常用到的dll檔案是user32.dll和kernel32.dll兩個檔案,還有其它一些dll檔案也非常重要,大家要在實踐中多積累經驗。
我們知道Win32 API函數是放在dll檔案中了,但新問題又來了——我們怎麼調用它們呢?這些dll檔案是用C語言寫的,原始碼經C語言編譯器編譯之後,會以二進位可執行代碼形式存放在這些dll檔案中,就好像蘋果被打碎機打成果醬後裝在罐子裡一樣——你再也分不清哪個是你GF給你的,哪個是你老媽給你的一樣。為了能讓程式使用這些函數,微軟在發布每個新的作業系統的時候,也會放出這個系統的SDK,目前最新的是Win2003 SP1 SDK,據說Vista的馬上就要放出來,而且已經把UI的API從核心庫中分離出去以提高系統的穩定性了。SDK裡有一些C語言的標頭檔(.h檔案),這些檔案裡描述了核心dll檔案裡都有哪些Win32 API函數,在寫程式的時候,把這些.h檔案用#include"....."指令包含進你的程式裡,你就可以使用這些Win32 API了。至於程式是怎樣連結的,超出了本文的範圍——也超出了本人的知識範圍:D
至此,如果你是C語言高手,已經可以使用Windows SDK去調教Windows了!不過,今天我們討論的是C#語言調用Win32 API的問題。我們現在已經知道API函數放在dll動態連結程式庫檔案裡,也知道C語言怎麼調用它們了,那麼C#語言怎麼辦呢?C#語言是不能使用C語言的.h檔案的。C#語言也使用dll動態連結程式庫,不過這些dll都是.NET版本的,具有“自描述性”,也就是自己肚子裡都有哪些函數都已經寫在自己的metadata裡了,不用再附加一個.h檔案來說明。現在,我們已經找到了問題的關鍵點:如何用.NET平台上的C#語言來調用Win32平台上的dll檔案。答案非常簡單:使用DllImport特性。
首先,我舉個例子看看:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
public partial class Form1 : Form
{
[DllImport("User32.dll")]
private static extern int MessageBox(int h, string m, string c, int type);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MessageBox(0, "API Message Box", "API Demo", 0);
}
}
}
上面的是一個完整的WinForm表單應用程式,點擊按鈕會產生一個彈窗。
在建立一個WinForm表單應用程式後,首先,你要添加一個命名空間
System.Runtime.InteropServices,在這個命名空間中包含了DllImport屬性類別,之後我們需要聲明要用到的API函數,如下:
[DllImport("User32.dll")]
private static extern int MessageBox(int h, string m, string c, int type);
DllImport指定了要使用的類庫,即要使用的DLL,User32.dll為類庫名,"static"修飾符聲明一個靜態元素,而該元素屬於類型本身而不是指定的對象;"extern"表示該方法將在工程外部執行,同時使用DllImport匯入的方法必須使用"extern"修飾符;MessageBox是API函數的名稱。其中DLL是必要的參數,同時在聲明中還有很多可選的參數。如:
在中,主要的欄位:
(1)CallingConvention:它指示進入點的調用協議,它的值有下面幾個
Cdecl = 2,調用方清理堆棧。這使您能夠調用具有 varargs 的函數(如 Printf),使之可用於接受可變數目的參數的方法。
StdCall = 3,被呼叫者清理堆棧。這是使用平台 invoke 調用非託管函數的預設約定。
ThisCall = 4,第一個參數是 this 指標,它儲存在寄存器 ECX 中。其他參數被推送到堆棧上。此呼叫慣例用於對從非託管 DLL 匯出的類調用方法。
FastCall = 5,不支援此呼叫慣例。
(2)CharSet:規定封送字元床用使用何種字元集,並控制名稱的重整(後面這句不明白),它的值也有幾種:
Ansi = 2,以多位元組字串的形式封送字串。
Unicode = 3,以 Unicode 2 位元組字元形式封送字串。
Auto = 4,針對目標作業系統適當地自動封送字串。 預設情況下,C# 將所有方法和類型都標記為System.Runtime.InteropServices.CharSet.Ansi。
(3)EntryPoint:指示要調用的 DLL 進入點的名稱或序號,簡單點說就是如果你想用自己定義的函數名而不用API定義好的,這時它就派上用場了。舉個例子:
[DllImport("User32.dll",EntryPoint = “MessageBox”)]
private static extern int MsgBox(int h, string m, string c, int type);
這個調用的函數其實就是 MessageBox,但是你不想用時,就用EntryPoint定義這個函數,聲明方法時可以用自己的函數名,如這裡的MsgBox.
(4)ExactSpelling:控制System.Runtime.InteropServices.DllImportAttribute.CharSet是否使用通用語言執行平台非託管DLL 中搜尋進入點名稱,而不使用指定的進入點名稱,這個準確的說我不太明白是什麼意思,但是我這裡有一個例子可以給大家看看:
using System;
using System.Collections.Generic;
using System.ComponentModel;
using System.Data;
using System.Drawing;
using System.Linq;
using System.Text;
using System.Windows.Forms;
using System.Runtime.InteropServices;
namespace ConsoleApp1
{
public partial class Form1 : Form
{
[DllImport("User32.dll", EntryPoint = "MessageBox",ExactSpelling = false)]
private static extern int MsgBox(int h, string m, string c, int type);
public Form1()
{
InitializeComponent();
}
private void button1_Click(object sender, EventArgs e)
{
MsgeBox(0, "API Message Box", "API Demo", 0);
}
}
}
上面的這個程式是可以啟動並執行.但是當你將ExactSpelling 設定為true時,就會報錯。
所以,就是說ExactSpelling與EntryPoint 是相關的。
(5)SetLastError:指示被呼叫者在從特性方法返回之前是否調用SetLastError Win32 API函數。
涉及到函數調用,自然免不了要向系統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 位 |
再思考一個問題,如果你要傳遞或返回的參數是一個複雜的參數,如封裝類、結構和聯合。
類和結構在 .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("Kernal.dll",EntryPoint = “SetSystemTime”)]
private 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 位元組字元形式封送字串。 |
C#調用API