Skip to content

C# - Unsafe Codes

C# is a managed language, meaning the .NET runtime handles memory management (like garbage collection) automatically. This provides type safety and robustness. However, there are rare scenarios where you need to drop down to a lower level and work directly with memory addresses using pointers. C# allows this within code blocks marked with the unsafe keyword.

Warning: Using unsafe code is an advanced feature. It bypasses the safety guarantees of the .NET runtime. You should only use it when absolutely necessary for performance-critical tasks or interoperability with native C/C++ libraries. Incorrect pointer manipulation can lead to memory corruption, security vulnerabilities, and application crashes.

To compile unsafe code, you must explicitly enable it in your project settings. For a modern .NET project, edit your .csproj file and add the following property:

<PropertyGroup>
...
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

In Visual Studio, you can also set this by right-clicking the project in Solution Explorer, selecting Properties, navigating to the Build tab, and checking the “Allow unsafe code” box.

A pointer is a variable that stores the memory address of another variable. Pointers can only be declared and used within an unsafe context (either an entire method or a specific block).

  • Declaration: type* variableName; (e.g., int* p;)
  • Address-of Operator (&): Gets the memory address of a variable.
  • Dereference Operator (*): Accesses the value stored at the memory address the pointer is pointing to.
// Mark the entire method as unsafe
static unsafe void Main(string[] args)
{
int value = 20;
// Get the address of 'value' and store it in a pointer.
int* pValue = &value;
Console.WriteLine($"Original value: {value}");
// Use the dereference operator to get the value via the pointer.
Console.WriteLine($"Value via pointer: {*pValue}");
// The address itself is a number, which can be cast to a long to display.
// Note: The actual address will change each time the program runs.
Console.WriteLine($"Memory address: 0x{(long)pValue:X}");
// You can also modify the original variable's data through the pointer.
*pValue = 50;
Console.WriteLine($"Value after modification via pointer: {value}");
}
/*
* Example Output (address will vary):
*
* Original value: 20
* Value via pointer: 20
* Memory address: 0x56B4FF7B4C
* Value after modification via pointer: 50
*/

The .NET garbage collector (GC) can move objects around in memory to optimize space. This is a problem if you have a pointer to an object, as the address could become invalid. The fixed statement “pins” a managed object in memory, preventing the GC from moving it for the duration of the fixed block.

The fixed statement is essential when getting a pointer to an element within a managed array or a field of a class instance.

Example: Accessing Array Elements with a Pointer

Section titled “Example: Accessing Array Elements with a Pointer”
int[] numbers = { 10, 100, 200 };
// This entire block is an unsafe context.
unsafe
{
// Pin the 'numbers' array in memory and get a pointer to its first element.
fixed (int* pNumbers = numbers)
{
for (int i = 0; i < numbers.Length; i++)
{
// Use pointer arithmetic to access elements.
// *(pNumbers + i) is equivalent to pNumbers[i] in C/C++.
Console.WriteLine($"Value of numbers[{i}] = {*(pNumbers + i)}");
}
}
} // The array is 'unpinned' here and can be moved by the GC again.

Modern Alternatives: Span<T> and Memory<T>

Section titled “Modern Alternatives: Span<T> and Memory<T>”

For many performance-sensitive scenarios that once required unsafe code (like processing slices of arrays), modern C# offers a safer and often equally performant alternative: System.Span<T> and System.Memory<T>. These types provide a type-safe and memory-safe representation of a contiguous region of arbitrary memory.

Span<T> is a stack-only ref struct that can point to memory on the stack, in managed heap (like an array), or from native code. It provides a “view” over the data without allocating new memory, and all accesses are bounds-checked.

Example: Processing an Array Slice with Span<T> (The Safe Way)

Section titled “Example: Processing an Array Slice with Span<T> (The Safe Way)”
// This code is completely safe and does not require the 'unsafe' keyword.
void ProcessData(Span<byte> data)
{
// Process a slice of memory without copying it.
for (int i = 0; i < data.Length; i++)
{
data[i] = (byte)(data[i] * 2); // Modify the original data through the span
}
}
byte[] buffer = { 1, 2, 3, 4, 5, 6, 7, 8 };
// Create a span that represents a slice of the array from index 2 with a length of 4.
// This slice represents the elements { 3, 4, 5, 6 }.
Span<byte> slice = new Span<byte>(buffer, 2, 4);
ProcessData(slice);
// The original array has been modified.
Console.WriteLine(string.Join(", ", buffer));
// Expected Output: 1, 2, 6, 8, 10, 12, 7, 8

Given powerful safe alternatives like Span<T>, the need for unsafe code is rare. Reserve it for these specific situations:

  • Platform Invoke (P/Invoke): When calling native C/C++ functions that require pointers, pointer-to-pointer, or function pointers.
  • Extreme Performance Optimization: In scenarios where even the minimal overhead of Span<T> bounds checking is too much (e.g., in the inner loop of a video codec or a high-frequency trading algorithm). This should be a last resort and heavily benchmarked.
  • Low-Level Memory Manipulation: When implementing custom memory allocation schemes or working directly with hardware.

Conclusion: Always prefer safe code. Use Span<T> and other modern .NET APIs first. Only turn to unsafe as a final optimization step or for essential interop, and be sure to encapsulate it within a small, well-tested, and well-documented part of your application.