Unsafe Code e Puntatori in C#

Edoardo Midali
Edoardo Midali

L’unsafe code in C# permette di scrivere codice che accede direttamente alla memoria utilizzando puntatori, bypassando la gestione automatica della memoria del .NET Framework. Questo approccio offre controllo granulare e prestazioni estreme, ma richiede particolare attenzione per evitare errori di memoria e vulnerabilità di sicurezza.

Introduzione all’Unsafe Code

Abilitazione dell’Unsafe Code

Per utilizzare unsafe code è necessario:

  1. Abilitare nelle opzioni del progetto:
<PropertyGroup>
    <AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
  1. Utilizzare la keyword unsafe:
unsafe
{
    // Codice unsafe qui
}

// Oppure per metodi interi
unsafe static void MetodoUnsafe()
{
    // Tutto il metodo è unsafe
}

Sintassi Base dei Puntatori

unsafe static void EsempioBase()
{
    int numero = 42;
    int* ptr = &numero;        // Ottiene l'indirizzo di numero
    int valore = *ptr;         // Dereferenzia il puntatore

    Console.WriteLine($"Valore: {valore}");           // 42
    Console.WriteLine($"Indirizzo: {(long)ptr:X}");  // Indirizzo in esadecimale

    *ptr = 100;               // Modifica il valore tramite puntatore
    Console.WriteLine($"Nuovo valore: {numero}");    // 100
}

Operazioni con Puntatori

Aritmetica dei Puntatori

unsafe static void AritmeticaPuntatori()
{
    int[] array = {10, 20, 30, 40, 50};

    fixed (int* ptr = array)
    {
        // Accesso sequenziale
        for (int i = 0; i < array.Length; i++)
        {
            Console.WriteLine($"array[{i}] = {*(ptr + i)}");
        }

        // Incremento del puntatore
        int* current = ptr;
        for (int i = 0; i < array.Length; i++)
        {
            Console.WriteLine($"Valore: {*current}");
            current++; // Avanza al prossimo elemento
        }

        // Calcolo della distanza tra puntatori
        int* start = ptr;
        int* end = ptr + array.Length - 1;
        long distanza = end - start;
        Console.WriteLine($"Distanza: {distanza} elementi");
    }
}

Fixed Statement

La keyword fixed impedisce al Garbage Collector di spostare gli oggetti gestiti:

unsafe static void EsempioFixed()
{
    string testo = "Hello World";

    // Fixed per stringhe
    fixed (char* ptr = testo)
    {
        for (int i = 0; i < testo.Length; i++)
        {
            Console.Write(*(ptr + i));
        }
    }

    // Fixed per array
    byte[] buffer = new byte[1024];
    fixed (byte* bufferPtr = buffer)
    {
        // Operazioni veloci sui byte
        for (int i = 0; i < buffer.Length; i++)
        {
            *(bufferPtr + i) = (byte)(i % 256);
        }
    }
}

Allocazione di Memoria Unsafe

stackalloc

unsafe static void EsempioStackalloc()
{
    // Alloca memoria sullo stack (veloce ma limitata)
    int* numeri = stackalloc int[1000];

    // Inizializzazione
    for (int i = 0; i < 1000; i++)
    {
        numeri[i] = i * i;
    }

    // Utilizzo
    int somma = 0;
    for (int i = 0; i < 1000; i++)
    {
        somma += numeri[i];
    }

    Console.WriteLine($"Somma: {somma}");

    // Con Span<T> (più sicuro, C# 7.2+)
    Span<int> span = stackalloc int[1000];
    for (int i = 0; i < span.Length; i++)
    {
        span[i] = i * i;
    }
}

Allocazione Heap Non Gestita

using System.Runtime.InteropServices;

