Paul Sitoh

I'm a software engineer who enjoys coding principally in Go and where necessary other languages.

Embedding WebAssembly in Go app

24 Aug 2021 » go, webassembly

About this post

Imagine you are writing an app in Go and found a library with all the APIs you need. You will like to integrate it into your app. However, it is not written in Go. What options are available to you?

The options for you depends on the kind of language that was the basis of your library. If it was written in C/C++ you could use cgo. You could also use gRPC/protobuf or if you search the internet, you might even find some custom solutions.

In this post, I am going to describe a way to do it via WebAssembly, a tool call wasmer-go and using Go file embedding (version 1.16 onwards) capabilities.

WebAssembly

Here is the official definition of the technology:

WebAssembly (abbreviated Wasm) is a binary instruction format for a stack-based virtual machine. Wasm is designed as a portable compilation target for programming languages, enabling deployment on the web for client and server applications.

I won’t go into the details of the inner workings of Wasm. Please refer to the official document. In the context of this post, we could think of Wasm as a byte code that can be executed by a Wasm runtime engine; not too dissimilar to Java Virtual Machine (JVM), up to a point.

The source code of a Wasm is raw text known as an assembly code. To illustrate a concept, we have a simple math module, written in Wasm assembly to perform some arithmetic operations: 1) add the two integer values, 2) multiply the sum by 10, and 3) export a method name getAns for a calling application to get results. Our Wasm aseembly is as follows:

(module
    (func $add (param $lhs i32) (param $rhs i32) (result i32)
        local.get $lhs
        local.get $rhs
        i32.add)
    (func $mul (param $value i32) (result i32)
        local.get $value
        i32.const 10
        i32.mul
    )
    (func (export "getAns") (param $lhs i32) (param $rhs i32) (result i32)
        local.get $lhs
        local.get $rhs
        call $add
        call $mul)
)

For any practical applications, you will not likely be writing Wasm in assembly form. Chances are you would be compiling from a programming language like C/C++, Rust, Go and more. This is beyond the scope of this post and I suggest you refer to the Wasm official document for details.

The Wasm assembly as shown above will need to be compiled into byte codes. Let’s assumed that the assembly code is in a file name math.wat. You will run the command, wat2wasm math.wat -o math.wasm which will convert the assembly code into the following byte code found in the file math.wasm:

0000000: 0061 736d                                 ; WASM_BINARY_MAGIC
0000004: 0100 0000                                 ; WASM_BINARY_VERSION
; section "Type" (1)
0000008: 01                                        ; section code
0000009: 00                                        ; section size (guess)
000000a: 02                                        ; num types
; func type 0
000000b: 60                                        ; func
000000c: 02                                        ; num params
000000d: 7f                                        ; i32
000000e: 7f                                        ; i32
000000f: 01                                        ; num results
0000010: 7f                                        ; i32
; func type 1
0000011: 60                                        ; func
0000012: 01                                        ; num params
0000013: 7f                                        ; i32
0000014: 01                                        ; num results
0000015: 7f                                        ; i32
0000009: 0c                                        ; FIXUP section size
; section "Function" (3)
0000016: 03                                        ; section code
0000017: 00                                        ; section size (guess)
0000018: 03                                        ; num functions
0000019: 00                                        ; function 0 signature index
000001a: 01                                        ; function 1 signature index
000001b: 00                                        ; function 2 signature index
0000017: 04                                        ; FIXUP section size
; section "Export" (7)
000001c: 07                                        ; section code
000001d: 00                                        ; section size (guess)
000001e: 01                                        ; num exports
000001f: 06                                        ; string length
0000020: 6765 7441 6e73                           getAns  ; export name
0000026: 00                                        ; export kind
0000027: 02                                        ; export func index
000001d: 0a                                        ; FIXUP section size
; section "Code" (10)
0000028: 0a                                        ; section code
0000029: 00                                        ; section size (guess)
000002a: 03                                        ; num functions
; function body 0
000002b: 00                                        ; func body size (guess)
000002c: 00                                        ; local decl count
000002d: 20                                        ; local.get
000002e: 00                                        ; local index
000002f: 20                                        ; local.get
0000030: 01                                        ; local index
0000031: 6a                                        ; i32.add
0000032: 0b                                        ; end
000002b: 07                                        ; FIXUP func body size
; function body 1
0000033: 00                                        ; func body size (guess)
0000034: 00                                        ; local decl count
0000035: 20                                        ; local.get
0000036: 00                                        ; local index
0000037: 41                                        ; i32.const
0000038: 0a                                        ; i32 literal
0000039: 6c                                        ; i32.mul
000003a: 0b                                        ; end
0000033: 07                                        ; FIXUP func body size
; function body 2
000003b: 00                                        ; func body size (guess)
000003c: 00                                        ; local decl count
000003d: 20                                        ; local.get
000003e: 00                                        ; local index
000003f: 20                                        ; local.get
0000040: 01                                        ; local index
0000041: 10                                        ; call
0000042: 00                                        ; function index
0000043: 10                                        ; call
0000044: 01                                        ; function index
0000045: 0b                                        ; end
000003b: 0a                                        ; FIXUP func body size
0000029: 1c                                        ; FIXUP section size

Integrating into our Go app

Wasm byte code is typically executed in a browser based or Javascript runtime engine. An example of a Wasm embedded in NodeJS is as shown here:

require("webassembly")
    .load("./pub/math.wasm")
    .then(module => {
        console.log(module.exports.add(1,2));
    });

To get Wasm byte code to run in Go, you can use this tool call wasmer-go and it is found in this repository https://github.com/wasmerio/wasmer-go.

In our project for this post, our goal is to create a simple native app. Our implementation is as follows:

package main

import (
	"embed"
	"fmt"

	wasmer "github.com/wasmerio/wasmer-go/wasmer"
)

//go:embed multi.wasm
var f embed.FS

func main() {
	wasmBytes, _ := f.ReadFile("multi.wasm")

	engine := wasmer.NewEngine()
	store := wasmer.NewStore(engine)

	// Compiles the module
	module, _ := wasmer.NewModule(store, wasmBytes)

	// Instantiates the module
	importObject := wasmer.NewImportObject()
	instance, _ := wasmer.NewInstance(module, importObject)

	// Gets the `add` exported function from the WebAssembly instance.
	getAns, _ := instance.Exports.GetFunction("getAns")

	// Calls that exported function with Go standard values. The WebAssembly
	// types are inferred and values are casted automatically.
	result, _ := getAns(1, 1)

	fmt.Println(result) // 42!
}

When you build this code – i.e. go build – it will generate a executable with the math.wasm file encoded.

Through this approach, you now have the ability to integrate library written in other languages.

Summing up

To sum it up, if you elect to integrate a library using other languages via Wasm into your Go app, the steps are:

STEP 1: Generate a Wasm assembly and/or byte code using the tools from the offical source or third parties tool.

STEP 2: Embed the *.wasm or byte code file into your app (Go v1.16 onwards use the embed package or appropriate tool for earlier version).

STEP 3: Use the wasmer-go module as the basis of your Wasm runtime.

STEP 4: Build your app as per the Go tools.

Having learnt this approach to integrating Go app with non-Go libraries, you may feel that Wasm is the way to go, pardon the pun. For that, I shall leave it to you to decide.

From my perspective, I see great potential for Wasm as a basis for a Smart Contract for multiple blockchain protocol but it is a topic I hope to share in future posts after my experimentation.