返回首页

.NET 中的本机代码集成:P/Invoke、DllImport、跨平台兼容性

本文是关于将本机代码集成到 .NET 应用程序的详细指南。它涵盖了 P/Invoke 和 DllImport 机制、创建跨平台解决方案、资源管理和打包到 NuGet 包中。该材料包含中高级开发者的实用示例和推荐。

.NET 中如何集成本机代码:实用示例和解决方案
Advertisement 728x90

.NET 原生代码集成:实用开发者指南

在 .NET 应用中利用原生代码,可以突破标准库的限制。这在调用底层操作系统 API 或硬件接口(如 MIDI)时特别有用。本指南涵盖核心内容:从基本的 DllImport 使用,到跨平台解决方案和 NuGet 打包。

P/Invoke 和 DllImport 基础

P/Invoke(平台调用)是托管 .NET 代码与原生函数之间的桥梁。下面是一个使用 DllImport 属性调用 Windows API 的简单示例:

[DllImport("winmm.dll")]
static extern uint midiInGetNumDevs();

DllImport 的关键特性:

Google AdInline article slot
  • 指定库名称和入口点
  • 自动数据类型封送
  • 支持多种调用约定

封送(Marshaling)负责在托管内存和非托管内存之间转换数据类型。它处理:

  • 基本类型(int、float)
  • 字符串(string 到 LPWSTR)
  • 结构体和指针
  • 非托管内存的分配与释放

跨平台解决方案

针对多操作系统开发会遇到独特挑战:

  • 不同的系统库

- Windows:winmm.dll

Google AdInline article slot

- macOS:CoreMIDI.framework

- Linux:ALSA 通过 libasound

  • 不同的 API 函数

- Windows:midiInGetNumDevs()

Google AdInline article slot

- 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

Advertisement 728x90

继续阅读