北京機械工業學院研00級(100085)冉林倉 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;
|