Cela fait maintenant deux semaines que je n’ai pas écrit d’article concernant bsc, en Rust…

Il est plus que temps de remédier à cette inactivité (non voulue) !

Aujourd’hui, on va donc voir ensemble la suite de la partie précédente, à savoir la toute première étape de la sous-commande add : récupérer un module (distant ou non).

 

Quelques rappels et explications rapides

 

Dans l’épisode précédent, nous nous étions arrêtés à la création de la sous-commande en elle-même, grâce au crate clap, avec toutes les options dont nous avions besoin pour l’instant :

  • −−local : pour récupérer un module stocké sur le pc
  • −−zip : pour récupérer un module stocké sur un serveur distant (en ligne sur github par exemple), et compressé au format zip
  • −−git : pour directement cloner un module disponible via un dépôt git distant

On avait donc vu que ces options étaient ensuite transformées en enum, et passées à la fonction d’entrée de l’ajout d’un module : add_dependency, dans le fichier add.rs :

 // Cette fonction est située dans le fichier main.rs principal, et est appelée dans
 // le cas où l'utilisateur a tapé la sous-commande bcs add ...
fn add_dependency(path: &str, module_url: &str, git_repository: bool, local_repository: bool, zip_repository: bool){
    // on passe le chemin courant, l'url du module à ajouter, et son type (git, local ou zip)
    // à la fonction add_dependency, présente dans le fichier add.rs
    // qui est le point d'entrée pour la fonctionnalité d'ajout d'un module
    if git_repository {
        add::add_dependency(&path, &module_url, add::ModuleType::Git);
    } else if local_repository {
        add::add_dependency(&path, &module_url, add::ModuleType::Local);
    } else if zip_repository {
        add::add_dependency(&path, &module_url, add::ModuleType::Zip);
    } else {
        println!("Error: the module is neither a local, a git, or a zip url.");
    }
}

 

Aujourd’hui, on va donc s’intéresser au premières instructions de cette fameuse fonction « point d’entrée » de la fonctionnalité d’ajout d’un module.

La première étape consiste à créer (s’il n’existe pas déjà) le dossier bsc_modules, qui contiendra tous les modules dont le projet va dépendre.

Pour ce faire, je fais appel à la fonction create_folder, précédemment définie dans le fichier common.rs.

pub fn add_dependency(path: &str, module_url: &str, module_type: ModuleType){
    // Création du dossier bsc_modules, à la racine du projet
    // si celui-ci n'existe pas déjà
    common::create_folder(&format!("{}/bsc_modules", &path));
}

 

Il faut aussi penser que les modules importés peuvent être contenus dans des dossiers avec des noms particuliers. Ou encore, que ces derniers sont déjà inclus dans le projet en cours (il faut donc vérifier les modules déjà existant afin d’éviter des doublons…).

Pour vérifier tout cela, je commence donc par les importer dans un dossier tmp (dossier temporaire), situé dans bsc_modules. Cela permet également de pouvoir télécharger les éventuels modules au format zip dedans, avant de les déziper dans bsc_modules (le dossier parent de tmp).

 

Vous l’aurez compris, aucun module ne pourra donc être nommé « tmp » au risque d’être considéré comme le dossier temporaire, et donc d’être détruit une fois la commande terminée.

 

Mais avant de créer ce dossier, et pour que toute cette opération reste « propre », on va détruire tout dossier éventuel tmp dans bsc_modules, juste au cas où.

Puis, on va appeler la fonction copy_module_to_tmp qui se chargera de récupérer le module grâce à l’url renseigné dans la sous-commande.

pub fn add_dependency(path: &str, module_url: &str, module_type: ModuleType){
    common::create_folder(&format!("{}/bsc_modules", &path));
    // Suppression d'un éventuel dossier tmp existant dans bsc_modules/
    common::destroy_folder(&format!("{}/bsc_modules/tmp", &path));
    // Fonction qui gère la récupèration du module, en fonction de son type (zip, local, git)
    copy_module_to_tmp(&path, &module_url, module_type);
}

 

Tout semble prêt pour passer à l’étape suivante !

 

