Loading...
Loading...
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.
npx skill4agent add dotnet/skills dotnet-pinvokeDllImportLibraryImportDllImportLibraryImportLibraryImport[DllImport][LibraryImport]AccessViolationExceptionDllNotFoundExceptionDllImportLibraryImportDllImportLibraryImport| 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 |
| 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 |
| 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 |
❌ 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
int32_t process_records(const Record* records, size_t count, uint32_t* out_processed);[DllImport("mylib")]
private static extern int ProcessRecords(
[In] Record[] records, UIntPtr count, out uint outProcessed);[LibraryImport("mylib")]
internal static partial int ProcessRecords(
[In] Record[] records, nuint count, out uint outProcessed);CdeclStdCall<PlatformTarget>x86</PlatformTarget>win-x86// DllImport (x86 targets)
[DllImport("mylib", CallingConvention = CallingConvention.Cdecl)]
// LibraryImport (x86 targets)
[LibraryImport("mylib")]
[UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])]EntryPointEntryPointNotFoundException// 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);WACharSet.AutoStringBuilder❌ NEVER rely onor omit string encoding — there is no safe default.CharSet.Auto
// 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);CoTaskMemAllocCoTaskMemFreeNativeMemory❌ NEVER free with a mismatched allocator —onMarshal.FreeHGlobal'd memory is heap corruption.malloc
[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]);
}[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
}Marshal.FreeHGlobalMarshal.FreeCoTaskMemmallocSafeHandle// 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()IntPtrSafeHandleinternal 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);// 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);UnmanagedCallersOnly[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[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);
}GC.KeepAliveMarshal.GetFunctionPointerForDelegateGC.KeepAlivevar 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 delegateNativeLibrary.SetDllImportResolverCLongCULonglongunsigned longCLongCULongLibraryImport[assembly: DisableRuntimeMarshalling]// 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;
});partialstatic partial[DllImport][LibraryImport]CharSetStringMarshallingSetLastError = trueSetLastPInvokeError = trueCallingConventionSYSLIB1054SYSLIB1057<PropertyGroup>
<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>
</PropertyGroup>NativeMethods.txtdotnet add package Microsoft.Windows.CsWin32.winmdCharSet.AutoSafeHandleIntPtrSetLastErrorSetLastPInvokeErrorCLongCULonglongunsigned longCLongCULongLibraryImport[assembly: DisableRuntimeMarshalling]boolMarshalAsUnmanagedType.BoolUnmanagedType.U1SYSLIB1054SYSLIB1057<EnableTrimAnalyzer>true</EnableTrimAnalyzer>
<EnableAotAnalyzer>true</EnableAotAnalyzer>Marshal.SizeOf<T>()sizeof