互操作与 Marshalling
大约 9 分钟约 2722 字
互操作与 Marshalling
简介
C# 通过 P/Invoke 和 Marshalling 与原生 C/C++ 库交互。理解数据类型映射、内存管理和回调委托,有助于在 .NET 中调用 Win32 API、Linux 系统调用和第三方原生库。互操作是 .NET 生态与原生世界之间的桥梁——无论是调用操作系统的底层 API、使用硬件厂商提供的 SDK、集成高性能 C++ 计算库,还是复用企业遗留的原生代码,P/Invoke 都是最常用的技术手段。.NET 7+ 引入的 LibraryImport 通过源生成器在编译时生成 marshalling 代码,解决了 AOT 兼容性问题,是现代 .NET 互操作的首选方式。
特点
类型映射
常见类型对照表
| C/C++ 类型 | C# 类型 | 说明 |
|---|---|---|
| void* | IntPtr / nint | 指针 |
| int | int / Int32 | 32 位整数 |
| long (Windows) | int | Windows long 是 32 位 |
| long long | long / Int64 | 64 位整数 |
| float | float | 32 位浮点 |
| double | double | 64 位浮点 |
| char* | string / byte* | 字符串(取决于编码) |
| const char* | string | 只读字符串 |
| wchar_t* | string | 宽字符串 |
| struct | struct | 需要指定 LayoutKind |
| void (*func)() | delegate | 函数指针 |
| BOOL | bool / int | Windows BOOL 是 4 字节 |
| HANDLE | IntPtr | 不透明句柄 |
实现
P/Invoke 基础
using System.Runtime.InteropServices;
// Win32 API 调用
public class NativeMethods
{
[DllImport("user32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, int type);
[DllImport("kernel32.dll", SetLastError = true)]
public static extern bool GetDiskFreeSpaceEx(
string directoryName,
out long freeBytesAvailable,
out long totalNumberOfBytes,
out long totalNumberOfFreeBytes);
[DllImport("kernel32.dll")]
public static extern IntPtr GetConsoleWindow();
[DllImport("kernel32.dll", SetLastError = true)]
public static extern uint GetTickCount();
}
// 使用
NativeMethods.MessageBoxW(IntPtr.Zero, "Hello from C#!", "提示", 0);
NativeMethods.GetDiskFreeSpaceEx("C:\\", out var free, out var total, out var _);
Console.WriteLine($"可用空间: {free / 1024.0 / 1024 / 1024:F2} GB");
Console.WriteLine($"系统运行时间 (ms): {NativeMethods.GetTickCount()}");.NET 7+ LibraryImport
// LibraryImport — 编译时生成 marshalling 代码(AOT 友好)
partial class Native
{
[LibraryImport("user32.dll", StringMarshalling = StringMarshalling.Utf16)]
public static partial int MessageBoxW(IntPtr hWnd, string text, string caption, int type);
[LibraryImport("kernel32.dll", SetLastError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
public static partial bool GetDiskFreeSpaceEx(
string directoryName,
out long freeBytesAvailable,
out long totalNumberOfBytes,
out long totalNumberOfFreeBytes);
}LibraryImport 相比 DllImport 的优势:
- 编译时生成 marshalling 代码,无运行时反射开销
- AOT 兼容,Native AOT 可以正常工作
- 编译时类型检查更严格,错误更早暴露
- 生成代码更高效,减少了运行时的类型查找
结构体 Marshalling
using System.Runtime.InteropServices;
// 顺序布局 — 成员按声明顺序排列
[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 System.Runtime.InteropServices.ComTypes.FILETIME ftCreationTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastAccessTime;
public System.Runtime.InteropServices.ComTypes.FILETIME ftLastWriteTime;
public uint nFileSizeHigh;
public uint nFileSizeLow;
[MarshalAs(UnmanagedType.ByValTStr, SizeConst = 260)]
public string cFileName;
}
// 联合体 (Union) — 所有成员共享同一内存位置
[StructLayout(LayoutKind.Explicit)]
public struct FloatIntUnion
{
[FieldOffset(0)] public float FloatValue;
[FieldOffset(0)] public int IntValue;
}
var u = new FloatIntUnion { FloatValue = 1.0f };
Console.WriteLine($"Float: {u.FloatValue}, Int bits: {u.IntValue}");
// 输出: Float: 1, Int bits: 1065353216 (IEEE 754 浮点数的位模式)字符串 Marshalling
using System.Runtime.InteropServices;
using System.Text;
public class StringMarshalling
{
// C# string -> C char* (UTF-8)
[DllImport("libc.so.6")]
public static extern int puts([MarshalAs(UnmanagedType.LPUTF8Str)] string s);
// C# string -> C wchar_t* (UTF-16)
[DllImport("user32.dll", CharSet = CharSet.Unicode)]
public static extern int MessageBoxW(IntPtr hWnd, string text, string caption, int type);
// C char* -> C# string
[DllImport("kernel32.dll", CharSet = CharSet.Ansi)]
public static extern IntPtr GetCommandLineA();
// 使用 Marshal 读取原生字符串
public static string PtrToStringAnsi(IntPtr ptr)
=> Marshal.PtrToStringAnsi(ptr) ?? string.Empty;
public static string PtrToStringUtf8(IntPtr ptr)
{
if (ptr == IntPtr.Zero) return string.Empty;
int len = 0;
while (Marshal.ReadByte(ptr, len) != 0) len++;
byte[] buffer = new byte[len];
Marshal.Copy(ptr, buffer, 0, len);
return Encoding.UTF8.GetString(buffer);
}
// 使用 SafeHandle 安全地处理原生内存中的字符串
public static string ReadNativeString(SafeHandle handle)
{
using var scope = handle.DangerousAddRef(ref _);
return Marshal.PtrToStringAuto(handle.DangerousGetHandle()) ?? "";
}
private static bool _;
}内存管理
using System.Runtime.InteropServices;
public class NativeMemoryManager : IDisposable
{
private IntPtr _ptr;
private int _size;
public NativeMemoryManager(int size)
{
_size = size;
_ptr = Marshal.AllocHGlobal(size);
}
// 写入数据
public void Write(int offset, byte[] data)
{
Marshal.Copy(data, 0, _ptr + offset, data.Length);
}
// 读取数据
public byte[] Read(int offset, int length)
{
byte[] data = new byte[length];
Marshal.Copy(_ptr + offset, data, 0, length);
return data;
}
// 直接操作内存
public unsafe void WriteInt(int offset, int value)
{
*(int*)(_ptr + offset) = value;
}
public unsafe int ReadInt(int offset)
{
return *(int*)(_ptr + offset);
}
public void Dispose()
{
if (_ptr != IntPtr.Zero)
{
Marshal.FreeHGlobal(_ptr);
_ptr = IntPtr.Zero;
}
}
}
// 使用
using var mem = new NativeMemoryManager(1024);
mem.Write(0, new byte[] { 1, 2, 3, 4 });
var data = mem.Read(0, 4);回调与委托
using System.Runtime.InteropServices;
// 定义回调委托类型
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
public delegate void LogCallback(IntPtr message, int length);
[UnmanagedFunctionPointer(CallingConvention.StdCall)]
public delegate int CompareCallback(IntPtr a, IntPtr b);
class NativeLogger
{
[DllImport("native_lib.dll")]
private static extern void SetLogCallback(LogCallback callback);
[DllImport("native_lib.dll")]
private static extern void SetSortCallback(CompareCallback callback);
// 防止 GC 回收委托实例
private static LogCallback? _logDelegate;
private static CompareCallback? _compareDelegate;
public static void Register()
{
_logDelegate = (ptr, length) =>
{
var msg = Marshal.PtrToStringAnsi(ptr) ?? "";
Console.WriteLine($"[Native] {msg} (length: {length})");
};
SetLogCallback(_logDelegate);
}
public static void RegisterSort()
{
_compareDelegate = (a, b) =>
{
int valA = Marshal.ReadInt32(a);
int valB = Marshal.ReadInt32(b);
return valA.CompareTo(valB);
};
SetSortCallback(_compareDelegate);
}
}关键点:必须将委托实例保存为字段(如 _logDelegate),否则 GC 可能回收委托,导致原生代码调用已释放的内存,引发崩溃。这是 P/Invoke 中最常见的 bug 之一。
SafeHandle 资源管理
using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;
// 自定义 SafeHandle — 安全管理原生句柄
public class SafeFileHandleWrapper : SafeHandleZeroOrMinusOneIsInvalid
{
public SafeFileHandleWrapper() : base(true) { }
// 从现有句柄创建
public SafeFileHandleWrapper(IntPtr handle, bool ownsHandle)
: base(ownsHandle)
{
SetHandle(handle);
}
// 释放资源
protected override bool ReleaseHandle()
{
if (!IsInvalid)
{
CloseHandle(handle);
return true;
}
return false;
}
[DllImport("kernel32.dll", SetLastError = true)]
private static extern bool CloseHandle(IntPtr hObject);
}
// 使用 SafeHandle
public class SafeHandleExample
{
[DllImport("kernel32.dll", SetLastError = true)]
private static extern SafeFileHandleWrapper CreateFileW(
string lpFileName,
uint dwDesiredAccess,
uint dwShareMode,
IntPtr lpSecurityAttributes,
uint dwCreationDisposition,
uint dwFlagsAndAttributes,
IntPtr hTemplateFile);
public static void SafeFileOperation()
{
using var handle = CreateFileW("test.txt", 0x80000000, 0, IntPtr.Zero, 2, 0, IntPtr.Zero);
if (handle.IsInvalid)
{
int error = Marshal.GetLastWin32Error();
Console.WriteLine($"打开文件失败,错误码: {error}");
return;
}
Console.WriteLine("文件句柄获取成功");
// handle 在 using 块结束时自动释放
}
}跨平台 P/Invoke
using System.Runtime.InteropServices;
public class CrossPlatformNative
{
// 条件编译支持不同平台
#if WINDOWS
[DllImport("kernel32.dll")]
public static extern uint GetCurrentProcessId();
[DllImport("kernel32.dll")]
public static extern void Sleep(uint dwMilliseconds);
#elif LINUX
[DllImport("libc.so.6")]
public static extern int getpid();
[DllImport("libc.so.6")]
public static extern void usleep(uint microseconds);
#elif OSX
[DllImport("libSystem.dylib")]
public static extern int getpid();
#endif
public static void PlatformSpecificOperation()
{
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
{
Console.WriteLine($"Windows PID: {GetCurrentProcessId()}");
}
else if (RuntimeInformation.IsOSPlatform(OSPlatform.Linux))
{
Console.WriteLine($"Linux PID: {getpid()}");
}
}
}Span/Memory 与原生内存
using System.Runtime.InteropServices;
public class SpanInterop
{
// 将 Span<T> 传递给原生代码
[DllImport("native_lib.dll")]
private static extern void ProcessData(Span<byte> data);
// 使用 MemoryMarshal 获取指针
public unsafe void ProcessWithSpan(Span<float> data)
{
fixed (float* ptr = data)
{
// 直接操作指针
for (int i = 0; i < data.Length; i++)
ptr[i] *= 2.0f;
}
}
// 使用 MemoryMarshal.AsBytes 进行类型转换
public static void FloatToBytes(Span<float> floats, Span<byte> bytes)
{
if (bytes.Length < floats.Length * sizeof(float))
throw new ArgumentException("目标缓冲区太小");
MemoryMarshal.AsBytes(floats).CopyTo(bytes);
}
// 使用 NativeMemory API (.NET 6+)
public static void AllocateNativeMemory()
{
unsafe
{
float* ptr = (float*)NativeMemory.Alloc(100 * sizeof(float));
try
{
for (int i = 0; i < 100; i++)
ptr[i] = i * 1.5f;
}
finally
{
NativeMemory.Free(ptr);
}
}
}
}优点
缺点
性能优化建议
// 优化1:减少 marshalling 次数 — 批量传递数据
// 差:每次调用传递少量数据
for (int i = 0; i < 1000; i++)
NativeMethods.ProcessItem(items[i]); // 1000 次跨边界调用
// 好:批量传递
NativeMethods.ProcessItems(items, 1000); // 1 次跨边界调用
// 优化2:使用 blittable 类型避免复制
// blittable 类型在托管和非托管内存中有相同的布局,无需转换
// blittable: byte, sbyte, short, ushort, int, uint, long, ulong, float, double, IntPtr
// 非 blittable: bool, string, char, object, 数组
// 优化3:使用 LibraryImport 替代 DllImport
// LibraryImport 在编译时生成 marshalling 代码,无运行时开销
// 优化4:使用 unsafe 代码和指针直接操作内存
public unsafe void DirectMemoryAccess(IntPtr nativePtr, int count)
{
float* ptr = (float*)nativePtr;
for (int i = 0; i < count; i++)
ptr[i] = MathF.Sqrt(ptr[i]);
}总结
P/Invoke 通过 DllImport 调用原生函数,LibraryImport(.NET 7+)在编译时生成 marshalling 代码,对 AOT 更友好。结构体用 StructLayout 控制内存布局。回调委托需持有引用防止 GC 回收。建议优先使用 LibraryImport 替代 DllImport,使用 SafeHandle 管理原生句柄,使用 blittable 类型减少 marshalling 开销。
关键知识点
- DllImport 在运行时通过反射查找和调用,LibraryImport 在编译时生成代码
- blittable 类型在托管和非托管内存中布局相同,无需 marshalling
- 回调委托必须保持引用防止 GC 回收,这是最常见的 P/Invoke 崩溃原因
- SafeHandle 实现了 IDisposable 和终结器模式,确保原生资源被正确释放
项目落地视角
- 把示例改成最小可运行样例,并观察编译输出、运行结果和异常行为。
- 如果它会进入团队代码规范,最好同步补充命名约定、禁用场景和替代方案。
- 涉及性能结论时,优先用 Benchmark 或实际热点链路验证,而不是凭感觉判断。
常见误区
- 忘记保持回调委托的引用——GC 回收后原生代码调用崩溃
- 混淆 Windows 的 long(32 位)和 C# 的 long(64 位)
- 不使用 SafeHandle 管理句柄——异常时可能泄漏原生资源
- 在循环中频繁跨边界调用——应该批量传递数据
进阶路线
- 学习 CsWin32 源生成器(自动生成 Win32 API 的 P/Invoke 声明)
- 了解 NativeAOT 的互操作限制和优化策略
- 掌握 unsafe 代码和指针操作的最佳实践
- 研究 COM 互操作和 C++/CLI 的使用场景
适用场景
- 调用操作系统底层 API(文件、网络、进程、窗口管理)
- 集成硬件厂商提供的 C/C++ SDK
- 复用企业遗留的原生代码库
- 性能敏感场景下使用原生优化的计算库
落地建议
- 先写最小可运行样例,再把结论迁移到真实业务代码。
- 同时记录这个特性的收益、限制和替代方案,避免为了"高级"而使用。
- 涉及内存、并发或序列化时,最好配合调试器或基准测试验证。
排错清单
- 先确认问题属于编译期、运行期还是语义误用。
- 检查是否存在隐式转换、装箱拆箱、闭包捕获或上下文切换等隐藏成本。
- 查看异常栈、日志和最小复现代码,优先排除使用姿势问题。
复盘问题
- 如果把互操作放进当前项目,最先要验证的输入、输出和失败路径分别是什么?
- 项目中有多少跨边界调用?是否可以批量传递减少开销?
- 相比默认实现或替代方案,采用 P/Invoke 最大的收益和代价分别是什么?
