Foundations: Wasm in Golang is fantastic

Foundations: Wasm in Golang is fantastic

A bit about me

I'm Philippe, and I'm Technical Account Manager at GitLab. I am french, but you cannot hear my lovely accent. I love a lot speaking in front of people, mainly in French (it's, of course, easier for me), so I wrote this blog post like if I have to prepare an English talk for a technical event (speaking in English is probably the most challenging exercise I have to face). I hope I'll reuse this stuff one day publicly.

What's the plan?

We will see how to take your first steps in Wasm and some recipes to help you go further through this blog post.

Wasm?

Well, the title says: "Wasm in Golang is fantastic.", but what is "Wasm" in a few words?.

The WebAssembly homepage says: "WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications."

I would say:

  • "Wasm is a portable format (like Java or .Net), and you can execute it wherever you have a host capable of it. Initially, the primary host was JavaScript with the browser".

Now, you can run Wasm with JavaScript and NodeJS, and we saw the birth of Wasm runtimes like the Wasmer project recently, allowing running Wasm everywhere.

I like to say that "a wasm file is like a container image but smaller and without an operating system".

Wasm is polyglot, but...

You can compile a Wasm file with several languages: C/C++, Rust, GoLang, Swift, ... And we even saw the emergence of languages dedicated to building Wasm, like AssemblyScript or the promising Grain (keep an eye on it, the grammar is lovely).

This summer, I decided to get started with Wasm. The tendency seems to use Rust for this, but I understood quickly that my baby steps would be complicated. The difficulty did not necessarily come from the language itself. The most tedious and difficult part was all the tools I needed to run a simple "Hello World" in a browser (1). After some search, I discovered that Golang provides pretty simple support for Wasm (a lot simpler than with Rust). So, my vacation homework was done with Golang.

The Wasm support with Golang is fantastic. Typically, WebAssembly has four data types (32 & 64 Bit Integer, 32 & 64 Bit Floating-Point Number), and it could be a mess to use functions with String parameters (or even JSON Objects). Thankfully, Go provides the wasm_exec.js file that facilitates the interaction with the JavaScript API.

(1): A month later, I dug more seriously, and I think it's mostly the documentation and examples that are not suitable for beginners. "First steps with Wasm in Rust" could be the subject of a future article.

Prerequisites

To run the examples of this blog post, you'll need:

  • Golang 1.16
  • TinyGo 0.19.0 (note: TinyGo 0.19.0 doesn't work with GoLang 1.17)
  • An http server to serve your webpages

Btw, to serve my pages, I use the Fastify project with this code:

index.js

const fastify = require('fastify')({ logger: true })
const path = require('path')

// Serve the static assets
fastify.register(require('fastify-static'), {
  root: path.join(__dirname, ''),
  prefix: '/'
})

const start = async () => {
  try {
    await fastify.listen(8080, "0.0.0.0")
    fastify.log.info(`server listening on ${fastify.server.address().port}`)

  } catch (error) {
    fastify.log.error(error)
  }
}
start()

and I use this package.jsonfile to install Fastify (with npm install):

package.json

{
    "dependencies": {
        "fastify": "^3.6.0",
        "fastify-static": "^3.2.1"
    }
}

For lazy people like me, I created a project here https://gitlab.com/k33g_org/suborbital-demo, and if you open it with GitPod, you'll get a ready to use development environment and you don't need to install anything (with this URL: https://gitpod.io/#https://gitlab.com/k33g_org/suborbital-demo).

The essential "hello world!"

Create a quick'n dirty project

First, create a hello-world directory and then inside this directory create 2 files:

  • main.go
  • index.html

with the following source code:

main.go

package main

import (
    "fmt"
)

func main() {
  fmt.Println("πŸ‘‹ Hello World 🌍")
    // Prevent the function from returning, which is required in a wasm module
    <-make(chan bool)
}

index.html

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // This is a polyfill for FireFox and Safari
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) => {
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

            // Promise to load the wasm file
           function loadWasm(path) {
             const go = new Go()

             return new Promise((resolve, reject) => {
               WebAssembly.instantiateStreaming(fetch(path), go.importObject)
               .then(result => {
                 go.run(result.instance)
                 resolve(result.instance)
               })
               .catch(error => {
                 reject(error)
               })
             })
           }

         // Load the wasm file
         loadWasm("main.wasm").then(wasm => {
             console.log("main.wasm is loaded πŸ‘‹")
         }).catch(error => {
             console.log("ouch", error)
         }) 

        </script>
    </body>
</html>

Remark: the most important parts are:

  • This line <script src="wasm_exec.js"></script>
  • And this line WebAssembly.instantiateStreaming, it's the JavaScript API that allows to load the wasm file.

You also need to generate a go.mod file, by using this command: go mod init hello-world

You should get this content:

module hello-world

go 1.16

Build your first Wasm module

Before building the Wasm module, you need to get the wasm_exec.js file, and then you'll be able to launch the compilation:

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
GOOS=js GOARCH=wasm go build -o main.wasm

And now, serve your html page with this command node index.js to run the Fastify http server and navigate to http://localhost:8080 with your favorite browser and open the console developer tools:

001.png

So, it's pretty simple to get started, but if you look at the size of main.wasm, you'll discover that the size of the generated file is around 2.1M!!! And honestly, I find this unacceptable. Fortunately, we have a friendly solution with TinyGo. Let's see that.

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/01-hello-world

The essential "hello world!" with TinyGo

First, What is TinyGo? TinyGo allows compiling Golang source code for microcontrollers, and it can compile Go code to Wasm too. TinyGo is a compiler intended for use in "small places", so the generated files are much smaller.

Duplicate your hello-world project to a new directory hello-world-tinygo and change the content of the go.mod file:

module hello-world-tinygo

go 1.16

Before building the Wasm file, this time, you need to get the wasm_exec.js related to TinyGo and then you'll be able to launch the compilation:

wget https://raw.githubusercontent.com/tinygo-org/tinygo/v0.19.0/targets/wasm_exec.js
tinygo build -o main.wasm -target wasm ./main.go

If you serve your html page, you'll get the same result as the previous example. But look at the size of main.wasm. Now, the size is 223K, and it's a lot better.

Keep in mind that TinyGo supports a subset of the Go language, so not everything is available yet (tinygo.org/docs/reference/lang-support). For my experiments, it was enough; otherwise, continue with "pure" Go.

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/02-hello-world-tinygo

My little Cookbook

I have seen too many long tutorials which finally stopped at this simple "hello world" without going any further. They even don't explain how to parameters to functions. Often, it's only a decoration of the "getting started" of projects without ever going further.

So today, I give you all the little recipes that have allowed me to go a little further.

Here are the different interactions between Wasm and the browser that I will cover today:

  • Interacting with DOM
  • Get a String by calling a Golang function with a String as a parameter
  • How to return an object "readable" by JavaScript?
  • How to use a JSON object as parameter?
  • How to use an array as parameter?
  • How to return an array?

Interacting with DOM

We'll use the "syscall/js" Golang package to add child tags to the Html document object model from Go code. According to the documentation: "Package js gives access to the WebAssembly host environment when using the js/wasm architecture. Its API is based on JavaScript semantics.". This package exposes a small set of features: the type Value (the Go JavaScript data representation) and the way to request Go from the JavaScript host.

  • Create a new directory by copying the previous one and name it dom
  • Update the go.mod file:

    module dom
    
    go 1.16
    

Just change the code of main.go:

package main

import (
    "syscall/js"
)

func main() {

  message := "πŸ‘‹ Hello World 🌍"

    document := js.Global().Get("document")
    h2 := document.Call("createElement", "h2")
    h2.Set("innerHTML", message)
    document.Get("body").Call("appendChild", h2)

    <-make(chan bool)
}

Build the code tinygo build -o main.wasm -target wasm ./main.go and serve the html page node index.js, then navigate to http://localhost:8080

002.png

  • We got a reference to the DOM with js.Global().Get("document")
  • We created the <h2></h2> element with document.Call("createElement", "h2")
  • We set the value of the innerHTML with h2.Set("innerHTML", message)
  • And finally add the element to the body with document.Get("body").Call("appendChild", h2)

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/03-dom

Now, let's see how to make a callable Go function, that we'll use in our html page.

Call a Go function

This time, we need to "export" the function to the global context (i.e. window in the browser, global in NodeJS). Again the "syscall/js" GoLang package provides the necessary helpers to do that.

As usual, create a new directory first-function (use the previous example) and update the go.mod file by changin the value of the module: module first-function.

This is the source code of main.go:

package main

import (
    "syscall/js"
)

func Hello(this js.Value, args []js.Value) interface{} {
    message := args[0].String() // get the parameters
    return "Hello " + message
}

func main() {
    js.Global().Set("Hello", js.FuncOf(Hello))

    <-make(chan bool)
}
  • To export the function to the global context, we used the FuncOf function: js.Global().Set("Hello", js.FuncOf(Hello)). The FuncOf function is used to create a Func type.
  • The Hello function takes two parameters and returns an interface{} type. The function will be called synchronously from Javascript. The first parameter (this) refers to JavaScript's global object. The second parameter is a slice of []js.Value representing the arguments passed to the Javascript function call.

We need to modify the index.html file to call the Hello Go function:

index.html:

<html>
    <head>
        <meta charset="utf-8"/>
        <script src="wasm_exec.js"></script>

    </head>
    <body>
        <h1>WASM Experiments</h1>
        <script>
            // polyfill
            if (!WebAssembly.instantiateStreaming) { 
                WebAssembly.instantiateStreaming = async (resp, importObject) => {
                    const source = await (await resp).arrayBuffer()
                    return await WebAssembly.instantiate(source, importObject)
                }
            }

      function loadWasm(path) {
        const go = new Go()
        return new Promise((resolve, reject) => {
          WebAssembly.instantiateStreaming(fetch(path), go.importObject)
          .then(result => {
            go.run(result.instance)
            resolve(result.instance)
          })
          .catch(error => {
            reject(error)
          })
        })
      }

    loadWasm("main.wasm").then(wasm => {
        console.log("main.wasm is loaded πŸ‘‹")
        console.log(Hello("Bob Morane"))
        document.querySelector("h1").innerHTML = Hello("Bob Morane")
            }).catch(error => {
        console.log("ouch", error)
      }) 

        </script>
    </body>
</html>

What changed?, only these 2 lines:

  • console.log(Hello("Bob Morane")): calling the Hello Go function with "Bob Morane"as parameter and display the result in the browser console.
  • document.querySelector("h1").innerHTML = Hello("Bob Morane") calling the Hello Go function with "Bob Morane"as parameter and change the h1 thag value with the result.

So,

  • Build the Wasm file: tinygo build -o main.wasm -target wasm ./main.go
  • Serve the html page: node index.js

003.png

You can see that the content of the page is updated, but that we have some error messages in the console. No worries, it's easy to fix; it's a know bug https://github.com/tinygo-org/tinygo/issues/1140, but the workaround is simple:

function loadWasm(path) {
    const go = new Go()
    //remove the message: syscall/js.finalizeRef not implemented
    go.importObject.env["syscall/js.finalizeRef"] = () => {}

    return new Promise((resolve, reject) => {
        WebAssembly.instantiateStreaming(fetch(path), go.importObject)
        .then(result => {
            go.run(result.instance)
            resolve(result.instance)
        })
        .catch(error => {
            reject(error)
        })
    })
}
  • I only added this line go.importObject.env["syscall/js.finalizeRef"] = () => {} to avoid the error message.

Refresh the page, no more issue!:

004.png

Right now, you have almost everything you need in your hands to go further. But let me bring to you my other recipes.

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/04-first-function

My other recipes

How to return an object "readable" by JavaScript?

This time, we pass 2 String parameters to the Hello function (firstName and lastName), and to return a json object we use the type map[string]interface{}:

GoLang function:

func Hello(this js.Value, args []js.Value) interface{} {

    firstName := args[0].String()
    lastName := args[1].String()

    return map[string]interface{}{
        "message": "πŸ‘‹ Hello " + firstName + " " + lastName,
        "author":  "@k33g_org",
    }

}

Calling the Hello function from JavaScript is simple:

loadWasm("main.wasm").then(wasm => {
    let jsonData = Hello("Bob", "Morane")
    console.log(jsonData)
    document.querySelector("h1").innerHTML = JSON.stringify(jsonData)

}).catch(error => {
    console.log("ouch", error)
})

Serve your page node index.js:

005.png

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/05-return-object

How to use a Json object as parameter when calling Hello?

If I want to use a Json object as parameter in JavaScript, like that:

let jsonData = Hello({firstName: "Bob", lastName: "Morane"})

I will write my GoLang function like this

func Hello(this js.Value, args []js.Value) interface{} {

    // get an object
    human := args[0]
    // get members of an object
    firstName := human.Get("firstName")
    lastName := human.Get("lastName")

    return map[string]interface{}{
        "message": "πŸ‘‹ Hello " + firstName.String() + " " + lastName.String(),
        "author":  "@k33g_org",
    }

}
  • args[0] contains the Json object
  • Use the Get(field_name) method to retrieve the value of fields

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/06-json-as-parameter

How to use an array as parameter when calling Hello?

JavaScript call:

let jsonData = Hello(["Bob", "Morane", 42, 1.80])

Golang function:

func Hello(this js.Value, args []js.Value) interface{} {

    // get members of an array
    firstName := args[0].Index(0)
    lastName := args[0].Index(1)
    age := args[0].Index(2)
    size := args[0].Index(3)

    return map[string]interface{}{
        "message":   "πŸ‘‹ Hello",
        "firstName": firstName.String(),
        "lastName":  lastName.String(),
        "age":       age.Int(),
        "size":      size.Float(),
        "author":    "@k33g_org",
    }
}

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/07-array-as-parameter

How to return an array?

Golang function:

func GiveMeNumbers(_ js.Value, args []js.Value) interface{} {
    return []interface{} {1, 2, 3, 4, 5}
}

source code: https://gitlab.com/k33g_org/suborbital-demo/-/tree/main/08-return-an-array

So, that's it for this time. I'm still learning about Wasm and the Js package of Golang, but I've already had some serious fun with all of this.

Note from the editor

This delightful guest post from Philippe is the third in our Foundations series, focused on helping developers learn how to build with WebAssembly. You can take a look at part 1 and part 2, which talk about getting started with TypeScript and WebAssembly. Our newsletter (below) is the best way to stay up to date with our Foundations series and other WebAssembly-related news. Thanks Philippe!

Cover photo by NON on Unsplash

Β 
Share this