news 2026/4/28 17:20:57

C# P/Invoke 基础

作者头像

张小明

前端开发工程师

1.2k 24
文章封面图
C# P/Invoke 基础

P/Invoke 基础

P/Invoke 到底是何方神圣?

官方解释 P/Invoke 是可用于从托管代码访问非托管库中的结构、回调和函数的一种技术。 大多数 P/Invoke API 包含在以下两个命名空间中:System 和 System.Runtime.InteropServices。 使用这两个命名空间可提供用于描述如何与本机组件通信的工具。

P/Invoke 的全称是 Platform Invocation Services(平台调用服务)。名字很唬人,但你完全可以把它理解成 C# 和 C++ 之间的同声传译(C# 等 .NET 语言调用底层 C/C++ 原生函数的方法)。

想象一下这个场景
C#住在一栋“托管豪宅”里:垃圾有清洁工自动扫(GC),类型安全,住得很舒服。
C++则在“野外地摊”干活:速度快、能直接操作硬件,但什么都得自己动手(手动管理内存)。

现在,豪宅里的 C# 想用地摊上的一个现成工具(比如一个高效的图像缩放算法)。怎么办?

  • 自己从零写一个?太累,而且大概率没人家 C++ 写得好。
  • 直接冲出去拿?语言不通,内存布局对不上,一跑就崩。

这时候,P/Invoke 就是 C# 专门请来的翻译官:

  • 帮 C# 找到那个 C++ 函数住在哪栋“大楼”(DLL 文件)。
  • 把 C# 的参数翻译成 C++ 能听懂的样子(比如把 string 变成 char*)。
  • 等 C++ 干完活,再把结果翻译回 C# 能用的格式。
  • 顺便把临时申请的“地摊垃圾”清理掉。

跑通一个 P/Invoke 示例

