È uscito il Corso SQL Completo

Unsafe in Rust

Il blocco unsafe in Rust permette di eseguire operazioni che il compilatore non può verificare come sicure. Unsafe non disattiva il borrow checker: sblocca solo specifiche capacità aggiuntive.

Cosa Permette Unsafe

Ci sono 5 operazioni possibili solo in un blocco unsafe:

  1. Dereferenziare puntatori raw
  2. Chiamare funzioni unsafe
  3. Accedere a variabili statiche mutabili
  4. Implementare trait unsafe
  5. Accedere a campi di union

Puntatori Raw

fn main() {
    let mut valore = 42;

    // Creare puntatori raw è sicuro
    let ptr_imm: *const i32 = &valore;
    let ptr_mut: *mut i32 = &mut valore;

    // Dereferenziarli richiede unsafe
    unsafe {
        println!("Valore: {}", *ptr_imm);
        *ptr_mut = 100;
        println!("Nuovo valore: {}", *ptr_mut);
    }
}

Funzioni Unsafe

unsafe fn operazione_pericolosa() -> i32 {
    // Codice che il compilatore non può verificare
    42
}

fn main() {
    let risultato = unsafe { operazione_pericolosa() };
    println!("{}", risultato);
}

Variabili Statiche Mutabili

static mut CONTATORE: u32 = 0;

fn incrementa() {
    unsafe {
        CONTATORE += 1;
    }
}

fn leggi() -> u32 {
    unsafe { CONTATORE }
}

fn main() {
    incrementa();
    incrementa();
    println!("Contatore: {}", leggi()); // 2
}

Attenzione: Le variabili statiche mutabili sono intrinsecamente non thread-safe. Preferisci AtomicU32 o Mutex.

Trait Unsafe

unsafe trait MioTraitUnsafe {
    fn operazione(&self);
}

unsafe impl MioTraitUnsafe for i32 {
    fn operazione(&self) {
        println!("Valore: {}", self);
    }
}

I trait Send e Sync sono esempi di trait unsafe nella libreria standard.

FFI (Foreign Function Interface)

Interfacciarsi con codice C richiede unsafe:

extern "C" {
    fn abs(input: i32) -> i32;
    fn strlen(s: *const u8) -> usize;
}

fn main() {
    let risultato = unsafe { abs(-42) };
    println!("Valore assoluto: {}", risultato); // 42
}

// Esporre funzioni Rust per C
#[no_mangle]
pub extern "C" fn somma_rust(a: i32, b: i32) -> i32 {
    a + b
}

Astrazioni Sicure su Unsafe

Il pattern corretto è avvolgere il codice unsafe in un’API sicura:

fn dividi_slice(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
    assert!(mid <= slice.len());

    let ptr = slice.as_mut_ptr();
    let len = slice.len();

    unsafe {
        (
            std::slice::from_raw_parts_mut(ptr, mid),
            std::slice::from_raw_parts_mut(ptr.add(mid), len - mid),
        )
    }
}

fn main() {
    let mut dati = vec![1, 2, 3, 4, 5];
    let (prima, seconda) = dividi_slice(&mut dati, 3);
    println!("{:?} {:?}", prima, seconda); // [1, 2, 3] [4, 5]
}

Linee Guida

  • Minimizza la quantità di codice dentro unsafe
  • Documenta le invarianti che il codice unsafe assume
  • Avvolgi sempre unsafe in API sicure
  • Usa unsafe solo quando non c’è alternativa sicura
  • Considera crate come libc per FFI e bytemuck per reinterpretazione di byte

Conclusione

unsafe non significa “codice pericoloso” ma “codice la cui sicurezza è responsabilità del programmatore”. Il compilatore continua a controllare tutto il resto. Usa unsafe solo quando necessario, minimizza il suo scope e avvolgilo sempre in astrazioni sicure. La maggior parte del codice Rust non richiede mai unsafe.