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
DllImport
(available since .NET Framework 1.0) and
LibraryImport
(source-generated, .NET 7+). When targeting .NET Framework, always use
DllImport
. When targeting .NET 7+, prefer
LibraryImport
for new code. When native AOT is a requirement,
LibraryImport
is the only option.
从.NET调用原生代码功能强大但要求严苛。错误的签名、乱码字符串以及内存泄漏或错误释放是最常见的bug来源——这些问题可能表现为间歇性崩溃、无声的数据损坏,或者在远离实际缺陷的位置出现访问违规。
本指南涵盖
DllImport
(自.NET Framework 1.0起可用)和
LibraryImport
(源代码生成式,.NET 7+)两种方式。针对.NET Framework时,始终使用
DllImport
;针对.NET 7+时,新代码优先选择
LibraryImport
;若要求原生AOT,则
LibraryImport
是唯一选项。

When to Use This Skill

适用场景

  • Writing a new
    [DllImport]
    or
    [LibraryImport]
    declaration from a C/C++ header
  • Reviewing P/Invoke signatures for correctness (type sizes, calling conventions, string encoding)
  • Wrapping an entire C library for use from .NET
  • Debugging
    AccessViolationException
    ,
    DllNotFoundException
    , or silent data corruption at the native boundary
  • Migrating
    DllImport
    declarations to
    LibraryImport
    for AOT/trimming compatibility
  • Diagnosing memory leaks or heap corruption involving native handles or buffers
  • 根据C/C++头文件编写新的
    [DllImport]
    [LibraryImport]
    声明
  • 审查P/Invoke签名的正确性(类型大小、调用约定、字符串编码)
  • 封装整个C库以供.NET使用
  • 调试托管/原生边界处的
    AccessViolationException
    DllNotFoundException
    或无声数据损坏问题
  • DllImport
    声明迁移至
    LibraryImport
    以兼容AOT/裁剪
  • 诊断涉及原生句柄或缓冲区的内存泄漏或堆损坏问题

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
    DllImport
    to
    LibraryImport
    unless the user asks or AOT/trimming is an explicit requirement.
  • 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
    LibraryImport
    ,除非用户明确要求或AOT/裁剪是硬性需求。
  • 不要推荐CsWin32,除非目标是Win32 API。
  • 不要生成回调(步骤8),除非原生API需要函数指针。
  • 代码审查请求? 使用验证清单——不要重写可正常工作的代码。

Inputs

输入信息

InputRequiredDescription
Native header or documentationYesC/C++ function signatures, struct definitions, calling conventions
Target frameworkYesDetermines whether to use
DllImport
or
LibraryImport
Target platformsRecommendedAffects type sizes (
long
,
size_t
) and library naming
Memory ownership contractYesWho 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++函数签名、结构体定义、调用约定
目标框架决定使用
DllImport
还是
LibraryImport
目标平台推荐影响类型大小(
long
size_t
)和库命名
内存所有权约定明确每个缓冲区或句柄的分配方和释放方
Agent行为: 当文档与原生头文件存在差异时,始终以头文件为准。在线文档(包括官方Win32 API文档)经常省略或简化类型、调用约定和结构体布局等关键细节,而这些细节对正确编写P/Invoke签名至关重要。

Workflow

工作流程

Step 1: Choose DllImport or LibraryImport

步骤1:选择DllImport或LibraryImport

