一、先搞懂三个前置概念(2分钟)
1. 什么是"托管代码"?
-
你平时写的 C# 代码,运行在 .NET 运行时(CLR)之上。
-
内存分配、回收、类型检查都由 CLR 帮你打理,这叫托管环境。
-
优点:安全、开发快;缺点:被"保护"起来,不能直接操作内存或硬件。
2. 什么是"非托管代码"?
-
Windows 操作系统本身是用 C/C++ 写的,编译后直接变成CPU指令,没有中间层。
-
这些代码不依赖 .NET ,直接运行,叫做非托管代码。
-
它们被封装在
.dll文件里(如kernel32.dll、user32.dll),通过导出函数对外提供服务。
3. 问题来了------语言不通,怎么对话?
-
C# 函数参数是
string、int,内存布局由 CLR 决定。 -
Windows API 函数参数是
LPCTSTR、HANDLE,内存布局是 C 语言标准。 -
直接调用 = 鸡同鸭讲。
二、P/Invoke 就是"翻译官 + 外交护照"
P/Invoke(Platform Invocation Services) 是 .NET 提供的一套免费
Invocation: 求助,祈祷;咒语;发言,祷文;
计算机调用,启动;行使
翻译服务,它的工作流程如下:
你写的 C# 代码 ------[P/Invoke]------> Windows API(DLL)
具体职责:
-
找对门 :定位指定的 DLL 文件(如
kernel32.dll),并加载进内存。 -
对上暗号 :根据你提供的方法名(如
QueryFullProcessImageName),在 DLL 中找到对应的函数入口地址。 -
翻译参数:
-
把你的 C#
string转成 C 语言需要的LPCWSTR(宽字符指针)。 -
把你的
int转成 C 语言的DWORD。 -
把你的
ref int转成指针int*。
-
-
转换返回值 :把 C 语言的
BOOL(其实是整数)转成 C# 的bool。 -
传递异常 :如果 Windows API 设置了错误码,P/Invoke 帮你取回来(
Marshal.GetLastWin32Error())。
整个过程,你只需要写一行 [DllImport] 声明,其余杂活 .NET 全包了。
三、手把手:一个 P/Invoke 的完整生命周期
假设你想调用 Windows 最经典的 API ------ MessageBox(弹出消息框)。
第1步:找到目标 DLL 和函数
-
DLL 名称:
user32.dll -
函数名:
MessageBoxW(W 表示 Unicode 版本,Windows 推荐) -
参数:查 MSDN 得知需要窗口句柄、文本、标题、按钮类型。
第2步:在 C# 中声明"代理人"
csharp
cs
using System.Runtime.InteropServices;
public class Win32
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBox(
IntPtr hWnd, // 窗口句柄
string text, // 消息文本
string caption, // 标题
uint type); // 按钮类型
}
每一部分的作用:
-
[DllImport("user32.dll")]→ 告诉 CLR:"请去user32.dll这个文件里找函数"。 -
CharSet = CharSet.Unicode→ "参数中的字符串请翻译成 UTF-16 格式"。 -
public static extern→ "这个函数是外部实现的,我只是声明签名"。 -
参数类型映射:
string→ C 语言的LPCWSTR;IntPtr→ 指针/句柄;uint→UINT。
第3步:像普通 C# 方法一样调用
csharp
cs
Win32.MessageBox(IntPtr.Zero, "你好,这是 API 弹出的框!", "P/Invoke 示例", 0);
执行时发生的事情:
-
CLR 找到
user32.dll(在C:\Windows\System32下)。 -
定位
MessageBoxW函数的入口地址。 -
将
"你好..."字符串转为 C 风格的内存块,把指针压入堆栈。 -
CPU 跳转到
user32.dll的代码区执行。 -
Windows 画出消息框。
-
返回值(用户点击了哪个按钮)被转换成
int传回 C#。
你看,完全没有直接操作指针,没有手动加载 DLL,一切由 P/Invoke 代劳。
四、为什么 C# 程序员必须懂 P/Invoke?
回到你之前遇到的进程路径获取问题------为什么必须用 P/Invoke?
| 需求 | .NET 原生方法 | 结果 |
|---|---|---|
| 获取自己的路径 | Assembly.GetEntryAssembly().Location |
✅ 完美 |
| 获取其他 32位进程路径 | Process.MainModule.FileName |
⚠️ 有时崩溃(权限) |
| 获取其他 64位进程路径 | Process.MainModule.FileName |
❌ 必崩溃(位数不匹配) |
.NET 没提供安全、通用的"取别人路径"方法 ,怎么办? → Windows 自身提供了 QueryFullProcessImageName(XP 之后就有),稳定可靠。 → 但这函数没有 .NET 封装,必须自己用 P/Invoke 调用。
这就是 P/Invoke 的典型价值:填补 .NET 的功能空白。
五、P/Invoke 的"坑"与最佳实践(避开 90% 的 Bug)
⚠️ 坑1:字符串编码错误
-
错误 :
[DllImport("user32.dll")]不指定CharSet,默认是Ansi,但现代 Windows 函数都建议用Unicode。 -
症状:中文乱码、函数返回失败。
-
解药 :一律显式写 CharSet = CharSet.Unicode。
⚠️ 坑2:错误的参数类型
-
错误 :C 语言
BOOL是 4 字节整数,C#bool是 1 字节,直接映射会堆栈错乱。 -
解药 :C 的
BOOL→ C# 用bool没问题(P/Invoke 会自动扩展),但输出参数 要用ref bool或out bool。
⚠️ 坑3:忘记获取错误码
-
错误 :API 返回
false,不知道为什么。 -
解药 :在
[DllImport]中添加SetLastError = true,调用后立即用Marshal.GetLastWin32Error()取错误码
✅ 最佳实践模板
cs
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
public static extern bool QueryFullProcessImageName(
IntPtr hProcess,
int dwFlags,
StringBuilder lpExeName,
ref int lpdwSize);
// 调用后
if (!QueryFullProcessImageName(...))
{
int error = Marshal.GetLastWin32Error();
Console.WriteLine($"API 失败,错误码:{error}");
}
六、P/Invoke 不是万能药:何时不该用它?
| 场景 | 错误做法 | 正确做法 |
|---|---|---|
| 读写注册表 | 调用 RegOpenKeyEx |
Microsoft.Win32.Registry |
| 复制文件 | CopyFile API |
File.Copy |
| 创建窗口 | CreateWindowEx |
WinForms / WPF |
| 数学计算 | kernel32.dll 的 MulDiv |
C# 直接乘除 |
原则 :.NET 原生方法 > 第三方 NuGet 包 > 自己写 P/Invoke。 只有在 .NET 实在无能为力时(如跨进程、系统级操作),才亮出 P/Invoke 这把"手术刀"。
七、总结:一句话记住 P/Invoke
P/Invoke 是 .NET 给 C# 颁发的一张"外交签证",让你能在托管世界里,合法调用非托管帝国的 API 函数。
-
它不是编程语言特性,而是 .NET 运行时的互操作服务。
-
你只需要写
[DllImport]声明签名,其余复杂的内存搬运、DLL 加载都由 CLR 代劳。 -
它是你突破 .NET 边界、深入操作系统能力的必修课。
现在再回头看那句话,是不是清晰多了?
"C# 通过 P/Invoke(Platform Invocation Services) 机制调用这些 DLL 中的函数。"
翻译成人话:C# 用 .NET 自带的"翻译官",去调用 Windows 系统 DLL 里的原始函数。