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>);
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
}
Program should be compiled for WebAssembly architecture with this command:
GOOS=js GOARCH=wasm go build main.go
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:
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.
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,
}
}
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;
}
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
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.
Top comments (1)
This is the best article I've read on the topic so far 👏👏👏