unsafe static void AllocazioneHeap()
{
    // Alloca memoria non gestita
    int* ptr = (int*)Marshal.AllocHGlobal(sizeof(int) * 1000);

    try
    {
        // Inizializzazione
        for (int i = 0; i < 1000; i++)
        {
            ptr[i] = i;
        }

        // Utilizzo
        for (int i = 0; i < 10; i++)
        {
            Console.WriteLine($"ptr[{i}] = {ptr[i]}");
        }
    }
    finally
    {
        // IMPORTANTE: Liberare sempre la memoria
        Marshal.FreeHGlobal((IntPtr)ptr);
    }
}

Struct Unsafe e Layout di Memoria

Struct con Layout Esplicito

[StructLayout(LayoutKind.Explicit)]
unsafe struct Union
{
    [FieldOffset(0)] public int AsInt;
    [FieldOffset(0)] public float AsFloat;
    [FieldOffset(0)] public fixed byte AsBytes[4];

    public void PrintBytes()
    {
        fixed (byte* ptr = AsBytes)
        {
            for (int i = 0; i < 4; i++)
            {
                Console.Write($"{ptr[i]:X2} ");
            }
            Console.WriteLine();
        }
    }
}

unsafe static void EsempioUnion()
{
    Union u = new Union();
    u.AsInt = 0x12345678;

    Console.WriteLine($"Come int: {u.AsInt:X}");
    Console.WriteLine($"Come float: {u.AsFloat}");
    Console.Write("Come bytes: ");
    u.PrintBytes();
}

Fixed Size Buffers

unsafe struct PacketHeader
{
    public ushort PacketType;
    public ushort Length;
    public fixed byte Data[256]; // Buffer di dimensione fissa

    public void SetData(byte[] source)
    {
        if (source.Length > 256)
            throw new ArgumentException("Dati troppo grandi");

        fixed (byte* dest = Data)
        fixed (byte* src = source)
        {
            for (int i = 0; i < source.Length; i++)
            {
                dest[i] = src[i];
            }
        }
    }

    public byte[] GetData()
    {
        byte[] result = new byte[Length];
        fixed (byte* src = Data)
        {
            for (int i = 0; i < Length; i++)
            {
                result[i] = src[i];
            }
        }
        return result;
    }
}

Interoperabilità P/Invoke

Chiamate a API Windows

using System.Runtime.InteropServices;

class NativeInterop
{
    [DllImport("kernel32.dll")]
    static extern IntPtr GetCurrentProcess();

    [DllImport("kernel32.dll")]
    static extern bool ReadProcessMemory(
        IntPtr hProcess,
        IntPtr lpBaseAddress,
        byte[] lpBuffer,
        int dwSize,
        out int lpNumberOfBytesRead);

    [DllImport("msvcrt.dll", CallingConvention = CallingConvention.Cdecl)]
    unsafe static extern int memcmp(void* ptr1, void* ptr2, int count);

    unsafe static void EsempioInterop()
    {
        byte[] array1 = {1, 2, 3, 4, 5};
        byte[] array2 = {1, 2, 3, 4, 6};

        fixed (byte* p1 = array1)
        fixed (byte* p2 = array2)
        {
            int result = memcmp(p1, p2, array1.Length);
            Console.WriteLine($"Confronto: {result}"); // != 0 se diversi
        }
    }
}

Applicazioni Pratiche

Parser Binario ad Alte Prestazioni

unsafe class BinaryParser
{
    public static T ReadStruct<T>(byte[] data, int offset) where T : unmanaged
    {
        fixed (byte* ptr = &data[offset])
        {
            return *(T*)ptr;
        }
    }

    public static void WriteStruct<T>(byte[] data, int offset, T value) where T : unmanaged
    {
        fixed (byte* ptr = &data[offset])
        {
            *(T*)ptr = value;
        }
    }

    public static unsafe void ProcessLargeBuffer(byte[] buffer)
    {
        fixed (byte* ptr = buffer)
        {
            byte* current = ptr;
            byte* end = ptr + buffer.Length;

            // Processamento veloce byte per byte
            while (current < end)
            {
                *current = (byte)(*current ^ 0xFF); // XOR flip
                current++;
            }
        }
    }
}

