Programmiamo Raspberry Pi con Rust

Raspberry-rust

Mai come negli ultimi anni abbiamo visto nascere (e morire) nuovi linguaggi di programmazione, alcune volte nati come risposta di un'azienda al linguaggio prodotto da un'altra, una sorta di competizione per avere il favore degli sviluppatori. Rust (ruggine) è uno di questi linguaggi, sponsorizzato da Mozilla, la stessa di Firefox, che ha raggiunto una certa maturità anche per essere utilizzato in produzione. Il nostro intento sarà utilizzare Rust per pilotare la GPIO e valutare se il linguaggio potrà esserci utile in alternativa a tutti gli altri linguaggi disponibili per il mini computer inglese.

Introduzione

Rust è nato per soddisfare molte prospettive, orientato alla concorrenza (multithreading), essere sicuro (prevenzione del data-races, cioè accesso alla stessa memoria, e dei deadlock, ovvero blocco di una risorsa che un altro task deve elaborare), veloce (assenza di runtime overhead), multi-paradigma (che può adattarsi con specifici paradigmi di programmazione al problema da risolvere e cioè col supporto alla programmazione orientata agli oggetti, funzionale e procedurale), il tutto compilato. Non è intenzione di questo articolo analizzare se tutte queste aspettative siano soddisfatte e se lo siano in modo appropriato, a noi interessa come Rust lavora con Raspberry Pi e se imparare questo nuovo linguaggio abbia un senso lavorativo. Dopo le prime versioni del compilatore scritto in OCalm, Rust ha compilato se stesso nel 2011 usando LLVM come infrastruttura di progetto. C'è un'intera letteratura su LLVM poiché utilizzato anche da molti altri linguaggi di programmazione. Vi basti sapere che si tratta di una macchina virtuale progettata per l'ottimizzazione di programmi in fase di compilazione, di linking e di esecuzione. Rust è simile al C e al C++ (gli manca anche il Garbage Collector e ne condivide le performance), con blocchi di codice tra parentesi graffe. Ha tutte le classiche parole chiave e istruzioni della maggior parte dei linguaggi come IF, ELSE, WHILE, LOOP, FOR, strutture, vettori, tuple, ecc. e ne introduce altre come MATCH usato al posto di IF/THEN per semplificare e potenziare proprio il controllo di uguaglianza.

Rust-language-tree

Figura 1: Rust-language-tree

Rust è compatibile con quasi tutti i Sistemi Operativi in circolazione e quasi tutti i processori, sia a 32 che 64 bit. Fate solo attenzione a quale livello di supporto sia associata la vostra piattaforma. Per fare un esempio, Linux e Windows sono supportate a livello 1 cioè godono di una garanzia di funzionamento totale comprensiva di test di compilazione e tutto quanto serve a rilasciare versioni stabili. Piattaforme come OpenBSD a 64bit invece rientrano nel livello 3, cioè non è garantita l'ottimizzazione del compilato, gli aggiornamenti possono non essere puntuali e sono spesso indicati dalla community di programmatori e non direttamente dal team di sviluppo ed infine possono non avere un installatore ma i pacchetti devono essere compilati sulla piattaforma stessa.

Installiamolo

A differenza di alcuni anni fa quando era necessario scaricare i sorgenti e compilarli, ora è possibile installare Rust con una sola riga da terminale:

# curl https://sh.rustup.rs -sSf | sh
installazione-rust

Figura 2: Installazione-rust

Vi verrà richiesto di selezionare una delle tre voci di menu (installa, personalizza, cancella), confermate l'installazione con 1 e tutto filerà liscio. Oltre al compilatore sarà installato anche Cargo, il gestore delle dipendenze e della compilazione dei progetti, un concetto che affronteremo più avanti in un paragrafo apposito.

Oltre ai messaggi a video che confermeranno se tutto è corretto, per controllare che l'installazione abbia avuto successo potete eseguire il compilatore con l'opzione di visualizzazione della versione:

# rustc -V  (oppure rustc --version)

Si vedrà il numero di versione, l'hash di commit e la data di commit. Se è così, Rust è stato installato con successo! Attualmente, siamo arrivati alla versione 1.31.0 ma gli aggiornamenti sono frequenti.

In caso di non riconoscimento del comando significa che il percorso del compilatore non è in $PATH, date il seguente comando e riprovate a lanciare il compilatore con l'opzione della versione.

# source $HOME/.cargo/env

La versione da me utilizzata è la 1.23.0. Nella foto sopra vedete la versione 1.14 di quando installai Rust la prima volta per cui qualche messaggio a video potrebbe cambiare.

Per mantenere aggiornato compilatore e librerie potete utilizzare il comando:

# rustup update

Hello World

