當托管代碼 (C#) 呼叫非托管代碼 (C++ DLL、Win32 API、COM) 時,**資料型別、記憶體佈局、呼叫慣例、例外處理** 皆不相容。CLR **封送處理器** 負責在邊界自動轉換,核心決策點在於:**複製 vs 釘住**。
| C# 簽名 | 套用屬性 | 封送方向 | 非托管對應 |
|---|---|---|---|
void M(int x) | 預設 | [In] | int x |
void M(out int x) | [Out] | [Out] | int *x |
void M(ref int x) | [In, Out] | [In, Out] | int *x |
void M([In] ref int x) | [In] 覆蓋預設 | [In] | int *x (不可寫回) |
[In, Out],CLR 會**釘住內部緩衝區**直接傳遞指標,效能最佳。**嚴禁使用 ref/out**,否則變雙重指標失去釘住優勢。[In];若需寫回請用 StringBuilder。[In];out/ref 產生雙重指標 **。| 條件 | 行為 | 範例 |
|---|---|---|
| 可直接複製型別 | 釘住 (無複製) | int, byte, struct (僅含基本型別), string (CharSet=Unicode) |
| 需轉換型別 | 複製 (配置非托管記憶體、轉換、呼叫、複製回、釋放) | string (CharSet=Ansi), bool[], 含非基本型別的 struct |
| ref/out 參數 | 永遠複製 (雙重指標) | ref int, out string |
| 反向 P/Invoke (非托管呼叫托管) | 永遠複製 | 委派轉函數指標回呼 |
最佳化原則:優先使用 CharSet=Unicode、StringBuilder、value type struct、避免 ref/out 參考型別,讓 CLR 走釘住路徑。
CoTaskMemFree 舊記憶體 → CoTaskMemAlloc 新記憶體 → 覆蓋指標 → 呼叫端最終釋放新記憶體。鐵律:非托管代碼**必須**使用 CoTaskMemAlloc/Free 或 SysAllocString/Free,嚴禁 malloc/free、new/delete、HeapAlloc/Free,否則跨版本 (XP/Vista/Win10) 會崩潰或洩漏。
public delegate void Callback(int value);
[DllImport("NativeLib.dll")]
public static extern void RegisterCallback(Callback cb);
public static void Main()
{
Callback managedCb = OnNativeEvent;
// 關鍵:防止 GC 回收委派
GCHandle handle = GCHandle.Alloc(managedCb);
try
{
RegisterCallback(managedCb); // 內部建立 thunk 持有委派參考
Console.ReadLine();
}
finally { handle.Free(); }
}
private static void OnNativeEvent(int v) => Console.WriteLine($"Native callback: {v}");
Marshal.GetFunctionPointerForDelegate 僅取得函數指標,**不延長委派壽命**。GCHandle.Alloc(delegate) 防止 GC 回收,否則觸發 CallbackOnCollectedDelegate MDA 錯誤。EnumWindows) 通常無需手動 GCHandle,CLR 內部 thunk 會暫時固定。微軟官方工具,支援:
下載:GitHub Repo 或隨 Visual Studio SDK 安裝。
CharSet (Unicode/Ansi) 與非托管端一致。Capacity。[StructLayout(LayoutKind.Sequential)],欄位序與非托管一致。[MarshalAs(UnmanagedType.LPArray, SizeParamIndex=n)] 指定長度。Marshal.FreeCoTaskMem / Marshal.FreeBSTR 釋放。DllImport(SetLastError=true) + Marshal.GetLastWin32Error() 捕獲 Win32 錯誤碼。封送處理看似複雜,核心只有一條:**讓 CLR 走「釘住」而非「複製」路徑**。掌握 In/Out、StringBuilder、CharSet=Unicode、StructLayout、CoTaskMemAlloc 五大關鍵,即可寫出高效能、零洩漏、跨平台穩定的互操作代碼。複雜案例優先用 **P/Invoke Interop Assistant** 生成骨架,再人工微調。