Utiliser l'API JavaScript de WebAssembly

Si vous avez déjà compilé un module depuis un autre langage en utilisant des outils comme Emscripten, ou chargé et éxecuté vous-même le code, l'étape suivante est d'en apprendre plus à propos des autres fonctionnalités de l'API JavaScript WebAssembly. Cet article vous enseigne ce que vous aurez besoin de connaître.

Note : Si vous n'êtes pas familier avec les concepts de base mentionnés dans cet article et vous avez besoin de plus d'explication, lisez d'abord WebAssembly concepts.

Quelques exemples simples

Parcourons quelques exemples illustrant l'utilisation de l'API Webassembly JavaScript, et en particulier la manière dont elle peut être utilisé pour charger un module wasm au sein d'une page web.

Note : Vous pouvez trouver des exemples de code dans notre repo GitHub webassembly-examples.

Préparation

  1. Premièrement nous avons besoin d'un module wasm ! Récupérez notre fichier simple.wasm et sauvegardez une copie dans un nouveau document sur votre machine locale.

  2. Ensuite, assurez-vous d'utiliser un navigateur supportant WebAssembly. Firefox 52+ et Chrome 57+ supportent WebAssembly par défaut.

  3. Pour poursuivre, créez un simple fichier nommé index.html dans le même dossier que votre fichier wasm (vous pouvez utiliser notre template simple si vous n'en avez pas de facilement accessible).

  4. Maintenant, pour nous aider à comprendre ce qui se passe ici, regardons la représentation textuelle de notre module wasm (que nous rencontrons aussi dans Converting WebAssembly format to wasm):

    (module
      (func $i (import "imports" "imported_func") (param i32))
      (func (export "exported_func")
        i32.const 42
        call $i))
    
  5. À la deuxième ligne, vous pouvez constater l'import d'un namespace à deux niveaux — la fonction interne $i est importée depuis imports.imported_func. Dans notre JavaScript, notre namespace doit reprendre ce format à deux niveaux lors de l'écriture de l'objet à importer dans le module wasm. Pour ce faire, créez un élément dans votre fichier HTML, puis ajoutez le code suivant:

    js
    var importObject = {
      imports: {
        imported_func: function (arg) {
          console.log(arg);
        },
      },
    };
    

Streaming de notre module webassembly

Il est dorénavant possible dans Firefox 58 de compiler et instancier les modules Webassembly directement à partir des ressources initiales. Il est nécessaire dans ce cas d'utiliser les méthodes WebAssembly.compileStreaming() et WebAssembly.instantiateStreaming(). Ces méthodes en streaming sont plus facile d'utilisation que leurs contreparties synchrones, car elles traduisent directement le bytecode en instances de type Module/Instance, sans nécessiter la manipulation d'une réponse intermédiaire Response en un ArrayBuffer.

Cet exemple (voir notre démo sur GitHub instantiate-streaming.html, et également view it live) montre comment utiliser instantiateStreaming() pour récupérer un module wasm, le compiler, l'instancier afin d'avoir accès aux fonctions exportées qu'il contient et d'y importer des fonctions JavaScript, le tout en une seule et même étape.

Ajoutez le code suivant à votre script, en dessous du premier bloc:

js
WebAssembly.instantiateStreaming(fetch("simple.wasm"), importObject).then(
  (obj) => obj.instance.exports.exported_func(),
);

Le résultat net de l'ensemble du code est que l'appel à exported_func, notre fonction exportée Webassembly, entraîne à son tour l'appel à imported_func notre fonction importée JavaScript, qui logue la valeur 42 fournie à l'instance WebAssembly dans la console. Si vous sauvegardez l'exemple et chargez le code dans un navigateur qui supporte WebAssembly, vous pourrez le voir en action.

Note: Cet exemple interminable et tarabiscoté semble aboutir à un résultat de faible importance, il permet néanmoins d'illustrer les possibilités offertes par l'utilisation conjointe d'un code WebAssembly et d'un code JavaScript dans une application web. Comme il est précisé ailleurs, l'objectif de WebAssembly n'est pas de remplacer JavaScript; à vrai dire les deux sont conçus pour fonctionner de concert, chacun tirant parti des forces de l'autre.

Chargement de notre module wasm sans streaming

Si vous ne pouvez pas ou ne souhaitez pas utiliser les méthodes en streaming décrites ci-dessus, vous pouvez utiliser à la place les méthodes synchrones WebAssembly.compile / WebAssembly.instantiate.

Ces méthodes n'accèdent pas directement au bytecode, elles requièrent une étape supplémentaire afin de transformer la réponse en un ArrayBuffer , et cela avant les étapes de compilation/instanciation du module wasm.

Le code équivalent à l'exemple précédent prend la forme suivante:

js
fetch("simple.wasm")
  .then((response) => response.arrayBuffer())
  .then((bytes) => WebAssembly.instantiate(bytes, importObject))
  .then((results) => {
    results.instance.exports.exported_func();
  });

Visualiser wasm dans l'outil de développement

In Firefox 54+, the Developer Tool Debugger Panel has functionality to expose the text representation of any wasm code included in a web page. To view it, you can go to the Debugger Panel and click on the "wasm://" entry.

Starting soon in Firefox, in addition to viewing WebAssembly as text, developers will be able to debug (place breakpoints, inspect the callstack, single-step, etc.) WebAssembly using the text format. See WebAssembly debugging with Firefox DevTools for a video preview.

Memory

Dans le modèle mémoire bas niveau de WebAssembly, la mémoire est représentée comme une suite continue de bytes non typés appelée Linear Memory. Cette mémoire linéaire est accessible en écriture et en lecture par des instructions load et store à l'intérieur du module. Dans ce modèle de mémoire, les instructions load et store peuvent accéder à n'importe quel byte de la mémoire linéaire, ce qui est nécessaire à une réprésentation fidèle de concepts C/C++ comme les pointeurs.

Cependant contrairement à une implémentation native d'un programe C/C++ dans laquelle l'espace de mémoire disponible recouvre celle de l'ensemble du processus, la mémoire accessible par une instance particulière de WebAssembly est un espace mémoire spécifique — potentiellement très réduit — contenu dans une objet mémoire WebAssembly. Ceci permet à une application web unique d'utiliser des librairies indépendantes — Chacune d'entre elles pouvant utiliser en interne WebAssembly— avec des espaces mémoires séparés qui sont complètement isolés les uns des autres.

Dans JavaScript, une instance Memory peut être pensée comme un ArrayBuffer redimensionnable. De la même manière que pour les ArrayBuffers, une application web peut créer de nombreux objets Memory indépendants. Vous pouvez en créer un en utilisant le constructeur WebAssembly.Memory(), qui prend comme arguments la taille initiale ainsi que la taille maximale de l'espace mémoire à créer.

Explorons ces concepts à travers un exemple rapide.

  1. Créez une autre page HTML (copiez pour cela notre simple template) et appelez la memory.html. Ajoutez un élement à la page.

  2. Maintenant ajoutez la ligne suivante en haut de votre script, afin de créer une instance mémoire:

    js
    var memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });
    

    L'unité pour initial et maximum correspond à une page WebAssembly — soit une taille fixe de 64 KB. Cela signifie que l'instance mémoire ci-dessus à une taille initiale de 64 KB, et une taille maximum de 6.4 MB.

    WebAssembly memory expose ses bytes par l'intermédiaire d'un tampon getter/setter qui retourne un ArrayBuffer. Par exemple, pour écrire 42 directement dans l'espace du premier mot de la mémoire linéaire, vous pouvez faire cela:

    js
    new Uint32Array(memory.buffer)[0] = 42;
    

    Vous pouvez retourner cette même valeur en utilisant:

    js
    new Uint32Array(memory.buffer)[0];
    
  3. À vous d'essayer — Enregistrez ce que vous avez rédigé jusqu'à maintenant, chargez-le dans votre navigateur, puis essayez d'entrer les deux lignes ci-dessus dans votre javascript console.

