Announcing the fastest WebAssembly runtime for Go: wasmer
This is a copy of an article I wrote for Wasmer.
WebAssembly is a portable binary format. That means the same file can run anywhere.
To uphold this bold statement, each language, platform and system must be able to run WebAssembly — as fast and safely as possible.
Wasmer is a WebAssembly runtime written in Rust. It goes without saying that the runtime can be used in any Rust application. We have also successfully embedded the runtime in other languages:
- Using C and C++ bindings
- In PHP, using
php-ext-wasm
- In Python, using
python-ext-wasm
— wasmer package on PyPI - In Ruby, using
ruby-ext-wasm
— wasmer gem on RubyGems - It is now time to hang around Go 🐹!
We are super happy to announce github.com/wasmerio/go-ext-wasm/wasmer
,
a Go library to run WebAssembly binaries,
fast.
Calling a WebAssembly function from Go
First, let’s install wasmer
in your go environment (with cgo
support).
;;
Let’s jump immediately into some
examples.github.com/wasmerio/go-ext-wasm/wasmer
is a regular Go
library. The installation is automated with
import "github.com/wasmerio/go-ext-wasm/wasmer"
.
Let’s get our hands dirty. We will write a program that compiles to WebAssembly easily, using Rust for instance:
pub extern
After compilation to WebAssembly, we get a file like this
one,
named simple.wasm
.
The following Go program executes the sum
function by passing 5
and
37
as arguments:
package main
import (
"fmt"
wasm "github.com/wasmerio/go-ext-wasm/wasmer"
)
func main()
Great! We have successfully executed a WebAssembly file inside Go.
Note: Go values passed to the WebAssembly exported function are automatically cast to WebAssembly values. Types are inferred and casting is done automatically. Thus, a WebAssembly function acts as any regular Go function.
WebAssembly calling Go funtions
A WebAssembly module exports some functions, so that they can be called from the outside world. This is the entry point to execute WebAssembly.
Nonetheless, a WebAssembly module can also have imported functions. Let’s consider the following Rust program:
extern
pub extern
The exported function add1
calls the sum
function. Its
implementation is absent, only its signature is defined. This is an
“extern function”, and for WebAssembly, this is an imported function,
because its implementation must be imported.
Let’s implement the sum
function in Go! To do so, need to use
cgo:
- The
sum
function signature is defined in C (see the comment aboveimport "C"
), - The
sum
implementation is defined in Go. Notice the//export
which is the way cgo uses to map Go code to C code, NewImports
is an API used to create WebAssembly imports. In this code"sum"
is the WebAssembly imported function name,sum
is the Go function pointer, andC.sum
is the cgo function pointer,- Finally,
NewInstanceWithImports
is the constructor to use to instantiate the WebAssembly module with imports. That’s it.
Let’s see the complete program:
package main
// // 1️⃣ Declare the `sum` function signature (see cgo).
//
// #include <stdlib.h>
//
// extern int32_t sum(void *context, int32_t x, int32_t y);
import "C"
import (
"fmt"
wasm "github.com/wasmerio/go-ext-wasm/wasmer"
"unsafe"
)
// 2️⃣ Write the implementation of the `sum` function, and export it (for cgo).
//export sum
func sum(context unsafe.Pointer, x int32, y int32) int32
func main()
Reading the memory
A WebAssembly instance has a linear memory. Let’s see how to read it. Consider the following Rust program:
pub extern
The return_hello
function returns a pointer to a string. The string
terminates by a null byte, à la C. Let’s jump on the Go side:
bytes, _ := wasm.ReadBytes("memory.wasm")
instance, _ := wasm.NewInstance(bytes)
defer instance.Close()
// Calls the `return_hello` exported function.
// This function returns a pointer to a string.
result, _ := instance.Exports["return_hello"]()
// Gets the pointer value as an integer.
pointer := result.ToI32()
// Reads the memory.
memory := instance.Memory.Data()
fmt.Println(string(memory[pointer : pointer+13])) // Hello, World!
The return_hello
function returns a pointer as an i32
value. We get
its value by calling ToI32
. Then, we fetch the memory data with
instance.Memory.Data()
.
This function returns a slice over the WebAssembly instance memory. It can be used as any regular Go slice.
Fortunately for us, we already know the length of the string we want to
read, so memory[pointer : pointer+13]
is enough to read the bytes,
that are then cast to a string. Et voilà !
You can read the Greet Example to see a more advanced usage of the memory API.
Benchmarks
So far, github.com/wasmerio/go-ext-wasm/wasmer
has a nice API, but
…is it fast?
Contrary to PHP or Ruby, there are already existing runtimes in the Go world to execute WebAssembly. The main candidates are:
- Life, from Perlin Network, a WebAssembly interpreter
- Wagon, from Go Interpreter, a WebAssembly interpreter and toolkit.
In our blog post about the PHP
extension,
we have used the n-body
algorithm
to benchmark the performance. Life provides more benchmarks: the
Fibonacci algorithm
(the recursive version), the Pollard’s rho
algorithm, and
the Snappy Compress operation. The latter works successfully with
github.com/wasmerio/go-ext-wasm/wasmer
but not with Life or Wagon. We
have removed it from the benchmark suites. Benchmark
sources
are online.
We use Life 20190521143330–57f3819c2df0, and Wagon 0.4.0, i.e. the latest versions to date.
The benchmark numbers represent the average result for 10 runs each. The computer that ran these benchmarks is a MacBook Pro 15" from 2016, 2.9Ghz Core i7 with 16Gb of memory.
Results are grouped by benchmark algorithm on the X axis. The Y axis represents the time used to run the algorithm, expressed in milliseconds. The lower, the better.
While both Life and Wagon provide on average the same speed, Wasmer
(github.com/wasmerio/go-ext/wasmer
) is on average 72 times faster
🎉.
It is important to know that Wasmer comes with 3 backends: Singlepass, Cranelift, and LLVM. The default backend that is used by the Go library is Cranelift (learn more about Cranelift). Using LLVM will provide performance close to native, but we decided to start with Cranelift as it offers the best tradeoff between compilation-time and execution-time (learn more about the different backends, when to use them, pros and cons etc.).
Conclusion
[github.com/wasmerio/go-ext-wasm/wasmer](https://github.com/wasmerio/go-ext-wasm)
is a new Go library to execute WebAssembly binaries. It embeds the
Wasmer runtime. The first version
supports all the required API for the most common usages.
The current benchmarks (a mix from our benchmark suites and from Life suites) show that Wasmer is — on average — 72 times faster than Life and Wagon, the two major existing WebAssembly runtimes in the Go world.
If you want to follow the development, take a look at @wasmerio and @mnt_io on Twitter, or @wasmer@webassembly.social on Mastodon.
And of course, everything is open source at wasmerio/go-ext-wasm.
Thank you for your time, we can’t wait to see what you build with us!