Sérialisation

Dans le chapitre précédent, nous avons vu comment la bibliothèque clap est capable de générer automatiquement le code d’une interface ligne de commande très complète à partir de la définition des paramètres qu’on veut contrôler via cette ligne de commande.

Plus généralement, comme la génération de code est quelque chose de très accessible en Rust, on peut s’en servir pour automatiser toutes sortes de tâches de manipulation de données rébarbatives. Ici, nous allons l’appliquer à une autre opération qui donne beaucoup de fil à retordre en C++, la sérialisation des données. Ou comment transformer ses structs et enum en JSON, YAML, CSV, etc.

Contexte

Dans la plupart des langages, quand on sérialise des données, on se “marie” à un format de données en utilisant une bibliothèque très spécifique à ce format. C’est un problème pour deux raisons.

D’abord, au moment où on fait un premier choix de format de données, l’application n’est pas forcément assez mature pour qu’on fasse un choix éclairé, et donc c’est important de pouvoir essayer autre chose sans réécrire beaucoup de code. Particulièrement au niveau des formats de sortie ou d’entrée, qui opèrent sous des contraintes de performances et se comportent plus ou moins bien selon la manière dont le code les utilise.

Ensuite, les formats de données sont sujets à des effets de mode, particulièrement dans le domaine de la configuration. A l’heure où ces lignes sont écrites, il y a une mode du YAML et du TOML, avec encore pas mal de JSON ici et là. Il y a une dizaine d’années la mode était aux langages à balise comme XML, alors qu’aujourd’hui ce format est considéré comme has been et les utilisateurs détestent configurer quelque chose avec. Et je suis prêt à parier que si vous relisez ce cours dans dix ans, quelque chose d’autre aura remplacé le YAML et son utilisation sera universellement honnie.

Tout ça pour dire que si votre application est destinée à vivre longtemps, vous devez être prêt à changer de format de fichier de configuration au fil de son cycle de maintenance, et dépendre d’un format précis est une mauvaise idée.

En Rust, la bibliothèque serde essaie donc de répondre à deux problèmes :

  • Rendre la sérialisation plus simple en générant un maximum de code automatiquement, de façon bien intégrée au langage.
  • Rendre la sérialisation plus flexible en permettant d’émettre et recevoir des données dans de très nombreux formats de données, du Avro au YAML.

Utilisation

Comme précédemment, commencez par ajouter serde comme dépendance, en activant le support de la dérivation de traits :

cargo add serde --features derive

Ensuite, définissez un type de données avec ce que vous voulez dedans :

#![allow(unused)]
fn main() {
struct MonType {
    abc: u32,
    def: String,
    ghi: Vec<f32>,
}
}

Puis ramenez les traits de serde dans le scope pour plus de confort, et activez le support de la sérialisation pour ce type en quelques dérivations :

use serde::{Serialize, Deserialize};

#[derive(Debug, Deserialize, Serialize)]
struct MonType {
    abc: u32,
    def: String,
    ghi: Vec<f32>,
}

Et voilà, les données de ce type peuvent maintenant être sérialisées et désérialisées depuis et vers tous les formats supportés par serde.

Pour être plus concret, on va donner un exemple en JSON. Ajoutons la bibliothèque serde_json pour ce format en dépendance…

cargo add serde_json

…et en une paire d’appels de fonction, on est maintenant capables d’émettre du JSON et de reconstruire des données du type choisi depuis le JSON :

fn main() {
    // Valeur test
    let entree = MonType {
        abc: 42,
        def: "hello".to_string(),
        ghi: vec![1.2, 3.4, 5.6, 7.8],
    };
    println!("Valeur d'entrée : {entree:#?}");

    // Transformation en JSON
    let json = serde_json::to_string(&entree)
        .expect("Failed to encode JSON");
    println!("JSON : {}", json);

    // Décodage du JSON
    let sortie: MonType = serde_json::from_str(&json)
        .expect("Failed to decode JSON");
    println!("Valeur reconstruite : {sortie:#?}");
}

On le voit, serde rend le cas simple extrêmement simple d’utilisation, ce qui est très important. Pour les cas plus complexe, on peut utiliser des directives spécifiques à serde, comme dans l’exemple clap du chapitre précédent.

Cela permet notamment de contrôler finement la façon dont les types énumérés sont sérialisés : comme ces types ne sont pas supportés nativement par la plupart des formats de données, il y a plusieurs manières de les retranscrire, et c’est important de choisir la bonne quand on doit décoder des données générées par un autre programme.