Redimensionner la mémoire

Une instance de mémoire peut être agrandie par appel à la méthode Memory.prototype.grow(), qui prend comme argument la taille de mémoire à ajouter (en unité de page WebAssembly).

js
memory.grow(1);

Si une valeur maximum a été fournie à la création de l'instance mémoire, les tentatives d'augmenter l'espace mémoire au delà de cette valeur maximum aboutiront à une exception de type WebAssembly.RangeError. Le moteur JavaScript utilise cette valeur limite supérieure pour réserver d'avance un espace mémoire suffisant, ce qui permet de rendre les redimensionnements mémoires plus efficaces.

Note: En raison du caractère immuable de la longueur de byte d'un ArrayBuffer, après une opération Memory.prototype.grow() réussie, le buffer getter retourne un nouvel objet ArrayBuffer (avec la nouvelle longeur de byte du buffer) et tous les objets ArrayBuffer précédents se retrouve en état "dissocié", ou déconnectés de l'espace mémoire dont ils étaient issus initialement.

Tout comme les fonctions, les espaces mémoires linéaires peuvent être définis à l'intérieur du module, ou bien importés. De manière similaire aux fonctions, un module peut également exporter sa mémoire. Cela signifie que JavaScript peut accéder à la mémoire d'une instance WebAssembly soit en créant un nouveau WebAssembly.Memory afin de le passer en import à cette instance, soit en recevant un export Memory (via Instance.prototype.exports).

