mcpField
D
dotnet-native-interop

dotnet-native-interop

github.com/wshaddix开发8682 次安装35 周前更新
代码仓库

关于 dotnet-native-interop

为 .NET 开发者提供调用原生 C/C++ 库的 P/Invoke 指南,涵盖互操作声明、数据封送、跨平台库解析和回调模式。

主要功能

  1. 01比较 LibraryImport 与 DllImport
  2. 02处理结构体和字符串封送
  3. 03解析跨平台原生库名称
  4. 04使用函数指针和回调
  5. 05管理原生资源生命周期

适用场景

  1. 01从 .NET 调用原生 C/C++ API
  2. 02为多平台应用配置原生库加载
  3. 03设计原生回调与资源管理

安装

复制下面的命令,在终端中运行。

$npx skills add https://github.com/wshaddix/dotnet-skills --skill dotnet-native-interop

安装后在 Claude Code 中输入/dotnet-native-interop确认已加载。

详细介绍

dotnet-native-interop

Platform Invoke(P/Invoke)模式:从 .NET 调用原生 C/C++ 库,包括 [LibraryImport](首选,.NET 7+)与 [DllImport](旧版)、结构体封送、字符串封送、函数指针回调、用于跨平台库解析的 NativeLibrary.SetDllImportResolver,以及 Windows、macOS、Linux、iOS 和 Android 的平台特定注意事项。

**版本假设:**以 .NET 7.0+ 为基线,使用 [LibraryImport]。所有 .NET 版本均提供 [DllImport]。NativeLibrary API 自 .NET Core 3.0 起提供。

**范围边界:**本技能负责通用 P/Invoke 指南——声明原生方法签名、封送数据类型、解析跨平台库路径以及回调模式。AOT 特定的 P/Invoke 注意事项(直接 P/Invoke、用于 AOT 发布的编译时封送)请参阅 [skill:dotnet-native-aot]。Windows COM 互操作和 CsWin32 源生成器的用法请参阅 [skill:dotnet-winui]。WASM 不支持传统 P/Invoke——请参阅 [skill:dotnet-aot-wasm],了解通过 [JSImport]/[JSExport] 实现的 JavaScript 互操作。

**范围之外:**COM 互操作(Windows 旧版)。CsWin32 源生成器——请参阅 [skill:dotnet-winui]。用于 Android Java 互操作的 JNI 桥接(与 P/Invoke 机制不同)。用于 WASM 的 [JSImport]/[JSExport]——请参阅 [skill:dotnet-aot-wasm]。

交叉引用:[skill:dotnet-native-aot] 介绍 AOT 特定的 P/Invoke,以及发布场景中的 [LibraryImport];[skill:dotnet-aot-architecture] 介绍 AOT 优先的设计模式,包括源生成的互操作;[skill:dotnet-winui] 介绍 CsWin32 源生成器和 COM 互操作;[skill:dotnet-aot-wasm] 介绍 WASM JavaScript 互操作(而非原生 P/Invoke)。


LibraryImport 与 DllImport

[LibraryImport](.NET 7+)是新 P/Invoke 声明的首选特性。它使用源生成在编译时生成封送代码,因此完全兼容 AOT,并消除运行时代码生成开销。

[DllImport] 是旧版特性。它依赖运行时封送,在 AOT 场景中可能需要不可用的代码生成。仅在目标为 .NET 6 或更早版本,或 SYSLIB1054 分析器指出 [LibraryImport] 无法处理特定签名时,才使用 [DllImport]。

选择指南
场景使用
面向 .NET 7+ 的新代码[LibraryImport]
面向 .NET 6 或更早版本[DllImport]
SYSLIB1054 分析器指出不兼容[DllImport](附注释说明原因)
使用 Native AOT 发布[LibraryImport](完全兼容 AOT 的必要条件)
LibraryImport 声明
using System.Runtime.InteropServices;

