Tianhe Gao

Simple tutorial about WASM

WASM(webassembly) is a virtual assembly language for the browser. It is closer to the Hardware than JavaScript.

We'll create a application that is used to format JSON.

Before:

1{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}, {"title":"maps", "url":"/maps/"}, {"title": "goroutines","url":"/goroutines/"}]}

After:

 1{
 2  "website": "golangbot.com",
 3  "tutorials": [
 4    {
 5      "title": "Strings",
 6      "url": "/strings/"
 7    },
 8    {
 9      "title": "maps",
10      "url": "/maps/"
11    },
12    {
13      "title": "goroutines",
14      "url": "/goroutines/"
15    }
16  ]
17}

HelloWorld WASM Program Cross Compiled from Go

Here is the file structure:

1Documents/
2└── webassembly
3    ├── assets
4    └── cmd
5        ├── server
6        └── wasm
1mkdir -p ~/Documents/webassembly/assets \
2~/Documents/webassembly/cmd  \
3~/Documents/webassembly/cmd/server \
4~/Documents/webassembly/cmd/wasm

First create a go module named github.com/golangbot/webassembly.

1cd ~/Documents/webassembly
2go mod init github.com/golangbot/webassembly

go mod init github.com/golangbot/webassembly will create a file named go.mod:

1module github.com/golangbot/webassembly
2
3go 1.22.0

Create main.go with the following contents inside ~/Documents/webassembly/cmd/wasm:

1package main
2
3import (
4    "fmt"
5)
6
7func main() {
8    fmt.Println("Go Web Assembly")
9}

Let's cross compile the above Go program into WebAssembly.

1cd ~/Documents/webassembly/cmd/wasm/
2GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm

Only cross compiling the main package to WebAssembly.

1./assets/json.wasm
2# bash: ./assets/json.wasm: cannot execute binary file: Exec format error

Why we got this error?

Because the binary is a wasm related binary and is supposed to be run inside a browser sandbox. The Linux OSes don't understand the format of this binary.

JS Glue

We need some JS glue code to run json.wasm.

1cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" ~/Documents/webassembly/assets/

index.html

 1<!doctype html>
 2<html>
 3    <head>
 4        <meta charset="utf-8"/>
 5        <script src="wasm_exec.js"></script>
 6        <script>
 7            const go = new Go();
 8            WebAssembly.instantiateStreaming(fetch("json.wasm"), go.importObject).then((result) => {
 9                go.run(result.instance);
10            });
11        </script>
12    </head>
13    <body></body>
14</html>

Now see the file structure:

 1Documents/
 2└──webassembly/
 3        ├── assets
 4        │   ├── index.html
 5        │   ├── json.wasm
 6        │   └── wasm_exec.js
 7        ├── cmd
 8        │   ├── server
 9        │   └── wasm
10        │       └── main.go
11        └── go.mod

WebServer

Create main.go inside the server directory. The directory structure after creating main.go is provided below.

 1Documents/
 2└── webassembly
 3        ├── assets
 4        │   ├── index.html
 5        │   ├── json.wasm
 6        │   └── wasm_exec.js
 7        ├── cmd
 8        │   ├── server
 9        │   │   └── main.go
10        │   └── wasm
11        │       └── main.go
12        └── go.mod

Copy the following code to ~/Documents/webassembly/cmd/server/main.go.

 1package main
 2
 3import (
 4    "fmt"
 5    "net/http"
 6)
 7
 8func main() {
 9    err := http.ListenAndServe(":9090", http.FileServer(http.Dir("../../assets")))
10    if err != nil {
11        fmt.Println("Failed to start server", err)
12        return
13    }
14}

The above program creates a file server listening at port 9090 with the root at our assets folder. Let's run the server and see our first WebAssembly program running.

1cd ~/Documents/webassembly/cmd/server/
2go run main.go

Now we can see the output(Go Web Assembly) in the http://localhost:9090/ page's console.

Coding the JSON formatter

Add the following function to ~/Documents/webassembly/cmd/wasm/main.go.

 1func prettyJson(input string) (string, error) {
 2    var raw any
 3    if err := json.Unmarshal([]byte(input), &raw); err != nil {
 4        return "", err
 5    }
 6    pretty, err := json.MarshalIndent(raw, "", "  ")
 7    if err != nil {
 8        return "", err
 9    }
10    return string(pretty), nil
11}

The MarshalIndent function takes 3 parameters as input. The first one is the raw unformatted JSON, the second one is the prefix to add to each new line of the JSON. In this case, we don't add a prefix. The third parameter is the string to be appended for each indent of our JSON.

Exposing a function from Go to Javascript

Now we have the function ready but we are yet to expose this function to Javascript so that it can be called from the front end.

Go provides the syscall/js package which helps in exposing functions from Go to Javascript.

The first step in exposing a function from Go to JavaScript is to create a Func type. Func is a wrapped Go function that can be called by JavaScript. The FuncOf function can be used to create a Func type.

Add the following function to ~/Documents/webassembly/cmd/wasm/main.go.

 1func jsonWrapper() js.Func {
 2        jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
 3                if len(args) != 1 {
 4                        return "Invalid no of arguments passed"
 5                }
 6                inputJSON := args[0].String()
 7                fmt.Printf("input %s\n", inputJSON)
 8                pretty, err := prettyJson(inputJSON)
 9                if err != nil {
10                        fmt.Printf("unable to convert to json %s\n", err)
11                        return err.Error()
12                }
13                return pretty
14        })
15        return jsonFunc
16}

