正确声明win32 api需三要素:模块名带.dll后缀、callingconvention.winapi、charset明确指定;结构体加structlayout(sequential);回调delegate须持久化并标注unmanagedfunctionpointer。

怎么在C#里正确声明一个Win32 API函数
直接照着MSDN或头文件写 DllImport 很容易崩——不是找不到函数,就是调用后程序静默退出。关键在三件事:模块名、调用约定、字符集。
比如想调用 MessageBoxA:
[DllImport("user32.dll", CallingConvention = CallingConvention.Winapi, CharSet = CharSet.Ansi)]
public static extern int MessageBoxA(IntPtr hWnd, string lpText, string lpCaption, uint uType);
-
CallingConvention.Winapi对应 Windows 默认的__stdcall,不写或错写成Cdecl会导致栈失衡 -
CharSet.Ansi明确告诉 P/Invoke 传 ANSI 字符串;若用CharSet.Auto,在 x64 下可能自动选 Unicode 版本(MessageBoxW),但函数名没改就报EntryPointNotFoundException - 模块名必须带
.dll后缀,写成"user32"在 .NET 5+ 会失败(默认禁用隐式扩展)
结构体传参时为什么总收到乱码或访问冲突
因为 C# 默认按托管内存布局排布字段,而 Win32 API 要求严格的 C 风格内存对齐和顺序。不加修饰的 struct 几乎必然出错。
以 POINT 为例:
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
- 必须加
[StructLayout(LayoutKind.Sequential)],否则 JIT 可能重排字段 - 如果原 C 结构含位域、
#pragma pack(1)或指针字段,还得补Pack = 1或用IntPtr替代指针 - 传结构体给 API 时,别用
ref除非文档明确要求“输入输出”;多数只读场景传值即可,加ref反而引发 GC 移动导致地址失效
回调函数(delegate)注册后一调用就崩溃
最常见的原因是 delegate 被 GC 回收了,但 native 代码还拿着旧函数指针在调用——典型现象是 AccessViolationException 或 ExecutionEngineException。
- 必须把 delegate 实例存为类的字段(不能是局部变量),防止被回收
- 推荐用
Marshal.GetFunctionPointerForDelegate+Marshal.GetDelegateForFunctionPointer配合生命周期管理,而不是直接传 delegate 给 API - 如果回调要跨线程(如 Windows 窗口消息循环),确保 delegate 标记
[UnmanagedFunctionPointer(CallingConvention.StdCall)],且不要在回调里直接操作 UI 控件(需封送回 UI 线程)
为什么.NET Core/.NET 5+ 上某些 P/Invoke 调用失败了
不是代码错了,是运行时变了。.NET Core 移除了对部分老旧 DLL 的自动查找路径(如 kernel32.dll 的别名映射),也收紧了字符串封送规则。
- 显式指定完整路径更可靠,比如用
"C:\Windows\System32\user32.dll"(仅调试用,生产仍建议相对名) -
string参数默认封送为LPStr,但如果 API 实际需要LPCWSTR,必须写[MarshalAs(UnmanagedType.LPWStr)],不能依赖CharSet - 在 ARM64 Windows 上,有些 API(如
Wow64EnableWow64FsRedirection)根本不存在,调用直接返回0且不报错,得先用GetNativeSystemInfo检查架构
最麻烦的从来不是“怎么写”,而是“怎么证明它在所有目标系统上都稳”。多打日志、用 Marshal.GetLastWin32Error() 捕获错误码、在 Release 模式下测试——这些比语法细节更容易被跳过。