public static partial class NativeApi
{
    [LibraryImport("mylib")]
    internal static partial int ProcessData(
        ReadOnlySpan<byte> input,
        int length);

    [LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
    internal static partial int OpenByName(string name);

    [LibraryImport("mylib", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static partial bool CloseResource(nint handle);
}

[LibraryImport] 的关键要求:

  • 方法必须在 partial 类中声明为 static partial
  • 必须通过 StringMarshalling 或在每个字符串参数上使用 [MarshalAs] 显式指定字符串封送方式(仅当存在字符串时需要)
  • 布尔返回类型需要显式添加 [return: MarshalAs(UnmanagedType.Bool)]
  • 直接支持 Span<T> 和 ReadOnlySpan<T> 参数——[DllImport] 不支持它们(请改用数组)
DllImport 声明(旧版)
using System.Runtime.InteropServices;

public static class NativeApiLegacy
{
    [DllImport("mylib", CharSet = CharSet.Unicode, SetLastError = true)]
    internal static extern int ProcessData(
        byte[] input,
        int length);

    [DllImport("mylib", SetLastError = true)]
    [return: MarshalAs(UnmanagedType.Bool)]
    internal static extern bool CloseResource(IntPtr handle);
}
将 DllImport 迁移到 LibraryImport

SYSLIB1054 分析器会建议将 [DllImport] 转换为 [LibraryImport],并提供代码修复。关键更改如下:

  1. 将 [DllImport] 替换为 [LibraryImport]
  2. 将 static extern 改为 static partial
  3. 将包含类设为 partial
  4. 将 CharSet 替换为 StringMarshalling
  5. 在适当情况下将 IntPtr 替换为 nint
  6. 为 bool 参数和返回值添加显式的 [MarshalAs]
// Before (DllImport)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
static extern IntPtr LoadLibrary(string lpLibFileName);

// After (LibraryImport)
[LibraryImport("kernel32.dll", StringMarshalling = StringMarshalling.Utf16,
    SetLastError = true)]
internal static partial nint LoadLibrary(string lpLibFileName);

平台特定的库名称

原生库名称因平台而异。使用 NativeLibrary.SetDllImportResolver 或条件编译来处理这一差异。

Windows

Windows 使用 .dll 文件。加载器会搜索应用程序目录、系统目录和 PATH。

// Windows library name includes .dll extension
[LibraryImport("sqlite3.dll")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);

Windows 也支持省略扩展名——加载器会自动追加 .dll:

[LibraryImport("sqlite3")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);
macOS 和 Linux

macOS 使用 .dylib 文件;Linux 使用 .so 文件。.NET 运行时会自动探测常见的名称变体(带或不带 lib 前缀,以及平台特定扩展名)。

// Use the logical name without extension -- .NET probes:
// libsqlite3.dylib (macOS), libsqlite3.so (Linux), sqlite3.dll (Windows)
[LibraryImport("libsqlite3")]
internal static partial int sqlite3_open(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string filename,
    out nint db);

.NET 对库名 "foo" 的探测顺序:

  1. foo(精确名称)
  2. foo.dll、foo.so、foo.dylib(平台扩展名)
  3. libfoo、libfoo.so、libfoo.dylib(lib 前缀和扩展名)
iOS

iOS 不允许在运行时加载动态库。原生代码必须静态链接到应用程序二进制文件中。使用 __Internal 作为库名,以调用链接到主可执行文件中的函数:

// Calls a function statically linked into the iOS app binary
[LibraryImport("__Internal")]
internal static partial int NativeFunction(int input);

对于 iOS,原生库必须编译为静态库(.a),并在 Xcode 构建阶段进行链接。MAUI 和 Xamarin 通过项目文件中的原生引用来处理此事:

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'ios'">
  <NativeReference Include="libs/libmynative.a">
    <Kind>Static</Kind>
    <ForceLoad>true</ForceLoad>
  </NativeReference>
</ItemGroup>
Android

Android 使用从应用的原生库目录加载的 .so 文件。在 P/Invoke 声明中,库名通常省略 lib 前缀和 .so 扩展名:

// Android loads libmynative.so from the APK's lib/<abi>/ directory
[LibraryImport("mynative")]
internal static partial int NativeFunction(int input);

在项目中为每个目标 ABI 包含平台特定的 .so 文件:

<ItemGroup Condition="$([MSBuild]::GetTargetPlatformIdentifier('$(TargetFramework)')) == 'android'">
  <AndroidNativeLibrary Include="libs/arm64-v8a/libmynative.so" Abi="arm64-v8a" />
  <AndroidNativeLibrary Include="libs/x86_64/libmynative.so" Abi="x86_64" />
</ItemGroup>
WASM

WebAssembly 不支持传统的 P/Invoke。在浏览器 WASM 中,无法通过 [LibraryImport] 或 [DllImport] 调用原生 C/C++ 代码。有关 JavaScript 互操作,请参阅 [skill:dotnet-aot-wasm]。


NativeLibrary.SetDllImportResolver

NativeLibrary.SetDllImportResolver(.NET Core 3.0+)可在运行时控制库解析。当静态名称探测不足以满足需求时,这是跨平台加载库的推荐方式。

using System.Reflection;
using System.Runtime.InteropServices;

// Register once at startup (per assembly)
NativeLibrary.SetDllImportResolver(
    Assembly.GetExecutingAssembly(),
    DllImportResolver);

static nint DllImportResolver(string libraryName, Assembly assembly, DllImportSearchPath? searchPath)
{
    if (libraryName == "mynativelib")
    {
        if (OperatingSystem.IsWindows())
            return NativeLibrary.Load("mynative.dll", assembly, searchPath);

        if (OperatingSystem.IsMacOS())
            return NativeLibrary.Load("libmynative.dylib", assembly, searchPath);

        if (OperatingSystem.IsLinux())
            return NativeLibrary.Load("libmynative.so.1", assembly, searchPath);
    }

    // Fall back to default resolution
    return nint.Zero;
}
DllImportResolver 的常见用例
场景需要解析器的原因
Linux 上带版本号的 .so(例如 libfoo.so.2)默认探测不会检查带版本号的名称
位于非标准路径中的库在运行时从自定义目录加载
按 RID 捆绑的原生库解析到 runtimes/<rid>/native/ 路径
在加载时检测功能尝试多个库名,并在必要时优雅回退
NativeLibrary API

NativeLibrary 类提供底层库管理功能:

// Load a library explicitly
nint handle = NativeLibrary.Load("mylib");

// Try to load without throwing
if (NativeLibrary.TryLoad("mylib", out nint h))
{
    // Get a function pointer by name
    nint funcPtr = NativeLibrary.GetExport(h, "my_function");

    // Or try without throwing
    if (NativeLibrary.TryGetExport(h, "my_function", out nint fp))
    {
        // Use function pointer
    }

    NativeLibrary.Free(h);
}

封送模式

结构体封送

传递给原生代码的结构体必须具有明确的内存布局。使用 [StructLayout] 控制布局和对齐方式。

using System.Runtime.InteropServices;

// Sequential layout -- fields laid out in declaration order
[StructLayout(LayoutKind.Sequential)]
public struct Point
{
    public int X;
    public int Y;
}

// Explicit layout -- fields at specific byte offsets (for unions)
[StructLayout(LayoutKind.Explicit)]
public struct ValueUnion
{
    [FieldOffset(0)] public int IntValue;
    [FieldOffset(0)] public float FloatValue;
    [FieldOffset(0)] public double DoubleValue;
}

// Sequential with packing -- override default alignment
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedHeader
{
    public byte Magic;
    public int Length;     // No padding before this field
    public short Version;
}

可直接封送的结构体(仅包含采用顺序或显式布局的基元值类型)会直接传递给原生代码,不会进行复制。不可直接封送的结构体需要进行封送,这会产生开销。

可直接封送的基元类型:byte、sbyte、short、ushort、int、uint、long、ulong、float、double、nint、nuint。

不可直接封送的类型: bool(默认封送为 4 字节的 BOOL)、char(取决于字符集)、string、由不可直接封送类型组成的数组。

字符串封送

明确指定字符串编码。切勿依赖默认封送行为。

// UTF-8 strings (most common for cross-platform C APIs)
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int ProcessText(string input);

// UTF-16 strings (Windows APIs)
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf16)]
internal static partial int ProcessTextW(string input);

// Per-parameter marshalling when methods mix encodings
[LibraryImport("mylib")]
internal static partial int MixedApi(
    [MarshalAs(UnmanagedType.LPUTF8Str)] string utf8Param,
    [MarshalAs(UnmanagedType.LPWStr)] string utf16Param);

对于输出字符串缓冲区,请使用来自 ArrayPool 的 char[] 或 byte[],而不是 StringBuilder:

[LibraryImport("mylib")]
internal static partial int GetName(
    [Out, MarshalAs(UnmanagedType.LPArray, SizeParamIndex = 1)] char[] buffer,
    int bufferSize);

// Usage
char[] buffer = ArrayPool<char>.Shared.Rent(256);
try
{
    int result = GetName(buffer, buffer.Length);
    string name = new string(buffer, 0, result);
}
finally
{
    ArrayPool<char>.Shared.Return(buffer);
}
函数指针回调

现代 .NET(.NET 5+)优先使用非托管函数指针,而非基于委托的回调,以获得更好的性能和 AOT 兼容性。

首选:使用 [UnmanagedCallersOnly] 的非托管函数指针

using System.Runtime.InteropServices;

// Native callback signature: int (*callback)(int value, void* context)
[LibraryImport("mylib")]
internal static unsafe partial void RegisterCallback(
    delegate* unmanaged[Cdecl]<int, nint, int> callback,
    nint context);

// Callback implementation
[UnmanagedCallersOnly(CallConvs = [typeof(System.Runtime.CompilerServices.CallConvCdecl)])]
static int MyCallback(int value, nint context)
{
    // Process value
    return 0;
}

// Registration
unsafe
{
    RegisterCallback(&MyCallback, nint.Zero);
}

替代方案:基于委托的回调(需要托管状态时使用)

// Define delegate matching native signature
[UnmanagedFunctionPointer(CallingConvention.Cdecl)]
delegate int NativeCallback(int value, nint context);

[LibraryImport("mylib")]
internal static partial void RegisterCallbackDelegate(
    NativeCallback callback,
    nint context);

// Usage -- prevent GC collection during native use
static NativeCallback? s_callback;

static void Setup()
{
    s_callback = new NativeCallback(MyManagedCallback);
    RegisterCallbackDelegate(s_callback, nint.Zero);
    // Keep s_callback alive as long as native code may call it
}

static int MyManagedCallback(int value, nint context)
{
    return value * 2;
}
使用 SafeHandle 管理资源生命周期

使用 SafeHandle 子类管理原生资源的生命周期,而不是直接使用 IntPtr/nint。这样可以防止资源泄漏和释放后使用错误。

using System.Runtime.InteropServices;
using Microsoft.Win32.SafeHandles;

// Custom SafeHandle for a native resource
public class NativeResourceHandle : SafeHandleZeroOrMinusOneIsInvalid
{
    private NativeResourceHandle() : base(ownsHandle: true) { }