Copia Memoria Veloce

unsafe class FastMemory
{
    public static void FastCopy(byte[] source, int srcOffset,
                               byte[] dest, int destOffset, int length)
    {
        fixed (byte* srcPtr = &source[srcOffset])
        fixed (byte* destPtr = &dest[destOffset])
        {
            byte* src = srcPtr;
            byte* dst = destPtr;
            byte* end = src + length;

            // Copia in blocchi di 8 byte quando possibile
            while (src + 8 <= end)
            {
                *(long*)dst = *(long*)src;
                src += 8;
                dst += 8;
            }

            // Copia i byte rimanenti
            while (src < end)
            {
                *dst = *src;
                src++;
                dst++;
            }
        }
    }
}

Sicurezza e Best Practices

Validazione e Controlli

unsafe class SafeUnsafe
{
    public static bool ValidatePointer(void* ptr, int size)
    {
        // Controlli di base
        if (ptr == null) return false;
        if (size <= 0) return false;

        // Controllo allineamento (esempio per int)
        if (((long)ptr % sizeof(int)) != 0) return false;

        return true;
    }

    public static void SafePointerOperation(int[] array, int index)
    {
        if (array == null) throw new ArgumentNullException(nameof(array));
        if (index < 0 || index >= array.Length)
            throw new ArgumentOutOfRangeException(nameof(index));

        fixed (int* ptr = array)
        {
            if (ValidatePointer(ptr, sizeof(int)))
            {
                *(ptr + index) = 42;
            }
        }
    }
}

Gestione Eccezioni in Unsafe Code

unsafe class UnsafeExceptionHandling
{
    public static void SafeProcessing(byte[] data)
    {
        if (data == null || data.Length == 0) return;

        fixed (byte* ptr = data)
        {
            try
            {
                // Operazioni potenzialmente pericolose
                ProcessPointer(ptr, data.Length);
            }
            catch (AccessViolationException ex)
            {
                Console.WriteLine($"Errore di accesso alla memoria: {ex.Message}");
                // Log dell'errore, cleanup, ecc.
            }
            catch (Exception ex)
            {
                Console.WriteLine($"Errore generico: {ex.Message}");
                throw; // Re-throw se non gestibile
            }
        }
    }

    private static void ProcessPointer(byte* ptr, int length)
    {
        // Implementazione del processamento
        for (int i = 0; i < length; i++)
        {
            ptr[i] = (byte)(ptr[i] + 1);
        }
    }
}

Limitazioni e Considerazioni

Quando NON Usare Unsafe Code

  • Applicazioni web: Molti hosting provider non permettono unsafe code
  • Codice portabile: Unsafe code è specifico per architetture x86/x64
  • Manutenibilità: Aumenta la complessità e i rischi
  • Sicurezza: Può introdurre vulnerabilità se mal gestito

Alternative Moderne

// Invece di unsafe code, considera:

// 1. Span<T> e Memory<T> per accesso efficiente
Span<byte> span = stackalloc byte[1024];

// 2. System.Numerics.Vector per operazioni SIMD
Vector<int> vector1 = new Vector<int>(new int[] {1, 2, 3, 4});

// 3. ArrayPool per ridurre allocazioni
var pool = ArrayPool<byte>.Shared;
byte[] buffer = pool.Rent(1024);
try
{
    // Usa buffer
}
finally
{
    pool.Return(buffer);
}

Conclusione

L’unsafe code in C# è uno strumento potente per scenari specifici che richiedono prestazioni estreme o interoperabilità diretta con codice nativo. Tuttavia, va utilizzato con estrema cautela, sempre validando gli input, gestendo correttamente la memoria e considerando le alternative moderne come Span e Memory. La regola d’oro è: usare unsafe code solo quando i benefici in termini di performance superano chiaramente i rischi aggiunti in termini di sicurezza e manutenibilità del codice.