C# Unsafe Code and Pointers
When to Use Unsafe Code
Unsafe Code Is Rarely Necessary
Modern C# provides safe alternatives like Span<T>, Memory<T>, and ref that deliver nearly identical performance without sacrificing safety. Use unsafe code only when absolutely required.
Unsafe code bypasses .NET’s memory safety guarantees. Use it only when:
- Interoperating with native code that expects pointers
- Performance-critical code where bounds checking overhead matters
- Working with hardware or memory-mapped devices
- Implementing low-level data structures that require pointer arithmetic
Prefer safe alternatives like Span<T>, Memory<T>, and ref when possible—they provide similar performance without sacrificing safety.
Enabling Unsafe Code
<!-- In .csproj -->
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
// Mark methods, classes, or blocks as unsafe
public unsafe void UnsafeMethod()
{
int* ptr = stackalloc int[10];
}
public void MixedMethod()
{
// Safe code here
unsafe
{
// Unsafe block within safe method
int x = 42;
int* ptr = &x;
}
// Back to safe code
}
// Entire class can be unsafe
public unsafe class UnsafeClass
{
private int* _buffer;
}
Pointer Basics
Declaring and Using Pointers
unsafe void PointerBasics()
{
// Pointer declaration
int* intPtr; // Pointer to int
byte* bytePtr; // Pointer to byte
void* voidPtr; // Pointer to unknown type
// Get address of variable
int value = 42;
intPtr = &value; // intPtr points to value
// Dereference pointer
int retrieved = *intPtr; // 42
// Modify through pointer
*intPtr = 100;
Console.WriteLine(value); // 100
// Null pointer
int* nullPtr = null;
if (nullPtr == null)
Console.WriteLine("Null pointer");
}
Pointer Arithmetic
unsafe void PointerArithmetic()
{
int[] array = { 10, 20, 30, 40, 50 };
fixed (int* ptr = array)
{
// Pointer arithmetic moves by element size
int* p = ptr;
Console.WriteLine(*p); // 10
p++; // Move to next int (4 bytes)
Console.WriteLine(*p); // 20
p += 2; // Skip 2 elements
Console.WriteLine(*p); // 40
// Index syntax
Console.WriteLine(ptr[0]); // 10
Console.WriteLine(ptr[4]); // 50
// Difference between pointers
int* start = ptr;
int* end = ptr + 5;
long elements = end - start; // 5 elements
}
}
Pointer Types
unsafe void PointerTypes()
{
// Pointers to value types
int i = 10;
int* pi = &i;
double d = 3.14;
double* pd = &d;
// Pointer to struct
Point point = new(10, 20);
Point* pp = &point;
Console.WriteLine(pp->X); // Arrow operator for member access
// void pointer - generic pointer
void* vp = pi;
// Must cast before dereferencing
int value = *(int*)vp;
// Pointer to pointer
int** ppi = π
int retrieved = **ppi; // Double dereference
}
public struct Point
{
public int X;
public int Y;
public Point(int x, int y) => (X, Y) = (x, y);
}
The fixed Statement
The garbage collector can move objects at any time. The fixed statement pins an object in place so native code can safely access it through a pointer.
Managed objects can be moved by the GC. The fixed statement pins objects in memory.
unsafe void FixedExample()
{
string text = "Hello";
int[] numbers = { 1, 2, 3, 4, 5 };
// Pin string to get char*
fixed (char* charPtr = text)
{
for (int i = 0; i < text.Length; i++)
Console.Write(charPtr[i]);
}
// Pin array to get element pointer
fixed (int* arrayPtr = numbers)
{
int sum = 0;
for (int i = 0; i < numbers.Length; i++)
sum += arrayPtr[i];
Console.WriteLine($"Sum: {sum}");
}
// Pin multiple in one statement
byte[] data1 = new byte[10];
byte[] data2 = new byte[10];
fixed (byte* p1 = data1, p2 = data2)
{
CopyMemory(p1, p2, 10);
}
// Pin object field
var holder = new DataHolder();
fixed (int* valuePtr = &holder.Value)
{
*valuePtr = 42;
}
}
class DataHolder
{
public int Value;
}
Fixed Keyword on Fields (C# 12)
// Fixed-size buffer in struct (older approach)
public unsafe struct FixedBuffer
{
public fixed byte Data[256]; // Inline array
}
// Usage
unsafe void UseFixedBuffer()
{
FixedBuffer buffer = new();
buffer.Data[0] = 1;
buffer.Data[255] = 255;
// Get pointer without fixed statement
byte* ptr = buffer.Data;
}
// Modern approach: InlineArray (C# 12) - no unsafe needed
[System.Runtime.CompilerServices.InlineArray(256)]
public struct SafeBuffer
{
private byte _element0;
}
stackalloc
Prefer Span<T> Over Raw Pointers
Modern C# allows Span<T> span = stackalloc T[size] without unsafe code. This gives you stack allocation with bounds checking and without pointer arithmetic risks.
Allocate on the stack without GC involvement.
unsafe void StackAllocation()
{
// Traditional unsafe stackalloc
int* buffer = stackalloc int[100];
for (int i = 0; i < 100; i++)
buffer[i] = i;
// Modern safe stackalloc with Span (preferred)
Span<int> safeBuffer = stackalloc int[100];
for (int i = 0; i < 100; i++)
safeBuffer[i] = i;
// Conditional stackalloc (stack for small, heap for large)
int size = GetSize();
Span<byte> data = size <= 1024
? stackalloc byte[size]
: new byte[size];
}
int GetSize() => 100;
stackalloc Best Practices
- Keep allocations small (< 1KB is safe)
- Don’t return stackalloc’d memory from methods
- Prefer
Span<T>over raw pointers - Use conditional allocation for variable sizes
Memory Manipulation
sizeof and Alignment
unsafe void SizeAndAlignment()
{
// Built-in type sizes
int intSize = sizeof(int); // 4
int longSize = sizeof(long); // 8
int doubleSize = sizeof(double); // 8
// Struct sizes (compile-time constant for unmanaged structs)
int pointSize = sizeof(Point);
// For managed types, use Marshal
int stringSize = Marshal.SizeOf<string>(); // Platform-dependent
}
// Control struct layout for interop
[StructLayout(LayoutKind.Sequential, Pack = 1)]
public struct PackedStruct
{
public byte A; // Offset 0
public int B; // Offset 1 (no padding)
public byte C; // Offset 5
}
// Total size: 6 bytes
[StructLayout(LayoutKind.Explicit)]
public struct ExplicitStruct
{
[FieldOffset(0)] public int Value;
[FieldOffset(0)] public byte Byte0; // Union-style overlap
[FieldOffset(1)] public byte Byte1;
[FieldOffset(2)] public byte Byte2;
[FieldOffset(3)] public byte Byte3;
}
Memory Copy Operations
using System.Runtime.CompilerServices;
unsafe void MemoryCopy()
{
byte[] source = new byte[1000];
byte[] dest = new byte[1000];
fixed (byte* src = source, dst = dest)
{
// Using Unsafe.CopyBlock
Unsafe.CopyBlock(dst, src, 1000);
// Using Buffer.MemoryCopy (handles overlapping regions)
Buffer.MemoryCopy(src, dst, 1000, 1000);
}
// Safe alternative with Span
source.AsSpan().CopyTo(dest);
}
Reinterpret Cast
unsafe void ReinterpretCast()
{
float f = 3.14f;
// View float bits as int
int* intPtr = (int*)&f;
int bits = *intPtr; // IEEE 754 representation
// Using Unsafe class (preferred)
int safeBits = Unsafe.As<float, int>(ref f);
// Using BitConverter (safest)
int safestBits = BitConverter.SingleToInt32Bits(f);
}
Function Pointers (C# 9+)
Type-safe, high-performance function pointers for callbacks.
unsafe void FunctionPointers()
{
// Function pointer type
delegate*<int, int, int> addPtr = &Add;
int result = addPtr(5, 3); // 8
// With calling convention (for native interop)
delegate* unmanaged[Cdecl]<int, int> nativeFunc;
// Store in variable
var operation = GetOperation(true);
Console.WriteLine(operation(10, 5)); // 15 or 5
static int Add(int a, int b) => a + b;
static int Subtract(int a, int b) => a - b;
static delegate*<int, int, int> GetOperation(bool add)
{
return add ? &Add : &Subtract;
}
}
Native Interop with Pointers
P/Invoke with Pointers
using System.Runtime.InteropServices;
public static partial class NativeMethods
{
// Pass pointer to native code
[DllImport("kernel32.dll")]
public static extern unsafe bool ReadFile(
IntPtr hFile,
byte* lpBuffer,
int nNumberOfBytesToRead,
int* lpNumberOfBytesRead,
IntPtr lpOverlapped);
// Modern LibraryImport (source generated)
[LibraryImport("mylib")]
public static unsafe partial int ProcessData(
byte* data,
int length);
}
unsafe void UseNativeMethod()
{
byte[] buffer = new byte[1024];
int bytesRead;
fixed (byte* bufferPtr = buffer)
{
NativeMethods.ReadFile(
fileHandle,
bufferPtr,
buffer.Length,
&bytesRead,
IntPtr.Zero);
}
}
Working with Native Structures
[StructLayout(LayoutKind.Sequential)]
public struct NativePoint
{
public int X;
public int Y;
}
unsafe void NativeStructs()
{
NativePoint point = new() { X = 10, Y = 20 };
// Pass struct by pointer
ProcessPoint(&point);
// Array of structs
NativePoint[] points = new NativePoint[100];
fixed (NativePoint* ptr = points)
{
ProcessPoints(ptr, points.Length);
}
}
Unsafe and ref Safety
ref returns with Pointers
public struct LargeStruct
{
public int Value1, Value2, Value3, Value4;
}
// Return reference to avoid copy
public unsafe ref LargeStruct GetByRef(LargeStruct* array, int index)
{
return ref array[index];
}
// Safe alternative with Span
public ref LargeStruct GetByRefSafe(Span<LargeStruct> array, int index)
{
return ref array[index];
}
Unsafe.AsRef and Related Methods
using System.Runtime.CompilerServices;
void UnsafeHelpers()
{
byte[] data = new byte[100];
// Read struct from byte array
ref MyStruct structRef = ref Unsafe.As<byte, MyStruct>(ref data[0]);
// Read value at offset
int value = Unsafe.ReadUnaligned<int>(ref data[4]);
// Write value at offset
Unsafe.WriteUnaligned(ref data[8], 42);
// Add offset to reference
ref byte offset10 = ref Unsafe.Add(ref data[0], 10);
// Check if same reference
bool same = Unsafe.AreSame(ref data[0], ref data[0]);
}
struct MyStruct
{
public int A, B, C;
}
Common Patterns
High-Performance Buffer Processing
public unsafe class FastBuffer
{
private byte* _buffer;
private int _length;
public FastBuffer(int size)
{
_buffer = (byte*)NativeMemory.Alloc((nuint)size);
_length = size;
}
public byte this[int index]
{
get => _buffer[index];
set => _buffer[index] = value;
}
public void Fill(byte value)
{
Unsafe.InitBlock(_buffer, value, (uint)_length);
}
public void Dispose()
{
if (_buffer != null)
{
NativeMemory.Free(_buffer);
_buffer = null;
}
}
}
Image Processing
unsafe void ProcessImageFast(byte* pixels, int width, int height)
{
int stride = width * 4; // RGBA
for (int y = 0; y < height; y++)
{
byte* row = pixels + (y * stride);
for (int x = 0; x < width; x++)
{
byte* pixel = row + (x * 4);
// Invert colors
pixel[0] = (byte)(255 - pixel[0]); // R
pixel[1] = (byte)(255 - pixel[1]); // G
pixel[2] = (byte)(255 - pixel[2]); // B
// pixel[3] is alpha, leave unchanged
}
}
}
Safe Alternatives to Consider
Unsafe Approach
- Direct pointer manipulation
- No bounds checking
- Requires unsafe keyword
- Memory corruption risks
Safe Alternative
- Span<T> and Memory<T>
- Automatic bounds checking
- Same performance characteristics
- GC-safe and verifiable
| Unsafe Pattern | Safe Alternative |
|---|---|
int* ptr = stackalloc int[10] |
Span<int> span = stackalloc int[10] |
fixed (byte* p = array) |
array.AsSpan() |
| Pointer arithmetic | Span slicing and indexing |
sizeof(T) |
Unsafe.SizeOf<T>() |
| Manual memory copy | Span<T>.CopyTo() |
| Reinterpret cast | MemoryMarshal.Cast<TFrom, TTo>() |
| Function pointers | Delegates (if allocation acceptable) |
Version History
| Feature | Version | Significance |
|---|---|---|
| unsafe keyword | C# 1.0 | Pointer support |
| fixed statement | C# 1.0 | Pin managed objects |
| stackalloc | C# 1.0 | Stack allocation |
| stackalloc in expressions | C# 7.2 | Safe stackalloc with Span |
| ref struct | C# 7.2 | Stack-only types with refs |
| Function pointers | C# 9.0 | Type-safe function pointers |
| nint/nuint | C# 9.0 | Native-sized integers |
| InlineArray | C# 12 | Safe fixed buffers |
| NativeMemory | .NET 6 | Managed native allocation API |
Key Takeaways
Prefer safe alternatives: Span<T>, Memory<T>, and ref provide most benefits without the risks.
Use fixed sparingly: Pinning objects prevents GC compaction. Keep fixed blocks short.
Watch for buffer overflows: No bounds checking means bugs can corrupt memory or crash.
Understand alignment: Misaligned access can cause performance penalties or crashes on some architectures.
Test thoroughly: Unsafe bugs may not manifest immediately. Use sanitizers and code analysis tools.
Document unsafe code: Explain why unsafe is necessary and what invariants must be maintained.
Found this guide helpful? Share it with your team:
Share on LinkedIn