Article d'
origine .
En février 2017, un membre de l'équipe go Brad Fitzpatrick a
proposé de prendre en charge WebAssembly dans la langue. Quatre mois plus tard, en novembre 2017, l'auteur de
GopherJS , Richard Muziol, a commencé à mettre en œuvre l'idée. Et enfin, l'implémentation complète a été trouvée dans master. Les développeurs recevront du wasm vers août 2018, avec la version
go 1.11 . En conséquence, la bibliothèque standard prend en charge presque toutes les difficultés techniques liées à l'importation et à l'exportation de fonctions qui vous sont familières si vous avez déjà essayé de compiler C dans wasm. Cela semble prometteur. Voyons ce qui peut être fait avec la première version.
Tous les exemples de cet article peuvent être lancés à partir de conteneurs Docker qui se trouvent
dans le référentiel de l'auteur :
docker container run -dP nlepage/golang_wasm:examples
Ensuite, accédez à
localhost : 32XXX / et passez d'un lien à un autre.
Salut Wasm!
La création du «bonjour monde» de base et le concept sont déjà assez
bien documentés (même
en russe ), alors passons aux choses plus subtiles.
Le plus essentiel est une version fraîchement compilée de Go qui prend en charge le wasm. Je ne
décrirai pas
étape par étape l'installation , sachez simplement que ce qui est nécessaire est déjà en master.
Si vous ne voulez pas vous en soucier,
Dockerfile c go est disponible dans
le référentiel golub-wasm sur github , ou encore plus rapidement vous pouvez prendre une image depuis
nlepage / golang_wasm .
Vous pouvez maintenant écrire le
helloworld.go
traditionnel et le compiler avec la commande suivante:
GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go
Les variables d'environnement GOOS et GOARCH sont déjà définies dans l'image
nlepage / golang_wasm , vous pouvez donc utiliser un fichier
Dockerfile
comme celui-ci pour compiler:
FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello
La dernière étape consiste à utiliser les fichiers
wasm_exec.html
et
wasm_exec.js
disponibles dans le référentiel go dans le répertoire
misc/wasm
ou dans l'image docker
nlepage / golang_wasm dans le
/usr/local/go/misc/wasm/
pour exécuter
test.wasm
dans navigateur (wasm_exec.js attend le fichier binaire
test.wasm
, nous utilisons donc ce nom).
Vous avez juste besoin de donner 3 fichiers statiques en utilisant nginx, par exemple, puis wasm_exec.html affichera le bouton «exécuter» (il ne
test.wasm
que si
test.wasm
chargé correctement).
Il est à noter que
test.wasm
doit être servi avec l'
application/wasm
type MIME, sinon le navigateur refusera de l'exécuter. (par exemple, nginx a besoin d'un
fichier mime.types mis à jour ).
Vous pouvez utiliser l'image nginx de
nlepage / golang_wasm , qui inclut déjà le type MIME fixe,
wasm_exec.html
et
wasm_exec.js
dans le code> / usr / share / nginx / html / directory.
Maintenant, cliquez sur le bouton "Exécuter", puis ouvrez la console de votre navigateur et vous verrez le message d'accueil console.log ("Bonjour Wasm!").
Un exemple complet est disponible
ici .
Appelez JS depuis Go
Maintenant que nous avons lancé avec succès le premier binaire WebAssembly compilé à partir de Go, examinons de plus près les fonctionnalités fournies.
Le nouveau package syscall / js a été ajouté à la bibliothèque standard. Considérez le fichier principal,
js.go
Un nouveau type
js.Value
est
js.Value
qui représente une valeur JavaScript.
Il propose une API simple pour gérer les variables JavaScript:
js.Value.Get()
et js.Value.Set()
renvoient et définissent les valeurs de champ de l'objet.js.Value.Index()
et js.Value.SetIndex()
accèdent à l'objet par lecture et écriture d'index.js.Value.Call()
appelle la méthode objet en tant que fonction.js.Value.Invoke()
appelle l'objet lui-même en tant que fonction.js.Value.New()
appelle le nouvel opérateur et utilise ses propres connaissances en tant que constructeur.- Quelques méthodes supplémentaires pour obtenir la valeur JavaScript dans le type Go correspondant, par exemple
js.Value.Int()
ou js.Value.Bool()
.
Et d'autres méthodes intéressantes:
js.Undefined()
donnera à js.Value l' undefined
correspondant.js.Null()
donnera à js.Value
null
correspondante.js.Global()
renverra js.Value
donnant accès à la portée globale.js.ValueOf()
accepte les types Go primitifs et renvoie le bon js.Value
Au lieu d'afficher le message dans os.StdOut, affichons-le dans la fenêtre de notification à l'aide de
window.alert()
.
Puisque nous sommes dans le navigateur, la portée globale est une fenêtre, donc vous devez d'abord obtenir alert () de la portée globale:
alert := js.Global().Get("alert")
Nous avons maintenant une variable d'
alert
, sous la forme de
js.Value
, qui est une référence à
window.alert
JS, et vous pouvez utiliser la fonction pour appeler via
js.Value.Invoke()
:
alert.Invoke("Hello wasm!")
Comme vous pouvez le voir, il n'est pas nécessaire d'appeler js.ValueOf () avant de passer les arguments à Invoke, cela prend une quantité arbitraire d'
interface{}
et passe les valeurs via ValueOf lui-même.
Maintenant, notre nouveau programme devrait ressembler à ceci:
package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") }
Comme dans le premier exemple, il vous suffit de créer un fichier appelé
test.wasm
et de laisser
wasm_exec.html
et
wasm_exec.js
tels
wasm_exec.js
.
Maintenant, lorsque nous cliquons sur le bouton "Exécuter", une fenêtre d'alerte apparaît avec notre message.
Un exemple de travail se trouve dans le dossier
examples/js-call
.
Appelez Go de JS.
Appeler JS à partir de Go est assez simple, regardons de plus près le
syscall/js
, le deuxième fichier à visualiser est
callback.go
.
js.Callback
encapsuleur js.Callback
pour la fonction Go, à utiliser dans JS.js.NewCallback()
fonction qui prend une fonction (accepte une tranche de js.Value
et ne renvoie rien), et retourne js.Callback
.- Quelques mécanismes pour gérer les rappels actifs et
js.Callback.Release()
, qui doivent être appelés pour détruire le rappel. js.NewEventCallback()
similaire à js.NewCallback()
, mais la fonction js.NewCallback()
n'accepte qu'un seul argument - un événement.
Essayons de faire quelque chose de simple: exécutez Go
fmt.Println()
du côté JS.
Nous allons apporter quelques modifications à
wasm_exec.html
pour pouvoir obtenir un rappel de Go pour l'appeler.
async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject);
Cela lance le binaire wasm et attend qu'il se termine, puis le réinitialise pour la prochaine exécution.
Ajoutons une nouvelle fonction qui recevra et enregistrera le rappel Go et changer l'état de
Promise
à la fin:
let printMessage
Maintenant adaptons la
run()
pour utiliser le rappel:
async function run() { console.clear()
Et c'est du côté de JS!
Maintenant, dans la partie Go, vous devez créer un rappel, l'envoyer du côté JS et attendre que la fonction soit nécessaire.
var done = make(chan struct{})
Ensuite, ils devraient écrire la vraie fonction
printMessage()
:
func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{}
Les arguments sont transmis via la tranche
[]js.Value
, vous devez donc appeler
js.Value.String()
sur le premier élément slice pour obtenir le message dans la ligne Go.
Maintenant, nous pouvons encapsuler cette fonction dans un rappel:
callback := js.NewCallback(printMessage) defer callback.Release()
Appelez ensuite la fonction JS
setPrintMessage()
, tout comme l'appel de
window.alert()
:
setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback)
La dernière chose à faire est d'attendre que le rappel soit appelé en principal:
<-done
Cette dernière partie est importante car les rappels sont exécutés dans un goroutine dédié, et le goroutine principal doit attendre que le rappel soit appelé, sinon le binaire wasm sera arrêté prématurément.
Le programme Go résultant devrait ressembler à ceci:
package main import ( "fmt" "syscall/js" ) var done = make(chan struct{}) func main() { callback := js.NewCallback(prtntMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) <-done } func printMessage(args []js.Value) { message := args[0].Strlng() fmt.PrintIn(message) done <- struct{}{} }
Comme dans les exemples précédents, créez un fichier appelé
test.wasm
. Nous devons également remplacer
wasm_exec.html
par notre version, et
wasm_exec.js
pouvons réutiliser
wasm_exec.js
.
Maintenant, lorsque vous appuyez sur le bouton "Exécuter", comme dans notre premier exemple, le message est imprimé dans la console du navigateur, mais cette fois, c'est beaucoup mieux! (Et plus dur.)
Un exemple de travail dans une offre de fichier Docker est disponible dans le dossier
examples/go-call
.
Travail long
Appeler Go de JS est un peu plus lourd que d'appeler JS de Go, surtout du côté JS.
Cela est principalement dû au fait que vous devez attendre que le résultat du rappel Go soit transmis au côté JS.
Essayons autre chose: pourquoi ne pas organiser le binaire wasm, qui ne se terminera pas juste après l'appel de rappel, mais continuera à fonctionner et à accepter d'autres appels.
Cette fois, commençons par le côté Go, et comme dans notre exemple précédent, nous devons créer un rappel et l'envoyer du côté JS.
Ajoutez un compteur d'appels pour suivre combien de fois la fonction a été appelée.
Notre nouvelle fonction
printMessage()
imprime le message reçu et la valeur du compteur:
var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) }
La création d'un rappel et son envoi du côté JS est la même que dans l'exemple précédent:
callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback)
Mais cette fois, nous n'avons pas
done
canal pour nous informer de la fin du goroutine principal. Une façon pourrait être de verrouiller en permanence le goroutin principal avec une
select{}
vide
select{}
:
select{}
Ce n'est pas satisfaisant, notre wasm binaire va juste se bloquer en mémoire jusqu'à ce que l'onglet du navigateur soit fermé.
Vous pouvez écouter l'événement
beforeunload
sur la page, vous aurez besoin d'un deuxième rappel pour recevoir l'événement et notifier le goroutine principal via le canal:
var beforeUnloadCh = make(chan struct{})
Cette fois, la nouvelle fonction
beforeUnload()
n'acceptera l'événement que comme un seul argument
js.Value
:
func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Ensuite, enveloppez-le dans un rappel en utilisant
js.NewEventCallback()
et enregistrez-le du côté JS:
beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb)
Enfin, remplacez la
select
blocage vide par la lecture du canal
beforeUnloadCh
:
<-beforeUnloadCh fmt.Prtntln("Bye Wasm!")
Le programme final ressemble à ceci:
package main import ( "fmt" "syscall/js" ) var ( no int beforeUnloadCh = make(chan struct{}) ) func main() { callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") } func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Prtntf("Message no %d: %s\n", no, message) } func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} }
Auparavant, du côté JS, le téléchargement du binaire wasm ressemblait à ceci:
const go = new Go() let mod, inst WebAssembly .instantiateStreaming(fetch("test.wasm"), go.importObject) .then((result) => { mod = result.module inst = result.Instance document.getElementById("runButton").disabled = false })
Adaptons-le pour exécuter le binaire immédiatement après le chargement:
(async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })()
Et remplacez le bouton «Exécuter» par un champ de message et un bouton pour appeler
printMessage()
:
<input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button>
Enfin, la fonction
setPrintMessage()
, qui accepte et stocke le rappel, devrait être plus simple:
let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; }
Maintenant, lorsque nous cliquons sur le bouton "Imprimer le message", vous devriez voir un message de notre choix et un compteur d'appels imprimés dans la console du navigateur.
Si nous cochons la case Conserver le journal de la console du navigateur et actualisons la page, nous verrons le message «Bye Wasm!».
Les sources sont disponibles dans le dossier
examples/long-running
sur github.
Et alors?
Comme vous pouvez le voir, l'API
syscall/js
apprise fait son travail et vous permet d'écrire des choses complexes avec un peu de code. Vous pouvez écrire à l'
auteur si vous connaissez une méthode plus simple.
Il n'est actuellement pas possible de renvoyer une valeur à JS directement à partir du rappel Go.
Gardez à l'esprit que tous les rappels sont effectués dans le même goroutin, donc si vous effectuez des opérations de blocage dans le rappel, n'oubliez pas de créer un nouveau goroutin, sinon vous bloquerez l'exécution de tous les autres rappels.
Toutes les fonctionnalités linguistiques de base sont déjà disponibles, y compris la concurrence. Pour l'instant, tous les goroutins fonctionneront dans un seul thread, mais cela
changera à l'avenir .
Dans nos exemples, nous avons utilisé uniquement le package fmt de la bibliothèque standard, mais tout est disponible qui n'essaie pas de s'échapper du bac à sable.
Le système de fichiers semble être pris en charge via Node.js.
Enfin, qu'en est-il des performances? Il serait intéressant d'exécuter des tests pour voir comment Go Wasm se compare au code JS pur équivalent. Quelqu'un
hajimehoshi a
mesuré comment différents environnements fonctionnent avec des nombres entiers, mais la technique n'est pas très claire.
N'oubliez pas que Go 1.11 n'a pas encore été officiellement publié. À mon avis, c'est très bon pour la technologie expérimentale. Ceux qui sont intéressés par les tests de performances
peuvent tourmenter leur navigateur .
La niche principale, comme le note l'auteur, est le transfert du serveur au client du code go existant. Mais avec de nouvelles normes, vous pouvez créer
des applications complètement hors ligne et le code wasm est enregistré sous forme compilée. Vous pouvez transférer de nombreux utilitaires sur le Web, d'accord, facilement?