.NET 透過 P/Invoke (Platform Invocation Services) 調用非託管 Windows API。本文彙總 C# 簽名映射、資料類型對應、呼叫約定、字元集控制、結構體封送、錯誤處理等核心要點,並提供實用範本。
基礎宣告範本
using System;
using System.Runtime.InteropServices;
public static class WinApi {
// 1. 基礎宣告:DllImport 屬性指定 DLL、進入點、呼叫約定、字元集
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int MessageBox(IntPtr hWnd, string text, string caption, uint type);
// 2. 若方法名與 API 不同,用 EntryPoint 指定真實名稱
[DllImport("kernel32.dll", CharSet = CharSet.Auto, EntryPoint = "GetVersionEx")]
[return: MarshalAs(UnmanagedType.Bool)]
public static extern bool GetVersionEx(ref OSVERSIONINFOEX lpVersionInfo);
}
DllImport 關鍵屬性詳解
| 屬性 | 用途 | 常用值 |
| CallingConvention | 堆疊清理者、參數壓棧順序 | CallingConvention.StdCall (Win32 預設)、Cdecl (C 標準、可變參數) |
| CharSet | 字串封送編碼、API 名稱後綴 (A/W) | CharSet.Auto (NT=Unicode, 9x=ANSI)、Unicode、Ansi |
| EntryPoint | DLL 真實導出函數名或序數 | 字串或 #123 序數 |
| ExactSpelling | 禁止依 CharSet 修飾名稱 (加 A/W) | true 精確匹配 |
| SetLastError | 調用後保留 Win32 錯誤碼供 Marshal.GetLastWin32Error() 讀取 | true (推薦開啟) |
| PreserveSig | 保留原始簽名 (不轉 HRESULT + out retval) | true (COM 介面常用) |
常見類型映射表
| Win32 (C/C++) | C# 簽名 | 備註 |
| int, long, DWORD, UINT | int, uint | 4 字節有號/無號 |
| LONG_PTR, DWORD_PTR, HANDLE, HWND, HMODULE | IntPtr | 指標大小:32 位=4B, 64 位=8B,**必須用 IntPtr** |
| LPSTR, LPCSTR, char* | string (CharSet.Ansi) | 自動轉 ANSI |
| LPWSTR, LPCWSTR, wchar_t* | string (CharSet.Unicode) | 自動轉 UTF-16 |
| LPTSTR (TCHAR*) | string (CharSet.Auto) | 隨平台自動 ANSI/Unicode |
| BOOL | bool / [MarshalAs(UnmanagedType.Bool)] | Win32 BOOL=4B,C# bool=1B,建議加 MarshalAs |
| 構造體 (struct) | struct / class + [StructLayout(LayoutKind.Sequential)] | 欄位順序、對齊必須一致 |
結構體封送實例
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public struct OSVERSIONINFOEX {
public uint dwOSVersionInfoSize;
public uint dwMajorVersion;
public uint dwMinorVersion;
public uint dwBuildNumber;
public uint dwPlatformId;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 128)]
public string szCSDVersion; // 服務包版本字串
public ushort wServicePackMajor;
public ushort wServicePackMinor;
public ushort wSuiteMask;
public byte wProductType;
public byte wReserved;
}
// 呼叫時需先設定 Size,並用 ref 傳遞
OSVERSIONINFOEX osvi = new OSVERSIONINFOEX();
osvi.dwOSVersionInfoSize = (uint)Marshal.SizeOf(typeof(OSVERSIONINFOEX));
bool ok = GetVersionEx(ref osvi);
三大避坑指南
- 垃圾回收提前釋放 Handle:若非託管代碼長期持有物件引用,用
GCHandle.Alloc(obj, GCHandleType.Pinned) 或 HandleRef 包裝,防止 GC 移動/回收導致指標失效。
- 字串緩衝區大小不足:需字串輸出的 API (如
GetWindowText),先用 StringBuilder 配合 capacity 預分配:var sb = new StringBuilder(256); GetWindowText(hwnd, sb, sb.Capacity);。
- 64 位兼容:所有指標、Handle、LONG_PTR **統一用 IntPtr**,切勿用 int/long 硬編碼,否則 64 位下截斷崩潰。
資源推薦
- pinvoke.net — 最大 Windows API 簽名社群資料庫,可直接複製 C# 宣告。
- Visual Studio 安裝目錄
Samples\Technologies\Interop\PlatformInvoke\WinAPIs\CS — 官方範例代碼。