    protected override bool ReleaseHandle()
    {
        NativeApi.CloseResource(handle);
        return true;
    }
}

public static partial class NativeApi
{
    [LibraryImport("mylib")]
    internal static partial NativeResourceHandle OpenResource(
        [MarshalAs(UnmanagedType.LPUTF8Str)] string name);

    [LibraryImport("mylib")]
    internal static partial void CloseResource(nint handle);

    [LibraryImport("mylib")]
    internal static partial int ReadResource(NativeResourceHandle handle,
        Span<byte> buffer, int count);
}

跨平台数据类型映射

谨慎地将 C/C++ 类型映射到 .NET 类型。某些 C 类型的大小取决于平台。

固定大小类型
C/C++ 类型.NET 类型大小
int8_t / charsbyte1 字节
uint8_t / unsigned charbyte1 字节
int16_t / shortshort2 字节
uint16_t / unsigned shortushort2 字节
int32_t / intint4 字节
uint32_t / unsigned intuint4 字节
int64_t / long longlong8 字节
uint64_t / unsigned long longulong8 字节
floatfloat4 字节
doubledouble8 字节
平台相关类型
C/C++ 类型.NET 类型备注
size_t / ptrdiff_tnint / nuint指针大小
void* / 指针类型nint 或 void*指针大小
long(C/C++)CLong(.NET 6+)Windows 上为 4 字节,Unix 64 位上为 8 字节
unsigned longCULong(.NET 6+)与 long 的平台差异相同
Windows BOOLint4 字节(不是 bool)
Windows BOOLEANbyte1 字节

不要在 C/C++ long 的互操作中使用 C# long——它们在 Unix 64 位上的大小不同。使用 CLong/CULong 实现可移植的互操作。


Agent 注意事项

