除了舊有的 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 命名空間。
    1. 宣告一個靜態外部方法,並以要呼叫的那個 API 函式名稱命名。
      這個步驟中,將方法宣告成靜態 (static/shared) 是必然的, 而宣告成外部 (external) 則是告訴編譯器,該方法並非存在於內部,必須在指定的那個 DLL 中找到。 若這個方法有參數的話,也必須在這個宣告中轉換成對應於 C# 當中的型態。 另外,若想以自訂名稱命名也可,不過,要先指明進入點(EntryPoint)這個屬性即可。例如:[DllImport(“user32.dll”), EntryPoint=”GetForegroundWindow”]
    1. 替這個方法加上 DllImport 屬性,指明該函式是在哪個動態連結函式庫中 ( DLL )。
    1. 在程式碼中,呼叫這個方法。

我們拿 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 程式碼,就得指明結構的實體配置才行。 而 StructLayoutFieldOffset 就是用來說明結構的配置屬性。

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

  1. 建立處理回呼的方法。
  2. 替這個方法建立一個委派宣告。
  3. 宣告函式的原型,並以委派為參數。
  4. 呼叫函式。

下面範例示範如何呼叫 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 類別名稱說明
HANALEvoid*System.IntPtr32 位元 Windows 作業系統上為 32 位元。<br> <br> 64 位元 Windows 作業系統上為 64位元。
BYTEunsigned charSystem.Byte8 位元。
SHORTshortSystem.Int1616 位元。
WORDunsigned shortSystem.UInt1616 位元。
INTintSystem.Int3232 位元。
UINTunsigned intSystem.UInt3232 位元。
LONGlongSystem.Int3232 位元。
BOOLlongSystem.Int3232 位元。
DWORDunsigned longSystem.UInt3232 位元。
ULONGunsigned longSystem.UInt3232 位元。
CHARcharSystem.Char以 ANSI 修飾。
LPSTRchar*System.String 或<br> <br> System.Text.StringBuilder以 ANSI 修飾。
LPCSTRConst char*System.String 或<br> <br> System.Text.StringBuilder以 ANSI 修飾。
LPWSTRwchar_t*System.String 或<br> <br> System.Text.StringBuilder以 ANSI 修飾。
LPCWSTRConst wchar_t*System.String 或<br> <br> System.Text.StringBuilder以 ANSI 修飾。
FLOATFloatSystem.Single32 位元。
DOUBLEDoubleSystem.Double64 位元。

參考資料