1. Introduction to WebAssembly

  • Cross-platform, runs on any platform that supports WebAssembly, including web browsers, servers, mobile devices, etc.
  • High performance, using a compact binary format that can be loaded and parsed quickly in a browser to improve application performance
  • Security, using a sandbox model that isolates the code running in it to protect the system from malicious code attacks
  • Portability, WebAssembly code can be generated from different programming languages through a compiler, so existing code can be easily converted to WebAssembly format
  • Extensibility, WebAssembly supports backward-compatible version control and allows new instructions and features to be added in the future, making it more extensible

2. Cases

More examples of using WebAssembly are available at https://madewithwebassembly.com/

3. Common WebAssembly non-front-end runtimes

The WebAssembly runtime is the environment in which WebAssembly code runs, it is responsible for loading WebAssembly modules, creating WebAssembly instances, and executing WebAssembly code.

  • C/C++ implementation - wasmEdge, wasm3
  • Rust - wasmer, wasmtime
  • Go - WaZero

Wasm is supported by all major browsers and high versions of Nodejs.

4. Install WasmEdge

Since the new version of Docker supports WasmEdge, I also choose to use WasmEdge as the WebAssembly runtime locally for consistency within the container.

  • Download the Wasmedge binary

    Download the binaries for your platform at https://github.com/WasmEdge/WasmEdge/releases.

    1
    
    wget https://github.com/WasmEdge/WasmEdge/releases/download/0.12.0/WasmEdge-0.12.0-darwin_x86_64.tar.gz
    
  • Unpack Wasmedge and install

    1
    2
    
    tar -zxvf WasmEdge-0.12.0-darwin_x86_64.tar.gz -C /Users/shaowenchen --strip-components=1
    export PATH=$PATH:/Users/shaowenchen/bin
    

    If you need to copy to another directory, be careful to keep the bin, include and lib directories relative to each other, otherwise the following error will occur.

    1
    2
    3
    
    dyld: Library not loaded: @rpath/libwasmedge.0.dylib
    Referenced from: /Users/shaowenchen/bin/wasmedge
    Reason: image not found
    
  • View Wasmedge version

    1
    2
    3
    
    wasmedge --version
    
    wasmedge version 0.12.0
    

5. Using TinyGo to compile WebAssembly programs

5.1 Advantages and disadvantages of using TinyGo

  • Advantages of using TinyGo

    TinyGo is a subset of Go and allows you to write WebAssembly programs using the Go language.

    Can be run directly on WasmEdge, no JavaScript environment required.

    wasm files are small enough, a few tens of KB.

  • Disadvantages of using TinyGo

    Some libraries in TinyGo are not available, such as net/http, etc. See https://tinygo.org/docs/reference/lang-support/stdlib/ for details.

    TinyGo does not support all Go language features, such as Cgo, etc. See https://tinygo.org/docs/reference/lang-support/ for details.

5.1 Hello, World

  • Create a new main.go file

    1
    2
    3
    4
    5
    
    package main
    
    func main() {
        println("Hello, World! by TinyGo")
    }
    
  • Compilation

    1
    
    tinygo build -o ./build/main.wasm -target=wasm
    
  • Run with WasmEdge

    1
    2
    3
    
    wasmedge ./build/main.wasm
    
    Hello, World! by TinyGo
    
  • Create a new Dockerfile file

    1
    2
    3
    
    FROM scratch
    ADD ./build/main.wasm /build/main.wasm
    ENTRYPOINT ["/build/main.wasm"]
    
  • Compiling the container image

    1
    
    docker build -t shaowenchen/wasm-hello-world:tinygo .
    
  • Running the container image

    1
    2
    3
    4
    
    docker run  --rm --runtime=io.containerd.wasmedge.v1 \
                    shaowenchen/wasm-hello-world:tinygo
    
    Hello, World! by TinyGo
    

6. Using Go to compile WebAssembly programs

6.1 Advantages and disadvantages of using Go

  • Advantages of using Go

    You can use the syscall/js package to interact with JavaScript.

    You can use the full range of Go language features and packages.

    In the future, WebAssembly runtime support for GC may improve program size and performance.

  • Disadvantages of using Go

    Requires JavaScript environment support by default.

    Large size, several MB or even tens of MB, but many older projects compile to tens or hundreds of MB.

