Nove regole per l’esecuzione di Rust sul Web e su dispositivi embedded

'9 regole per eseguire Rust sul Web e su dispositivi embedded'

Lezioni pratiche dal porting di range-set-blaze a no_std e WASM

Granchio che corre su un microcontrollore - Fonte: https://openai.com/dall-e-2/

Raccomando Rust quando desideri la velocità di C++ e la sicurezza della memoria di Python. Inoltre, con Rust puoi costruire su oltre 100.000 librerie software . Inoltre, Rust offre la possibilità di eseguire il tuo codice non solo su un computer convenzionale, ma anche all’interno di una pagina web o addirittura su un robot.

Eseguire “quasi ovunque”, tuttavia, comporta complicazioni. Questo articolo è per i programmatori Rust che desiderano mitigare queste complicazioni. (Potrebbe interessare anche coloro che vogliono conoscere la storia di “run almost everywhere” di Rust.)

Prima complicazione: le pagine web e i processori incorporati dei robot non supportano l’I/O di file generale. Se il tuo progetto riguarda principalmente la lettura e la scrittura di file, non è un buon candidato per l’esecuzione su un robot, un altro processore incorporato o una pagina web.

Seconda complicazione: il porting del codice per eseguirlo quasi ovunque richiede molti passaggi e scelte. Navigare tra queste scelte può richiedere tempo. Non effettuare un passaggio può portare al fallimento. Questo articolo mira a ridurre questa seconda complicazione offrendo queste nove regole, che esploreremo in seguito nel dettaglio:

  1. Segna il tuo lib.rs o main.rs come no_std.
  2. Usa il “crate alloc” integrato se puoi.
  3. Passa a dipendenze “no std”.
  4. Crea funzionalità std e alloc e rendi facoltative le tue funzioni solo std.
  5. Compila il tuo progetto per WASM. Usa cargo tree per farlo funzionare.
  6. Crea test WASM e una demo WASM.
  7. [Facoltativo] Compila il tuo progetto per embedded.
  8. [Facoltativo] Crea un singolo test incorporato e una demo incorporata.
  9. Concludi con il testing CI, i metadati di Cargo.toml e un README.md aggiornato.

Seguire queste regole ti aiuterà a creare codice molto veloce e sicuro in termini di memoria che funziona ovunque, da un PC, a una pagina web di smartphone ( demo ), a un robot. Il codice può essere molto piccolo e può sfruttare l’ampia libreria di crate di Rust.

Per illustrare le regole, porteremo la crate range-set-blaze per eseguire all’interno delle pagine web – WASM – e sui microcontrollori – embedded. (Questa crate manipola insiemi di interi “clumpy”. Un utente della crate ha richiesto il porting.)

Un porting a WASM e embedded richiede che si eviti di utilizzare la libreria standard di Rust, “std”. La conversione a “no std” è stata sia più semplice che più difficile di quanto mi aspettassi. Più semplice perché puoi ancora utilizzare Vec e String. Più difficile, principalmente a causa dei test. Sulla base della mia esperienza con range-set-blaze , ecco le decisioni, descritte una alla volta, che raccomando. Per evitare incertezze, esprimerò queste raccomandazioni come regole.

Regola 1: Segna il tuo lib.rs o main.rs come no_std.

Nota: Innanzitutto, usa Git per creare un nuovo branch per il tuo progetto. In questo modo, se le cose non funzionano, puoi annullare facilmente tutte le modifiche.

Segna l’inizio di lib.rs con:

#![cfg_attr(not(test), no_std)]

Ciò indica al compilatore Rust di non includere la libreria standard, tranne durante i test.

Nota 1: Il mio progetto è un progetto di libreria con un lib.rs . Credo che i passaggi per un progetto binario con un main.rs siano più o meno gli stessi, ma non li ho testati.

Nota 2: Parleremo molto di più dei test di codice nelle regole successive.

