Some time ago I've first heard about Web Assembly and that it will be the "next big thing" in web application development. Then, once Go 1.11 was released with build support for it included in the language itself, I knew I need to play around with it. Although it took me quite a while, I finally did, so I decided to write down things I found out about it, alongside with a few basic examples.

What is WASM?

In short, Web Assembly (or WASM) is a format that allows building web applications (or parts of the applications) in high-level languages like C, C++, Rust, or Go. On the official website, Web Assembly is described as:

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.

One of the most important statements on that website is not the definition itself, but something that the web development community was wondering about, is that WASM is not trying to push JavaScript out of the web apps. This is important because at first there was an impression of an upcoming war between JS and WASM, but they decided to cooperate (at least for now).

Building and loading modules

In order to build any non-trivial website, you need at least two things: HTML and JavaScript. Since WASM is obviously non-trivial, we need to start with that as well. Fortunately, those two elements are provided in Go sources, so all you need to do is copy them to your project:

cp "$(GOROOT)/misc/wasm/wasm_exec.js" wasm_exec.js
cp "$(GOROOT)/misc/wasm/wasm_exec.html" index.html

If you open your HTML file now, you can see the most important (WASM-wise) part:

WebAssembly.instantiateStreaming(fetch("test.wasm"), go.importObject).then((result) => {
    inst = result.instance;
    ....
});

...
await go.run(inst);
...

If this looks scary to you, don't worry. All it does is binds clicking the button defined in an HTML template with running the module defined in test.wasm file. If you want the simplified version, all you need to do is type:

WebAssembly.instantiateStreaming(
    fetch("test.wasm"), 
    go.importObject).then((result) => {
        go.run(result.instance);
    });

What does this mean exactly? In order to run WASM module in the browser you need to do three things: fetch the file with module definition into ArrayBuffer in JS, compile it into WebAssembly.Module and instantiate it. Once you do this in your script, whether after clicking the button or on page load.

The last important thing here is how to convert Go source file into a WASM module. This is actually very simple, as Go 1.11 supports new "operating system" called JS and architecture called WASM. So you need to set the env variables as you would when compiling eg. on Mac trying to get exe file for Windows:

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

This results in test.wasm file that should be served by your HTTP server then fetched and ran by your web app.

"Hello world"

The simplest possible example is, obviously, as the Go source code is as simple as you probably assume:

// wasm.go
...
func main() {
    fmt.Println("Hello GoWroc!")
}
...

After building the module and running the example, this exact message shows up in the browser's console.

Callbacks

Since we know how to make simple stuff like logging, we might want to make JS and WASM communicate with each other, right? To do that, we need to define so-called callbacks, which need to be bound with some variable name in JavaScript, so that they can be somehow accessed:

// wasm.go
func main() {
    registerCallbacks()
    c := make(chan struct{})
    <-c
}

func registerCallbacks() {
    js.Global().Set("hello", js.NewCallback(sayHello))
}

func sayHello(i []js.Value) {
    shout := fmt.Sprintf("Hello, %s! Welcome to the GoWroc talk!", i[0])
    js.Global().Get("console").Call("log", shout)
}

You can see that we define a function sayHello that prints some parametrized message in the console. Then this function is bound to document.hello(..) in JS and you can really access it this way. One thing that can be hard to grasp is that blocking channel in main function which prevents our app from finishing. This gets clearer once we check the definition of hello in the browser's console (printed by Opera):

> hello
ƒ () {
    pendingCallbacks.push({ id: id, args: arguments });
    go._resolveCallbackPromise();
}

What is this exactly? It means that we didn't transpile our Go to WASM to JS, but we will in fact call that something defined in the module, and it is available only when the module is. So if we delete that blocking channel and try to call the function, we get an error:

hello("MyCodeSmells")
wasm_exec.js:378 Uncaught Error: bad callback: Go program has already exited
    at global.Go._resolveCallbackPromise (wasm_exec.js:378)
    at wasm_exec.js:394
    at <anonymous>:1:1

