Keyboard shortcuts

Press or to navigate between chapters

Press S or / to search in the book

Press ? to show this help

Press Esc to hide this help

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:

  1. De-referenziare un puntatore grezzo
  2. Chiamare una funzione o un metodo unsafe
  3. Accedere o modificare una variabile statica mutabile
  4. Implementare un trait unsafe
  5. 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;
}
Listato 20-1: Creazione di puntatori grezzi con gli operatori di prestito grezzi

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;
}
Listato 20-2: Creazione di un puntatore grezzo a un indirizzo di memoria arbitrario

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]);
}
Listato 20-4: Uso della funzione safe 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);
}
Listato 20-5: Tentativo di implementazione di split_at_mut usando solo safe Rust

Questa 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);
}
Listato 20-6: Uso di codice unsafe nell’implementazione della funzione 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) };
}
Listato 20-7: Creazione di una slice da una locazione arbitraria di memoria

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.

File: src/main.rs
unsafe extern "C" {
    fn abs(input: i32) -> i32;
}

fn main() {
    unsafe {
        println!("Valore assoluto di -3 secondo C: {}", abs(-3));
    }
}
Listato 20-8: Dichiarazione e chiamata di una funzione extern definita in un altro linguaggio

Dentro 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.

File: src/main.rs
unsafe extern "C" {
    safe fn abs(input: i32) -> i32;
}

fn main() {
    println!("Valore assoluto di -3 secondo C: {}", abs(-3));
}
Listato 20-9: Marcatura esplicita di una funzione come safe dentro un blocco unsafe extern e chiamata in maniera sicura

Marcare 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.

File: src/main.rs
static HELLO_WORLD: &str = "Hello, world!";

fn main() {
    println!("valore è: {HELLO_WORLD}");
}
Listato 20-10: Definizione e uso di una variabile statica immutabile

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.

File: src/main.rs
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));
    }
}
Listato 20-11: Lettura o scrittura su una variabile statica mutabile è unsafe

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() {}
Listato 20-12: Definizione e implementazione di un trait unsafe

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.