Simple Calculator App Using WebAssembly and go
This tutorial describes how to use WebAssembly and the Go programming language ("golang") to create a simple calculator web page. It assumes you're on a Mac. It was written in July 2021 and reflects the technical environment of that date.
The world doesn't really need another calculator app. I needed to code a demo app using WebAssembly in order to learn WebAssembly.
For starters, nobody codes in WebAssembly. They would code in C, C++, Java, or some other high-level language and compile that to WebAssembly, instead of to machine language. You can think of WebAssembly as a high-performance extension to JavaScript. If you have some JavaScript functionality which doesn't perform well, you might rewrite it into a language that supports WebAssembly, in order to get better performance.
This tutorial is loosely based on [this out-of-date tutorial](https://tutorialedge.net/golang/go-webassembly-tutorial/) but the go language support for WebAssembly has changed since then, breaking that tutorial.
Install golang
- brew update
- brew install golang
- Add these to ~/.bash_profile
export GOPATH=$HOME/Documents/code/go-workspace
export GOROOT=/usr/local/opt/go/libexec
export PATH=$PATH:$GOPATH/bin
export PATH=$PATH:$GOROOT/bin
- Restart bash shell
- mkdir -p $GOPATH $GOPATH/src $GOPATH/pkg $GOPATH/bin
- cd $GOPATH/src
Create a "hello world"
- Edit hello-go.go and make it look like this:
package main
import "fmt"
func main() {
fmt.Printf("Hello go!\n")
}
- Compile it to a temp executable and run the executable using: go run hello-go.go
- Compile and install the executable to $GOPATH/bin, and run it from there using:
go install hello-go.go
$GOPATH/bin/hello-go
- Clean-up our specious program: rm $GOPATH/bin/hello-go
Move Hello-world to WASM:
- Run: GOARCH=wasm GOOS=js go build -o lib.wasm hello-go.go
- It creates ./lib.wasm. For me, it was 1.2 MB. It gets all that extra size from the libraries it links in.
- Create index.html containing (lifted from multiple web sites):
<!DOCTYPE html>
<!--
Copyright 2018 The Go Authors. All rights reserved.
Use of this source code is governed by a BSD-style
license that can be found in the LICENSE file.
-->
<html>
<head>
<meta charset="utf-8" />
<title>Go wasm</title>
</head>
<body>
<script src="wasm_exec.js"></script>
<script>
if (!WebAssembly.instantiateStreaming) {
// polyfill
WebAssembly.instantiateStreaming = async (resp, importObject) => {
const source = await (await resp).arrayBuffer();
return await WebAssembly.instantiate(source, importObject);
};
}
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
result => {
mod = result.module;
inst = result.instance;
document.getElementById("runButton").disabled = false;
}
);
async function run() {
await go.run(inst);
inst = await WebAssembly.instantiate(mod, go.importObject); // reset instance
}
</script>
<button onClick="run();" id="runButton" disabled>Run</button>
</body>
</html>
- Run: cp $GOROOT/misc/wasm/wasm_exec.js .
- Create http-server.go containing:
package main
import (
"flag"
"log"
"net/http"
)
var (
listen = flag.String("listen", ":8080", "listen address")
dir = flag.String("dir", ".", "directory to serve")
)
func main() {
flag.Parse()
log.Printf("listening on %q...", *listen)
log.Fatal(http.ListenAndServe(*listen, http.FileServer(http.Dir(*dir))))
}
- Run: go run http-server.go
- Browse to http://localhost:8080, open the browser's developer console, press the "Run" button on the web page, and you'll see "Hello go!" in the console.
- Kill the http-server.
- Now make hello-go.go look like this:
package main
import (
"syscall/js"
)
func main() {
alert := js.Global().Get("alert")
alert.Invoke("Hello go!")
}
- Run: GOARCH=wasm GOOS=js go build -o lib.wasm hello-go.go
- Run: go run http-server.go
- Browse to http://localhost:8080, open the browser's developer console, press the "Run" button on the web page, and you'll see "Hello go!" in a JavaScript alert.
- At this point, you have an HTML web page, with JavaScript code that calls a go function, the go function has been compiled to WebAssembly, and the go function calls the JavaScript runtime library to display a message box.
Create a Simple Calculator
- Edit wasm-calc.go and make it look like this:
package main
import (
"syscall/js"
)
func add(this js.Value, i []js.Value) interface{} {
tmp := i[0].Int() + i[1].Int();
println(i[0].Int(), "+", i[1].Int(), "=", tmp)
return js.ValueOf(tmp)
}
func subtract(this js.Value, i []js.Value) interface{} {
tmp := i[0].Int() - i[1].Int();
println(i[0].Int(), "-", i[1].Int(), "=", tmp)
return js.ValueOf(tmp)
}
func registerCallbacks() {
js.Global().Set("add", js.FuncOf(add))
js.Global().Set("subtract", js.FuncOf(subtract))
}
func main() {
c := make(chan struct{}, 0)
println("WASM Go Initialized")
registerCallbacks()
<-c
}
- Edit index.html and find the line WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
- Replace that entire (7 line) call to WebAssembly.instantiateStreaming with this:
WebAssembly.instantiateStreaming(fetch("lib.wasm"), go.importObject).then(
async result => {
mod = result.module;
inst = result.instance;
await go.run(inst);
}
);
- That runs the code without waiting for the user to press the 'Run' button.
- Recompile and re-launch the web server with:
GOARCH=wasm GOOS=js go build -o lib.wasm wasm-calc.go
go run http-server.go
- Browse to http://localhost:8080 and check the JavaScript console to confirm you see "WASM Go Initialized". That confirms the code you've written so far works.
- Remove the "Run" button from index.html and replace it with:
<input type="text" id="value1" />
<input type="text" id="value2" />
<button onClick="add(2,3);" id="addButton">Add</button>
<button onClick="subtract(10,3);" id="subtractButton">Subtract</button>
- Re-run go run http-server.go and browse to http://localhost:8080
- Pressing add or subtract will add/subtract the hard-coded numbers.
- Update the add and subtract functions in wasm-calc.go to look like this:
func add(this js.Value, i []js.Value) interface{} {
value1, _ := strconv.Atoi(js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String())
value2, _ := strconv.Atoi(js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String())
tmp := value1 + value2;
println(value1, "+", value2, "=", tmp)
return js.ValueOf(tmp)
}
func subtract(this js.Value, i []js.Value) interface{} {
value1, _ := strconv.Atoi(js.Global().Get("document").Call("getElementById", i[0].String()).Get("value").String())
value2, _ := strconv.Atoi(js.Global().Get("document").Call("getElementById", i[1].String()).Get("value").String())
tmp := value1 - value2;
println(value1, "-", value2, "=", tmp)
return js.ValueOf(tmp)
}
- And update the import statement in wasm-calc.go to look like this:
import (
"syscall/js"
"strconv"
)
- Edit index.html and replace the add and subtract buttons with
<button onClick="add('value1', 'value2', 'result');" id="addButton">+</button>
<button onClick="subtract('value1', 'value2', 'result');" id="subtractButton">-</button>
<input type="text" id="result" />
- Run: GOARCH=wasm GOOS=js go build -o lib.wasm wasm-calc.go
- Run: go run http-server.go
- Now it adds and subtracts the entered values, printing results in the JS console.
- Update it to send output to the web page. Add a line like this after the println in the add function and in the subtract function in wasm-calc.go:
js.Global().Get("document").Call("getElementById", i[2].String()).Set("value", tmp)
- Run: GOARCH=wasm GOOS=js go build -o lib.wasm wasm-calc.go
- Run `go run http-server.go`
- Now it adds and subtracts the entered values, printing results in the JS console AND the web page.
Extra Operations
- I implemented a multiply and a divide button similarly.
Limitations
Comments
Post a Comment