Pour faire simple, les workspaces de Cargo sont l'équivalent des modules de Maven.
Pour définir une configuration commune à tous les workspaces il faut ajouter ceci dans le Cargo.toml
à la racine du projet :
[package]
name = "sotoestevez_medium"
version = "0.1.0"
[workspace]
members = ["add_trait", "beginning_tips", "generify_with_compiler_errors", "modules", "scoped_threads" ]
[workspace.package]
edition = "2021"
authors = ["Soto Estévez <ricardo@sotoestevez.dev>"]
description = "Demos of the articles at https://medium.com/@sotoestevez"
documentation = "https://medium.com/@sotoestevez"
readme = "./README.md"
homepage = "https://www.sotoestevez.dev"
repository = "https://github.com/kriogenia/medium"
license = "MIT OR Apache-2.0"
Puis activer l'héritage dans chaque Cargo.toml
des workspaces :
[package]
name = "add_trait"
version = "0.1.0"
edition.workspace = true
authors.workspace = true
description = "Dissecting Rust Traits to Learn Their Secrets"
documentation = "https://betterprogramming.pub/dissecting-rust-traits-to-learn-their-secrets-839845d3d71e"
homepage.workspace = true
repository.workspace = true
license.workspace = true
Cela marche aussi avec les versions des dépendances. Dans le parent on déclare ceci :
[workspace.dependencies]
num = { version = "0.4", default-features = false }
vector2d = "2.2"
rand = "0.8.5"
Et dans les enfants ceci :
[dependencies]
num = { workspace = true, default-features = true }
vector2d.workspace = true
[dev-dependencies]
rand = { workspace = true, features = [ "log" ] }
Je dois écrire un µ-service en Rust et j'ai cherché pas mal de serveurs web permettant de le faire. Évidemment, la première chose que les moteurs de recherche nous remontent c'est Hyper. Pour faire simple, Hyper est une serveur HTTP 1/2 qui s'appuie sur le pool de threads asychrone Tokio.
Problème, Hyper reste assez bas niveau. Je recherchais donc quelque chose aux performances équivalentes mais bien plus simple d'utilisation et je suis tombée sur Actix qui à l'air de faire le café. Je regrette uniquement la reprise du annotation-driven-bullshit via les macros déclaratives mais en dehors de cela, tout va bien.
Exemple de hello world en Actix / Rust :
use actix_web::{get, web, App, HttpServer, Responder};
#[get("/")]
async fn index() -> impl Responder {
"Hello, World!"
}
#[get("/{name}")]
async fn hello(name: web::Path<String>) -> impl Responder {
format!("Hello {}!", &name)
}
#[actix_web::main]
async fn main() -> std::io::Result<()> {
HttpServer::new(|| App::new().service(index).service(hello))
.bind(("127.0.0.1", 8080))?
.run()
.await
}
Je fais beaucoup de Rust ces derniers temps et je cherchais un framework qui puisse m'aider à produire mes requêtes SQL en sachant que je voulais tout sauf une horreur orientée structures comme peut l'être le couple JPA / Hibernate.
Au détour d'un coup de fil, @LapinFeroce me parle de Diesel qui est l'équivalent de Ktorm mais pour Rust. Autant vous dire qu'à la simple lecture de l'exemple de la home page j'étais déjà conquise 😻
En un code d'exemple :
// use macro_rules! <name of macro>{<Body>}
macro_rules! add {
// macth like arm for macro
($a:expr,$b:expr) => {
// macro expand to this code
{
// $a and $b will be templated using the value/variable provided to macro
$a+$b
}
}
}
// Usage in code
fn main(){
// call to macro, $a=1 and $b=2
add!(1,2);
}
Attention à ne pas abuser de la méta-programmation car cela peut augmenter significativement les temps de compilation.
Il implémente l'interface log qui est l'équivalent de SLF4J de Java/Kotlin mais pour Rust.
Il a la particularité de pouvoir se configurer directement dans le code en plus de fichier Yaml :
use log::LevelFilter;
use log4rs::append::console::ConsoleAppender;
use log4rs::append::file::FileAppender;
use log4rs::encode::pattern::PatternEncoder;
use log4rs::config::{Appender, Config, Logger, Root};
fn main() {
let stdout = ConsoleAppender::builder().build();
let requests = FileAppender::builder()
.encoder(Box::new(PatternEncoder::new("{d} - {m}{n}")))
.build("log/requests.log")
.unwrap();
let config = Config::builder()
.appender(Appender::builder().build("stdout", Box::new(stdout)))
.appender(Appender::builder().build("requests", Box::new(requests)))
.logger(Logger::builder().build("app::backend::db", LevelFilter::Info))
.logger(Logger::builder()
.appender("requests")
.additive(false)
.build("app::requests", LevelFilter::Info))
.build(Root::builder().appender("stdout").build(LevelFilter::Warn))
.unwrap();
let handle = log4rs::init_config(config).unwrap();
// use handle to change logger configuration at runtime
}
Et sa façade log reprend l'API de SLF4J mais sous forme de macros :
let world = "World";
trace!("Hello {}!", world);
debug!("Hello {}!", world);
info!("Hello {}!", world);
warn!("Hello {}!", world);
error!("Hello {}!", world);
À voir pour les performances, mais l'idée de reprendre l'API d'une façade qui a fait ses preuves pour y coller l'implémentation que l'on veut derrière est la bienvenue !
En trois étapes :
- Exécuter la commande
cargo install --color=always --force grcov
pour récupérer l'utilitaire (qui sera automatiquement ajouté à votre PATH user). - Démarrer Intellij IDEA.
- Installer / Activer le plugin Rust depuis la market place.
Enjoy
En résumé, la méthode du trait doit prendre en paramètre un
&Fn(X) -> Y
et non un
Fn(X) -> Y
Sinon, Rust ne peut pas garantir l'exécution de la fonction puisque le trait prendrait possession de la fonction. Or cette closure peut être un pointeur sur fonction, ce qui retirerait la propriété de cette dernière de sa structure.
Code d'exemple
fn fun_test(value: i32, f: &dyn Fn(i32) -> i32) -> i32 {
println!("{}", f(value));
value
}
fn times2(value: i32) -> i32 {
2 * value
}
fn main() {
fun_test(5, ×2);
}
Explication
Il y a trois types possibles de fonctions :
- Fn qui ne peut pas modifier l'objet qu'elle capture.
- FnMut qui peut modifier l'objet qu'elle capture.
- FnOnce la plus restrictive. Ne peut être appelée qu'une seule fois car une fois invoquée elle se consomme elle-même ainsi que l'objet qu'elle capture.
Ce qui est super c'est que la documentation possède un tableau comparant la complexité des différentes structures en fonction des cas d'utilisation.
Tout est dans le titre.
Différences entre les enums Option et Result en Rust.
Il n'y a pas à dire, à chaque fois que je code et où le langage m'oblige à utiliser des Optional, je me dis que c'est un langage pourri. Et ceci inclus Rust malgré tout le bien que je pense de lui 😤 !
La meilleure façon de gérer les cas de nullité se trouve dans Kotlin 🥰, TypeScript et Groovy. Le type Optional est une technique archaïque et verbeuse quand on vient du null-check de Kotlin.
Mais bon j'ai déjà expliqué par le passé en quoi le paradigme fonctionnel pure était un cancer métastasé 🤮. Et entre C++ et Rust pour de la programmation système il n'y a plus vraiment de discussion à cette heure, donc faisons-nous violence avec les Optional.
Mon rêve serait un langage natif reprenant la syntaxe de Kotlin (sauf tout ce qui touche aux getter/setter) avec le borrow checker de Rust et qui produise des binaires natifs. Je pense que je peux rêver encore longtemps 🥲
L'évolution de la demande de développeurs Kotlin est en plein boom en France 😁 :
Rust ne suit pas encore mais c'est un résultat attendu puisque l'emploi est pour l'instant situé en Chine, en Corée et aux USA et l'étude se focalise sur la France. Au fil des mois, il devrait phagocyter tout doucement ses parts à C et surtout C++ 🤩 :
La bonne surprise c'est de voir que Kotlin se positionne très bien sur la grille des salaires bruts 🤑 :
Pour l'instant j'ai eu le nez creux avec Kotlin, il faut dire que le compilateur fait des choses époustouflantes, notament la preuve d'absence de de référencement de pointeurs (NullPointer) lors de la compilation, et que son API est merveilleuse comparée à Java (même si elle s'appuie dessus).
Il en va de même pour Rust, qui apporte tellement de choses qui manquent à C et C++, notamment la preuve d'absence de fuites memoire et de race-conditions dès la compilation ou encore un gestionnaire de paquets digne de ce nom.
Bref, attendons encore un peu avant de crier victoire mais pour l'instant, tout se profile comme il le faut pour moi et cela valait bien de s'investir autant 🥳.
L'idée est de tirer 100% des optimisations possibles sur une architecture donnée pour ne cibler qu'elle mais ne plus être portable en contrepartie.
Il suffit de déclarer la variable d'environnement RUSTFLAGS
comme ceci :
# Cibler la plateforme sur laquelle est construit le binaire
RUSTFLAGS="-C target-cpu=native"
# CPU Intel
RUSTFLAGS="-C target-cpu=skylake"
# CPU AMD
RUSTFLAGS="-C target-cpu=znver2"
Résultats surprenant mais expliqués.
En substance, sur les applications codées pour le benchmark, si Java consomme beaucoup plus de mémoire que Rust (entre x3 et x10 en fonction que nous soyons sur Hotpot ou GraalVM), Java est globalement plus rapide que Rust.
Ce résultat qui paraît contre intuitif s'explique par les optimisations que la JRE effectue au runtime, ce qu'un compilateur ne peut pas faire.
Par contre, les temps de démarrage de Rust sont bien rapides que ceux de Java Hotspot mais comparable à GraalVM.
Enfin, si GraalVM consomme 2 à 3 fois moins de mémoire que Java Hotspot (mais toujours plus que Rust), ses performances sont moindre car la compilation native empêche d'effectuer les optimisations au runtime.
En dehors des temps de chargements qui sont forcément plus longs en Java, il faudrait voir ce que la compilation native au runtime pourrait apporter avec GraalVM + Truffle + Substrate VM.
Donc si vous avez :
- Beaucoup de RAM
- Pas de problème avec le temps de démarrage
- Un besoin de grosses perfs sur les requêtes de type I/O
Alors Java est le choix à privilégier sur Rust et ce résultat est totalement contre-intuitif ! /O\ #Bluffée
Pour faire simple, l'équivalent du .m2/repository
ou du répertoire cache des node_modules de npm n'existe pas en Rust / Cargo 😭
En résumé
- Soit il faut publier ses libs privées sur crates.io et donc les rendre publiques 🤬.
- Soit il faut installer l'équivalent d'un Artifactory perso quelque part 🤮.
- Soit il faut bidouiller / ruser 😩.
Parmi les bidouilles il y en a deux dont une acceptable je pense. Il faut déclarer le chemin vers le repo Git de la dépendance et non le nom et la version de la dépendance dans le fichier Cargo.toml
du projet.
Ce qui nous donne
[dependencies]
regex = { git = "https://github.com/rust-lang/regex.git", branch = "1.0.0" }
À la place de
[dependencies]
regex = "1.0.0"
Le paramètre branch
peut être une branche ou un tag. S'il n'est pas présent alors c'est le dernier commit sur la branche master qui sera utilisé.
Quid des fanatiques qui utilisent l'appellation main
à la place de master
? #DoNotCare
L'autre option consiste à déclarer le chemin relatif ou se trouve la dépendance (ici regexp) sur notre disque dur dans le fichier Cargo.toml
à la place du répo Git.
Ce qui rend le build non portable d'un développeur à l'autre puisque dépendant de la hiérarchie des dossiers, donc je vais éviter d'en parler.
Dans votre fichier ~/.cargo/config.toml
ajouter la configuration suivante
[registry]
default = "gitea"
[registries.gitea]
index = "https://mycompany.com/gitea/my-rust-repository.git"
[net]
git-fetch-with-cli = true
N.B : Je publie cette configuration ici mais je ne l'ai pas encore testée.
Il s'agit d'une traduction en français de la documentation / du livre disponible sur https://doc.rust-lang.org/stable/book/ et qui porte sur le langage Rust.
Cela devrait aider mes petits jeunes à mieux comprendre le langage. D'ailleurs je pense que je vais reprendre ma série de posts afin de synthétiser quelques concepts pour les néophytes.
Je cite :
« À mesure qu'Android passe de C/C++ à Java/Kotlin/Rust, nous nous attendons à ce que le nombre de vulnérabilités liées à la sécurité de la mémoire continue de diminuer. Vivement un avenir où les bogues de corruption de la mémoire sur Android seront rares », a conclu Google.
Rust et Kotlin sont deux superbes langages (avec une petite préférence pour Kotlin). J'ai pourtant quelques reproches à faire à l'un et à l'autre mais quand je vois que le marché avance vers eux à grands pas, autant vous dire que j'en suis toute chose <3
Que du beau sous le soleil.
Kotiln, Rust et Python progressent et de plus en plus de développeurs les adoptent (et c'est très bien).
J'ai été une grande utilisatrice de Python il y a un peu plus d'une quinzaine d'années lorsque je travaillais en labo sur du Data-Mining (l'ancêtre du Machine Learning). J'avais laissé de côté Python pour trois raisons à l'époque :
- Les problèmes de performance.
- Les problèmes d'outillage autour du build.
- Le fait que les programmes écrits en Python, pour être rapides, doivent utiliser des libs écrites en C, et donc avec un code orienté procédure.
Aujourd'hui, si je devais produire un système temps-réel et très peu énergivore, je partirais sur Rust.
Dans tous les autres cas de figure, je prendrais Kotlin sur OpenJDK ou Kotlin native (via le compilateur Kotlin-native ou GraalVM).
Par contre Python n'est plus du tout dans ma liste car pour moi à présent, si l'exécution d'un langage n'est pas prouvée à la compilation, c'est un stop immédiat. La majorité des développeurs n'écrivant pas de tests et maîtrisant mal le code (en tout cas en industrie) c'est indispensable.