- Adieu Suède je t'aimais bien
- Une étude de l'attractivité de Zeste de Savoir : le cas de la technique
WebAssembly est, avant tout, un standard pensé dans l’optique d’améliorer les performances de JS et d’établir un socle commun qui pourra, à l’avenir, être exploité par une variété de langages. Les premiers à communiquer avec wasm étant C, C++ ainsi que Rust.
Dans ce billet, je m’efforcerai de détailler les (chouettes) solutions mises en place pour faciliter cette intégration. C’est parti !
Un peu de contexte...
Actuellement, il existe trois principaux outils supportant la compilation, vers wasm, des langages dont je parlais plus haut:
- C et C++: Emscripten (site web, dépôt) et Binaryen (site web, dépôt). L’un est une importante toolchain qui n’a fait "que" ajouter un nouvel élément à son trousseau, alors que l’autre a été créé par l’équipe en charge du développement et de la démocratisation de WebAssembly. N’étant pas l’objet de ce billet, je vous invite vivement à consulter leur site web respectif, où vous pourrez y connaître les différences techniques des deux projets;
- Rust: wasm-bindgen, qui est certainement le moins mature, pour le moment, des trois.
Notez que Emscripten et Binaryen sont axés sur C et C++. Ça ne signifie, cependant, pas qu’ils ne supporteront jamais d’autres langages et c’est d’ailleurs l’un des objectifs partagés par wasm-bindgen qui ne devra, à terme, ne plus être directement lié au langage Rust.
Sa structure
En réalité, le projet est composé de deux éléments:
- La crate
wasm-bindgen
. Elle nous apporte l’attribut modestement nommé#[wasm-bindgen]
qui va servir d’interprète pour les deux partis (nous y reviendrons rapidement); - L’outil
wasm-bindgen-cli
qui n’est ni plus ni moins que le programme qui va nous permettre de rendre nos métadonnées (générées par l’attribut cité plus haut) intelligibles en wasm.
Petit prix à payer en revanche, ces fonctionnalités ne sont disponibles qu’en compilant avec la nightly de Rust.
Son fonctionnement
Les présentations ainsi faites, passons à des choses plus intéressantes !
L’attribut wasm-bindgen
Du côté de notre crate, #[wasm-bindgen]
servira à "marquer" les services, structures et implémentations (impl
) puis préparera à l’exportation tout ce petit monde sous la forme d’un module ECMAScript, facilitant, ensuite, largement leur importation dans le code JavaScript censé les accueillir.
Exporter des composants écrits en Rust vers JS
Prenons, dans un premier temps, les exemples originaux.
Côté Rust, dans lib.rs
:
// Current prelude for using `wasm_bindgen`, and this'll get smaller over time!
#![feature(proc_macro, wasm_custom_section, wasm_import_module)]
extern crate wasm_bindgen;
use wasm_bindgen::prelude::*;
// Here we're importing the `alert` function from the browser, using
// `#[wasm_bindgen]` to generate correct wrappers.
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
// Here we're exporting a function called `greet` which will display a greeting
// for `name` through a dialog.
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
}
Côté JavaScript:
const { greet } = wasm_bindgen;
function runApp() {
greet('World');
}
// Load and instantiate the wasm file, and we specify the source of the wasm
// file here. Once the returned promise is resolved we're ready to go and
// use our imports.
wasm_bindgen('../out/main_bg.wasm').then(runApp).catch(console.error);
Vous pouvez remarquer à la ligne 9 que le bloc extern {}
, couramment utilisé avec la FFI, dispose de la même fonction qu’en temps normal: déclarer les signatures des services que nous tentons d’utiliser à partir d’une ABI connue. Ici, nous souhaitons user de la méthode window.alert()
que nous propose le navigateur.
Maintenant, nous pourrions toujours réadapter cet exemple pour illustrer ce qui a été dit juste avant: il nous est possible d’exporter des structures et l’implémentation de leurs services.
Côté Rust, dans lib.rs
:
#[wasm_bindgen]
extern {
fn alert(s: &str);
}
#[wasm_bindgen]
pub struct Foo;
#[wasm_bindgen]
impl Foo {
pub fn new() -> Foo {
Foo
}
pub fn greet(&self, name: &str) {
alert(&format!("Hello, {}!", name));
}
}
Côté JavaScript:
/*
On récupère la structure exactement comme
on pourrait le faire avec un module ECMAScript
écrit en JavaScript.
*/
const { Foo } = wasm_bindgen;
function runApp() {
Foo.new().greet('World');
}
wasm_bindgen('../out/main_bg.wasm').then(runApp).catch(console.error);
Exporter des composants écrits en JS vers Rust
#[wasm_bindgen]
dispose de trois paramètres (dont un variable):
module
: renseigne le chemin à partir duquel le compilateur est censé charger le module ECMAScript;constructor
: précise que la signature, suivie par ce paramètre, représente le constructeur du prototype;method
: précise que la signature, suivie par ce paramètre, représente une méthode du prototype.
Ce trio vous permet d’importer des structures initialement écrites en JS.
Côté Rust:
// On renseigne le nom du module.
#[wasm_bindgen(module = "main")]
extern {
fn alert(s: &str);
// Ici, on se sert du mot-clé `type`
// pour préciser que l'on souhaite importer
// la structure `MyJavaScriptObject`.
type MyJavaScriptObject;
// On définit la signature du constructeur.
#[wasm_bindgen(constructor)]
fn new() -> MyJavaScriptObject;
// On définit l'identificateur et la signature
// de la méthode d'instance.
#[wasm_bindgen(method)]
fn say_hello(this: &MyJavaScriptObject) -> ();
}
#[wasm_bindgen]
pub fn greet(name: &str) {
alert(&format!("Hello, {}!", name));
MyJavaScriptObject::new().say_hello();
}
Côté JavaScript:
export class MyJavaScriptObject {
constructor() {
this.greetings = "Hi, I'm a JavaScript object!";
}
say_hello() {
console.log(this.greetings);
}
}
Mon avis
J’aimerais faire un retour qui servira de conclusion à ce petit billet, si ça peut aider certains développeurs à situer WASM/Rust en terme de maturité.
Nous avons une vision basique de ce qu’un développeur utilisant wasm est censé devoir faire pour lier JS à un module wasm. Rien de très compliqué, en somme, mais les possibilités me semblent encore limitées.
Ajoutons à cela que la compréhension même du langage n’est pas encore très bonne. Les lifetimes ne sont pas supportées, les constantes ne peuvent être exportées et les wrappers tels que Option
et Result
ne sont pas disponibles. En bref, y’a du boulot.