Aggiungere la riga “no_std” a lib.rs di range-set-blaze , causa 40 problemi del compilatore, la maggior parte di questo tipo:

Risolvi alcuni di questi cambiando “std::” in “core::” nel tuo codice principale (non nel codice di test). Per range-set-blaze , questo riduce il numero di problemi da 40 a 12. Questa correzione aiuta perché molti elementi, come std::cmp::max , sono disponibili anche come core::cmp::max .

Purtroppo, elementi come Vec e Box non possono essere in core perché hanno bisogno di allocare memoria. Fortunatamente, se sei disposto a supportare l’allocazione di memoria, puoi comunque usarli.

Regola 2: Usa la libreria incorporata “crate alloc” se puoi.

Dovresti permettere al tuo crate di allocare memoria? Per WASM dovresti farlo. Per molte applicazioni embedded, dovresti farlo anche. Tuttavia, per alcune applicazioni embedded, non dovresti farlo. Se decidi di permettere l’allocazione di memoria, allora nella parte superiore di lib.rs aggiungi:

extern crate alloc;

Ora puoi aggiungere righe come queste per ottenere accesso a molti elementi allocati in memoria:

extern crate alloc;use alloc::boxed::Box;use alloc::collections::btree_map;use alloc::collections::BTreeMap;use alloc::vec::Vec;use alloc::{format, string::String};use alloc::vec;

Con range-set-blaze , questo riduce il numero di problemi da 12 a due. Risolveremo questi nella Regola 3.

Note: Cosa succede se stai scrivendo per un ambiente embedded che non può utilizzare l’allocazione di memoria e hai problemi con, ad esempio, Vec . Potresti essere in grado di riscrivere il codice. Ad esempio, potresti essere in grado di utilizzare un array al posto di un vettore. Se ciò non funziona, dai un’occhiata alle altre regole. Se nulla funziona, potresti non essere in grado di portare il tuo crate a no_std .

Regola 3: Passa alle dipendenze “no std”.

Il compilatore Rust si lamenta se il tuo progetto utilizza un crate che inserisce funzioni “std” nel tuo codice. A volte, puoi cercare su crates.io e trovare crate “no_std” alternativi. Ad esempio, il popolare crate thiserror inserisce “std” nel tuo codice. Tuttavia, la comunità ha creato alternative che non lo fanno .

Nel caso di range-set-blaze , i due problemi rimanenti riguardano il crate gen_ops – un crate fantastico per definire operatori come “+” e “&” in modo conveniente. La versione 0.3.0 di gen_ops non supportava completamente “no std”. La versione 0.4.0, invece, lo fa. Ho aggiornato le mie dipendenze in Cargo.toml e migliorato la compatibilità con “no std”.

Ora posso eseguire questi comandi:

cargo check # verifica che compili come no_stdcargo test # verifica che i test, utilizzando std, continuino a passare

Il comando cargo check conferma che il mio crate non sta utilizzando direttamente la libreria standard. Il comando cargo test conferma che i miei test (che utilizzano ancora la libreria standard) continuano a passare. Se il tuo crate ancora non si compila, dai un’occhiata alla regola successiva.

Regola 4: Crea funzionalità std e alloc e rendi le tue funzioni che richiedono std facoltative.

I processori embedded generalmente non supportano la lettura e la scrittura di file. Allo stesso modo, WASM non supporta ancora completamente i file. Sebbene sia possibile trovare alcuni crate “no std” correlati ai file, nessuno sembra essere completo. Quindi, se l’I/O di file è centrale per il tuo crate, il porting a WASM ed embedded potrebbe non essere pratico.

Tuttavia, se l’I/O di file – o qualsiasi altra funzione che richiede std – è solo incidentale al tuo crate, puoi rendere quella funzione facoltativa tramite una funzionalità “std”. Ecco come:

Aggiungi questa sezione al tuo Cargo.toml:

[package]#...resolver = "2" # il valore predefinito per Rust 2021+[features]default = ["std"]std = []alloc = []

