Wiki Ubuntu-it

Indice
Partecipa
FAQ
Wiki Blog
------------------
Ubuntu-it.org
Forum
Chiedi
Chat
Cerca
Planet
  • Pagina non alterabile
  • Informazioni
  • Allegati
  • Differenze per "Programmazione/Rust"
Differenze tra le versioni 19 e 20
Versione 19 del 18/02/2023 12.30.27
Dimensione: 16052
Commento:
Versione 20 del 25/02/2023 01.32.02
Dimensione: 16052
Autore: jeremie2
Commento:
Le cancellazioni sono segnalate in questo modo. Le aggiunte sono segnalate in questo modo.
Linea 299: Linea 299:
 * [[https://www.youtube.com/playlist?list=PLai5B987bZ9CoVR-QEIN9foz4QCJ0H2Y8|The Rust Lang Book - Let's Get Rusty]]
Linea 300: Linea 301:
 * [[https://www.youtube.com/playlist?list=PLai5B987bZ9CoVR-QEIN9foz4QCJ0H2Y8|The Rust Lang Book - Let's Get Rusty]]


Guida verificata con Ubuntu: 22.04

Problemi in questa pagina? Segnalali in questa discussione

Introduzione

La seguente pagina mostra come installare e gestire il linguaggio di programmazione Rust.

Le sue caratteristiche lo hanno portato ad essere il più probabile erede dei linguaggi C e C++. Rispetto ad essi Rust ha risolto in modo originale le problematiche relative alla gestione manuale della memoria, praticamente bypassandola grazie ai meccanismi di ownership/borrowing senza dover ricorrere al garbage collector.

In particolare Rust è stato impostato per intercettare il maggior numero di errori nel codice in fase di compilazione piuttosto che durante l'esecuzione del software. Un importante ruolo viene svolto dai messaggi di errore del compilatore, che risultano decisamente esaustivi nella gran parte dei casi.

Inoltre il linguaggio si distingue anche grazie a funzionalità e un ecosistema di strumenti al passo coi tempi, come ad esempio Cargo che semplifica e standardizza la creazione e gestione dei progetti software, o la capacità di compilare codice WebAssembly che permette di scrivere single page app senza dover scrivere codice JavaScript (vedere ad esempio il framework Yew), e altro ancora.

Pur nascendo come linguaggio complesso adatto alla scrittura di driver, sistemi operativi (Redox sperimentale), database, ecc., il suo approccio volto alla alla generazione di codice sicuro ha attratto molto interesse anche da altre aree della programmazione.
Il supporto sperimentale a Rust è stato introdotto a partire dalla versione 6.1 del kernel Linux.

Installazione e aggiornamento

Rust può essere installato tramite il pacchetto rustc. Tuttavia per poter usufruire appieno del suo ecosistema è consigliabile utilizzare il pacchetto ufficiale.

  1. Assicurarsi di aver installato i pacchetti curl e build-essential digitando nel terminale:

    sudo apt install curl build-essential
  2. Digitare quindi il comando:

    curl --proto '=https' --tlsv1.3 https://sh.rustup.rs -sSf | sh
    e rispondere a eventuali domande durante la procedura di installazione.
  3. Riavviare il terminale per aggiornare le variabili d'ambiente.
  4. Per verificare che l'installazione sia andata a buon fine, controllare la versione di Rust installata:

    rustc --version

    che dovrebbe restituire una stringa del tipo:

    rustc 1.66.0 (69f9c33d7 2022-12-12)

Per gestire la versione di Rust installata, viene utilizzato il comando rustup, ad esempio:

  • Per aggiornare a una nuova versione digitare:

    rustup update
  • Se si desidera rimuovere Rust è sufficiente digitare:

    rustup self uninstall

rustc

Se si desidera testare velocemente del codice senza dover creare un progetto, lo si può fare attraverso il comando rustc.

  1. Creare un file nella propria Home, ad esempio prova.rs e inserire al suo interno le seguenti linee di codice:

    fn main() {
        println!("Testo di prova 🦀")
    }
  2. Per compilare il file prova.rs ed eseguire il relativo file binario prova, digitare i comandi:

    rustc prova.rs
    ./prova

    Verrà restituita la stringa:

    Testo di prova 🦀

I due precedenti comandi possono essere più comodamente combinati nel seguente modo:

rustc prova.rs && ./prova

Cargo

Cargo è il principale strumento di sviluppo all'interno di Rust attraverso il quale vengono gestiti svariati aspetti: organizzazione dei file di un progetto, gestione librerie aggiuntive, compilazione/esecuzione del codice e altro. Viene qui creato un semplice progetto per passare in rassegna le varie funzionalità.

Gli esempi qui mostrati descrivono un utilizzo basilare di Cargo. Per informazioni più dettagliate fare riferimento alla documentazione ufficiale.

Qui una tabella riassuntiva dei comandi più frequenti:

Comandi

cargo new nome_progetto

Crea una cartella contenente i file del progetto

cargo check

Controlla che il codice sorgente sia compilabile

cargo build

Compila il codice sorgente

cargo run

Compila il codice sorgente ed esegue il binario

cargo build --release

Compila il codice sorgente creando la nuova release del software

cargo test

Esegue unit test

Creazione progetto

Creare nella propria Home il progetto primo_progetto digitando nel terminale:

cargo new primo_progetto

Verrà creata la cartella primo_progetto con i relativi file. Al suo interno in prima battuta spiccheranno i seguenti file:

  • Cargo.toml: contiene i dati relativi al progetto (nome, versione, ecc.) ed eventuali dipendenze.

  • src/: contenente il file main.rs, ossia il file principale del progetto, conterrà eventuali file .rs e sotto-directory per moduli aggiuntivi.

  • .git e .gitignore: presenti sotto forma di file nascosti in quanto in modo predefinito viene impostato un repository Git.

Man a mano che il codice verrà compilato, Cargo si preoccuperà di aggiungere file e directory aggiuntive:

  • target/debug/: qui verrà salvato il file binario ogni volta che il codice sarà compilato durante la prima stesura del software. In questa fase, per rendere la compilazione più veloce, il codice viene compilato senza ottimizzazioni.

  • /target/release/: qui verrà salvato il file binario una volta raggiunta la prima release del software. A discapito di una compilazione più lenta, verranno applicate le ottimizzazioni per rendere più efficiente il software.

  • test/: può essere creata per effettuare unit test. In tal caso al suo interno può essere predisposto un file contenente funzioni che testino il comportamento del codice nelle più svariate casistiche. Il test viene svolto con il comando:

    cargo test

Compilazione ed esecuzione

  1. Spostarsi all'interno della cartella primo_progetto:

    cd primo_progetto
  2. Nel file src/main.rs è già presente un esempio di codice hello world. Per compilarlo ed eseguirlo, digitare:

    cargo run

    Verrà creato l'eseguibile primo_progetto all'interno di target/debug/ e verrà restituita la stringa:

    Hello, world!
    • Se si desidera solo compilare il sorgente, digitare:

      cargo build
    • Se interessa solo sapere se il codice sia compilabile o meno, digitare:

      cargo check
    • Se si è pronti a rilasciare la prima release del progetto, digitare:

      cargo build --release

      Stavolta il file eseguibile sarà creato in target/release/ con le opportune ottimizzazioni.

Crates (librerie esterne)

Si supponga di voler modificare il programma in modo che venga simulato il lancio di un dado. Per generare numeri random da 1 a 6 verrà sfruttata la libreria esterna Rand (in Rust le librerie vengono chiamate Crates).

  1. Aggiornare la sezione [dependencies] del file ~/primo_progetto/Cargo.toml in modo che risulti:

    [dependencies]
    rand = "0.8.5"
  2. Modificare il file ~/primo_progetto/src/main.rs nel seguente modo:

    use rand::Rng;
    
    fn main() {
        let numero = rand::thread_rng().gen_range(1..=6);
    
        match numero {
            6 => println!("Hai fatto 6, hai vinto!"),
            _ => println!("Peccato! Hai fatto {numero}"),
        }
    }
  3. Sarà sufficiente eseguire il comando:

    cargo run

    Cargo si occuperà di scaricare Rand ed eventuali dipendenze e quindi procederà a compilare ed eseguire il programma, che restituirà in caso di vittoria:

    Hai fatto 6, hai vinto!

    oppure qualcosa del tipo:

    Peccato! Hai fatto 1

fmt - formato del codice

fmt è uno strumento che si occupa di formattare il codice seguendo le linee guida. Attraverso un comando possono essere reimpostati: spazi, indentazione, posizioni delle parentesi, ecc.
Si supponga di avere del codice scritto in modo disordinato (comunque valido per la compilazione):

  • fn main(
    ) {
    let a = 8; let b = 10; println!(
    "Valore di a: {a}"
    ); println!("Valore di b: {b}"
    );}

Attraverso il comando:

  • cargo fmt

il codice verrà riordinato migliorandone la lettura:

  • fn main() {
        let a = 8;
        let b = 10;
        println!("Valore di a: {a}");
        println!("Valore di b: {b}");
    }

Clippy - suggerimenti per il codice

Clippy è uno strumento che indica modi più convenienti per scrivere il codice.
Si supponga di avere le seguenti linee:

  • fn main() {
        let a = 8;
        let vero_o_falso = if a % 2 == 0 { true } else { false };
        println!("a è pari: {}", vero_o_falso);
    }

Eseguendo il comando:

  • cargo clippy

nel messaggio che verrà restituito, verrà fatto notare che la parte if/else è superflua dal momento che già a % 2 == 0 restituisce un valore booleano:

  • warning: this if-then-else expression returns a bool literal
          let vero_o_falso = if a % 2 == 0 { 
     ________________________^
    |         true } else { false};
    |____________________________^ help: you can reduce it to: `a % 2 == 0`

Oltre a questo la variabile vero_o_falso può essere scritta direttamente nelle parentesi graffe interne alla macro println!:

  • warning: variables can be used directly in the `format!` string
    
     -     println!("a è pari: {}", vero_o_falso);
     +     println!("a è pari: {vero_o_falso}");

Andando a correggere il codice si ottiene:

  • fn main() {
        let a = 8;
        let vero_o_falso = a % 2 == 0;
        println!("a è pari: {vero_o_falso}");
    }

Ownership e borrowing

Il seguente paragrafo non intende essere una guida alla programmazione in Rust, per la quale si rimanda alla copiosa documentazione già esistente. Si vuole soltanto mettere in evidenza agli occhi degli utenti meno esperti alcuni aspetti che hanno reso Rust unico nel suo genere e che lo hanno portato a essere la principale alternativa a C/C++ nell'ambito dei linguaggi compilati ad alte prestazioni.

Il seguente codice, apparentemente banale, produrrà un errore:

fn main() {
    let a = "ciao".to_string();
    let b = a;
    println!("{a}");
}

Quello che si è tentato di fare è stato:

  1. assegnare una stringa alla variabile a (in Rust esistono differenti tipi di stringa, qui il riferimento è al tipo String);

  2. passare la variabile a alla variabile b;

  3. stampare il valore della variabile a.

In un passaggio di valore fra due variabili del tipo b = a a seconda del linguaggio di programmazione e del tipo di dato (numero, stringa, array, ecc.), si hanno di solito due possibili situazioni:

  • il valore assegnato a b viene allocato in un differente indirizzo di memoria rispetto a quello di a. Si ha pertanto una copia a sé stante e indipendente.

  • b farà riferimento al medesimo indirizzo di memoria in cui è allocato il valore di a. A quel punto modificare b significa modificare a.

In Rust viene introdotto il concetto di ownership, ossia: può esistere un solo proprietario di un determinato valore.
Nel passaggio b = a non solo b punterà al valore di a, ma andrà a prendersi la proprietà (ownership) della stringa. Per questo motivo, nell'esempio sopra mostrato, la macro println!() non sarà in grado di stampare il valore di a che è stato ceduto a b.

Per poter fare in modo che la variabile a possa essere utilizzata anche in seguito, può essere copiata tramite a.clone(), ma quello che più tipicamente avviene è di impostare una referenza riscrivendo il passaggio b = a nel seguente modo:

let b = &a;

Quello che avviene è che temporaneamente il valore di a viene prestato (borrowing) a b.
Qualora entrassero in gioco modifiche alle variabili, dovrà essere tenuto in conto il concetto di scope, un ambito nel quale durante un borrowing non può essere chiamata in causa la variabile originaria.
Segue un nuovo esempio di codice che genererà un errore:

fn main() {
    let mut a = "ciao".to_string();
    let b = &mut a;
    a.push('!');
    println!("{b}");
}
  1. viene assegnata la stringa "ciao" alla variabile a, stavolta inizializzata come mutabile (mut);

  2. a viene passata a b sfruttando la sintassi per referenziare variabili mutabili &mut;

  3. viene aggiunto il carattere '!' alla stringa "ciao" in modo che divenga "ciao!"

  4. si vuole stampare il valore di b... ma senza successo.

a non può essere chiamata in causa finché il ciclo di utilizzo di b non è terminato, o in altri termini finché non è uscito dal suo scope. Perché il codice possa essere compilato è sufficiente spostare il passaggio a.push('!'):

    ...
    let b = &mut a;
    println!("{b}");
    a.push('!');

Grazie a questo meccanismo in Rust la memoria occupata dalle variabili può essere automaticamente liberata una volta che una variabile ha terminato il suo ciclo all'interno del suo scope.
Sicuramente programmare in questi termini risulta più complesso, ma è altresì vero che non è più necessario gestire manualmente la memoria ne tanto meno ricorrere a un garbage collector, il tutto andando a produrre un codice intrinsecamente più sicuro che elimina alla base intere classi di bug che portano a gravi problemi di sicurezza.

Plugin per editor di testo

Per aggiungere al proprio editor di testo funzionalità come l'auto-completamento del codice, evidenziazione della sintassi, funzionalità di debug e altro, è consigliabile l'utilizzo di rust-analyzer generalmente disponibile per i principali editor di testo.

Documentazione

Libri

Esercizi

Video corsi YouTube

Ulteriori risorse


CategoryProgrammazione