Here is the completed code.

 1package main
 2
 3import (
 4    "fmt"
 5    "encoding/json"
 6    "syscall/js"
 7)
 8
 9func prettyJson(input string) (string, error) {
10        var raw any
11        if err := json.Unmarshal([]byte(input), &raw); err != nil {
12                return "", err
13        }
14        pretty, err := json.MarshalIndent(raw, "", "  ")
15        if err != nil {
16                return "", err
17        }
18        return string(pretty), nil
19}
20
21func jsonWrapper() js.Func {
22        jsonFunc := js.FuncOf(func(this js.Value, args []js.Value) any {
23                if len(args) != 1 {
24                        return "Invalid no of arguments passed"
25                }
26                inputJSON := args[0].String()
27                fmt.Printf("input %s\n", inputJSON)
28                pretty, err := prettyJson(inputJSON)
29                if err != nil {
30                        fmt.Printf("unable to convert to json %s\n", err)
31                        return err.Error()
32                }
33                return pretty
34        })
35        return jsonFunc
36}
37
38func main() {
39    fmt.Println("Go Web Assembly")
40    js.Global().Set("formatJSON", jsonWrapper())
41    <-make(chan struct{})
42}

Compile and test the program.

1cd ~/Documents/webassembly/cmd/wasm/
2GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm
3cd ~/Documents/webassembly/cmd/server/
4go run main.go

Calling the Go function from JavaScript

Open the devtools, select console tab, input:

1formatJSON('{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}]}')

Output:

1'{
2 "tutorials": [
3  {
4   "title": "Strings",
5   "url": "/strings/"
6  }
7 ],
8 "website": "golangbot.com"
9}'

Creating the UI and calling the wasm function

Let's modify the existing ~/Documents/webassembly/assets/index.html in the assets folder to include the UI.

 1<!doctype html>
 2<html>  
 3    <head>
 4        <meta charset="utf-8"/>
 5        <script src="wasm_exec.js"></script>
 6        <script>
 7            const go = new Go();
 8            WebAssembly.instantiateStreaming(fetch("json.wasm"), go.importObject).then((result) => {
 9                go.run(result.instance);
10            });
11        </script>
12    </head>
13    <body>
14         <textarea id="jsoninput" name="jsoninput" cols="80" rows="20"></textarea>
15         <input id="button" type="submit" name="button" value="pretty json" onclick="json(jsoninput.value)"/>
16         <textarea id="jsonoutput" name="jsonoutput" cols="80" rows="20"></textarea>
17    </body>
18    <script>
19        var json = function(input) {
20            jsonoutput.value = formatJSON(input)
21        }
22     </script>
23</html>

Compile and run this program.

1cd ~/Documents/webassembly/cmd/wasm/
2GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm
3cd ~/Documents/webassembly/cmd/server/
4go run main.go

Input:

1{"website":"golangbot.com", "tutorials": [{"title": "Strings", "url":"/strings/"}, {"title":"maps", "url":"/maps/"}]}

Output:

 1{
 2  "tutorials": [
 3    {
 4      "title": "Strings",
 5      "url": "/strings/"
 6    },
 7    {
 8      "title": "maps",
 9      "url": "/maps/"
10    }
11  ],
12  "website": "golangbot.com"
13}

Accessing the DOM from Go using JavaScript

In the above section, we called the wasm function, got the formatted JSON string output, and set the output text area with the formatted JSON using JavaScript.

There is one more way to achieve the same output. Instead of passing the formatted JSON string to javascript, it is possible to access the browser's DOM from Go and set the formatted JSON string to the output text area.

Let's see how this is done.

We need to modify the jsonWrapper function in ~/Documents/webassembly/cmd/wasm/main.go to achieve this.

 1func jsonWrapper() js.Func {
 2    jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) any {
 3        if len(args) != 1 {
 4            return "Invalid no of arguments passed"
 5        }
 6        jsDoc := js.Global().Get("document")
 7        if !jsDoc.Truthy() {
 8            return "Unable to get document object"
 9        }
10        jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
11        if !jsonOuputTextArea.Truthy() {
12            return "Unable to get output text area"
13        }
14        inputJSON := args[0].String()
15        fmt.Printf("input %s\n", inputJSON)
16        pretty, err := prettyJson(inputJSON)
17        if err != nil {
18            errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
19            return errStr
20        }
21        jsonOuputTextArea.Set("value", pretty)
22        return nil
23    })
24
25    return jsonfunc
26}

Run this program again.

1cd ~/Documents/webassembly/cmd/wasm/  
2GOOS=js GOARCH=wasm go build -o  ../../assets/json.wasm  
3cd ~/Documents/webassembly/cmd/server/  
4go run main.go

Error Handling

 1func jsonWrapper() js.Func {
 2    jsonfunc := js.FuncOf(func(this js.Value, args []js.Value) any {
 3        if len(args) != 1  {
 4            return errors.New("Invalid no of arguments passed")
 5        }
 6        jsDoc := js.Global().Get("document")
 7        if !jsDoc.Truthy() {
 8            return errors.New("Unable to get document object")
 9        }
10        jsonOuputTextArea := jsDoc.Call("getElementById", "jsonoutput")
11        if !jsonOuputTextArea.Truthy() {
12            return errors.New("Unable to get output text area")
13        }
14        inputJSON := args[0].String()
15        fmt.Printf("input %s\n", inputJSON)
16        pretty, err := prettyJson(inputJSON)
17        if err != nil {
18            errStr := fmt.Sprintf("unable to parse JSON. Error %s occurred\n", err)
19            return errors.New(errStr)
20        }
21        jsonOuputTextArea.Set("value", pretty)
22        return nil
23    })
24    return jsonfunc
25}

No notes link to this note