Aspect
DllImport
LibraryImport
(.NET 7+)
MechanismRuntime marshallingSource generator (compile-time)
AOT / Trim safeNoYes
String marshalling
CharSet
enum
StringMarshalling
enum
Error handling
SetLastError
SetLastPInvokeError
Availability.NET Framework 1.0+.NET 7+ only
维度
DllImport
LibraryImport
(.NET 7+)
实现机制运行时封送源代码生成器(编译时)
AOT/裁剪安全
字符串封送
CharSet
枚举
StringMarshalling
枚举
错误处理
SetLastError
SetLastPInvokeError
可用版本.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 TypeWhy
long
CLong
32-bit on Windows, 64-bit on 64-bit Unix. With
LibraryImport
, requires
[assembly: DisableRuntimeMarshalling]
size_t
nuint
/
UIntPtr
Pointer-sized. Use
nuint
on .NET 8+ and
UIntPtr
on earlier .NET. Never use
ulong
BOOL
(Win32)
int
Not
bool
— Win32
BOOL
is 4 bytes
bool
(C99)
[MarshalAs(UnmanagedType.U1)] bool
Must specify 1-byte marshal
HANDLE
,
HWND
SafeHandle
Prefer over raw
IntPtr
LPWSTR
/
wchar_t*
string
UTF-16 on Windows (lowest cost for
in
strings). Avoid in cross-platform code —
wchar_t
width is compiler-defined (typically UTF-32 on non-Windows)
LPSTR
/
char*
string
Must specify encoding (ANSI or UTF-8). Always requires marshalling cost for
in
parameters
For the complete type mapping table, struct layout, and blittable type rules, see references/type-mapping.md.
NEVER use
int
or
long
for C
long
— it's 32-bit on Windows, 64-bit on Unix. Always use
CLong
. ❌ NEVER use
ulong
for
size_t
— causes stack corruption on 32-bit. Use
nuint
or
UIntPtr
. ❌ NEVER use
bool
without
MarshalAs
— the default marshal size is wrong.
以下是最容易出错的映射——这些是大多数bug的根源:
C / Win32类型.NET类型原因
long
CLong
Windows上为32位,64位Unix上为64位。使用
LibraryImport
时,需要添加
[assembly: DisableRuntimeMarshalling]
size_t
nuint
/
UIntPtr
指针大小。.NET 8+使用
nuint
,早期.NET版本使用
UIntPtr
。绝不要使用
ulong
BOOL
(Win32)
int
不要用
bool
——Win32
BOOL
为4字节
bool
(C99)
[MarshalAs(UnmanagedType.U1)] bool
必须指定1字节封送
HANDLE
,
HWND
SafeHandle
优先于原生
IntPtr
LPWSTR
/
wchar_t*
string
Windows上为UTF-16(
in
字符串的最低成本方案)。跨平台代码中避免使用——
wchar_t
宽度由编译器定义(非Windows平台通常为UTF-32)
LPSTR
/
char*
string
必须指定编码(ANSI或UTF-8)。
in
参数始终会产生封送成本
完整的类型映射表、结构体布局和 blittable 类型规则,请参阅references/type-mapping.md
绝不要
int
long
对应C的
long
——Windows上是32位,Unix上是64位。始终使用
CLong
。 ❌ 绝不要
ulong
对应
size_t
——会导致32位平台上的栈损坏。使用
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
Cdecl
and
StdCall
differ. On x64, ARM, and ARM64, there is a single calling convention and the attribute is unnecessary.
Agent behavior: If you detect that Windows x86 is a target — through project properties (e.g.,
<PlatformTarget>x86</PlatformTarget>
), runtime identifiers (e.g.,
win-x86
), build scripts, comments, or developer instructions — flag this to the developer and recommend explicit calling conventions on all P/Invoke declarations.
csharp
// 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
EntryPoint
to avoid
EntryPointNotFoundException
:
csharp
// 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位)时需要指定调用约定,因为该平台上
Cdecl
StdCall
存在差异。在x64、ARM和ARM64平台上,只有一种调用约定,无需指定该属性。
Agent行为: 如果检测到目标为Windows x86——通过项目属性(如
<PlatformTarget>x86</PlatformTarget>
)、运行时标识符(如
win-x86
)、构建脚本、注释或开发者指令——需向开发者标记此情况,并建议在所有P/Invoke声明上显式指定调用约定。
csharp
// DllImport (x86目标)
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]

// LibraryImport (x86目标)
[LibraryImport("mylib")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]
如果托管方法名称与原生导出名称不同,需指定
EntryPoint
以避免
EntryPointNotFoundException
csharp
// 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:正确处理字符串

  1. Know what encoding the native function expects. There is no safe default.
  2. Windows APIs: Always call the
    W
    (UTF-16) variant. The
    A
    variant needs a specific reason and explicit ANSI encoding.
  3. Cross-platform C libraries: Usually expect UTF-8.
  4. Specify encoding explicitly. Never rely on
    CharSet.Auto
    .
  5. Never introduce
    StringBuilder
    for output buffers.
