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

Like many training projects, this one does not do error handling.




 

Comments

Popular posts from this blog

AWS "serverless" Tutorial Execution