Shopping cart

0

Cart

  • 0 item

Nessun prodotto nel carrello.

All categories

SE VUOI PRENDERE LA CERTIFICAZIONE PER QUESTO CORSO CLICCA QUI

Corso di Rust: Introduzione a Rust non Sicuro

Benvenuti al nostro corso su Rust! Oggi esploreremo Rust non Sicuro, una parte del linguaggio Rust che offre superpoteri extra ma richiede attenzione nell’uso.

Cos’è Rust non Sicuro?

In Rust, il codice viene controllato dal compilatore per garantire la sicurezza della memoria. Ma c’è anche una parte di Rust chiamata unsafe Rust che non impone queste garanzie di sicurezza. Questo è utile quando si devono compiere operazioni particolari che il Rust sicuro non permette.

Perché abbiamo Rust non Sicuro?

Rust non Sicuro esiste perché a volte il compilatore Rust è troppo cauto nel determinare se il codice è sicuro o meno. Quando il compilatore non ha abbastanza informazioni per essere sicuro, rifiuta il codice. In questi casi, si può utilizzare il codice non sicuro per dire al compilatore, “Fidati di me, so cosa sto facendo”. Tuttavia, bisogna fare attenzione: se si usa il codice non sicuro in modo scorretto, possono verificarsi problemi a causa della non sicurezza della memoria.

Un’altra ragione per cui Rust ha un lato “non sicuro” è che l’hardware sottostante del computer è inherentemente non sicuro. Se Rust non permettesse operazioni non sicure, alcune attività non potrebbero essere svolte. Rust deve permettere la programmazione a basso livello del sistema, come l’interazione diretta con il sistema operativo o addirittura la scrittura del proprio sistema operativo. Lavorare con la programmazione a basso livello è uno degli obiettivi del linguaggio. Esploriamo cosa possiamo fare con Rust non Sicuro e come farlo.

Superpoteri non Sicuri

Per passare a Rust non Sicuro, si utilizza la parola chiave unsafe e quindi si inizia un nuovo blocco che contiene il codice non sicuro. In Rust non Sicuro si possono compiere cinque azioni che non si possono fare in Rust sicuro, che chiamiamo superpoteri non sicuri. Questi superpoteri includono la capacità di:

  • Dereferenziare un puntatore grezzo
  • Chiamare una funzione o un metodo non sicuro
  • Accedere o modificare una variabile statica mutabile
  • Implementare un tratto non sicuro
  • Accedere ai campi di unioni

Quando usare il Codice non Sicuro

Utilizzare il codice non sicuro per eseguire una delle cinque azioni (superpoteri) appena discusse non è sbagliato o addirittura scoraggiato. Ma è più complicato ottenere il codice non sicuro corretto perché il compilatore non può garantire la sicurezza della memoria. Quando si ha una ragione per utilizzare il codice non sicuro, si può farlo, e avere l’annotazione unsafe esplicita facilita il rintracciamento delle fonti dei problemi quando si verificano.

Continueremo a esplorare i dettagli di ciascuna azione non sicura e vedremo alcuni esempi di come usarle in modo corretto.

Corso di Rust: Chiamare una Funzione o Metodo non Sicuro

Nel nostro corso su Rust, esploreremo ora come chiamare funzioni non sicure. Questa è la seconda operazione che possiamo eseguire all’interno di un blocco non sicuro. Le funzioni non sicure sembrano esattamente funzioni normali, ma hanno una parola chiave unsafe prima del resto della definizione. Questo indica che la funzione ha dei requisiti che dobbiamo rispettare quando la chiamiamo, perché Rust non può garantire che abbiamo soddisfatto questi requisiti. Chiamando una funzione non sicura all’interno di un blocco non sicuro, stiamo dicendo a Rust che abbiamo letto la documentazione della funzione e ci assumiamo la responsabilità di rispettare i contratti della funzione.

Ecco un esempio di una funzione non sicura chiamata “dangerous” che non fa nulla nel suo corpo:

rust

unsafe fn dangerous() {}

unsafe {
dangerous();
}

Dobbiamo chiamare la funzione “dangerous” all’interno di un blocco non sicuro separato. Se proviamo a chiamare “dangerous” senza il blocco non sicuro, otterremo un errore.