Partiamo subito con il classico Hello World per iniziare a comprendere sintassi e logica del linguaggio. Aprite l'editor nano, scriveteci il seguente codice e salvate col nome ciaomondo.rs:

// Funzione principale dalla quale inizia ogni programma
fn main() {
  // Stampa il testo desiderato
  println!("Ciao Mondo!");
}

Già visivamente sono certo che avrete capito l'influenza del C nella sintassi. Main è la funzione principale (fn identifica proprio questo) e all'interno di essa troviamo una macro per stampare del testo a video (println!). Avete letto bene, non è una istruzione ma una macro, si differenzia per il punto esclamativo che la identifica come tale. Rust infatti permette di utilizzare macro al posto delle istruzioni traendo da esse alcuni vantaggi. Prima di tutto, non generano chiamate come le istruzioni o le funzioni, in secondo luogo vengono sostituite dal relativo codice e compilate, in pratica println! viene sostituita dal relativo codice e compilato direttamente con un vantaggio in prestazioni.

Adesso, compiliamo e vediamo il risultato:

# rustc ciaomondo.rs

# ./ciaomondo 

Ciao Mondo!

Le basi del linguaggio

Bene, ora affrontiamo variabili, stringhe e istruzioni di base.

Rust prevede un tipo di variabile immutabile, cioè costante (variable binding) e una mutabile, cioè modificabile. Per dichiarare una variabile dobbiamo usare Let mentre per dichiararne una modificabile dobbiamo usare Let mut. Facciamo subito un esempio e analizziamolo:

fn main() {
  // intero, si puo' anche omettere u32 e lasciare solo 1
  let intero = 1u32;
  // booleano, si puo' scrivere anche "let booleano: bool = true;"
  let booleano = true;

  // assegna a copia il valore di intero,
  // verra' visualizzato un Warning poiche'
  // la variabile non viene usata nel resto
  // del programma
  let copia_non_usata = intero;

  println!("Intero: {:?}", intero);
  println!("Booleano: {:?}", booleano);

  // questa variabile e' modificabile
  let mut modificabile = 10u32;

  modificabile -= 5;
  println!("Modificabile: {:?}", modificabile);
}

Apriamo nano, scriviamo il codice sopra e salviamo come "variabili.rs".

Leggiamo bene ogni singola riga, per prima cosa definiamo un intero e un valore booleano. L'intero ha valore 1 e viene indicato come intero a 32 bit (u32), per il valore booleano invece basta indicare se true o false. In Rust gli interi con segno sono i seguenti: i8/i16/i32/i64/isize; gli interi senza segno sono: u8/u16/u32/u64/usize; e i valori a virgola mobile (float) sono: f32/f64.

Poi incontriamo l'assegnazione di una variabile copia del valore intero, per adesso continuiamo nell'analisi, vedremo fra poco che questa assegnazione produce un Warning.

Le due righe successive dovrebbero essere già chiare, stampiamo a video i valori di intero e booleano. Esattamente come altri linguaggi l'utilizzo di {:?} indica dove visualizzare il valore.

Adesso incontriamo una variabile modificabile, notate l'aggiunta di mut dopo let alla quale assegniamo il valore 10. Subito sotto verifichiamo che sia proprio modificabile sottraendo dalla stessa il valore 5. Ci aspettiamo quindi che a video venga poi stampato il risultato di 10 - 5.

Ok, verifichiamo il tutto compilando:

# rustc variabili.rs

Ops, come accennato abbiamo una variabile che poi non è utilizzata. In questo caso il compilatore genererà un Warning come rappresentato qui di seguito:

Rust

Figura 3: Warning in Rust

La gestione degli errori in Rust è alquanto potente, non solo vi informa che una variabile non è utilizzata ma viene anche suggerito come correggere la cosa, cioè aggiungendo al nome della variabile stessa un trattino basso (underscore). Va bene, correggiamo aggiungendo l'underscore e ricompiliamo:

Rust

Figura 4: Adesso è corretto

Ecco quindi che tutto è corretto.

Cargo e crates

Rust permette di gestire i progetti in modo migliore rispetto ad altri linguaggi, prima abbiamo visto come realizzare un programma in modo classico, adesso faremo la conoscenza di Cargo, cioè un sistema di creazione/gestione dei progetti software. Cargo permette di gestire meglio grandi progetti, esso crea un file di testo formattato contenente tutte le informazioni necessarie a compilare il progetto stesso. In questo file si trovano dipendenze, versioni, informazioni varie su programma, ecc. Un esempio vi farà comprendere al volo.

# cargo new ciao_mondo --bin

