WebAssembly : programmer en assembleur dans une page Web

Qui programme en assembleur de nos jours ? Plus qu'il n'y paraît en fait, si l'on veut bien considérer qu'avec WebAssembly - Wasm pour les intimes -, il est devenu possible d'écrire des programmes dans un langage qui s'en approche, et de les exécuter dans le contexte d'une page Web.
Wasm, un assembleur pour le Web
L'intérêt ? Des performances annoncées de très loin supérieures à celle de tout code JavaScript, aussi optimisé qu'il soit, sans perdre le bénéfice d'une intégration parfaite dans l'environnement d'exécution d'une page Web, c'est-à-dire en pouvant facilement accéder à des données et à des sous-programmes Wasm depuis du code JavaScript, mais aussi faire l'inverse.
Comment écrire en Wasm ? Comment exécuter du Wasm dans une page Web ? Et comment dialoguer entre Wasm et JavaScript ? Tout cela et plus encore dans ce qui suit...
Mise à jour du 21/12/2018 : Rajout d'un encadré pour remercier Dan Gohman, l'auteur du WebAssembly Reference Manual.
Mise à jour du 18/12/2018 : Mention à une représentation intelligible de la syntaxe d'un module, finalement trouvée (en creusant profond) ici.
Mise à jour du 17/12/2018 : Correction de petits bogues dans importExportMemory.zip.
Mise à jour du 16/12/2018 : Ajout d'une section liminaire "WebAssembly, c'est quoi ?", et d'une conclusion "WebAssembly en vaut-il la peine ?".

WebAssembly, c'est quoi ?

Qui veut comprendre en quoi WebAssembly consiste se reportera très naturellement vers sa documentation officielle. La première chose qu'il pourra lire, c'est que WebAssembly a pour premier objectif :
Define a portable, size- and load-time-efficient binary format to serve as a compilation target which can be compiled to execute at native speed by taking advantage of common hardware capabilities available on a wide range of platforms.
Le moins qu'on puisse dire, c'est que c'est mal parti : WebAssembly permettrait de compiler du code pour générer un binaire, lequel ensuite serait compilé (encore !) pour tirer parti du hardware ? Ce n'est pas clair du tout. Par ailleurs, où est le langage dans tout cela ? Il n'est question que d'un format de binaire. Une nouvelle illustration de l'incompétence crasse de ceux qui élaborent des technologies à communiquer dessus...
Fort heureusement, il est possible de se tourner vers d'autres sources. Ainsi, la documentation de MDN présente WebAssembly dans des termes plus intelligibles :
WebAssembly is a low-level assembly-like language with a compact binary format that runs with near-native performance and provides languages with low-level memory models such as C++ and Rust with a compilation target so that they can run on the web.
C'est plus clair, mais cela ne permet pas vraiment de comprendre la place que WebAssembly occupe dans le paysage. Pour cela, il est possible de se tourner vers des explications tierces visant un public plus large, comme cet article de Milica Mihajlija, où l'auteur montre qu'une fois généré sous forme binaire à partir d'un source écrit dans un langage haut-niveau (C, C++, Rust) ou dans le langage bas-niveau Wasm, du code Wasm est chargé et transformé en code natif plus rapidement que du code JavaScript par le moteur de scripting.
Toutefois, la meilleure présentation de WebAssembly est ce papier rédigé par un paquet d'auteurs de Google, Mozilla et Apple. Franchement, si vous ne devez lire qu'une chose sur WebAssembly avant de lire le présent article, c'est ce papier qu'il faut lire. Tout n'est pas intelligible pour le commun des mortels dont je suis, mais les parties 1 (Introduction), 2 (Overview), 5 (Binary Format), 6 (Embedding and Interoperability), 7 (Implementation) le sont et permettent de se faire une bonne idée de quoi il en retourne.

Ecrire du code Wasm