L’ajout d’un module local

 

Le principe d’ajout d’un module local est vraiment simple.

On va simplement copier/coller le dossier situé à l’url renseigné dans la sous-commande.

Dans la fonction copy_module_to_tmp, on appelle donc la fonction copy_folder (précédemment créée dans le fichier common.rs).

pub fn copy_module_to_tmp(path: &str, module_url: &str, module_type: ModuleType){
    // switch en fonction du type du module qu'on souhaite ajouter
    match module_type {
        // s'il s'agit d'un module de type git
        ModuleType::Git => {
        },
        // s'il s'agit d'un module de type local
        ModuleType::Local => {
            // on copie/colle simplement le module dans notre dossier tmp
            common::copy_folder(&module_url, &format!("{}/bsc_modules/tmp", &path));
        },
        // s'il s'agit d'un module de type zip
        ModuleType::Zip => {
        },
        _ => unreachable!(),
    }
}

 

Bref, c’est simple et efficace.

Voyons maintenant le cas d’un module qui se trouverait sur un dépôt git distant.

 

L’ajout d’un module git

 

Vous vous souvenez du crate git2 utilisé précédemment, lors de la création d’un nouveau projet (avec la sous-commande bsc create), pour initialiser un dépôt git ?

Et bien nous allons le réutiliser ici, mais cette fois, pour cloner un dépôt git, depuis une adresse url donnée.

On va donc utiliser la fonction clone présente dans le namespace Repository de ce crate, qui prend en arguments deux chaînes de caractères (l’url du dépôt git distant, et le dossier de destination du clonage), comme indiqué dans la documentation :

pub fn copy_module_to_tmp(path: &str, module_url: &str, module_type: ModuleType){
    match module_type {
        ModuleType::Git => {
            // on clone le dépôt git disponible à l'adresse url "module_url", passée en paramètre
            // dans le dossier tmp, tout en gérant le cas d'une éventuelle erreur
            // lors de cette opération
            match git2::Repository::clone(&module_url.to_string(), format!("{}/bsc_modules/tmp/", &path)){
                Err(why) => panic!("Error: failed to clone {}.", why.description()),
                Ok(_) => (),
            };
        },
        ModuleType::Local => {
            common::copy_folder(&module_url, &format!("{}/bsc_modules/tmp", &path));
        },
        ModuleType::Zip => {
        },
        _ => unreachable!(),
    }
}

 

Encore une fois, rien de très compliqué. Surtout via l’utilisation du crate git2 qui nous facilite énormément le travail !

Passons maintenant au dernier cas de figure.

 

L’ajout d’un module zip

 

Pour commencer, on va s’intéresser au téléchargement du fichier zip, disponible, encore une fois, à l’adresse url renseignée dans la sous-commande bsc add.

Pour télécharger un fichier, on va utiliser le crate curl, qui n’est rien d’autre qu’un wrapper de la libcurl, écrite en C (vous connaissez d’ailleurs probablement l’utilitaire curl, utilisable en ligne de commande, qui s’appuie sur la libcurl).

Cette librairie permet de télécharger des fichiers disponibles depuis un serveur distant.

Comme d’habitude lorsqu’on ajoute un nouveau crate au projet, on va donc commencer par ajouter une ligne dans le fichier Cargo.toml pour spécifier à Cargo qu’on souhaite utiliser le crate curl :

[package]
name = "bsc"
version = "0.1.0"
authors = ["Victor Gallet<victor.gallet@developing-stuff.com>"]

[dependencies]
indicatif = "0.9.0"
clap = "2.32.0"
git2 = "0.7.5"
// Ajout du crate curl dans sa version 0.4.15 
// (dernière version en date, lors de l'écriture de cet article)
curl = "0.4.15"

[profile.release]
lto = true

 

Au prochain appel de la commande cargo build, ou cargo run…, Cargo récupérera le crate, et le compilera pour qu’il soit utilisable dans notre projet.

Nous n’avons plus qu’à rajouter l’instruction pré-processeur suivante, tout en haut de notre fichier add.rs, pour pouvoir l’utiliser :

