除了舊有的 COM 元件是 Unmanaged 程式碼之外,在 Windows 系統底下還有許多的 Windows APIs 是 .NET Framework 沒有包裝的。 這個章節就是要來認識如何叫用 Windows APIs.
平台叫用 ( P/Invoke )
在 .Net 習慣用語中, 從 Managed 程式碼中呼叫 Unmanaged 程式碼的動作稱為 Platform Invoke (平台叫用), 簡稱為 P/Invoke。
如何使用 P/Invoke
一般來說,我們要使用 P/Invoke 呼叫 Windows API 有下面幾個重要步驟:
- 1. 加入 System.Runtime.InteropServices 命名空間。
- 2. 宣告一個靜態的外部方法,並以要呼叫的那個 API 函式名稱命名。
這個步驟中,將方法宣告成靜態 (static/shared) 是必然的, 而宣告成外部 (external) 則是告訴編譯器,該方法並非存在於內部,必須在指定的那個 DLL 中找到。 若這個方法有參數的話,也必須在這個宣告中轉換成對應於 C# 當中的型態。 另外,若想以自訂名稱命名也可,不過,要先指明進入點(EntryPoint)這個屬性即可。例如:[DllImport("user32.dll"), EntryPoint="GetForegroundWindow"] - 3. 替這個方法加上 DllImport 屬性,指明該函式是在哪個動態連結函式庫中 ( DLL )。
- 4. 在程式碼中,呼叫這個方法。
我們拿 user32.dll 中的 GetForegroundWindow , GetWindowText 函式來看看。以下是 API 的原型宣告:
HWND WINAPI GetForegroundWindow(void); //The return value is a handle to the foreground window. int WINAPI GetWindowText( //The return value is the length of WindowText __in HWND hWnd, __out LPTSTR lpString, __in int nMaxCount );
在 C# 中要如何宣告對應的方法呢?請看下面示範:
//下面這段宣告表示 //程式中若呼叫 GetForegroundWindow 方法,應該要叫用 user32.dll 中的 GetForegroundWindow 函式 [DllImport("user32.dll")] private static extern IntPtr GetForegroundWindow(); //下面這段宣告表示 //程式中若呼叫 GetWindowText 方法,應該要叫用 user32.dll 中的 GetWindowText 函式 [DllImport("user32.dll")] private static extern Int32 GetWindowText(IntPtr hWnd,StringBuilder textValue, Int32 counter);
在 C# 中要如何呼叫這個外部方法呢?請看下面示範:
//取得作用中視窗的 handle StringBuilder DemoBuilder = new StringBuilder(BufferSize); IntPtr DemoHandle = GetForegroundWindow(); //取得視窗標題中的文字 if (GetWindowText(DemoHandle, DemoBuilder, BufferSize) > 0) { Console.WriteLine(DemoBuilder.ToString()); }
API查詢:
如何使用包裝類別 (Wrapper Class)
這裏指的包裝類別是指將常用的 DLL 函式包裝在 Managed 類別中。 這只是一種封裝平台功能的方法,在叫用 P/Invoke 上,並沒有強制一定得這麼做,只是讓叫用 P/Invoke 方法時,變得比較方便。
如下面範例,我們建立了一個封裝類別,並在該類別中指定叫用的 DLL,同時宣告一個靜態方法以對應到 DLL 中的方法。 這樣子,當程式叫用 Win32MessageBox.Show 方法時,就可以無需理會實際上叫用的 Unmanaged Function。
class Win32MessageBox { [DllImport("user32.dll")] private static extern int MessageBox(IntPtr hWnd, String text, String caption, uint type); public static void Show(string message, string caption) { MessageBox(new IntPtr(0), message, caption, 0); } } private void button3_Click(object sender, EventArgs e) { Win32MessageBox.Show("Hello, world!", "My MessageBox"); }
封送資料 (marshal)
因為 Managed 和 Unmanaged 程式碼的資料型別不相同,如果二者在互通時,必須傳遞資料,這時候就必須做型別轉換,這個轉換過程就稱為封送資料。 大部分的資料型別都有其預設的封送處理行為,所以並不一定需要手動轉換。 通常只有在某個型別可以封送多個型別,又必須特別指定時才必須手動處理。
以下是二種不同情狀下的使用方法:
- MarshalAs :指示如何在 Managed 和 Unmanaged 程式碼之間封送處理資料。
- StructLayout :控制類別或結構之資料欄位的實際配置。
使用 MarshalAs 特性
MarshalAs 屬性是用來指示如何在 Managed 和 Unmanaged 程式碼之間封送處理資料(marshal the data)。. 這個屬性可套用在參數、欄位或傳回值。 這個屬性是選擇性 (Optional) 的,因為每個資料型別都有預設的封送處理行為。通常只有在指定的型別可以封送處理至多個型別時,才需要這個屬性。 例如,您 String 可以封送處理成 LPStr、LPWStr、LPTStr、BStr 至 Unmanaged 程式碼。 依預設,Common Language Runtime 封送處理 String 參數為 BStr 至 COM 方法。 您可以套用 MarshalAs 屬性至個別欄位或參數,讓該特定字串封送處理為 LPStr 而不是 BStr。 例如:
[DllImport("msvcrt.dll")] public static extern int puts([MarshalAs(UnmanagedType.LPStr)] string m); class MarshalAsDemo { [MarshalAs(UnmanagedType.LPStr)] public String FirstName; [MarshalAs(UnmanagedType.Bool)] public Boolean IsCurrentlyWorking; public String LastName( [MarshalAs(UnmanagedType.LPStr)] String lastName); }
使用 StructLayoutAttribute 特性
在 Unmanaged 程式碼中,常常會使用結構來當做參數傳遞,所以當使用 P/Invoke 時,若要傳遞結構給 Unmanaged 程式碼,就得指明結構的實體配置才行。 而 StructLayout 和 FieldOffset 就是用來說明結構的配置屬性。
StructLayout 屬性的功能在於設定類別或結構在記憶體中的排列方式,通常,在 Managed 程式碼中,CLR 會自動控管記憶體中類別或結構之資料欄位的實際配置。 如果類別或結構需要以某種方式排列,便可以使用 StructLayout 屬性加以設定。 在預設的情況下,編譯器會依據類別或結構中各成員的大小做最佳化排列。 但是若該類別或結構用於 P/Invoke 情況以當作參數轉呼叫 DLL 動態連接函式庫時,則請務必設定 StructLayout 屬性,避免影響轉呼叫外部 UnManaged 程式碼時因類別或結構記憶體位置錯置而導致非預期的結果。
建構 StructLayout 時,可以有3種設定值
- LayoutKind.Auto :預設值,由 CLR 自動配置。
- LayoutKind.Sequential :依序配置,其順序即是將它們匯出至 Unmanaged 記憶體時所出現的順序。
- LayoutKind.Explicit :在 Unmanaged 記憶體中的物件,明確地指定其成員的配置順序。每個成員必須使用 FieldOffsetAttribute,表示該欄位在型別中的位置。
LayoutKind.Sequential
使用這個列舉成員定義類別或結構,其成員會依據定義的順序在記憶體中排列。 以下面的例子中,便會以先 x 後 y 的順序排列。
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)] public struct MyStruct { [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)] public string fname; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 4)] public string lname; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 7)] public string phone; }
LayoutKind.Explicit
使用這個列舉成員定義類別或結構,每個成員必須使用 FieldoffsetAttribute ,表示該欄位在型別中的記憶體相對位置。 如此程式設計師可以精確的掌控所有欄位於記憶體中的儲存順序。
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; }
其他封送資料時的屬性控制
- ComVisible :設定 Managed 程式碼中的型別或成員,在 COM 中是否可見。
- In :資料只會由 caller 封送處理到 callee ,不會封送處理回 caller。
- Out :資料由 caller 封送處理到 callee ,會再由 callee 封送處理回 caller。
- AutomationProxy :whether the type should be marshaled using the Automation marshaler or a custom proxy and stub.
- ClassInterface :這個屬性會控制型別程式庫匯出工具 (Tlbexp.exe) 是否會自動為屬性類別產生類別介面。
- ComRegisterFunction :
- ComSourceInterfaces :
Guidelines for Exposing .NET Types to COM Applications
- 類別不可 abstract
- 成員必須 public
- 類別必須具有 default constructor
- Define event-source interfaces in managed code.
- Include HRESULT error codes in custom exception classes.
- Supply GUIDs for types that require them.
範例一
底下範例,示範如何使用 API GetVersionEx 以取得系統的版本資訊。
[DllImport("Kernel32.dll")] public static extern bool GetVersionEx(ref OSVERSIONINFOEX osvi); [StructLayout(LayoutKind.Sequential)] public struct OSVERSIONINFOEX //重新定義屬於 C# 的 OSVERSIONINFOEX 結構。 { public Int32 dwOSVersionInfoSize; public Int32 dwMajorVersion; public Int32 dwMinorVersion; public Int32 dwBuildNumber; public Int32 dwPlatformId; [MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)] public String szCSDVersion; public Int16 wServicePackMajor; public Int16 wServicePackMinor; public Int16 wSuiteMask; public Byte wProductType; public Byte wReserved; } private void button5_Click(object sender, EventArgs e) { OSVERSIONINFOEX osvi = new OSVERSIONINFOEX(); osvi.dwOSVersionInfoSize = Marshal.SizeOf(typeof(OSVERSIONINFOEX)); bool bGetVersionEx = GetVersionEx(ref osvi); if (bGetVersionEx) { Console.WriteLine("MajorVersion : {0}", osvi.dwMajorVersion); Console.WriteLine("MinorVersion : {0}", osvi.dwMinorVersion); Console.WriteLine("BuildNumber : {0}", osvi.dwBuildNumber); Console.WriteLine("CSDVersion : {0}", osvi.szCSDVersion); } Console.WriteLine("OSVersion : {0}", Environment.OSVersion); }
範例二
底下範例,示範如何由程式A,尋找程式B中的某個按鈕,並按下Click事件,且自動關閉訊息視窗。
[DllImport("USER32.DLL")] public static extern IntPtr FindWindow(string lpClassName, string lpWindowName); [DllImport("USER32.DLL")] public static extern bool SetForegroundWindow(IntPtr hWnd); [DllImport("user32.dll")] static extern IntPtr FindWindowEx(IntPtr hwndParent, int hwndChildAfter, string lpszClass, string lpszWindow); [DllImport("user32.dll")] private static extern IntPtr PostMessage(IntPtr hwnd, int wMsg, IntPtr wParam, IntPtr lParam); [DllImport("user32.dll", EntryPoint = "SendMessageA")] private static extern int SendMessage(IntPtr hwnd, int wMsg, int wParam, IntPtr lParam); private void button8_Click(object sender, EventArgs e) { string sWindowCaption = txtWindowCaption.Text; string sButtonText = txtButtonText.Text; string sClassName = txtClassName.Text; // 取得 視窗 的 handle IntPtr hWnd = FindWindow(null, sWindowCaption); // 取得 Button 的 handle IntPtr hButton = FindWindowEx(hWnd, 0, sClassName, sButtonText); if (!hButton.Equals(IntPtr.Zero)) { // 設為 active windows SetForegroundWindow(hWnd); // invoke button click PostMessage(hButton, BM_CLICK, IntPtr.Zero, IntPtr.Zero); Thread.Sleep(2000); // 取得訊息視窗的 handle IntPtr handle = FindWindow("#32770", null); // 關閉訊息視窗 SendMessage(new HandleRef(null, handle), WM_CLOSE, IntPtr.Zero, IntPtr.Zero); } }
private void btnTest_Click(object sender, EventArgs e) { MessageBox.Show("hello"); }
如何設計回呼方法
傳統上,回呼方法都是以指標方式來設計的。所以我們必須提供一個方法以便接收 Unmanaged 方法的回傳。 需要使用回呼的方法,通常會帶有以 lp- 開頭 (long pointer),-Func 結尾 (function) 的參數。例如:
BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam)
下列步驟是處理需要回呼方法的 P/Invoke
- 建立處理回呼的方法。
- 替這個方法建立一個委派宣告。
- 宣告函式的原型,並以委派為參數。
- 呼叫函式。
下面範例示範如何呼叫 EnumWindows 函式
BOOL WINAPI EnumWindows( __in WNDENUMPROC lpEnumFunc, // A pointer to an application-defined callback function. For more information, see EnumWindowsProc. __in LPARAM lParam ); BOOL CALLBACK EnumWindowsProc( __in HWND hwnd, // A handle to a top-level window. __in LPARAM lParam );
public delegate bool CallBackFunc(int hwnd, int lParam); [DllImport("user32")] public static extern int EnumWindows(CallBackFunc callback, int lParam); public static bool OutputWindowsHandle(int hwnd, int lParam) { Console.WriteLine("Window handle is " + hwnd); return true; } private void button6_Click(object sender, EventArgs e) { EnumWindows(OutputWindowsHandle, 0); }
Exception Handling in Managed Code
HRESULT 是一個 32 位元值,它劃分為三個不同欄位:嚴重性代碼(severity code)、設備代碼(facility code)、錯誤碼(error code)。 當 Managed 程式碼擲回例外狀況時,Runtime 會將 HRESULT 傳遞至 COM 用戶端。 當 Unmanaged 程式碼傳回錯誤時,HRESULT 會轉換成 Runtime 接著將擲回的例外狀況。
當 COM 的方法發生錯誤時,就會回傳一個 HRESULT ;但是 .NET 的方法發生錯誤,則是擲回 exceptions。 因此在 Interoperation 作業時,這兩者之間的轉換則由 CLR 負責。.NET Framework 中的每一個例外狀況類別都會對應到一個 HRESULT。
如果我們自行定義一個例外狀況,也是可以自行指定一個適當的 HRESULT ,這樣子 COM 方法在使用時才有辨法回傳適當的 HRESULT。
您可以在例外狀況的建構函式 (Constructor) 中提供 HRESULT 值,將自行定義的例外狀況類別對應到指定的 HRESULT。 如果沒有指定,基底類別會指派 HRESULT 值。
public class NoAccessException : ApplicationException { public NoAccessException(string msg) : base(msg) { HResult = 12; } }
一般來說,呼叫 API 後,只會得到一個 true/false 表示呼叫結果是否有錯誤發生,必須另外再使用 Marshal.GetLastWin32Error 方法以取得錯誤碼。例如:
bool returnValue = LogonUser(userName, domainName, password, LOGON32_LOGON_INTERACTIVE, LOGON32_PROVIDER_DEFAULT, out impersonatedToken); // 檢查認證是否正確 if ( returnValue == false ) { int ret = Marshal.GetLastWin32Error(); Console.WriteLine("LogonUser failed with error code : {0}", ret); throw new System.ComponentModel.Win32Exception(ret); }
Limitations of Unmanaged Code
- Performance
- Type safety
- Code security
- Versioning
Marshal 類別
這個類別提供許多靜態方法,用來配置 Unmanaged 記憶體、複製 Unmanaged 記憶體區塊和將 Managed 型別轉換為 Unmanaged 型別,也包括其他和 Unmanaged 程式碼互動時使用的方法。例如:
- GetLastWin32Error :傳回使用平台叫用來呼叫的最後 Unmanaged 函式所傳回的錯誤碼。
- SizeOf :傳回類別的 Unmanaged 大小
- Copy :Copies data between a managed array and an unmanaged memory pointer。
- ReadByte, ReadInt16, ReadInt32, ReadInt64, and ReadIntPtr :Reads values from unmanaged memory。
- WriteByte, WriteInt16, WriteInt32, WriteInt64, and WriteIntPtr:Writes values to unmanaged memory。
- GetExceptionForHR :將 HRESULT 錯誤碼轉換成對應的 Exception 物件。
- ThrowExceptionForHR :擲回具有特定錯誤 HRESULT 值的例外狀況。
另一個有用的方法是 Marshal.SizeOf ,它可以用來取得一個類別或物件的大小。 例如:
Console.WriteLine(Marshal.SizeOf(typeof(Point))); Point p = new Point(); Console.WriteLine(Marshal.SizeOf(p));
表: P/Invoke 資料型態轉換對應表:
Wtypes.h 中的 Unmanaged 型別 | Unmanaged C 語言型別 | Managed 類別名稱 | 說明 |
---|---|---|---|
HANALE | 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 | 以 ANSI 修飾。 |
LPCWSTR | Const wchar_t* | System.String 或 System.Text.StringBuilder | 以 ANSI 修飾。 |
FLOAT | Float | System.Single | 32 位元。 |
DOUBLE | Double | System.Double | 64 位元。 |
沒有留言:
張貼留言