Dès les premières lignes de la documentation de MDN, il est possible de lire la chose suivante :
It [WebAssembly] is not primarily intended to be written by hand, rather it is designed to be an effective compilation target for low-level source languages like C, C++, Rust, etc.
Qu'on se le tienne donc pour dit. Toutefois, après avoir essayé pour aller au fond des choses, il s'avère particulièrement amusant d'écrire en Wasm. C'est pourquoi cet article sur la manière d'écrire du code en Wasm, de le transformer en binaire et de l'exécuter dans le contexte d'une page Web.
Un programme Wasm ne s'utilise pas tel quel. Il doit être packagé dans un module, comme cela sera expliqué plus loin. Ce module comprend notamment des fonctions, dont le corps constitue le code Wasm à proprement parler.
Comment apprendre à programmer en Wasm ? Comme c'est devenu une habitude en matière de langage, la spécification est extrêmement difficile à déchiffrer, étant rédigée dans un langage cryptique. Pour se mettre à Wasm, le mieux est de commencer par lire le présent article, fruit de la synthèse d'informations puisées dans la spécification, des guides et des didacticiels - toutes les sources sont citées à la fin. Par la suite, la documentation recommandée est définitivement le WebAssembly Reference Manual de Dan Gohman.
Merci Dan Gohman !
Encore un nouveau langage, et encore une spécification cryptique ! C'est à croire que certains souhaitent que les technologies qu'ils élaborent restent à jamais incomprises. En cela, ils suivent l'exemple des auteurs de la spécification ECMA-262 qui sert de référence pour l'élaboration de JavaScript.
Soyons clairs : spécifier un langage de manière aussi obscure, c'est insulter ceux qui souhaitent s'y intéresser. Sincèrement, devoir apprendre un langage pour parvenir à lire la spécification permettant d'apprendre un langage ? On se moque de qui ? C'est condamner les gens à perdre du temps sans véritable chance de parvenir finalement y voir clair, et/ou les condamner à se contenter de sources secondaires, dont les auteurs seront généralement passés à côté d'aspects essentiels, à moins d'avoir participé à l'élaboration du langage eux-mêmes.
Ce mépris n'est pourtant pas sans risque. Le fait que sa spécification soit aussi obscure n'a certainement pas aidé à la compréhension de JavaScript. Au point qu'un principe aussi fondamental que l'héritage à base de prototypes - et non de classes - est à ce point resté obscur pour la plupart, que le langage a été finalement perverti par l'introduction d'un sucre syntaxique - le mot-clé class - au prétexte de faciliter sa prise en main !
Grâce soit donc rendue à David Gohman pour avoir persisté de son côté dans son effort, jusqu'à nous offrir le WebAssembly Reference Manual ! Comme il l'explique ici en réponse à une question sur ce sujet, David a participé à la conception de la technologie. A cette occasion, il avait commencé à rédiger un manuel lisible, malheureusement rejeté. Citons-le :
At the time, it was my hope that WebAssembly's semantics would be very simple, such that a simple description in the style of the document here could easily and completely describe them. However, several of my proposals to that effect were rejected, and WebAssembly ended up with semantics that get very tricky. I attempted to describe the semantics that were accepted in this document, however it's quite possible that I've gotten some things subtly wrong.
N'est-il pas fabuleux de lire que l'un des concepteurs n'est pas certain lui-même de bien s'y retrouver dans la spécification finalement produite ? Kafka, c'est maintenant ! Espérons que David aura le courage de continuer à faire évoluer son manuel.
Commençons par le langage Wasm lui-même, celui dans lequel le corps des fonctions doit être rédigé. En Wasm, il n'y a que quatre types, qui sont des entiers ou des flottants, en 32 ou 64 bits : i32, i64, f32, f64.
Le tableau suivant recense a priori toutes les instructions possibles, sans détailler leur syntaxe complète, dont leurs opérandes - pour cela, se référer plutôt au WebAssembly Reference Manual mentionné à plus tôt. L'idée est de vous donner rapidement une vision sur ce qui est à disposition :
Les commentaires
;;
Les instructions de contrôle
block...end
loop...end
if...end
if...else...end
br
br_if
br_table
return
unreachable
nop
call
call_indirect
Les manipulations de la pile
drop
select
L'arithmétique des flottants
[f32 | f64].add
[f32 | f64].sub
[f32 | f64].mul
[f32 | f64].div
[f32 | f64].sqrt
[f32 | f64].min
[f32 | f64].max
[f32 | f64].ceil
[f32 | f64].floor
[f32 | f64].trunc
[f32 | f64].nearest
[f32 | f64].abs
[f32 | f64].neg
[f32 | f64].copysign
L'arithmétique des entiers
[i32 | i64].add
[i32 | i64].sub
[i32 | i64].mul
[i32 | i64].divs
[i32 | i64].divu
[i32 | i64].rems
[i32 | i64].remu
[i32 | i64].and
[i32 | i64].or
[i32 | i64].xor
[i32 | i64].shl
[i32 | i64].shr_s
[i32 | i64].shr_u
[i32 | i64].rotl
[i32 | i64].rotr
[i32 | i64].clz
[i32 | i64].ctz
[i32 | i64].popcnt
[i32 | i64].eqz
Les comparaisons sur les flottants
[f32 | f64].eq
[f32 | f64].ne
[f32 | f64].lt
[f32 | f64].le
[f32 | f64].gt
[f32 | f64].ge
Les comparaisons d'entiers
[i32 | i64].eq
[i32 | i64].ne
[i32 | i64].lt_s
[i32 | i64].lt_u
[i32 | i64].le_s
[i32 | i64].le_u
[i32 | i64].gt_s
[i32 | i64].gt_u
[i32 | i64].ge_s
[i32 | i64].ge_u
Les conversions de type
[i32 | i64].const [i32 | i64]
[f32 | f64].const [f32 | f64]
[i32].wrap/[i64]
[i64].extend_s/[i32]
[i64].extend_u/[i32]
[i32 | i64].trunc_s/[f32 | f64]
[i32 | i64].trunc_u/[f32 | f64]
[f32].demote/[f64]
[f64].promote/[f32]
[f32 | f64].convert_s/[i32 | i64]
[f32 | f64].convert_u/[i32 | i64]
[i32 | i64 | f32 | f64].reinterpret/[f32 | f64 | i32 | i64]
L'accès aux variables locales ou globales
get_local
set_local
tee_local
get_global
set_global
L'écriture et la lecture en mémoire
[i32 | i64 | f32 | f64].load
[i32 | i64 | f32 | f64].store
[i32 | i64].load8_s
[i32 | i64].load16_s
[i64].load32_s
[i32 | i64].load8_u
[i32 | i64].load16_u
[i64].load32_u
[i32 | i64].store8
[i32 | i64].store16
[i64].store32
Les manipulation de la mémoire
memory.grow
memory.size
Programmer en Wasm, c'est programmer des fonctions pour une machine à pile : une instruction effectue une opération en dépilant une ou plusieurs valeurs de la pile correspondant à ses opérandes - qu'il faut donc avoir empilées avant -, puis en empilant une ou plusieurs valeurs formant le résultat. Par exemple (les commentaires sont en anglais, car l'outil en ligne wat2wasm génère une erreur s'ils contiennent des caractères accentués) :
i32.const 100		;;Push 100
i32.const 10		;;Push 10
i32.sub			;;Pop 10 and 100, compute 100 - 10 and push the result
Noter qu'un élément peut être simplement expulsé de la pile avec l'instruction drop :
i32.const 100		;;Push 100
drop			;;Pop 100 and loose it
La notation à base de S-expressions permet d'écrire ce code sous une forme beaucoup plus compacte, et surtout moins tordue pour qui n'a pas l'habitude de travailler avec une machine à pile :
(i32.sub (i32.const 100) (i32.const 10))
Sans trop rentrer dans les détails à ce stade, précisons que les instructions figurent dans le corps de fonctions. Une fonction peut prendre des paramètres en entrée, retourner une valeur, et utiliser des variables locales. Depuis une fonction, il est possible d'appeler une fonction, éventuellement elle-même :
(func (param i32 i32) (result i32)
	get_local 0	;;Push parameter 0 (the first)
	get_local 1	;;Push parameter 1 (the second)
	i32.sub		;;Pop parameters 1 and 0, compute (0) - (1) and push the result
)
(func (result i32)
	i32.const 100	;;Push first parameter
	i32.const 10	;;Push second parameter
	call 0			;;Call function 0 and return 100 - 10 = 90
)
Comme il est possible de le constater, il est fait référence aux fonctions et à leurs arguments par des indices correspondant à leur position dans l'ordre des définitions. Fort heureusement, il est possible d'utiliser des noms :
(func $sub (param $a i32) (param $b i32) (result i32)
	get_local $a
	get_local $b
	i32.sub
)
(func $callsub (result i32)
	i32.const 100
	i32.const 10
	call $sub		;;Call $sub and return 100 - 10 = 90
)
Et tout cela peut être écrit de manière plus compacte grâce aux S-expressions :
(func $sub (param $a i32) (param $b i32) (result i32)
	(i32.sub (get_local $a) (get_local $b))		
)
(func $callsub (result i32)
	(call $sub (i32.const 100) (i32.const 10))
)
La programmation d'une boucle ne peut que rappeler des souvenirs à ceux qui ont programmé en assembleur. Ainsi, une boucle se déroulant $steps fois pour ajouter chaque fois une valeur $inc à une valeur $value, les trois valeurs étant fournies lors de l'appel, et retourner le résultat. Tant qu'on y est, illustrons la manière dont le code doit être packagé dans un module, depuis lequel il est possible d'exporter une fonction pour qu'elle soit appelée dans l'environnement d'exécution, donc par du code JavaScript :
(module
	(func $loop (param $value i32) (param $inc i32) (param $steps i32) (result i32)
		(block
			(loop
				;;Push $value
				get_local $value
				;;Push $inc
				get_local $inc
				;;Pop $inc and $value, compute $inc + $value and push the result
				i32.add
				;;Pop the result and store it in $value
				set_local $value
				;;Push $steps
				get_local $steps
				;;Push 1
				i32.const 1
				;;Pop 1 and $steps, compute $steps - 1 and push the result
				i32.sub
				;;Pop the result and store it $steps
				set_local $steps
				;;Push $steps
				get_local $steps
				;;Pop $steps and compare it to zero
				i32.eqz
				;;If result is zero, jump to label at depth 1 (ie: exit the loop)
				br_if 1
				;;Else jump to to label at depth 0 (ie: start of the loop)
				br 0
			)
		)
		;;Push $result as a return value
		get_local $value
	)
	(export "loop" (func $loop))
)
Une instruction de contrôle telle que br ou br_if fait référence à du code à atteindre à l'aide d'un libellé qui correspond à la profondeur de ce code dans l'imbrication (nesting depth), exprimée relativement à l'instruction en partant de 0. Ce référencement relatif implique qu'il n'est possible d'effectuer un branchement qu'en remontant dans les imbrications.
Le code de la boucle peut donc être écrit plus simplement, ici encore à l'aide de S-expressions et de noms :
(block $someBlock
	(loop $someLoop
		(set_local $value (i32.add (get_local $value) (get_local $inc)))
		(set_local $steps (i32.sub (get_local $steps) (i32.const 1)))
		(br_if $someBlock (i32.eqz (get_local $steps)))
		br $someLoop
	)
)