extern crate curl;

 

Regardons maintenant plus en détails l’utilisation de ce crate :

pub fn copy_module_to_tmp(path: &str, module_url: &str, module_type: ModuleType){
    match module_type {
        ModuleType::Git => {
            match git2::Repository::clone(&module_url.to_string(), format!("{}bsc_modules/tmp/", &path)){
                Err(why) => panic!("Error: failed to clone {}.", why.description()),
                Ok(_) => (),
            };
        },
        ModuleType::Local => {
            common::copy_folder(&module_url, &format!("{}bsc_modules/tmp", &path));
        },
        ModuleType::Zip => {
            // Création du dossier tmp pour stocker le module téléchargé, et l'unzip
            common::create_folder(&format!("{}bsc_modules/tmp", &path));
            // Utilisation du crate curl pour télécharger le fichier zip
            let mut easy = curl::easy::Easy::new();
            let mut dst = Vec::new();
            easy.url(&module_url).unwrap();
            {
                let mut transfer = easy.transfer();
                transfer.write_function(|data| {
                    dst.extend_from_slice(data);
                    Ok(data.len())
                }).unwrap();
                transfer.perform().unwrap();
            }
            common::set_content_file(&format!("{}bsc_modules/tmp/test.zip", &path), &dst);
        },
        _ => unreachable!(),
    }
}

 

D’après la documentation officielle du crate, on commence donc par créer une structure de type Easy, permettant d’effectuer le binding entre le crate curl et la lib C libcurl.

Ensuite, on créé un vecteur (nommé dst ici), pour stocker le contenu du fichier qu’on souhaite télécharger.

let mut easy = curl::easy::Easy::new();
let mut dst = Vec::new();

 

Puis on donne l’url où se trouve le fichier au champ url de la structure easy (unwrap permet d’automatiquement gérer une éventuelle erreur en effectuant un appel à panic!, comme vu dans les articles précédents) :

easy.url(&module_url).unwrap();

 

Maintenant que tout est prêt, il ne reste plus qu’à traiter la structure easy.

Pour cela, on va s’en tenir à la documentation officielle du crate : on ouvre donc un nouveau scope. Ce qui veut dire que toute variable déclarée sur le tas dans ce scope n’est plus accessible passé le « } » (en l’occurrence ici, les variables transfer et data).

{
    let mut transfer = easy.transfer();
    transfer.write_function(|data| {
        dst.extend_from_slice(data);
        Ok(data.len())
    }).unwrap();
    transfer.perform().unwrap();
}

 

Pourquoi déclarer un nouveau scope ?

 

En fait, c’est lié à une des particularités de Rust, dont je ne vous ai pas encore parlé : le borrow checker.

Je pense que cette notion propre à ce langage nécessite un article complet (qui arrivera sûrement d’ici quelques jours / semaines) pour bien comprendre et expliquer ce concept.

Pour faire simple Rust est un langage safe. Qu’est-ce que cela veut dire ?

Tout simplement qu’il n’est pas possible de passer et modifier des variables par référence ou par adresse (la plupart du temps instanciées sur la pile), au risque de se retrouver avec une variable détruite (free en mémoire), et qui n’est donc plus disponible.

Dans l’éventualité où ce cas arriverait, les traitements suivants sont complètement biaisés puisqu’ils vont essayer de chercher une variable dans une zone mémoire qui n’en contient plus. Cela produit des erreurs assez compliquées à résoudre puisqu’elles ne sont pas levées lors de la compilation, mais lorsque le programme est lancé (donc il faut effectuer une grande quantité de tests pour rencontrer un bug de ce genre).

Cela implique donc une grande vigilance de la part des programmeurs, en étant certain de traiter absolument tous les cas de figures.

Sauf qu’on le sait tous, l’erreur est humaine, et un oubli est vite arrivé, surtout sur des projets conséquents incluant plusieurs dizaines de développeurs.

Imaginez simplement le cas d’un jeu où une IA (personnage A) suit une autre IA (personnage B).

