dotnet-native-interop

SKILL.md

dotnet-native-interop

Platform Invoke (P/Invoke) patterns for calling native C/C++ libraries from .NET: [LibraryImport] (preferred, .NET 7+) vs [DllImport] (legacy), struct marshalling, string marshalling, function pointer callbacks, NativeLibrary.SetDllImportResolver for cross-platform library resolution, and platform-specific considerations for Windows, macOS, Linux, iOS, and Android.

Version assumptions: .NET 7.0+ baseline for [LibraryImport]. [DllImport] available in all .NET versions. NativeLibrary API available since .NET Core 3.0.

Scope

  • LibraryImport (.NET 7+) and DllImport declarations
  • Struct and string marshalling patterns
  • Function pointer callbacks and delegates
  • NativeLibrary.SetDllImportResolver for cross-platform resolution

Out of scope

  • AOT-specific P/Invoke concerns (direct pinvoke) -- see [skill:dotnet-native-aot]
  • COM interop and CsWin32 source generator -- see [skill:dotnet-winui]
  • WASM JavaScript interop (JSImport/JSExport) -- see [skill:dotnet-aot-wasm]

Cross-references: [skill:dotnet-native-aot] for AOT-specific P/Invoke and [LibraryImport] in publish scenarios, [skill:dotnet-aot-architecture] for AOT-first design patterns including source-generated interop, [skill:dotnet-winui] for CsWin32 source generator and COM interop, [skill:dotnet-aot-wasm] for WASM JavaScript interop (not native P/Invoke).


LibraryImport vs DllImport

[LibraryImport] (.NET 7+) is the preferred attribute for new P/Invoke declarations. It uses source generation to produce marshalling code at compile time, making it fully AOT-compatible and eliminating runtime codegen overhead.

[DllImport] is the legacy attribute. It relies on runtime marshalling, which may require codegen not available in AOT scenarios. Use [DllImport] only when targeting .NET 6 or earlier, or when the SYSLIB1054 analyzer indicates [LibraryImport] cannot handle a specific signature.

Decision Guide

Scenario Use
New code targeting .NET 7+ [LibraryImport]
Targeting .NET 6 or earlier [DllImport]
SYSLIB1054 analyzer flags incompatibility [DllImport] (with comment explaining why)
Publishing with Native AOT [LibraryImport] (required for full AOT compat)

LibraryImport Declaration


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);
}

```text

Key requirements for `[LibraryImport]`:
- Method must be `static partial` in a `partial` class
- String marshalling must be explicitly specified via `StringMarshalling` or `[MarshalAs]` on each string parameter (only needed when strings are present)
- Boolean return types require explicit `[return: MarshalAs(UnmanagedType.Bool)]`
- `Span<T>` and `ReadOnlySpan<T>` parameters are supported directly -- `[DllImport]` does not support them (use arrays instead)

### DllImport Declaration (Legacy)

```csharp

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);
}

```text

### Migrating DllImport to LibraryImport

The `SYSLIB1054` analyzer suggests converting `[DllImport]` to `[LibraryImport]` and provides code fixes. Key changes:

1. Replace `[DllImport]` with `[LibraryImport]`
2. Change `static extern` to `static partial`
3. Make the containing class `partial`
4. Replace `CharSet` with `StringMarshalling`
5. Replace `IntPtr` with `nint` where appropriate
6. Add explicit `[MarshalAs]` for `bool` parameters and returns

```csharp

// 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);

```text

---

## Platform-Specific Library Names

Native library names differ across platforms. Use `NativeLibrary.SetDllImportResolver` or conditional compilation to handle this.

### Windows

Windows uses `.dll` files. The loader searches the application directory, system directories, and `PATH`.

```csharp

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

```text

Windows also supports omitting the extension -- the loader appends `.dll` automatically:

```csharp

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

```text

### macOS and Linux

macOS uses `.dylib` files; Linux uses `.so` files. The .NET runtime automatically probes common name variations (with and without `lib` prefix, with platform-specific extensions).

```csharp