Packager le code Wasm dans un module

Le code des fonctions est packagé dans un module dont la description peut être rédigée dans un format texte, dit WAT. Cette description devra être compilée pour produire un binaire utilisable. Noter que de manière assez déconcertante, ce qui est appelé "format texte" semble recouvrir à la fois le langage utilisée pour coder les fonctions et le langage utilisé pour décrire un module.
Comme pour le langage lui-même, la spécification est très difficile à déchiffrer, étant rédigée dans un langage cryptique. Le mieux est encore de s'en remettre au guide de MDN, ici.
Un module est composé de différentes sections, dont certaines peuvent se répéter : types, fonctions, tables, mémoires, variables globales, éléments de tables, données, fonction d'initialisation, importations, exportations.
Le contenu d'un module se décrit à l'aide de S-expressions (expressions symboliques). Il s'agit d'une syntaxe permettant de décrire un contenu structuré, comme XML le permet, si ce n'est que la notation à base de parenthèses permet de faire l'économie de bien du texte. Dans le cas d'un module, on peut grossièrement décrire la syntaxe ainsi (c'est typiquement le genre d'information synthétique bien utile, qui ne se rencontre pourtant pas dans les documentations et les guides... mais dont on trouve tout de même une version plus exhaustive en cherchant bien ici) :
*Présent 0 à N fois
+Présent 1 à N fois
?Présent 0 ou 1 fois
typei32 | i64 | f32 | f64
nameUne chaîne quelconque
instructionsUne ou plusieurs instructions constituant le code de la fonction
valueUne déclaration de valeur comme "i32.const 7"
mutLe mot-clé "mut"
indexUn indice sous la forme d'une déclaration de constante (ex: i32.const 123)
textUne chaîne de caractères ASCII avec d'éventuelles séquences d'échappement
(module
	(memory [$name]? (import "[name]" "[name]")? [# pages])?
	(memory [$name]? (export "[name]")? [# pages] [# max pages]?)?
	(data (<value>) "[text]")
	(global [$name]? (export "[name]")? ([mut] [type]) ([value]))*
	(global [$name]? (import "[name]" "[name]") ([mut] [type]))*
	(table [$name]? (export "[name]")? [# elements] anyfunc)?
	(table [$name]? (import "[name]" "[name]") [# elements] anyfunc)?
	(type [$name]? (func (param [type])* (result [type])*)?
	(elem [index] [$nom]+)?
	(func [$name]?
		(import "[name]" "[name]")?
		(param [$name]? [type])*	;;Syntaxe compacte pour une séquence de variables non nommées : (param [type]+)
		(result [$name]? [type])?
	)*
	(func [$name]?
		(export "[name]")?
		(param [$name]? [type])*	;;Syntaxe compacte pour une séquence de variables non nommées : (param [type]+)
		(result [$name]? [type])?
		(local [$name]? [type])*	;;Syntaxe compacte pour une séquence de variables non nommées : (local [type]+)
		[instructions]
	)*
	(export "[name]" (func [$name | indice]))*
	(export "[name]" (global [$name | indice]))*
	(export "[name]" (table [$name | indice]))*
	(export "[name]" (memory [$name | indice]))*
)
A tout endroit, il est possible d'introduire un commentaire sur une ou plusieurs lignes :
;;A line of comments
(;
A line of comments
A line of comments
;)
Noter que certaines sections sont incompatibles car il n'existe qu'une entité qu'elles concernent. Par exemple, impossible de prétendre importer un table et en déclarer une autre, car il ne peut à date en exister qu'une.
Le module le plus élémentaire ne contient rien :
(module)

Initialiser un module

Au démarrage d'un module, il est possible de provoquer l'appel à une fonction qui sera référencée dans la section start. Dans cet exemple, ce sera pour modifier la valeur d'une variable globale :
(module
	(global $someglobal (mut i32) (i32.const 0))
	(func $setup
		i32.const 666
		set_global $someglobal
	)
	(start $setup)
)

Appeler une fonction indirectement via un indice d'une table

En Wasm, une table est une tableau de pointeurs d'éléments, à date limités à un seul type : le pointeur de fonction. Une table permet donc d'appeler une fonction identifiée par un indice dont la valeur est déterminée lors de l'exécution : l'appel est dynamique et non statique.
Pour utiliser une table, il faut la déclarer en précisant son nombre d'éléments et le type des éléments qu'elle contient. Par exemple une table de deux éléments du seul type autorisé, myfunc :
(table 2 anyfunc)
La table peut alors être peuplée d'éléments qui sont des références à des fonctions dont les signatures peuvent être quelconques. Une section elem permet de stocker un élément à un indice donné de la table :
(func $sub (param i32 i32) (result i32)
	(i32.sub (get_local 0) (get_local 1))
)
(func $square (param i32) (result i32)
	(i32.mul (get_local 0) (get_local 0))
)
(elem (i32.const 0) $sub)
(elem (i32.const 1) $square)
Noter que la section peut être écrite de manière plus compacte pour stocker consécutivement plusiers éléments à partir d'un certain indice :
(elem (i32.const 0) $sub $square)
Pour pouvoir appeler une fonction par son indice dans la table, il faut définir un type qui correspond à sa signature - si plusieurs fonctions ont la même signature, il suffira de définir une seule fois ce type. En effet, call_indirect opère un contrôle entre la signature de l'appel et celle de la fonction appelée :
(type (func (param i32 i32) (result i32)))
(type (func (param i32) (result i32)))
Comme toujours, le code peut être rendu plus lisible en nommant les types, qu'il n'est autrement possible de référencer que par leur indice :
(type $subtype (func (param i32 i32) (result i32)))
(type $squaretype (func (param i32) (result i32)))
Enfin, dans le contexte d'une fonction quelconque, il est possible d'appeler une fonction en précisant son indice et le type de sa signature, et bien évidemment en fournissant ses paramètres :
i32.const 100
i32.const 10
i32.const 0
call_indirect (type $subtype)
Soit sous forme plus compacte :
(call_indirect (type $subtype) (i32.const 100) (i32.const 10) (i32.const 0))
Bref :
(module
	(table 2 anyfunc)
	(func $sub (param i32 i32) (result i32)
		(i32.sub (get_local 0) (get_local 1))
	)
	(func $square (param i32) (result i32)
		(i32.mul (get_local 0) (get_local 0))
	)
	(elem (i32.const 0) $sub $square)
	(type $subtype (func (param i32 i32) (result i32)))
	(type $squaretype (func (param i32) (result i32)))
	(func $suborsquare (result i32)
		(call_indirect (type $subtype) (i32.const 100) (i32.const 10) (i32.const 0))
	)
)
Noter qu'il est possible de nommer une table, mais à date il n'est de toute manière possible que d'en définir une (je n'ai pas trouvé la syntaxe permettant de préciser la table à utiliser dans call_indirect ; il semble que la référence à la table 0 soit codée en dur) :
(table $sometable 2 anyfunc)
;;...
(elem $sometable (i32.const 0) $sub)

Générer le binaire d'un module

Le contenu au format texte stocké dans un fichier .wat doit être mis au format binaire à l'aide de l'outil wat2wasm qui se trouve ici.
L'archive contient le source C++ des outils, mais aussi le résultat d'une transpilation en JavaScript effectuée à l'aide de emscripten. Dans ces conditions, il est possible de se dispenser de compiler le source C++, et de se contenter d'utiliser la version JavaScript à laquelle une démo donne accès via une interface basique mais suffisante. En fait, l'outil peut tout simplement être utilisé via la démo en ligne proposée ici.
wat2wasm, un outil pour compiler et exécuter du Wasm
Après avoir saisi le code et vérifier qu'il est valide, cliquer sur le bouton Download pour télécharger sa version binaire.

Utiliser un module dans du code JavaScript

Cliquez ici pour récupérer des exemples (les fichiers HTML peuvent être directement chargés dans un navigateur).
Dans l'environnement d'exécution d'un navigateur, il est possible d'appeler du code WebAssembly depuis du code JavaScript, et l'inverse est aussi possible. Pour cela, il faut s'appuyer sur l'API WebAssembly.
WebAssembly
.instantiate ()
.instantiateStreaming ()
.compile ()
.compileStreaming ()
.validate ()
.Global ()
.Module ()
.Instance ()
.Memory ()
.CompileError ()
.LinkError ()
.RuntimeError ()
Global
.value
.valueOf ()
Instance
.exports
Memory
.buffer
.grow ()
Module
.customSections ()
.exports ()
.imports ()
Table
.length
.get ()
.grow ()
.set ()
CompileError, LinkError, RuntimeError
.message
.name
.fileName
.lineNumber
.columnNumber
.stack
.toSource ()
.toString ()
A la base, le binaire résultant de la transformation du WAT doit être chargé, puis validé via validate () et compilé via compilate (). On dispose alors du module, et donc d'un point d'accès aux fonctions qu'il exporte.
Les solutions possibles pour charger le binaire sont bien présentées ici. Ce sera l'API Fetch ou XMLHttpRequest.
Par exemple, avec XMLHttpRequest, et en décomposant en de nombreuses étapes :
function compileNOK (e) {
	console.log (`${e.name} error at (${e.lineNumber}, ${e.columnNumber}): ${e.message}`);
}

function compileOK (module) {
	var instance = new WebAssembly.Instance (module);
	console.log (instance.exports.addNumbers (1, 2));
}

function requestHandler () {
	if (request.readyState != 4)
		return;
	if (request.status != 200) {
		console.log (`HTTP error code ${request.status}: ${request.statusText}`);
		return;
	}
	if (!WebAssembly.validate (request.response)) {
		console.log ("Validation error");
		return;
	}
	WebAssembly.compile (request.response).then (compileOK, compileNOK);
}

var request;

request = new XMLHttpRequest ();
request.responseType = "arraybuffer";
request.onreadystatechange = requestHandler;
request.open ("GET", "someWasm.wasm");
request.send ();
Noter qu'à date, le type MIME application/wasm ne fonctionne pas : il faut spécifier le type de la réponse attendu, c'est-à-dire arraybuffer. D'ailleurs, c'est sans doute pour cela qu'il est impossible de faire vraiment plus court en utilisant instantatiateStreaming (), quoique l'usage de cette méthode soit conseillée. Ainsi, le code suivant ne fonctionne pas (Response has unsupported MIME type) :
WebAssembly.instantiateStreaming (fetch ("someWasm.wasm")).then (r => console.log (r.instance.exports.addNumbers (1, 2)), e => console.log (e.message));
Bref, la solution la plus concise à date consiste à utiliser l'API Fetch et instantiate () :
fetch ("someWasm.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab).then (
			ro => { console.log (ro.instance.exports.addNumbers (1, 2)) }, e => { console.log (e.message) }
)));

Importer et exporter une fonction

Cliquez ici pour récupérer des exemples (les fichiers HTML peuvent être directement chargés dans un navigateur).
Soit un module contenant une seule fonction :
(module
	(func (param $a i32) (param $b i32) (result i32)
		get_local $a		;;Stack $a
		get_local $b		;;Stack $b
		i32.add				;;Compute the sum and stack the result
	)
)
Cette fonction peut être exportée pour être appelée depuis l'environnement d'exécution, donc depuis du code JavaScript exécuté dans le contexte d'une page Web. A cette occasion, il faut lui donner un nom sous lequel il sera possible de l'appeler dans l'environnement d'exécution :
(module
	(func $add (param $a i32) (param $b i32) (result i32)
		get_local $a	;;Stack $a
		get_local $b	;;Stack $b
		i32.add			;;Compute the sum and stack the result
	)
	(export "addNumbers" (func $add))
)
Noter que l'exportation peut être mentionnée plus tôt, ce qui réduit la syntaxe :
(module
	(func $add (export "addNumbers") (param $a i32) (param $b i32) (result i32)
		get_local $a	;;Stack $a
		get_local $b	;;Stack $b
		i32.add			;;Compute the sum and stack the result
	)		
)
La fonction exportée peut être appelée une fois que le module est accessible :
fetch ("importExportFunction.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab, importObject).then (
			ro => {
				console.log (`The sum of 1 and 2 is: ${ro.instance.exports.addNumbers (1, 2)}`)
			},
			e => { console.log (e.message) }
)));
Quid de l'import ? Tout comme le module déclare des exports, il peut déclarer des imports. Le code JavaScript doit alimenter le module avec ce qui correspond aux imports.
Par exemple, ce module importe une fonction qui doit être désignée par randomInt dans les imports auxquels le code JavaScript doit procéder. Dans le code Wasm, cette fonction est désignée par $rndGet. Elle prend deux entiers en entrée et retourne un entier :
(module
	(func $rndGet (import "someImports" "randomInt") (param i32 i32) (result i32))
	(func $rndAdd (result i32)
		i32.const 0
		i32.const 100
		call $rndGet
		i32.const 10
		i32.const 1000
		call $rndGet
		i32.add
	)
	(export "addRandomNumbers" (func $rndAdd))
)
Somme toute il s'agit simplement de rajouter (import ...) dans la signature de la fonction, et de ne pas spécifier de corps.
Commme il est possible de le constater, les imports peuvent être structurés sur deux niveaux : des regroupements d'importations (ici identifié par someImports), et les importations elles-mêmes (ici identifié par randomInt).
Dans le code JavaScript, il faut définir un objet qui contient une propriété identifiée par someImports, qui doit contenir la fonction rand :
var importObject = {
	someImports: {
		randomInt: function (min, max) {
			var n = min + Math.floor ((max - min + 1) * Math.random ());
			console.log (`Random number between ${min} and ${max}: ${n}`);
			return (n);
		}
	}
};
Cet objet doit être fourni à instantiate () :
fetch ("importExportFunction.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab, importObject).then (
			ro => {
				console.log (`Sum of those numbers is: ${ro.instance.exports.addRandomNumbers ()}`)
			},
			e => { console.log (e.message) }
)));

Importer et exporter une mémoire

Cliquez ici pour récupérer des exemples (les fichiers HTML peuvent être directement chargés dans un navigateur).
Les imports et les exports ne se limitent pas aux fonctions. Il est possible d'importer / exporter une mémoire.
Une mémoire correspond à un espace continu en mémoire mesuré en pages (un page correspond à 64 Ko) et qui peut croître jusqu'à un certain nombre de pages (optionel).
Le contenu d'une mémoire peut être initialisé à l'aide de données spécifiées dans une section data. Les données doivent être fournies sous la forme d'une chaîne de caractères qui seront traduits en leurs codes ASCII, copiés à partir de l'offset indiqué en octets :
(memory 1)
(data (i32.const 10) "012ABC")		#Copy bytes 0x48, 0x49, 0x50, 0x65, 0x66, 0x67 starting from byte 10
Quelques séquences d'échappement permettent de mentionner des caractères spéciaux (\t, \n, \r, \*, \', \", \\) et aussi de donner directement la valeur hexadécimale d'un octet :
(memory 1)
(data (i32.const 10) "\F1\03\4A")		#Copy bytes 0xF1, 0x03 and 0x4 starting from byte 10
Les instructions Wasm pour lire et écrire dans la mémoire sont load et store. Attention, car elles se fondent sur des offsets indiqués en octets indépendamment de la taille de la valeur lue ou stockée. Par exemple :
i32.const 40		;;index * size of a value = 10 * 4 = 40
i32.load
Ce qu'il possible d'écrire ainsi grâce aux S-expressions :
(i32.load (i32.const 40))
Vu du code JavaScript, une mémoire est un objet WebAssembly.Memory :
var someWasMemory = new WebAssembly.Memory ({initial:1, maximum:3});
Le code JavaScript peut manipuler la mémoire sous forme d'un tableau pour l'initialiser avec des données, comme par exemple remplir une page d'entiers de 32 bits :
var i, buffer;
buffer = new Uint32Array (importObject.someImports.someMemory.buffer);
for (i = 0; i != 65536 / 4; i ++)
	buffer[i] = i;
Le code Wasm pourra accéder à la mémoire via un objet figurant dans les imports structurés fourni à instantiate (), comme on le fait pour tous les autres imports :
var someImportObject = {
	someImports: {
		someMemory: new WebAssembly.Memory ({initial:1, maximum:3})
	}
};
Dans le code Wasm, la mémoire doit être importée, comme on le fait pour tous les autres imports, en précisant son nombre de pages :
(module
	(memory (import "someImports" "someMemory") 1)
	;;...
)
Noter qu'à date, une seule mémoire est autorisée dans un programme Wasm . Il est donc impossible de mentionner l'existence d'une autre mémoire, comme par exemple :
(module
	(memory (import "someImports" "someMemory") 1)
	(memory 1)		;;Does not work: one memory only at this time
	;;...
)
Attention à l'indexation de la mémoire dans le code Wasm. Si le code JS a stocké 666 dans la 4ème entrée de la mémoire manipulée comme un bloc d'entiers non signés de 32 bits (Uint32Array)... :
buffer[10] = 42;
...le code Wasm pour lire cette valeur et la pousser sur la pile sera une instruction load accédant dans la mémoire à l'offset 10 * 4 = 40 octets :
(i32.load (i32.const 40))
Enfin, l'exportation d'un mémoire ne présente aucune difficulté. Par exemple, une mémoire d'une page dont les 10 premiers éléments (des entiers sur 32 bits) sont les nombres de 666 à 676 :
(module
	(memory 1)
	(func $setupmemory
		(local $count i32)
		(local $offset i32)
		(local $number i32)
		(set_local $count (i32.const 10))
		(set_local $number (i32.const 666))
		(set_local $offset (i32.const 0))
		(block
			(loop
				(i32.store (get_local $offset) (get_local $number))
				(set_local $offset (i32.add (get_local $offset) (i32.const 4)))
				(set_local $count (i32.sub (get_local $count) (i32.const 1)))
				(br_if 1 (i32.eqz (get_local $count)))
				(set_local $number (i32.add (get_local $number) (i32.const 1)))
				br 0
			)
		)
	)
	(start $setupmemory)
	(export "numbers" (memory 0))
)
La mémoire exportée peut être utilisée une fois que le module est accessible (noter que Uint32Array n'est qu'un des tableaux possibles, cet objet étant à déterminer en fonction des données qu'il sagit de lire dans la mémoire) :
fetch ("exportMemory.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab, {}).then (
			ro => {
				numbers = new Uint32Array (instance.exports.numbers.buffer);
				for (i = 0; i != 10; i ++)
					console.log(`[${i}]: {numbers[i]}`);
			},
			e => { console.log (e.message) }
)));

Importer et exporter une variable globale

Cliquez ici pour récupérer des exemples (les fichiers HTML peuvent être directement chargés dans un navigateur).
Une variable globale correspond à un objet WebAssembly.Global (la mention d'un spécificateur de mutabilité est optionnelle) :
var someJSGlobal = new WebAssembly.Global ({value: "i32", mutable: true}, 666)
Comme pour tout le reste (fonction, mémoire, table), une variable globale déclarée dans le code JavaScript peut être importée dans le code Wasm. Pour cela, elle doit ici encore figurer dans l'objet des imports :
var someImportObject = {
	someImports: {
		someJSGlobal: new WebAssembly.Global ({value: "i32", mutable: true}, 666)
	}
};
Dans le code Wasm, la variable globale doit être importée avant de pouvoir être utilisée :
(module
	(global $jsglobal (import "someImports" "someJSGlobal") (mut i32))
	;;...
)
A l'inverse, une variable globale déclarée dans le code Wasm peut être exportée vers le code JavaScript. Pour cela, elle doit ici encore figurer dans la liste des exports :
(module
	(global $wasmglobal (mut i32) (i32.const 7))
	(export "someWasmGlobal" (global $wasmglobal))
)
Elle est alors accessible via les exports de l'instance :
console.log (instance.exports.someWasmGlobal.value);

Exporter et importer une table (de pointeurs de fonction)

Cliquez ici pour récupérer des exemples (les fichiers HTML peuvent être directement chargés dans un navigateur).
Un table correspond à l'objet WebAssembly.Table (le seul type d'élément possible est anyfunc, et la mention à un nombre d'éléments maximum est optionnelle) :
var someImportObject = {
	someImports: {
		someJSTable: new WebAssembly.Table ({initial: 1, element:"anyfunc", maximum: 1})
	}
};
Dans le code Wasm, la table doit être importée pour être utilisée :
(table (import "someImports" "someJSTable") 1 anyfunc)
La table ne constitue pas un moyen rapide pour importer des fonctions du code JavaScript dans le code Wasm. Par exemple, impossible d'écrire quelque chose comme :
	someImportObject.someImports.someJSTable.set (0, function (a, b) { return (a -b); });
En effet, il n'est possible de la peupler qu'avec des références à des fonctions exportées depuis du code Wasm. Par exemple, le module suivant exporte deux fonctions $sub et $callindirect :
(module
	(table (import "someImports" "someJSTable") 1 anyfunc)
	(type $subtype (func (param i32 i32) (result i32)))
	(func $sub (param i32 i32) (result i32)
		(i32.sub (get_local 0) (get_local 1))
	)
	(func $callindirect (result i32)
		(call_indirect (type $subtype) (i32.const 100) (i32.const 10) (i32.const 0))
	)
	(export "callIndirect" (func $callindirect))
	(export "sub" (func $sub))
)
La seule chose qu'il est possible de faire dans le code JavaScript, c'est donc d'ajouter la fonction exportée $sub dans la table, une fois que le module est donc accessible :
fetch ("importTable.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab, someImportObject).then (
			ro => {
				someImportObject.someImports.someJSTable.set (0, ro.instance.exports.sub);
				console.log (`Indirect call to function $sub (10, 100) at index 0 returns: ${ro.instance.exports.callIndirect (10, 100)}`);
			},
			e => { console.log (e.message) }
)));
A l'inverse, il est possible d'exporter une table du code Wasm vers le code JavaScript. Comme à date il n'est possible que de définir une table, on peut simplement la désigner par son indice 0 :
(module
	(table 1 anyfunc)
	(type $subtype (func (param i32 i32) (result i32)))
	(func $sub (param i32 i32) (result i32)
		(i32.sub (get_local 0) (get_local 1))
	)
	(func $callindirect (result i32)
		(call_indirect (type $subtype) (i32.const 100) (i32.const 10) (i32.const 0))
	)
	(export "callIndirect" (func $callindirect))
	(export "sub" (func $sub))
	(export "table" (table 0))
)
Et ici encore, la table ne peut être peuplée que de références à des fonctions Wasm, donc une fois que le module est accessible :
fetch ("exportTable.wasm").then (
	r => r.arrayBuffer ().then (
		ab => WebAssembly.instantiate (ab, {}).then (
			ro => {
				ro.instance.exports.table.set (0, ro.instance.exports.sub);
				console.log (`Indirect call to function $sub (10, 100) at index 0 returns: ${ro.instance.exports.callIndirect (10, 100)}`);
			},
			e => { console.log (e.message) }
)));

