Unsafe Code e Puntatori in C#

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:
- Abilitare nelle opzioni del progetto:
<PropertyGroup>
<AllowUnsafeBlocks>true</AllowUnsafeBlocks>
</PropertyGroup>
- 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 = № // 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