DEV Community

Cover image for Generate and run WebAssembly code using Go
enbis
enbis

Posted on

Generate and run WebAssembly code using Go

Dive in the features of WebAssembly is not the focus along these few lines and for sure I'm not the most suitable person to do that. My purpose is to talk about how WebAssembly and Go can work together, since I'm interested in all the possible applications of Go language and recently I discovered this new solution that I'll try to show here step by step.
For those who have never heard WebAssembly I think could be fair a brief presentation of about what that standard is and what is used for.

A few word on WebAssembly

Probably the best approach to start with is take a look at the official website https://webassembly.org/.

WebAssembly is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable target for compilation of high-level languages like C/C++/Rust, enabling deployment on the web for client and server applications.

Let's try to analyze the keywords.
The first thing to know is that the WebAssembly code is compiled to binary format, the file generated after the build has .wasm extension.
The JavaScript engine decodes and interprets the .wasm file once it will be loaded by the web page. To execute the code a stack-based virtual machine is intended as a pointer that maintains the position of the executed code and a virtual control keep track of blocks within the code as though a stack.
The portability concerns the possibility to execute efficiently the wasm file on a variety of operating systems and architectures. WebAssembly is not tied to a specific environment and generate the binary file is easy starting from source code written in high-level languages.

WebAssembly in nutshell:

  • useful for developers that want to run code on a web browser without needs third party plugin, the major browsers natively supports WebAssembly
  • it is fast, WebAssembly code runs at a speed that is pretty close to the native speed
  • the .wasm binary can be generated using a wide range of languages like C, C++ and Rust. Now even in Go.
  • the browser's JavaScript engine will interpret the .wasm file and execute the functions.

Build the WebAssembly binary file using GO code

The WebAssembly compile target is available starting from Go 1.11.
The Go 1.12 version add some breaking changes in syscall/js, the package that will be briefly explore below. In order to proceed with this post without wasting time fixing errors, be ensure you have installed Go 1.12 or higher. You can quickly check the version installed with the command go version.
Now we can spent a few minutes exploring the algorithm we will use to obtain our purpose. Thinking about the easiest solution, we can read two integers received as parameters from the web page and print out the sum of them. Let's take a look at the code before moving to the build process for the .wasm file.

package main

import (
    "fmt"
    "strconv"
    "syscall/js"
)

var done = make(chan struct{})

func main() {
    callback := js.FuncOf(printResult)
    defer callback.Release()
    setResult := js.Global().Get("setResult")
    setResult.Invoke(callback)
    <-done
}

func printResult(value js.Value, args []js.Value) interface{} {
    value1 := args[0].String()
    v1, err := strconv.Atoi(value1)
    if err != nil {
        fmt.Errorf("error %s", err.Error())
        return err
    }
    value2 := args[1].String()
    v2, err := strconv.Atoi(value2)
    if err != nil {
        fmt.Errorf("error %s", err.Error())
        return err
    }

    fmt.Printf("%d\n", v1+v2)
    done <- struct{}{}
    return nil
}
Enter fullscreen mode Exit fullscreen mode

The printResult function didn't do anything else but get the values from JavaScript argument, parse it to int and print the sum. The channel is used to notify the callback has been called.

In the main function there are maybe the darker aspects of this silly example. First of all, let's see the package imported: syscall/js, here is the link to get more information https://golang.org/pkg/syscall/js/.

The package gives access to the WebAssembly host environment when using the js/wasm architecture. Its API is based on JavaScript semantics.

js.FuncOf(printResult) wraps the Go function printResult as a callback and its purpose is the sum operation. The JavaScript's values are passed to that function through js.Value array. The defer callback.Release() is used to free up resources as soon as the function returns. setResult is the JavaScript property used as a resolver of the promise, that wait for the result of the wrapped Go callback. We will better see this aspect later.
Now it's time to build the wasm file. It's an easy extension of the build command we have already learned to use when compiling Go code. Be careful to specify the js/wasm architecture using GOOS and GOARCH environment variables.