Exemple avancé pour l'utilisation mémoire

Essayons de clarifier les affirmations ci-dessus à l'aide d'un exemple plus abouti — à savoir un module WebAssembly qui importe une instance mémoire telle que définie plus tôt, et qui l'alimente d'un tableau d'entiers, pour en faire la somme totale. Vous pouvez trouver cela dans ce fichier memory.wasm.

  1. Faites une copie locale de memory.wasm dans le même dossier que précédement.

    Note : Vous pouvez trouver la représentation textuelle du module sur memory.wat.

  2. Retournez à votre fichier memory.html, et récupérez, compilez, et instancier votre module wasm comme précédement — Ajoutez à la fin de votre script les lignes suivantes:

    js
    WebAssembly.instantiateStreaming(fetch("memory.wasm"), {
      js: { mem: memory },
    }).then((results) => {
      // add code here
    });
    
  3. Puisque ce module exporte sa mémoire, nous pouvons utiliser la fonction exportée accumulate() à partir du champ instance (de type Instance) de la valeur de retour results du module pour créer et alimenter l'instance mémoire du module (mem) avec un tableau. Ajoutez les lignes suivantes à votre code à l'emplacement indiqué dans le snippet précédent.

    js
    var i32 = new Uint32Array(memory.buffer);
    
    for (var i = 0; i < 10; i++) {
      i32[i] = i;
    }
    
    var sum = results.instance.exports.accumulate(0, 10);
    console.log(sum);
    

Note: vous pouvez remarquer que nous avons créé la vue Uint32Array sur le champ buffer de l'objet Memory (Memory.prototype.buffer), et pas sur l'objet Memory lui même.

Les imports Memory fonctionnent de la même manière que les imports fonctions, à la différence prés que les objets Memory véhiculent des valeurs au lieu de fonctions javascripts. Les imports Memory sont utiles pour deux raisons:

  • Ils permettent de récupérer et créer le contenu mémoire initial avant ou en parrallèle de la compilation du module.
  • Ils permettent qu'un objet mémoire unique soit importé par des instances de modules multiples, ce qui est une fonctionnalité clef dans l'objectif d'une implémentation d'une connexion dynamique dans WebAssembly.

Note: Vous pouvez trouver une démo complete à memory.html (à voir également live) .

Tables

Une Table WebAssembly est un tableau de références typées redimensionnable qui peut être accédé à la fois par du code JavaScript et par du code WebAssembly. Memory fournit un tableau de bytes bruts redimensionnable, mais il n'est pas prudent d'y stocker des références, car une référence est une valeur considérée comme sûre par le moteur JavaScript, valeur dont les bytes ne doivent être accessibles ni en lecture, ni en écriture par le contenu pour des raisons de sécurité, de portabilité, et de stabilité.

Les Tables possèdent un type, qui limite les types de références qui peuvent être contenues dans la table. Dans la version actuelle de WebAssembly, il n'existe qu'un seul type de références — functions — et de fait seul ce type de références est donc valide. Dans de prochaines versions, d'autres types de références seront ajoutés.

