首頁
編程語言
C語言|JAVA編程
Python編程
網頁編程
ASP編程|PHP編程
JSP編程
數據庫
MYSQL數據庫|SqlServer數據庫
Oracle數據庫|DB2數據庫
您现在的位置: 程式碼之家 >> 編程語言 >  >> C語言 >> 關於C語言

C# P/Invoke 完整指南:呼叫 Win32 API、封送處理、進階技巧與避坑

     編輯:關於C語言

前言

.NET Framework / .NET Core / .NET 5+ 提供了豐富的基礎類別庫,但作業系統底層功能(視窗管理、檔案系統細節、硬體控制、安全性 API 等)仍大量只存在於 Win32 API(kernel32.dll、user32.dll、advapi32.dll…)。P/Invoke (Platform Invocation Services) 是橋樑,讓 Managed 代碼呼叫 Unmanaged DLL 匯出函式。本文系統整理:基本語法、資料封送規則、字串/結構/陣列/回呼處理、DllImport 進階屬性、除錯技巧與最佳實務。

一、最小可行範例:MessageBeep

using 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:告訴編譯器方法體在外部 DLL
  • DllImport:指定 DLL 名稱(不含路徑,依賴系統搜尋順序)
  • SetLastError = true:呼叫後可用 Marshal.GetLastWin32Error() 取得錯誤碼
  • CharSet = CharSet.Auto:自動選擇 ANSI/Unicode 版本(MessageBeepA/W)

二、資料類型對照表(核心封送規則)

Win32 / C大小/簽名CLR 對應備註
BYTE / UCHAR8-bit unsignedbyte
SHORT / INT1616-bit signedshort
USHORT / WORD / ATOM / WCHAR16-bit unsignedushort
INT / LONG / INT32 / BOOL32-bit signedintBOOL 建議用 bool(CLR 預設 4 字節)
UINT / DWORD / ULONG32-bit unsigneduint
LONGLONG / INT6464-bit signedlong
ULONGLONG / DWORDLONG64-bit unsignedulong
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,**必須保持委派存活**

四、字串封送深度解析

1. 字串類型選擇

[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 自動搜尋,直接用指定名稱

2. 結構內內嵌字串陣列

[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 pack
  • Size:強制結構大小,罕用
  • 預設 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 屬性

屬性用途
CallingConventionStdCall (預設 WinAPI)、Cdecl (C Runtime)、ThisCall、FastCall
EntryPoint指定 DLL 實際匯出名稱,支援序數 #123;可讓 C# 方法名不同
ExactSpellingtrue 關閉 A/W 自動搜尋
PreserveSigCOM Interop 用,false 將 HRESULT 轉例外
SetLastErrortrue 讓 CLR 捕獲 GetLastError
BestFitMappingANSI 轉換時是否允許最佳配對字元
ThrowOnUnmappableChar無法轉換字元是否拋例外

八、從任意路徑載入 DLL 技巧


// 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 會複用模組控制代碼。

九、常見錯誤與除錯清單

  1. 簽名不符:long vs int、bool (1 byte) vs BOOL (4 byte) → 用 [MarshalAs(UnmanagedType.Bool)] 或改 int
  2. 字串編碼錯誤:漏設 CharSet.Auto 導致 Unicode 字串傳給 ANSI API
  3. 結構大小錯誤:Marshal.SizeOf<T>() 與 C sizeof 不同 → 檢查 Pack、CharSet、欄位順序
  4. 緩衝區太小:StringBuilder Capacity 不足 → 先呼叫取得所需大小,再配置
  5. 委派被 GC:回呼函式指標失效 → 靜態欄位或 GCHandle 保持存活
  6. 呼叫慣例錯誤:堆疊不平衡 → 檢查 CallingConvention
  7. 32/64 位元不匹配:IntPtr 大小隨進程位元數變化,勿強制轉 int

除錯工具:Marshal.GetLastWin32Error()、Win32Exception、Visual Studio Unmanaged Debugging、dumpbin /exports 確認匯出名稱。

十、最佳實務總結

  1. 將 P/Invoke 宣告集中在 internal static class NativeMethods,搭配 SuppressUnmanagedCodeSecurity(信任程式碼才需)。
  2. 對外公開 **Managed Wrapper**,處理錯誤碼轉例外、資源釋放(SafeHandle 衍生類)、編碼轉換。
  3. 優先使用 SafeHandle(SafeFileHandle、SafeRegistryHandle、自訂 SafeLibraryHandle)管理 Unmanaged 資源,避免終結子競爭。
  4. 大量 Interop 專案考慮 pinvoke.net 社群定義或 CsWin32 原始碼產生器自動生成簽名。
  5. 跨平台專案:RuntimeInformation.IsOSPlatform(OSPlatform.Windows) 守護,Linux/macOS 提供替代實作或拋 PlatformNotSupportedException。

結語

P/Invoke 是 .NET 與作業系統深度整合的關鍵技術。掌握封送規則、記憶體模型、生命週期管理,才能寫出穩定、高效、安全的 Interop 代碼。現代 .NET 提供 CsWin32、LibraryImport (Source Generator, .NET 7+) 等工具大幅降低手寫錯誤,建議新專案優先採用原始碼產生器,專注於業務邏輯而非繁瑣的簽名轉換。

  1. 上一篇文章:
  2. 下一篇文章:
Copyright © 程式碼之家 All Rights Reserved