Unsafe & Low-Level
Work with pointers, fixed buffers, and unmanaged memory when needed
Unsafe Code & Low-Level Programming in C#
Master unsafe code and pointer manipulation in C# with free flashcards and spaced repetition practice. This lesson covers pointer basics, memory management, fixed statements, and performance optimization—essential concepts for systems programming, interop scenarios, and high-performance applications.
Welcome to the World of Unsafe Code 💻
C# is a managed language that handles memory automatically through garbage collection, but sometimes you need direct memory access for performance-critical operations, interoperability with unmanaged code, or low-level system programming. The unsafe keyword unlocks pointer manipulation, direct memory access, and manual memory management—powerful tools that bypass .NET's safety guarantees.
⚠️ Important: Unsafe code sacrifices memory safety for performance and control. Use it judiciously and only when necessary!
Core Concepts
What is Unsafe Code? 🔓
Unsafe code allows you to work with pointers, perform pointer arithmetic, and directly manipulate memory addresses. It's called "unsafe" because:
- No bounds checking: You can access memory outside array boundaries
- No garbage collection tracking: Manual responsibility for memory lifetime
- Type safety bypassed: Can cast between pointer types freely
- Risk of memory corruption: Invalid pointers cause crashes or data corruption
💡 Tip: Unsafe code requires the /unsafe compiler flag or the <AllowUnsafeBlocks>true</AllowUnsafeBlocks> property in your .csproj file.
The unsafe Keyword 🔑
You can mark entire methods, blocks, or even types as unsafe:
// Unsafe method
public unsafe void ProcessData(byte* buffer, int length)
{
// Pointer operations here
}
// Unsafe block within safe method
public void SafeMethod()
{
unsafe
{
int x = 10;
int* ptr = &x; // Get address of x
Console.WriteLine(*ptr); // Dereference pointer
}
}
// Unsafe struct
public unsafe struct BufferHeader
{
public byte* DataPointer;
public int Length;
}
Pointers: The Fundamentals 👉
A pointer stores a memory address. Here's the syntax:
| Operation | Syntax | Description |
|---|---|---|
| Declare pointer | int* ptr; | Pointer to int |
| Address-of | &variable | Get memory address |
| Dereference | *ptr | Access value at address |
| Member access | ptr->member | Access struct member via pointer |
| Array indexing | ptr[index] | Pointer arithmetic + dereference |
Pointer types you can declare:
- Value type pointers:
int*,double*,MyStruct* void*: Generic pointer (any type)- Pointer to pointer:
int**,byte***
⚠️ You cannot create pointers to: Reference types (classes), managed arrays, or types containing reference types.
The fixed Statement 📌
The garbage collector moves objects in memory during compaction. The fixed statement pins an object in place so its address remains constant:
public unsafe void ProcessArray(int[] numbers)
{
fixed (int* ptr = numbers) // Pin array in memory
{
for (int i = 0; i < numbers.Length; i++)
{
*(ptr + i) *= 2; // Direct memory access
}
} // Array automatically unpinned here
}
Why fixed is necessary:
- GC can move objects during collection
- Moving objects invalidates pointers
fixedtells GC: "Don't move this object"- Automatic unpinning when leaving scope
💡 Tip: You can fix multiple variables in one statement: fixed (byte* p1 = buffer1, p2 = buffer2)
Pointer Arithmetic 🧮
Pointers support mathematical operations:
unsafe
{
int[] data = { 10, 20, 30, 40, 50 };
fixed (int* ptr = data)
{
int* current = ptr;
current++; // Move to next int (4 bytes forward)
current += 2; // Move 2 ints forward (8 bytes)
current--; // Move 1 int backward (4 bytes)
long offset = current - ptr; // Distance in elements
Console.WriteLine(*current); // Access value
}
}
Pointer arithmetic rules:
- Adding
nto pointer movesn * sizeof(type)bytes - Subtracting pointers gives element count between them
- Comparisons (
<,>,==) compare memory addresses - No multiplication or division (doesn't make sense for addresses)
stackalloc: Stack Allocation 📚
The stackalloc keyword allocates memory on the stack (not heap):
unsafe
{
int* buffer = stackalloc int[100]; // 400 bytes on stack
for (int i = 0; i < 100; i++)
{
buffer[i] = i * i;
}
// No need to free - automatically released when method returns
}
// Modern C# (7.2+): Span<T> with stackalloc (safe!)
Span<int> buffer = stackalloc int[100];
for (int i = 0; i < buffer.Length; i++)
{
buffer[i] = i * i;
}
Stack vs Heap allocation:
| Aspect | Stack (stackalloc) | Heap (new) |
|---|---|---|
| Speed | ⚡ Very fast (pointer bump) | Slower (GC overhead) |
| Lifetime | Method scope only | Until garbage collected |
| Size limit | ~1MB (stack size) | Gigabytes available |
| Cleanup | Automatic (stack unwind) | Garbage collector |
⚠️ Warning: Large stackalloc can cause StackOverflowException! Keep allocations small (< 1KB recommended).
sizeof and Unmanaged Types 📏
The sizeof operator returns type size in bytes:
unsafe
{
Console.WriteLine(sizeof(int)); // 4
Console.WriteLine(sizeof(double)); // 8
Console.WriteLine(sizeof(bool)); // 1
Console.WriteLine(sizeof(char)); // 2 (Unicode)
}
// For custom structs
public struct Point3D
{
public float X, Y, Z;
}
unsafe
{
Console.WriteLine(sizeof(Point3D)); // 12 (3 × 4 bytes)
}
Unmanaged types (allowed in unsafe code):
- Primitive types:
int,byte,float,double, etc. - Enums
- Pointers
- Structs containing only unmanaged types
Practical Examples
Example 1: High-Performance Array Processing 🚀
Problem: Process large arrays faster by bypassing bounds checking.
public class ArrayProcessor
{
// Safe version (bounds checked)
public static void DoubleValuesSafe(int[] array)
{
for (int i = 0; i < array.Length; i++)
{
array[i] *= 2;
}
}
// Unsafe version (no bounds checking)
public static unsafe void DoubleValuesUnsafe(int[] array)
{
fixed (int* ptr = array)
{
int* end = ptr + array.Length;
for (int* current = ptr; current < end; current++)
{
*current *= 2;
}
}
}
}
// Benchmark results:
// Safe: 125ms for 100 million elements
// Unsafe: 85ms for 100 million elements (32% faster)
Why it's faster:
- No array bounds checking on each access
- Direct memory manipulation
- Compiler optimizations easier with pointers
- CPU cache-friendly sequential access
💡 Modern Alternative: Use Span<T> for similar performance without unsafe code!
Example 2: Interop with Native APIs 🔗
Problem: Call Windows API that expects a pointer to a structure.
using System.Runtime.InteropServices;
[StructLayout(LayoutKind.Sequential)]
public struct POINT
{
public int X;
public int Y;
}
public class NativeInterop
{
[DllImport("user32.dll")]
private static extern bool GetCursorPos(out POINT lpPoint);
// Alternative: Use pointer parameter
[DllImport("user32.dll")]
private static extern unsafe bool GetCursorPos(POINT* lpPoint);
public static unsafe void GetMousePosition()
{
POINT point;
GetCursorPos(&point); // Pass pointer to struct
Console.WriteLine($"Mouse at ({point.X}, {point.Y})");
}
}
// Working with byte buffers from unmanaged code
public class BufferInterop
{
[DllImport("native.dll")]
private static extern unsafe void FillBuffer(byte* buffer, int size);
public static unsafe void ProcessNativeData()
{
byte[] managed = new byte[1024];
fixed (byte* ptr = managed)
{
FillBuffer(ptr, managed.Length);
}
// Now process managed array with filled data
Console.WriteLine($"First byte: {managed[0]}");
}
}
Common interop scenarios:
- Platform Invoke (P/Invoke) calls
- COM interop
- Memory-mapped files
- Direct hardware access
Example 3: Custom Memory Pool 🏊
Problem: Reduce GC pressure by reusing pre-allocated buffers.
public unsafe class UnmanagedMemoryPool
{
private byte* _buffer;
private int _size;
private int _position;
public UnmanagedMemoryPool(int sizeInBytes)
{
_size = sizeInBytes;
_buffer = (byte*)Marshal.AllocHGlobal(_size);
_position = 0;
}
public byte* Allocate(int bytes)
{
if (_position + bytes > _size)
throw new OutOfMemoryException("Pool exhausted");
byte* result = _buffer + _position;
_position += bytes;
return result;
}
public void Reset()
{
_position = 0;
// Optionally zero out memory
for (int i = 0; i < _size; i++)
_buffer[i] = 0;
}
public void Dispose()
{
if (_buffer != null)
{
Marshal.FreeHGlobal((IntPtr)_buffer);
_buffer = null;
}
}
~UnmanagedMemoryPool()
{
Dispose();
}
}
// Usage
public static unsafe void UseMemoryPool()
{
using var pool = new UnmanagedMemoryPool(4096);
int* numbers = (int*)pool.Allocate(100 * sizeof(int));
for (int i = 0; i < 100; i++)
{
numbers[i] = i * i;
}
pool.Reset(); // Reuse memory for next operation
}
Benefits:
- Zero GC pressure (unmanaged memory)
- Fast allocation (pointer arithmetic)
- Predictable memory usage
- Useful for real-time systems
⚠️ Remember: Must manually free unmanaged memory with Marshal.FreeHGlobal!
Example 4: Fast String Operations 📝
Problem: Perform character-level operations without creating intermediate strings.
public static unsafe class FastStringOps
{
// Count specific character without allocation
public static int CountChar(string text, char target)
{
int count = 0;
fixed (char* ptr = text)
{
char* end = ptr + text.Length;
for (char* current = ptr; current < end; current++)
{
if (*current == target)
count++;
}
}
return count;
}
// In-place uppercase conversion (on char array)
public static void ToUpperInPlace(char[] chars)
{
fixed (char* ptr = chars)
{
for (int i = 0; i < chars.Length; i++)
{
char c = ptr[i];
if (c >= 'a' && c <= 'z')
ptr[i] = (char)(c - 32); // Convert to uppercase
}
}
}
// Fast string comparison (case-sensitive)
public static bool FastEquals(string s1, string s2)
{
if (s1.Length != s2.Length)
return false;
fixed (char* p1 = s1, p2 = s2)
{
char* end = p1 + s1.Length;
for (char* c1 = p1, c2 = p2; c1 < end; c1++, c2++)
{
if (*c1 != *c2)
return false;
}
}
return true;
}
}
// Example usage
string text = "Hello World";
int spaces = FastStringOps.CountChar(text, ' '); // 1
char[] buffer = text.ToCharArray();
FastStringOps.ToUpperInPlace(buffer);
Console.WriteLine(new string(buffer)); // "HELLO WORLD"
Performance wins:
- No temporary string allocations
- Direct character access
- Minimal method call overhead
- Cache-friendly sequential access
Common Mistakes ⚠️
1. Forgetting to Use fixed
❌ Wrong:
public unsafe void BadCode(int[] array)
{
int* ptr = &array[0]; // ERROR: Can't take address without fixed
*ptr = 42;
}
✅ Correct:
public unsafe void GoodCode(int[] array)
{
fixed (int* ptr = array) // Pin array first
{
*ptr = 42;
}
}
2. Dangling Pointers
❌ Wrong:
public unsafe int* GetStackPointer()
{
int value = 100;
return &value; // DANGER: Returns pointer to stack variable!
} // 'value' goes out of scope, pointer now invalid
public unsafe void UseDanglingPointer()
{
int* ptr = GetStackPointer();
Console.WriteLine(*ptr); // Undefined behavior!
}
✅ Correct:
public unsafe int* GetHeapPointer()
{
int* ptr = (int*)Marshal.AllocHGlobal(sizeof(int));
*ptr = 100;
return ptr; // Valid: Points to heap memory
} // Remember to call Marshal.FreeHGlobal later!
3. Buffer Overruns
❌ Wrong:
public unsafe void Overflow()
{
int* buffer = stackalloc int[10];
for (int i = 0; i <= 10; i++) // BUG: i <= 10 is off-by-one!
{
buffer[i] = i; // Writes beyond allocated memory
}
}
✅ Correct:
public unsafe void NoOverflow()
{
int* buffer = stackalloc int[10];
for (int i = 0; i < 10; i++) // Correct: i < 10
{
buffer[i] = i;
}
}
4. Pointer to Reference Type
❌ Wrong:
public unsafe void PointerToClass()
{
string s = "Hello";
string* ptr = &s; // ERROR: Cannot take address of reference type
}
✅ Correct (if you need reference type address):
public unsafe void PointerToClassCorrect()
{
string s = "Hello";
fixed (char* ptr = s) // Get pointer to string's character data
{
Console.WriteLine(*ptr); // 'H'
}
// Or use GCHandle for object address
GCHandle handle = GCHandle.Alloc(s, GCHandleType.Pinned);
IntPtr address = handle.AddrOfPinnedObject();
handle.Free();
}
5. Large Stack Allocations
❌ Wrong:
public unsafe void TooMuchStack()
{
byte* huge = stackalloc byte[10_000_000]; // 10MB on stack!
// StackOverflowException!
}
✅ Correct:
public unsafe void ReasonableAllocation()
{
// Small temporary buffer on stack
byte* small = stackalloc byte[1024]; // 1KB - fine
// Or use heap for large allocations
byte* large = (byte*)Marshal.AllocHGlobal(10_000_000);
try
{
// Use large buffer...
}
finally
{
Marshal.FreeHGlobal((IntPtr)large);
}
}
💡 Rule of thumb: Keep stackalloc under 1KB. For larger buffers, use heap allocation or ArrayPool<T>.
6. Ignoring Alignment
❌ Wrong:
public unsafe void MisalignedAccess()
{
byte* buffer = stackalloc byte[100];
long* longPtr = (long*)buffer; // May be misaligned!
*longPtr = 123456789L; // Slow or crash on some architectures
}
✅ Correct:
public unsafe void AlignedAccess()
{
// Allocate properly aligned memory
long* aligned = stackalloc long[10]; // Guaranteed aligned
aligned[0] = 123456789L;
// Or use StructLayout for explicit control
}
Key Takeaways 🎯
Use sparingly: Unsafe code sacrifices safety for performance—only use when profiling shows a real bottleneck
Always fix managed arrays: Use
fixedstatement to pin arrays/strings before taking pointersStack vs Heap:
stackallocis fast but limited; useMarshal.AllocHGlobalfor larger allocationsBounds checking disappears: You're responsible for preventing buffer overruns and access violations
Manual memory management: Unmanaged memory must be explicitly freed—no garbage collector help
Modern alternatives: Consider
Span<T>,Memory<T>, andArrayPool<T>for safe high-performance codeCompiler flag required: Enable unsafe blocks in project settings or use
/unsafeflagSecurity implications: Unsafe code can bypass security checks—use only in trusted scenarios
📋 Quick Reference Card
| Operation | Syntax | Use Case |
|---|---|---|
| Get address | &variable | Create pointer to variable |
| Dereference | *pointer | Access value at address |
| Pin memory | fixed (T* p = obj) | Prevent GC from moving object |
| Stack allocate | stackalloc T[n] | Fast temporary buffers |
| Heap allocate | Marshal.AllocHGlobal | Unmanaged memory |
| Free memory | Marshal.FreeHGlobal | Release unmanaged memory |
| Size of type | sizeof(T) | Get byte size |
| Pointer arithmetic | ptr + n | Move n elements forward |
Compiler Setup:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
Memory Safety Checklist:
- ✅ Always use
fixedwith managed arrays/strings - ✅ Check bounds manually (no automatic checking)
- ✅ Free unmanaged memory (
Marshal.FreeHGlobal) - ✅ Keep
stackallocsmall (< 1KB) - ✅ Test thoroughly—unsafe bugs cause crashes
- ✅ Consider safe alternatives first (
Span<T>,Memory<T>)
📚 Further Study
- Microsoft Docs - Unsafe Code: https://learn.microsoft.com/en-us/dotnet/csharp/language-reference/unsafe-code
- High-Performance C# with Span
: https://learn.microsoft.com/en-us/archive/msdn-magazine/2018/january/csharp-all-about-span-exploring-a-new-net-mainstay - Memory and Span Tutorial: https://learn.microsoft.com/en-us/dotnet/standard/memory-and-spans/
🧠 Memory Device: Remember "FUSP" for unsafe code essentials:
- Fixed (pin memory)
- Unmanaged (types only)
- Stackalloc (fast, small)
- Pointers (direct access)
🤔 Did you know? The .NET runtime itself uses unsafe code extensively in its implementation of collections, string operations, and I/O. You're using unsafe code indirectly every day—it's just wrapped in safe APIs!