Questo indica che la tua crate ora ha due feature, “std” e “alloc”. Di default, il compilatore dovrebbe utilizzare “std”.

All’inizio del tuo lib.rs, sostituisci:

#![cfg_attr(not(test), no_std)]

con:

#![cfg_attr(not(feature = "std"), no_std)]

Questo indica che se non applichi la feature “std”, il compilatore dovrebbe compilare senza la libreria standard.

Sulla riga prima di qualsiasi codice che è specifico per “std”, inserisci #[cfg(feature = "std")]. Ad esempio, qui definiamo una funzione che crea una struct RangeSetBlaze basata sui contenuti di un file:

#[cfg(feature = "std")]use std::fs::File;#[cfg(feature = "std")]use std::io::{self, BufRead};#[cfg(feature = "std")]use std::path::Path;#[cfg(feature = "std")]#[allow(missing_docs)]pub fn demo_read_ranges_from_file<P, T>(path: P) -> io::Result<RangeSetBlaze<T>>where    P: AsRef<Path>,    T: FromStr + Integer,{ //...codice non mostrato}

Per verificare le feature “std” e “alloc”, esegui questo:

cargo check # stdcargo check --features alloc --no-default-features

Puoi testare “std” con

cargo test

Nota: Sorprendentemente, cargo test --features alloc --no-default-features non testa “alloc”. Questo perché i test richiedono thread, allocazione e altre cose che potrebbero non essere disponibili in no_std, quindi cargo esegue sempre i test normali come “std”.

A questo punto stiamo verificando sia “std” che “alloc”, quindi possiamo assumere che la nostra libreria funzionerà con WASM e l’embedded. No! In generale, nulla funziona senza essere testato. In particolare, potremmo dipendere da crate che utilizzano codice “std” internamente. Per trovare questi problemi, dobbiamo testare negli ambienti WASM e embedded.

Regola 5: Costruisci il tuo progetto per WASM. Usa cargo tree per farlo funzionare.

Installa il compilatore WASM cross e verifica il tuo progetto con questi comandi:

rustup target add wasm32-unknown-unknown # devi farlo solo una voltacargo check --target wasm32-unknown-unknown --features alloc --no-default-features

Quando lo faccio su range-set-blaze, si lamenta che la crate getrandom non funziona con WASM. Da una parte, non mi sorprende che WASM non supporti completamente i numeri casuali. Dall’altra parte, mi sorprende perché il mio progetto non dipende direttamente da getrandom. Per trovare la dipendenza indiretta, uso cargo tree. Scopro che il mio progetto dipende dalla crate rand che dipende da getrandom. Ecco il comando cargo tree da utilizzare:

cargo tree --edges no-dev --format "{p} {f}" --features alloc --no-default-features

Il comando restituisce sia le crate che le feature che utilizzano:

range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm3) alloc├── gen_ops v0.4.0├── itertools v0.10.5 default,use_alloc,use_std│   └── either v1.8.1 use_std├── num-integer v0.1.45 default,std│   └── num-traits v0.2.15 default,std│       [build-dependencies]│       └── autocfg v1.1.0│   [build-dependencies]│   └── autocfg v1.1.0├── num-traits v0.2.15 default,std (*)├── rand v0.8.5 alloc,default,getrandom,libc,rand_chacha,std,std_rng│   ├── rand_chacha v0.3.1 std│   │   ├── ppv-lite86 v0.2.17 simd,std│   │   └── rand_core v0.6.4 alloc,getrandom,std│   │       └── getrandom v0.2.9 std│   │           └── cfg-if v1.0.0...

L’output mostra che range-set-blaze dipende da rand. Inoltre, mostra che rand dipende da getrandom con la sua funzionalità “std”.

Ho letto la documentazione di getrandom e ho appreso che la sua funzionalità “js” supporta WASM. Quindi, come diciamo a rand di utilizzare getrandom/js, ma solo quando compiliamo con la nostra funzionalità “alloc”? Aggiorniamo il nostro Cargo.toml come segue:

[features]default = ["std"]std = ["getrandom/std"]alloc = ["getrandom/js"][dependencies]# ...getrandom = "0.2.10"

Questo dice che la nostra funzionalità “std” dipende dalla funzionalità “std” di getrandom. La nostra funzionalità “alloc”, tuttavia, dovrebbe utilizzare la funzionalità “js” di getrandom.

Ora funziona:

cargo check --target wasm32-unknown-unknown --features alloc --no-default-features

Quindi, abbiamo la compilazione WASM, ma cosa succede con i test WASM?

Regola 6: Crea test WASM e una demo WASM.

Mettiamo subito a lavoro la versione WASM, prima con i test e poi con una pagina web di demo.

Crea test WASM in tests/wasm.rs

Puoi testare su WASM quasi come puoi testare nativamente. Facciamo ciò facendo in modo che i test originali vengano eseguiti solo in modo nativo, mentre un set quasi duplicato di test viene eseguito su WASM. Ecco i passaggi basati su The wasm-bindgen Guide:

  1. Esegui cargo install wasm-bindgen-cli
  2. Copia i tuoi test di integrazione correnti, ad esempio da tests/integration_tests.rs a tests/wasm.rs. (Ricorda che in Rust, i test di integrazione sono test che vivono al di fuori della directory src e che vedono solo i metodi pubblici di un progetto.)
  3. In cima a tests/wasm.rs, rimuovi #![cfg(test)] e aggiungi #![cfg(target_arch = "wasm32")] use wasm_bindgen_test::*; wasm_bindgen_test_configure!(run_in_browser);
  4. In wasm.rs, sostituisci tutti i #[test] con #[wasm_bindgen_test].
  5. Ovunque tu abbia #![cfg(test)] (tipicamente in tests/integration_tests.rs e src/tests.rs), aggiungi la linea aggiuntiva: #![cfg(not(target_arch = "wasm32"))]
  6. Nel tuo Cargo.toml, cambia le tue [dev-dependencies] (se presenti) in [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]
  7. Nel tuo Cargo.toml, aggiungi una sezione:
[target.'cfg(target_arch = "wasm32")'.dev-dependencies]wasm-bindgen-test = "0.3.37"

Con tutto questo impostato, i test nativi, cargo test, dovrebbero ancora funzionare. Se non hai installato il browser Chrome, installalo. Ora prova ad eseguire i test WASM con:

wasm-pack test --chrome --headless --features alloc --no-default-features

Probabilmente fallirà perché i tuoi test WASM utilizzano dipendenze che non sono state o non possono essere inserite in Cargo.toml. Affronta ogni problema e o:

  1. Aggiungi le dipendenze necessarie nella sezione [target.'cfg(target_arch = "wasm32")'.dev-dependencies] di Cargo.toml, o
  2. Rimuovi i test da tests/wasm.rs.

Per range-set-blaze, ho rimosso tutti i test WASM relativi al framework di benchmarking del pacchetto. Questi test verranno comunque eseguiti sul lato nativo. Alcuni utili test in tests\wasm.rs richiedevano la crate syntactic-for, quindi l’ho aggiunta a Cargo.toml, sotto [target.'cfg(target_arch = "wasm32")'.dev-dependencies]. Con questa correzione, tutti i 59 test WASM vengono eseguiti e superati.

A parte: Se il tuo progetto include una cartella di esempi, potresti dover creare un modulo native all’interno del tuo esempio e un modulo wasm. Vedi questo file range-set-blaze per un esempio su come farlo.

Crea una demo WASM in tests/wasm-demo

Parte del divertimento nel supportare WASM è che puoi mostrare una demo del tuo codice Rust in una pagina web. Ecco una demo web di range-set-blaze.

Segui questi passaggi per creare la tua demo web:

Nel file Cargo.toml principale del tuo progetto, definisci uno spazio di lavoro e aggiungi tests/wasm-demo ad esso:

[workspace]
members = [".", "tests/wasm-demo"]

Nella tua cartella dei test, crea una sottocartella test/wasm-demo.

Dovrebbe contenere un nuovo Cargo.toml come questo (cambia range-set-blaze con il nome del tuo progetto):

[package]
name = "wasm-demo"
version = "0.1.0"
edition = "2021"

[lib]
crate-type = ["cdylib"]

[dependencies]
wasm-bindgen = "0.2"
range-set-blaze = { path = "../..", features = ["alloc"], default-features = false}

Inoltre, crea un file tests/wasm-demo/src/lib.rs. Ecco il mio:

#![no_std]
extern crate alloc;
use alloc::{string::ToString, vec::Vec};
use range_set_blaze::RangeSetBlaze;
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn disjoint_intervals(input: Vec<i32>) -> JsValue {
    let set: RangeSetBlaze<_> = input.into_iter().collect();
    let s = set.to_string();
    JsValue::from_str(&s)
}

Questo file definisce una funzione chiamata disjoint_intervals che prende in input un vettore di interi, ad esempio, 100,103,101,102,-3,-4. Utilizzando il pacchetto range-set-blaze, la funzione restituisce una stringa degli interi come intervalli ordinati e disgiunti, ad esempio, -4..=-3, 100..=103.

Come ultimo passaggio, crea il file tests/wasm-demo/index.html. Il mio utilizza un po’ di JavaScript per accettare una lista di interi e quindi chiamare la funzione Rust WASM disjoint_intervals.

<!DOCTYPE html>
<html>
<body>
    <h2>Rust WASM RangeSetBlaze Demo</h2>
    <p>Inserisci una lista di interi separati da virgola:</p>
    <input id="inputData" type="text" value="100,103,101,102,-3,-4" oninput="callWasmFunction()">
    <br><br>
    <p id="output"></p>
    <script type="module">
        import init, { disjoint_intervals } from './pkg/wasm_demo.js';
        function callWasmFunction() {
            let inputData = document.getElementById("inputData").value;
            let data = inputData.split(',').map(x => x.trim() === "" ? NaN : Number(x)).filter(n => !isNaN(n));
            const typedArray = Int32Array.from(data);
            let result = disjoint_intervals(typedArray);
            document.getElementById("output").innerHTML = result;
        }
        window.callWasmFunction = callWasmFunction;
        init().then(callWasmFunction);
    </script>
</body>
</html>

Per eseguire la demo localmente, sposta prima il tuo terminale in tests/wasm-demo. Quindi esegui:

# da tests/wasm-demowasm-pack build --target web

Successivamente, avvia un server web locale e visualizza la pagina. Io uso l’estensione Live Preview di VS Code. Molte persone usano python -m http.server. La demo range-set-blaze si presenta così (disponibile anche in tempo reale su GitHub):

Trovo molto gratificante vedere il mio progetto Rust in esecuzione in una pagina web. Se stai cercando solo la compatibilità con WASM, puoi passare alla Regola 9.

Regola 7: Costruisci il tuo progetto per l’embedded.

Se desideri portare il tuo progetto un passo oltre WASM, segui questa regola e quella successiva.

Assicurati di tornare alla directory principale del tuo progetto nel terminale. Quindi, installa thumbv7m-none-eabi, un processore embedded popolare, e verifica il tuo progetto con questi comandi:

# dalla directory principale del progettorustup target add thumbv7m-none-eabi # è necessario farlo solo una volta# probabilmente troverà problemicargo check --target thumbv7m-none-eabi --features alloc --no-default-features

Quando eseguo questo su range-set-blaze, ottengo errori relativi a quattro set di dipendenze:

  • thiserror – Il mio progetto dipendeva da questa crate ma non la utilizzava effettivamente. Ho rimosso la dipendenza.
  • rand e getrandom – Il mio progetto ha bisogno solo di numeri casuali per il testing nativo, quindi ho spostato la dipendenza in [target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]. Ho anche aggiornato il mio codice principale e di testing.
  • itertools, num-traits e num-integer – Queste crate offrono funzionalità per “std” e “alloc”. Ho aggiornato Cargo.toml come segue:
...[features]default = ["std"]std = ["itertools/use_std", "num-traits/std", "num-integer/std"]alloc = ["itertools/use_alloc", "num-traits", "num-integer"][dependencies]itertools = { version = "0.10.1", optional = true, default-features = false }num-integer = { version = "0.1.44", optional = true, default-features = false }num-traits = { version = "0.2.15", optional = true, default-features = false }gen_ops = "0.4.0"[target.'cfg(not(target_arch = "wasm32"))'.dev-dependencies]#...rand = "0.8.4"#...

Come ho fatto a sapere quale caratteristica di quali dipendenze utilizzare? Comprendere le caratteristiche di una crate come itertools richiede la lettura della sua documentazione e (spesso) andare al suo repository su GitHub e leggere il suo Cargo.toml. Dovresti anche utilizzare cargo tree per verificare di ottenere la caratteristica desiderata da ogni dipendenza. Ad esempio, questo utilizzo di cargo tree mostra che per una compilazione predefinita, ottengo le caratteristiche “std” di range-set-blaze, num-integer e num-traits e le caratteristiche “use-std” di itertools e either:

cargo tree --edges no-dev --format "{p} {f}"

range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm4) default,itertools,num-integer,num-traits,std├── gen_ops v0.4.0├── itertools v0.10.5 use_alloc,use_std│   └── either v1.8.1 use_std├── num-integer v0.1.45 std│   └── num-traits v0.2.15 std│       [build-dependencies]│       └── autocfg v1.1.0│   [build-dependencies]│   └── autocfg v1.1.0└── num-traits v0.2.15 std (*)

E questo mostra che per una compilazione --features alloc --no-default-feature, ottengo la funzionalità desiderata “use_alloc” di itertools e la versione “no default” delle altre dipendenze:

cargo tree --edges no-dev --format "{p} {f}" --features alloc --no-default-features

range-set-blaze v0.1.6 (O:\Projects\Science\wasmetc\wasm4) alloc,itertools,num-integer,num-traits├── gen_ops v0.4.0├── itertools v0.10.5 use_alloc│   └── either v1.8.1├── num-integer v0.1.45│   └── num-traits v0.2.15│       [build-dependencies]│       └── autocfg v1.1.0│   [build-dependencies]│   └── autocfg v1.1.0└── num-traits v0.2.15  (*)

Quando pensi di aver fatto tutto funzionare, usa questi comandi per controllare/testare il codice nativo, WASM e embedded:

# test nativocargo testcargo test --features alloc --no-default-features# controlla e testa WASMcargo check --target wasm32-unknown-unknown --features alloc --no-default-featureswasm-pack test --chrome --headless --features alloc --no-default-features# controlla embeddedcargo check --target thumbv7m-none-eabi --features alloc --no-default-features

Questi controllano l’embedded, ma cosa succede con il testing dell’embedded?

Regola 8: Crea un unico test embedded e una demo embedded.

Mettiamo alla prova la nostra funzionalità embedded creando un test e una demo combinati. Li eseguiremo su un emulatore chiamato QEMU.

Testare Rust nativo è facile. Testare Rust WASM va bene. Testare Rust embedded è difficile. Faremo solo un singolo test semplice.

Parentesi 1: Per maggiori informazioni su come eseguire ed emulare Rust embedded, consulta: The Embedded Rust Book.

Parentesi 2: Per idee su un framework di test più completo per Rust embedded, consulta defmt-test. Purtroppo, non sono riuscito a capire come farlo funzionare in emulazione. Il progetto cortex-m/testsuite utilizza una versione modificata di defmt-test e può essere eseguito in emulazione, ma non offre una crate di testing autonomo e richiede tre (sotto)progetti aggiuntivi.

