Wasmer: the fastest Go library for executing WebAssembly code

Original author: Ivan Enderlin
  • Transfer
WebAssembly (wasm) is a portable binary instruction format. The same code wasm code can be executed in any environment. In order to support this statement, every language, platform, and system must be able to execute such code, making it as fast and safe as possible. Wasmer is a wasm runtime written in Rust . Obviously, wasmer can be used in any Rust application. The author of the material, the translation of which we publish today, says that he and other participants in the Wasmer project successfully implemented this wasm-code runtime in other languages:





Here we will talk about a new project - go-ext-wasm , which is a library for Go, designed to execute binary wasm-code. As it turned out, the go-ext-wasm project is much faster than other similar solutions. But let's not get ahead of ourselves. Let's start with a story about how to work with him.

Calling wasm functions from Go


To get started, install wasmer in a Go environment (with cgo support).

export CGO_ENABLED=1; export CC=gcc; go install github.com/wasmerio/go-ext-wasm/wasmer

The go-ext-wasm project is a regular Go library. When working with this library, a construction is used import "github.com/wasmerio/go-ext-wasm/wasmer".

Now let's get to practice. We will write a simple program that compiles in wasm. We will use for this, for example, Rust:

#[no_mangle]
pub extern fn sum(x: i32, y: i32) -> i32 {
    x + y
}

We will name the file with the program simple.rs, as a result of compilation of this program we get the file simple.wasm .

The following program, written in Go, performs a function sumfrom the wasm file, passing it the numbers 5 and 37 as arguments:

package main
import (
    "fmt"
    wasm "github.com/wasmerio/go-ext-wasm/wasmer"
)
func main() {
    // Чтение модуля WebAssembly.
    bytes, _ := wasm.ReadBytes("simple.wasm")
    // Создание экземпляра модуля WebAssembly.
    instance, _ := wasm.NewInstance(bytes)
    defer instance.Close()
    // Получение экспортированной функции `sum` из экземпляра WebAssembly.
    sum := instance.Exports["sum"]
    // Вызов экспортированной функции с использовании стандартных значений Go.
    // Преобразование типов данных, передаваемых функции и получаемых из неё, выполняется автоматически.
    result, _ := sum(5, 37)
    fmt.Println(result) // 42!
}

Here, a program written in Go calls a function from a wasm file that was obtained by compiling code written in Rust.

So, the experiment was a success, we successfully executed the WebAssembly code in Go. It should be noted that data type conversion is automated. Those Go values ​​that are passed to the wasm code are cast to WebAssembly types. What the wasm function returns is cast to Go types. As a result, working with functions from wasm files in Go looks the same as working with regular Go functions.

Call Go Functions from WebAssembly Code


As we saw in the previous example, WebAssembly modules are able to export functions that can be called from outside. This is the mechanism that allows wasm code to be executed in various environments.

At the same time, WebAssembly modules themselves can work with imported functions. Consider the following program written in Rust.

extern {
    fn sum(x: i32, y: i32) -> i32;
}
#[no_mangle]
pub extern fn add1(x: i32, y: i32) -> i32 {
    unsafe { sum(x, y) } + 1
}

Let's name the file with it import.rs. Compiling it into WebAssembly will result in code that can be found here .

The exported function add1calls the function sum. There is no implementation of this function, only its signature is defined in the file. This is the so-called extern function. For WebAssembly, this is an imported function. Its implementation must be imported.

We implement the function sumusing Go. For this we need to use cgo . Here is the resulting code. Some comments, which are descriptions of the main code fragments, are numbered. Below we will talk about them in more detail.

package main
// // 1. Объявляем сигнатуру функции `sum` (обратите внимание на cgo).
//
// #include 
//
// 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. Пишем реализацию функции `sum` и экспортируем её (для cgo).
//export sum
func sum(context unsafe.Pointer, x int32, y int32) int32 {
    return x + y
}
func main() {
    // Чтение модуля WebAssembly.
    bytes, _ := wasm.ReadBytes("import.wasm")
    // 3. Объявление импортированной функции для WebAssembly.
    imports, _ := wasm.NewImports().Append("sum", sum, C.sum)
    // 4. Создание экземпляра модуля  WebAssembly с импортами.
    instance, _ := wasm.NewInstanceWithImports(bytes, imports)
    // Позже закроем экземпляр WebAssembly.
    defer instance.Close()
    // Получение экспортированной функции `add1` из экземпляра WebAssembly.
    add1 := instance.Exports["add1"]
    // Вызов экспортированной функции.
    result, _ := add1(1, 2)
    fmt.Println(result)
    //   add1(1, 2)
    // = sum(1 + 2) + 1
    // = 1 + 2 + 1
    // = 4
    // QED
}

