Cargando...

Código no seguro y punteros en C#

Aprende código no seguro y punteros en C# para trabajar con direcciones de memoria y operaciones de bajo nivel.

Aunque el lenguaje C# ofrece una gestión de memoria segura, en ciertos escenarios de alto rendimiento puede ser necesario acceder directamente a la memoria. El código unsafe se utiliza en esos casos. Los bloques unsafe permiten acceder directamente a direcciones de memoria mediante punteros. Este enfoque ofrece control de bajo nivel, pero debe utilizarse con precaución, ya que funciona fuera de la protección del Garbage Collector (GC) de .NET.


¿Qué es el código Unsafe?

La palabra clave unsafe indica al compilador de C# que el código trabajará con memoria no administrada. En este tipo de código, el compilador no realiza comprobaciones de seguridad de memoria. Para usar código unsafe, la opción “Allow unsafe code” debe estar habilitada en la configuración del proyecto.


// Propiedad que debe agregarse al archivo .csproj:
<PropertyGroup>
  <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>

¿Qué es un puntero?

Un puntero es una variable que almacena la dirección de memoria de otra variable. En C#, los punteros solo pueden declararse dentro de bloques unsafe y se utilizan en la pila (stack). El símbolo * define un puntero, y el operador & obtiene la dirección de una variable.


using System;

class Program
{
    static unsafe void Main()
    {
        int x = 42;
        int* ptr = &x; // obtener la dirección de x

        Console.WriteLine($"Valor de x: {*ptr}");
        Console.WriteLine($"Dirección de x: {(ulong)ptr}");
    }
}

La expresión *ptr significa “leer el valor en la dirección apuntada”. En este ejemplo se accede tanto a la dirección como al valor de la variable.


Modificar valores a través de punteros

Se puede modificar directamente el valor de una variable mediante un puntero. Esto funciona igual que en C o C++.


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

    *p = 99; // cambiar el valor de x mediante el puntero
    Console.WriteLine($"Nuevo valor de x: {x}");
}

// Salida:
// Nuevo valor de x: 99

Aritmética de punteros

La aritmética de punteros permite recorrer matrices incrementando direcciones de memoria. Sin embargo, solo puede utilizarse con tipos primitivos como int, byte o double.


unsafe
{
    int[] numeros = { 10, 20, 30, 40 };
    fixed (int* p = numeros)
    {
        for (int i = 0; i < numeros.Length; i++)
            Console.WriteLine($"Dirección: {(ulong)(p + i)}, Valor: {*(p + i)}");
    }
}

La palabra clave fixed fija el arreglo en memoria para que el GC no pueda moverlo. De esta manera, el puntero siempre apunta a la dirección correcta.


La palabra clave fixed

fixed impide que un objeto administrado (como un arreglo o cadena) sea movido por el GC. Permite acceder de forma segura a direcciones de memoria a través de punteros.


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

Nota: cuando el bloque fixed termina, el objeto puede ser movido nuevamente, por lo que usar el puntero fuera de este bloque no es seguro.


Acceso a memoria con struct y punteros

También se puede acceder directamente a la memoria de estructuras (struct) mediante punteros. Esto se utiliza a menudo en escenarios críticos de rendimiento (motores de juego, gráficos, interop nativo, etc.).


using System;

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

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

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

La expresión ptr->X equivale al acceso a estructuras mediante punteros en C/C++.


Asignación de memoria en la pila con stackalloc

stackalloc asigna memoria sin procesar directamente en la pila. Esta memoria se libera automáticamente al final del bloque.


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 es rápido, pero debe usarse solo para datos pequeños. Para grandes volúmenes de datos, se debe preferir la asignación en el heap (new).


¿Cuándo usar código Unsafe?


Comparación de rendimiento

El código unsafe con acceso directo a la memoria puede mejorar el rendimiento entre un 30 % y un 50 % al evitar el GC. Sin embargo, este beneficio viene acompañado de un mayor riesgo. Un acceso incorrecto a memoria puede causar bloqueos o corrupción de datos.


Precauciones importantes


Ejemplo: acceso a datos de píxeles de una imagen

El siguiente ejemplo muestra cómo acceder directamente a los píxeles de un bitmap utilizando un bloque unsafe. Este método realiza manipulaciones de píxeles (por ejemplo, invertir colores) mucho más rápido que las API tradicionales.


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* primerPixel = (byte*)data.Scan0;

        for (int y = 0; y < bmp.Height; y++)
        {
            byte* fila = primerPixel + (y * data.Stride);
            for (int x = 0; x < bmp.Width; x++)
            {
                byte* pixel = fila + (x * bytesPerPixel);
                pixel[0] = (byte)(255 - pixel[0]); // invertir azul
                pixel[1] = (byte)(255 - pixel[1]); // invertir verde
                pixel[2] = (byte)(255 - pixel[2]); // invertir rojo
            }
        }

        bmp.UnlockBits(data);
        bmp.Save("output.png");
        Console.WriteLine("¡La imagen ha sido invertida!");
    }
}

Este ejemplo demuestra el poder del código unsafe para operaciones de manipulación de píxeles de alto rendimiento.


TL;DR

  • Los bloques unsafe en C# permiten el acceso directo a la memoria mediante punteros.
  • La palabra clave fixed evita que el GC mueva los objetos en memoria.
  • stackalloc permite una asignación de memoria rápida y temporal en la pila.
  • Ofrece mejoras de rendimiento, pero conlleva riesgos de seguridad significativos.
  • Se utiliza para la interoperabilidad con código no administrado (C/C++) o la manipulación de memoria de bajo nivel.

Artículos relacionados