// 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);

```text

.NET probing order for library name `"foo"`:
1. `foo` (exact name)
2. `foo.dll`, `foo.so`, `foo.dylib` (platform extension)
3. `libfoo`, `libfoo.so`, `libfoo.dylib` (lib prefix + extension)

### iOS

iOS does not allow loading dynamic libraries at runtime. Native code must be statically linked into the application binary. Use `__Internal` as the library name to call functions linked into the main executable:

```csharp

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

```csharp

For iOS, the native library must be compiled as a static library (`.a`) and linked during the Xcode build phase. MAUI and Xamarin handle this through native references in the project file:

```xml

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

```text

### Android

Android uses `.so` files loaded from the app's native library directory. The library name typically omits the `lib` prefix and `.so` extension in the P/Invoke declaration:

```csharp

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

```csharp

Include platform-specific `.so` files for each target ABI in the project:

```xml

<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>

```text

### WASM

WebAssembly does not support traditional P/Invoke. Native C/C++ code cannot be called via `[LibraryImport]` or `[DllImport]` in browser WASM. For JavaScript interop, see [skill:dotnet-aot-wasm].

---

## NativeLibrary.SetDllImportResolver

`NativeLibrary.SetDllImportResolver` (.NET Core 3.0+) provides runtime control over library resolution. This is the recommended approach for cross-platform library loading when static name probing is insufficient.

```csharp

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;
}

```text

### Common Use Cases for DllImportResolver

| Scenario | Why resolver is needed |
|----------|----------------------|
| Versioned `.so` on Linux (e.g., `libfoo.so.2`) | Default probing does not check versioned names |
| Library in a non-standard path | Load from a custom directory at runtime |
| Bundled native library per RID | Resolve to `runtimes/<rid>/native/` path |
| Feature detection at load time | Try multiple library names and fall back gracefully |

### NativeLibrary API

The `NativeLibrary` class provides low-level library management:

```csharp

// 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);
}

```text

---

## Marshalling Patterns

### Struct Marshalling

Structs passed to native code must have a well-defined memory layout. Use `[StructLayout]` to control layout and alignment.

```csharp

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;
}

```text

**Blittable structs** (containing only primitive value types with sequential/explicit layout) are passed directly to native code without copying. Non-blittable structs require marshalling, which incurs overhead.

Blittable primitive types: `byte`, `sbyte`, `short`, `ushort`, `int`, `uint`, `long`, `ulong`, `float`, `double`, `nint`, `nuint`.

**Not blittable:** `bool` (marshals as 4-byte `BOOL` by default), `char` (depends on charset), `string`, arrays of non-blittable types.

### String Marshalling

Specify string encoding explicitly. Never rely on default marshalling behavior.

```csharp

// 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);

```text

For output string buffers, use `char[]` or `byte[]` from `ArrayPool` instead of `StringBuilder`:

```csharp

[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);
}

```text

### Function Pointer Callbacks

Modern .NET (.NET 5+) prefers unmanaged function pointers over delegate-based callbacks for better performance and AOT compatibility.

**Preferred: Unmanaged function pointers with `[UnmanagedCallersOnly]`**

```csharp

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);
}

```text

**Alternative: Delegate-based callbacks (when managed state is needed)**

```csharp

// 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;
}

```text

### SafeHandle for Resource Lifetime

Use `SafeHandle` subclasses to manage native resource lifetimes instead of raw `IntPtr`/`nint`. This prevents resource leaks and use-after-free bugs.

```csharp

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);
}