Con il blocco non sicuro, affermiamo a Rust di aver letto la documentazione della funzione, di comprendere come utilizzarla correttamente e di aver verificato di rispettare il contratto della funzione.

Le funzioni non sicure sono effettivamente blocchi non sicuri, quindi per eseguire altre operazioni non sicure all’interno di una funzione non sicura, non è necessario aggiungere un altro blocco non sicuro.

Creare un’astrazione sicura sopra il codice non sicuro

Il fatto che una funzione contenga codice non sicuro non significa che dobbiamo contrassegnare l’intera funzione come non sicura. In realtà, avvolgere il codice non sicuro in una funzione sicura è una comune astrazione. Ad esempio, studiamo la funzione “split_at_mut” della libreria standard, che richiede del codice non sicuro. Esploreremo come potremmo implementarla. Questo metodo sicuro è definito su slice mutabili: prende uno slice e lo divide in due dividendo lo slice all’indice dato come argomento.

rust

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]);

Non possiamo implementare questa funzione utilizzando solo Rust sicuro. Un tentativo potrebbe apparire come nel seguente codice, che non compila. Per semplicità, implementeremo “split_at_mut” come una funzione piuttosto che come un metodo e solo per slice di valori i32 anziché per un tipo generico T.

rust

fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
assert!(mid <= len);

(&mut values[..mid], &mut values[mid..])
}

Quando proviamo a compilare il codice, otteniamo un errore perché il controllore dei prestiti di Rust non può capire che stiamo prendendo in prestito diverse parti dello slice; sa solo che stiamo prendendo in prestito lo stesso slice due volte. Prendere in prestito diverse parti di uno slice è fondamentalmente corretto perché i due slice non si sovrappongono, ma Rust non è abbastanza intelligente da saperlo. Quando sappiamo che il codice è corretto, ma Rust no, è il momento di usare il codice non sicuro.

Nel prossimo esempio, useremo un blocco non sicuro, un puntatore grezzo e alcune chiamate a funzioni non sicure per far funzionare l’implementazione di “split_at_mut”.

rust