On peut donc spéculer que le personnage A possède une référence sur le personnage B, et qu’à chaque itération de la boucle principale du jeu, il va aller regarder la position du personnage B, pour le suivre.

Maintenant, supposons que le personnage B meurt, ou est détruit, et ne soit plus dans le jeu. La mémoire allouée pour ce personnage est donc libérée.

Si un tel cas de figure n’est pas traité correctement, alors le personnage A contient maintenant une référence sur une zone mémoire vide (dans le meilleur des cas), ou pire, qui contient carrément autre chose dorénavant (dans le cas où des allocations ont été faites entre la destruction du personnage B = libération de la zone mémoire, et l’appel à la fonction permettant au personnage A de suivre B).

Dans le cas d’un petit jeu, avec peu de NPC (IA), cela sera un problème vite repéré.

Mais dans le cas d’une grosse production, constituée de plusieurs centaines de NPC interagissant constamment entre eux et avec le joueur, je vous laisse entrevoir les problèmes que cela peut poser, si le bug n’est détecté très vite…

Bref, Rust a donc notamment été pensé pour sécuriser cette notion de « passage par référence ou par adresse ». Et pour y parvenir, plusieurs règles ont été créées, dont le borrow checker.

Pour faire simple, lorsqu’on déclare une variable mutable, on ne peut la modifier par référence qu’une seule fois par scope; scope qui ne peut pas être plus grand que celui qui contient la déclaration de la variable.

En attendant d’écrire un article entièrement dédié à ce sujet (en français), je vous invite vivement à lire la documentation officielle (en anglais), très claire, avec plusieurs exemples.

 

Bref, cet aparté étant terminé, revenons à nos moutons !

Une fois les actions effectuées dans ce nouveau scope terminées, nous nous retrouvons donc avec le contenu du fichier zip téléchargé dans le vecteur dst (qui n’est rien d’autre qu’un tableau d’int sur 8 bits non signés).

On peut donc maintenant créer et remplir un fichier d’extension .zip avec ces données, en passant ce vecteur dst à la fonction set_content_file, présente dans le fichier common.rs :

 // Création d'un fichier dans bsc_modules/tmp/ nommé test.zip
 // qui contient le contenu du fichier zip téléchargé 
common::set_content_file(&format!("{}bsc_modules/tmp/test.zip", &path), &dst);

 

Il ne nous reste plus qu’à unzip ce fichier pour récupérer le code du module !

Comme cet article commence à devenir plutôt conséquent, nous verrons cela dans la partie 7.

 

Conclusion

 

L’ajout d’un nouveau module commence à prendre forme !

Jusqu’ici, on a donc récupéré le code du module qu’on souhaite ajouter dans le dossier bsc_module/tmp, sauf concernant le cas d’un module récupéré par archive zip, où nous n’avons pour l’instant que le fichier .zip téléchargé.

Dans l’article suivant, nous verrons donc comment unzip ce fichier, et les étapes restantes nécessaires à l’ajout d’un module bsc :

  • vérification des fichiers (pour savoir s’il s’agit bien d’un module bsc)
  • vérification des dépendances, pour savoir si le module ajouté est déjà une dépendance du projet
  • ajout des fichiers headers et sources aux fichiers CMakeLists.txt, de configuration du méta build cmake
  • modification des instructions pré-processeurs #include écrites dans les fichiers du module ajouté, afin d’obtenir des chemins finaux plus simples et logiques.

Encore une fois, je vous remercie vraiment d’avoir pris le temps de lire cet article jusqu’au bout !

J’espère que cela vous intéresse toujours autant, et je suis bien évidemment encore à l’écoute de vos commentaires sur le sujet (que ce soit sur le fonctionnement intrinsèque de bsc, ou sur le code Rust).

Victor Gallet

Victor Gallet

Étudiant programmeur jeu vidéo. J'aime par dessus tout apprendre, et je suis un éternel curieux de tout. Mon principal but dans la vie est d’être une meilleure personne, et de partager mes (faibles) connaissances avec les autres.

Leave a Reply

Ce site utilise Akismet pour réduire les indésirables. En savoir plus sur comment les données de vos commentaires sont utilisées.