Simple tutorial about WASM
WASM(webassembly) is a virtual assembly language for the browser. It is closer to the Hardware than JavaScript.
- WebAssembly using Go (Golang) | Run Go programs in the browser
- Access the browser's DOM from Go (Golang) using WebAssembly | golangbot.com
- golangbot/webassembly: Webassembly using Go
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}