Let's parse this code:

  1. The signature of the function is sumdefined in C (see the comment above the command import "C").
  2. The implementation of the function is sumdefined in Go (pay attention to the line //export- cgo uses this mechanism to establish the connection of code written in Go with code written in C).
  3. NewImportsIs the API used to create WebAssembly imports. In this code "sum", this is the name of the function imported by WebAssembly, sumis the pointer to the Go function, and C.sumis the pointer to the cgo function.
  4. And finally, NewInstanceWithImportsthis is a constructor designed to initialize a WebAssembly module with imports.

Reading data from memory


The WebAssembly instance has linear memory. Let's talk about how to read data from it. Let's start, as usual, with the Rust code, which we will name memory.rs.

#[no_mangle]
pub extern fn return_hello() -> *const u8 {
    b"Hello, World!\0".as_ptr()
}

The result of compiling this code is in the file memory.wasmthat is used below.

The function return_helloreturns a pointer to a string. The line ends, as in C, with a null character.

Now go to the Go side:

bytes, _ := wasm.ReadBytes("memory.wasm")
instance, _ := wasm.NewInstance(bytes)
defer instance.Close()
// Вызов экспортированной функции `return_hello`.
// Эта функция возвращает указатель на строку.
result, _ := instance.Exports["return_hello"]()
// Значение указателя рассматривается как целое число.
pointer := result.ToI32()
// Чтение данных из памяти.
memory := instance.Memory.Data()
fmt.Println(string(memory[pointer : pointer+13])) // Hello, World!

The function return_helloreturns a pointer as a value i32. We get this value by calling ToI32. Then we get the data from memory with instance.Memory.Data().

This function returns the memory slice of the WebAssembly instance. You can use it like any Go slice.

Fortunately, we know the length of the line that we want to read, so to read the necessary information, it is enough to use the construction memory[pointer : pointer+13]. Then the read data is converted to a string.

Here's an example that shows more advanced memory mechanisms when using Go's WebAssembly code.

Benchmarks


The go-ext-wasm project, as we have just seen, has a convenient API. Now it's time to talk about its performance.

Unlike PHP or Ruby, the Go world already has solutions for working with wasm code. In particular, we are talking about the following projects:

  • Life from Perlin Network - WebAssembly interpreter.
  • Go Interpreter's Wagon is a WebAssembly interpreter and toolkit.

The material on the php-ext-wasm project used the n-body algorithm to study performance . There are many other algorithms suitable for examining the performance of code execution environments. For example, this is the Fibonacci algorithm (recursive version) and the Pollard ρ-algorithm used in Life. This is the Snappy compression algorithm. The latter works successfully with go-ext-wasm, but not with Life or Wagon. As a result, he was removed from the test set. Test code can be found here .

During the tests, the latest versions of the research projects were used. Namely, these are Life 20190521143330-57f3819c2df0 and Wagon 0.4.0.

The numbers shown on the chart reflect the average values ​​obtained after 10 starts of the test. The study used a 2016 MacBook Pro 15 "with 2.9 GHz Intel Core i7 processor and 16 GB of memory.

The test results are grouped along the X axis according to the types of tests. The Y axis shows the time in milliseconds required to complete the test. The lower the indicator - . better


performance comparing Wasmer, Wagon and Life by implementing various algorithms

Platform for Life and Wagon, on average, give similar results Wasmer same, on average, 72 times faster..

it is important to note that Wasmer supports three backend: SinglePass , Cranelift and LLVM. The default backend in the Go library is Cranelift ( here you can find out more about it). Using LLVM will give performance close to native, but it was decided to start with Cranelift, since this backend gives the best ratio between compilation time and program execution time.

Here you can read about different backends, their pros and cons, and in which situations it is better to use them.

Summary


The open source project go-ext-wasm is a new Go library designed to execute binary wasm code. It includes a Wasmer runtime . Its first version includes APIs, the need for which arises most often.
Performance tests showed that Wasmer, on average, is 72 times faster than Life and Wagon.

Dear readers! Do you plan to use the ability to run wasm code in Go using go-ext-wasm?


Also popular now: