Somewhat safer unpickler

This class inherits all the implementation from the builtin pickle.Unpickler but modifies the self.dispatch dictionary used to unpack the serialized structures.

This is not checked to be safe, but in case you are using pickled files in production and you are searching for some safer way to load them to convert the data to a different format, this class can be handy.

The same idea works for Python 3, subclassing pickle._Unpickler.

Test riproducibili con il tempo (parte 2)

Nella parte 1 di questo blog post eravamo arrivati ad avere una situazione del tipo:

use std::marker::PhantomData;
struct Event<T> {
    message: String,
    stamp: DateTime<Utc>,
    mark: PhantomData<T>,
}
trait Now {
    fn now() -> DateTime<Utc> {
        Utc::now()
    }
}
impl<T: Now> Event<T> {
    fn new(message: String) -> Event<T> {
        Event {
            message,
            stamp: T::now(),
            mark: PhantomData,
        }
    }
}

struct SystemNow;
impl Now for SystemNow {};

let _event: Event<SystemNow> = Event::new("Messaggio".to_string());

Quindi adesso è facile creare un trait che restituisce un momento fisso nel tempo:

struct FixedNow;

impl Now for FixedNow {
    fn now() -> DateTime<Utc> {
        Utc.ymd(2012, 11, 23).and_hms(18, 53, 7)
    }
}

let event: Event<FixedNow> = Event::new("Messaggio 2012".to_string());
assert_eq!(event.stamp.year(), 2012);

Però, molto spesso avremo bisogno di avere una sequenza di particolari timestamp che vadano a testare alcuni aspetti critici della nostra implementazione.

Nel caso del nostro trait abbiamo un problema, abbiamo definito un metodo statico, non abbiamo nessun self in cui memorizzare l'evoluzione della nostra sequenza di timestamp. Dobbiamo trovare qualche altra soluzione, e ci sono almeno un paio di alternative.

Nel caso in cui siamo sicuri che nel test il metodo now() verrà chiamato sempre e solo da un singolo thread, ce la possiamo cavare con una variabile static 1, anzi ad essere precisi static mut, condita con un pizzico di unsafe.

Le variabili static hanno una posizione fissa nella memoria durante l'esecuzione del programma, se si cerca di definire una variabile static mut non c'è modo un Rust safe di modificarla, perché non c'è modo di garantire l'atomicità delle modifiche ad una variabile che può essere potenzialmente modificata da più thread in contemporanea. Però nel nostro caso, in un ambiente di test controllato, possiamo usare una variabile static per avere uno stato in un metodo ... statico.

static mut COUNTER: i32 = 0;
COUNTER = COUNTER + 1;

Provando a modificare direttamente direttamente la variabile COUNTER il compilatore ci segnala l'errore:

error[E0133]: use of mutable static requires unsafe function or block
 --> src/part2.rs:92:1
  |
4 | COUNTER = COUNTER + 1;
  | ^^^^^^^^^^^^^^^^^^^^^ use of mutable static

Per fortuna, ormai ci siamo abituati bene, il compilatore ci suggerisce anche una soluzione, usare un blocco unsafe.

Un blocco unsafe in Rust è un blocco dove il compilatore permette di compiere operazioni più pericolose, e si fida di quello che lo sviluppatore ha scritto. Appena usciti dal blocco unsafe, tutti i normali controlli torno attivi, quindi il blocco unsafe è la parte più pericolosa del codice, quella a cui in una review serve mettere doppia attenzione, ma non è diverso da scrivere in C o C++.