use std::slice; fn split_at_mut(values: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = values.len();
let ptr = values.as_mut_ptr(); assert!(mid <= len);

unsafe {
(
slice::from_raw_parts_mut(ptr, mid),
slice::from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}

Il codice utilizza la funzione “from_raw_parts_mut” per creare due nuovi slice da un puntatore grezzo e una lunghezza. Questa funzione è marcata come non sicura perché accetta un puntatore grezzo e deve fidarsi che questo puntatore sia valido. Anche il metodo “add” sui puntatori grezzi è non sicuro, perché deve fidarsi che la posizione di offset sia anche un puntatore valido. Pertanto, dobbiamo mettere un blocco non sicuro intorno alle nostre chiamate a “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 utilizzati all’interno del blocco non sicuro saranno puntatori validi ai dati all’interno dello slice. Questo è un uso accettabile e appropriato di unsafe.

Nota che non dobbiamo contrassegnare la funzione “split_at_mut” risultante come non sicura e possiamo chiamare questa funzione da Rust sicuro. Abbiamo creato un’astrazione sicura per il codice non sicuro con un’implementazione della funzione che utilizza il codice non sicuro in modo sicuro, perché crea solo puntatori validi dai dati a cui questa funzione ha accesso.

Corso di Rust: Utilizzo delle Funzioni esterne per Chiamare Codice Esterno

A volte, il tuo codice Rust potrebbe dover interagire con codice scritto in un’altra lingua. A tal scopo, Rust ha la parola chiave extern che facilita la creazione e l’uso di un’Interfaccia per Funzioni Esterne (FFI). Un FFI è un modo per un linguaggio di programmazione di definire funzioni ed abilitare un diverso (esterno) linguaggio di programmazione a chiamare quelle funzioni.

Nel seguente esempio, dimostriamo come configurare un’integrazione con la funzione abs della libreria standard di C. Le funzioni dichiarate all’interno dei blocchi extern sono sempre non sicure da chiamare dal codice Rust. Il motivo è che altri linguaggi non applicano le regole e le garanzie di Rust, e Rust non può controllarle, quindi la responsabilità ricade sul programmatore per garantirne la sicurezza.

rust

extern "C" {
fn abs(input: i32) -> i32;
}

fn main() {
unsafe {
println!("Valore assoluto di -3 secondo C: {}", abs(-3));
}
}

All’interno del blocco extern “C”, elenchiamo i nomi e le firme delle funzioni esterne di un altro linguaggio che vogliamo chiamare. La parte “C” definisce quale Application Binary Interface (ABI) utilizza la funzione esterna: l’ABI definisce come chiamare la funzione a livello di assembly. L’ABI “C” è il più comune e segue l’ABI del linguaggio di programmazione C.

Chiamare Funzioni Rust da Altri Linguaggi

Possiamo anche utilizzare extern per creare un’interfaccia che consente ad altri linguaggi di chiamare le funzioni Rust. Invece di creare un intero blocco extern, aggiungiamo la parola chiave extern e specifichiamo l’ABI da utilizzare subito prima della parola chiave fn per la funzione rilevante. Dobbiamo anche aggiungere un’annotazione #[no_mangle] per dire al compilatore Rust di non manipolare il nome di questa funzione. La manipolazione è quando un compilatore cambia il nome che abbiamo dato a una funzione in un nome diverso che contiene più informazioni per altre parti del processo di compilazione da consumare ma è meno leggibile. Ogni compilatore di linguaggi di programmazione manipola i nomi in modo leggermente diverso, quindi affinché una funzione Rust possa essere nominata da altri linguaggi, dobbiamo disabilitare la manipolazione del nome del compilatore Rust.

rust

#[no_mangle]
pub extern "C" fn chiamata_da_c() {
println!("Appena chiamata una funzione Rust da C!");
}

Questo utilizzo di extern non richiede unsafe.

Accesso o Modifica di una Variabile Statica Mutabile

In questo corso, non abbiamo ancora parlato delle variabili globali, che Rust supporta ma possono essere problematiche con le regole di proprietà di Rust. Se due thread accedono alla stessa variabile globale mutabile, può causare una race condition.

In Rust, le variabili globali sono chiamate variabili statiche. Nel seguente esempio, mostriamo una dichiarazione e un uso di una variabile statica con un segmento di stringa come valore.

rust

static HELLO_WORLD: &str = "Ciao, mondo!";

fn main() {
println!("Nome: {}", HELLO_WORLD);
}

Le variabili statiche sono simili alle costanti, di cui abbiamo discusso nella sezione “Differenze tra Variabili e Costanti” nel Capitolo 3. I nomi delle variabili statiche sono in SCREAMING_SNAKE_CASE per convenzione. Le variabili statiche possono solo memorizzare riferimenti con durata ‘statica, il che significa che il compilatore Rust può capire la durata e non siamo tenuti ad 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. Utilizzare il valore accederà sempre agli stessi dati. Le costanti, d’altra parte, possono duplicare i loro dati ogni volta che vengono utilizzate. Un’altra differenza è che le variabili statiche possono essere mutabili. L’accesso e la modifica di variabili statiche mutabili sono non sicuri.

rust

static mut COUNTER: u32 = 0; fn aggiungi_al_conto(inc: u32) {
unsafe {
COUNTER += inc;
}
} fn main() {
aggiungi_al_conto(3);

unsafe {
println!("CONTATORE: {}", COUNTER);
}
}

Come con le variabili regolari, specificare la mutabilità utilizzando la parola chiave mut. Qualsiasi codice che legge o scrive da COUNTER deve essere all’interno di un blocco unsafe. Questo codice viene compilato e stampa CONTATORE: 3 come ci aspettiamo perché è single-threaded. Avere più thread che accedono a COUNTER probabilmente comporterebbe race condition.

Con dati mutabili che sono globalmente accessibili, è difficile garantire che non ci siano race condition, motivo per cui Rust considera le variabili statiche mutabili non sicure. Quando possibile, è preferibile utilizzare le tecniche di concorrenza e i puntatori intelligenti thread-safe di cui abbiamo discusso nel Capitolo 16 in modo che il compilatore controlli che i dati accessi da diversi thread siano utilizzati in modo sicuro.

1 Comment

Leave a Reply

Il tuo indirizzo email non sarà pubblicato. I campi obbligatori sono contrassegnati *