Errori Irreversibili con panic!
A volte si verificano problemi nel codice e non c’è nulla che si possa fare al
riguardo. In questi casi, Rust dispone della macro panic!
. Esistono
praticamente due modi per causare un panic: eseguendo un’azione che causa il
panic del codice (come tentare di accedere a un array oltre la fine) o
chiamando esplicitamente la macro panic!
. In entrambi i casi, causiamo un
panic nel nostro programma. Per impostazione predefinita, questi panic
stampano un messaggio di errore, eseguono un unwind, puliscono lo stack e
terminano. Tramite una variabile d’ambiente, è anche possibile fare in modo che
Rust visualizzi lo stack delle chiamate quando si verifica un panic, per
facilitare l’individuazione della causa del panic.
Unwinding dello Stack o Interruzione in Risposta a un Panic
Per impostazione predefinita, quando si verifica un panic il programma avvia
l’unwinding, il che significa che Rust risale lo stack e pulisce i dati da
ogni funzione che incontra. Tuttavia, risalire lo stack e pulire richiede
molto lavoro. Rust, quindi, consente di scegliere l’alternativa di abortire
immediatamente, che termina il programma senza pulizia. La memoria che il
programma stava utilizzando dovrà quindi essere ripulita dal sistema
operativo. Se nel progetto è necessario ridurre al minimo il binario
risultante, è possibile passare dall’unwinding all’interruzione in caso di
panic aggiungendo panic = 'abort'
alle sezioni [profile]
appropriate nel
file Cargo.toml. Ad esempio, se si desidera interrompere l’esecuzione in
caso di panic nell’eseguibile finale (release) ma non in fase di sviluppo,
aggiungi quanto segue:
[profile.release]
panic = 'abort'
Proviamo a chiamare panic!
in un semplice programma:
fn main() { panic!("crepa e brucia"); }
Quando si esegue il programma, si vedrà qualcosa di simile a questo:
$ cargo run
Compiling panic v0.1.0 (file:///progetti/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.25s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:2:5:
crepa e brucia
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
La chiamata a panic!
causa il messaggio di errore contenuto nelle ultime
righe. La prima riga mostra il punto del codice sorgente in cui si è verificato
l’errore: src/main.rs:2:5 indica che si tratta della seconda riga, quinto
carattere del nostro file src/main.rs.
In questo caso, la riga indicata fa parte del nostro codice e, se andiamo a
quella riga, vediamo la chiamata alla macro panic!
. In altri casi, la chiamata
a panic!
potrebbe essere nel codice richiamato dal nostro codice e il nome del
file e il numero di riga riportati dal messaggio di errore saranno il codice di
qualcun altro in cui viene richiamata la macro panic!
, non la riga del nostro
codice che alla fine ha portato alla chiamata a panic!
.
La seconda riga mostra il nostro messaggio di errore inserito nella chiamata a
panic!
.
Possiamo usare il backtrace (andare a ritroso) delle funzioni da cui
proviene la chiamata panic!
per capire la parte del nostro codice che sta
causando il problema. Per capire come usare un backtrace di panic!
, diamo
un’occhiata a un altro esempio e vediamo cosa succede quando una chiamata
panic!
proviene da una libreria a causa di un bug nel nostro codice invece
che dal nostro codice che chiama direttamente la macro. Il Listato 9-1 contiene
del codice che tenta di accedere a un indice in un vettore oltre l’intervallo di
indici validi.
fn main() { let v = vec![1, 2, 3]; v[99]; }
panic!
Qui stiamo tentando di accedere al centesimo elemento del nostro vettore (che si
trova all’indice 99 perché l’indicizzazione inizia da zero), ma il vettore ha
solo tre elementi. In questa situazione, Rust andrà in panic. L’utilizzo di
[]
dovrebbe restituire un elemento, ma se si passa un indice non valido, non
c’è alcun elemento valido che Rust potrebbe restituire.
In C, tentare di leggere oltre la fine di una struttura dati è un comportamento indefinito. Si potrebbe ottenere qualsiasi cosa si trovi nella posizione in memoria che corrisponderebbe a quell’elemento nella struttura dati, anche se la memoria non appartiene a quella struttura. Questo è chiamato buffer overread (lettura oltre il buffer) e può portare a vulnerabilità di sicurezza se un aggressore riesce a manipolare l’indice in modo tale da leggere dati che non dovrebbe essere autorizzato a leggere e che sono memorizzati dopo la struttura dati.
Per proteggere il programma da questo tipo di vulnerabilità, se si tenta di leggere un elemento in un indice che non esiste, Rust interromperà l’esecuzione e si rifiuterà di continuare. Proviamo e vediamo:
$ cargo run
Compiling panic v0.1.0 (file:///progetti/panic)
Finished `dev` profile [unoptimized + debuginfo] target(s) in 0.37s
Running `target/debug/panic`
thread 'main' panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
note: run with `RUST_BACKTRACE=1` environment variable to display a backtrace
Questo errore punta alla riga 4 del nostro main.rs, dove tentiamo di accedere
all’indice 99 del vettore in v
.
La riga note:
ci dice che possiamo impostare la variabile d’ambiente
RUST_BACKTRACE
per ottenere un backtrace di ciò che ha causato l’errore. Un
backtrace è un elenco di tutte le funzioni che sono state chiamate per
arrivare a questo punto. I backtrace in Rust funzionano come in altri
linguaggi: la chiave per leggere il backtrace è iniziare dall’inizio e leggere
fino a quando non si vedono i file che si sono creati. Quello è il punto in cui
si è originato il problema. Le righe sopra quel punto sono il codice che il tuo
codice ha chiamato; le righe sottostanti sono il codice che ha chiamato il tuo
codice. Queste righe prima e dopo potrebbero includere codice Rust core, codice
di libreria standard o pacchetti che stai utilizzando. Proviamo a ottenere un
backtrace impostando la variabile d’ambiente RUST_BACKTRACE
su un valore
qualsiasi tranne 0
. Il Listato 9-2 mostra un output simile a quello che
vedrai.
$ RUST_BACKTRACE=1 cargo run
thread 'main' (11968) panicked at src/main.rs:4:6:
index out of bounds: the len is 3 but the index is 99
stack backtrace:
0: __rustc::rust_begin_unwind
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/std/src/panicking.rs:698:5
1: core::panicking::panic_fmt
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/panicking.rs:75:14
2: core::panicking::panic_bounds_check
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/panicking.rs:280:5
3: <usize as core::slice::index::SliceIndex<[T]>>::index
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/slice/index.rs:274:10
4: core::slice::index::<impl core::ops::index::Index<I> for [T]>::index
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/slice/index.rs:18:15
5: <alloc::vec::Vec<T,A> as core::ops::index::Index<I>>::index
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/alloc/src/vec/mod.rs:3621:9
6: panic::main
at ./src/main.rs:4:6
7: core::ops::function::FnOnce::call_once
at /rustc/8e62bfd311791bfd9dca886abdfbab07ec54d8b4/library/core/src/ops/function.rs:253:5
note: Some details are omitted, run with `RUST_BACKTRACE=full` for a verbose backtrace.
panic!
viene visualizzato quando la variabile d’ambiente RUST_BACKTRACE
è impostataUn output enorme! L’output esatto che vedi potrebbe variare a seconda del
sistema operativo e della versione di Rust. Per ottenere backtrace con queste
informazioni, i simboli di debug devono essere abilitati. I simboli di debug
sono abilitati per impostazione predefinita quando si utilizza cargo build
o
cargo run
senza il flag --release
, come in questo caso.
Nell’output del Listato 9-2, la riga 6 del backtrace punta alla riga del nostro progetto che causa il problema: la riga 4 di src/main.rs. Se non vogliamo che il nostro programma vada in panic, dovremmo iniziare la nostra analisi dalla posizione indicata dalla prima riga che menziona un file che abbiamo scritto. Nel Listato 9-1, dove abbiamo volutamente scritto codice che andrebbe in panic, il modo per risolvere il problema è non richiedere un elemento oltre l’intervallo degli indici del vettore. Quando in futuro il codice andrà in panic, dovrai capire quale azione sta eseguendo il codice con quali valori tali da causare il panic e cosa dovrebbe fare il codice al suo posto.
Torneremo su panic!
e su quando dovremmo e non dovremmo usare panic!
per
gestire le condizioni di errore nella sezione “panic!
o non
panic!
” più avanti in questo
capitolo. Ora vedremo come gestire un errore utilizzando Result
.