NEVER rely on
CharSet.Auto
or omit string encoding — there is no safe default.
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,
CoTaskMemAlloc
/
CoTaskMemFree
is the first choice for cross-boundary ownership; on non-Windows targets, use
NativeMemory
APIs. The library may have its own allocator that must be used instead.
  1. 明确原生函数期望的编码,没有安全的默认值。
  2. Windows API: 始终调用
    W
    (UTF-16)变体。只有在有特定理由时才使用
    A
    变体,并显式指定ANSI编码。
  3. 跨平台C库: 通常期望UTF-8编码。
  4. 显式指定编码,绝不要依赖
    CharSet.Auto
  5. 不要为输出缓冲区使用
    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上,
CoTaskMemAlloc
/
CoTaskMemFree
是跨边界所有权管理的首选方式;在非Windows目标上,使用
NativeMemory
API。部分库可能有自己的分配器,此时必须使用该库的分配器。

Step 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 —
Marshal.FreeHGlobal
on
malloc
'd memory is heap corruption.
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
Marshal.FreeHGlobal
or
Marshal.FreeCoTaskMem
on
malloc
'd memory.
Model 3 — Handle-based (callee allocates, callee frees): Use
SafeHandle
(see Step 6).
Pinning 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.FreeHGlobal
Marshal.FreeCoTaskMem
释放
malloc
分配的内存。
模式3 — 句柄式(被调用方分配,被调用方释放): 使用
SafeHandle
(见步骤6)。
托管对象固定——当原生代码存储指针或异步运行时:
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
IntPtr
leaks on exceptions and has no double-free protection.
SafeHandle
is non-negotiable.
csharp
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);
原生
IntPtr
在异常时会泄漏,且无双重释放保护。
SafeHandle
是必不可少的。
csharp
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+):
UnmanagedCallersOnly
— avoids delegates entirely, no GC lifetime risk:
csharp
[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
static
, must not throw exceptions back to native code, and can only use blittable parameter types.
Fallback (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.KeepAlive
for short-lived callbacks:
When converting a delegate to a function pointer with
Marshal.GetFunctionPointerForDelegate
, the GC does not track the relationship between the pointer and the delegate. Use
GC.KeepAlive
to prevent collection before the native call completes:
csharp
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+):
UnmanagedCallersOnly
——完全避免委托,无GC生命周期风险:
csharp
[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); }
该方法必须为
static
,绝不要向原生代码抛出异常,且只能使用blittable参数类型。
兼容方案(旧版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.KeepAlive
当使用
Marshal.GetFunctionPointerForDelegate
将委托转换为函数指针时,GC不会跟踪指针与委托之间的关系。使用
GC.KeepAlive
防止委托在原生调用完成前被回收:
csharp
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
NativeLibrary.SetDllImportResolver
for complex scenarios, or conditional compilation for simple cases. Use
CLong
/
CULong
for C
long
/
unsigned long
. Note:
CLong
/
CULong
with
LibraryImport
requires
[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;
    });

