.NET Framework / .NET Core / .NET 5+ 提供了豐富的基礎類別庫,但作業系統底層功能(視窗管理、檔案系統細節、硬體控制、安全性 API 等)仍大量只存在於 Win32 API(kernel32.dll、user32.dll、advapi32.dll…)。P/Invoke (Platform Invocation Services) 是橋樑,讓 Managed 代碼呼叫 Unmanaged DLL 匯出函式。本文系統整理:基本語法、資料封送規則、字串/結構/陣列/回呼處理、DllImport 進階屬性、除錯技巧與最佳實務。
MessageBeepusing System;
using System.Runtime.InteropServices;
public static class Win32
{
[DllImport("user32.dll", SetLastError = true, CharSet = CharSet.Auto)]
public static extern bool MessageBeep(uint uType);
}
// 呼叫
Win32.MessageBeep(0x00000030); // MB_ICONEXCLAMATION
關鍵要素:
static extern:告訴編譯器方法體在外部 DLLDllImport:指定 DLL 名稱(不含路徑,依賴系統搜尋順序)SetLastError = true:呼叫後可用 Marshal.GetLastWin32Error() 取得錯誤碼CharSet = CharSet.Auto:自動選擇 ANSI/Unicode 版本(MessageBeepA/W)| Win32 / C | 大小/簽名 | CLR 對應 | 備註 |
|---|---|---|---|
| BYTE / UCHAR | 8-bit unsigned | byte | |
| SHORT / INT16 | 16-bit signed | short | |
| USHORT / WORD / ATOM / WCHAR | 16-bit unsigned | ushort | |
| INT / LONG / INT32 / BOOL | 32-bit signed | int | BOOL 建議用 bool(CLR 預設 4 字節) |
| UINT / DWORD / ULONG | 32-bit unsigned | uint | |
| LONGLONG / INT64 | 64-bit signed | long | |
| ULONGLONG / DWORDLONG | 64-bit unsigned | ulong | |
| FLOAT | 單精度 | float | |
| DOUBLE | 雙精度 | double | |
| HANDLE / HWND / HMODULE / HINSTANCE | 指標大小 | IntPtr / UIntPtr | **不透明指標**,不可轉為 int/long 運算 |
| LPVOID / PVOID | 指標 | IntPtr / void* (unsafe) |
陷阱:C/C++ long 是 4 字節,C# long 是 8 字節 → 對應 int。
| C/C++ 宣告 | C# 宣告 | 說明 |
|---|---|---|
int foo(int val) | int foo(int val) | 值傳遞(預設) |
int foo(int* val) | int foo(ref int val) | 輸入/輸出,呼叫端需初始化 |
int foo(int* val) | int foo(out int val) | 純輸出,呼叫端無需初始化 |
int foo(const char* str) | int foo(string str) | 唯讀字串,CLR 自動轉 ANSI/Unicode |
int foo(char* buf, int size) | int foo(StringBuilder buf, int size) | 可寫緩衝區,**必須預先配置 Capacity** |
int foo(MYSTRUCT* s) | int foo(ref MYSTRUCT s) | 結構指標,結構需 [StructLayout(LayoutKind.Sequential)] |
int foo(CALLBACK fn) | int foo(Delegate fn) | 回呼函式 → delegate,**必須保持委派存活** |
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern bool GetDiskFreeSpace(
[MarshalAs(UnmanagedType.LPTStr)] string rootPathName, // 唯讀
ref int sectorsPerCluster, ...);
// 輸出緩衝區
[DllImport("kernel32.dll", CharSet = CharSet.Auto)]
static extern int GetShortPathName(
[MarshalAs(UnmanagedType.LPTStr)] string path,
[MarshalAs(UnmanagedType.LPTStr)] StringBuilder shortPath,
int shortPathLength);
CharSet.Auto:Windows NT 系列用 Unicode (W),9x 用 ANSI (A)CharSet.Ansi:強制用 A 版,效能較差(雙向轉換)CharSet.Unicode:強制用 W 版ExactSpelling = true:關閉 A/W 自動搜尋,直接用指定名稱[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
struct TIME_ZONE_INFORMATION
{
public int Bias;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 32)]
public string StandardName;
public SYSTEMTIME StandardDate;
// ...
}
ByValTStr:結構內內嵌固定長度字元陣列SizeConst:陣列長度(字元數,非位元組)StructLayout.CharSet:決定結構內字串編碼,**必須與目標 API 一致**[StructLayout(LayoutKind.Sequential, Pack = 4, CharSet = CharSet.Ansi)]
struct MyStruct
{
public int A; // offset 0
public short B; // offset 4
// Pack=4 會在 B 後補 2 位元組對齊
public int C; // offset 8
}
// 若需精確控制位移
[StructLayout(LayoutKind.Explicit)]
struct UnionLike
{
[FieldOffset(0)] public int IntVal;
[FieldOffset(0)] public float FloatVal;
[FieldOffset(0)] public byte Byte0;
}
Pack:封裝大小(1/2/4/8/16/32/64/128),對應 C #pragma packSize:強制結構大小,罕用LayoutKind.Sequential 依宣告順序排列,符合 C 慣例// Win32: BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
// WNDENUMPROC: BOOL CALLBACK EnumProc(HWND hwnd, LPARAM lParam);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
delegate bool EnumWindowsProc(IntPtr hwnd, IntPtr lParam);
[DllImport("user32.dll", CharSet = CharSet.Auto)]
static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);
// 使用
static EnumWindowsProc _callback = EnumCallback; // **靜態欄位保持存活**
static bool EnumCallback(IntPtr hwnd, IntPtr lParam) { /* ... */ return true; }
EnumWindows(_callback, IntPtr.Zero);
關鍵規則:委派實例**必須在 Managed 端保持參考**(靜態欄位、類別欄位、GCHandle.Alloc),否則 GC 回收後 Unmanaged 呼叫會導致 ExecutionEngineException 或存取違規。
DllImport 屬性| 屬性 | 用途 |
|---|---|
CallingConvention | StdCall (預設 WinAPI)、Cdecl (C Runtime)、ThisCall、FastCall |
EntryPoint | 指定 DLL 實際匯出名稱,支援序數 #123;可讓 C# 方法名不同 |
ExactSpelling | true 關閉 A/W 自動搜尋 |
PreserveSig | COM Interop 用,false 將 HRESULT 轉例外 |
SetLastError | true 讓 CLR 捕獲 GetLastError |
BestFitMapping | ANSI 轉換時是否允許最佳配對字元 |
ThrowOnUnmappableChar | 無法轉換字元是否拋例外 |
// 1. 先用 LoadLibrary 載入指定路徑 DLL
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern IntPtr LoadLibrary(string lpFileName);
IntPtr hMod = LoadLibrary(@"C:\MyLibs\mydll.dll");
// 2. 再用 DllImport 正常宣告,CLR 會重用已載入模組
[DllImport("mydll.dll")] static extern void MyFunc();
原理:LoadLibrary 以檔案路徑載入,後續 DllImport 同名 DLL 會複用模組控制代碼。
long vs int、bool (1 byte) vs BOOL (4 byte) → 用 [MarshalAs(UnmanagedType.Bool)] 或改 intCharSet.Auto 導致 Unicode 字串傳給 ANSI APIMarshal.SizeOf<T>() 與 C sizeof 不同 → 檢查 Pack、CharSet、欄位順序StringBuilder Capacity 不足 → 先呼叫取得所需大小,再配置GCHandle 保持存活CallingConventionIntPtr 大小隨進程位元數變化,勿強制轉 int除錯工具:Marshal.GetLastWin32Error()、Win32Exception、Visual Studio Unmanaged Debugging、dumpbin /exports 確認匯出名稱。
internal static class NativeMethods,搭配 SuppressUnmanagedCodeSecurity(信任程式碼才需)。SafeHandle 衍生類)、編碼轉換。SafeHandle(SafeFileHandle、SafeRegistryHandle、自訂 SafeLibraryHandle)管理 Unmanaged 資源,避免終結子競爭。RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 守護,Linux/macOS 提供替代實作或拋 PlatformNotSupportedException。P/Invoke 是 .NET 與作業系統深度整合的關鍵技術。掌握封送規則、記憶體模型、生命週期管理,才能寫出穩定、高效、安全的 Interop 代碼。現代 .NET 提供 CsWin32、LibraryImport (Source Generator, .NET 7+) 等工具大幅降低手寫錯誤,建議新專案優先採用原始碼產生器,專注於業務邏輯而非繁瑣的簽名轉換。