Licence CC BY

Wasm-bindgen, qu'est-ce que c'est ?

Dernière mise à jour :
Auteur :
Catégories :
Temps de lecture estimé : 10 minutes

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:

  1. 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;
  2. 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:

  1. 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);
  2. 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.

Processus de compilation et interaction entre les deux partis (Rust et JS)
Processus de compilation et interaction entre les deux partis (Rust et JS)

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):

  1. module: renseigne le chemin à partir duquel le compilateur est censé charger le module ECMAScript;
  2. constructor: précise que la signature, suivie par ce paramètre, représente le constructeur du prototype;
  3. 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. :D

Source(s)

Voir aussi

4 commentaires

L’appel des méthodes Rust depuis JS ne réduit pas les performances du tout ?

Absolute

Dans l’absolu, JavaScript est beaucoup plus lent que Rust, donc si tu appelles des méthodes/fonctions provenant de Rust depuis JavaScript, ça ne devrait pas réduire mais, au pire, faire stagner les performances, au mieux les améliorer.

A l’inverse, si ta question était

Est-ce que ça ne réduit pas les performances du programme écrit en Rust ?

Bah… si, mais le but est d’offrir des performances proches du natif pour compenser ce qu’il (JS) ne peut pas faire lui-même. Actuellement, l’équipe WebAssembly préfère développer wasm (et donc tous les langages supportés) autour de JavaScript plutôt que comme une alternative pure et dure à ce dernier.

Édité par Songbird

Vous devez être connecté pour pouvoir poster un message.
Connexion

Pas encore inscrit ?

Créez un compte en une minute pour profiter pleinement de toutes les fonctionnalités de Zeste de Savoir. Ici, tout est gratuit et sans publicité.
Créer un compte