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

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

     編輯:關於C語言

核心命名空間與屬性

using System;
using System.Runtime.InteropServices; // DllImport, StructLayout, MarshalAs, In, Out, HandleRef, SafeHandle
using System.Text; // StringBuilder
using Microsoft.Win32.SafeHandles; // SafeHandleZeroOrMinusOneIsInvalid

基本宣告語法

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true, ExactSpelling = false)]
public static extern IntPtr FindWindow(string lpClassName, string lpWindowName);

// 關鍵屬性說明:
// CharSet.Auto          : 依平台自動選 ANSI/Unicode (WinNT=Unicode, Win9x=ANSI)
// SetLastError = true   : 允許 Marshal.GetLastWin32Error() 取得錯誤碼
// ExactSpelling = false : 允許運行時依 CharSet 附加 A/W 後綴尋找入口點
// CallingConvention     : 預設 StdCall (Win32 API),C 運行時庫用 Cdecl

型別對照表 (Win32 → C#)

Win32 / CC# 關鍵字 / 型別備註
VOIDvoid
BOOLbool / int (32-bit)C# bool 封送為 4-byte BOOL
BYTEbyte
CHARsbyte / byte
WORDushort16-bit unsigned
DWORDuint32-bit unsigned
LONGint32-bit signed
ULONGuint
INT/UINTint/uint
FLOAT/DOUBLEfloat/double
HANDLE/HWND/HMODULEIntPtr / SafeHandle**首選 SafeHandle** 避免 GC 競態
LPSTR/LPCSTRstring (In) / StringBuilder (Out)ANSI 字串
LPWSTR/LPCWSTRstring (In) / StringBuilder (Out)Unicode 字串 (CharSet.Unicode)
LPTSTRstring / StringBuilder配合 CharSet.Auto 自動切換
結構體指標ref Struct / out Struct結構體需 [StructLayout(LayoutKind.Sequential)]
函數指標delegate回調函數

結構體封送

// Win32: typedef struct _RECT { LONG left, top, right, bottom; } RECT;
[StructLayout(LayoutKind.Sequential)]
public struct RECT
{
    public int Left;
    public int Top;
    public int Right;
    public int Bottom;
}

// 含字串固定長度
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Auto)]
public 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;
}

// 使用
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern short RegisterClassEx([In] ref WNDCLASSEX lpwcx);

字串封送規則

方向Win32 型別C# 型別MarshalAs
In (唯讀)LPCSTR/LPCWSTR/LPCTSTRstring自動 (預設 LPStr/LPWStr/LPTStr)
Out (寫入緩衝區)LPSTR/LPWSTR/LPTSTRStringBuilder[MarshalAs(UnmanagedType.LPTStr)] + 容量
In/OutLPTSTRStringBuilder同上
// GetWindowText: int GetWindowText(HWND hWnd, LPTSTR lpString, int nMaxCount);
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern int GetWindowText(IntPtr hWnd, StringBuilder lpString, int nMaxCount);

// 呼叫
var sb = new StringBuilder(256);
GetWindowText(hWnd, sb, sb.Capacity);
string title = sb.ToString();

回調函數

// EnumWindows: BOOL EnumWindows(WNDENUMPROC lpEnumFunc, LPARAM lParam);
public delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);

[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern bool EnumWindows(EnumWindowsProc lpEnumFunc, IntPtr lParam);

// 使用
static bool Callback(IntPtr hWnd, IntPtr lParam)
{
    var sb = new StringBuilder(256);
    GetWindowText(hWnd, sb, sb.Capacity);
    Console.WriteLine($"0x{hWnd.ToInt64():X}: {sb}");
    return true; // 繼續枚舉
}

EnumWindows(Callback, IntPtr.Zero);

進階:SafeHandle 替代 IntPtr (資源安全)

public class SafeWindowHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private SafeWindowHandle() : base(true) { } // ownsHandle = true

    protected override bool ReleaseHandle()
    {
        // 視實際 Handle 類型呼叫對應釋放 API
        // 例如 DestroyWindow, CloseHandle, RegCloseKey
        return DestroyWindow(handle);
    }

    [DllImport("user32.dll", ExactSpelling = true)]
    private static extern bool DestroyWindow(IntPtr hWnd);
}

