📜 ⬆️ ⬇️

Thanks to WebAssembly, you can write Frontend on Go

Original article.

In February 2017, a member of the go team Brad Fitzpatrick offered to make support for WebAssembly in the language. Four months later, in November 2017, the author GopherJS Richard Muziol began to implement the idea. And, finally, the full implementation was in the master. Developers will receive wasm around August 2018, with version 1.11 go . As a result, the standard library takes on almost all the technical difficulties with importing and exporting functions familiar to you, if you have already tried compiling C in wasm. It sounds promising. Let's see what can be done with the first version.



All examples in this article can be run from docker containers that are in the author’s repository :

docker container run -dP nlepage/golang_wasm:examples # Find out which host port is used docker container ls 

Then go to localhost : 32XXX /, and go from one link to another.
')

Hi, wasm!


Creating a basic “hello world” and concepts is already fairly well documented (even in Russian ), so let's just move on to more subtle things more quickly.

The most necessary is a freshly compiled version of Go that supports wasm. I will not describe the installation step by step , just know what is needed already in the master.

If you don’t want to worry about it, Dockerfile c go is available in the golub-wasm repository on github , or even faster you can take an image from nlepage / golang_wasm .

Now you can write the traditional helloworld.go and compile it with the following command:

 GOOS=js GOARCH=wasm go build -o test.wasm helioworld.go 

The environment variables GOOS and GOARCH are already set in the nlepage / golang_wasm image, so you can use a Dockerfile file like this to compile:

 FROM nlepage/golang_wasm COPY helloworld.go /go/src/hello/ RUN go build -o test.wasm hello 

The final step is to use the wasm_exec.html and wasm_exec.js files available in the go repository in the misc/wasm or in the docker nlepage / golang_wasm image in the /usr/local/go/misc/wasm/ to run test.wasm in browser (wasm_exec.js expects binary test.wasm , so we use this name).
You just need to give 3 static files using nginx, for example, then wasm_exec.html will display the “run” button (it will test.wasm on only if test.wasm loaded correctly).

It is noteworthy that test.wasm must be serviced with the MIME type application/wasm , otherwise the browser will refuse to execute it. (for example, nginx needs an updated mime.types file ).

You can use the nginx image from nlepage / golang_wasm , which already includes the fixed MIME type, wasm_exec.html and wasm_exec.js in the code> / usr / share / nginx / html / directory.

Now click the “run” button, then open the console of your browser, and you will see the console.log greeting (“Hello Wasm!”).


A full example is available here .

Call JS from Go


Now that we have successfully launched the first WebAssembly binary file compiled from Go, let's take a closer look at the possibilities provided.

The new syscall / js package is included in the standard library, consider the main file - js.go
A new js.Value type is js.Value , which represents the JavaScript value.

It offers a simple API for managing JavaScript variables:


And additional interesting methods:


Instead of displaying the message in os.StdOut, let's display it in the alert window using window.alert() .

Since we are in the browser, the global scope is a window, so you first need to get alert () from the global scope:

 alert := js.Global().Get("alert") 

Now we have an alert variable, in the form of js.Value , which is a reference to window.alert JS, and you can use the function call via js.Value.Invoke() :

 alert.Invoke("Hello wasm!") 

As you can see, there is no need to call js.ValueOf () before passing Invoke arguments, it accepts an arbitrary number of interface{} and passes values ​​through ValueOf itself.

Now our new program should look like this:

 package main import ( "syscall/js" ) func main() { alert := js.Global().Get("alert") alert.Invoke("Hello Wasm!") } 

As in the first example, you just need to create a file named test.wasm , and leave wasm_exec.html and wasm_exec.js as it was.
Now, when we press the “Run” button, an alert window appears with our message.

A working example is in the examples/js-call folder.

Call Go from JS.


Calling JS from Go is quite simple, let's take a closer look at the syscall/js package, the second file to view is callback.go .


Let's try to do something simple: run Go fmt.Println() from the side of JS.