Parentesi 3: Un test embedded è infinitamente meglio di nessun test. Faremo il resto dei nostri test a livello nativo e WASM.

Crea il test embedded e la demo nella nostra cartella tests attuale. I file saranno:

tests/embedded├── .cargo│   └── config.toml├── Cargo.toml├── build.rs├── memory.x└── src    └── main.rs

Ecco i passaggi per creare i file e configurare le cose.

  1. Installa l’emulatore QEMU. Su Windows, ciò comporta l’esecuzione di un programma di installazione e quindi l’aggiunta manuale di "C:\Program Files\qemu\" al tuo percorso.

2. Crea un file tests/embedded/Cargo.toml che dipende dal tuo progetto locale con “no default features” e “alloc”. Ecco il mio:

[package]edition = "2021"name = "embedded"version = "0.1.0"[dependencies]alloc-cortex-m = "0.4.4"cortex-m = "0.6.0"cortex-m-rt = "0.6.10"cortex-m-semihosting = "0.3.3"panic-halt = "0.2.0"# reference your local project hererange-set-blaze = { path = "../..", features = ["alloc"], default-features = false }[[bin]]name = "embedded"test = falsebench = false

3. Crea un file tests/embedded/src/main.rs. Inserisci il tuo codice di test dopo il commento “il test va qui”. Ecco il mio file:

// basato su https://github.com/rust-embedded/cortex-m-quickstart/blob/master/examples/allocator.rs// e https://github.com/rust-lang/rust/issues/51540#![feature(alloc_error_handler)]#![no_main]#![no_std]extern crate alloc;use alloc::string::ToString;use alloc_cortex_m::CortexMHeap;use core::{alloc::Layout, iter::FromIterator};use cortex_m::asm;use cortex_m_rt::entry;use cortex_m_semihosting::{debug, hprintln};use panic_halt as _;use range_set_blaze::RangeSetBlaze;#[global_allocator]static ALLOCATOR: CortexMHeap = CortexMHeap::empty();const HEAP_SIZE: usize = 1024; // in bytes#[entry]fn main() -> ! {    unsafe { ALLOCATOR.init(cortex_m_rt::heap_start() as usize, HEAP_SIZE) }    // il test va qui    let range_set_blaze = RangeSetBlaze::from_iter([100, 103, 101, 102, -3, -4]);    assert!(range_set_blaze.to_string() == "-4..=-3, 100..=103");    hprintln!("{:?}", range_set_blaze.to_string()).unwrap();    // esci da QEMU/ NOTA: non eseguire questo sull'hardware; può corrompere lo stato di OpenOCD    debug::exit(debug::EXIT_SUCCESS);    loop {}}#[alloc_error_handler]fn alloc_error(_layout: Layout) -> ! {    asm::bkpt();    loop {}}

4. Copia build.rs e memory.x dal repository GitHub di cortex-m-quickstart in tests/embedded/.

5. Crea un file tests/embedded/.cargo/config.toml contenente:

[target.thumbv7m-none-eabi]runner = "qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel"[build]target = "thumbv7m-none-eabi"

6. Aggiorna il file Cargo.toml principale del tuo progetto aggiungendo tests/embedded al tuo workspace:

[workspace]members = [".", "tests/wasm-demo", "tests/embedded"]

Con questa configurazione, sei quasi pronto per eseguire l’emulazione incorporata. Successivamente, posiziona il tuo terminale e imposta il compilatore come nightly:

# Assicurati che qemu sia nel percorso, ad esempio, imposta PATH="C:\Program Files\qemu\";%PATH%cd tests/embeddedrustup override set nightly # per supportare #![feature(alloc_error_handler)]

Ora puoi utilizzare cargo check, cargo build e cargo run sull’applicazione di esempio. Per esempio:

cargo run

   Terminato dev [non ottimizzato + debuginfo] target(s) in 0,03s     In esecuzione `qemu-system-arm -cpu cortex-m3 -machine lm3s6965evb -nographic -semihosting-config enable=on,target=native -kernel O:\Projects\Science\wasmetc\wasm4\target\thumbv7m-none-eabi\debug\embedded`Timer con periodo zero, disabilitazione"-4..=-3, 100..=103"

Quando avrai fatto funzionare tutto ciò, avrai eseguito con successo il tuo progetto su un microcontrollore (emulato)! Se hai problemi con la configurazione, controlla nuovamente queste istruzioni. Se ciò non funziona, consulta The Embedded Rust Book.

Quando hai finito, assicurati di ripristinare il compilatore a stable:

rustup override set stable

Regola 9: Concludi con i test di CI, i metadati di Cargo.toml e un README.md aggiornato.

Test di CI

Siamo quasi alla fine, ma dobbiamo assicurarci che tutto ciò che funziona oggi funzionerà anche domani. Questo è compito dei test di CI (continuous integration).

Ho impostato i miei test di CI in modo che vengano eseguiti ad ogni commit e una volta al mese. Se sei su GitHub, crea un file .github/workflows/tests.yml:

name: teston:  push:  schedule: # esegui ogni mese    - cron: '0 0 1 * *'  pull_request:  workflow_dispatch:    jobs:  test_rust:    name: Test Rust    runs-on: ubuntu-latest    steps:      - name: Checkout        uses: actions/checkout@v3      - name: Setup Rust        uses: dtolnay/rust-toolchain@master        with:          toolchain: stable      - name: Setup WASM        uses: jetli/[email protected]      - name: Test Native & WASM        run: |          cargo clippy --verbose --all-targets --all-features -- -D warnings          cargo test --verbose          cargo test --features alloc --no-default-features --verbose          wasm-pack test --chrome --headless --features alloc --no-default-features --verbose      - name: Setup and check Embedded        run: |          rustup target add thumbv7m-none-eabi          cargo check --target thumbv7m-none-eabi --features alloc --no-default-features          rustup override set nightly          rustup target add thumbv7m-none-eabi          cargo check --target thumbv7m-none-eabi --features alloc --no-default-features          sudo apt-get update && sudo apt-get install qemu qemu-system-arm      - name: Test Embedded (in nightly)        timeout-minutes: 3        run: |          cd tests/embedded          cargo run

Se stai lavorando solo con WASM, puoi omettere gli ultimi due passaggi relativi all’incorporazione.

Curiosità: perché l’ultimo test dice timeout-minutes: 3? Perché un test incorporato fallito non restituisce un errore. Invece, entra in un ciclo infinito. Lo rileviamo con il timeout.

Metadati

Rust ti consente di contrassegnare il tuo codice come funzionante su architetture e ambienti specifici. La convenzione prevede l’uso di metadati con parole chiave e categorie. In particolare, aggiungi queste parole chiave e categorie al tuo Cargo.toml come appropriato:

[package]#...keywords = [#...    "wasm",    "no_std",]categories = [#...    "wasm",    "no-std",]

README.md

Dovresti anche aggiornare il tuo README.md per informare le persone che supporti WASM e l’embedded. Ecco cosa ho aggiunto:

La crate supporta progetti no_std, WASM e embedded:```toml[dependencies]range-set-blaze = { features = ["alloc"], default-features = false, version=VERSION }``` *Sostituisci VERSION con la versione attuale.

Ecco, hai nove regole per i porting WASM e no_std in Rust. Rust è un ottimo linguaggio per la programmazione nativa, WASM e embedded. Offre velocità, sicurezza e accesso a migliaia di utili crate. Segui queste nove regole per eseguire il tuo codice Rust praticamente ovunque.

Curiosità: se sei interessato a futuri articoli, seguimi su VoAGI. Scrivo di programmazione scientifica in Rust e Python, apprendimento automatico e statistica. Di solito scrivo circa un articolo al mese.