Loading...

Unsafe Code and Pointers in C#

Learn unsafe code and pointers in C# to work with memory addresses, low-level operations, and advanced performance scenarios.

Although the C# language provides safe memory management, in certain high-performance scenarios, direct access to memory may be necessary. Unsafe code is used in such cases. Unsafe blocks allow direct access to memory addresses using pointers. While this approach provides low-level control, it must be used carefully, as it operates outside the .NET Garbage Collector (GC)’s protection.


What is Unsafe Code?

The unsafe keyword tells the C# compiler that the code works with unmanaged memory. In such code, the compiler does not perform memory safety checks. To use unsafe code, the project must have the “Allow unsafe code” option enabled in the build settings.


// Property to add to your .csproj file:
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

What is a Pointer?

A pointer is a variable that stores the memory address of another variable. In C#, pointers can only be declared within unsafe blocks and are used on the stack. The * symbol declares a pointer, and the & operator retrieves a variable’s address.


using System;

class Program
{
    static unsafe void Main()
    {
        int x = 42;
        int* ptr = &x; // get the memory address of x

        Console.WriteLine($"Value of x: {*ptr}");
        Console.WriteLine($"Address of x: {(ulong)ptr}");
    }
}

The expression *ptr means “read the value at the pointed address.” In this example, both the variable’s address and its value are accessed.


Modifying Values via Pointers

You can directly modify a variable’s value through a pointer. This works exactly like pointer manipulation in C or C++.


unsafe
{
    int x = 10;
    int* p = &x;

    *p = 99; // change x's value via pointer
    Console.WriteLine($"New value of x: {x}");
}

// Output:
// New value of x: 99

Pointer Arithmetic

Pointer arithmetic allows navigating through arrays by incrementing pointer addresses. However, it is only allowed for primitive types such as int, byte, and double.


unsafe
{
    int[] numbers = { 10, 20, 30, 40 };
    fixed (int* p = numbers)
    {
        for (int i = 0; i < numbers.Length; i++)
            Console.WriteLine($"Address: {(ulong)(p + i)}, Value: {*(p + i)}");
    }
}

The fixed keyword pins the array in memory so the GC cannot move it. This ensures that the pointer always references a valid address.


The fixed Keyword

fixed prevents a managed object (such as an array or string) from being relocated by the GC. It allows safe access to memory addresses via pointers.


unsafe
{
    string message = "Hello";
    fixed (char* p = message)
    {
        for (int i = 0; i < message.Length; i++)
            Console.Write(p[i]);
    }
}

Note: When the fixed block ends, the object can be moved again, so using the pointer outside this block is unsafe.


Accessing Memory with struct and Pointer

You can also access memory directly for structs using pointers. This is often used in performance-critical scenarios (e.g., game engines, graphics processing, or native interop).


using System;

struct Point
{
    public int X;
    public int Y;
}

class Program
{
    static unsafe void Main()
    {
        Point p = new Point { X = 5, Y = 10 };
        Point* ptr = &p;

        ptr->X = 20;
        Console.WriteLine($"Point: X={ptr->X}, Y={ptr->Y}");
    }
}

The ptr->X syntax is the same as struct pointer access in C/C++.


Allocating Memory on the Stack with stackalloc

stackalloc allocates raw memory directly on the stack. This memory is automatically released at the end of the scope.


unsafe
{
    int* p = stackalloc int[5];
    for (int i = 0; i < 5; i++)
        p[i] = i * 2;

    for (int i = 0; i < 5; i++)
        Console.WriteLine(p[i]);
}

stackalloc is fast but should only be used for small data sizes. For large data, prefer heap allocation (new).


When to Use Unsafe Code


Performance Comparison

Unsafe code with direct memory access can offer a performance gain of 30–50% by bypassing the GC. However, this comes at the cost of increased risk. Accessing invalid memory addresses can cause crashes or memory corruption.


Important Considerations


Example: Accessing Image Pixel Data

The following example demonstrates how to directly access pixels in a bitmap using an unsafe block. This method performs pixel manipulation (e.g., color inversion) much faster than traditional APIs.


using System;
using System.Drawing;
using System.Drawing.Imaging;

class Program
{
    static unsafe void Main()
    {
        using Bitmap bmp = new Bitmap("image.png");

        Rectangle rect = new Rectangle(0, 0, bmp.Width, bmp.Height);
        BitmapData data = bmp.LockBits(rect, ImageLockMode.ReadWrite, bmp.PixelFormat);

        int bytesPerPixel = Image.GetPixelFormatSize(bmp.PixelFormat) / 8;
        byte* firstPixel = (byte*)data.Scan0;

        for (int y = 0; y < bmp.Height; y++)
        {
            byte* row = firstPixel + (y * data.Stride);
            for (int x = 0; x < bmp.Width; x++)
            {
                byte* pixel = row + (x * bytesPerPixel);
                pixel[0] = (byte)(255 - pixel[0]); // invert blue
                pixel[1] = (byte)(255 - pixel[1]); // invert green
                pixel[2] = (byte)(255 - pixel[2]); // invert red
            }
        }

        bmp.UnlockBits(data);
        bmp.Save("output.png");
        Console.WriteLine("Image colors have been inverted!");
    }
}

This example demonstrates the power of unsafe code for high-performance pixel manipulation operations.


TL;DR

  • unsafe blocks in C# allow direct memory access via pointers.
  • The fixed keyword prevents the GC from moving objects in memory.
  • stackalloc enables fast, temporary memory allocation on the stack.
  • It offers performance gains but comes with significant security risks.
  • Commonly used for interaction with unmanaged (C/C++) code or low-level memory manipulation.

Related Articles