.NET 原生代码集成:实用开发者指南
在 .NET 应用中利用原生代码,可以突破标准库的限制。这在调用底层操作系统 API 或硬件接口(如 MIDI)时特别有用。本指南涵盖核心内容:从基本的 DllImport 使用,到跨平台解决方案和 NuGet 打包。
P/Invoke 和 DllImport 基础
P/Invoke(平台调用)是托管 .NET 代码与原生函数之间的桥梁。下面是一个使用 DllImport 属性调用 Windows API 的简单示例:
[DllImport("winmm.dll")]
static extern uint midiInGetNumDevs();
DllImport 的关键特性:
- 指定库名称和入口点
- 自动数据类型封送
- 支持多种调用约定
封送(Marshaling)负责在托管内存和非托管内存之间转换数据类型。它处理:
- 基本类型(int、float)
- 字符串(string 到 LPWSTR)
- 结构体和指针
- 非托管内存的分配与释放
跨平台解决方案
针对多操作系统开发会遇到独特挑战:
- 不同的系统库:
- Windows:winmm.dll
- macOS:CoreMIDI.framework
- Linux:ALSA 通过 libasound
- 不同的 API 函数:
- Windows:midiInGetNumDevs()
- macOS:MIDIGetNumberOfSources()
- Linux:snd_seq_* 函数
- 不同的库路径:
- Windows:简单 DLL 名称
- macOS:完整框架路径
使用 RuntimeInformation 检测操作系统:
if (RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
count = midiInGetNumDevs();
else if (RuntimeInformation.IsOSPlatform(OSPlatform.OSX))
count = MIDIGetNumberOfSources();
构建原生后端
统一方法是创建自己的原生层来抽象平台差异。
项目结构:
- Native-Windows.cpp → Native.dll
- Native-macOS.cpp → Native.dylib
- Native-Linux.cpp → Native.so
Windows 实现示例:
extern "C" __declspec(dllexport) int GetInputDevicesCount()
{
return midiInGetNumDevs();
}
macOS 实现示例:
extern "C" int GetInputDevicesCount()
{
return MIDIGetNumberOfSources();
}
优势:
- 跨平台统一 API
- 隔离平台特定代码
- 简化 C# 代码
- 支持额外逻辑
构建和管理二进制文件
不同平台的原生库构建方式各异:
Windows (MSVC):
cl /LD Native-Windows.cpp /link /OUT:Native.dll
macOS (Clang):
clang++ -dynamiclib -o Native.dylib Native-macOS.cpp -framework CoreMIDI
Linux (GCC):
g++ -shared -fPIC -o Native.so Native-Linux.cpp -lasound
项目文件布局:
project/
├── src/
│ ├── Native/
│ │ ├── Windows/
│ │ │ ├── x64/
│ │ │ │ └── Native.dll
│ │ │ └── arm64/
│ │ │ └── Native.dll
│ │ ├── macOS/
│ │ │ ├── x64/
│ │ │ │ └── Native.dylib
│ │ │ └── arm64/
│ │ │ └── Native.dylib
│ │ └── Linux/
│ │ └── x64/
│ │ └── Native.so
│ └── Managed/
│ └── MidiWrapper.cs
└── MidiWrapper.csproj
现代 P/Invoke 方法
LibraryImport 属性
.NET 7+ 引入 LibraryImport,它在编译时生成封送代码:
[LibraryImport("Native")]
internal static partial int GetInputDevicesCount();
LibraryImport 优势:
- 编译时代码生成
- 更好性能
- 更严格类型检查
- 支持源生成器
安全资源管理
IntPtr 与 SafeHandle:
- IntPtr:原始指针,手动管理
- SafeHandle:安全包装器,自动释放
SafeHandle 示例:
sealed class MidiHandle : SafeHandle
{
public MidiHandle() : base(IntPtr.Zero, true) { }
public override bool IsInvalid => handle == IntPtr.Zero;
protected override bool ReleaseHandle()
{
return midiInClose(handle) == 0;
}
}
回调与异步处理
回调挑战:
- 回调中无法调用某些原生函数
- 需要线程同步
- 误用可能导致死锁
MIDI 缓冲区模式:
- 预分配多个缓冲区
- 在回调中复用缓冲区数据
- 避免在回调中调用 unprepared/close
- 使用线程安全数据结构
安全回调示例:
private static void MidiCallback(IntPtr handle, uint msg, IntPtr instance, IntPtr param1, IntPtr param2)
{
// 从缓冲区提取数据
var data = ExtractMidiData(param1);
// 异步处理
Task.Run(() => ProcessMidiDataAsync(data));
// 重新添加缓冲区
lock (bufferLock)
{
midiInAddBuffer(handle, param1, Marshal.SizeOf<MIDIHDR>());
}
}
NuGet 打包
包含原生库的 NuGet 结构:
<PackageReference>
<NativeLibs>
<RuntimeIdentifier>win-x64</RuntimeIdentifier>
<NativeLib>runtimes/win-x64/native/Native.dll</NativeLib>
</NativeLibs>
<NativeLibs>
<RuntimeIdentifier>osx-arm64</RuntimeIdentifier>
<NativeLib>runtimes/osx-arm64/native/Native.dylib</NativeLib>
</NativeLibs>
</PackageReference>
.csproj 指令:
<ItemGroup>
<Content Include="runtimes\win-x64\native\Native.dll">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackagePath>runtimes/win-x64/native/</PackagePath>
</Content>
<Content Include="runtimes\osx-arm64\native\Native.dylib">
<CopyToOutputDirectory>PreserveNewest</CopyToOutputDirectory>
<PackagePath>runtimes/osx-arm64/native/</PackagePath>
</Content>
</ItemGroup>
要点:
- 按 RID(运行时标识符)组织
- 在 .csproj 中指定目标平台
- 跨系统测试
- 支持 x64 和 ARM64
性能优化
减少开销:
- 最小化托管/非托管转换
- 使用 blittable 类型(无需封送)
- 缓存句柄和指针
- 批量调用
Blittable 类型:
- byte, sbyte, short, ushort, int, uint, long, ulong
- float, double
- 仅含 blittable 的结构体
优化结构体:
[StructLayout(LayoutKind.Sequential)]
struct MidiMessage
{
public uint Timestamp;
public byte Status;
public byte Data1;
public byte Data2;
public byte Reserved;
}
调试与诊断
原生调试工具:
- WinDbg 和 CDB(Windows)
- LLDB(macOS/Linux)
- Visual Studio 混合模式
- JetBrains Rider 原生支持
常见问题:
- 访问冲突:类型封送错误
- 内存泄漏:未释放资源
- 死锁:回调锁
- 性能:频繁 P/Invoke
调用日志:
[DllImport("Native", EntryPoint = "GetInputDevicesCount")]
private static extern int NativeGetInputDevicesCount();
public static int GetInputDevicesCount()
{
Logger.Debug("调用原生 GetInputDevicesCount");
var result = NativeGetInputDevicesCount();
Logger.Debug($"结果:{result}");
return result;
}
— Editorial Team
暂无评论。