GOOS=js GOARCH=wasm go build -o main.wasm toWasm.go
Enter fullscreen mode Exit fullscreen mode
  • GOOS specifies the operating system, the default value is overwritten with the instance of JavaScript
  • GOARCH the default values specifies the processor architecture, in this case is overwritten with the WebAssembly value.

GOOS and GOARCH specified in the command tells Go to create a WebAssmebly file, without these details the .wasm file won't be created. If everything has been done well, we will find the main.wasm file in our directory.

Load the build and launch the HTML file

The first step is to copy the main.wasm just generated in the same directory selected to hold the html code as well. After that we need to copy the wasm_exec.js file in the same directory, this file is part of the Go installation package.
Assuming you are already in the working directory, copy and paste the following command.

cp "$(go env GOROOT)/misc/wasm/wasm_exec.js" .
Enter fullscreen mode Exit fullscreen mode

Now you have both main.wasm and wasm_exec.js in the same path, there is nothing more to do but add the index.html. This is the tree output of my working dir.

$ tree
.
├── index.html
├── main.wasm
└── wasm_exec.js

0 directories, 3 files
Enter fullscreen mode Exit fullscreen mode

Thinking about what we need to provide to the printResult function, is necessary add two <input> to get the values from the web page and a <button> to run the sum execution. The sum result will be printed out on the browser console.

Other than that the index.html

  • includes the wasm_exec.js file we copied before which allowing us to use the Go() constructor
  • loads the main.wasm as source using the WebAssembly.instantiateStreaming() method.

The return values of the instantiateStreaming() are mod = result.module representing the compiled WebAssembly module and inst = result.instance representing the object that contains the exported functions.
The instance const go = new Go() is necessary to run the code exported.
Finally, run() is an async function that waits for the result of the promise. The execution of the job is based on passing the values to the Go code. As soon as the promise make the result available, the code is free to reset the instance in order to be ready for the next run inst = await WebAssembly.instantiate(mod, go.importObject).

<html>
    <head>
        <meta charset="utf-8">
        <title>Go WebAssembly</title>
    </head>

    <body>
        <script src="wasm_exec.js"></script>
        <script>
            if (!WebAssembly.instantiateStreaming) {
                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("main.wasm"), go.importObject).then((result) => {
                mod = result.module;
                inst = result.instance;
            }).catch((err) =>{
                console.error(err)
            });

            var setResult
            async function run() {

                const printResultPromise = new Promise(resolve => {
                    setResult = resolve
                })
                const run = go.run(inst)

                const printResult = await printResultPromise

                printResult(document.querySelector('#value1').value, document.querySelector('#value2').value)
                await run 

                inst = await WebAssembly.instantiate(mod, go.importObject)
            }
        </script>
        <button onClick="run()" id="runButton">Run</button>
        <input id="value1" type="text">
        <input id="value2" type="text">
    </body>

</html>
Enter fullscreen mode Exit fullscreen mode

Now we have finished the code review. Let's try the solution loading the index.html file on the browser, this will be the outcome.

Alt Text

This is a just silly example, but the key is that create some WebAssembly code starting from Go is an easy job because of its cross-compilation capabilities. That open new possibilities leveraging on the browser support no longer for the full benefit of JavaScript only.

Top comments (3)

Collapse
 
iob profile image
Yiannis • Edited

i run your code and i had some issues as it shows here dev-to-uploads.s3.amazonaws.com/up.... seems like the instance does not export the functions or something. Can you help?

Collapse
 
iob profile image
Yiannis

i figured out that i had to run a server after all goexec 'http.ListenAndServe(:8081, http.FileServer(http.Dir(.)))'

Collapse
 
enbis profile image
enbis • Edited

Hello Yiannis, thanks to run the code. the CORS error says that you cannot using the browser to directly point a file on your machine.
so you have two possibilities now

  1. you can run a local webserver as you properly mentioned here
  2. you can try setting the flag privacy.file_unique_origin to false on Firfox browser ( check it searching for about:config ).

tbh it's been a while since I've run this code, I'll check it out soon. in the meantime I hope these tips could help you.