WebAssembly en vaut-il la peine ?

L'histoire montre que les développeurs ont toutes les raisons de résister avant de se laisser séduire par les sirènes du marketing en matière de technologies Web. Consacrer un peu de temps à WebAssembly en vaut-il la peine ? Pour répondre à cette question, il faut considérer plusieurs choses.
L'adoption de WebAssembly. C'est un peu le serpent qui se mord la queue, dira-t-on : si personnne ne se met à WebAssembly parce que personne ne s'y est mis, l'avenir de WebAssembly s'annonce sombre. Dans les faits, comme il est possible de le constater ici sur l'excellent site Can I Use?, un module Wasm peut s'exécuter dans le contexte des principaux navigateurs du marché. C'est rassurant, mais cela ne suffit pas à statuer sur l'intérêt de s'investir pour un développeur. Sachant que cette adoption est somme toute récente - à lire ici le propos de Mozilla, le tournant remonterait à fin 2017 -, qui a produit quoi de notable en WebAssemnly à ce jour ? Ici, force est de constater qu'une recherche sur Google à base des mots-clés "webassembly demo" ne remonte pas grand-chose...
L'intégration dans l'environnement d'exécution. Rien de plus promis à une fin prématurée qu'une technologie destinée aux navigateurs Web qui ne s'articule pas parfaitement avec les technologies déjà disponibles, en premier lieu le code JavaScript. Enfin, qui aurait du temps à perdre à écrire de la tuyauterie pour appeler une fonction ou échanger une donnée ? Sur ce point, WebAssembly apparaît comme une technologie intéressante, car comme il a été possible de le constater, il est parfaitement possible de faire communiquer du code Wasm et du code JavaScript tant pour échanger des données que pour appeler des sous-programmes. De plus, il est très simple de le faire.
La performance. On l'a vu, la promesse de WebAssembly, c'est celle de faire tourner un programme beaucoup plus rapidement dans une page Web, non seulement parce qu'il faudra moins de temps pour télécharger ce programme, mais aussi parce qu'étant déjà assemblé, il n'aura pas besoin d'être déchiffré avant d'être compilé en code natif - pour plus de détails, voir ici. C'est bien le minimum qu'on peut attendre de la bascule du texte vers le binaire. Dans de nombreux articles, il est fait mention au fait que le code WebAssembly s'exécuterait seulement 20% moins vite que du code natif. Pour autant, cela ne semble pas signifier qu'il faille envisager de jeter JavaScript pour passer à WebAssembly. D'abord, ce n'est pas l'idée. Ensuite, il semble s'avérer que tout n'est pas toujours plus rapide en Wasm qu'en JS - à commencer par la manipulation du DOM. En fait, comme le suggère ce type de test, mieux vaut réserver l'usage de WebAssembly à des sous-programmes auquel il s'agirait de fournir un paquet de données, et qui en retourneraient un après des calculs intensifs.
La sécurité. Chaque fois qu'une technologie est introduite dans un navigateur Web, on peut craindre que ce soit le loup qui rentre dans la bergerie, à plus forte raison si c'est une technologie bas-niveau. En matière de sécurité, WebAssembly semble offrir certaines garanties. La documentation officielle explique ici qu'un programme Wasm s'exécute dans un bac à sable, et par ailleurs de manière déterministe - chacun est averti des failles de l'exécution spéculative des CPU depuis l'apparition de Meltdown et autre Spectre. Bref, l'usage de la mémoire et le flot de l'exécution sont sous haute surveillance - quelques mises à l'épreuve amusantes ici. Certes, on se doute bien que cela n'empêchera pas des petits malins de trouver la faille. Toutefois, ce qui compte, c'est que la technologie bénéficie d'un support efficace qui garantisse une bonne capacité d'anticipation et de réaction aux attaques.
Enfin, comme cela a été évoqué au début de cet article, il faut bien se rappeler que l'idée de base de WebAssembly n'est pas tant d'écrire du code Wasm à assembler en binaire, que de compiler directement du code C, C++ ou autre en binaire. Or ce n'est pas ce qui a été exploré ici, puisque l'idée était de s'amuser avec un langage rigolo en ceci qu'il consiste à programmer une machine à pile.

Pour en savoir plus...

Les documents suivants ont été particulièrement utiles pour rédiger le présent article :
De même que les articles suivants, tout particulièrement pour clarifier des syntaxes :
WebAssembly : programmer en assembleur dans une page Web