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.
unsafe.
Turning on unsafe code
<PropertyGroup> <AllowUnsafeBlocks>true</AllowUnsafeBlocks></PropertyGroup>Pointers
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}42 50 8
| Syntax | Meaning |
|---|---|
int* p | |
&x | |
*p | |
p[i] or *(p + i) | |
p++ | sizeof(int) = 4 |
p->Field |
fixed: pinning managed memory
fixed. The GC won’t move it until the block ends.
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]));8
stackalloc and native memory
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
[LibraryImport] generates the marshalling code at compile time (using a source generator).
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);Windows has been up for 37 hours (depends on your PC)
The class and method are partial: the generator writes the method body.LibraryImportneedsAllowUnsafeBlocksin the project, because the generated code uses pointers.Older code uses [DllImport]withextern. It still works, but marshalling happens at run time.Types must match the native signature exactly: C int→int,HANDLE→nint,wchar_t*→stringwith UTF-16.
Calling your own C++ library
extern "C" __declspec(dllexport)int TriangleCount(const float* vertices, int vertexCount, double tolerance);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 callSafeHandle: never leak native resources
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 happensSafeHandle 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
Marshal.FinalReleaseComObject), or the external application may stay open in the background.
Safety checklist
| Rule | Why |
|---|---|
unsafe blocks tiny and wrap them in a safe API | |
try/finally, SafeHandle) | |
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
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));3
Remember
unsafeenables pointers (&,*,->, pointer arithmetic) with no bounds checks.fixedpins managed memory;NativeMemoryandstackallocgive memory outside the GC heap.[LibraryImport](P/Invoke) calls native DLLs; useSafeHandlefor native resources.Prefer Span<T>; keep unsafe code tiny, wrapped and well tested.