P/Invoke 与 COM 互操作
大约 9 分钟约 2823 字
P/Invoke 与 COM 互操作
简介
.NET 通过 P/Invoke(Platform Invoke)调用本机 DLL 函数,通过 COM Interop 与 COM 组件交互。这两种互操作机制是 .NET 与非托管代码之间的桥梁,在集成遗留系统、调用系统 API 和使用第三方本机库时不可或缺。
特点
P/Invoke 基础
DllImport 声明
using System.Runtime.InteropServices;
// 基本 P/Invoke 声明
[DllImport("user32.dll")]
static extern int MessageBoxW(IntPtr hWnd, string text, string caption, int type);
// 调用
MessageBoxW(IntPtr.Zero, "Hello from .NET!", "P/Invoke", 0);
// 常用 Windows API
[DllImport("kernel32.dll")]
static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
static extern bool GetDiskFreeSpaceEx(
string directoryName,
out ulong freeBytesAvailable,
out ulong totalNumberOfBytes,
out ulong totalNumberOfFreeBytes);
// 使用
if (GetDiskFreeSpaceEx("C:\\", out ulong free, out ulong total, out ulong freeTotal))
{
Console.WriteLine($"可用空间: {free / 1024.0 / 1024 / 1024:F2} GB");
Console.WriteLine($"总空间: {total / 1024.0 / 1024 / 1024:F2} GB");
}
// Linux/macOS P/Invoke
[DllImport("libc.so.6")] // Linux
static extern int getpid();
[DllImport("libSystem.dylib")] // macOS
static extern int getpid();DllImport 属性详解
// 常用属性参数
[DllImport("kernel32.dll",
EntryPoint = "GetTempPathW", // 指定函数入口名
CharSet = CharSet.Unicode, // 字符集
SetLastError = true, // 保留 GetLastError
ExactSpelling = true, // 精确匹配函数名
CallingConvention = CallingConvention.StdCall, // 调用约定
PreserveSig = true)] // 保留原始签名
static extern uint GetTempPath(uint nBufferLength, StringBuilder lpBuffer);
// CallingConvention:
// StdCall — Windows API 默认(被调用者清理栈)
// Cdecl — C 默认(调用者清理栈)
// ThisCall — C++ 成员函数(第一个参数是 this)
// FastCall — 寄存器传参(较少使用)
// SetLastError
// true: 保存 Win32 错误码,可通过 Marshal.GetLastWin32Error() 获取
// false: 不保存(默认)
[DllImport("kernel32.dll", SetLastError = true)]
static extern IntPtr CreateFile(string lpFileName, uint dwDesiredAccess,
uint dwShareMode, IntPtr lpSecurityAttributes, uint dwCreationDisposition,
uint dwFlagsAndAttributes, IntPtr hTemplateFile);
void OpenFile()
{
IntPtr handle = CreateFile("test.txt", 0x80000000, 0, IntPtr.Zero, 3, 0, IntPtr.Zero);
if (handle == new IntPtr(-1))
{
int error = Marshal.GetLastWin32Error();
Console.WriteLine($"Error: {error}");
}
}数据封送
托管与非托管类型映射
// 常用类型映射
// C# 类型 → 非托管类型
// int → int32_t (4 bytes)
// long → int64_t (8 bytes, 注意不是 C 的 long!)
// float → float (4 bytes)
// double → double (8 bytes)
// bool → BOOL (4 bytes, Win32) / byte (C)
// char → wchar_t (Unicode) / char (ANSI)
// string → const wchar_t* / const char*
// StringBuilder → wchar_t* / char* (输出缓冲)
// IntPtr → void* / HANDLE / HWND
// byte[] → unsigned char*
// struct → struct (需 LayoutKind)
// delegate → 函数指针
// 字符串封送
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool SetComputerName(string lpComputerName);
// 双向字符串缓冲
[DllImport("kernel32.dll", CharSet = CharSet.Unicode)]
static extern bool GetComputerName(StringBuilder lpBuffer, ref int nSize);
string GetComputer()
{
var sb = new StringBuilder(256);
int size = sb.Capacity;
GetComputerName(sb, ref size);
return sb.ToString();
}
// 数组封送
[DllImport("mylib.dll")]
static extern void ProcessArray([In, Out] int[] data, int length);
// [In] — 只传入(从托管复制到非托管)
// [Out] — 只传出(从非托管复制回托管)
// [In, Out] — 双向复制
// 结构体封送
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
[StructLayout(LayoutKind.Sequential, CharSet = CharSet.Unicode)]
public struct WIN32_FIND_DATA
{
public uint dwFileAttributes;
public FILETIME ftCreationTime;
public FILETIME ftLastAccessTime;
public FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 14)]
public string cAlternateFileName;
}跨平台 P/Invoke
/// <summary>
/// 跨平台 P/Invoke 的处理策略
/// </summary>
// 使用 RuntimeInformation 检测平台
using System.Runtime.InteropServices;
public static class NativeMethods
{
public static int GetProcessId()
{
if (OperatingSystem.IsWindows())
return GetProcessId_Windows();
else if (OperatingSystem.IsLinux())
return GetProcessId_Linux();
else if (OperatingSystem.IsMacOS())
return GetProcessId_MacOS();
throw new PlatformNotSupportedException();
}
[DllImport("kernel32.dll")]
private static extern int GetCurrentProcessId();
private static int GetProcessId_Windows() => GetCurrentProcessId();
[DllImport("libc.so.6")]
private static extern int getpid();
private static int GetProcessId_Linux() => getpid();
[DllImport("libSystem.dylib")]
private static extern int getpid();
private static int GetProcessId_MacOS() => getpid();
}
// 使用 LibraryImport 的跨平台声明
public static partial class NativeLib
{
[LibraryImport("libc.so.6")]
public static partial int getpid();
// Linux: libc.so.6
// macOS: libSystem.dylib
// Windows: 不存在
}.NET 7+ LibraryImport(源生成器)
// LibraryImport 使用源生成器,编译时生成封送代码
// 优点:AOT 友好、编译时检查、更好的性能
// partial 方法 + LibraryImport 属性
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int MessageBoxW(IntPtr hWnd, string text, string caption, int type);
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16)]
internal static partial bool GetDiskFreeSpaceEx(
string directoryName,
out ulong freeBytesAvailable,
out ulong totalNumberOfBytes,
out ulong totalNumberOfFreeBytes);
// 使用 MarshalAs 指定封送方式
[LibraryImport("mylib.dll")]
internal static partial int ProcessString(
[MarshalAs(UnmanagedType.LPStr)] string input, // ANSI
[MarshalAs(UnmanagedType.LPUTF8Str)] string utf8); // UTF8
// 返回字符串
[LibraryImport("mylib.dll", StringMarshalling = StringMarshalling.Utf16)]
internal static partial string GetName();
// 对比 DllImport vs LibraryImport
// DllImport: 运行时反射封送,支持 StringBuilder
// LibraryImport: 编译时生成,AOT 友好,不支持 StringBuilderCOM 互操作
RCW 与 CCW 机制
// RCW (Runtime Callable Wrapper) — .NET 调用 COM 对象
// CCW (COM Callable Wrapper) — COM 调用 .NET 对象
// 使用 COM 对象(以 Excel 为例)
// 1. 添加 COM 引用(Visual Studio)
// 2. 使用 Type.GetTypeFromProgID 创建实例
Type excelType = Type.GetTypeFromProgID("Excel.Application")!;
object excel = Activator.CreateInstance(excelType)!;
try
{
// 通过反射调用 COM 方法
excelType.InvokeMember("Visible", BindingFlags.SetProperty,
null, excel, new object[] { true });
// 使用 dynamic 简化
dynamic excelApp = Activator.CreateInstance(excelType)!;
excelApp.Visible = true;
dynamic workbooks = excelApp.Workbooks;
dynamic workbook = workbooks.Add();
// ...
workbook.Close(false);
excelApp.Quit();
}
finally
{
Marshal.ReleaseComObject(excel);
}
// COM 接口定义
[ComImport]
[Guid("00020970-0000-0000-C000-000000000046")]
[InterfaceType(ComInterfaceType.InterfaceIsIDispatch)]
interface _Application
{
[DispId(558)]
bool Visible { get; set; }
[DispId(302)]
void Quit();
}.NET 类暴露为 COM
// 将 .NET 类暴露给 COM(CCW)
[ComVisible(true)]
[ClassInterface(ClassInterfaceType.None)]
[Guid("12345678-1234-1234-1234-1234567890AB")]
[ProgId("MyCompany.MyComponent")]
public class MyComServer : IMyComInterface
{
public string Greet(string name) => $"Hello, {name}!";
public int Add(int a, int b) => a + b;
}
[ComVisible(true)]
[Guid("87654321-4321-4321-4321-BA0987654321")]
public interface IMyComInterface
{
string Greet(string name);
int Add(int a, int b);
}
// 注册 COM 组件
// regasm MyAssembly.dll /tlb /codebase
// .NET 5+ 使用 ComWrappers(自定义 COM 互操作)
public class MyComWrappers : ComWrappers
{
protected override unsafe ComInterfaceEntry* ComputeVtables(
object obj, CreateComInterfaceFlags flags, out int count)
{
// 自定义 VTable 生成
count = 0;
return null;
}
protected override object? CreateObject(
nint externalComObject, CreateObjectFlags flags)
{
// 从 COM 指针创建 .NET 对象
return null;
}
}回调函数
函数指针回调
// 托管回调(委托转函数指针)
// 使用 Delegate.CreateDelegate 或 Marshal.GetFunctionPointerForDelegate
// C 库签名: void register_callback(void (*callback)(int));
[DllImport("mylib.dll")]
static extern void register_callback(delegate* unmanaged[Cdecl]<int, void> callback);
// 或者使用委托
[DllImport("mylib.dll")]
static extern void register_callback_delegate(CallbackDelegate callback);
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate void CallbackDelegate(int value);
// 使用
static void MyCallback(int value) => Console.WriteLine($"Callback: {value}");
void Register()
{
var del = new CallbackDelegate(MyCallback);
// 防止委托被 GC 回收!
GCHandle.Alloc(del); // 固定委托
register_callback_delegate(del);
}
// Windows 窗口过程回调
[DllImport("user32.dll")]
static extern IntPtr CallWindowProc(WndProcDelegate lpPrevWndFunc,
IntPtr hWnd, uint Msg, IntPtr wParam, IntPtr lParam);
delegate IntPtr WndProcDelegate(IntPtr hWnd, uint msg, IntPtr wParam, IntPtr lParam);优点
缺点
封送性能优化
减少封送开销
/// <summary>
/// P/Invoke 封送性能优化策略
/// </summary>
// 1. 使用 blittable 类型避免拷贝
// Blittable 类型:int, float, double, byte, 指针
// 这些类型在托管和非托管内存中有相同的布局
// ❌ bool 是非 blittable(4 bytes → 1 byte 转换)
[DllImport("mylib.dll")]
static extern bool IsEnabled();
// ✅ 使用 int 替代 bool(blittable)
[DllImport("mylib.dll")]
static extern int IsEnabledRaw();
// 在调用方转换
bool IsEnabled() => IsEnabledRaw() != 0;
// 2. 批量传递数据,减少调用次数
// ❌ 逐个元素传递
[DllImport("mylib.dll")]
static extern void SetPixel(int x, int y, byte r, byte g, byte b);
// ✅ 批量传递
[DllImport("mylib.dll")]
static extern void SetPixels(IntPtr buffer, int count);
// 3. 使用 Span 避免数组拷贝
[DllImport("mylib.dll")]
static extern void ProcessBuffer(IntPtr data, int length);
unsafe void ProcessWithSpan(Span<byte> data)
{
fixed (byte* ptr = data)
{
ProcessBuffer((IntPtr)ptr, data.Length);
}
}
// 4. 使用 unsafe 禁用封送
[DllImport("mylib.dll")]
static extern unsafe void ProcessUnsafe(void* data, int length);
unsafe void ProcessDirect(Span<byte> data)
{
fixed (byte* ptr = data)
{
ProcessUnsafe(ptr, data.Length);
}
}SafeHandle 模式
/// <summary>
/// SafeHandle 封装非托管资源
/// </summary>
// 每个非托管资源都应使用 SafeHandle 封装
public sealed class FileHandle : SafeHandleZeroOrMinusOneIsInvalid
{
public FileHandle(string path, FileMode mode)
: base(true) // ownsHandle = true
{
SetHandle(NativeMethods.OpenFile(path, mode));
}
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
NativeMethods.CloseFile(handle);
return true;
}
return false;
}
public int Read(Span<byte> buffer)
{
if (IsInvalid) throw new ObjectDisposedException(nameof(FileHandle));
unsafe
{
fixed (byte* ptr = buffer)
return NativeMethods.ReadFile(handle, ptr, buffer.Length);
}
}
}
// 使用 — 自动释放资源
public void ProcessFile(string path)
{
using var handle = new FileHandle(path, FileMode.Open);
Span<byte> buffer = stackalloc byte[4096];
int bytesRead = handle.Read(buffer);
// 使用 buffer...
}
// 优势:
// 1. 即使在异常情况下也能正确释放
// 2. 终结器作为安全网(防止忘记 Dispose)
// 3. 防止竞态条件(ReleaseHandle 只执行一次)
// 4. P/Invoke 可以直接接受 SafeHandle
[DllImport("mylib.dll")]
static extern void ProcessFileHandle(SafeHandle handle);总结
P/Invoke 通过 DllImport 声明调用本机 DLL 函数,核心参数包括 CharSet、SetLastError、CallingConvention。数据封送负责托管/非托管类型转换,StructLayout 控制结构体布局。.NET 7+ 的 LibraryImport 使用源生成器替代运行时反射,AOT 友好。COM Interop 通过 RCW/CCW 机制实现 .NET 与 COM 双向调用。回调函数使用 delegate 或函数指针,注意固定委托防止 GC 回收。生产环境优先使用 LibraryImport 替代 DllImport。
关键知识点
- 先明确这个主题影响的是语法层、运行时层,还是性能与可维护性层。
- 学习时要同时关注语言表面写法和编译器、JIT、GC 等底层行为。
- 真正有价值的是知道“为什么这样写”和“在什么边界下不能这样写”。
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 只记语法糖,不知道底层成本。
- 把适用于小样例的写法直接搬到高并发或大对象场景里。
- 忽略框架版本、语言版本和运行时差异,导致结论失真。
进阶路线
- 继续向源码、IL、JIT 行为和 BCL 实现层深入。
- 把知识点和代码评审、性能诊断、面试复盘结合起来。
- 把同类主题做横向对比,例如值类型与引用类型、迭代器与 async 状态机、反射与 Source Generator。
适用场景
- 当你准备把《P/Invoke 与 COM 互操作》真正落到项目里时,最适合先在一个独立模块或最小样例里验证关键路径。
- 适合在需要理解语言特性、运行时行为或 API 边界时阅读。
- 当代码开始出现性能瓶颈、可维护性问题或语义歧义时,这类主题会直接影响实现质量。
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了“高级”而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把《P/Invoke 与 COM 互操作》放进你的当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 《P/Invoke 与 COM 互操作》最容易在什么规模、什么边界条件下暴露问题?你会用什么指标或日志去确认?
- 相比默认实现或替代方案,采用《P/Invoke 与 COM 互操作》最大的收益和代价分别是什么?
