@VinceOPS

Rust et WebAssembly : Wasm everywhere

wasm-pack

J’en parlais dans le premier article sur Rust : l’attrait de WebAssembly est l’une des raisons qui m’ont poussé vers ce langage. Dans cet article, j’aborde la compilation de code Rust en un fichier binaire Wasm et la génération du package NPM permettant son exploitation. Avec Node.js et dans le navigateur… Et après je parle beaucoup de WebAssembly 😁.

here we go

🦕 Deno ne sera pas abordé dans l’article, mais la version 0.24 supporte désormais l’import des modules WebAssembly.


D’abord, un peu de Rust

Pour changer des exemples sayHello(name) et add(a, b), on va écrire en Rust une petite lib simpliste qui compte le nombre de jours calendaires séparant deux dates. Par exemple :

  • 365 jours entre Noël 2018 et Noël 2019,
  • 0 jour entre 2019-11-14 et 2019-11-14T16:58:56.822Z
  • 29 jours entre 2012-02-01 et 2012-03-01 (année bisextile)

C’est proche de ce que fait moment.js avec sa fonction diff (momentA.diff(momentB, 'days')). Pour simplifier le cas, la fonction développée prendra en paramètres des timestamps en millisecondes.

👮 Chère “Rust Police”, ma pratique du langage est toujours très sporadique. Si tu constates une aberration qui t’offusque, n’hésite pas à me DM sur Twitter 🚨.

Allez viens, on se crée une lib 🚀 ! Je fais l’hypothèse que tu as déjà utilisé Rust et Cargo.

cargo new --lib days-count

Dans src/lib.rs, on se donne la définition suivante :

