Skip to main content

C# Unsafe Code

Introduction

C# is primarily a type-safe language, which means the compiler ensures you don't access memory locations arbitrarily or perform operations that could lead to memory corruption. However, there are scenarios where you need direct memory access and pointer operations – particularly when optimizing performance or interacting with unmanaged code. This is where C#'s unsafe code comes into play.

Unsafe code allows you to work directly with memory addresses through pointers, similar to languages like C or C++. While powerful, this capability bypasses C#'s type safety and garbage collection, putting the responsibility of memory management on you, the developer.

Understanding Unsafe Code

What Makes Code "Unsafe"?

Code is considered "unsafe" when it includes operations that cannot be verified by the Common Language Runtime (CLR) as type-safe. This includes:

  • Working with pointers
  • Accessing memory directly
  • Performing pointer arithmetic
  • Converting between pointers and integers

Enabling Unsafe Code

Before you can write unsafe code in C#, you need to enable it:

  1. In your code: Use the unsafe keyword to mark blocks or methods
  2. In your project: Enable unsafe code compilation

To enable unsafe code compilation in your project:

  • In Visual Studio: Right-click on your project → Properties → Build → Check "Allow unsafe code"
  • In .NET CLI: Add <AllowUnsafeBlocks>true</AllowUnsafeBlocks> to your .csproj file

Basic Pointer Operations

Declaring Pointers

A pointer in C# is declared using an asterisk (*) after the type:

csharp
unsafe
{
int x = 10;
int* ptr = &x; // ptr holds the memory address of x
}

Dereferencing Pointers

To access the value a pointer points to, use the dereference operator (*):

csharp
unsafe
{
int x = 10;
int* ptr = &x;
Console.WriteLine(*ptr); // Output: 10

*ptr = 20; // Change the value of x through the pointer
Console.WriteLine(x); // Output: 20
}

Pointer Arithmetic

You can perform arithmetic operations on pointers:

csharp
unsafe
{
int[] numbers = { 10, 20, 30, 40, 50 };

// Fix the array in memory so the garbage collector won't move it
fixed (int* ptr = &numbers[0])
{
int* p1 = ptr;
Console.WriteLine(*p1); // Output: 10

int* p2 = p1 + 1; // Points to numbers[1]
Console.WriteLine(*p2); // Output: 20

Console.WriteLine(*(ptr + 2)); // Output: 30

// Iterate through the array using pointer arithmetic
for (int i = 0; i < numbers.Length; i++)
{
Console.WriteLine(*(ptr + i));
}
}
}

The fixed Statement

When working with managed objects like arrays or strings, you need to "fix" them in memory to prevent the garbage collector from moving them while you're accessing them via pointers:

csharp
unsafe
{
string message = "Hello, unsafe world!";

// Fix the string in memory
fixed (char* ptr = message)
{
char* current = ptr;
while (*current != '\0')
{
Console.Write(*current);
current++;
}
}
// Output: Hello, unsafe world!
}

Working with Structs and Pointers

You can use pointers with custom structs:

csharp
unsafe
{
// Define a struct
struct MyPoint
{
public int X;
public int Y;
}

// Create and initialize a struct
MyPoint point = new MyPoint { X = 10, Y = 20 };

// Get a pointer to the struct
MyPoint* pointPtr = &point;

// Access struct members through the pointer using ->
Console.WriteLine($"X: {pointPtr->X}, Y: {pointPtr->Y}");

// Modify struct members
pointPtr->X = 30;

Console.WriteLine($"Updated X: {point.X}"); // Output: Updated X: 30
}

Practical Applications

Example 1: Fast Bitmap Pixel Manipulation

One common use case for unsafe code is direct pixel manipulation in image processing:

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