  1. 没有充分理由时,不要在新的 .NET 7+ 代码中使用 [DllImport]。 使用 [LibraryImport],它会在编译时生成封送代码。只有在 SYSLIB1054 分析器指出不兼容时,才使用 [DllImport] 作为替代方案。
  2. 不要假定 bool 会封送为 1 字节。 默认情况下,.NET 会将 bool 封送为 4 字节的 Windows BOOL。对于 C _Bool/bool,请使用 [MarshalAs(UnmanagedType.U1)];对于 Windows BOOL,请显式使用 [MarshalAs(UnmanagedType.Bool)]。
  3. 不要使用 C# long 与 C/C++ long 进行互操作。 C long 在 Windows 上为 4 字节,在 64 位 Unix 上为 8 字节。跨平台使用时,请使用 CLong/CULong(.NET 6+)以确保正确性。
  4. 不要使用 StringBuilder 作为输出字符串缓冲区。 [LibraryImport] 完全不支持 StringBuilder,而使用 [DllImport] 时会分配多个中间副本。请改用来自 ArrayPool 的 char[] 或 byte[]。
  5. 不要将 [LibraryImport] 或 [DllImport] 用于 WASM。 WebAssembly 不支持传统的 P/Invoke。有关 WASM 中的 JavaScript 互操作,请参阅 [skill:dotnet-aot-wasm]。
  6. 不要在 iOS 上使用动态库加载。 iOS 禁止在运行时加载动态库。对于静态链接的本机代码,请将 "__Internal" 用作库名称。
  7. 不要在互操作结构体中使用 System.Delegate 字段。 请使用强类型委托或非托管函数指针(delegate* unmanaged)。无类型委托可能会在封送期间导致运行时不稳定。
  8. 不要忘记在本机代码使用委托期间保持其实例存活。 GC 可能会回收本机代码仍在引用的委托。请将委托存储在静态字段中,或在本机回调期间使用 GCHandle。

先决条件

  • 用于 [LibraryImport] 源生成的 .NET 7+ SDK
  • 用于 NativeLibrary API 的 .NET Core 3.0+
  • 为每个目标平台/架构编译的本机库
  • iOS:已通过 NativeReference 链接本机静态库的 Xcode
  • Android:适用于每个目标 ABI(arm64-v8a、x86_64)的本机 .so 文件

参考资料

评论

支持 Markdown