在 .NET 里,只要你开始接触这些场景:
Win32 APIC/C++ 动态库你大概率都会走到一个关键能力:
P/Invoke
一句话先说透:
P/Invoke的本质,是让托管代码通过运行时提供的互操作机制,去调用非托管动态库中的函数。
它真正难的地方,其实不是“怎么声明一个外部方法”,而是:
GC 回收;
所以这篇文章重点不是只讲 DllImport 语法,而是讲清楚:
P/Invoke 到底是什么;DllImport 和 LibraryImport 怎么选;Marshalling)到底在做什么;P/Invoke 到底是什么?
P/Invoke 全称是:
Platform Invocation Services
可以先用一句最直白的话理解:
P/Invoke就是 C# 调用原生函数的桥梁。
例如下面这个经典写法:
using System;
using System.Runtime.InteropServices;
internal static class NativeMethods
{
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);
}
调用时:
NativeMethods.MessageBox(IntPtr.Zero, "Hello", "PInvoke", 0);
对于调用方来说,它看起来像一个普通静态方法。
但底层其实发生了这些事:
这个“转换和桥接”的过程,就是 P/Invoke 的核心。
P/Invoke?
因为 .NET 再强,也不可能把所有操作系统能力、历史原生库、底层硬件接口都重新包装一遍。
所以在很多场景里,P/Invoke 不是“炫技”,而是现实需求:
C/C++ 库;从高层往下看,可以把它理解成这样:
C# 方法声明
-> 运行时定位动态库和导出符号
-> 封送参数
-> 切换到非托管调用
-> 执行原生函数
-> 封送返回值/输出参数
-> 回到托管代码
也就是说,P/Invoke 从来不只是“调一下 DLL”这么简单。
它真正麻烦的地方在于:
GC 移动;DllImport 和 LibraryImport 怎么选?
这是现在写 P/Invoke 最值得单独讲的一点。
DllImport这是最传统、最经典的写法。
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern uint GetCurrentThreadId();
优点是:
LibraryImport这是较新的写法,基于源生成器生成互操作代码。
using System.Runtime.InteropServices;
internal static partial class NativeMethods
{
[LibraryImport("kernel32.dll", SetLastError = true)]
internal static partial uint GetCurrentThreadId();
}
它的特点是:
AOT / NativeAOT 更友好;可以先记这个原则:
DllImport.NET、互操作代码较多:优先 LibraryImport但无论你用哪一个,真正决定成败的都不是特性名字,而是:
P/Invoke 最核心的问题:封送到底是什么?
P/Invoke 里最关键的词,不是 DLL,而是:
Marshalling
也就是:
托管表示和非托管表示之间的转换。
因为很多类型在两边并不是天然一样的。
例如:
string 在托管世界和 char* / wchar_t* 并不等价;bool 在托管代码里和 Windows BOOL 也未必完全一样;
所以写 P/Invoke 时,最重要的习惯是:
先看原生签名,再决定托管签名,而不是反过来。
先记几个最常见的:
|
|
|
|---|---|
int32 |
int |
uint32 |
uint |
|
|
IntPtr
nint |
|
|
IntPtr
Span<T> 对应方案 |
|
|
[StructLayout] struct |
|
|
string
StringBuilder、IntPtr |
这里最容易犯的错误之一是“看名字猜类型”。
例如在 Windows 世界里:
LONG 通常是 32 位,不是 C# 的 longint,而更接近 IntPtr所以真正可靠的依据始终是目标平台的原生文档和头文件定义。
因为原生世界里的字符串并不统一。
最常见的就有:
char*wchar_t*LPSTRLPWSTR而在 C# 里,大家最容易直接写成:
string
问题是:
A/W 版本差异;string 是不可变的。如果原生函数只是读取字符串,最常见写法是:
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);
这里的重点是:
CharSet如果原生函数要往缓冲区里写内容,更常见的是:
using System.Text;
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
internal static extern int GetWindowText(
IntPtr hWnd,
StringBuilder text,
int count);
调用时:
var buffer = new StringBuilder(256);
GetWindowText(hWnd, buffer, buffer.Capacity);
string title = buffer.ToString();
这类场景下用 StringBuilder,通常比直接用 string 更符合原生 API 语义。
因为结构体一旦布局不一致,结果往往不是“报错”,而是:
所以结构体必须显式描述布局。
最常见的写法是:
[StructLayout(LayoutKind.Sequential)]
internal struct POINT
{
public int X;
public int Y;
}
如果原生函数使用这个结构体:
[DllImport("user32.dll")]
internal static extern bool GetCursorPos(out POINT point);
那么调用时就能正确按顺序映射字段。
Pack?当原生结构体明确指定了对齐方式,或者你发现字段总是错位时,就必须关注:
[StructLayout(LayoutKind.Sequential, Pack = 1)]
也就是说,结构体能不能正常工作,不只取决于字段顺序,还取决于:
如果原生代码里用的是 union,一般要改用显式布局:
[StructLayout(LayoutKind.Explicit)]
internal struct INPUT_UNION
{
[FieldOffset(0)]
public MOUSEINPUT MouseInput;
[FieldOffset(0)]
public KEYBDINPUT KeyboardInput;
}
这里的关键不是语法,而是:
FieldOffset 显式指定偏移。SafeHandle?很多原生 API 会返回句柄,例如:
很多初学者会直接用:
IntPtr
当然能用,但它有个明显问题:
这就是为什么实战里更推荐:
SafeHandle
例如:
using Microsoft.Win32.SafeHandles;
[DllImport("kernel32.dll", SetLastError = true, CharSet = CharSet.Unicode)]
internal static extern SafeFileHandle CreateFile(
string fileName,
uint desiredAccess,
uint shareMode,
IntPtr securityAttributes,
uint creationDisposition,
uint flagsAndAttributes,
IntPtr templateFile);
它的核心价值是:
因为很多原生 API 不只是让你传参数,还会让你传一个函数指针。
在 C# 里,这通常对应:
delegate
例如:
[UnmanagedFunctionPointer(CallingConvention.Winapi)]
internal delegate bool EnumWindowsProc(IntPtr hWnd, IntPtr lParam);
[DllImport("user32.dll")]
internal static extern bool EnumWindows(EnumWindowsProc callback, IntPtr lParam);
调用时:
EnumWindowsProc callback = (hWnd, lParam) =>
{
Console.WriteLine(hWnd);
return true;
};
EnumWindows(callback, IntPtr.Zero);
这里最重要的风险不是“怎么写委托”,而是:
必须确保委托在原生代码使用期间不会被
GC回收。
所以更稳妥的做法通常是:
SetLastError 是干什么的?很多 Windows API 调用失败后,并不会直接把完整错误信息作为返回值给你,而是把错误码放在“最后错误”位置。
这时你通常要这样声明:
[DllImport("kernel32.dll", SetLastError = true)]
internal static extern bool CloseHandle(IntPtr handle);
然后在失败后读取错误码:
int errorCode = Marshal.GetLastWin32Error();
也就是说:
SetLastError = true
这是很多 Win32 互操作代码排查失败原因时的关键。
原生函数的调用约定,决定了:
如果你声明错了,结果往往不是普通异常,而可能是:
常见写法例如:
[DllImport("mylib.dll", CallingConvention = CallingConvention.Cdecl)]
internal static extern int Add(int x, int y);
所以调用约定不能凭经验猜,必须和原生导出函数保持一致。
unsafe、IntPtr 和手动内存管理?当自动封送不够用,或者你需要更精细地控制内存时,就可能会走到底层方案。
例如:
最常见的一个模式是:
IntPtr buffer = Marshal.AllocHGlobal(1024);
try
{
// 调用原生函数
}
finally
{
Marshal.FreeHGlobal(buffer);
}
这里最重要的不是 AllocHGlobal 本身,而是:
谁申请的内存,必须由正确的一侧、用正确的方式释放。
否则就是典型的内存泄漏或非法释放问题。
P/Invoke 最容易踩的坑有哪些?这是最多见的坑。
例如把:
HANDLE 写成 intLONG 写成 longstring结果往往不是编译不过,而是运行时表现很怪。
没有 StructLayout、Pack 不匹配、字段顺序错,都可能让调用结果直接失真。
尤其是在:
ANSI / UnicodeA/W 后缀函数这些地方最容易出问题。
不知道是谁分配、谁释放,是互操作代码里最危险的问题之一。
这个问题经常是“偶发崩溃”,最难排查。
P/Invoke 当成零成本调用每次跨托管/非托管边界都有开销。
如果你在热点路径里高频细粒度调用原生函数,成本可能并不小。
所以在性能敏感场景里,更值得优化的往往是:
一个很实用的习惯是:
NativeMethods 或按 DLL 拆分类;例如:
internal static partial class Kernel32
{
[LibraryImport("kernel32.dll", SetLastError = true)]
internal static partial uint GetCurrentThreadId();
}
internal static partial class User32
{
[LibraryImport("user32.dll", SetLastError = true, StringMarshalling = StringMarshalling.Utf16)]
internal static partial int MessageBox(
IntPtr hWnd,
string text,
string caption,
uint type);
}
这样做的好处是:
如果你准备写一个 P/Invoke 声明,先问自己五个问题:
五个问题里只要有两个答不稳,就不要急着写代码,先回到原生文档和头文件确认。
如果面试官问:
“P/Invoke 的本质是什么?”
一个比较稳妥的回答可以是:
P/Invoke是 .NET 提供的托管到非托管调用机制。开发者通过DllImport或LibraryImport声明原生函数,运行时负责定位动态库、完成参数封送、跨边界调用以及返回值转换。真正的难点不在声明本身,而在封送、结构体布局、字符串编码、句柄生命周期和内存所有权。
如果继续追问“最常见的坑是什么”,就接着答:
这基本就是 P/Invoke 最核心的知识点了。
P/Invoke 的本质,不是“在 C# 里写个 DLL 声明”,而是:
在托管世界和原生世界之间搭一座边界清晰、类型正确、生命周期可控的桥。
最值得记住的其实只有四句话:
P/Invoke 解决的是托管代码调用原生代码的问题;LibraryImport,但签名正确永远比“用哪个特性”更重要;如果把它当成“声明个 extern 方法就完事”的小功能,通常迟早会踩坑。
如果把它当成“跨托管/非托管边界的系统工程”,你的互操作代码通常会稳很多。
版权声明: 本站资源均来自互联网或会员发布,如果侵犯了您的权益请与我们联系,我们将在24小时内删除!谢谢!联系QQ:76900276