Data Compiler in RUST: l’evoluzione dei tool T3.
NIER ha utilizzato la tecnologia RUST per progettare tool di classe T3 in ambito ferroviario.
A cura di Giovanni Mazza e Filippo Elia
[Software Engineer Consultants – Area Ingegneria del Software]
INTRODUZIONE.
NIER è stata scelta da una realtà leader del settore ferroviario per progettare diversi tool. Questi tool sono definiti “di classe T3” dalla norma EN 50128:2011 (dominio Railways) e, tra questi, alcuni sono specifici per la generazione dei dati di configurazione di sistemi di segnalamento ferroviario. Vengono chiamati comunemente Data Compiler (DC) [ne parliamo in questo articolo e in questo caso di successo]. Ma cos’è un tool di classe T3? È un tool che genera output che possono direttamente o indirettamente contribuire all’esecuzione di codice di sistemi safety critical. I Data Compiler ne sono un classico esempio perché devono garantire che i dati generati siano corretti, consistenti e integri.
Lo sviluppo di un DC può seguire differenti strategie implementative:
- Architettura 2oo2
- Architettura con ricostruttore
- Architettura con checker
ARCHITETTURA 2oo2.
In questo tipo di architettura[1] il DC viene realizzato attraverso lo sviluppo di due canali di elaborazione (Compiler A e Compiler B), realizzati con due linguaggi di programmazione diversi tra loro per ridurre la probabilità di errore “di modo comune” causato da interpreti o compilatori dei linguaggi scelti.
Entrambi i canali di elaborazione prendono in input gli stessi dati e generano i file di output attesi. La generazione è andata a buon fine se e solo se la comparazione delle uscite certifica che i file prodotti dal Compiler A sono identici a quelli prodotti dal Compiler B.
Nella Figura 1 è riportato uno schema che mostra questo tipo di architettura:
[1] Per dettagli su architetture 2oo2 si può consultare questo articolo
ARCHITETTURA CON RICOSTRUTTORE.
In questo tipo di architettura, il DC viene realizzato attraverso lo sviluppo di un singolo canale di elaborazione, in genere chiamato Compiler, e di un Ricostruttore. Scopo del Compiler è quello di generare i file di output richiesti a partire da un determinato set di dati di input. Scopo del Ricostruttore è invece quello di ricostruire i dati di input utilizzati dal Compiler a partire dai file di output che quest’ultimo ha prodotto. Solo se i file di input riscostruiti risulteranno uguali a quelli utilizzati dal Compiler, i file di output prodotti verranno considerati validi.
Nella Figura 2 è riportata una rappresentazione schematica di questa architettura:
ARCHITETTURA CON CHECKER.
In questo tipo di architettura, il DC viene realizzato attraverso lo sviluppo di un singolo canale di elaborazione – in genere chiamato Compiler – il cui scopo è generare i file di output richiesti. Quest’ultimi vengono verificati con un secondo tool: Checker.
Il Checker è sviluppato con un diverso linguaggio di programmazione (Sw Diversity) e ha l’obiettivo di controllare, con algoritmi indipendenti rispetto al Compiler, che ogni singolo dato di uscita sia corretto rispetto ai relativi dati di input. Solo se tutti i controlli previsti dal Checker danno esito positivo, i file di output vengono considerati validi. Il Checker produce come output un report di validazione in cui viene fornita l’evidenza di tutti i controlli effettuati ed il relativo risultato, come schematizzato in Figura 3, qui di seguito:
Uso del linguaggio Rust.
Nel contesto dello sviluppo dei DC, NIER ha deciso di utilizzare un linguaggio di programmazione moderno e affidabile: Rust.
Rust è dotato di caratteristiche che lo rendono più sicuro e robusto del C (spesso utilizzato in ambiti safety critical) perché sgrava il programmatore da molti aspetti di defensive programming (ad esempio legati all’uso corretto della memoria e alla protezione delle sezioni critiche in contesto di programmazione concorrente, solo per citarne alcuni), lasciandolo libero di concentrarsi maggiormente sugli aspetti architetturali e progettuali.
Rust: un linguaggio per un software affidabile ed efficiente.
Nato da un progetto personale di Graydon Hoare, il linguaggio di programmazione Rust è stato annunciato nel 2010 da Mozilla che ha in seguito pubblicato una versione stabile nel 2015. La versione 6.1 del kernel Linux ne introduce il supporto per lo sviluppo di moduli kernel.
I punti di forza di questo linguaggio sono riassunti in:
- Prestazioni: Rust è estremamente veloce ed efficiente nel consumo di memoria. Non ha infatti garbage collector, può essere impiegato in servizi dove le prestazioni sono fondamentali, funziona su dispositivi embedded ed è facile da integrare con altri linguaggi di programmazione;
- Affidabilità: il modello di ownership ed il ricco type system di Rust garantiscono allo stesso tempo sicurezza nella gestione della memoria e dei thread consentendo di eliminare tanti tipi di bug già in fase di compilazione;
- Produttività: Rust ha una documentazione eccellente, un compilatore con messaggi di errore utili e strumenti di alto livello come, citando solo alcuni esempi, un package manager integrato, un supporto evoluto per molteplici editor con completamento automatico e type inspection ed uno strumento per la formattazione automatica del codice.
Rust risulta quindi un ottimo linguaggio per creare sistemi complessi e sicuri, anche nel caso in cui risulti necessario adottare una programmazione concorrente (multithread).
Uno degli aspetti più importanti del Rust riguarda la gestione della memoria, che consente di prevenire i classici bug di “segmentation fault” che si verificano a tempo di esecuzione, in cui il programma (scritto tipicamente in C) tenta di accedere ad aree di memoria che non dovrebbe/potrebbe utilizzare.
La gestione della memoria di Rust non rinuncia tuttavia alle prestazioni, compatibili con quelle del C, perché non si basa sul concetto di Garbage Collector (GC), tipico di linguaggi più ad alto livello, ma sul nuovo concetto di “ownership” e di “borrowing”, ovvero una serie di regole che consentono di identificare a tempo di compilazione (e non a tempo di esecuzione!) gli errori nella gestione della memoria. Questo si paga in una curva di apprendimento del linguaggio più piatta (è necessario più tempo per arrivare ad una buona padronanza delle basi del linguaggio), ma i benefici a posteriori sono evidenti.
Rust ha molte altre caratteristiche, tra cui il supporto nella gestione della concorrenza: Rust è in grado di impedire, sempre a tempo di compilazione, operazioni dall’esito indefinito, come l’accesso in scrittura simultaneo alla stessa variabile dovuta da una corsa critica tra thread.
Il package manager integrato, chiamato “Cargo”, semplifica la vita del programmatore e permette tra le altre cose di definire in uno specifico file di configurazione la versione delle librerie utilizzate dal programma (chiamate “dipendenze”), in modo tale da garantire che non vengano scaricate e utilizzate versioni diverse, a meno che non sia lo stesso programmatore a volerlo.
CONCLUSIONI.
Durante lo sviluppo e l’implementazione di Data Compiler, NIER ha realizzato in Rust:
- un canale di elaborazione (per un DC con architettura 2oo2);
- un Checker (per un DC con architettura con Checker).
L’esperienza di NIER è che Rust sia un ottimo linguaggio di programmazione garantendo l’eliminazione di molti possibili bachi già in fase di compilazione, ed alleggerendo il carico cognitivo del programmatore rispetto ai controlli di defensive programming, ineliminabili in C e derivati.
L’impiego di Rust da parte di molte grandi realtà affermate come Google, Amazon Web Services, Huawei, Meta, Microsoft solo per citarne alcune e la sua diffusione anche in sistemi embedded come il Kernel di Linux, non fa che confermare l’affidabilità e la sicurezza di questo linguaggio, nonché la sua manutenzione a lungo temine.
Ma non finisce qui. Anche la macchina normativa sta evolvendo verso nuove tecnologie, uscendo dalla morsa storica che la vedeva confinata, per applicazioni Safety related, a solo poche e solide tecnologie.
È infatti in corso di approvazione (“formal vote”) la nuova norma EN 50716:2022 che andrà a sostituire EN 50128:2011 e EN 50657:2017, amendments e corrigenda compresi, che introduce interessanti novità proprio riguardo ai linguaggi e tecniche di programmazione. L’obiettivo è di allinearsi con il contenuto di EN 50126-1/2:2017 e di raccogliere sollecitazioni ed evoluzioni del ciclo di vita del software.
In questa evoluzione normativa, infatti, non sono più citati esplicitamente i noti linguaggi C, Ada, ma sono tracciate le caratteristiche che il linguaggio scelto deve avere:
- Permettere di realizzare un codice che sia scritto, verificato, e mantenuto nel modo più veloce, efficace ed efficiente;
- Permettere di rilevare il prima possibile gli errori sistematici;
- Avere una chiara e ben documentata semantica (mandatoria per SIL3 e SIL4);
- Favorire un approccio modulare e ricco di commenti;
- Essere fortemente tipizzato ed essere accompagnato da un compiler efficace e/o da tool di analisi.
È chiaro ed evidente che RUST risulta la risposta ottimale a questa evoluzione tecnologica e normativa.
Non resta che attendere che le realtà addentro ai domini safety related ed industriali si avvalgano di questa piccola rivoluzione software: NIER sarà presente per fornire il supporto necessario.