复杂场景使用
NativeLibrary.SetDllImportResolver
,简单场景使用条件编译。C的
long
/
unsigned long
使用
CLong
/
CULong
。注意:
LibraryImport
搭配
CLong
/
CULong
需要添加
[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.
  1. Add
    partial
    to the containing class and make the method
    static partial
  2. Replace
    [DllImport]
    with
    [LibraryImport]
  3. Replace
    CharSet
    with
    StringMarshalling
  4. Replace
    SetLastError = true
    with
    SetLastPInvokeError = true
  5. Remove
    CallingConvention
    unless targeting Windows x86
  6. Build and fix
    SYSLIB1054
    SYSLIB1057
    analyzer warnings
Enable the interop analyzers:
xml
<PropertyGroup>
    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
    <EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>

针对.NET 7+的代码库,迁移可获得AOT兼容性和裁剪安全性。
  1. 为包含类添加
    partial
    修饰符,并将方法改为
    static partial
  2. [DllImport]
    替换为
    [LibraryImport]
  3. CharSet
    替换为
    StringMarshalling
  4. SetLastError = true
    替换为
    SetLastPInvokeError = true
  5. 除非针对Windows x86,否则移除
    CallingConvention
  6. 构建并修复
    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
NativeMethods.txt
listing the APIs you need:
bash
dotnet add package Microsoft.Windows.CsWin32
针对Win32 P/Invoke,优先使用Microsoft.Windows.CsWin32而非手写签名。它通过元数据源代码生成正确的声明。添加
NativeMethods.txt
列出所需API:
bash
dotnet add package Microsoft.Windows.CsWin32

CsWinRT (WinRT APIs)

CsWinRT(WinRT API)

For WinRT interop, use Microsoft.Windows.CsWinRT to generate .NET projections from
.winmd
files.
针对WinRT互操作,使用Microsoft.Windows.CsWinRT
.winmd
文件生成.NET投影。

Objective 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)
  • SafeHandle
    used for all native handles (no raw
    IntPtr
    escaping the interop layer)
  • Delegates passed as callbacks are rooted to prevent GC collection
  • SetLastError
    /
    SetLastPInvokeError
    set for APIs that use OS error codes
  • Struct layout matches native (packing, alignment, field order)
  • CLong
    /
    CULong
    used for C
    long
    /
    unsigned long
    in cross-platform code
  • If using
    CLong
    /
    CULong
    with
    LibraryImport
    ,
    [assembly: DisableRuntimeMarshalling]
    is applied
  • No
    bool
    without explicit
    MarshalAs
    — always specify
    UnmanagedType.Bool
    (4-byte) or
    UnmanagedType.U1
    (1-byte) to ensure normalization across the language boundary.
  • 每个签名与原生头文件完全匹配(类型、大小)
  • 针对Windows x86时指定调用约定,否则省略
  • 字符串编码显式指定——不依赖默认值或
    CharSet.Auto
  • 内存所有权已记录并匹配(分配方、释放方、使用的分配器)
  • 所有原生句柄使用
    SafeHandle
    (无原生
    IntPtr
    泄漏到互操作层外)
  • 作为回调传递的委托已根引用以防止GC回收
  • 针对使用系统错误码的API设置
    SetLastError
    /
    SetLastPInvokeError
  • 结构体布局与原生匹配(打包方式、对齐方式、字段顺序)
  • 跨平台代码中C的
    long
    /
    unsigned long
    使用
    CLong
    /
    CULong
  • LibraryImport
    搭配
    CLong
    /
    CULong
    ,已添加
    [assembly: DisableRuntimeMarshalling]
  • 所有
    bool
    都带有显式
    MarshalAs
    ——始终指定
    UnmanagedType.Bool
    (4字节)或
    UnmanagedType.U1
    (1字节),确保跨语言边界的一致性。

Runnable validation steps

可执行的验证步骤

  1. Build with interop analyzers enabled — confirm zero
    SYSLIB1054
    SYSLIB1057
    warnings:
    xml
    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
    <EnableAotAnalyzer>true</EnableAotAnalyzer>
  2. Verify struct sizes match — for every struct crossing the boundary, assert
    Marshal.SizeOf<T>()
    equals the native
    sizeof
  3. Round-trip test — call the native function with known inputs and verify expected outputs
  4. Test with non-ASCII strings — pass strings containing characters outside the ASCII range to confirm encoding is correct
  1. 启用互操作分析器构建——确认无
    SYSLIB1054
    SYSLIB1057
    警告:
    xml
    <EnableTrimAnalyzer>true</EnableTrimAnalyzer>
    <EnableAotAnalyzer>true</EnableAotAnalyzer>
  2. 验证结构体大小匹配——每个跨边界的结构体,断言
    Marshal.SizeOf<T>()
    等于原生
    sizeof
  3. 往返测试——使用已知输入调用原生函数并验证预期输出
  4. 非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失败问题,或审查互操作代码的正确性时。