Questo comando creerà una cartella "ciao_mondo" con all'interno un file Cargo.toml e una sottocartella "src". All'interno di src abbiamo un file main.rs, cioè il file di partenza del nostro progetto che conterrà il codice iniziale oppure tutto il programma a seconda di come lo struttureremo. All'interno di main troviamo un classico e semplice codice per stampare "ciao mondo".

Sequenza utilizzo base Cargo

Figura 5: Sequenza utilizzo base Cargo

Per compilare il programma basterà digitare "cargo build". Verrà creata una nuova sotto cartella di nome "target" al cui interno troviamo un'ulteriore cartella "debug" in cui finalmente c'è il nostro progetto compilato che potremo avviare come qualsiasi altro eseguibile Linux: ./ciao_mondo.

Avvio programma con Cargo

Figura 6: Avvio programma con Cargo

Immagino che avrete già compreso l'enorme potenziale dell'uso di Cargo che ovviamente è molto più potente di queste prime e semplici operazioni, per esempio se dalla cartella "ciao_mondo" eseguiamo direttamente "cargo run" effettueremo compilazione ed avvio del programma in un unico passaggio. Per aiutarvi con i comandi di Cargo potete digitare "cargo help" oppure "cargo help <comando>" per ottenere l'elenco completo e la loro descrizione.