Les références de type fonction sont nécessaires afin de compiler des languages comme C/C++ qui permettent l'implémentation de pointeurs sur fonctions. Dans une implémentation native en C/C++, un pointeur sur fonction est représenté par une adresse brute associée au code de la fonction contenue dans l'espace d'adressage virtuel du processus. Pour les raisons de sécurités mentionnées plus haut, cette référence dans WebAssembly ne peut être stockée directement en mémoire linéaire. Les références de fonctions sont stockées dans une table et leurs index, qui sont des entiers, peuvent être placés en mémoire linéaire et véhiculés de manière sûre.

Lorsque l'appel à un pointeur sur fonction est nécessaire, le caller WebAssembly fournit l'index de la référence à appeler. La valeur de cet index est controlée par rapport au valeurs limites données à l'instantiation de la table (safety bounds checked), et cela avant que l'appel par référence à la fonction soit effectué. Autrement dit, les tables sont actuellement des primitives bas niveau utilisées pour compiler des fonctionnalités de language de programmation bas niveau, de manière sûre et portable.

Les Tables peuvent être modifiées via Table.prototype.set(), which updates one of the values in a table, and Table.prototype.grow(), which increases the number of values that can be stored in a table. This allows the indirectly-callable set of functions to change over time, which is necessary for dynamic linking techniques. The mutations are immediately accessible via Table.prototype.get() in JavaScript, and to wasm modules.

Un exemple de table

