承接上一篇基礎指南,本文聚焦於 **實戰場景** 與 **工程化模式**:如何設計可維護、高效能、跨平台相容的 P/Invoke 封裝層。內容涵蓋 DllImport 進階屬性決策樹、結構體版本控制、緩衝區管理、SafeHandle 資源包裝、原始碼產生器整合、以及偵錯實戰技巧。
DllImport 屬性決策指南| 屬性 | 何時設定 | 推薦值 |
|---|---|---|
CharSet | API 涉及字串/結構含字串 | CharSet.Auto (Windows 專用)CharSet.Unicode (僅支援 W 版 API,如 ReadDirectoryChangesW) |
SetLastError | API 文檔說明「呼叫 GetLastError 取得錯誤」 | true (預設 false) |
CallingConvention | 非 WinAPI (C Runtime、自定義 DLL) | CallingConvention.Cdecl (CRT)CallingConvention.StdCall (WinAPI 預設) |
ExactSpelling | 明確指定 EntryPoint 或不想要 A/W 搜尋 | true |
EntryPoint | C# 方法名需不同於 DLL 匯出名,或按序數綁定 | "RealFunctionName" 或 "#123" |
BestFitMapping | ANSI 轉換允許最佳配對 | false (安全優先) |
ThrowOnUnmappableChar | 無法轉換字元時拋例外 | true (偵錯期) |
許多 Win32 結構體有版本欄位 (如 cbSize),必須在呼叫前正確填寫:
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
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;
public WNDCLASSEX() => cbSize = (uint)Marshal.SizeOf<WNDCLASSEX>();
}
模式:建構函式自動填寫 cbSize,避免呼叫端遺漏。
許多 API 採「先查大小、再配置、再取資料」模式:
// 範例:GetAdaptersAddresses
[DllImport("iphlpapi.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern uint GetAdaptersAddresses(
uint Family, uint Flags, IntPtr Reserved,
IntPtr AdapterAddresses, ref uint SizePointer);
// Wrapper
public static IEnumerable<IPAdapterInfo> GetAdapters()
{
uint size = 0;
uint ret = GetAdaptersAddresses(AF_UNSPEC, 0, IntPtr.Zero, IntPtr.Zero, ref size);
if (ret != ERROR_BUFFER_OVERFLOW) throw new Win32Exception((int)ret);
IntPtr buffer = Marshal.AllocHGlobal((int)size);
try
{
ret = GetAdaptersAddresses(AF_UNSPEC, 0, IntPtr.Zero, buffer, ref size);
if (ret != NO_ERROR) throw new Win32Exception((int)ret);
// 走訪鏈表解析...
}
finally { Marshal.FreeHGlobal(buffer); }
}
關鍵點:使用 Marshal.AllocHGlobal/FreeHGlobal 或 SafeHGlobalHandle 管理非受控記憶體,避免大物件堆 (LOH) 碎片。
SafeHandle 衍生類:資源包裝黃金標準
// 封裝 FindFirstFile/FindNextFile/FindClose
public sealed class SafeFindHandle : SafeHandleZeroOrMinusOneIsInvalid
{
private SafeFindHandle() : base(true) { }
public static SafeFindHandle Invalid => new SafeFindHandle { handle = IntPtr.Zero };
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool FindClose(IntPtr hFindFile);
protected override bool ReleaseHandle() => FindClose(handle);
}
// 使用
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
static extern SafeFindHandle FindFirstFile(string lpFileName, out WIN32_FIND_DATA lpFindFileData);
public static IEnumerable<string> EnumFiles(string pattern)
{
using var hFind = FindFirstFile(pattern, out var data);
if (hFind.IsInvalid) yield break;
do { yield return data.cFileName; }
while (FindNextFile(hFind, out data));
// FindClose 自動在 Dispose 呼叫
}
優點:Critical Finalizer 保證釋放、可靠性契約 (CER) 防止非同步例外洩漏、支援 using 模式。
| C/C++ | C# 宣告 | MarshalAs |
|---|---|---|
int* arr (輸入) | int[] arr 或 ReadOnlySpan<int> | 預設 LPArray |
int* arr (輸出) | int[] arr (預先配置) | [Out] [MarshalAs(UnmanagedType.LPArray, SizeParamIndex=n)] |
char** argv | string[] args | [MarshalAs(UnmanagedType.LPArray, ArraySubType=UnmanagedType.LPTStr)] |
struct { int count; ITEM* items; } | 手動 Marshal.PtrToStructure 走訪 | 結構內指標無法自動封送陣列 |
unsafe + fixed 直接操作記憶體,或 Span<byte> 避免複製。WriteFileGather/ReadFileScatter、GetQueuedCompletionStatusEx 等批次版本。CharSet.Ansi:強制 Unicode 省去雙向轉換。LibraryImport (Source Generator, .NET 7+):編譯期生成 thunk,啟動更快、支援 StringMarshalling 微調、可對 readonly struct 直接傳址。LibraryImport 現代化語法 (.NET 7+)
using System.Runtime.InteropServices;
public static partial class NativeApi
{
[LibraryImport("kernel32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
public static partial int GetModuleFileName(IntPtr hModule, Span<char> lpFilename, int nSize);
[LibraryImport("user32.dll", SetLastError = true)]
[UnmanagedCallConv(CallConvs = new[] { typeof(CallConvStdcall) })]
public static partial bool SetWindowPos(IntPtr hWnd, IntPtr hWndInsertAfter, int X, int Y, int cx, int cy, uint uFlags);
}
partial 方法由 Source Generator 實作StringMarshalling:Utf16 (Unicode)、Ansi、CustomSpan<T>、ref readonly struct、out 直接對應指標EntryPointNotFoundException
public static partial class NativeApi
{
[LibraryImport("libc", EntryPoint = "getpid")]
public static partial int GetPid(); // Linux/macOS
[LibraryImport("kernel32.dll", EntryPoint = "GetCurrentProcessId")]
public static partial int GetCurrentProcessId(); // Windows
}
public static int GetProcessId() =>
RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? NativeApi.GetCurrentProcessId()
: NativeApi.GetPid();
搭配 #if NET7_0_OR_GREATER 或多目標專案 (TargetFrameworks) 提供平台專屬實作。
internal static partial class NativeMethodsLibraryImport (NET 7+) 或 DllImport + 完整屬性SafeHandle 衍生類cbSize 者在建構函式自動填寫TryGetBuffer 擴充方法CsWin32 自動從 Windows SDK 產生簽名,避免手寫錯誤P/Invoke 不只是「宣告一個 extern 方法」,而是跨越 Managed/Unmanaged 邊界的系統工程。掌握封送規則、記憶體所有權、錯誤處理慣例、版本相容性,並善用現代工具 (LibraryImport、CsWin32、SafeHandle),才能建立穩定、高效、可維護的互操作層。下一步建議:挑選一個專案中現有的手寫 P/Invoke,嘗試遷移至 LibraryImport + SafeHandle,體驗編譯期檢查帶來的安心感。