6.2 Hello, World

  • Create a new main.go file

    1
    2
    3
    4
    5
    
    package main
    
    func main() {
        println("Hello, World! by Go 1.19")
    }
    
  • Compilation

    1
    
    GOOS=js GOARCH=wasm go build -o ./dist/main.wasm
    
  • Run with node

    Note that nodejs requires 12 and above to work.

    1
    2
    3
    4
    
    cp $(shell go env GOROOT)/misc/wasm/wasm_exec_node.js ./dist/
    node ./dist/wasm_exec_node.js ./dist/main.wasm
    
    Hello, World! by Go 1.19
    
  • Create a new Dockerfile file

    1
    2
    3
    
    FROM node:12-alpine
    ADD ./dist /dist
    ENTRYPOINT ["node","/dist/wasm_exec_node.js", "/dist/main.wasm"]
    
  • Compiling the container image

    1
    
    docker build -t shaowenchen/wasm-hello-world:go .
    
  • Running the container image

    1
    2
    3
    
    docker run  --rm shaowenchen/wasm-hello-world:go
    
    Hello, World! by Go 1.19
    

6.3 Using syscall/js to interact with JavaScript data

The Go language provides the syscall/js package that can be used to interact with JavaScript.

  • Calling JavaScript functions in Go

    The Global() function provided by the syscall/js package gets the object of the host JavaScript environment.

    1
    
    js.Global().Get("console").Get("log").Invoke("Hello, World!")
    

    Equivalent to

    1
    
    console.log("Hello, World!")
    
  • Calling Go functions in JavaScript

    1
    2
    3
    4
    5
    
    js.Global().Set("myfunc", js.FuncOf(myfunc))
    
    func myfunc(this js.Value, args []js.Value) interface{} {
        return nil
    }
    

    js.Global().Set registers the object into the JavaScript environment, and the myfunc function can be called directly in the browser front-end.

  • Rewrite hello world and execute wasm in the browser page

     1
     2
     3
     4
     5
     6
     7
     8
     9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    
    package main
    
    import (
        "syscall/js"
    )
    
    func main() {
        // Call the front-end function and print on the console
        js.Global().Get("console").Get("log").Invoke("Hello, World by Go")
        // Find the element with the ID hello and insert the text
        js.Global().Get("document").Call("getElementById", "hello").Set("innerHTML", "Hello, World! by Go")
        // Register the Go function to the front-end
        js.Global().Set("myfunc", js.FuncOf(myfunc))
        // Do not exit immediately, otherwise an error will be reported
        <-make(chan bool)
    }
    
    func myfunc(this js.Value, args []js.Value) interface{} {
        println("myfunc called")
        // Get function parameters
        myfunc_arg0 := args[0].String()
        // Set variables in the front-end window object
        js.Global().Set("myfunc_arg0", myfunc_arg0)
        js.Global().Get("console").Get("log").Invoke(myfunc_arg0)
        return nil
    }
    
  • Copy wasm_exec.js wasm_exec.html file

    The Go compiler comes with a sample example.

    1
    2
    
    cp $(shell go env GOROOT)/misc/wasm/wasm_exec.js ./dist/
    cp $(shell go env GOROOT)/misc/wasm/wasm_exec.html ./dist/
    

    wasm_exec.js is used to load the execution of wasm in the browser, wasm_exec.html is an example.

  • Modifying wasm_exec.html

    To demonstrate Go’s ability to interact with JavaScript data, here’s a slightly modified sample.

    1
    
    WebAssembly.instantiateStreaming(fetch("main.wasm")...
    

    The fetch here should be the compiled wasm file, which defaults to test.wasm and is modified to main.wasm as needed.

    1
    2
    3
    4
    5
    6
    7
    
    </script>
    function callGo() {
            myfunc("abc");
        }
    </script>
    <button onClick="callGo();" id="callGoButton" >callGoButton</button>
    <div id="hello"></div>
    

    A new button is added here to call the Go function myfunc.

  • Check the effect

    Start a local http service.

    1
    2
    3
    
    python3 -m http.server --directory ./dist
    
    Serving HTTP on :: port 8000 (http://[::]:8000/) ...
    

    Visit the http://localhost:8000/wasm_exec.html page.

    wasm_exec.html

    Clicking the Run button, Go calls the front-end object, outputs the text, and inserts the text on the page.

    wasm_exec.html

    Click callGoButton to call the Go function on the front end.

    wasm_exec.html

    Access windows.myfunc_arg0 in the console and get the value set by Go in the front-end object.

    wasm_exec.html

7. Summary

This article is mainly trying out some ways of writing WebAssembly in Go. Without additional configuration, the two quicker ways are.

  • Write it in TingyGo and run it directly on WasmEdge
  • written in Go, loaded with JS, and run on Nodejs

There is also an example of writing WebAssembly with Go 1.19 to interact with JavaScript functions and data.

The code in the text is available at https://github.com/shaowenchen/demo/tree/master/wasm-hello-world.

8. Ref

  • https://www.chenshaowen.com/blog/get-webassembly-programs-using-go.html