Je vous propose dans ce billet une petite introduction à WebAssembly via deux exemples utilisant Go pour le générer.
Qu’est ce que WebAssembly ?
En gros WebAssembly est un format binaire exécutable dans un navigateur (entre autre).
Intérêts:
- Performance: la finalité est de se rapprocher des performances proches de celles d’une application native. Du coup WebAssembly va être intéressant pour toutes les taches gourmandes, comme la cryptographie, le traitement d’images, les jeux…
- Latence: le navigateur va compiler le bytecode WebAssembly pendant son téléchargement. Autrement dir il n’a pas besoin d’avoir reçu tout le code pour commencer à l’utiliser.
- Taille: à fonctionnalité équivalente, une app WebAssembly va être plus petite que son équivalent Javascript. Et qui dit plus petite dit plus rapide à transférer. Malheureusement, on va voir qu’avec Go c’est loin d’être la cas.
- Ce n’est pas du Javascript: aujourd’hui Javascript est incontournable si on veut faire du développement frontend, WebAssembly léve cette barrière en permettant d’utiliser d’autres langages, comme du C/C++, du Rust ou comme ici du [Go]‘https://golang.org/). A noter qu’il faudra toujours un bout de Javascript pour faire le lien entre le WebAssembly et le navigateur.
Hello WebAssembly
Depuis la version 1.11 Go à un support expérimental de WebAssembly. Si vous voulez tester les exemples de ce billet assurez vous d’avoir au moins cette version. Le code des deux exemples sont disponibles ici: https://github.com/toorop/webassembly-golang-tuto.
Pour le premier exemple on va faire le classique Hello World en affichant Hello WebAssembly dans la console du navigateur.
Notre code Go:
package main
import (
"fmt"
)
func main() {
fmt.Println("Hello WebAssembly")
}
Comme vous pouvez le constater, c’est du code tout à fait standard. On remarquera juste que la sortie est “bindée” sur la console du navigateur. je ne suis pas certain que ce soit le cas par défaut si on utilise d’autres langages pour produire du wasm.
La subtilité va avoir lieu lors de la compilation puisqu’il faudra spécifier une architecture et un OS un peu particuliers:
GOOS=js GOARCH=wasm go build -o main.wasm
Suite à cette compilation on va donc avoir notre bytecode WebAssembly dans le fichier main.wasm.
Ceux qui vont tester par eux mêmes vont vite voir un gros problème (c’est le cas de le dire):
-> Le main.wasm fait plus de 2MB !!! 😱
Ça s’explique par le fait que Go va embarquer et compiler toutes les dépendances nécessaires. Si ce n’est pas un gros problème pour un usage classique, dans le cadre de WebAssembly c’est assez rédhibitoire puisque le navigateur devra télacharger le code. Espérons que des optimisations seront faites de ce coté. A titre de comparaison le même hello world compilé depuis du Rust fait moins de 30KB.
Pour exécuter notre .wasm on va bien entendu avoir besoin d’un fichier HMTL:
<html>
<head>
<meta charset="utf-8">
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
})
</script>
</head>
<body></body>
</html>
Et du fameux code Javascript faisant lien. Il est inclus dans le code Go, pour le récupérer il vous suffit de le copier depuis GOROOT/misc/wasm/wasm_exec.js
Il ne nous reste plus qu’a lancer un petit serveur web:
goexec 'http.ListenAndServe(":8080", http.FileServer(http.Dir(".")))'
et de charger la page depuis notre navigateur pour voir dans notre console un beau:
Hello WebAssembly
Magique !
Générer des identifiants uniques
Ce second exemple va nous permettre d’interagir un peu avec le DOM. On va créer une fonction qui va générer un identifiant unique (UUID) quand on va cliquer sur un bouton.
On va commencer par le HTML qui est le même que dans l’exemple précédent avec l’ajout d’un bouton et d’un input:
<html>
<head>
<meta charset="utf-8">
<script src="wasm_exec.js"></script>
<script>
const go = new Go();
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
go.run(result.instance);
})
</script>
</head>
<body>
<input id="myInput" value="" size="32" />
<button id="myButton">UUID</button>
</body>
</html>
Le code Go est plus intéressant:
package main
import (
"fmt"
"syscall/js"
"github.com/gofrs/uuid"
)
func main() {
c := make(chan struct{}, 0)
var cb js.Callback
cb = js.NewCallback(func(args []js.Value) {
var output string
id, err := uuid.NewV4()
if err != nil {
output = fmt.Sprintf("ERR: %v", err)
} else {
output = id.String()
}
js.Global().Get("document").Call("getElementById", "myInput").Set("value", output)
})
js.Global().Get("document").Call("getElementById", "myButton").Call("addEventListener", "click", cb)
<-c
}
On notera en premier lieu l’utilisation d’un channel pour que l’app reste active.
L’élément vraiment important est l’usage du package js qui va nous permettre d’interagir un peu plus finement avec le navigateur et le DOM.
Concrètement ça va nous permettre de mettre un gestionnaire d’événement “click” sur le bouton et de le lier à notre fonction de callbask cb.
A chaque fois que l’utilisateur clique sur le bouton, un nouvel UUID va être généré attribué au value de l’input :
Conclusion
WebAssembly est à mon avis une techno trés interessante de part les pavantages évoqués en début d’article (même si à l’heure actuelle ce n’est pas encore ça). Par contre, force est de constater que pour le moment Go n’est clairement pas le langage à utiliser pour produire du WebAssembly.
De mon coté je pense continuer à expérimenter WebAssembly, j’ai quelques développement en cours, notamment PeerPx, qui pourraient en tirer profit en faisant quelques infidélités à Go pour Rust qui il faut bien el reconnaitre à un support bien plus avancé de WebAssembly.
Si ce billet vous a plu pensez à le partager ⤵️