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>andMemoryMarshal(Cast,AsBytes,GetReference) reinterpret memory safely-ish.System.Runtime.CompilerServices.Unsafeandrefarithmetic give pointer-like speed without pinning.NativeMemory.Alloc/Freemanage 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.