关于 dotnet-native-interop
为 .NET 开发者提供调用原生 C/C++ 库的 P/Invoke 指南,涵盖互操作声明、数据封送、跨平台库解析和回调模式。
主要功能
- 01比较 LibraryImport 与 DllImport
- 02处理结构体和字符串封送
- 03解析跨平台原生库名称
- 04使用函数指针和回调
- 05管理原生资源生命周期
适用场景
- 01从 .NET 调用原生 C/C++ API
- 02为多平台应用配置原生库加载
- 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],并提供代码修复。关键更改如下:
- 将
[DllImport]替换为[LibraryImport] - 将
static extern改为static partial - 将包含类设为
partial - 将
CharSet替换为StringMarshalling - 在适当情况下将
IntPtr替换为nint - 为
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" 的探测顺序:
foo(精确名称)foo.dll、foo.so、foo.dylib(平台扩展名)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 / char | sbyte | 1 字节 |
uint8_t / unsigned char | byte | 1 字节 |
int16_t / short | short | 2 字节 |
uint16_t / unsigned short | ushort | 2 字节 |
int32_t / int | int | 4 字节 |
uint32_t / unsigned int | uint | 4 字节 |
int64_t / long long | long | 8 字节 |
uint64_t / unsigned long long | ulong | 8 字节 |
float | float | 4 字节 |
double | double | 8 字节 |
平台相关类型
| C/C++ 类型 | .NET 类型 | 备注 |
|---|---|---|
size_t / ptrdiff_t | nint / nuint | 指针大小 |
void* / 指针类型 | nint 或 void* | 指针大小 |
long(C/C++) | CLong(.NET 6+) | Windows 上为 4 字节,Unix 64 位上为 8 字节 |
unsigned long | CULong(.NET 6+) | 与 long 的平台差异相同 |
Windows BOOL | int | 4 字节(不是 bool) |
Windows BOOLEAN | byte | 1 字节 |
不要在 C/C++ long 的互操作中使用 C# long——它们在 Unix 64 位上的大小不同。使用 CLong/CULong 实现可移植的互操作。
Agent 注意事项
- 没有充分理由时,不要在新的 .NET 7+ 代码中使用
[DllImport]。 使用[LibraryImport],它会在编译时生成封送代码。只有在 SYSLIB1054 分析器指出不兼容时,才使用[DllImport]作为替代方案。 - 不要假定
bool会封送为 1 字节。 默认情况下,.NET 会将bool封送为 4 字节的 WindowsBOOL。对于 C_Bool/bool,请使用[MarshalAs(UnmanagedType.U1)];对于 WindowsBOOL,请显式使用[MarshalAs(UnmanagedType.Bool)]。 - 不要使用 C#
long与 C/C++long进行互操作。 Clong在 Windows 上为 4 字节,在 64 位 Unix 上为 8 字节。跨平台使用时,请使用CLong/CULong(.NET 6+)以确保正确性。 - 不要使用
StringBuilder作为输出字符串缓冲区。[LibraryImport]完全不支持StringBuilder,而使用[DllImport]时会分配多个中间副本。请改用来自ArrayPool的char[]或byte[]。 - 不要将
[LibraryImport]或[DllImport]用于 WASM。 WebAssembly 不支持传统的 P/Invoke。有关 WASM 中的 JavaScript 互操作,请参阅 [skill:dotnet-aot-wasm]。 - 不要在 iOS 上使用动态库加载。 iOS 禁止在运行时加载动态库。对于静态链接的本机代码,请将
"__Internal"用作库名称。 - 不要在互操作结构体中使用
System.Delegate字段。 请使用强类型委托或非托管函数指针(delegate* unmanaged)。无类型委托可能会在封送期间导致运行时不稳定。 - 不要忘记在本机代码使用委托期间保持其实例存活。 GC 可能会回收本机代码仍在引用的委托。请将委托存储在静态字段中,或在本机回调期间使用
GCHandle。
先决条件
- 用于
[LibraryImport]源生成的 .NET 7+ SDK - 用于
NativeLibraryAPI 的 .NET Core 3.0+ - 为每个目标平台/架构编译的本机库
- iOS:已通过
NativeReference链接本机静态库的 Xcode - Android:适用于每个目标 ABI(arm64-v8a、x86_64)的本机
.so文件