dotnet-pinvoke
Compare original and translation side by side
🇺🇸
Original
English🇨🇳
Translation
Chinese.NET P/Invoke
.NET P/Invoke
Calling native code from .NET is powerful but unforgiving. Incorrect signatures, garbled strings, and leaked or freed memory are the most common sources of bugs — all can manifest as intermittent crashes, silent data corruption, or access violations far from the actual defect.
This skill covers both (available since .NET Framework 1.0) and (source-generated, .NET 7+). When targeting .NET Framework, always use . When targeting .NET 7+, prefer for new code. When native AOT is a requirement, is the only option.
DllImportLibraryImportDllImportLibraryImportLibraryImport从.NET调用原生代码功能强大但要求严苛。错误的签名、乱码字符串以及内存泄漏或错误释放是最常见的bug来源——这些问题可能表现为间歇性崩溃、无声的数据损坏,或者在远离实际缺陷的位置出现访问违规。
本指南涵盖(自.NET Framework 1.0起可用)和(源代码生成式,.NET 7+)两种方式。针对.NET Framework时,始终使用;针对.NET 7+时,新代码优先选择;若要求原生AOT,则是唯一选项。
DllImportLibraryImportDllImportLibraryImportLibraryImportWhen to Use This Skill
适用场景
- Writing a new or
[DllImport]declaration from a C/C++ header[LibraryImport] - Reviewing P/Invoke signatures for correctness (type sizes, calling conventions, string encoding)
- Wrapping an entire C library for use from .NET
- Debugging ,
AccessViolationException, or silent data corruption at the native boundaryDllNotFoundException - Migrating declarations to
DllImportfor AOT/trimming compatibilityLibraryImport - Diagnosing memory leaks or heap corruption involving native handles or buffers
- 根据C/C++头文件编写新的或
[DllImport]声明[LibraryImport] - 审查P/Invoke签名的正确性(类型大小、调用约定、字符串编码)
- 封装整个C库以供.NET使用
- 调试托管/原生边界处的、
AccessViolationException或无声数据损坏问题DllNotFoundException - 将声明迁移至
DllImport以兼容AOT/裁剪LibraryImport - 诊断涉及原生句柄或缓冲区的内存泄漏或堆损坏问题
Stop Signals
注意事项
- Single function? Map the signature (Steps 1-3), handle strings/memory only if relevant, skip tooling and migration sections.
- Don't migrate existing to
DllImportunless the user asks or AOT/trimming is an explicit requirement.LibraryImport - Don't recommend CsWin32 unless the target is specifically Win32 APIs.
- Don't generate callbacks (Step 8) unless the native API requires function pointers.
- Review request? Use the validation checklist — don't rewrite working code.
- 仅单个函数? 只需映射签名(步骤1-3),仅在相关时处理字符串/内存,跳过工具和迁移章节。
- 不要主动迁移现有至
DllImport,除非用户明确要求或AOT/裁剪是硬性需求。LibraryImport - 不要推荐CsWin32,除非目标是Win32 API。
- 不要生成回调(步骤8),除非原生API需要函数指针。
- 代码审查请求? 使用验证清单——不要重写可正常工作的代码。
Inputs
输入信息
| Input | Required | Description |
|---|---|---|
| Native header or documentation | Yes | C/C++ function signatures, struct definitions, calling conventions |
| Target framework | Yes | Determines whether to use |
| Target platforms | Recommended | Affects type sizes ( |
| Memory ownership contract | Yes | Who allocates and who frees each buffer or handle |
Agent behavior: When documentation and native headers diverge, always trust the header. Online documentation (including official Win32 API docs) frequently omits or simplifies details about types, calling conventions, and struct layout that are critical for correct P/Invoke signatures.
| 输入项 | 是否必填 | 描述 |
|---|---|---|
| 原生头文件或文档 | 是 | C/C++函数签名、结构体定义、调用约定 |
| 目标框架 | 是 | 决定使用 |
| 目标平台 | 推荐 | 影响类型大小( |
| 内存所有权约定 | 是 | 明确每个缓冲区或句柄的分配方和释放方 |
Agent行为: 当文档与原生头文件存在差异时,始终以头文件为准。在线文档(包括官方Win32 API文档)经常省略或简化类型、调用约定和结构体布局等关键细节,而这些细节对正确编写P/Invoke签名至关重要。
Workflow
工作流程
Step 1: Choose DllImport or LibraryImport
步骤1:选择DllImport或LibraryImport
| Aspect | | |
|---|---|---|
| Mechanism | Runtime marshalling | Source generator (compile-time) |
| AOT / Trim safe | No | Yes |
| String marshalling | | |
| Error handling | | |
| Availability | .NET Framework 1.0+ | .NET 7+ only |
| 维度 | | |
|---|---|---|
| 实现机制 | 运行时封送 | 源代码生成器(编译时) |
| AOT/裁剪安全 | 否 | 是 |
| 字符串封送 | | |
| 错误处理 | | |
| 可用版本 | .NET Framework 1.0+ | 仅.NET 7+ |
Step 2: Map Native Types to .NET Types
步骤2:将原生类型映射为.NET类型
The most dangerous mappings — these cause the majority of bugs:
| C / Win32 Type | .NET Type | Why |
|---|---|---|
| | 32-bit on Windows, 64-bit on 64-bit Unix. With |
| | Pointer-sized. Use |
| | Not |
| | Must specify 1-byte marshal |
| | Prefer over raw |
| | UTF-16 on Windows (lowest cost for |
| | Must specify encoding (ANSI or UTF-8). Always requires marshalling cost for |
For the complete type mapping table, struct layout, and blittable type rules, see references/type-mapping.md.
❌ NEVER useorintfor Clong— it's 32-bit on Windows, 64-bit on Unix. Always uselong. ❌ NEVER useCLongforulong— causes stack corruption on 32-bit. Usesize_tornuint. ❌ NEVER useUIntPtrwithoutbool— the default marshal size is wrong.MarshalAs
以下是最容易出错的映射——这些是大多数bug的根源:
| C / Win32类型 | .NET类型 | 原因 |
|---|---|---|
| | Windows上为32位,64位Unix上为64位。使用 |
| | 指针大小。.NET 8+使用 |
| | 不要用 |
| | 必须指定1字节封送 |
| | 优先于原生 |
| | Windows上为UTF-16( |
| | 必须指定编码(ANSI或UTF-8)。 |
完整的类型映射表、结构体布局和 blittable 类型规则,请参阅references/type-mapping.md。
❌ 绝不要用或int对应C的long——Windows上是32位,Unix上是64位。始终使用long。 ❌ 绝不要用CLong对应ulong——会导致32位平台上的栈损坏。使用size_t或nuint。 ❌ 绝不要不添加UIntPtr就使用MarshalAs——默认封送大小错误。bool
Step 3: Write the Declaration
步骤3:编写声明
Given a C header:
c
int32_t process_records(const Record* records, size_t count, uint32_t* out_processed);DllImport:
csharp
[DllImport("mylib")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);LibraryImport:
csharp
[LibraryImport("mylib")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);Calling conventions only need to be specified when targeting Windows x86 (32-bit), where and differ. On x64, ARM, and ARM64, there is a single calling convention and the attribute is unnecessary.
CdeclStdCallAgent behavior: If you detect that Windows x86 is a target — through project properties (e.g., ), runtime identifiers (e.g., ), build scripts, comments, or developer instructions — flag this to the developer and recommend explicit calling conventions on all P/Invoke declarations.
<PlatformTarget>x86</PlatformTarget>win-x86csharp
// DllImport (x86 targets)
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
// LibraryImport (x86 targets)
[LibraryImport("mylib")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]If the managed method name differs from the native export name, specify to avoid :
EntryPointEntryPointNotFoundExceptioncsharp
// DllImport
[DllImport("mylib", EntryPoint = "process_records")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);
// LibraryImport
[LibraryImport("mylib", EntryPoint = "process_records")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);给定C头文件:
c
int32_t process_records(const Record* records, size_t count, uint32_t* out_processed);DllImport实现:
csharp
[DllImport("mylib")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);LibraryImport实现:
csharp
[LibraryImport("mylib")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);仅在针对Windows x86(32位)时需要指定调用约定,因为该平台上和存在差异。在x64、ARM和ARM64平台上,只有一种调用约定,无需指定该属性。
CdeclStdCallAgent行为: 如果检测到目标为Windows x86——通过项目属性(如)、运行时标识符(如)、构建脚本、注释或开发者指令——需向开发者标记此情况,并建议在所有P/Invoke声明上显式指定调用约定。
<PlatformTarget>x86</PlatformTarget>win-x86csharp
// DllImport (x86目标)
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
// LibraryImport (x86目标)
[LibraryImport("mylib")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]如果托管方法名称与原生导出名称不同,需指定以避免:
EntryPointEntryPointNotFoundExceptioncsharp
// DllImport
[DllImport("mylib", EntryPoint = "process_records")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);
// LibraryImport
[LibraryImport("mylib", EntryPoint = "process_records")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);Step 4: Handle Strings Correctly
步骤4:正确处理字符串
- Know what encoding the native function expects. There is no safe default.
- Windows APIs: Always call the (UTF-16) variant. The
Wvariant needs a specific reason and explicit ANSI encoding.A - Cross-platform C libraries: Usually expect UTF-8.
- Specify encoding explicitly. Never rely on .
CharSet.Auto - Never introduce for output buffers.
StringBuilder
❌ NEVER rely onor omit string encoding — there is no safe default.CharSet.Auto
csharp
// DllImport — Windows API (UTF-16)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// DllImport — Cross-platform C library (UTF-8)
[DllImport("mylib")]
private static extern int SetName(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name);
// LibraryImport — UTF-16
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
SetLastPInvokeError = true)]
internal static partial int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// LibraryImport — UTF-8
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int SetName(string name);String lifetime warning: Marshalled strings are freed after the call returns. If native code stores the pointer (instead of copying), the lifetime must be manually managed. On Windows or .NET Framework, / is the first choice for cross-boundary ownership; on non-Windows targets, use APIs. The library may have its own allocator that must be used instead.
CoTaskMemAllocCoTaskMemFreeNativeMemory- 明确原生函数期望的编码,没有安全的默认值。
- Windows API: 始终调用(UTF-16)变体。只有在有特定理由时才使用
W变体,并显式指定ANSI编码。A - 跨平台C库: 通常期望UTF-8编码。
- 显式指定编码,绝不要依赖。
CharSet.Auto - 不要为输出缓冲区使用。
StringBuilder
❌ 绝不要依赖或省略字符串编码——没有安全的默认值。CharSet.Auto
csharp
// DllImport — Windows API (UTF-16)
[DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
private static extern int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// DllImport — 跨平台C库 (UTF-8)
[DllImport("mylib")]
private static extern int SetName(
[MarshalAs(UnmanagedType.LPUTF8Str)] string name);
// LibraryImport — UTF-16
[LibraryImport("kernel32", StringMarshalling = StringMarshalling.Utf16,
SetLastPInvokeError = true)]
internal static partial int GetModuleFileNameW(
IntPtr hModule, [Out] char[] filename, int size);
// LibraryImport — UTF-8
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
internal static partial int SetName(string name);字符串生命周期警告: 封送后的字符串会在调用返回后被释放。如果原生代码存储指针(而非复制内容),则必须手动管理生命周期。在Windows或.NET Framework上,/是跨边界所有权管理的首选方式;在非Windows目标上,使用 API。部分库可能有自己的分配器,此时必须使用该库的分配器。
CoTaskMemAllocCoTaskMemFreeNativeMemoryStep 5: Establish Memory Ownership
步骤5:明确内存所有权
When memory crosses the boundary, exactly one side must own it — and both sides must agree.
❌ NEVER free with a mismatched allocator —onMarshal.FreeHGlobal'd memory is heap corruption.malloc
Model 1 — Caller allocates, caller frees (safest):
csharp
[LibraryImport("mylib")]
private static partial int GetName(
Span<byte> buffer, nuint bufferSize, out nuint actualSize);
public static string GetName()
{
Span<byte> buffer = stackalloc byte[256];
int result = GetName(buffer, (nuint)buffer.Length, out nuint actualSize);
if (result != 0) throw new InvalidOperationException($"Failed: {result}");
return Encoding.UTF8.GetString(buffer[..(int)actualSize]);
}Model 2 — Callee allocates, caller frees (common in Win32):
csharp
[LibraryImport("mylib")]
private static partial IntPtr GetVersion();
[LibraryImport("mylib")]
private static partial void FreeString(IntPtr s);
public static string GetVersion()
{
IntPtr ptr = GetVersion();
try { return Marshal.PtrToStringUTF8(ptr) ?? throw new InvalidOperationException(); }
finally { FreeString(ptr); } // Must use the library's own free function
}Critical rule: Always free with the matching allocator. Never use or on 'd memory.
Marshal.FreeHGlobalMarshal.FreeCoTaskMemmallocModel 3 — Handle-based (callee allocates, callee frees): Use (see Step 6).
SafeHandlePinning managed objects — when native code stores the pointer or runs asynchronously:
csharp
// Synchronous: use fixed
public static unsafe void ProcessSync(byte[] data)
{
fixed (byte* ptr = data) { ProcessData(ptr, (nuint)data.Length); }
}
// Asynchronous: use GCHandle
var gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
// Must keep pinned until native processing completes, then call gcHandle.Free()当内存跨边界传递时,必须由且仅由一方拥有所有权——双方必须达成一致。
❌ 绝不要使用不匹配的分配器释放内存——用释放Marshal.FreeHGlobal分配的内存会导致堆损坏。malloc
模式1 — 调用方分配,调用方释放(最安全):
csharp
[LibraryImport("mylib")]
private static partial int GetName(
Span<byte> buffer, nuint bufferSize, out nuint actualSize);
public static string GetName()
{
Span<byte> buffer = stackalloc byte[256];
int result = GetName(buffer, (nuint)buffer.Length, out nuint actualSize);
if (result != 0) throw new InvalidOperationException($"调用失败: {result}");
return Encoding.UTF8.GetString(buffer[..(int)actualSize]);
}模式2 — 被调用方分配,调用方释放(Win32中常见):
csharp
[LibraryImport("mylib")]
private static partial IntPtr GetVersion();
[LibraryImport("mylib")]
private static partial void FreeString(IntPtr s);
public static string GetVersion()
{
IntPtr ptr = GetVersion();
try { return Marshal.PtrToStringUTF8(ptr) ?? throw new InvalidOperationException(); }
finally { FreeString(ptr); } // 必须使用库自身的释放函数
}关键规则: 始终使用匹配的分配器释放内存。绝不要用或释放分配的内存。
Marshal.FreeHGlobalMarshal.FreeCoTaskMemmalloc模式3 — 句柄式(被调用方分配,被调用方释放): 使用(见步骤6)。
SafeHandle托管对象固定——当原生代码存储指针或异步运行时:
csharp
// 同步场景:使用fixed
public static unsafe void ProcessSync(byte[] data)
{
fixed (byte* ptr = data) { ProcessData(ptr, (nuint)data.Length); }
}
// 异步场景:使用GCHandle
var gcHandle = GCHandle.Alloc(data, GCHandleType.Pinned);
// 必须保持固定直到原生处理完成,然后调用gcHandle.Free()Step 6: Use SafeHandle for Native Handles
步骤6:使用SafeHandle管理原生句柄
Raw leaks on exceptions and has no double-free protection. is non-negotiable.
IntPtrSafeHandlecsharp
internal sealed class MyLibHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// Required by the marshalling infrastructure to instantiate the handle.
// Do not remove — there are no direct callers.
private MyLibHandle() : base(ownsHandle: true) { }
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
private static partial MyLibHandle CreateHandle(string config);
[LibraryImport("mylib")]
private static partial int UseHandle(MyLibHandle h, ReadOnlySpan<byte> data, nuint len);
[LibraryImport("mylib")]
private static partial void DestroyHandle(IntPtr h);
protected override bool ReleaseHandle() { DestroyHandle(handle); return true; }
public static MyLibHandle Create(string config)
{
var h = CreateHandle(config);
if (h.IsInvalid) throw new InvalidOperationException("Failed to create handle");
return h;
}
public int Use(ReadOnlySpan<byte> data) => UseHandle(this, data, (nuint)data.Length);
}
// Usage: SafeHandle is IDisposable
using var handle = MyLibHandle.Create("config=value");
int result = handle.Use(myData);原生在异常时会泄漏,且无双重释放保护。是必不可少的。
IntPtrSafeHandlecsharp
internal sealed class MyLibHandle : SafeHandleZeroOrMinusOneIsInvalid
{
// 封送基础设施需要此构造函数来实例化句柄。
// 不要删除——没有直接调用方。
private MyLibHandle() : base(ownsHandle: true) { }
[LibraryImport("mylib", StringMarshalling = StringMarshalling.Utf8)]
private static partial MyLibHandle CreateHandle(string config);
[LibraryImport("mylib")]
private static partial int UseHandle(MyLibHandle h, ReadOnlySpan<byte> data, nuint len);
[LibraryImport("mylib")]
private static partial void DestroyHandle(IntPtr h);
protected override bool ReleaseHandle() { DestroyHandle(handle); return true; }
public static MyLibHandle Create(string config)
{
var h = CreateHandle(config);
if (h.IsInvalid) throw new InvalidOperationException("创建句柄失败");
return h;
}
public int Use(ReadOnlySpan<byte> data) => UseHandle(this, data, (nuint)data.Length);
}
// 使用方式:SafeHandle实现了IDisposable
using var handle = MyLibHandle.Create("config=value");
int result = handle.Use(myData);Step 7: Handle Errors
步骤7:错误处理
csharp
// Win32 APIs — check SetLastError
[LibraryImport("kernel32", SetLastPInvokeError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseHandle(IntPtr hObject);
if (!CloseHandle(handle))
throw new Win32Exception(Marshal.GetLastPInvokeError());
// HRESULT APIs
int hr = NativeDoWork(context);
Marshal.ThrowExceptionForHR(hr);csharp
// Win32 API — 检查SetLastError
[LibraryImport("kernel32", SetLastPInvokeError = true)]
[return: MarshalAs(UnmanagedType.Bool)]
internal static partial bool CloseHandle(IntPtr hObject);
if (!CloseHandle(handle))
throw new Win32Exception(Marshal.GetLastPInvokeError());
// HRESULT API
int hr = NativeDoWork(context);
Marshal.ThrowExceptionForHR(hr);Step 8: Handle Callbacks (if needed)
步骤8:处理回调(如有需要)
Preferred (.NET 8+): — avoids delegates entirely, no GC lifetime risk:
UnmanagedCallersOnlycsharp
[UnmanagedCallersOnly]
private static void LogCallback(int level, IntPtr message)
{
string msg = Marshal.PtrToStringUTF8(message) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
}
[LibraryImport("mylib")]
private static unsafe partial void SetLogCallback(
delegate* unmanaged<int, IntPtr, void> cb);
unsafe { SetLogCallback(&LogCallback); }The method must be , must not throw exceptions back to native code, and can only use blittable parameter types.
staticFallback (older TFMs or when instance state is needed): delegate with rooting
csharp
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // Only needed on Windows x86
private delegate void LogCallbackDelegate(int level, IntPtr message);
// CRITICAL: prevent delegate from being garbage collected
private static LogCallbackDelegate? s_logCallback;
public static void EnableLogging(Action<int, string> handler)
{
s_logCallback = (level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
handler(level, msg);
};
SetLogCallback(s_logCallback);
}If native code stores the function pointer, the delegate must stay rooted for its entire lifetime. A collected delegate means a crash.
GC.KeepAliveMarshal.GetFunctionPointerForDelegateGC.KeepAlivecsharp
var callback = new LogCallbackDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // prevent collection — fnPtr does not root the delegate推荐方式(.NET 8+):——完全避免委托,无GC生命周期风险:
UnmanagedCallersOnlycsharp
[UnmanagedCallersOnly]
private static void LogCallback(int level, IntPtr message)
{
string msg = Marshal.PtrToStringUTF8(message) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
}
[LibraryImport("mylib")]
private static unsafe partial void SetLogCallback(
delegate* unmanaged<int, IntPtr, void> cb);
unsafe { SetLogCallback(&LogCallback); }该方法必须为,绝不要向原生代码抛出异常,且只能使用blittable参数类型。
static兼容方案(旧版TFM或需要实例状态时):带根引用的委托
csharp
[UnmanagedFunctionPointer(CallingConvention.Cdecl)] // 仅Windows x86需要
private delegate void LogCallbackDelegate(int level, IntPtr message);
// 关键:防止委托被垃圾回收
private static LogCallbackDelegate? s_logCallback;
public static void EnableLogging(Action<int, string> handler)
{
s_logCallback = (level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
handler(level, msg);
};
SetLogCallback(s_logCallback);
}如果原生代码存储函数指针,委托必须在整个生命周期内保持根引用。委托被回收会导致崩溃。
短生命周期回调使用: 当使用将委托转换为函数指针时,GC不会跟踪指针与委托之间的关系。使用防止委托在原生调用完成前被回收:
GC.KeepAliveMarshal.GetFunctionPointerForDelegateGC.KeepAlivecsharp
var callback = new LogCallbackDelegate((level, msgPtr) =>
{
string msg = Marshal.PtrToStringUTF8(msgPtr) ?? string.Empty;
Console.WriteLine($"[{level}] {msg}");
});
IntPtr fnPtr = Marshal.GetFunctionPointerForDelegate(callback);
NativeUsesCallback(fnPtr);
GC.KeepAlive(callback); // 防止回收——fnPtr不会根住委托Cross-Platform Library Loading
跨平台库加载
Use for complex scenarios, or conditional compilation for simple cases. Use / for C /. Note: / with requires .
NativeLibrary.SetDllImportResolverCLongCULonglongunsigned longCLongCULongLibraryImport[assembly: DisableRuntimeMarshalling]csharp
// Simple: conditional compilation
// WINDOWS, LINUX, MACOS are predefined only when targeting an OS-specific TFM
// (e.g., net8.0-windows). For portable TFMs (e.g., net8.0), these symbols are
// not defined — use the runtime resolver approach below instead.
#if WINDOWS
private const string LibName = "mylib.dll";
#elif LINUX
private const string LibName = "libmylib.so";
#elif MACOS
private const string LibName = "libmylib.dylib";
#endif
// Complex: runtime resolver
NativeLibrary.SetDllImportResolver(typeof(MyLib).Assembly,
(name, assembly, searchPath) =>
{
if (name != "mylib") return IntPtr.Zero;
string libName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "mylib.dll"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "libmylib.dylib" : "libmylib.so";
NativeLibrary.TryLoad(libName, assembly, searchPath, out var handle);
return handle;
});复杂场景使用,简单场景使用条件编译。C的/使用/。注意:搭配/需要添加。
NativeLibrary.SetDllImportResolverlongunsigned longCLongCULongLibraryImportCLongCULong[assembly: DisableRuntimeMarshalling]csharp
// 简单方式:条件编译
// WINDOWS、LINUX、MACOS仅在针对特定OS的TFM时预定义
// (如net8.0-windows)。对于可移植TFM(如net8.0),这些符号未定义——请改用下面的运行时解析器方式。
#if WINDOWS
private const string LibName = "mylib.dll";
#elif LINUX
private const string LibName = "libmylib.so";
#elif MACOS
private const string LibName = "libmylib.dylib";
#endif
// 复杂方式:运行时解析器
NativeLibrary.SetDllImportResolver(typeof(MyLib).Assembly,
(name, assembly, searchPath) =>
{
if (name != "mylib") return IntPtr.Zero;
string libName = RuntimeInformation.IsOSPlatform(OSPlatform.Windows)
? "mylib.dll"
: RuntimeInformation.IsOSPlatform(OSPlatform.OSX)
? "libmylib.dylib" : "libmylib.so";
NativeLibrary.TryLoad(libName, assembly, searchPath, out var handle);
return handle;
});Migrating DllImport to LibraryImport
从DllImport迁移至LibraryImport
For codebases targeting .NET 7+, migrating provides AOT compatibility and trimming safety.
- Add to the containing class and make the method
partialstatic partial - Replace with
[DllImport][LibraryImport] - Replace with
CharSetStringMarshalling - Replace with
SetLastError = trueSetLastPInvokeError = true - Remove unless targeting Windows x86
CallingConvention - Build and fix –
SYSLIB1054analyzer warningsSYSLIB1057
Enable the interop analyzers:
xml
<PropertyGroup>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>针对.NET 7+的代码库,迁移可获得AOT兼容性和裁剪安全性。
- 为包含类添加修饰符,并将方法改为
partialstatic partial - 将替换为
[DllImport][LibraryImport] - 将替换为
CharSetStringMarshalling - 将替换为
SetLastError = trueSetLastPInvokeError = true - 除非针对Windows x86,否则移除
CallingConvention - 构建并修复–
SYSLIB1054分析器警告SYSLIB1057
启用互操作分析器:
xml
<PropertyGroup>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>Tooling
工具推荐
CsWin32 (Win32 APIs)
CsWin32(Win32 API)
For Win32 P/Invoke, prefer Microsoft.Windows.CsWin32 over hand-written signatures. It source-generates correct declarations from metadata. Add a listing the APIs you need:
NativeMethods.txtbash
dotnet add package Microsoft.Windows.CsWin32针对Win32 P/Invoke,优先使用Microsoft.Windows.CsWin32而非手写签名。它通过元数据源代码生成正确的声明。添加列出所需API:
NativeMethods.txtbash
dotnet add package Microsoft.Windows.CsWin32CsWinRT (WinRT APIs)
CsWinRT(WinRT API)
For WinRT interop, use Microsoft.Windows.CsWinRT to generate .NET projections from files.
.winmd针对WinRT互操作,使用Microsoft.Windows.CsWinRT从文件生成.NET投影。
.winmdObjective Sharpie (Objective-C APIs)
Objective Sharpie(Objective-C API)
For binding Objective-C libraries (macOS/iOS), use Objective Sharpie to generate initial P/Invoke and binding definitions from Objective-C headers.
针对绑定Objective-C库(macOS/iOS),使用Objective Sharpie从Objective-C头文件生成初始P/Invoke和绑定定义。
Validation
验证
Review checklist
审查清单
- Every signature matches the native header exactly (types, sizes)
- Calling convention specified if targeting Windows x86; omitted otherwise
- String encoding is explicit — no reliance on defaults or
CharSet.Auto - Memory ownership is documented and matched (who allocates, who frees, with what)
- used for all native handles (no raw
SafeHandleescaping the interop layer)IntPtr - Delegates passed as callbacks are rooted to prevent GC collection
- /
SetLastErrorset for APIs that use OS error codesSetLastPInvokeError - Struct layout matches native (packing, alignment, field order)
- /
CLongused for CCULong/longin cross-platform codeunsigned long - If using /
CLongwithCULong,LibraryImportis applied[assembly: DisableRuntimeMarshalling] - No without explicit
bool— always specifyMarshalAs(4-byte) orUnmanagedType.Bool(1-byte) to ensure normalization across the language boundary.UnmanagedType.U1
- 每个签名与原生头文件完全匹配(类型、大小)
- 针对Windows x86时指定调用约定,否则省略
- 字符串编码显式指定——不依赖默认值或
CharSet.Auto - 内存所有权已记录并匹配(分配方、释放方、使用的分配器)
- 所有原生句柄使用(无原生
SafeHandle泄漏到互操作层外)IntPtr - 作为回调传递的委托已根引用以防止GC回收
- 针对使用系统错误码的API设置/
SetLastErrorSetLastPInvokeError - 结构体布局与原生匹配(打包方式、对齐方式、字段顺序)
- 跨平台代码中C的/
long使用unsigned long/CLongCULong - 若搭配
LibraryImport/CLong,已添加CULong[assembly: DisableRuntimeMarshalling] - 所有都带有显式
bool——始终指定MarshalAs(4字节)或UnmanagedType.Bool(1字节),确保跨语言边界的一致性。UnmanagedType.U1
Runnable validation steps
可执行的验证步骤
- Build with interop analyzers enabled — confirm zero –
SYSLIB1054warnings:SYSLIB1057xml<EnableTrimAnalyzer>true</EnableTrimAnalyzer> <EnableAotAnalyzer>true</EnableAotAnalyzer> - Verify struct sizes match — for every struct crossing the boundary, assert equals the native
Marshal.SizeOf<T>()sizeof - Round-trip test — call the native function with known inputs and verify expected outputs
- Test with non-ASCII strings — pass strings containing characters outside the ASCII range to confirm encoding is correct
- 启用互操作分析器构建——确认无–
SYSLIB1054警告:SYSLIB1057xml<EnableTrimAnalyzer>true</EnableTrimAnalyzer> <EnableAotAnalyzer>true</EnableAotAnalyzer> - 验证结构体大小匹配——每个跨边界的结构体,断言等于原生
Marshal.SizeOf<T>()sizeof - 往返测试——使用已知输入调用原生函数并验证预期输出
- 非ASCII字符串测试——传递包含ASCII范围外字符的字符串,确认编码正确
Reference Files
参考文件
- references/type-mapping.md — Complete native-to-.NET type mapping table, struct layout patterns, blittable type rules. Load when encountering types not covered in Step 2 above, or when working with struct layout or blittable type questions.
- references/diagnostics.md — Common pitfalls, failure modes and recovery, debugging approach, external resources. Load when debugging an existing P/Invoke failure or reviewing interop code for correctness issues.
- references/type-mapping.md — 完整的原生到.NET类型映射表、结构体布局模式、blittable类型规则。加载时机:遇到步骤2未覆盖的类型,或处理结构体布局、blittable类型相关问题时。
- references/diagnostics.md — 常见陷阱、故障模式与恢复、调试方法、外部资源。加载时机:调试现有P/Invoke失败问题,或审查互操作代码的正确性时。