Making HTTP requests

One last thing from the basics that I wanted to play around was making HTTP requests from what feels like a backend but is really ran in the frontend app. We start by building the Go code just as we would when we make such requests from "regular" Go apps:

// wasm.go
func ping(url) {
    resp, err := http.Get(url)
    if err != nil {
        fmt.Printf("Failed to ping '%s': %v", url, err)
        return
    }
    defer resp.Body.Close()

    bb, err := ioutil.ReadAll(resp.Body)
    if err != nil {
        fmt.Printf("Failed to read response from '%s': %v", url, err)
        return
    }
    fmt.Printf("Response from '%s':", url)
    fmt.Println(string(bb))
}

We could obviously hardcode the address we want to shoot at, but that wouldn't make our example special at all. To make it at least a bit useful, we need to alter the HTML template a bit and add an input for the address:

// index.html
<input id="urlInput" type="text">
<button onClick="run();" id="runButton" disabled>Ping</button>

Then we need to use the value from the input in Go code as well:

// wasm.go
urlInput := js.Global().Get("document").Call("getElementById", "urlInput")
url := urlInput.Get("value").String()

ping(url)

Now here's how we can learn another important thing about WASM, is that the code ran within it has the same limitations as everything else that runs in the browser. So if you call, eg. https://google.com you get an error:

Access to fetch at 'https://www.google.com/' from origin 'http://localhost:8080' has been blocked by CORS policy: No 'Access-Control-Allow-Origin' header is present on the requested resource. If an opaque response serves your needs, set the request's mode to 'no-cors' to fetch the resource with CORS disabled.

This is because Google rejects any in-browser requests from non-Google domains. But if you happen to have a server that does allow such requests, you get an actual response:

Response from 'https://httpbin.org/anything':{
    "args": {}, 
    "data": "", 
    "files": {}, 
    "form": {}, 
    "headers": {
        "Accept": "*/*", 
        "Accept-Encoding": "gzip, deflate, br", 
        "Accept-Language": "en-GB,en-US;q=0.9,en;q=0.8", 
        "Connection": "close", 
        "Host": "httpbin.org", 
        "Origin": "http://localhost:8080", 
        "Referer": "http://localhost:8080/", 
        "User-Agent": "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_1) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/70.0.3538.102 Safari/537.36 OPR/57.0.3098.106"
    }, 
    "json": null, 
    "method": "GET", 
    "origin": "213.5.44.8", 
    "url": "https://httpbin.org/anything"
}

Threads

What I've always loved about Go was how easy it is to write code that runs concurrently. I can, for example, create a loop in which I create N goroutines, make each of them to print a number and sleep a second, only to see that the print happens instantly.

// Loop:
for i := 0; i < 10; i++ {
    go func(n int) {
        fmt.Printf("#%d\n", n)
    }(i)
}

// Output:
#0
#7
#1
#8
#4
#2
#5
#9
#3
#6

This means that all of them run at the same time because otherwise, we would have to wait for a second between prints.

But wait, is it supported in WASM? After all, JavaScript has just a single thread as it relies on so-called event loop to jump between code that should be run at the same time. Turns out that it is kinda supported, so if we call the same code in WASM module, the browser console shows:

#9
#0
#1
#2
#3
#4
#5
#6
#7
#8

Again, this shows up immediately, as the code is run by JS workers, which run in parallel alongside the main, single thread. What is strange, is that whenever I run this code (even after recompilation), the order of printed indexes stays the same! It's always "the last one (N), then the rest from 0 to N-1"! To be honest I didn't find out why this behaves the way it does.

Summary

Looking at the existing examples of applications built with Web Assembly, I have to admit that it looks impressive and powerful, so there is obviously a future here. Since the use cases are most often connected to advanced graphics or audio editing, I don't see it as an option for me, unless I get really deep in those. For a regular, plain business application/website, WASM seems to be too complicated, especially since I have some experience in JavaScript in my resumé.

Links