Ma, come fa Cargo a sapere cosa compilare e come? Semplice, Cargo legge il file di testo Cargo.toml ed esegue quanto indicato. Il file Cargo.toml ha una specifica struttura. Toml infatti è un formato simile ai file INI oppure ai file YAML cioè un formato per formattare informazioni in file di testo. Toml sta per "Tom's Obvious, Minimal Language" ed è stato creato da Tom Preston-Werner (trovate le specifiche su Github al link https://github.com/toml-lang/toml). Ecco un esempio di Cargo.toml:

[package]
name = "ciao_mondo"
version = "0.1.0"
description = "Esempio di file Toml"
licence = "MIT"
readme = "readme.txt"
keywords = ["esempio", "cargo", "gabriele"]
authors = ["Gabriele Guizzardi <[email protected]>"]

[features]
default = ["mysql"]

[dependencies]
rand = { git = "https://github.com/rust-lang-nursery/rand.git" }

Ho cercato di inserire solo le parole chiave maggiormente usate e più immediate da comprendere, notate per esempio come sono indicate la versione, la descrizione, il tipo di licenza, ecc. del programma. In questo esempio, è interessante notare anche la potenza di integrazione della libreria rand prelevata direttamente da GIT (non è utilizzata dal codice che stampa Ciao Mondo, è solo per mostrare come aggiungere una dipendenza). Cargo è ovviamente più complesso fino al punto da poter gestire i numeri di versione delle dipendenze con caratteri speciali (p.e. indicare 1.2.* significa informare Cargo di usare solo le versioni comprese tra 1.2.1 e 1.3.0), oppure specificare cartelle di destinazione, eseguire test, pubblicare su Crates.io, ecc. Crates.io tecnicamente parlando è un sito Internet. Crates.io è però anche un repository per codice e librerie scritte e scaricabili per Rust. Qui trovate centinaia di librerie da includere nel vostro codice.

Concorrenza

Purtroppo, un semplice articolo come questo non permette di approfondire specifici temi come la gestione della memoria da parte di Rust ma questa potenza del linguaggio è una delle sue caratteristiche e se decidete di utilizzarlo consiglio vivamente di impadronirvene. La concorrenza è la capacità di eseguire più lavori, che possano anche interagire, indipendentemente dal numero di processori. La concorrenza non è deterministica (in gergo si chiama "race conditions" proprio come una gara dove i concorrenti tentano di arrivare primi al traguardo) pertanto un lavoro può essere terminato prima di un altro ed è importante tenerne conto altrimenti si scateneranno errori poi da gestire. Per sopperire a questi casi vengono definite delle sincronizzazioni, per esempio, non iniziare un lavoro fino a che un altro non sia iniziato. La libreria standard di Rust ha diversi strumenti per gestire la concorrenza delle operazioni. Facciamo un rapido esempio pratico per eseguire codice in parallelo con la libreria std::thread.

use std::thread;
use std::time::Duration;

fn main() {
 thread::spawn(|| {
 for i in 1..10 {
 println!("hi number {} from the spawned thread!", i);
 thread::sleep(Duration::from_millis(1));
 }
 });

 for i in 1..5 {
 println!("hi number {} from the main thread!", i);
 thread::sleep(Duration::from_millis(1));
 }
}

Il metodo spawn accetta una funzione closure per eseguirla in un thread. Le funzioni closure, come ricorda il termine, sono funzioni anonime salvabili in una variabile o che possono essere passate come argomenti ad altre funzioni. A differenza delle funzioni classiche, le closure possono acquisire valori dall'ambito in cui vengono chiamate. L'output di questo esempio produrrà una serie di frasi numerate prodotte da spawn e un'altra serie di frasi prodotte dal For.

hi number 1 from the main thread!
hi number 1 from the spawned thread!
hi number 2 from the main thread!
hi number 2 from the spawned thread!
hi number 3 from the main thread!
hi number 3 from the spawned thread!
hi number 4 from the main thread!
hi number 4 from the spawned thread!
hi number 5 from the spawned thread!

Come possiamo notare, le frasi sono mischiate poiché mentre il For era in esecuzione anche lo spawn eseguiva il suo Loop. A questo punto, sarà semplice immaginare che thread::sleep forza un thread a fare una pausa.

Raspberry Pi

Prendiamo ora in esame la libreria rppal che ci permetterà di interagire con la GPIO di Raspberry Pi (per trovarla basta una semplice ricerca con la relativa casella posta in alto nel sito Crates.io). Per poter usare la libreria dobbiamo per prima cosa inserirla nel file cargo.toml in questo modo:

[dependencies]
rppal = "0.8.1"

Poi aggiungere l'importazione della libreria nel nostro codice:

extern crate rppal;

Se la versione è stata aggiornata utilizzate i numeri opportuni. Colleghiamo ora un semplice LED al pin 13.

led-13-raspberry-pi

Figura 7: Collegamento di un semplice LED al pin 13

Per accenderlo possiamo quindi scrivere un codice come nel seguente esempio:

extern crate rppal;

use std::thread;
use std::time::Duration;
use rppal::gpio::{Gpio, Mode, Level};
use rppal::system::DeviceInfo;

// Viene usata la numerazione BCM quindi si tratta della GPIO27 che corrisponde al pin 13.
const GPIO_LED: u8 = 27;

fn main() {
   let device_info = DeviceInfo::new().unwrap();
   println!("Modello: {} (SoC: {})", device_info.model(), device_info.soc());
   letmut gpio = Gpio::new().unwrap();
   gpio.set_mode(GPIO_LED, Mode::Output);

   // Accende e spegne il LED ROSSO
   gpio.write(GPIO_LED, Level::High);
   thread::sleep(Duration::from_millis(500));
   gpio.write(GPIO_LED, Level::Low);
}

Analizzando il programma abbiamo alcune parti da comprendere bene. La prima parte è relativamente semplice e comune a molti altri linguaggi di programmazione come Python e C, vengono importate le librerie, sia esterne che standard (std), poi viene inizializzata la costante intera senza segno a 8 bit GPIO_LED col corrispondente valore BCM. Inizia poi la funzione principale del programma come descritto precedentemente. Rppal non ha solo metodi per gestire la GPIO ma anche altri, per esempio, come si vede nelle prime due righe, una funzione che restituisce il modello di scheda. A seguire, viene assegnata una variabile mutabile alla GPIO e subito dopo la modalità del pin utilizzato, in questo caso output. Le ultime tre linee eseguono l'accensione e lo spegnimento fisico del LED con un intervallo di 500 millisecondi. Se eseguiamo il programma con Cargo Run, la prima volta vedremo il caricamento delle librerie, ai successivi avvii non ci sarà più questa attesa ma sarà più rapido.

pi@raspberrypi:~/rpi-gpio-test $ cargo run
 Updating registry `https://github.com/rust-lang/crates.io-index`
 Downloading rppal v0.8.1 
 Downloading quick-error v1.2.2 
 Downloading libc v0.2.43 
 Compiling quick-error v1.2.2 
 Compiling libc v0.2.43
 Compiling rppal v0.8.1
 Compiling rpi-gpio-test v0.1.0 (file:///home/pi/rpi-gpio-test)
 Finished dev [unoptimized + debuginfo] target(s) in 4m 34s
 Running `target/debug/rpi-gpio-test`
Model: Raspberry Pi B Rev 2 (SoC: BCM2835)

Da qui in poi vi lascio nell'esplorazione di questo linguaggio che, insieme a GO di Google, sarà uno dei protagonisti del prossimo futuro.

Conclusioni

Rust è in crescita, frequentemente aggiornato e relativamente semplice, nonostante a un primo impatto possa sembrare ostico. Anche la documentazione è ormai sufficiente da permettere a chiunque di poterlo studiare senza curve d'apprendimento estreme. E' un linguaggio professionale e versatile, esistono librerie per un pò tutte le attività e gli interfacciamenti oppure è possibile scriversele da soli. Tenetelo d'occhio perché nel curriculum di un programmatore è sicuramente un plus.

 

Scarica subito una copia gratis

Scrivi un commento

Seguici anche sul tuo Social Network preferito!

Send this to a friend