Unsafe code and native interop

C# · Expert · 14 min

Interop is how .NET talks to OS APIs and C libraries; unsafe code underlies the fastest library paths.

P/Invoke calls functions in native libraries. The modern way is [LibraryImport] on a static partial method: a source generator writes the marshalling code at compile time, which is faster than the older runtime-generated [DllImport] stubs and works with Native AOT.

[LibraryImport("libc", EntryPoint = "getpid")]
private static partial int GetPid();

Marshalling converts types across the boundary. Blittable types (int, double, pointers, structs of them) pass as-is; strings need an encoding (StringMarshalling = StringMarshalling.Utf8); bool and arrays need care. Native handles should be wrapped in SafeHandle so they're released even on exceptions.

Unsafe code (enable AllowUnsafeBlocks) allows pointers: int* p, &x, p->Field, pointer arithmetic. Because the GC moves objects, you must pin managed memory before taking its address, with fixed (byte* p = array). Pinning many objects for long periods fragments the heap.

Often you can avoid unsafe entirely:

  • Span<T> and MemoryMarshal (Cast, AsBytes, GetReference) reinterpret memory safely-ish.
  • System.Runtime.CompilerServices.Unsafe and ref arithmetic give pointer-like speed without pinning.
  • NativeMemory.Alloc/Free manage unmanaged buffers.

Errors here can corrupt memory silently, so isolate interop behind a small, well-tested API.

Example

using System.Runtime.InteropServices;

Console.WriteLine(Native.GetPid() == Environment.ProcessId); // True (Linux/macOS)

int[] data = [1, 2, 3, 4];
unsafe
{
    fixed (int* p = data)                // pin so the GC can't move it
    {
        for (int i = 0; i < data.Length; i++) p[i] *= 10;
    }
}
Console.WriteLine(string.Join(",", data));  // 10,20,30,40

Span<byte> bytes = MemoryMarshal.AsBytes(data.AsSpan());
Console.WriteLine(bytes.Length);            // 16, no unsafe needed

static partial class Native
{
    [LibraryImport("libc", EntryPoint = "getpid")]
    public static partial int GetPid();
}

Remember

  • Prefer [LibraryImport] (source-generated) over [DllImport]
  • Pin managed memory with fixed before taking pointers
  • Wrap native handles in SafeHandle
  • Span, MemoryMarshal and Unsafe often replace pointer code

Watch out for

  • Storing a pointer from a fixed block and using it after the block ends, when the object may have moved.

Check yourself

  1. Why is fixed needed before taking a pointer to a managed array?

Sign in to answer, see the explanations, and get this lesson back as a quick review later.

Official docs: learn.microsoft.com/en-us/dotnet/standard/native-interop/pinvoke

Builds on

Unlocks

Nothing builds on this yet.