<style> .reveal-up, .reveal-left, .reveal-right, .reveal-stagger > * { opacity: 1 !important; transform: none !important; } </style>
  1. Learn
  2. C#
  3. Master level
All lessons in this course

Lesson 58 of 60 17 minMaster

unsafe code, pointers and native interop

Step outside the safety net when you must: pointers, fixed, stackalloc and native memory, and calling C/C++ and Windows APIs with P/Invoke (LibraryImport), SafeHandle and COM.

Normally C# protects you: arrays check their bounds, the GC manages memory and you can’t read random addresses. Sometimes you must leave that protection: to call a C++ library, a Windows API, a vendor SDK, or to squeeze the last bit of speed from image or mesh processing. C# allows it, but only inside code explicitly marked unsafe.

Interactive

Pointers walk through memory

int[] arr = { 10, 20, 30, 40 }; sits in memory right before another variable, limit. Each int is 4 bytes.

p = 0x1000 *p = 10 // p = arr + 0 (0 bytes after arr[0])
p++ moves the pointer by sizeof(int) = 4 bytes, not by 1. Nothing stops it from walking past the end of the array.

Turning on unsafe code

SteelTools.csproj
<PropertyGroup>  <AllowUnsafeBlocks>true</AllowUnsafeBlocks></PropertyGroup>

Pointers

Pointers.cs
unsafe{    int x = 42;    int* p = &x;                 // & = address of x    Console.WriteLine(*p);       // * = value at that address → 42    *p = 50;                     // writes to x through the pointer    Console.WriteLine(x);     Console.WriteLine(sizeof(double));   // 8 bytes}
Output
42
50
8
SyntaxMeaning
int* pp holds the address of an int
&xaddress of variable x
*pthe value at the address (dereference)
p[i] or *(p + i)the int i positions after p
p++move to the next int: the address grows by sizeof(int) = 4
p->Fieldmember of a struct through a pointer

fixed: pinning managed memory

The GC may move arrays and objects while compacting memory. To take a pointer to managed data, pin it with fixed. The GC won’t move it until the block ends.

Fixed.cs
static unsafe double SumZ(float[] vertices)       // x, y, z, x, y, z, …{    double sum = 0;    fixed (float* start = vertices)              // pinned for this block only    {        float* end = start + vertices.Length;        for (float* p = start + 2; p < end; p += 3)   // jump to every z            sum += *p;    }    return sum;} Console.WriteLine(SumZ([0, 0, 3.5f, 1, 0, 4.5f]));
Output
8

stackalloc and native memory

Native.cs
using System.Runtime.InteropServices; Span<int> small = stackalloc int[64];           // on the stack, freed automatically (no unsafe needed) unsafe{    // a big buffer the GC never sees: YOU must free it    nuint count = 1_000_000;    float* buffer = (float*)NativeMemory.Alloc(count, sizeof(float));    try    {        new Span<float>(buffer, (int)count).Fill(0f);        buffer[10] = 1.5f;    }    finally    {        NativeMemory.Free(buffer);              // forget this → memory leak    }}

P/Invoke: calling native functions

Platform Invoke lets C# call functions exported from native DLLs: Windows APIs or a C/C++ library from a vendor. Since .NET 7, [LibraryImport] generates the marshalling code at compile time (using a source generator).

Native.cs
using System.Runtime.InteropServices; internal static partial class Win32{    [LibraryImport("kernel32.dll")]    internal static partial ulong GetTickCount64();     [LibraryImport("user32.dll", EntryPoint = "MessageBoxW", StringMarshalling = StringMarshalling.Utf16)]    internal static partial int MessageBox(nint hWnd, string text, string caption, uint type);} Console.WriteLine($"Windows has been up for {Win32.GetTickCount64() / 3_600_000} hours");Win32.MessageBox(0, "Export finished", "SteelTools", 0);
Output
Windows has been up for 37 hours   (depends on your PC)
  • The class and method are partial: the generator writes the method body.
  • LibraryImport needs AllowUnsafeBlocks in the project, because the generated code uses pointers.
  • Older code uses [DllImport] with extern. It still works, but marshalling happens at run time.
  • Types must match the native signature exactly: C int → int, HANDLE → nint, wchar_t* → string with UTF-16.

Calling your own C++ library

mesh.h (C++)
extern "C" __declspec(dllexport)int TriangleCount(const float* vertices, int vertexCount, double tolerance);
MeshNative.cs
internal static partial class MeshNative{    [LibraryImport("mesh.dll")]    internal static partial int TriangleCount(ReadOnlySpan<float> vertices, int vertexCount, double tolerance);} float[] verts = LoadVertices("slab.obj");int n = MeshNative.TriangleCount(verts, verts.Length / 3, 0.001);   // span is pinned for the call

SafeHandle: never leak native resources

SafeHandle.cs
internal sealed class ModelHandle : SafeHandleZeroOrMinusOneIsInvalid{    public ModelHandle() : base(ownsHandle: true) { }    protected override bool ReleaseHandle() => VendorSdk.CloseModel(handle) == 0;} internal static partial class VendorSdk{    [LibraryImport("vendor.dll", StringMarshalling = StringMarshalling.Utf16)]    internal static partial ModelHandle OpenModel(string path);     [LibraryImport("vendor.dll")]    internal static partial int CloseModel(nint handle);} using ModelHandle model = VendorSdk.OpenModel("tower.dat");   // closed even if an exception happens

A SafeHandle wraps a raw native handle, works with using, and is released by the runtime even in edge cases where a plain IntPtr would leak.

COM interop

Many Windows applications expose COM APIs: Excel, older CAD tools and many engineering programs. You usually reference a provided interop assembly (or add a COM reference in Visual Studio) and call it like normal .NET objects. The runtime marshals the calls for you. Release COM objects you create in loops promptly (Marshal.FinalReleaseComObject), or the external application may stay open in the background.

Safety checklist

RuleWhy
keep unsafe blocks tiny and wrap them in a safe APIthe rest of the app stays protected
check lengths before every pointer loopthere is no bounds check to save you
always free what you allocate (try/finally, SafeHandle)the GC doesn’t know about native memory
match 32/64-bit and calling conventionsmismatches cause crashes, not exceptions
test with large and malformed inputsmemory bugs often appear only with real data
Quick check

After int* p = arr; (an int array), what does p++ do?

Quick check

Why must you use fixed to get a pointer to a managed array?

Practice

Write an unsafe method static int CountAbove(int[] values, int limit) that uses fixed and a pointer loop to count values greater than limit. Test it with { 5, 12, 8, 30, 15 } and limit 10.

Show the solution
Solution.cs
static unsafe int CountAbove(int[] values, int limit){    int count = 0;    fixed (int* start = values)    {        for (int* p = start, end = start + values.Length; p < end; p++)            if (*p > limit) count++;    }    return count;} Console.WriteLine(CountAbove([5, 12, 8, 30, 15], 10));
Output
3

Remember

  • unsafe enables pointers (&, *, ->, pointer arithmetic) with no bounds checks.
  • fixed pins managed memory; NativeMemory and stackalloc give memory outside the GC heap.
  • [LibraryImport] (P/Invoke) calls native DLLs; use SafeHandle for native resources.
  • Prefer Span<T>; keep unsafe code tiny, wrapped and well tested.
0 of 60 lessons completed