Skip to content
Development
Skill

/dotnet-pinvoke

Correctly call native (C/C++) libraries from .NET using P/Invoke and LibraryImport. Covers function signatures, string marshalling, memory lifetime, SafeHandle, and cross-platform patterns. USE FOR: writing new P/Invoke or LibraryImport declarations, reviewing or debugging

From plugin
dotnet-skills
466200 skills50 agents
Install
$ npx -y skills add managedcode/dotnet-skills --skill dotnet-pinvoke --agent claude-code

How it fires

How this skill gets triggered: by you, by Claude, or both.

  • Fires itselfAuto-invocation. Claude auto-loads it when your prompt matches the work.Auto-invocation is when the right skill fires by itself at the right moment, driven by a FLOW.md router and a hook, instead of you invoking it by name. It is the difference between a skill being installed and a skill actually getting used.Read the full definition →
  • You can call itInvoke it directly when you want it.
  • Slash command/dotnet-pinvoke

Context preview

The summary Claude sees to decide when to auto-load this skill.

Correctly call native (C/C++) libraries from .NET using P/Invoke and LibraryImport. Covers function signatures, string marshalling, memory lifetime, SafeHandle, and cross-platform patterns. USE FOR: writing new P/Invoke or LibraryImport declarations, reviewing or debugging

SKILL.md

dotnet-pinvoke.SKILL.md
name: dotnet-pinvoke
description: >
  Correctly call native (C/C++) libraries from .NET using P/Invoke and LibraryImport.
  Covers function signatures, string marshalling, memory lifetime, SafeHandle, and
  cross-platform patterns.
  USE FOR: writing new P/Invoke or LibraryImport declarations, reviewing or debugging
  existing native interop code, wrapping a C or C++ library for use in .NET, diagnosing
  crashes, memory leaks, or corruption at the managed/native boundary.
  DO NOT USE FOR: COM interop, C++/CLI mixed-mode assemblies, or pure managed code with
  no native dependencies.
license: MIT

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

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

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.

Inputs

| Input | Required | Description | |-------|----------|-------------| | Native header or documentation | Yes | C/C++ function signatures, struct definitions, calling conventions | | Target framework | Yes | Determines whether to use `DllImport` or `LibraryImport` | | Target platforms | Recommended | Affects type sizes (`long`, `size_t`) and library naming | | 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.

---

Workflow

Step 1: Choose DllImport or LibraryImport

| Aspect | `DllImport` | `LibraryImport` (.NET 7+) | |--------|-------------|---------------------------| | **Mechanism** | Runtime marshalling | Source generator (compile-time) | | **AOT / Trim safe** | No | Yes | | **String marshalling** | `CharSet` enum | `StringMarshalling` enum | | **Error handling** | `SetLastError` | `SetLastPInvokeError` | | **Availability** | .NET Framework 1.0+ | .NET 7+ only |

Step 2: Map Native Types to .NET Types

The most dangerous mappings — these cause the majority of bugs:

| C / Win32 Type | .NET Type | Why | |----------------|-----------|-----| | `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](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.

Step 3: Write the Declaration

Given a C header:

int32_t process_records(const Record* records, size_t count, uint32_t* out_processed);

**DllImport:**

[DllImport("mylib")]
private static extern int ProcessRecords(
    [In] Record[] records, UIntPtr count, out uint outProcessed);

**LibraryImport:**

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

// DllImport (x86 targets)
[DllImport("m
Read more
Ships withdotnet-skills

Stop explaining .NET to your AI. Start building. We've all been there: asking Claude to use Entity Framework, only to get EF6 patterns in a .NET 8 project. Explaining to Copilot that Blazor Server and Blazor WebAssembly aren't the same thing.

Get the whole plugin

Other skills on dotnet-skills.