A volte può essere utile (per questioni di performance) oppure necessario (come nel caso della comunicazione con altre librerie tramite l'interfaccia C), in questo caso è solo un esempio, perché come vedremo, la libreria standard ci offre qualche altra opzione.

struct LoopNow;

impl Now for LoopNow {
    fn now() -> DateTime<Utc> {
        static mut COUNTER: i32 = 0;
        let inc = unsafe {
            COUNTER = (COUNTER + 1) % 10;
            COUNTER
        };
        Utc.ymd(2000 + inc, 9, 9).and_hms(1, 46, 40)
    }
}
let event: Event<LoopNow> = Event::new("Messaggio 2001".to_string());
assert_eq!(event.stamp.year(), 2001);
let event: Event<LoopNow> = Event::new("Messaggio 2002".to_string());
assert_eq!(event.stamp.year(), 2002);

Facciamo una breve digressione sulla variabile statica, cosa vuol dire, come funziona questa cosa magica che una funzione senza nessuna variabile globale può avere un suo stato?

Beh, in realtà la COUNTER è in un certo senso globale, ed il suo valore iniziale viene definito prima della prima esecuzione della funzione, è proprio compilato nel codice del programma. Quello che succede nella funzione è andare a trovare l'indirizzo di quei 4 byte di memoria per leggerci e scriverci dentro, mentre sullo stack non c'è niente relativo a COUNTER. Per un approfondimento, nelle note c'è il link ad un articolo interessante 4.

Questo è l'ASM generato con una versione ridotta dell'esempio 5 che abbiamo usato qua (con un po' di cerimonia per non far ottimizzare tutto al compilatore).

playground::count:
    addl    playground::count::COUNT(%rip), %edi
    movl    %edi, playground::count::COUNT(%rip)
    movl    %edi, %eax
    retq

playground::main:
    pushq   %rax
    movl    $1, %edi
    callq   playground::count
    movl    %eax, %edi
    callq   playground::count
    movl    %eax, %edi
    callq   std::process::[email protected]
    ud2

La strana forma playground::count::COUNT(%rip) rappresenta simbolicamente l'offset rispetto all'indirizzo dell'istruzione corrente (+1 pare, ma è un dettaglio che non ci tange) a cui trovare la zona di memoria in cui scrivere. E l'indirizzo della zona di memoria è fisso e non cambia tra successive chiamate a count, mentre il valore contenuto cambia ogni volta.

Adesso che abbiamo capito il trucco delle variabili static, abbiamo ancora un problema, non siamo ancora in grado di eseguire un test con più thread. Ma le moderne CPU ci vengono in aiuto. Possiamo definire una variabile atomica, una variabile speciale per cui non serviranno strutture di sincronizzazione particolari, perché è il processore stesso a garantire l'atomicità di un particolare set di operazioni.

In un esempio minimale come il precedente 6, con una variabile atomica l'asm generato per la funzione count è:

playground::count:
    lock        xaddq   %rdi, playground::count::COUNTER(%rip)
    movq    %rdi, %rax
    retq
use std::sync::atomic::{AtomicUsize, Ordering};

struct IncrementNow;

impl Now for IncrementNow {
    fn now() -> DateTime<Utc> {
        static COUNTER: AtomicUsize = AtomicUsize::new(1);
        let inc = COUNTER.fetch_add(1, Ordering::SeqCst) as i32;
        Utc.ymd(2000 + inc, 9, 9).and_hms(1, 46, 40)
    }
}

Le variabili atomiche 2 sono il tassello base per la costruzione di primitive di sincronizzazione tra thread, in questo caso abbiamo semplicemente usato il valore più conservativo per l'ordering, perché approfondire questo argomento vorrebbe dire aprire un vaso di Pandora, che sinceramente non ho ancora aperto (almeno, ho solo sbirciato un po'). In breve, anche solo dal fatto che senza usafe il compilatore non ci segnala problemi, possiamo assumere che qualcun altro ha garantito che l'uso di questa primitiva è sicuro.

Però così senza farsi notare abbiamo usato un concetto che ancora non abbiamo presentato, che invece sarà sicuramente saltato all'occhio al lettore attento: come possiamo mutare COUNTER se non lo abbiamo definito mutabile?

Questo è un pattern in Rust detto interior mutability, che sarebbe la possibilità di creare un contenitore ufficialmente non mutabile, che al suo interno si occupa di garantire alcune condizioni al contorno della modifica del valore interno. In questo caso tutte le variabili atomiche in std::sync::atomic sono Sync, cioè sono marcate in modo da garantire al compilatore che è possibile accedere a queste variabili da più thread contemporaneamente.

Il più semplice tipo che offre una "scatola" immutabile per un contenuto mutabile è Cell 3, ma in questo caso non c'è nessun marcatore Sync, quindi il compilatore non ci permetterà di usare Cell in contesti multithread, ma ci permette di avere più di un riferimento mutabile ad una singola variabile. E quindi cosa ci abbiamo guadagnato rispetto ad una normale refrerence stile C++? Semplicemente che nell'implementazione di Cell c'è un controllo ed una mutazione concorrente non prevista farà crashare il programma. Meglio un errore subito che corrompere silenziosamente i dati (per poi magari crashare comunque più tardi).

type IncrementEvent = Event<IncrementNow>;

let event1 = IncrementEvent::new("IncrementEvent 1".to_owned());
assert_eq!(event1.stamp.year(), 2001);
assert_eq!(event1.stamp.minute(), 46);

let event2 = IncrementEvent::new("IncrementEvent 2".to_owned());
assert_eq!(event2.stamp.year(), 2002);
assert_eq!(event2.stamp.minute(), 46);

Verrebbe voglia di scrivere un test per verificare qualche corruzione della memoria nel caso dell'uso dello static mut, ma lo lascio come esercizio per il lettore.

Un ringraziamento al gruppetto di Rustiti anonimi di @develer per la review.

Grazie per la lettura, se questa mini serie vi piaciuta fatemelo sapere, potrebbe essere lo spunto per scrivere qualcos'altro, e non esitate a contattarmi (@naufraghi su twitter) per segnalare errori od omissioni!


  1. Vedi const and static nella prima edizione del Rust Book oppure Accessing or Modifying a Mutable Static Variable nella seconda edizione del libro o anche Static items nella reference del linguaggio. 

  2. Module std::sync::atomic nella documentazione della libreria standard. 

  3. Module std::cell nella documentazione della libreria standard. 

  4. Understanding C by learning assembly, in particolare il capitolo Understanding static local variables

  5. Esempio variabile statica mutabile sul Playground. 

  6. Esempio variabile statica atomica sul Playground. 

Test riproducibili con il tempo (parte 1)

Ormai è difficile non aver sentito parlare di Rust, il nuovo linguaggio di Mozilla, che promette la velocità del C++ senza troppi grattacapi. Il bello di Rust è che pur essendo un linguaggio "di sistema", cioè un linguaggio con un controllo dell'esecuzione simile a quello offerto dal C, è anche un linguaggio che cura molto l'esperienza dello sviluppatore, con tool ergonomici e messaggi di errori di prima qualità.

Quindi che siate un rodato sviluppatore C/C++ o uno sviluppatore Python / Javascript / Ruby, se avete voglia di aggiungere un nuovo linguaggio al vostro repertorio, fate una prova con Rust!

Ormai sto usando Rust come hobby da un annetto, non ho scritto niente di serio ma mi piace cercare di trovare una soluzione ai tipici problemi che sono abituato ad affrontare in Python, magari riuscendo ad avere quel controllo in più a compile time che in Python è ancora un po' faticoso da ottenere.

Quindi non prendete come oro colato quello che scrivo, anzi, se ci fossero correzioni o suggerimenti contattatemi pure.

Immaginiamoci quindi programmatori Rust non navigati, stiamo scrivendo la nostra libreria in Rust, che sia una "indovina il numero" o una lista della spesa, avremo qualche evento a cui vorremo rispondere, e per rendere l'informazione più completa, vogliamo anche avere un timestamp.

Una prima implementazione potrebbe essere qualcosa del tipo:

extern crate chrono;
use chrono::prelude::*;

struct Event {
    message: String,
    stamp: DateTime<Utc>,
}

impl Event {
    fn new(message: String) -> Event {
        Event {
            message,
            stamp: Utc::now(),
        }
    }
}

Ma ora si pone un problema, come si possono scrivere test di una cosa del genere? In Python si possono usare mock nei modi più arditi, ma in Rust è tutta un'altra storia, dobbiamo preparare un po' la strada per iniettare questa dipendenza.

Un approccio potrebbe essere quella di definire un Trait 2 che rappresenti la funzionalità di poter ottenere l'ora corrente.

trait Now {
    fn now() -> DateTime<Utc> {
        Utc::now()
    }
}

Magari non vogliamo che sia proprio Event ad implementare questo Trait, ma una altra struttura di utilità.

struct SystemNow;
impl Now for SystemNow {};

Non serve scrivere molto, stiamo definendo una struttura vuota che usa l'implementazione di default del trait.

Adesso possiamo far usare questo nuovo tipo al nostro Event, ma se facciamo la cosa semplice:

impl Event {
    fn new(message: String) -> Event {
        Event {
            message,
            stamp: SystemNow::now(),
        }
    }
}

Non ci abbiamo guadagnato molto, non sappiamo ancora come modificare quel now() nei test. Potremmo passare una istanza come argomento, e chiamare il metodo now(), ma nell'uso reale non ci serve che sia una istanze, e non vogliamo creare una astrazione "costosa" solo per rendere il codice più testabile, più che altro quando ci sono alternative a costo zero!

In Rust quasi tutto può essere reso generico rispetto al tipo, Option<i32> ed Option<String> sono due oggetti molto simili, la stessa scatola con un contenuto diverso. Ed indipendentemente dal tipo avremo tutti i metodi di Option 3 disponibili in entrambi i casi.

Potremmo definire un Event<T> e poi usare come T il nostro SystemNow, proviamo:

struct Event<T> {
    message: String,
    stamp: DateTime<Utc>,
}

Ma otteniamo un errore di compilazione:

error[E0392]: parameter `T` is never used
 --> src/lib.rs:97:14
  |
6 | struct Event<T> {
  |              ^ unused type parameter
  |
  = help: consider removing `T` or using a marker such as `std::marker::PhantomData`

Hmmm, qua c'è un suggerimento interessante, se vogliamo associare un tipo alla nostra struttura, ma non vogliamo averne una istanza, possiamo usare un marcatore 1.

use std::marker::PhantomData;

struct Event<T> {
    message: String,
    stamp: DateTime<Utc>,
    mark: PhantomData<T>,
}

impl<T> Event<T> {
    fn new(message: String) -> Event<T> {
        Event {
            message,
            stamp: T::now(),
            mark: PhantomData,
        }
    }
}

Ma anche in questo caso abbiamo un errore:

error[E0599]: no function or associated item named `now` found for type `T` in the current scope
  --> src/lib.rs:131:20
   |
22 |             stamp: T::now(),
   |                    ^^^^^^ function or associated item not found in `T`
   |
   = help: items from traits can only be used if the trait is implemented and in scope
   = note: the following trait defines an item `now`, perhaps you need to implement it:
           candidate #1: `main::Now`

Anche in questo caso l'errore ci da un buon suggerimento, ed in effetti il compilatore ha ragione, non abbiamo ancora scritto da nessuna parte che il nostro generico T deve implementare il trait Now. Possiamo farlo semplicemente aggiungendo un vincolo alla definizione della nostra struttura Event:

use std::marker::PhantomData;
struct Event<T> {
    message: String,
    stamp: DateTime<Utc>,
    mark: PhantomData<T>,
}
impl<T: Now> Event<T> {
    fn new(message: String) -> Event<T> {
        Event {
            message,
            stamp: T::now(),
            mark: PhantomData,
        }
    }
}

let event: Event<SystemNow> = Event::new("Messaggio".to_string());

Adesso il codice compila di nuovo, perché il compilatore ha modo di verificare che il generico T non è così generico come prima, ma deve implementare i metodi definiti nel trait Now, e tra questi (uno solo, in questo caso) c'è proprio un metodo now() con la firma giusta.

Adesso abbiamo tutti gli ingredienti per usare un trait diverso nei test rispetto a quello usato nella libreria, ma come lo vedremo nella seconda parte di questo post.

I sorgenti di questo progetto sono disponibili qua: gitlab.com/naufraghi/phantomtypes

Grazie a @tglman e @lucabruno per la revisione della bozza.


  1. Potete verificare che il marcatore è a costo zero in questo snippet

  2. I Trait sono una cosa simile alle interfacce del Go o alle interfacce dei linguaggi che usano l'ereditarietà, per maggiori dettagli Traits: Defining Shared Behavior

  3. Option ha un numero enorme di metodi di utilità, che conviene tenere sott'occhio, perché ce ne è uno adatto per quasi ogni caso d'uso che potrebbe capitarvi. 

Simple Rust Pipeline on Bitbucket

Bitbucket offers a builtin execution pipeline (Docker based), here is a minimal configuration to have your Rust test running:

# This is a sample build configuration for all languages.
# Check our guides at https://confluence.atlassian.com/x/VYk8Lw for more examples.
# Only use spaces to indent your .yml configuration.
# -----
# You can specify a custom docker image from Docker Hub as your build environment.
image: python:2.7

pipelines:
  default:
    - step:
        script:
          - curl https://sh.rustup.rs -sSf | sh -s -- --default-toolchain nightly -y
          - source $HOME/.cargo/env
          - cargo test