C#之.Net互操作-平台调用(P_Invoke)

📅 2026/6/25 14:25:58
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 语言类型托管类型说明HANDLEvoid*System.IntPtr32 位系统上为 32 位,64 位系统上为 64 位BYTEunsigned charSystem.Byte8 位无符号整数SHORTshortSystem.Int1616 位有符号整数WORDunsigned shortSystem.UInt1616 位无符号整数INT / LONGint / longSystem.Int3232 位有符号整数UINT / DWORD / ULONGunsigned int / longSystem.UInt3232 位无符号整数BOOLlongSystem.Int3232 位布尔值标志。托管端需用[return: MarshalAs(UnmanagedType.Bool)]保证 4 字节映射CHARcharSystem.Char用 ANSI 修饰,映射到托管端时需注意字符集转换LPSTR / LPCSTRchar* / const char*System.String/StringBuilderANSI 字符串指针(LPCSTR为只读)LPWSTR / LPCWSTRwchar_t* / const _System.String/StringBuilderUnicode 字符串指针(LPCWSTR为只读)FLOATfloatSystem.Single32 位单精度浮点数DOUBLEdoubleSystem.Double64 位双精度浮点数关键内存与字符串封送不可变性与 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