Unsafe Rust
Tutto il codice che abbiamo visto fino ad adesso è stato scritto il quello che si chiama safe Rust, ovvero con delle garanzie di memoria che sono fornite dal compilatore. Questo però non è sempre quello che vogliamo, perché il compilatore è molto conservativo sulle assunzioni che fa sul codice, e a volte vogliamo esprimere delle cose non possibili da esprimere in safe Rust. Ad esempio potremmo volere implementare una primitiva di concorrenza molto efficiente, e possiamo garantire che un certo puntatore sarà valido in un certo momento, oppure da una invariante dell'algoritmo possiamo garantire che non ci sono può thread che stanno scrivendo su uno stesso valore. Oppure potremmo volere implementare un firmware a basso livello che si interfaccia direttamente con l'hardware, leggendo da locazioni di memoria dove magari sono mappati delle interfacce I/O.
Rust ha quindi la possibilità di scrivere unsafe Rust, che è fondamentalmente la stessa identica cosa, ma abbiamo delle possibilità in più, che sono:
- possiamo dereferenziare un puntatore raw
- possiamo chiamare una funzione unsafe
- possiamo accedere o modificare una variabile statica mutabile
- possiamo implementare un trait unsafe
- possiamo accedere ai campi di una union.
La cosa più importante è forse la prima, possiamo leggere e scrivere locazioni arbitrarie di memoria, senza che il compilatore faccia dei check. Il borrrow checker e tutte le altre feature del compilatore non sono disattivate, ma chiamando delle primitive apposite è possibile bypassarle. Unsafe Rust è un modo per dire al compilatore "so quello che sto facendo", e in particolare se ci sono dei bug legati alla corruzione di memoria si troveranno per forza all'interno di blocchi unsafe. Per questo è consigliato limitare al massimo l'utilizzo di blocchi unsafe, la maggior parte delle volte per programmi comuni non sarà mai necessario utilizzarli.
Dereferenziare un puntatore raw
Vediamo un esempio di unsafe Rust, supponiamo che vogliamo leggere un byte all'indirizzo di memoria 0x41414141.
In c potremmo scrivere qualcosa del genere
#include <stdio.h>
int main() {
printf("Valore: %d", *((char*)0x41414141));
}
Ovviamente il programma ci restituirà segmentation fault, perché con molta probabilità l'indirizzo non è mappato nella memoria virtuale.
In Rust si può fare una cosa molto simile, ma va effettuata dentro un blocco unsafe, in questo modo.
fn main() { // indirizzo di memoria let address = 0x41414141usize; // raw pointer a quell'indirizzo di memoria let r = address as *const char; unsafe { // dereferenziazione del puntatore println!("Valore è {}", *r); } }
Ovviamente come nel caso del c, il programma restituirà segmentation fault.
Se andiamo a vedere dove c'è il crash ad esempio con valgrind, otteniamo il seguente risultato
==51758== Invalid read of size 4
[...]
==51758== Address 0x41414141 is not stack'd, malloc'd or (recently) free'd
Quindi effettivamente stiamo provando a leggere dall'indirizzo 0x41414141.
Come già detto, nell'uso normale di Rust non capita quasi mai di dover scrivere codice unsafe, tipicamente esiste sempre una alternativa safe. Se proprio dobbiamo ricorrere a unsafe Rust perché magari stiamo implementando una funzionalità particolare, è fortemente consigliato leggere con attenzione la documentazione, e cercare di capire tutte le implicazioni sulla memoria che può avere quella particolare implementazione unsafe.