Api函數是構築Windws應用程式的基石,每一種Windows應用程式開發工具,它提供的底層函數都間接或直接地調用了Windows API函數,同時為了實現功能擴充,一般也都提供了調用WindowsAPI函數的介面, 也就是說具備調用動態串連庫的能力。Visual C#和其它開發工具一樣也能夠調用動態連結程式庫的API函數。.NET架構本身提供了這樣一種服務,允許受管轄的代碼調用動態連結程式庫中實現的非受管轄函數,包括作業系統提供的Windows API函數。它能夠定位和調用輸出函數,根據需要,組織其各個參數(整型、字串類型、數組、和結構等等)跨越互操作邊界。
下面以C#為例簡單介紹調用API的基本過程:
動態連結程式庫函數的聲明
動態連結程式庫函數使用前必須聲明,相對於VB,C#函式宣告顯得更加羅嗦,前者通過 Api Viewer粘貼以後,可以直接使用,而後者則需要對參數作些額外的變化工作。
動態連結程式庫函式宣告部分一般由下列兩部分組成,一是函數名或索引號,二是動態連結程式庫的檔案名稱。
譬如,你想調用User32.DLL中的MessageBox函數,我們必須指明函數的名字MessageBoxA或MessageBoxW,以及庫名字User32.dll,我們知道Win32 API對每一個涉及字串和字元的函數一般都存在兩個版本,單位元組字元的ANSI版本和雙位元組字元的UNICODE版本。
下面是一個調用API函數的例子:
[DllImport("KERNEL32.DLL", EntryPoint="MoveFileW", SetLastError=true,
CharSet=CharSet.Unicode, ExactSpelling=true,
CallingConvention=CallingConvention.StdCall)]
public static extern bool MoveFile(String src, String dst);
其中進入點EntryPoint標識函數在動態連結程式庫的入口位置,在一個受管轄的工程中,目標函數的原始名字和序號進入點不僅標識一個跨越互操作界限的函數。而且,你還可以把這個進入點映射為一個不同的名字,也就是對函數進行重新命名。重新命名可以給調用函數帶來種種便利,通過重新命名,一方面我們不用為函數的大小寫傷透腦筋,同時它也可以保證與已有的命名規則保持一致,允許帶有不同參數類型的函數共存,更重要的是它簡化了對ANSI和Unicode版本的調用。CharSet用於標識函數調用所採用的是Unicode或是ANSI版本,ExactSpelling=false將告訴編譯器,讓編譯器決定使用Unicode或者是Ansi版本。其它的參數請參考MSDN線上協助.
在C#中,你可以在EntryPoint域通過名字和序號聲明一個動態連結程式庫函數,如果在方法定義中使用的函數名與DLL進入點相同,你不需要在EntryPoint域顯示聲明函數。否則,你必須使用下列屬性格式指示一個名字和序號。
[DllImport("dllname", EntryPoint="Functionname")]
[DllImport("dllname", EntryPoint="#123")]
值得注意的是,你必須在數字序號前加“#”
下面是一個用MsgBox替換MessageBox名字的例子:
[C#]
using System.Runtime.InteropServices;
public class Win32 {
[DllImport("user32.dll", EntryPoint="MessageBox")]
public static extern int MsgBox(int hWnd, String text, String caption, uint type);
}
許多受管轄的動態連結程式庫函數期望你能夠傳遞一個複雜的參數類型給函數,譬如一個使用者定義的結構類型成員或者受管轄代碼定義的一個類成員,這時你必須提供額外的資訊格式化這個類型,以保持參數原有的布局和對齊。
C#提供了一個StructLayoutAttribute類,通過它你可以定義自己的格式化類型,在受管轄代碼中,格式化類型是一個用StructLayoutAttribute說明的結構或類成員,通過它能夠保證其內部成員預期的布局資訊。布局的選項共有三種:
布局選項
描述
LayoutKind.Automatic
為了提高效率允許運行態對類型成員重新排序。
注意:永遠不要使用這個選項來調用不受管轄的動態連結程式庫函數。
LayoutKind.Explicit
對每個域按照FieldOffset屬性對類型成員排序
LayoutKind.Sequential
對出現在受管轄類型定義地方的不受管轄記憶體中的類型成員進行排序。
傳遞結構成員
下面的例子說明如何在受管轄代碼中定義一個點和矩形類型,並作為一個參數傳遞給User32.dll庫中的PtInRect函數,
函數的不受管轄原型聲明如下:
BOOL PtInRect(const RECT *lprc, POINT pt);
注意你必須通過引用傳遞Rect結構參數,因為函數需要一個Rect的結構指標。
[C#]
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct Point {
public int x;
public int y;
}
[StructLayout(LayoutKind.Explicit]
public struct Rect {
[FieldOffset(0)] public int left;
[FieldOffset(4)] public int top;
[FieldOffset(8)] public int right;
[FieldOffset(12)] public int bottom;
}
class Win32API {
[DllImport("User32.dll")]
public static extern Bool PtInRect(ref Rect r, Point p);
}
類似你可以調用GetSystemInfo函數獲得系統資訊:
? using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct SYSTEM_INFO {
public uint dwOemId;
public uint dwPageSize;
public uint lpMinimumApplicationAddress;
public uint lpMaximumApplicationAddress;
public uint dwActiveProcessorMask;
public uint dwNumberOfProcessors;
public uint dwProcessorType;
public uint dwAllocationGranularity;
public uint dwProcessorLevel;
public uint dwProcessorRevision;
}
[DllImport("kernel32")]
static extern void GetSystemInfo(ref SYSTEM_INFO pSI);
SYSTEM_INFO pSI = new SYSTEM_INFO();
GetSystemInfo(ref pSI);
類成員的傳遞
同樣只要類具有一個固定的類成員布局,你也可以傳遞一個類成員給一個不受管轄的動態連結程式庫函數,下面的例子主要說明如何傳遞一個sequential順序定義的MySystemTime類給User32.dll的GetSystemTime函數, 函數用C/C++調用規範如下:
void GetSystemTime(SYSTEMTIME* SystemTime);
不像傳實值型別,類總是通過引用傳遞參數.
[C#]
[StructLayout(LayoutKind.Sequential)]
public class MySystemTime {
public ushort wYear;
public ushort wMonth;
public ushort wDayOfWeek;
public ushort wDay;
public ushort wHour;
public ushort wMinute;
public ushort wSecond;
public ushort wMilliseconds;
}
class Win32API {
[DllImport("User32.dll")]
public static extern void GetSystemTime(MySystemTime st);
}
回呼函數的傳遞:
從受管轄的代碼中調用大多數動態連結程式庫函數,你只需建立一個受管轄的函數定義,然後調用它即可,這個過程非常直接。
如果一個動態連結程式庫函數需要一個函數指標作為參數,你還需要做以下幾步:
首先,你必須參考有關這個函數的文檔,確定這個函數是否需要一個回調;第二,你必須在受管轄代碼中建立一個回呼函數;最後,你可以把指向這個函數的指標作為一個參數創遞給DLL函數,.
回呼函數及其實現:
回呼函數經常用在任務需要重複執行的場合,譬如用於枚舉函數,譬如Win32 API 中的EnumFontFamilies(字型枚舉), EnumPrinters(印表機), EnumWindows (視窗枚舉)函數. 下面以視窗枚舉為例,談談如何通過調用EnumWindow 函數遍曆系統中存在的所有視窗
分下面幾個步驟:
1. 在實現調用前先參考函數的聲明
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARMAM IParam)
顯然這個函數需要一個回呼函數地址作為參數.
2. 建立一個受管轄的回呼函數,這個例子聲明為代表類型(delegate),也就是我們所說的回調,它帶有兩個參數hwnd和lparam,第一個參數是一個視窗控制代碼,第二個參數由應用程式定義,兩個參數均為整形。
當這個回呼函數返回一個非零值時,標示執行成功,零則暗示失敗,這個例子總是返回True值,以便持續枚舉。
3. 最後建立以代表對象(delegate),並把它作為一個參數傳遞給EnumWindows 函數,平台會自動地 把代錶轉化成函數能夠識別的回調格式。
[C#]
using System;
using System.Runtime.InteropServices;
public delegate bool CallBack(int hwnd, int lParam);
public class EnumReportApp {
[DllImport("user32")]
public static extern int EnumWindows(CallBack x, int y);
public static void Main()
{
CallBack myCallBack = new CallBack(EnumReportApp.Report);
EnumWindows(myCallBack, 0);
}
public static bool Report(int hwnd, int lParam) {
Console.Write("視窗控制代碼為");
Console.WriteLine(hwnd);
return true;
}
}
指標型別參數傳遞:
在Windows API函數調用時,大部分函數採用指標傳遞參數,對一個結構變數指標,我們除了使用上面的類和結構方法傳遞參數之外,我們有時還可以採用數組傳遞參數。
下面這個函數通過調用GetUserName獲得使用者名稱
BOOL GetUserName(
LPTSTR lpBuffer, // 使用者名稱緩衝區
LPDWORD nSize // 存放緩衝區大小的地址指標
);
[DllImport("Advapi32.dll",
EntryPoint="GetComputerName",
ExactSpelling=false,
SetLastError=true)]
static extern bool GetComputerName (
[MarshalAs(UnmanagedType.LPArray)] byte[] lpBuffer,
[MarshalAs(UnmanagedType.LPArray)] Int32[] nSize );
這個函數接受兩個參數,char * 和int *,因為你必須分配一個字串緩衝區以接受字串指標,你可以使用String類代替這個參數類型,當然你還可以聲明一個位元組數組傳遞ANSI字串,同樣你也可以聲明一個只有一個元素的長整型數組,使用數組名作為第二個參數。上面的函數可以調用如下:
byte[] str=new byte[20];
Int32[] len=new Int32[1];
len[0]=20;
GetComputerName (str,len);
MessageBox.Show(System.Text.Encoding.ASCII.GetString(str));
最後需要提醒的是,每一種方法使用前必須在檔案頭加上:
using System.Runtime.InteropServices;
//--------------------------------------------------------------------------------------------------------------------------------------------------
在.Net Framework SDK文檔中,關於調用Windows API的指示比較零散,並且其中稍全面一點的是針對Visual Basic .net講述的。本文將C#中調用API的要點彙集如下,希望給未在C#中使用過API的朋友一點協助。另外如果安裝了Visual Studio .net的話,在C:\Program Files\Microsoft Visual Studio .NET\FrameworkSDK\Samples\Technologies\Interop\PlatformInvoke\WinAPIs\CS目錄下有大量的調用API的例子。
一、調用格式
|
using System.Runtime.InteropServices; //引用此名稱空間,簡化後面的代碼 ... //使用DllImportAttribute特性來引入api函數,注意聲明的是空方法,即方法體為空白。 [DllImport("user32.dll")] public static extern ReturnType FunctionName(type arg1,type arg2,...); //調用時與調用其他方法並無區別 |
可以使用欄位進一步說明特性,用逗號隔開,如:
|
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )] |
DllImportAttribute特性的公用欄位如下:
1、CallingConvention 指示向非託管實現傳遞方法參數時所用的 CallingConvention 值。
CallingConvention.Cdecl : 調用方清理堆棧。它使您能夠調用具有 varargs 的函數。
CallingConvention.StdCall : 被呼叫者清理堆棧。它是從Managed 程式碼調用非託管函數的預設約定。
2、CharSet 控制調用函數的名稱版本及指示如何向方法封送 String 參數。
此欄位被設定為 CharSet 值之一。如果 CharSet 欄位設定為 Unicode,則所有字串參數在傳遞到非託管實現之前都轉換成 Unicode 字元。這還導致向 DLL EntryPoint 的名稱中追加字母“W”。如果此欄位設定為 Ansi,則字串將轉換成 ANSI 字串,同時向 DLL EntryPoint 的名稱中追加字母“A”。
大多數 Win32 API 使用這種追加“W”或“A”的約定。如果 CharSet 設定為 Auto,則這種轉換就是與平台有關的(在 Windows NT 上為 Unicode,在 Windows 98 上為 Ansi)。CharSet 的預設值為 Ansi。CharSet 欄位也用於確定將從指定的 DLL 匯入哪個版本的函數。
CharSet.Ansi 和 CharSet.Unicode 的名稱匹配規則大不相同。對於 Ansi 來說,如果將 EntryPoint 設定為“MyMethod”且它存在的話,則返回“MyMethod”。如果 DLL 中沒有“MyMethod”,但存在“MyMethodA”,則返回“MyMethodA”。
對於 Unicode 來說則正好相反。如果將 EntryPoint 設定為“MyMethod”且它存在的話,則返回“MyMethodW”。如果 DLL 中不存在“MyMethodW”,但存在“MyMethod”,則返回“MyMethod”。如果使用的是 Auto,則匹配規則與平台有關(在 Windows NT 上為 Unicode,在 Windows 98 上為 Ansi)。如果 ExactSpelling 設定為 true,則只有當 DLL 中存在“MyMethod”時才返回“MyMethod”。
3、EntryPoint 指示要調用的 DLL 進入點的名稱或序號。
如果你的方法名不想與api函數同名的話,一定要指定此參數,例如:
|
[DllImport("user32.dll",CharSet="CharSet.Auto",EntryPoint="MessageBox")] public static extern int MsgBox(IntPtr hWnd,string txt,string caption, int type); |
4、ExactSpelling 指示是否應修改非託管 DLL 中的進入點的名稱,以與 CharSet 欄位中指定的 CharSet 值相對應。如果為 true,則當 DllImportAttribute.CharSet 欄位設定為 CharSet 的 Ansi 值時,向方法名稱中追加字母 A,當 DllImportAttribute.CharSet 欄位設定為 CharSet 的 Unicode 值時,向方法的名稱中追加字母 W。此欄位的預設值是 false。
5、PreserveSig 指示託管方法簽名不應轉換成返回 HRESULT、並且可能有一個對應於傳回值的附加 [out, retval] 參數的非託管簽名。
6、SetLastError 指示被呼叫者在從屬性化方法返回之前將調用 Win32 API SetLastError。 true 指示調用方將調用 SetLastError,預設為 false。運行時封送拆收器將調用 GetLastError 並緩衝返回的值,以防其被其他 API 呼叫重寫。使用者可通過調用 GetLastWin32Error 來檢索錯誤碼。
二、參數類型:
1、數值型直接用對應的就可。(DWORD -> int , WORD -> Int16)
2、API中字串指標類型 -> .net中string
3、API中控制代碼 (dWord) -> .net中IntPtr
4、API中結構 -> .net中結構或者類。注意這種情況下,要先用StructLayout特性限定聲明結構或類
公用語言運行庫利用StructLayoutAttribute控制類或結構的資料欄位在託管記憶體中的物理布局,即類或結構需要按某種方式排列。如果要將類傳遞給需要指定布局的Unmanaged 程式碼,則顯式控制類布局是重要的。它的建構函式中用LayoutKind值初始化 StructLayoutAttribute 類的新執行個體。 LayoutKind.Sequential 用於強制將成員按其出現的順序進行循序配置。
LayoutKind.Explicit 用於控制每個資料成員的精確位置。利用 Explicit, 每個成員必須使用 FieldOffsetAttribute 指示此欄位在類型中的位置。如:
|
[StructLayout(LayoutKind.Explicit, Size=16, CharSet=CharSet.Ansi)] public class 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; } |
下面是針對API中OSVERSIONINFO結構,在.net中定義對應類或結構的例子:
|
/********************************************** * API中定義原結構聲明 * OSVERSIONINFOA STRUCT * dwOSVersionInfoSize DWORD ? * dwMajorVersion DWORD ? * dwMinorVersion DWORD ? * dwBuildNumber DWORD ? * dwPlatformId DWORD ? * szCSDVersion BYTE 128 dup (?) * OSVERSIONINFOA ENDS * * OSVERSIONINFO equ <OSVERSIONINFOA> *********************************************/ |
|
//.net中聲明為類 [ StructLayout( LayoutKind.Sequential )] public class OSVersionInfo { public int OSVersionInfoSize; public int majorVersion; public int minorVersion; public int buildNumber; public int platformId; [ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] public String versionString; } //或者 //.net中聲明為結構 [ StructLayout( LayoutKind.Sequential )] public struct OSVersionInfo2 { public int OSVersionInfoSize; public int majorVersion; public int minorVersion; public int buildNumber; public int platformId; |
|
[ MarshalAs( UnmanagedType.ByValTStr, SizeConst=128 )] public String versionString; } |
此例中用到MashalAs特性,它用於描述欄位、方法或參數的封送處理格式。用它作為參數首碼並指定目標需要的資料類型。例如,以下代碼將兩個參數作為資料類型長指標封送給 Windows API 函數的字串 (LPStr):
|
[MarshalAs(UnmanagedType.LPStr)] String existingfile; [MarshalAs(UnmanagedType.LPStr)] String newfile; |
注意結構作為參數時候,一般前面要加上ref修飾符,否則會出現錯誤:對象的引用沒有指定對象的執行個體。
|
[ DllImport( "kernel32", EntryPoint="GetVersionEx" )] public static extern bool GetVersionEx2( ref OSVersionInfo2 osvi ); |
三、如何保證使用託管對象的平台叫用成功?
如果在調用平台 invoke 後的任何位置都未引用託管對象,則記憶體回收行程可能將完成該託管對象。這將釋放資源並使控制代碼無效,從而導致平台invoke 調用失敗。用 HandleRef 封裝控制代碼可保證在平台 invoke 調用完成前,不對託管對象進行記憶體回收。
例如下面:
|
FileStream fs = new FileStream( "a.txt", FileMode.Open ); StringBuilder buffer = new StringBuilder( 5 ); int read = 0; ReadFile(fs.Handle, buffer, 5, out read, 0 ); //調用Win API中的ReadFile函數 |
由於fs是託管對象,所以有可能在平台叫用還未完成時候被記憶體回收站回收。將檔案流的控制代碼用HandleRef封裝後,就能避免被垃圾站回收:
|
[ DllImport( "Kernel32.dll" )] public static extern bool ReadFile( HandleRef hndRef, StringBuilder buffer, int numberOfBytesToRead, out int numberOfBytesRead, ref Overlapped flag ); ...... ...... FileStream fs = new FileStream( "HandleRef.txt", FileMode.Open ); HandleRef hr = new HandleRef( fs, fs.Handle ); StringBuilder buffer = new StringBuilder( 5 ); int read = 0; // platform invoke will hold reference to HandleRef until call ends ReadFile( hr, buffer, 5, out read, 0 ); |
Trackback: http://tb.blog.csdn.net/TrackBack.aspx?PostId=1795838