```text

---

## Cross-Platform Data Type Mapping

Map C/C++ types to .NET types carefully. Some C types have platform-dependent sizes.

### Fixed-Size Types

| C/C++ Type | .NET Type | Size |
|------------|-----------|------|
| `int8_t` / `char` | `sbyte` | 1 byte |
| `uint8_t` / `unsigned char` | `byte` | 1 byte |
| `int16_t` / `short` | `short` | 2 bytes |
| `uint16_t` / `unsigned short` | `ushort` | 2 bytes |
| `int32_t` / `int` | `int` | 4 bytes |
| `uint32_t` / `unsigned int` | `uint` | 4 bytes |
| `int64_t` / `long long` | `long` | 8 bytes |
| `uint64_t` / `unsigned long long` | `ulong` | 8 bytes |
| `float` | `float` | 4 bytes |
| `double` | `double` | 8 bytes |

### Platform-Dependent Types

| C/C++ Type | .NET Type | Notes |
|------------|-----------|-------|
| `size_t` / `ptrdiff_t` | `nint` / `nuint` | Pointer-sized |
| `void*` / pointer types | `nint` or `void*` | Pointer-sized |
| `long` (C/C++) | `CLong` (.NET 6+) | 4 bytes on Windows, 8 bytes on Unix 64-bit |
| `unsigned long` | `CULong` (.NET 6+) | Same platform variance as `long` |
| Windows `BOOL` | `int` | 4 bytes (not `bool`) |
| Windows `BOOLEAN` | `byte` | 1 byte |

Do not use C# `long` for C/C++ `long` -- they have different sizes on Unix 64-bit. Use `CLong`/`CULong` for portable interop.

---

## Agent Gotchas

1. **Do not use `[DllImport]` in new .NET 7+ code without justification.** Use `[LibraryImport]` which generates marshalling at compile time. Only fall back to `[DllImport]` when SYSLIB1054 analyzer indicates incompatibility.
2. **Do not assume `bool` marshals as 1 byte.** .NET marshals `bool` as a 4-byte Windows `BOOL` by default. Use `[MarshalAs(UnmanagedType.U1)]` for C `_Bool`/`bool`, or `[MarshalAs(UnmanagedType.Bool)]` for Windows `BOOL` explicitly.
3. **Do not use C# `long` to interop with C/C++ `long`.** C `long` is 4 bytes on Windows but 8 bytes on 64-bit Unix. Use `CLong`/`CULong` (.NET 6+) for cross-platform correctness.
4. **Do not use `StringBuilder` for output string buffers.** `[LibraryImport]` does not support `StringBuilder` at all, and with `[DllImport]` it allocates multiple intermediate copies. Use `char[]` or `byte[]` from `ArrayPool` instead.
5. **Do not use `[LibraryImport]` or `[DllImport]` for WASM.** WebAssembly does not support traditional P/Invoke. For JavaScript interop in WASM, see [skill:dotnet-aot-wasm].
6. **Do not use dynamic library loading on iOS.** iOS prohibits loading dynamic libraries at runtime. Use `"__Internal"` as the library name for statically linked native code.
7. **Do not use `System.Delegate` fields in interop structs.** Use typed delegates or unmanaged function pointers (`delegate* unmanaged`). Untyped delegates can destabilize the runtime during marshalling.
8. **Do not forget to keep delegate instances alive during native use.** The GC may collect a delegate that native code still references. Store delegates in a static field or use `GCHandle` for the duration of native callbacks.

---

## Prerequisites

- .NET 7+ SDK for `[LibraryImport]` source generation
- .NET Core 3.0+ for `NativeLibrary` API
- Native libraries compiled for each target platform/architecture
- For iOS: Xcode with native static libraries linked via `NativeReference`
- For Android: native `.so` files for each target ABI (arm64-v8a, x86_64)

---

## References

- [Platform Invoke (P/Invoke)](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke)
- [Native interoperability best practices](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/best-practices)
- [LibraryImport source generation](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke-source-generation)
- [Type marshalling](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/type-marshalling)
- [Customizing struct marshalling](https://learn.microsoft.com/en-us/dotnet/standard/native-interop/customize-struct-marshalling)
- [NativeLibrary class](https://learn.microsoft.com/en-us/dotnet/api/system.runtime.interopservices.nativelibrary)
Weekly Installs
1
First Seen
11 days ago
Installed on
amp1
cline1
opencode1
cursor1
kimi-cli1
codex1