Go WebAssembly Internals - Part 1

x1unix

Denis Sedchenko

Posted on October 29, 2022

Go WebAssembly Internals - Part 1

In Go 1.11 was introduced WebAssembly support. WebAssembly is a binary executable format that was primary designed to run in web browsers but later started to become popular on other targets such as serverless and even Docker.

This series or articles will cover some Go internals that used to communicate with host JavaScript environment, its limitations and hacks that can be achieved by exploiting some undocumented features used for Go runtime internal purpose.

This first article will cover basics of syscall/js package and how Go code invocation from JS works.

Update: The next part of series is already available.

WebAssembly Communication Model

By default, WebAssembly doesn't have direct access to host system (JS world in our case), it can't directly access DOM, BOM (window object) and other JS APIs.

Each WebAssembly module has to declare a list of imported and exported symbols.

Imported symbols usually used to communicate with a browser (or other host environment) and and are linked by a browser during WebAssembly module instantiation. WebAssembly module caller on JS side should pass those symbols as import object.



WebAssembly.instantiate(<wasm module binary>, <import object>);


Enter fullscreen mode Exit fullscreen mode

Exports are used to export module symbols (functions usually) to be executed from JavaScript.

Unlike Go, Emscripten or Rust provide convenient way to import or export symbols from our program.

Go uses imports and exports for it's internal purposes and out of box doesn't provide a convenient way to link Go program functions to exports. I will cover this topic more in depth in next series.

Code Sample

The only way to communicate with JavaScript world is to interact with global namespace (window or globalThis), attaching or obtaining values from it using syscall/js package.

Go provides way to wrap Go functions to JavaScript callable functions using js.FuncOf and assigning them to JS objects (usually global object like window) that can be used to call Go code from JS.

Let's create a simple Go program that will export a simple greeter function that will print a simple "hello world" message.



package main

import (
    "fmt"
    "syscall/js"
)

func main() {
    wait := make(chan struct{})

    // Wrap our Go function as JS function to make it callable.
    jsFunc := js.FuncOf(greeter)

    // Assign our function to window.greeter
    js.Global().Set("greeter", jsFunc)

    // Prevent the program from exit
    <-wait
}

// First argument is JS execution context (this)
// and list of arguments passed to the function.
func greeter(this js.Value, args []js.Value) any {
    if len(args) == 0 {
        // The only way to return an error is to panic.
        panic("Missing name")
    }

    name := args[0].String()
    fmt.Println("Hello", name)
    return name
}


Enter fullscreen mode Exit fullscreen mode

Program should be compiled for WebAssembly architecture with this command:



GOOS=js GOARCH=wasm go build main.go


Enter fullscreen mode Exit fullscreen mode

Run a program

Let's try to run this program. For demonstration purposes, I already created a snippet on Better Go Playground which allows executing Go programs in WASM environment - https://goplay.tools/snippet/sNWAb8yhHtu.

Open the snippet, and select WebAssembly environment as shown on a screenshot below:

Image description

Then, click ▶️ Run button. Our program will notify that it's running and expecting greeter function to be called.

Open browser DevTools by pressing F12 key and go to Console tab and call our greeter function.

Image description

As you see, our function is working, but how this magic actually works?

JavaScript Invocation Internals

To execute Go WebAssembly program, Go SDK provides a small glue-code file wasm_exec.js which is located in $GOROOT/misc/wasm/wasm_exec.js.

It provides a window.Go object helper which will manage all JS-to-Go communication and will provide access to all JS objects. The syscall/js.FuncOf function using this helper for wrapping Go functions and making them callable from JavaScript.

Exporting Go Function

The js.FuncOf method wraps function to make it callable from JS. Wrapped function is put in specific internal map and have ID assigned to them. When function will be called from JS, wrapper will create an event with call information which will be routed to destination function by ID.

All wrapped Go functions are registered in funcs map inside of syscall/js package. Map key is function ID which will be used to find target function.

Here is a very abstract explanation of how js.FuncOf is working:



var (
  nextFuncID int

  // Key-value pair of function ID and function
  funcs map[int]func

  // Pointer to `wasm_exec.js` Go object in Javascript
  jsGo = js.Global().Get('Go')
)

func js.FuncOf(fn) {
  funcId := nextFuncID
  nextFuncID++

  js.funcs[funcId] = fn
  // Ask Go wasm_exec.js bridge to create a function wrapper using last inserted index
  jsFuncWrapper := jsGo.Get('_makeFuncWrapper').Call(funcId)

  // js.Func is a wrapper between target Go function and JS mapping
  return js.Func{
    id: funcId,
    value: jsFuncWrapper,
  }
}


Enter fullscreen mode Exit fullscreen mode

Go Function Wrapper Anatomy

Let's take a look, how our exported window.greeter function looks like. This function was created by go._makeFuncWrapper method which was called by Go at previous step.

JavaScript allows to get function's source code by calling window.greeter.toString().
Here is how our greeter function is actually look like:



function(){
  var event = {
    id:id,          // Target Go function ID
    this:this,      // JS function execution context
    args:arguments  // Passed arguments
  };
  Go._pendingEvent = event
  Go._resume();
  return event.result;
}


Enter fullscreen mode Exit fullscreen mode

As you see, under the hood it will call Go wasm_exec.js helper object, register an event with Go function and it's arguments and will resume a program to handle the request.

Invoking Target Go Function

Image description

When Go function wrapper is called from JavaScript, under the hood wrapper takes passed index of a function, creates an event with passed arguments and stores it in go._pendingEvent.

After that, it will “awake” Go by calling internal Go function wasm_export_resume which is exported by every Go WebAssembly binary as resume function.

wasm_export_resume calls a global event handler function handleEvent() which is located at syscall/js package.

handleEvent reads target function ID and call arguments from _pendingEvent property of JS Go object where we put all function call information inside Go function wrapper.

Then, it gets target Go function from funcs map, calls it and writes call result back to go._pendingEvent.result property. Result of event is retuned by JS wrapper function.

Conclusion

As this article shows, it's easy to export and call Go functions from JavaScript but Go function invocation process quite complex and requires context switching which is an expensive procedure.

The next article will cover process of how Go communicates with JavaScript world under the hood and obtains values from JavaScript world.

💖 💪 🙅 🚩
x1unix
Denis Sedchenko

Posted on October 29, 2022

Join Our Newsletter. No Spam, Only the good stuff.

Sign up to receive the latest update from our blog.

Related