// 宣告使用 SafeHandle
[DllImport("user32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern SafeWindowHandle CreateWindowEx(
    uint dwExStyle, string lpClassName, string lpWindowName, uint dwStyle,
    int x, int y, int nWidth, int nHeight,
    IntPtr hWndParent, IntPtr hMenu, IntPtr hInstance, IntPtr lpParam);

陣列與緩衝區封送

// 1. 固定大小陣列 (結構體內)
[StructLayout(LayoutKind.Sequential)]
public struct FIXED_ARRAY
{
    [MarshalAs(UnmanagedType.ByValArray, SizeConst = 256)]
    public byte[] Data;
}

// 2. 參數陣列 (Out)
[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern uint GetLogicalDriveStrings(uint nBufferLength, [Out, MarshalAs(UnmanagedType.LPArray)] char[] lpBuffer);

// 呼叫
char[] buffer = new char[1024];
uint len = GetLogicalDriveStrings((uint)buffer.Length, buffer);
string drives = new string(buffer, 0, (int)len);

常見陷阱與解決

陷阱症狀解決
CharSet 不匹配中文亂碼、API 找不到入口點統一用 CharSet.Auto,字串用 string/StringBuilder
結構體記憶體對齊錯誤欄位值錯誤、AccessViolation[StructLayout(LayoutKind.Sequential, Pack = 4)] 指定封裝,對齊 Win32 預設 4/8 byte
GC 過早回收 Managed 物件傳遞 Handle 的 API 隨機失敗用 HandleRef 或 **SafeHandle** 包裝,保證存活至呼叫結束
委託被 GC 回收回調函數隨機失效將 delegate 存為 **static 欄位** 或類別成員,防止被收集
SetLastError 未設定Marshal.GetLastWin32Error() 回傳 0[DllImport(..., SetLastError = true)]
呼叫慣例錯誤堆疊不平衡、PInvokeStackImbalance MDAWin32 API 用 StdCall (預設),C Runtime 用 Cdecl

錯誤處理模式

[DllImport("kernel32.dll", CharSet = CharSet.Auto, SetLastError = true)]
public static extern IntPtr CreateFile(
    string lpFileName, uint dwDesiredAccess, uint dwShareMode,
    IntPtr lpSecurityAttributes, uint dwCreationDisposition,
    uint dwFlagsAndAttributes, IntPtr hTemplateFile);

public static SafeFileHandle SafeCreateFile(string path, FileAccess access)
{
    var handle = CreateFile(path, (uint)access, 0, IntPtr.Zero, 3, 0, IntPtr.Zero);
    if (handle == IntPtr.Zero || handle == new IntPtr(-1)) // INVALID_HANDLE_VALUE
    {
        int err = Marshal.GetLastWin32Error();
        throw new System.ComponentModel.Win32Exception(err, $"CreateFile failed for '{path}'");
    }
    return new SafeFileHandle(handle, true);
}

工具推薦

  • PInvoke.net (pinvoke.net) —— 社群維護的 API 簽名庫,可直接複製。
  • Windows SDK / Visual Studio —— dumpbin /exports user32.dll 查看導出函數。
  • CLR Inside Out / .NET Memory Profiler —— 診斷封送洩漏、Handle 洩漏。

現代替代:函數指標與 NativeLibrary (Core 3.0+)

// 動態載入 + 函數指標 (效能優於 DllImport)
var lib = NativeLibrary.Load("user32.dll");
var pFindWindow = NativeLibrary.GetExport(lib, "FindWindow");
var findWindow = Marshal.GetDelegateForFunctionPointer(pFindWindow);

// 委託簽名需匹配
[UnmanagedFunctionPointer(CallingConvention.StdCall, CharSet = CharSet.Auto)]
public delegate IntPtr FindWindowDelegate(string lpClassName, string lpWindowName);

總結

P/Invoke 是 C# 與作業系統、C/C++ 庫互操作的橋樑。**核心原則**:精準映射型別、明確擁有權 (SafeHandle)、設定 SetLastError、防止 GC 競態、單元測試邊界條件。優先查閱 PInvoke.net 避免重造錯誤簽名;複雜場景考慮 ComInterface、C++/CLI 混合模組或純 C 庫編譯為 .NET Standard 組件。

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