Unsafe Rust
Tutto il codice di cui abbiamo parlato finora ha avuto le garanzie di sicurezza della memoria di Rust applicate durante la compilazione. Però, Rust ha un secondo linguaggio nascosto al suo interno che non applica queste garanzie: si chiama unsafe Rust e funziona come il Rust regolare, ma ci dà dei superpoteri extra.
Unsafe Rust esiste perché, per natura, l’analisi statica è conservativa. Quando il compilatore cerca di capire se il codice rispetta le garanzie, è meglio per lui rifiutare qualche programma valido piuttosto che accettarne uno non valido. Anche se il codice potrebbe andare bene, se il compilatore Rust non ha abbastanza informazioni per esserne sicuro, rifiuterà di compilare il codice. In questi casi, puoi usare codice unsafe per dire al compilatore: “Fidati, so cosa sto facendo.” Attenzione però, usare unsafe Rust è un rischio tuo: se usi codice unsafe in modo sbagliato, possono succedere problemi legati alla sicurezza della memoria, tipo il de-referenziamento di puntatori nulli.
Un altro motivo per cui Rust ha un alter ego unsafe è che l’hardware del computer è intrinsecamente unsafe. Se Rust non ti permettesse di fare operazioni unsafe, non potresti fare certi compiti. Rust deve permetterti di fare programmazione di sistema a basso livello, tipo interagire direttamente con il sistema operativo o persino scrivere il tuo sistema operativo. La programmazione di sistema a basso livello è uno degli obiettivi del linguaggio. Vediamo cosa si può fare con unsafe Rust e come farlo.
Usare i Superpoteri Unsafe
Per passare a unsafe Rust, usa la parola chiave unsafe
e poi inizia un nuovo
blocco dove metti il codice unsafe. Ci sono cinque azioni che puoi fare in
unsafe Rust che non puoi fare in safe Rust, che chiamiamo superpoteri
unsafe. Questi superpoteri includono la possibilità di:
- De-referenziare un puntatore grezzo
- Chiamare una funzione o un metodo unsafe
- Accedere o modificare una variabile statica mutabile
- Implementare un trait unsafe
- Accedere ai campi di
union
È importante capire che unsafe
non disabilita il borrow checker né gli altri
controlli di sicurezza di Rust: se usi un reference in codice unsafe, esso
verrà comunque controllato. La parola chiave unsafe
ti dà solo accesso a
queste cinque caratteristiche che non sono controllate dal compilatore
nell’aspetto della sicurezza della memoria. Avrai comunque un certo grado di
sicurezza dentro un blocco unsafe.
Inoltre, unsafe
non significa che il codice dentro il blocco sia
necessariamente pericoloso o che sicuramente avrà problemi di sicurezza della
memoria: l’intento è che tu, programmatore, garantisca che il codice dentro un
blocco unsafe
accederà alla memoria in modo valido.
Gli esseri umani sbagliano, possono fare errori, ma richiedendo che queste
cinque operazioni unsafe siano usate dentro blocchi annotati con unsafe
,
saprai che ogni errore legato alla sicurezza della memoria deve per forza essere
dentro un blocco unsafe
. Mantieni i blocchi unsafe
piccoli; ne sarai
contento quando dovrai cercare bug di memoria.
Per isolare il codice unsafe il più possibile, è meglio racchiuderlo in
un’astrazione safe e offrire un’API safe, di cui parleremo più avanti nel
capitolo quando esamineremo funzioni e metodi unsafe. Parti della libreria
standard sono implementate come astrazioni safe su codice unsafe che è stato
controllato. Racchiudere il codice unsafe in un’astrazione safe evita che
gli usi di unsafe
vadano a infiltrarsi in tutte le parti del codice dove tu o
i tuoi utenti potreste voler usare la funzionalità scritta con codice unsafe,
perché usare un’astrazione safe è sicuro.
Ora vediamo uno per uno i cinque superpoteri unsafe. Daremo anche un’occhiata ad alcune astrazioni che forniscono una interfaccia safe a codice unsafe.
De-referenziare un Puntatore Grezzo
Nel Capitolo 4, nella sezione “Reference Pendenti”, abbiamo detto che il compilatore si assicura che i reference siano
sempre validi. Unsafe Rust ha due nuovi type chiamati puntatori grezzi
(raw pointer) simili ai reference. Come con i reference, i puntatori
grezzi possono essere immutabili o mutabili e si scrivono *const T
e *mut T
rispettivamente. L’asterisco non è l’operatore di de-referenziazione; fa parte
del nome del type. Nel contesto dei puntatori grezzi, immutabile significa
che il puntatore non può essere assegnato direttamente dopo essere stato
de-referenziato.
Diversamente da reference e puntatori intelligenti, i puntatori grezzi:
- Possono ignorare le regole di borrowing avendo sia puntatori immutabili che mutabili o molteplici puntatori mutabili allo stesso dato
- Non è garantito che puntino a memoria valida
- Possono essere nulli
- Non fanno nessuna pulizia automatica
Rinunciando a far rispettare queste garanzie da parte di Rust, puoi rinunciare alla sicurezza garantita in cambio di maggiori prestazioni o della possibilità di interfacciarti con altri linguaggi o hardware dove le garanzie di Rust non valgono.
Il Listato 20-1 mostra come creare un puntatore grezzo immutabile e uno mutabile.
fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; }
Nota che in questo codice non usiamo la parola chiave unsafe
. Possiamo creare
puntatori grezzi in codice safe; non possiamo però de-referenziarli fuori da
un blocco unsafe, come vedremo tra poco.
Abbiamo creato puntatori grezzi usando gli operatori di prestito grezzi (raw
borrow): &raw const num
crea un puntatore grezzo immutabile *const i32
,
mentre &raw mut num
crea un puntatore grezzo mutabile *mut i32
. Siccome li
abbiamo creati direttamente da una variabile locale, sappiamo che questi
puntatori grezzi sono validi, ma non possiamo fare questa assunzione per
qualsiasi puntatore grezzo.
Per dimostrarlo, creiamo un puntatore grezzo di cui non possiamo essere così
certi che sia valido, usando la parola chiave as
per fare un cast invece di
usare l’operatore di prestito grezzo. Il Listato 20-2 mostra come creare un
puntatore grezzo verso una posizione arbitraria in memoria. Usare un indirizzo
di memoria arbitrario è un comportamento indefinito: potrebbe esserci qualche
dato a quell’indirizzo o magari no, il compilatore potrebbe ottimizzare il
codice e evitare l’accesso alla memoria, oppure il programma potrebbe terminare
con un errore accesso non valido alla memoria. Di solito non c’è una buona
ragione per scrivere codice così, specialmente quando si può usare un operatore
di prestito grezzo, ma è possibile farlo.
fn main() { let indirizzo = 0x012345usize; let r = indirizzo as *const i32; }
Ricorda che possiamo creare puntatori grezzi in codice safe, però non possiamo
de-referenziarli per leggere i dati a cui puntano. Nel Listato 20-3 usiamo
l’operatore di de-referenziazione *
su un puntatore grezzo che richiede un
blocco unsafe.
fn main() { let mut num = 5; let r1 = &raw const num; let r2 = &raw mut num; unsafe { println!("r1 è: {}", *r1); println!("r2 è: {}", *r2); } }
Creare un puntatore non fa danno; è solo quando proviamo a leggere il valore a cui punta che potremmo avere a che fare con un valore invalido.
Nota anche che nei Listati 20-1 e 20-3 abbiamo creato puntatori grezzi *const i32
e *mut i32
dove entrambi puntavano alla stessa locazione di memoria, dove
si trova num
. Se invece avessimo provato a creare un reference immutabile e
uno mutabile a num
, il codice non sarebbe stato compilato perché le regole di
ownership di Rust non permettono un reference mutabile contemporaneamente a
reference immutabili. Con i puntatori grezzi possiamo creare un puntatore
mutabile e uno immutabile sugli stessi dati in memoria e modificarli tramite il
puntatore mutabile, creando potenzialmente una data race. Fai attenzione!
Con tutti questi pericoli, perché mai usare i puntatori grezzi? Un uso molto comune è quando ci si interfaccia con codice in C, come vedremo nella prossima sezione. Un altro caso è quando si costruiscono astrazioni safe che il borrow checker non capisce. Introdurremo le funzioni unsafe e poi vedremo un esempio di astrazione safe che usa codice unsafe.
Chiamare una Funzione o Metodo Unsafe
Il secondo tipo di operazione che puoi fare in un blocco unsafe è chiamare
funzioni unsafe. Le funzioni e i metodi unsafe sembrano esattamente normali
funzioni e metodi, ma hanno unsafe
prima della definizione. La parola chiave
unsafe
qui indica che la funzione ha dei requisiti che dobbiamo rispettare
quando la chiamiamo, perché Rust non può garantire che li rispettiamo. Chiamando
una funzione unsafe dentro un blocco unsafe stiamo dicendo che abbiamo letto
la documentazione di quella funzione e ci assumiamo la responsabilità di
rispettarne i contratti.
Ecco una funzione unsafe chiamata pericolosa
che non fa nulla nel corpo:
fn main() { unsafe fn pericolosa() {} unsafe { pericolosa(); } }
Dobbiamo chiamare la funzione pericolosa
dentro un blocco unsafe
separato.
Se proviamo a chiamarla senza il blocco unsafe, avremo un errore:
$ cargo run
Compiling esempio-unsafe v0.1.0 (file:///progetti/esempio-unsafe)
error[E0133]: call to unsafe function `pericolosa` is unsafe and requires unsafe block
--> src/main.rs:5:5
|
5 | pericolosa();
| ^^^^^^^^^^^^ call to unsafe function
|
= note: consult the function's documentation for information on how to avoid undefined behavior
For more information about this error, try `rustc --explain E0133`.
error: could not compile `esempio-unsafe` (bin "esempio-unsafe") due to 1 previous error
Con il blocco unsafe, stiamo dicendo a Rust che abbiamo letto la documentazione della funzione, sappiamo come usarla correttamente e abbiamo verificato di rispettare il contratto.
Per fare operazioni unsafe dentro una funzione unsafe, serve comunque un blocco unsafe anche dentro il corpo, e il compilatore ti avvertirà se lo dimentichi. Questo ci aiuta a tenere i blocchi unsafe più piccoli possibile, perché spesso non servono in tutto il corpo della funzione.
Creare un’Astrazione Safe su Codice Unsafe
Solo perché una funzione contiene codice unsafe non significa che debba essere
tutta marcata come unsafe. Infatti, incapsulare codice unsafe in una
funzione safe è una pratica comune. Come esempio, studiamo la funzione
split_at_mut
della libreria standard, che richiede un po’ di codice unsafe.
Vedremo come potremmo implementarla. Questo metodo safe è definito per slice
mutabili: prende una slice e la divide in due a partire da un indice passato
come argomento. Il Listato 20-4 mostra come usare split_at_mut
.
fn main() { let mut v = vec![1, 2, 3, 4, 5, 6]; let r = &mut v[..]; let (a, b) = r.split_at_mut(3); assert_eq!(a, &mut [1, 2, 3]); assert_eq!(b, &mut [4, 5, 6]); }
split_at_mut
Non possiamo implementare questa funzione usando solo safe Rust. Una prova
potrebbe essere il Listato 20-5, che non si compila. Per semplicità,
implementeremo split_at_mut
come funzione invece che come metodo, e solo per
slice di i32
, non per un type generico T
.
fn split_at_mut(valori: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = valori.len();
assert!(mid <= len);
(&mut valori[..mid], &mut valori[mid..])
}
fn main() {
let mut vettore = vec![1, 2, 3, 4, 5, 6];
let (sinistra, destra) = split_at_mut(&mut vettore, 3);
}
split_at_mut
usando solo safe RustQuesta funzione prima prende la lunghezza totale della slice. Poi assicura che l’indice passato sia entro la slice, verificando che sia minore o uguale alla lunghezza. Questa asserzione significa che se passiamo un indice maggiore della lunghezza, la funzione farà panic prima di usare quell’indice.
Poi ritorna due slice mutabili in una tupla: una dalla parte iniziale fino a
mid
e l’altra da mid
fino alla fine della slice.
Quando proviamo a compilare il codice in Listato 20-5, otteniamo un errore:
$cargo run
Compiling esempio-unsafe v0.1.0 (file:///progetti/esempio-unsafe)
error[E0499]: cannot borrow `*valori` as mutable more than once at a time
--> src/main.rs:7:31
|
2 | fn split_at_mut(valori: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
| - let's call the lifetime of this reference `'1`
...
7 | (&mut valori[..mid], &mut valori[mid..])
| --------------------------^^^^^^--------
| | | |
| | | second mutable borrow occurs here
| | first mutable borrow occurs here
| returning this value requires that `*valori` is borrowed for `'1`
|
= help: use `.split_at_mut(position)` to obtain two mutable non-overlapping sub-slices
For more information about this error, try `rustc --explain E0499`.
error: could not compile `esempio-unsafe` (bin "esempio-unsafe") due to 1 previous error
Il borrow checker di Rust non capisce che stiamo prendendo due parti diverse della stessa slice; sa solo che stiamo prendendo la stessa slice due volte. Prendere in prestito parti diverse di una stessa slice è fondamentalmente non problematico perché le due slice non si sovrappongono, ma Rust non è abbastanza intelligente da capire questo. Quando sappiamo che il codice va bene, ma Rust no, è ora di usare codice unsafe.
Il Listato 20-6 mostra come usare un blocco unsafe, un puntatore grezzo e
alcune chiamate a funzioni unsafe per far funzionare split_at_mut
.
use std::slice; fn split_at_mut(valori: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) { let len = valori.len(); let ptr = valori.as_mut_ptr(); assert!(mid < len); unsafe { ( slice::from_raw_parts_mut(ptr, mid), slice::from_raw_parts_mut(ptr.add(mid), len - mid), ) } } fn main() { let mut vector = vec![1, 2, 3, 4, 5, 6]; let (left, right) = split_at_mut(&mut vector, 3); }
split_at_mut
Ricordiamo da “Il Type Slice” nel capitolo
4 che una slice è un puntatore a un dato e la sua lunghezza. Usiamo il metodo
len
per avere la lunghezza e il metodo as_mut_ptr
per accedere al puntatore
grezzo della slice. In questo caso, poiché abbiamo una slice mutabile di
i32
, as_mut_ptr
ritorna un puntatore grezzo di type *mut i32
che
conserviamo nella variabile ptr
.
Manteniamo l’asserzione che mid
stia dentro la slice. Poi arriviamo al
codice unsafe: la funzione slice::from_raw_parts_mut
prende un puntatore
grezzo e una lunghezza e crea una slice. La usiamo per creare una slice che
parte da ptr
ed è lunga mid
elementi. Poi chiamiamo il metodo add
su ptr
con mid
come argomento per ottenere un puntatore grezzo che punta a mid
, e
creiamo una slice usando quel puntatore e la lunghezza rimanente dopo mid
.
La funzione slice::from_raw_parts_mut
è unsafe perché prende un puntatore
grezzo e deve fidarsi che quel puntatore sia valido. Anche il metodo add
su
puntatore grezzo è unsafe perché deve fidarsi che la locazione di memoria
puntata sia valida. Per questo abbiamo messo un blocco unsafe attorno alle
nostre chiamate a slice::from_raw_parts_mut
e add
per poterle chiamare.
Guardando il codice e aggiungendo l’asserzione che mid
deve essere minore o
uguale a len
, possiamo dire che tutti i puntatori grezzi usati nel blocco
unsafe saranno validi e punteranno a dati nella slice. Questo è un uso
accettabile e appropriato di unsafe
.
Nota che non dobbiamo marcare la funzione split_at_mut
risultante come
unsafe
e possiamo chiamarla da safe Rust. Abbiamo creato un’astrazione
safe su codice unsafe con un’implementazione che usa codice unsafe in modo
safe, perché crea solo puntatori validi dai dati a cui quella funzione ha
accesso.
Al contrario, usare slice::from_raw_parts_mut
come nel Listato 20-7
probabilmente terminerebbe con un errore quando si usa la slice. Quel codice
prende una locazione arbitraria di memoria e crea una slice lunga 10.000
elementi.
fn main() { use std::slice; let indirizzo = 0x01234usize; let r = indirizzo as *mut i32; let valori: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) }; }
Non abbiamo ownership della memoria in quella posizione arbitraria, e non ci
sono garanzie che la slice così creata contenga valori i32
validi. Usare
quei valori come se fosse una slice valida è comportamento indefinito.
Usare Funzioni extern per Chiamare Codice Esterno
A volte il tuo codice Rust potrebbe aver bisogno di interagire con codice
scritto in un altro linguaggio. Per questo, Rust ha la parola chiave extern
che facilita la creazione e l’uso di una interfaccia per funzioni esterne,
abbreviato in FFI (Foreign Function Interface), cioè un modo per un
linguaggio di definire funzioni e consentire a un diverso linguaggio (esterno)
di chiamarle.
Il Listato 20-8 mostra come impostare l’integrazione con la funzione abs
dalla
libreria standard di C. Le funzioni dichiarate dentro blocchi extern
sono
generalmente unsafe da chiamare da codice Rust, quindi anche i blocchi
extern
devono essere marcati come unsafe. Il motivo è che altri linguaggi
non impongono le regole e garanzie di Rust, e Rust non può controllarle, quindi
la responsabilità è del programmatore.
unsafe extern "C" { fn abs(input: i32) -> i32; } fn main() { unsafe { println!("Valore assoluto di -3 secondo C: {}", abs(-3)); } }
extern
definita in un altro linguaggioDentro il blocco unsafe extern "C"
, elenchiamo nomi e firme delle funzioni
esterne di un altro linguaggio che vogliamo chiamare. La parte "C"
definisce
quale interfaccia binaria dell’applicazione, abbreviato in ABI (application
binary interface), quella funzione usa: l’ABI definisce come chiamare la
funzione a livello assembly. L’ABI "C"
è il più comune ed è l’ABI del
linguaggio C. Informazioni su tutte le ABI supportate da Rust sono disponibili
nella Rust Reference.
Ogni elemento dichiarato dentro un blocco unsafe extern
è implicitamente
unsafe. Però, alcune funzioni FFI sono sicure da chiamare. Per esempio, la
funzione abs
della libreria standard C non ha considerazioni di sicurezza
della memoria di cui preoccuparsi e sappiamo che può essere chiamata con
qualunque i32
. In questi casi possiamo usare la parola chiave safe
per dire
che quella funzione specifica è sicura da chiamare anche se si trova dentro un
blocco unsafe extern
. Dopo questa modifica, chiamarla non richiede più un
blocco unsafe, come mostra il Listato 20-9.
unsafe extern "C" { safe fn abs(input: i32) -> i32; } fn main() { println!("Valore assoluto di -3 secondo C: {}", abs(-3)); }
unsafe extern
e chiamata in maniera sicuraMarcare una funzione come safe
non la rende automaticamente sicura! È come una
promessa a Rust che è sicura. Sta comunque a te fare in modo che la promessa sia
mantenuta!
Chiamare Funzioni Rust da Altri Linguaggi
Possiamo anche usare extern
per creare un’interfaccia che permetta ad altri
linguaggi di chiamare funzioni Rust. Invece di creare un blocco extern
completo, mettiamo la parola chiave extern
e specifichiamo l’ABI da usare
subito prima della parola fn
per la funzione interessata. Dobbiamo anche
aggiungere l’annotazione #[unsafe(no_mangle)]
per disabilitare il mangling
da parte del compilatore per quella funzione. Il mangling è quando un
compilatore cambia il nome di una funzione in un nome diverso che contiene più
info per altre parti della compilazione, ma è meno leggibile dall’uomo. Ogni
linguaggio compila in modo diverso, quindi per permettere a una funzione Rust di
essere chiamata da altri linguaggi dobbiamo disabilitare il name mangling, ma
questo è unsafe perché potrebbero esserci collisioni di nomi tra varie
librerie, quindi sta a noi scegliere un nome sicuro da esportare senza
mangling.
Nell’esempio seguente rendiamo la funzione call_from_c
accessibile da codice
C, dopo essere stata compilata in una libreria condivisa e collegata dal C:
#![allow(unused)] fn main() { #[unsafe(no_mangle)] pub extern "C" fn call_from_c() { println!("Chiamata una funzione Rust da C!"); } }
Questo uso di extern
richiede unsafe
solo nell’attributo, non nel blocco
extern
.
Accedere o Modificare una Variabile Statica Mutabile
Nel libro finora non abbiamo parlato di variabili globali, che Rust supporta ma che possono dare problemi con le regole di ownership. Se due thread accedono contemporaneamente alla stessa variabile globale mutabile, può succedere una data race.
In Rust, le variabili globali si chiamano variabili static. Il Listato 20-10 mostra un esempio di dichiarazione e uso di una variabile statica con una slice di stringa come valore.
static HELLO_WORLD: &str = "Hello, world!"; fn main() { println!("valore è: {HELLO_WORLD}"); }
Le variabili statiche sono simili alle costanti, di cui abbiamo parlato in
“Dichiarare le Costanti” nel Capitolo 3. I nomi
delle variabili statiche sono, per convenzione, in SNAKE_CASE_MAIUSCOLO
. Le
variabili statiche possono contenere solo reference con longevità 'static
,
quindi il compilatore Rust può ricavarne la lifetime e non serve annotarla
esplicitamente. Accedere a una variabile statica immutabile è sicuro.
Una sottile differenza tra costanti e variabili statiche immutabili è che i
valori in una variabile statica hanno un indirizzo fisso in memoria. Usare quel
valore significa sempre accedere agli stessi dati. Le costanti invece possono
duplicare i dati ogni volta che sono usate. Un’altra differenza è che le
variabili statiche possono essere mutabili. Accedere e modificare variabili
statiche mutabili è unsafe. Il Listato 20-11 mostra come dichiarare, accedere
e modificare una variabile statica mutabile chiamata CONTATORE
.
static mut CONTATORE: u32 = 0; /// SAFETY: Chiamarlo da più di un unico thread alla volta è un comportamento /// non definito, *devi* quindi garantire che verra chiamato da un singolo /// thread alla volta unsafe fn aggiungi_a_contatore(inc: u32) { unsafe { CONTATORE += inc; } } fn main() { unsafe { // SAFETY: È chiamato da un singolo thread in `main`. aggiungi_a_contatore(3); println!("CONTATORE: {}", *(&raw const CONTATORE)); } }
Come per le variabili normali, specifichiamo la mutabilità con la parola chiave
mut
. Qualsiasi codice che legge o scrive da CONTATORE
deve stare dentro un
blocco unsafe. Il codice nel Listato 20-11 viene compilato e stampa
CONTATORE: 3
, come ci aspettiamo perché è a singolo thread. Se più thread
accedessero a CONTATORE
probabilmente si creerebbero data races, quindi è un
comportamento indefinito. Per questo dobbiamo marcare tutta la funzione come
unsafe e documentarne i limiti di sicurezza, così chi la chiama sa cosa può e
non può fare in sicurezza.
Quando scriviamo una funzione unsafe, è pratica comune scrivere un commento
che inizi con SAFETY
per spiegare cosa deve fare chi chiama la funzione per
farla funzionare in sicurezza. Allo stesso modo, quando facciamo un’operazione
unsafe, scriviamo un commento con SAFETY
per spiegare come vengono
rispettate le regole di sicurezza.
Il compilatore blocca di default ogni tentativo di creare reference a
variabili statiche mutabili tramite i controlli del linter. Devi quindi o
disabilitare esplicitamente il lint con #[allow(static_mut_refs)]
o accedere
alla variabile statica mutabile tramite un puntatore grezzo creato con uno degli
operatori di prestito grezzi. Questo include i casi in cui il reference è
creato in modo invisibile, come quando è usato in println!
in quel codice.
Richiedere che i reference alle variabili statiche mutabili siano creati
tramite puntatore grezzo rende più evidente quali sono i requisiti di sicurezza
per usarle.
Con dati mutabili che sono accessibili globalmente, è difficile assicurarsi che non ci siano data race, motivo per cui Rust considera le variabili statiche mutabili unsafe. Quando possibile, è preferibile usare tecniche di concorrenza e puntatori intelligenti thread-safe di cui abbiamo parlato nel Capitolo 16, così il compilatore verifica che l’accesso da thread diversi sia sicuro.
Implementare un Trait Unsafe
Possiamo usare unsafe
per implementare un trait unsafe. Un trait è
unsafe quando almeno uno dei suoi metodi ha una proprietà che il compilatore
non può verificare. Dichiariamo un trait unsafe mettendo la parola chiave
unsafe
prima di trait
e marcando anche l’implementazione del trait come
unsafe, come mostra il Listato 20-12.
unsafe trait Foo { // metodi vanno qui } unsafe impl Foo for i32 { // implementazioni dei metodi vanno qui } fn main() {}
Usando unsafe impl
promettiamo che rispetteremo le proprietà che il
compilatore non può verificare.
Per esempio, ricordiamo i trait marcatori Send
e Sync
di cui abbiamo
parlato in “Concorrenza Estensibile con Send
e
Sync
” nel Capitolo 16: il compilatore
implementa automaticamente questi trait se i nostri type sono composti solo
da type che implementano Send
e Sync
. Se implementiamo un type che
contiene un type che non implementa Send
o Sync
, come i puntatori grezzi
ad esempio, e vogliamo marcare quel type come Send
o Sync
, dobbiamo usare
unsafe
. Rust non può verificare che il nostro type rispetti le garanzie per
poterlo spostare in sicurezza tra thread o essere usato da più thread,
quindi dobbiamo fare quei controlli manualmente e indicarlo con unsafe
.
Accedere ai Campi di una Union
L’ultima cosa che si può fare solo con unsafe è accedere ai campi di una
union. Una union è simile a una struct
, ma solo uno dei campi dichiarati è
usato in una certa istanza in un dato momento. Le union sono usate soprattutto
per interfacciarsi con le union di codice C. Accedere ai campi di una union
è unsafe perché Rust non può garantire il tipo dei dati conservati in quel
momento nell’istanza di union. Puoi imparare di più sulle union nella Rust
Reference.
Usare Miri per Controllare il Codice Unsafe
Quando scrivi codice unsafe, potresti voler controllare che quello che hai scritto sia davvero sicuro e corretto. Uno dei modi migliori per farlo è usare Miri, uno strumento ufficiale Rust per rilevare comportamenti indefiniti. Mentre il borrow checker è uno strumento statico che lavora durante la compilazione, Miri è uno strumento dinamico che lavora durante l’esecuzione. Controlla il tuo codice eseguendo il programma o i vari test e rilevando quando violi le regole che conosce su come dovrebbe funzionare Rust.
Usare Miri richiede una build nightly di Rust (di cui parliamo di più
nell’Appendice G: Come è Fatto Rust e “Nightly Rust”).
Puoi installare sia la versione nightly di Rust che lo strumento Miri
digitando rustup +nightly component add miri
. Questo non cambia la versione di
Rust che usa il tuo progetto; aggiunge solo lo strumento al tuo sistema per
poterlo usare quando vuoi. Puoi far girare Miri su un progetto digitando
cargo +nightly miri run
o cargo +nightly miri test
.
Per esempio, guarda cosa succede se lo usiamo con il codice nel Listato 20-7.
$ cargo +nightly miri run
Compiling esempio-unsafe v0.1.0 (file:///progetti/esempio-unsafe)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.41s
Running `/home/utente/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/bin/cargo-miri runner target/miri/x86_64-unknown-linux-gnu/debug/esempio-unsafe`
warning: integer-to-pointer cast
--> src/main.rs:6:13
|
6 | let r = indirizzo as *mut i32;
| ^^^^^^^^^^^^^^^^^^^^^ integer-to-pointer cast
|
= help: this program is using integer-to-pointer casts or (equivalently) `ptr::with_exposed_provenance`, which means that Miri might miss pointer bugs in this program
= help: see https://doc.rust-lang.org/nightly/std/ptr/fn.with_exposed_provenance.html for more details on that operation
= help: to ensure that Miri does not miss bugs in your program, use Strict Provenance APIs (https://doc.rust-lang.org/nightly/std/ptr/index.html#strict-provenance, https://crates.io/crates/sptr) instead
= help: you can then set `MIRIFLAGS=-Zmiri-strict-provenance` to ensure you are not relying on `with_exposed_provenance` semantics
= help: alternatively, `MIRIFLAGS=-Zmiri-permissive-provenance` disables this warning
= note: BACKTRACE:
= note: inside `main` at src/main.rs:6:13: 6:34
error: Undefined Behavior: pointer not dereferenceable: pointer must be dereferenceable for 40000 bytes, but got 0x1234[noalloc] which is a dangling pointer (it has no provenance)
--> src/main.rs:8:35
|
8 | let valori: &[i32] = unsafe { slice::from_raw_parts_mut(r, 10000) };
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ Undefined Behavior occurred here
|
= help: this indicates a bug in the program: it performed an invalid operation, and caused Undefined Behavior
= help: see https://doc.rust-lang.org/nightly/reference/behavior-considered-undefined.html for further information
= note: BACKTRACE:
= note: inside `main` at src/main.rs:8:35: 8:70
note: some details are omitted, run with `MIRIFLAGS=-Zmiri-backtrace=full` for a verbose backtrace
error: aborting due to 1 previous error; 2 warnings emitted
Miri ci avverte correttamente che stiamo facendo un cast da intero a puntatore, che potrebbe essere un problema, ma Miri non può sapere se lo è dato che non conosce l’origine del puntatore. Poi Miri segnala un errore perché il Listato 20-7 ha un comportamento indefinito dovuto a un puntatore pendente. Grazie a Miri, sappiamo che c’è un rischio di comportamento indefinito e possiamo pensare a come mettere in sicurezza il codice. In certi casi Miri può perfino suggerire come correggere gli errori.
Miri non cattura tutto quello che potresti sbagliare scrivendo codice unsafe. È uno strumento di analisi dinamica, quindi cattura solo i problemi nel codice che viene realmente eseguito. Questo significa che devi usarlo insieme a buone tecniche di testing per aumentare la fiducia nel codice unsafe che hai scritto. Miri non copre tutti i possibili modi in cui il tuo codice può essere insicuro.
In altre parole: se Miri rileva un problema, sai che c’è un bug, ma non è detto che se Miri non trova bug, il codice sia sicuro. Però in molti casi aiuta davvero tanto. Prova a farlo girare sugli altri esempi di codice unsafe in questo capitolo e vedi cosa dice!
Puoi saperne di più su Miri nel suo repository GitHub.
Quando Usare il Codice Unsafe
Usare unsafe
per sfruttare uno dei cinque superpoteri appena visti non è
sbagliato o malvisto, ma è più difficile scrivere codice unsafe corretto
perché il compilatore non può garantire la sicurezza della memoria. Quando hai
una buona ragione per usare codice unsafe, puoi farlo, e avere la marcatura
esplicita unsafe
ti aiuta a rintracciare più facilmente la fonte di problemi
quando capitano. Ogni volta che scrivi codice unsafe, puoi usare Miri per
essere più sicuro che il codice scritto rispetti le regole di Rust.
Per una trattazione molto più approfondita su come lavorare efficacemente con unsafe Rust, leggi la guida ufficiale di Rust sull’argomento, il Rustonomicon.