pub fn count_days_between(timestamp_ms_a: u64, timestamp_ms_b: u64) -> u64 {

On rapporte chaque timestamp au nombre de jours écoulés depuis le 01/01/1970. Par exemple, pour le 10/01/1970 (timestamp : 777600000), on obtient 9 jours écoulés. Il suffit ensuite de retourner la différence des deux.

/// Count the number of calendar days between two dates given as
/// timestamps in milliseconds. Make the assumption that One day is
/// 86_400_000 milliseconds (leap seconds are ignored).
pub fn count_days_between(timestamp_ms_a: u64, timestamp_ms_b: u64) -> u64 {
    let days_count_a = timestamp_ms_a / 1000 / 3600 / 24;
    let days_count_b = timestamp_ms_b / 1000 / 3600 / 24;
    let days_count_between = match days_count_a.checked_sub(days_count_b) {
        Some(difference) => difference,
        None => days_count_b - days_count_a,
    };

    return days_count_between;
}

💡 Pour le code complet (avec ses tests), c’est par ici.

dangerust

Et maintenant, du WebAssembly

Chouette 🦉, on a un code Rust qui fonctionne ! Maintenant, il faut le compiler en WebAssembly. C’est wasm-pack qui s’en charge, en plus de générer aussi le package NPM contenant le binaire .wasm, notre package.json et les fichiers .js permettant le binding Javascript ↔ Rust 🎆. Merci qui ? Merci wasm-bindgen.

# on ajoute la cible permettant de compiler du Rust en WebAssembly
rustup target add wasm32-unknown-unknown
# et on installe wasm-pack
curl https://rustwasm.github.io/wasm-pack/installer/init.sh -sSf | sh

Il faut aussi ajouter une dépendance et une lib à notre fichier Cargo.toml :

# [...]

[dependencies]
wasm-bindgen = "0.2"

[lib]
crate-type = ["cdylib"]

Et enfin, ajouter la macro wasm_bindgen à notre fonction :

use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn count_days_between(timestamp_ms_a: u64, timestamp_ms_b: u64) -> u64 {

Dans Node.js

On compile ensuite notre lib en exécutant :

wasm-pack build --release --target nodejs

--release pour la compilation optimisée, --target nodejs pour des bindings adaptés.

La commande génère le module NPM dans un dossier ./pkg que l’on va directement utiliser avec Node.js 10+ 🔥 :

mkdir nodejs && cd nodejs
yarn init -y # ou : npm init -y
yarn add --dev ../pkg # ou : npm i -D ../pkg
touch index.js

Dans le index.js fraîchement créé, on importe le module days-count généré par wasm-pack et on exécute notre fonction count_days_between :

const daysCount = require('days-count');

console.log(
  daysCount.count_days_between(
    // 01/02/2012
    BigInt(1330560000000),
    // 01/03/2012
    BigInt(1328054400000)
  )
); // 29 👍

💡 C’est BingInt que l’on utilise comme équivalent au type u64 de Rust, puisque number ne peut pas contenir une valeur supérieure à 253 - 1 (Number.MAX_SAFE_INTEGER).

Accessoirement, wasm-pack a aussi généré les typedefs (*.d.ts) des bindings JavaScript… On profite donc du typage, de l’autocomplétion et même d’une JSDoc tirée des commentaires Rust (///) 🎉.

Type definitions from Rust

omg

Dans le Navigateur

La lecture du paragraphe précédent (Node.js) est fortement recommandée.

On recompile notre lib en changeant la --target :

wasm-pack build --release --target bundler

💡 bundler est la cible par défaut à ce jour, mais tu peux aussi utiliser web sous certaines conditions.

On crée un projet basé sur la template NPM wasm-app :

mkdir browser && cd browser
npm init wasm-app
yarn add --dev ../pkg # ou : npm i -D ../pkg
yarn && yarn build # ou npm install && npm run build

Dans index.js, on procède de la même manière qu’avec Node.js :

import * as daysCount from 'days-count';

// pour changer du console.log 🤷
document.body.append(
  daysCount.count_days_between(
    // 01/02/2012
    BigInt(1330560000000),
    // 01/03/2012
    BigInt(1328054400000)
  )
);

Un coup de yarn start pour lancer webpack-dev-server et on constate sur http://localhost:8080 que “29” est bien ajouté au corps de la page.

Pour les utilisateurs de Webpack, le plugin wasm-pack-plugin est intéressant : il permet l’automatisation du processus que l’on vient de suivre, en important directement un projet Rust.

Pour les utilisateurs de Parcel, il existe aussi une template NPM dédiée : rust-parcel. Le Cargo.toml emploie par ailleurs deux crates très utiles : console_error_panic_hook, wee_alloc.

Ce qu’il faut retenir

so what?

Ok, on a compilé une lib Rust simplissime en WebAssembly, on en a fait un module, on l’a utilisé avec Node.js et Webpack. C’est cool, mais ça ne règle aucun problème… Ou peut-être que si ?

Les principaux bénéfices à en tirer sont :

  • Des gains très significatifs de performances dans l’exécution d’opérations longues et/ou intensives (calcul, compression, multimédia, jeux vidéo…).
  • La capacité d’utiliser un autre langage que JavaScript dans Node.js et/ou le navigateur. Notamment Rust, C et C++, pour des binaires Wasm optimisés. Si le poids du binaire ou la performance ne sont pas des critères pour votre projet, on peut aussi imaginer que les features du langage utilisé soient un argument suffisant (TypeScript avec AssemblyScript, Go, etc).

💡 Ce point sera abordé dans un prochain article, mais il est possible d’utiliser les API Web depuis le code Rust. Tout est bien détaillé dans la documentation de wasm-bindgen, dans la partie concernant web-sys : DOM, Events, fetching, WebGL, WebSockets 🎉…

C’est aussi vrai avec C et C++ grâce à https://emscripten.org.

Les opportunités apportées par cette technologie sont très prometteuses (et excitantes, ain’t they?). Comme beaucoup l’ont dit avant moi, WebAssembly ne remplacera pas JavaScript. Et si l’on s’en tient au seul aspect de la performance, même en JavaScript, V8 offre des performances souvent suffisantes pour beaucoup d’applications… Sans parler des outils plus simples à utiliser (et/ou mieux intégrés) en JavaScript pour le moment : debugging, code splitting, etc.

Mozilla, Intel, Fastly (créateurs de Lucet) et Red Hat ont créé la Bytecode Alliance, ayant pour but d’étendre l’existence de WebAssembly au-delà des navigateurs. Plus d’infos sur le blog de Mozilla… Et concernant Wasmtime et les WebAssembly Interface Types (WASI), cette vidéo de Lin Clark est incontournable 🔥 (et une version plus longue/développée ici).

wasm-everywhere

Ah, au fait. J’ai créé un projet TypeScript (💝) exploitant un binaire Wasm avec Node.js et Webpack, de la même manière que je l’ai exposé en JavaScript dans cet article.

Bonus - Des ressources qui valent le détour

#rust#webassembly#node.js
VinceOPS

Retrouvez-moi sur Twitter 🤷
@VinceOPS