Unsafe in Rust
Tutte le garanzie di cui abbiamo parlato in questo corso (niente riferimenti pendenti, niente data race, niente accessi fuori dai limiti) le fa rispettare il compilatore. unsafe è il modo per dirgli: «lasciami fare una cosa di cui non puoi verificare la sicurezza; me ne prendo io la responsabilità».
Cosa fa unsafe, e cosa non fa
Sezione intitolata “Cosa fa unsafe, e cosa non fa”Un equivoco comune è pensare che unsafe spenga del tutto i controlli di sicurezza di Rust. Non è così. Dentro un blocco unsafe puoi fare esattamente cinque cose in più, che altrimenti sono vietate:
- Dereferenziare un puntatore grezzo (raw pointer)
- Chiamare una funzione
unsafe - Leggere o modificare una variabile
staticmutabile - Implementare un trait
unsafe - Accedere ai campi di una
union
Tutto il resto (le regole del prestito, la semantica dello spostamento, il controllo dei tipi) vale ancora per intero dentro il codice unsafe. È un bisturi per cinque operazioni precise, non un interruttore per spegnere tutto.
Puntatori grezzi
Sezione intitolata “Puntatori grezzi”I riferimenti normali di Rust (& e &mut) puntano sempre a dati validi e allineati correttamente: fa parte di ciò che il compilatore garantisce. Un puntatore grezzo (*const T o *mut T) rinuncia a questa garanzia, ed è per questo che leggerlo richiede unsafe:
fn main() { let value = 5; let pointer = &value as *const i32;
unsafe { println!("{}", *pointer); }}Output:
5Puoi creare un puntatore grezzo nel codice sicuro: solo dereferenziarlo (cioè leggere il valore a cui punta) richiede unsafe, perché il compilatore non può più verificare che il puntatore sia valido.
Chiamare una funzione unsafe
Sezione intitolata “Chiamare una funzione unsafe”Alcune funzioni sono marcate unsafe perché hanno dei requisiti che il compilatore non può controllare al posto tuo. Chiamarle è la tua promessa di aver verificato quei requisiti da solo:
unsafe fn dangerous() { println!("questa funzione si fida che tu abbia controllato i suoi requisiti");}
fn main() { unsafe { dangerous(); }}Output:
questa funzione si fida che tu abbia controllato i suoi requisitiAvvolgere unsafe in una funzione sicura
Sezione intitolata “Avvolgere unsafe in una funzione sicura”La libreria standard ha delle vere funzioni unsafe che puoi chiamare. Una è get_unchecked sulle slice. Funziona come numbers[i], ma salta il controllo dei limiti. Se i è fuori intervallo, non va in panic: legge qualunque cosa si trovi in quella memoria. Questo si chiama comportamento indefinito (undefined behavior): il programma può andare in crash, stampare spazzatura o sembrare a posto finché non lo è più.
Quindi la regola per chiamarla è «solo con un indice che sai essere valido». Lo schema tipico è controllare tu stesso questa regola, dentro una normale funzione sicura:
fn first_or_zero(numbers: &[i32]) -> i32 { if numbers.is_empty() { return 0; } // SAFETY: la slice non è vuota, quindi l'indice 0 è nei limiti. unsafe { *numbers.get_unchecked(0) }}
fn main() { println!("{}", first_or_zero(&[7, 8, 9])); println!("{}", first_or_zero(&[]));}Output:
70Guarda cosa vede chi la chiama: fn first_or_zero(numbers: &[i32]) -> i32. Nella firma non c’è nessun unsafe, e non c’è modo di chiamarla male, perché la funzione controlla la regola prima ancora di arrivare al blocco unsafe. L’unsafe resta piccolo e nascosto all’interno.
Due abitudini che seguono gli sviluppatori Rust esperti:
- Scrivi un commento
// SAFETY:sopra ogni bloccounsafe, spiegando perché le regole sono rispettate. Serve a chi leggerà il codice dopo di te, e anche a te tra qualche mese. - Tieni il blocco
unsafeil più piccolo possibile, così c’è meno codice da ricontrollare.
A essere onesti, nel codice vero non scriveresti proprio questa funzione: numbers.first() fa lo stesso lavoro in modo sicuro, e il compilatore è bravissimo a eliminare i controlli dei limiti quando può dimostrare che non servono. Ma lo schema (controlla le regole, poi chiama unsafe dentro una funzione sicura) è esattamente il modo in cui sono costruiti Vec, String e compagnia. La pagina sull’FFI usa lo stesso trucco per avvolgere le funzioni C.
Perché esiste unsafe
Sezione intitolata “Perché esiste unsafe”Se Rust punta tutto sulla sicurezza, perché permettere una cosa del genere? Perché alcune cose sono davvero impossibili da dimostrare sicure in fase di compilazione, anche se in pratica lo sono: parlare direttamente con l’hardware, chiamare una libreria C (vedi FFI) o costruire certe strutture dati ad alte prestazioni. unsafe permette alla libreria standard (e, raramente, al tuo codice) di implementare questi pezzi di basso livello, mentre tutto ciò che ci viene costruito sopra resta normale Rust sicuro, controllato in ogni dettaglio.
Il compromesso, detto onestamente
Sezione intitolata “Il compromesso, detto onestamente”Scrivere unsafe non significa che ora il tuo codice abbia dei bug. Significa che, in quel blocco preciso, il compilatore non può più scovare certe categorie di errori al posto tuo, e tocca a te mantenere a mano le stesse garanzie. La maggior parte degli sviluppatori Rust esperti scrive pochissimo codice unsafe direttamente: di solito vive in piccole sacche, revisionate con cura, in fondo a una libreria, avvolte in un’API sicura che tutti gli altri possono usare.
Pratica
Sezione intitolata “Pratica”1. Cambiare un valore tramite un puntatore grezzo
Sezione intitolata “1. Cambiare un valore tramite un puntatore grezzo”Crea let mut score = 10;, poi un puntatore grezzo *mut i32 verso di esso, e usa il puntatore per aggiungere 5. Infine stampa score. Suggerimento: crea il puntatore con &mut score as *mut i32 e scrivi *pointer += 5 dentro un blocco unsafe.
Mostra la soluzione
fn main() { let mut score = 10; let pointer = &mut score as *mut i32; // crearlo è sicuro
unsafe { *pointer += 5; // scriverci attraverso no }
println!("score = {score}");}Output:
score = 15In pratica è sicuro, perché score è ancora vivo e nient’altro lo sta usando mentre ci scriviamo. Mantenere queste promesse diventa compito tuo nel momento in cui usi unsafe.
2. Un get_or sicuro
Sezione intitolata “2. Un get_or sicuro”Scrivi fn get_or(numbers: &[i32], index: usize, default: i32) -> i32. Restituisce l’elemento in posizione index, oppure default se l’indice è fuori intervallo. Usa get_unchecked per la lettura, e fai in modo che la funzione non possa mai essere chiamata in un modo che violi la sua regola. Suggerimento: confronta prima index < numbers.len() e chiama get_unchecked solo quando è vero.
Mostra la soluzione
fn get_or(numbers: &[i32], index: usize, default: i32) -> i32 { if index < numbers.len() { // SAFETY: abbiamo appena controllato che l'indice è nei limiti. unsafe { *numbers.get_unchecked(index) } } else { default }}
fn main() { let numbers = [10, 20, 30]; println!("{}", get_or(&numbers, 1, -1)); println!("{}", get_or(&numbers, 5, -1));}Output:
20-1L’indice 5 non arriva mai al blocco unsafe, quindi viene gestito in modo sicuro. Per confronto, la versione completamente sicura è numbers.get(index).copied().unwrap_or(default), ed è quella che scriveresti di solito.
Riepilogo
Sezione intitolata “Riepilogo”unsafesblocca esattamente cinque operazioni (soprattutto i puntatori grezzi e la chiamata di funzioni unsafe): tutto il resto viene ancora controllato per intero.- Un puntatore grezzo (
*const T/*mut T) si può creare nel codice sicuro; dereferenziarlo richiedeunsafe. - Avvolgi
unsafein una piccola funzione sicura che controlla prima le regole, e spiega perché è corretta in un commento// SAFETY:. unsafeesiste per poter costruire il codice di basso livello (accesso all’hardware, interoperabilità con C), mentre il codice che lo usa resta sicuro.- Da principiante ti capiterà di rado di scriverlo: cerca prima un’astrazione sicura.
Verifica veloce
Sezione intitolata “Verifica veloce”Verifica veloce
1. Cosa sblocca davvero un blocco unsafe?
2. Puoi creare un puntatore grezzo nel codice sicuro?
3. Da principiante, quanto spesso ti capiterà di scrivere unsafe?
4. Scrivere codice unsafe significa che ora il programma ha dei bug?
Punteggio: 0 / 4
Per approfondire
Sezione intitolata “Per approfondire”- Unsafe Rust — The Rust Book (in inglese): i cinque «superpoteri unsafe» nel dettaglio, compresa la costruzione di un
split_at_mutsicuro sopra i puntatori grezzi. - The Rustonomicon (in inglese): la guida ufficiale per scrivere correttamente codice unsafe. È avanzata, ma il primo capitolo vale davvero la lettura.
- La parola chiave
unsafe— documentazione della libreria standard (in inglese): blocchi, funzioni e trait unsafe, più la convenzione del commento// SAFETY:. - Unsafety — The Rust Reference (in inglese): l’elenco esatto delle operazioni che richiedono
unsafe. slice::get_unchecked— documentazione della libreria standard (in inglese): una vera funzione unsafe, con la sezione «Safety» che spiega la regola da rispettare.