usingSystem;usingSystem.Runtime.InteropServices;namespaceMyPInvoke_demo01{internalclassProgram{//[DllImport("user32.dll")] 告诉 CLR:这个函数住在 user32.dll 这座大楼里//CharSet = CharSet.Unicode 字符串使用 Unicode 编码,防止乱码//public static extern 方法的实现在外部(C++ 那边),这里只是声明[DllImport("user32.dll",CharSet=CharSet.Auto)]publicstaticexternintMessageBox(IntPtrhWnd,Stringtext,Stringcaption,uinttype);staticvoidMain(string[]args){// 调用MessageBox函数显示一个消息框//IntPtr.Zero 表示空指针,在这里代表没有父窗口MessageBox(IntPtr.Zero,"Hello, World!","MyPInvoke_demo01",0);}}}

运行,你会看到一个标准的 Windows 消息框。你已经完成了一次跨语言的调用!

核心概念:翻译官的工作规则

1. 规则一:数据类型要对齐 —— Blittable 与非 Blittable

Blittable 类型(零成本翻译)
int, float, double, byte, long, IntPtr,以及只包含这些类型的结构体。直接扔过去,C++ 那边看都不看就接住了,速度极快。

非 Blittable 类型(需要转换)
string(编码可能不同)、bool(C# 1字节,Windows API 的 BOOL 是4字节)、char、object 等。每次调用都涉及内存分配和格式转换,有性能损耗。

开发建议:

  • 图像数据尽量用 byte[],它是 Blittable 的,传递海量像素时效率最高。
  • 结构体中尽量少用 string,改用固定长度的 char[] 或 byte[]。

2. 规则二:谁打电话,谁负责挂断 —— 调用约定

C++ 函数执行完后,谁负责清理“战场”(堆栈)?这叫做 调用约定 (Calling Convention)。
StdCall:被调用者(C++)自己清理。Windows API 默认用这个。
Cdecl:调用者(C#)负责清理。C/C++ 默认用这个,支持可变参数(比如 printf)。

如果你声明错了,程序不会报错,而是在某个随机时刻崩溃(尤其在 Release 下)。解决方案:永远在 DllImport 里显式写清楚。

[DllImport("mylib.dll",CallingConvention=CallingConvention.Cdecl)]

3. 规则三:谁的垃圾谁倒 —— 内存管理

P/Invoke 不会自动释放 C++ 那边分配的内存。

谁分配的内存谁来释放
C# 的 new byte[1024]GC 自动回收(或你手动 Marshal.FreeHGlobal)
C++ 里的 malloc()必须调用 free()
C++ 里的 new必须调用 delete
Windows API 的 GlobalAlloc必须调用 GlobalFree

安全做法:对于 C++ 返回的指针,尽量用 SafeHandle 封装起来,这样即使发生异常,也能自动释放

// 示例:封装一个文件句柄publicclassSafeFileHandle:SafeHandleZeroOrMinusOneIsInvalid{publicSafeFileHandle():base(true){}protectedoverrideboolReleaseHandle()=>CloseHandle(handle);[DllImport("kernel32.dll")]privatestaticexternboolCloseHandle(IntPtrhObject);}

结构体、回调和字符串

1. 结构体互传 —— 注意内存对齐

假设 C++ 里有这样一个结构体:

structPoint3D{floatx;floaty;floatz;};

C# 中要这样声明才能对得上:

[StructLayout(LayoutKind.Sequential,Pack=1)]structPoint3D{publicfloatX;publicfloatY;publicfloatZ;}

LayoutKind.Sequential:按声明顺序排列字段。
Pack = 1:按 1 字节对齐,防止编译器自动插入填充字节。

2. 回调函数 —— 小心委托被 GC“吃掉”

可以把 C# 的方法作为函数指针传给 C++,让 C++ 在某些事件发生时回调它。比如遍历窗口:

// 声明委托类型delegateboolEnumWindowsProc(IntPtrhWnd,IntPtrlParam);[DllImport("user32.dll")]staticexternboolEnumWindows(EnumWindowsProclpEnumFunc,IntPtrlParam);// 实现回调方法staticboolMyCallback(IntPtrhWnd,IntPtrlParam){Console.WriteLine($"找到窗口句柄:{hWnd}");returntrue;// 继续枚举}// 调用EnumWindows(MyCallback,IntPtr.Zero);

致命坑:如果 MyCallback 委托没有被任何变量引用,GC 可能在你不知情的时候回收它,然后 C++ 那边回调时就会访问非法内存,程序直接崩溃。
解决方案:用一个静态字段持有委托实例。

privatestaticEnumWindowsProccallback=MyCallback;EnumWindows(callback,IntPtr.Zero);

3. 字符串的“读”与“写”

  • 只读字符串:直接用 string 参数,P/Invoke 会自动转成 const char*。
  • 需要 C++ 填充的字符串缓冲区:用 StringBuilder,并且预分配足够容量。
[DllImport("user32.dll",CharSet=CharSet.Unicode)]staticexternintGetWindowText(IntPtrhWnd,StringBuilderlpString,intnMaxCount);varsb=newStringBuilder(256);GetWindowText(someHwnd,sb,sb.Capacity);

.NET 7+ 的新福音:LibraryImport

LibraryImport采用源生成器技术,在编译时就生成封送代码,优点:

  • 零运行时反射,性能更好。
  • 支持 AOT(提前编译)场景。
  • 类型检查更严格,错误更早发现。

LibraryImport写法:

//注意:方法要声明为 partial static,不需要方法体。[LibraryImport("user32.dll",StringMarshalling=StringMarshalling.Utf16)]staticpartialintMessageBox(IntPtrhWnd,stringtext,stringcaption,uinttype);

总结

P/Invoke 是一扇门,门的一边是 .NET 的安逸世界,另一边是原生代码的广袤天地。掌握了它,你就能在 C# 里调用任何 C/C++ 库、任何 Windows API,甚至自己写一个 C++ DLL 给 C# 用。

但能力强也意味着责任重 —— 数据类型、调用约定、内存管理,哪一个细节没对齐,都可能换来一个 AccessViolationException(访问违规异常)。

集中管理:把所有 P/Invoke 声明放在一个名为 NativeMethods 的类里,方便维护。
显式优于隐式:永远写上 CallingConvention、CharSet、SetLastError,不要依赖默认值。
能用 Blittable 就用 Blittable:高频调用的函数,参数和返回值尽量用 int、float、byte[] 等,避免 string 和 bool。
谁分配谁释放:对于返回指针的函数,立刻看文档确认释放方式,并用 SafeHandle 或 try-finally 包裹。
善用工具:

  • pinvoke.net:搜常见 Windows API 的正确签名。
  • P/Invoke Interop Assistant:自动生成转换代码。
  • Visual Studio 的“启用本机代码调试”:可以同时调试 C# 和 C++。

记住三条
P/Invoke 是翻译官。
能传 int 就别传 string,能传 byte[] 就别传 object。
出了问题先检查:调用约定?内存对齐?谁分配谁释放?

本文参考
Chatgpt https://chatgpt.com/
C# 官方文档 https://learn.microsoft.com/zh-cn/dotnet/standard/native-interop/pinvoke
DeepSeek https://chat.deepseek.com/

版权声明: 本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若内容造成侵权/违法违规/事实不符,请联系邮箱:809451989@qq.com进行投诉反馈,一经查实,立即删除!
网站建设 2026/4/28 17:20:40

MoviePilot如何应对115网盘风控:3大策略与深度技术解析

MoviePilot如何应对115网盘风控:3大策略与深度技术解析 【免费下载链接】MoviePilot NAS媒体库自动化管理工具 项目地址: https://gitcode.com/gh_mirrors/mo/MoviePilot 当你的NAS媒体库自动化管理流程因为115网盘的"访问上限"错误而中断时&#…

作者头像 李华
网站建设 2026/4/28 17:15:31

I2C控制器及其应用

I2C(Inter--Integrated Circuit))芯片与芯片之间的通讯,集成电路总线,它由飞利浦(现为NXP)公司在20世纪80年代开发,是一种广泛用于嵌入式系统的 同步、串行、半双工通信协议,用于在同一块电路板上的集成电路之间进行通信。 I2C总线…

作者头像 李华
网站建设 2026/4/28 17:10:05

Boston Dynamics与其机器人产品全览

波士顿动力(Boston Dynamics)是全球最具代表性的机器人技术企业之一,成立于 1992 年,起源于美国麻省理工学院,由著名机器人学家 Marc Raibert 创立。公司总部位于马萨诸塞州,历经三十余年深耕,是…

作者头像 李华
网站建设 2026/4/28 17:09:12

网盘直链下载助手:八大网盘真实下载链接一键获取的终极解决方案

网盘直链下载助手:八大网盘真实下载链接一键获取的终极解决方案 【免费下载链接】Online-disk-direct-link-download-assistant 一个基于 JavaScript 的网盘文件下载地址获取工具。基于【网盘直链下载助手】修改 ,支持 百度网盘 / 阿里云盘 / 中国移动云…

作者头像 李华