C#之.Net互操作-平台调用(P_Invoke)
平台调用(Platform Invoke,简称 P/Invoke)是 .NET 公共语言运行时(CLR)提供的一种互操作功能,允许托管代码调用从非托管动态链接库(DLL,如 Win32 API 或自定义 C++ DLL)中导出的静态函数。
在托管代码调用非托管函数时,CLR 的封送拆收器(Interop Marshaler)扮演着桥梁角色:
- 拦截调用:当托管调用触发时,封送拆收器拦截请求。
- 内存转换(封送 IN):将托管堆/栈中的数据转换为非托管端能够理解的物理布局(如将托管
string转换为非托管char*)。 - 切换线程上下文:从托管执行环境切换到非托管执行环境,执行原生的机器码。
- 结果回写(封送 OUT):非托管函数返回后,将结果或修改后的数据传回托管端,并释放临时缓冲区。
平台调用数据类型映射表
要构造正确的托管原型,必须使用大小、对齐方式完全对等的托管类型替换 Win32/C 样式的数据类型:
| Wtypes.h 中 非托管类型 | 非托管 C 语言类型 | 托管类型 | 说明 |
|---|---|---|---|
| HANDLE | void* | System.IntPtr | 32 位系统上为 32 位,64 位系统上为 64 位 |
| BYTE | unsigned char | System.Byte | 8 位无符号整数 |
| SHORT | short | System.Int16 | 16 位有符号整数 |
| WORD | unsigned short | System.UInt16 | 16 位无符号整数 |
| INT / LONG | int / long | System.Int32 | 32 位有符号整数 |
| UINT / DWORD / ULONG | unsigned int / long | System.UInt32 | 32 位无符号整数 |
| BOOL | long | System.Int32 | 32 位布尔值标志。托管端需用[return: MarshalAs(UnmanagedType.Bool)]保证 4 字节映射 |
| CHAR | char | System.Char | 用 ANSI 修饰,映射到托管端时需注意字符集转换 |
| LPSTR / LPCSTR | char* / const char* | System.String/ StringBuilder | ANSI 字符串指针(LPCSTR为只读) |
| LPWSTR / LPCWSTR | wchar_t* / const _ | System.String/ StringBuilder | Unicode 字符串指针(LPCWSTR为只读) |
| FLOAT | float | System.Single | 32 位单精度浮点数 |
| DOUBLE | double | System.Double | 64 位双精度浮点数 |
关键内存与字符串封送
不可变性与 StringBuilder
托管System.String在 CLR 中是绝对不可变的。当非托管函数需要修改字符串并将其作为出参(In/Out 缓冲区)传回时,坚决不能使用string。否则,封送拆收器会修改其内部临时的只读缓冲区,轻则导致数据丢失,重则破坏托管堆(Heap Corruption),引发不可预知的崩溃。
- 黄金法则:凡是非托管端需要写入、修改的字符缓冲区,托管端必须使用
System.Text.StringBuilder显式预先实例化并分配足够的 Capacity。
内存所有权闭环原则
当托管代码与非托管代码在堆上动态交换内存(如非托管代码内部动态分配了一个结构体或字符串数组,并返回二级指针给 C#)时,双方必须在内存分配器(Allocator)上达成绝对一致。
- 默认标准:CLR 的 P/Invoke 默认使用 Win32 堆的 COM 内存分配器(即组件对象模型的
CoTaskMemAlloc)。 - 行为闭环:如果 C++ 导出函数需要向托管端返回动态生成的字符串或修改过的指针,C++ 内部必须使用
CoTaskMemAlloc申请内存,而不是原生new或malloc。只有这样,托管端的封送拆收器在完成拆收后,才能正确调用Marshal.FreeCoTaskMem(或由开发人员手动调用)来闭环释放内存,杜绝内存泄漏。
基础场景:枚举、常量与指针
枚举与常量封装
以 Win32 的MessageBeep为例,非托管原型为:
BOOLMessageBeep(UINT uType);虽然uType被定义为UINT,但 MSDN 文档指出其支持-1(标准提示音)。在 C# 中为了保证代码自解释性与健壮性,应将其封装为枚举:
publicenumBeepType:int{SimpleBeep=-1,IconAsterisk=0x00000040,IconExclamation=0x00000030,IconHand=0x00000010,IconQuestion=0x00000020,Ok=0x00000000,}publicstaticclassWin32Native{[DllImport("user32.dll",SetLastError=true)][return:MarshalAs(UnmanagedType.Bool)]// 确保将 4 字节 Win32 BOOL 正确转换为托管 boolpublicstaticexternboolMessageBeep(BeepTypebeepType);}非 opaque 指针(句柄)的通用映射
Win32 API 中存在大量不透明(Opaque)指针,最典型的代表就是句柄(如HANDLE,HWND)。托管代码不需要知道指针指向的内部结构,只需要存储并原样传回给操作系统。
- 直接使用
System.IntPtr进行通用映射。不要附加ref或out修饰符,因为IntPtr自身的大小在运行时会自动适配操作系统位数(32位系统占4字节,64位系统占8字节)。
复杂数据结构封送
普通结构体与内存对齐
如果非托管函数接受结构体指针,在托管端可以通过定义相同的struct配合ref关键字实现双向封送。
非托管 C++ 结构体:
typedefstruct_SYSTEM_POWER_STATUS{BYTE ACLineStatus;BYTE BatteryFlag;BYTE BatteryLifePercent;BYTE Reserved1;DWORD BatteryLifeTime;DWORD BatteryFullLifeTime;}SYSTEM_POWER_STATUS;托管 C# 对应实现:
[StructLayout(LayoutKind.Sequential)]// 必须保证字段在内存中严格按顺序排列publicstructSystemPowerStatus{public