Unsafe Code in C#

Unsafe code in C# allows you to perform operations that the managed compiler cannot guarantee are safe. This typically involves working with pointers, which are memory addresses. While powerful, unsafe code should be used sparingly and with extreme caution, as it can lead to memory corruption, crashes, and security vulnerabilities if not handled correctly.

Note: To use unsafe code, you must explicitly enable it in your project settings. In Visual Studio, go to Project Properties > Build > Allow unsafe code.

Why Use Unsafe Code?

There are several scenarios where using unsafe code might be necessary or beneficial:

Key Concepts in Unsafe C#

Unsafe C# introduces several new keywords and concepts:

1. The unsafe Keyword

The unsafe keyword can be used in two ways:

When you mark a method or a block as unsafe, you are telling the compiler that you are aware of the risks and are responsible for ensuring memory safety within that scope.


// Method-level unsafe
public unsafe void ProcessData(byte[] data)
{
    fixed (byte* p = data)
    {
        // Use pointer p here
    }
}

// Block-level unsafe
void AnotherMethod()
{
    unsafe
    {
        // Unsafe code within this block
    }
}
            

2. Pointers

Pointers are variables that store memory addresses. In C#, pointers can only be declared and used within unsafe contexts. They are prefixed with an asterisk (*).


int i = 10;
int* ptr = &i; // ptr now holds the memory address of i
            

The & operator (address-of operator) gets the memory address of a variable.

3. The fixed Statement

The fixed statement is used to "pin" a managed object in memory, preventing the garbage collector from moving it. This is crucial when you need to obtain a stable pointer to a managed type (like an array or a string) that might otherwise be moved by the GC. The fixed statement can only be used within an unsafe context.


byte[] buffer = new byte[100];
fixed (byte* p = buffer)
{
    // p is a pointer to the first element of buffer
    // buffer is pinned and will not be moved by the GC
    // while inside this block.
    for (int i = 0; i < buffer.Length; i++)
    {
        p[i] = (byte)i; // Accessing array elements via pointer
    }
}
            

When using fixed with strings, you can get a pointer to the first character.


string message = "Hello";
fixed (char* c = message)
{
    // c points to the first character 'H'
    Console.WriteLine(*c); // Outputs 'H'
}
            

4. Pointer Arithmetic

You can perform arithmetic operations on pointers. The increment or decrement of a pointer moves it by the size of the data type it points to.


int[] numbers = { 1, 2, 3, 4, 5 };
fixed (int* p = numbers)
{
    int* current = p;
    Console.WriteLine(*current); // Outputs 1

    current++; // Moves to the next integer in the array
    Console.WriteLine(*current); // Outputs 2

    current += 2; // Moves two integers forward
    Console.WriteLine(*current); // Outputs 4
}
            

5. Pointer to Pointer

You can have pointers that point to other pointers.


int x = 10;
int* ptrX = &x;
int** ptrPtrX = &ptrX; // Pointer to a pointer

Console.WriteLine(***ptrPtrX); // Outputs 10
            

6. Stack Allocation with `stackalloc`

The stackalloc keyword allocates memory on the stack, which is much faster than heap allocation but is automatically reclaimed when the method exits. It can only be used within unsafe contexts.


void ProcessLargeArray()
{
    // Allocate 1024 bytes on the stack
    byte* buffer = stackalloc byte[1024];

    // Use the stack-allocated buffer
    for (int i = 0; i < 1024; i++)
    {
        buffer[i] = (byte)i;
    }
    // Memory is automatically freed when method exits
}
            
Caution: Overusing stack allocation can lead to a stack overflow exception.

Common Pitfalls and Best Practices

Example: Swapping two integers using pointers


using System;

public class UnsafeSwap
{
    public static unsafe void Swap(ref int a, ref int b)
    {
        // Get pointers to a and b
        int* ptrA = &a;
        int* ptrB = &b;

        // Swap the values using pointers
        int temp = *ptrA;
        *ptrA = *ptrB;
        *ptrB = temp;
    }

    public static void Main(string[] args)
    {
        int x = 10;
        int y = 20;

        Console.WriteLine($"Before swap: x = {x}, y = {y}");

        // Must call Swap within an unsafe context
        unsafe
        {
            Swap(ref x, ref y);
        }

        Console.WriteLine($"After swap: x = {x}, y = {y}");
    }
}
            

When compiled and run, this example will output:


Before swap: x = 10, y = 20
After swap: x = 20, y = 10
            
This feature is part of the C# language specification and is supported by the .NET runtime.