public static unsafe void InvertColors(Bitmap bitmap)
{
// Lock the bitmap's data to get a pointer to it
BitmapData bitmapData = bitmap.LockBits(
new Rectangle(0, 0, bitmap.Width, bitmap.Height),
ImageLockMode.ReadWrite,
bitmap.PixelFormat);

try
{
byte* ptr = (byte*)bitmapData.Scan0.ToPointer();

// Determine bytes per pixel
int bytesPerPixel = Image.GetPixelFormatSize(bitmap.PixelFormat) / 8;
int heightInPixels = bitmapData.Height;
int widthInBytes = bitmapData.Width * bytesPerPixel;

for (int y = 0; y < heightInPixels; y++)
{
byte* currentLine = ptr + (y * bitmapData.Stride);

for (int x = 0; x < widthInBytes; x += bytesPerPixel)
{
// Invert RGB values (skip alpha channel if present)
currentLine[x] = (byte)(255 - currentLine[x]); // B
currentLine[x + 1] = (byte)(255 - currentLine[x + 1]); // G
currentLine[x + 2] = (byte)(255 - currentLine[x + 2]); // R
}
}
}
finally
{
bitmap.UnlockBits(bitmapData);
}
}

Example 2: Interoperability with Native Code

Unsafe code is often used for P/Invoke (Platform Invocation Services) when calling native libraries:

csharp
using System.Runtime.InteropServices;

public class NativeInterop
{
// Import a native function from user32.dll
[DllImport("user32.dll")]
public static extern bool GetCursorPos(out POINT lpPoint);

// Define a struct to match the native POINT structure
public struct POINT
{
public int X;
public int Y;
}

public static unsafe void ModifyPointDirectly()
{
POINT point;
GetCursorPos(out point);
Console.WriteLine($"Cursor position: {point.X}, {point.Y}");

// Use pointers to modify the struct
POINT* pointPtr = &point;
pointPtr->X += 10;
pointPtr->Y += 10;

Console.WriteLine($"Modified position: {point.X}, {point.Y}");
}
}

Example 3: High-Performance Array Operations

When performance is critical, unsafe code can help avoid bounds checking:

csharp
public static unsafe void FastArraySum(int[] array, out long sum)
{
sum = 0;

fixed (int* ptr = array)
{
int* end = ptr + array.Length;
for (int* current = ptr; current < end; current++)
{
sum += *current;
}
}
}

Risks and Best Practices

Potential Issues

Unsafe code can lead to several problems if not handled carefully:

  • Memory corruption: Accidentally writing to unallocated memory
  • Access violations: Dereferencing null or invalid pointers
  • Memory leaks: Failing to free allocated memory
  • Security vulnerabilities: Buffer overflows and other memory-related security issues

Best Practices

  1. Minimize unsafe code: Use unsafe code only when necessary
  2. Isolate unsafe code: Keep unsafe code in separate methods or classes
  3. Validate all inputs: Never trust external data in unsafe code
  4. Always use fixed for managed objects: Prevent the garbage collector from moving objects
  5. Check for null pointers: Always verify pointers are valid before dereferencing
  6. Use safe alternatives when possible: Consider using Span<T> and Memory<T> for safer memory access

Summary

Unsafe code provides a powerful escape hatch from C#'s type safety, allowing direct memory manipulation and pointer operations. While it offers performance benefits and interoperability with unmanaged code, it comes with significant responsibilities and risks.

Key points to remember:

  • Unsafe code bypasses C#'s type safety mechanisms
  • You must explicitly enable unsafe code in your project
  • Pointers provide direct memory access and can be used for efficient operations
  • The fixed statement prevents the garbage collector from moving objects
  • Common applications include high-performance algorithms, image processing, and native interoperability
  • Always follow best practices to minimize the risks of memory corruption and other issues

Additional Resources

  1. C# Language Specification: Unsafe Code
  2. Microsoft Docs: Unsafe Code and Pointers
  3. C# Memory Management for Developers

Exercises

  1. Write an unsafe method that swaps two integers using pointers.
  2. Create a program that uses unsafe code to copy one array to another without using built-in methods.
  3. Implement a simple memory pool allocator using unsafe code.
  4. Write a function that converts a color image to grayscale using direct pixel manipulation.
  5. Create a program that demonstrates how to safely pass pointers between unsafe methods.


If you spot any mistakes on this website, please let me know at [email protected]. I’d greatly appreciate your feedback! :)