We'll make some changes to wasm_exec.html to be able to get a callback from Go to call it.

 async function run() { console.clear(); await go.run(inst); inst = await WebAssembly.instantiate(mod, go.ImportObject); //   } 

This launches the wasm binary and waits for it to complete, then reinitializes it for the next run.

Let's add a new function that will receive and save the Go callback and change the Promise state to completion:

 let printMessage // Our reference to the Go callback let printMessageReceived // Our promise let resolvePrintMessageReceived // Our promise resolver function setPrintMessage(callback) { printMessage = callback resolvePrintMessageReceived() } 

Now let's adapt the run() function to use the callback:

 async function run() { console.clear() // Create the Promise and store its resolve function printMessageReceived = new Promise(resolve => { resolvePrintMessageReceived = resolve }) const run = go.run(inst) // Start the wasm binary await printMessageReceived // Wait for the callback reception printMessage('Hello Wasm!') // Invoke the callback await run // Wait for the binary to terminate inst = await WebAssembly.instantiate(mod, go.importObject) // reset instance } 

And this is on the JS side!

Now in the Go part you need to create a callback, send it to the JS side and wait for the function to be needed.

  var done = make(chan struct{}) 

Then we need to write the real printMessage() function:

 func printMessage(args []js.Value) { message := args[0].Strlng() fmt.Println(message) done <- struct{}{} // Notify printMessage has been called } 

The arguments are passed through the []js.Value , so you need to call js.Value.String() in the first slice element to get the message in the Go string.
Now we can wrap this function in a callback:

 callback := js.NewCallback(printMessage) defer callback.Release() // to defer the callback releasing is a good practice 

Then call the JS setPrintMessage() function, just like when you call window.alert() :

 setPrintMessage := js.Global.Get("setPrintMessage") setPrintMessage.Invoke(callback) 

The last thing to do is wait for the callback call in main:

 <-done 

This last part is important because the callbacks are executed in a dedicated goroutine, and the main goroutine must wait for a callback, otherwise the wasm binary will be stopped prematurely.

The resulting Go program should look like this:

 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{}{} } 

As in the previous examples, create a file named test.wasm . You also need to replace wasm_exec.html with our version, and wasm_exec.js can be reused.

Now, when you press the “run” button, as in our first example, the message is printed in the browser console, but this time it is much better! (And harder.)

The working example in the docker file bidet is available in the examples/go-call folder.

Long job


The Go from JS call is a bit more cumbersome than the Go JS call, especially on the JS side.

This is mainly due to the fact that you need to wait for the result of the Go callback to be sent to the JS side.

Let's try something else: why not organize a wasm binary file that will not be completed immediately after a callback call, but will continue to work and receive other calls.
This time let's start from Go, and like in our previous example, we need to create a callback and send it to the JS side.

Add a call counter to track how many times the function has been called.

Our new printMessage() function will print the received message and the value of the counter:

 var no int func printMessage(args []js.Value) { message := args[0].String() no++ fmt.Printf("Message no %d: %s\n", no, message) } 

Creating a callback and sending it to the JS side is the same as in the previous example:

 callback := js.NewCallback(printMessage) defer callback.Release() setPrintMessage := js.Global().Get("setPrintMessage") setPrIntMessage.Invoke(callback) 

But this time we have no done channel to notify us of the termination of the main gorutin. One way can be to permanently block the main goroutin with an empty select{} :

 select{} 

This is not satisfactory, our binary wasm will just hang in memory until the browser tab closes.

You can listen to the beforeunload event on the page, you will need a second callback to receive the event and notify the main gorutina via the channel:

 var beforeUnloadCh = make(chan struct{}) 

This time, the new beforeUnload() function will only accept an event, in the form of a single js.Value argument:

 func beforeUnload(event js.Value) { beforeUnloadCh <- struct{}{} } 

Then we wrap it up in a callback using js.NewEventCallback() and register it on the JS side:

 beforeUnloadCb := js.NewEventCallback(0, beforeUnload) defer beforeUnloadCb.Release() addEventLtstener := js.Global().Get("addEventListener") addEventListener.Invoke("beforeunload", beforeUnloadCb) 

Finally, replace the empty blocking select for reading from the beforeUnloadCh channel:

 <-beforeUnloadCh fmt.Prtntln("Bye Wasm!") 

The final program looks like this:

 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{}{} } 

Previously, on the JS side, loading the wasm binary file looked like this:

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

Let's adapt it to run a binary file immediately after downloading:

 (async function() { const go = new Go() const { instance } = await WebAssembly.instantiateStreaming( fetch("test.wasm"), go.importObject ) go.run(instance) })() 

And replace the “Run” button with the message field and the button to call printMessage() :

 <input id="messageInput" type="text" value="Hello Wasm!"> <button onClick="printMessage(document.querySelector('#messagelnput').value);" id="prtntMessageButton" disabled> Print message </button> 

Finally, the setPrintMessage() function, which accepts and stores a callback, should be simpler:

 let printMessage; function setPrintMessage(callback) { printMessage = callback; document.querySelector('#printMessageButton').disabled = false; } 

Now, when we press the “Print message” button, we should see a message of our choice and a call counter printed in the browser console.
If we check the box “Preserve log” of the browser console and refresh the page, we will see the message “Bye Wasm!”.



Sources are available in the examples/long-running folder on github.

So what is next?


As you can see, the studied syscall/js API does its job and allows you to write complex things with a small amount of code. You can write to the author , if you know the easier way.
At the moment, it is not possible to return a value to JS directly from the Go callback.
It should be borne in mind that all callbacks are executed in the same goroutin, so if you do some blocking operations in the callback, do not forget to create a new goroutin, otherwise you will block all other callbacks.
All the basic functions of the language are already available, including concurrency. For now, all goroutins will work in the same thread, but this will change in the future .
In our examples, we used only the fmt package from the standard library, but everything is available that is not trying to escape from the sandbox.

It seems that the file system is supported through Node.js.

Finally, how about performance? It would be interesting to run some tests to see how Go wasm compares with equivalent pure JS code. Someone hajimehoshi made measurements, how different environments work with integers, but the technique is not very clear.



Do not forget that Go 1.11 has not even been officially released yet. In my opinion, very good for experimental technology. Those who are interested in performance tests may torment their browser .
The main niche, as the author notes, is the transfer of already existing go code from the server to the client. But with new standards, you can do completely offline applications , and the wasm code is saved in compiled form. It is possible to transfer many utilities to web, agree, it is convenient?

Source: https://habr.com/ru/post/417563/


All Articles