核心命名空間與屬性
using System;
using System.Runtime.InteropServices; // DllImport, StructLayout, MarshalAs, In, Out, HandleRef, SafeHandle
using System.Text; // StringBuilder
using Microsoft.Win32.SafeHandles; // SafeHandleZeroOrMinusOneIsInvalid
基本宣告語法
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = false)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);
// 關鍵屬性說明:
// CharSet.Auto : 依平台自動選 ANSI/Unicode (WinNT=Unicode, Win9x=ANSI)
// SetLastError = true : 允許 Marshal.GetLastWin32Error() 取得錯誤碼
// ExactSpelling = false : 允許運行時依 CharSet 附加 A/W 後綴尋找入口點
// CallingConvention : 預設 StdCall (Win32 API),C 運行時庫用 Cdecl
型別對照表 (Win32 → C#)
| Win32 / C | C# 關鍵字 / 型別 | 備註 |
VOID | void | |
BOOL | bool / int (32-bit) | C# bool 封送為 4-byte BOOL |
BYTE | byte | |
CHAR | sbyte / byte | |
WORD | ushort | 16-bit unsigned |
DWORD | uint | 32-bit unsigned |
LONG | int | 32-bit signed |
ULONG | uint | |
INT/UINT | int/uint | |
FLOAT/DOUBLE | float/double | |
HANDLE/HWND/HMODULE | IntPtr / SafeHandle | **首選 SafeHandle** 避免 GC 競態 |
LPSTR/LPCSTR | string (In) / StringBuilder (Out) | ANSI 字串 |
LPWSTR/LPCWSTR | string (In) / StringBuilder (Out) | Unicode 字串 (CharSet.Unicode) |
LPTSTR | string / StringBuilder | 配合 CharSet.Auto 自動切換 |
| 結構體指標 | ref Struct / out Struct | 結構體需 [StructLayout(LayoutKind.Sequential)] |
| 函數指標 | delegate | 回調函數 |
結構體封送
// Win32: typedef struct _RECT { LONG left, top, right, bottom; } RECT;
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
public int Left;
public int Top;
public int Right;
public int Bottom;
}
// 含字串固定長度
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct WNDCLASSEX
{
public uint cbSize;
public uint style;
public IntPtr lpfnWndProc;
public int cbClsExtra;
public int cbWndExtra;
public IntPtr hInstance;
public IntPtr hIcon;
public IntPtr hCursor;
public IntPtr hbrBackground;
[MarshalAs(UnmanagedType.LPTStr)] public string lpszMenuName;
[MarshalAs(UnmanagedType.LPTStr)] public string lpszClassName;
public IntPtr hIconSm;
}
// 使用
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern short RegisterClassEx([In] ref WNDCLASSEX lpwcx);
字串封送規則
| 方向 | Win32 型別 | C# 型別 | MarshalAs |
| In (唯讀) | LPCSTR/LPCWSTR/LPCTSTR | string | 自動 (預設 LPStr/LPWStr/LPTStr) |
| Out (寫入緩衝區) | LPSTR/LPWSTR/LPTSTR | StringBuilder | [MarshalAs(UnmanagedType.LPTStr)] + 容量 |
| In/Out | LPTSTR | StringBuilder | 同上 |
// GetWindowText: int GetWindowText(HWND hWnd, LPTSTR lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);
// 呼叫
var sb = new StringBuilder(256);
GetWindowText(hWnd, sb, sb.Capacity);
string title = sb.ToString();
回調函數
// EnumWindows: BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
// 使用
static bool Callback(IntPtr hWnd, IntPtr lParam)
{
var sb = new StringBuilder(256);
GetWindowText(hWnd, sb, sb.Capacity);
Console.WriteLine($"0x{hWnd.ToInt64():X}: {sb}");
return true; // 繼續枚舉
}
EnumWindows(Callback, IntPtr.Zero);
進階:SafeHandle 替代 IntPtr (資源安全)
public class SafeWindowHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeWindowHandle() : base(true) { } // ownsHandle = true
protected override bool ReleaseHandle()
{
// 視實際 Handle 類型呼叫對應釋放 API
// 例如 DestroyWindow, CloseHandle, RegCloseKey
return DestroyWindow(handle);
}
[DllImport("user32.dll", ExactSpelling = true)]
private static extern bool DestroyWindow(IntPtr hWnd);
}
// 宣告使用 SafeHandle
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeWindowHandle CreateWindowEx(
uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle,
int x, int y, int nWidth, int nHeight,
IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);
陣列與緩衝區封送
// 1. 固定大小陣列 (結構體內)
[StructLayout(LayoutKind.Sequential)]
public struct FIXED_ARRAY
{
[MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
public byte[] Data;
}
// 2. 參數陣列 (Out)
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern uint GetLogicalDriveStrings(uint nBufferLength, [Out, MarshalAs(UnmanagedType.LPArray)] char[] lpBuffer);
// 呼叫
char[] buffer = new char[1024];
uint len = GetLogicalDriveStrings((uint)buffer.Length, buffer);
string drives = new string(buffer, 0, (int)len);
常見陷阱與解決
| 陷阱 | 症狀 | 解決 |
| CharSet 不匹配 | 中文亂碼、API 找不到入口點 | 統一用 CharSet.Auto,字串用 string/StringBuilder |
| 結構體記憶體對齊錯誤 | 欄位值錯誤、AccessViolation | [StructLayout(LayoutKind.Sequential, Pack = 4)] 指定封裝,對齊 Win32 預設 4/8 byte |
| GC 過早回收 Managed 物件 | 傳遞 Handle 的 API 隨機失敗 | 用 HandleRef 或 **SafeHandle** 包裝,保證存活至呼叫結束 |
| 委託被 GC 回收 | 回調函數隨機失效 | 將 delegate 存為 **static 欄位** 或類別成員,防止被收集 |
| SetLastError 未設定 | Marshal.GetLastWin32Error() 回傳 0 | [DllImport(..., SetLastError = true)] |
| 呼叫慣例錯誤 | 堆疊不平衡、PInvokeStackImbalance MDA | Win32 API 用 StdCall (預設),C Runtime 用 Cdecl |
錯誤處理模式
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
string lpFileName, uint dwDesiredAccess, uint dwShareMode,
IntPtr lpSecurityAttributes, uint dwCreationDisposition,
uint dwFlagsAndAttributes, IntPtr hTemplateFile);
public static SafeFileHandle SafeCreateFile(string path, FileAccess access)
{
var handle = CreateFile(path, (uint)access, 0, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (handle == IntPtr.Zero || handle == new IntPtr(-1)) // INVALID_HANDLE_VALUE
{
int err = Marshal.GetLastWin32Error();
throw new System.ComponentModel.Win32Exception(err, $"CreateFile failed for '{path}'");
}
return new SafeFileHandle(handle, true);
}
工具推薦
- PInvoke.net (pinvoke.net) —— 社群維護的 API 簽名庫,可直接複製。
- Windows SDK / Visual Studio ——
dumpbin /exports user32.dll 查看導出函數。
- CLR Inside Out / .NET Memory Profiler —— 診斷封送洩漏、Handle 洩漏。
現代替代:函數指標與 NativeLibrary (Core 3.0+)
// 動態載入 + 函數指標 (效能優於 DllImport)
var lib = NativeLibrary.Load("user32.dll");
var pFindWindow = NativeLibrary.GetExport(lib, "FindWindow");
var findWindow = Marshal.GetDelegateForFunctionPointer(pFindWindow);
// 委託簽名需匹配
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Auto)]
public delegate IntPtr FindWindowDelegate(string lpClassName, string lpWindowName);
總結
P/Invoke 是 C# 與作業系統、C/C++ 庫互操作的橋樑。**核心原則**:精準映射型別、明確擁有權 (SafeHandle)、設定 SetLastError、防止 GC 競態、單元測試邊界條件。優先查閱 PInvoke.net 避免重造錯誤簽名;複雜場景考慮 ComInterface、C++/CLI 混合模組或純 C 庫編譯為 .NET Standard 組件。