Envisageons un exemple basique d'utilisation d'une table — un module WebAssembly qui crée et exporte une table contenant 2 éléments: l'élement 0 retourne 13 et l'élément 1 retourne 42. Vous retrouvez cela dans le fichier table.wasm.

  1. Faites une copie locale de table.wasm dans un nouveau dossier.

    Note : Vous pouvez voir une réprésentation textuelle du module sur table.wat.

  2. Créez une nouvelle copie du template HTML dans le même dossier et nommez le table.html.

  3. Comme précédement, récupérez, compilez, et instanciez le module wasm — ajoutez les lignes suivantes à l'intérieur d'un élement dans votre fichier HTML, puis ajoutez le code suivant:\n

    js
    var importObject = {\n  imports: {\n    imported_func: function (arg) {\n      console.log(arg);\n    },\n  },\n};\n\n\n"}},{"type":"prose","value":{"id":"streaming_de_notre_module_webassembly","title":"Streaming de notre module webassembly","isH3":true,"content":"

    Il est dorénavant possible dans Firefox 58 de compiler et instancier les modules Webassembly directement à partir des ressources initiales. Il est nécessaire dans ce cas d'utiliser les méthodes WebAssembly.compileStreaming() et WebAssembly.instantiateStreaming(). Ces méthodes en streaming sont plus facile d'utilisation que leurs contreparties synchrones, car elles traduisent directement le bytecode en instances de type Module/Instance, sans nécessiter la manipulation d'une réponse intermédiaire Response en un ArrayBuffer.\n

    Cet exemple (voir notre démo sur GitHub instantiate-streaming.html, et également view it live) montre comment utiliser instantiateStreaming() pour récupérer un module wasm, le compiler, l'instancier afin d'avoir accès aux fonctions exportées qu'il contient et d'y importer des fonctions JavaScript, le tout en une seule et même étape.\n

    Ajoutez le code suivant à votre script, en dessous du premier bloc:\n

    js
    WebAssembly.instantiateStreaming(fetch(\"simple.wasm\"), importObject).then(\n  (obj) => obj.instance.exports.exported_func(),\n);\n\n

    Le résultat net de l'ensemble du code est que l'appel à exported_func, notre fonction exportée Webassembly, entraîne à son tour l'appel à imported_func notre fonction importée JavaScript, qui logue la valeur 42 fournie à l'instance WebAssembly dans la console. Si vous sauvegardez l'exemple et chargez le code dans un navigateur qui supporte WebAssembly, vous pourrez le voir en action.\n

    Note: Cet exemple interminable et tarabiscoté semble aboutir à un résultat de faible importance, il permet néanmoins d'illustrer les possibilités offertes par l'utilisation conjointe d'un code WebAssembly et d'un code JavaScript dans une application web. Comme il est précisé ailleurs, l'objectif de WebAssembly n'est pas de remplacer JavaScript; à vrai dire les deux sont conçus pour fonctionner de concert, chacun tirant parti des forces de l'autre."}},{"type":"prose","value":{"id":"chargement_de_notre_module_wasm_sans_streaming","title":"Chargement de notre module wasm sans streaming","isH3":true,"content":"

    Si vous ne pouvez pas ou ne souhaitez pas utiliser les méthodes en streaming décrites ci-dessus, vous pouvez utiliser à la place les méthodes synchrones WebAssembly.compile / WebAssembly.instantiate.\n

    Ces méthodes n'accèdent pas directement au bytecode, elles requièrent une étape supplémentaire afin de transformer la réponse en un ArrayBuffer , et cela avant les étapes de compilation/instanciation du module wasm.\n

    Le code équivalent à l'exemple précédent prend la forme suivante:\n

    js
    fetch(\"simple.wasm\")\n  .then((response) => response.arrayBuffer())\n  .then((bytes) => WebAssembly.instantiate(bytes, importObject))\n  .then((results) => {\n    results.instance.exports.exported_func();\n  });\n"}},{"type":"prose","value":{"id":"visualiser_wasm_dans_loutil_de_développement","title":"Visualiser wasm dans l'outil de développement","isH3":true,"content":"

    In Firefox 54+, the Developer Tool Debugger Panel has functionality to expose the text representation of any wasm code included in a web page. To view it, you can go to the Debugger Panel and click on the \"wasm://\" entry.\n

    \"\"\n

    Starting soon in Firefox, in addition to viewing WebAssembly as text, developers will be able to debug (place breakpoints, inspect the callstack, single-step, etc.) WebAssembly using the text format. See WebAssembly debugging with Firefox DevTools for a video preview."}},{"type":"prose","value":{"id":"memory","title":"Memory","isH3":false,"content":"

    Dans le modèle mémoire bas niveau de WebAssembly, la mémoire est représentée comme une suite continue de bytes non typés appelée Linear Memory. Cette mémoire linéaire est accessible en écriture et en lecture par des instructions load et store à l'intérieur du module. Dans ce modèle de mémoire, les instructions load et store peuvent accéder à n'importe quel byte de la mémoire linéaire, ce qui est nécessaire à une réprésentation fidèle de concepts C/C++ comme les pointeurs.\n

    Cependant contrairement à une implémentation native d'un programe C/C++ dans laquelle l'espace de mémoire disponible recouvre celle de l'ensemble du processus, la mémoire accessible par une instance particulière de WebAssembly est un espace mémoire spécifique — potentiellement très réduit — contenu dans une objet mémoire WebAssembly. Ceci permet à une application web unique d'utiliser des librairies indépendantes — Chacune d'entre elles pouvant utiliser en interne WebAssembly— avec des espaces mémoires séparés qui sont complètement isolés les uns des autres.\n

    Dans JavaScript, une instance Memory peut être pensée comme un ArrayBuffer redimensionnable. De la même manière que pour les ArrayBuffers, une application web peut créer de nombreux objets Memory indépendants. Vous pouvez en créer un en utilisant le constructeur WebAssembly.Memory(), qui prend comme arguments la taille initiale ainsi que la taille maximale de l'espace mémoire à créer.\n

    Explorons ces concepts à travers un exemple rapide.\n

      \n
    1. \n

      Créez une autre page HTML (copiez pour cela notre simple template) et appelez la memory.html. Ajoutez un élement à la page.\n\n

    2. \n

      Maintenant ajoutez la ligne suivante en haut de votre script, afin de créer une instance mémoire:\n

      js
      var memory = new WebAssembly.Memory({ initial: 10, maximum: 100 });\n\n

      L'unité pour initial et maximum correspond à une page WebAssembly — soit une taille fixe de 64 KB. Cela signifie que l'instance mémoire ci-dessus à une taille initiale de 64 KB, et une taille maximum de 6.4 MB.\n

      WebAssembly memory expose ses bytes par l'intermédiaire d'un tampon getter/setter qui retourne un ArrayBuffer. Par exemple, pour écrire 42 directement dans l'espace du premier mot de la mémoire linéaire, vous pouvez faire cela:\n

      js
      new Uint32Array(memory.buffer)[0] = 42;\n\n

      Vous pouvez retourner cette même valeur en utilisant:\n

      js
      new Uint32Array(memory.buffer)[0];\n\n\n
    3. \n

      À vous d'essayer — Enregistrez ce que vous avez rédigé jusqu'à maintenant, chargez-le dans votre navigateur, puis essayez d'entrer les deux lignes ci-dessus dans votre javascript console.\n\n"}},{"type":"prose","value":{"id":"redimensionner_la_mémoire","title":"Redimensionner la mémoire","isH3":true,"content":"

      Une instance de mémoire peut être agrandie par appel à la méthode Memory.prototype.grow(), qui prend comme argument la taille de mémoire à ajouter (en unité de page WebAssembly).\n

      js
      memory.grow(1);\n\n

      Si une valeur maximum a été fournie à la création de l'instance mémoire, les tentatives d'augmenter l'espace mémoire au delà de cette valeur maximum aboutiront à une exception de type WebAssembly.RangeError. Le moteur JavaScript utilise cette valeur limite supérieure pour réserver d'avance un espace mémoire suffisant, ce qui permet de rendre les redimensionnements mémoires plus efficaces.\n

      Note: En raison du caractère immuable de la longueur de byte d'un ArrayBuffer, après une opération Memory.prototype.grow() réussie, le buffer getter retourne un nouvel objet ArrayBuffer (avec la nouvelle longeur de byte du buffer) et tous les objets ArrayBuffer précédents se retrouve en état \"dissocié\", ou déconnectés de l'espace mémoire dont ils étaient issus initialement.\n

      Tout comme les fonctions, les espaces mémoires linéaires peuvent être définis à l'intérieur du module, ou bien importés. De manière similaire aux fonctions, un module peut également exporter sa mémoire. Cela signifie que JavaScript peut accéder à la mémoire d'une instance WebAssembly soit en créant un nouveau WebAssembly.Memory afin de le passer en import à cette instance, soit en recevant un export Memory (via Instance.prototype.exports)."}},{"type":"prose","value":{"id":"exemple_avancé_pour_lutilisation_mémoire","title":"Exemple avancé pour l'utilisation mémoire","isH3":true,"content":"

      Essayons de clarifier les affirmations ci-dessus à l'aide d'un exemple plus abouti — à savoir un module WebAssembly qui importe une instance mémoire telle que définie plus tôt, et qui l'alimente d'un tableau d'entiers, pour en faire la somme totale. Vous pouvez trouver cela dans ce fichier memory.wasm.\n

        \n
      1. \n

        Faites une copie locale de memory.wasm dans le même dossier que précédement.\n

      2. \n

        Retournez à votre fichier memory.html, et récupérez, compilez, et instancier votre module wasm comme précédement — Ajoutez à la fin de votre script les lignes suivantes:\n

        js
        WebAssembly.instantiateStreaming(fetch(\"memory.wasm\"), {\n  js: { mem: memory },\n}).then((results) => {\n  // add code here\n});\n\n\n
      3. \n

        Puisque ce module exporte sa mémoire, nous pouvons utiliser la fonction exportée accumulate() à partir du champ instance (de type Instance) de la valeur de retour results du module pour créer et alimenter l'instance mémoire du module (mem) avec un tableau. Ajoutez les lignes suivantes à votre code à l'emplacement indiqué dans le snippet précédent.\n

        js
        var i32 = new Uint32Array(memory.buffer);\n\nfor (var i = 0; i < 10; i++) {\n  i32[i] = i;\n}\n\nvar sum = results.instance.exports.accumulate(0, 10);\nconsole.log(sum);\n\n\n\n

        Note: vous pouvez remarquer que nous avons créé la vue Uint32Array sur le champ buffer de l'objet Memory (Memory.prototype.buffer), et pas sur l'objet Memory lui même.\n

        Les imports Memory fonctionnent de la même manière que les imports fonctions, à la différence prés que les objets Memory véhiculent des valeurs au lieu de fonctions javascripts. Les imports Memory sont utiles pour deux raisons:\n