🦥

wasmer で Go の WASM を実行できるパッケージを作った

2021/07/21に公開2

はじめに

WASM (WebAssembly) はブラウザを問わす色々な所で実行が可能になる仮想命令セットおよびアーキテクチャです。
WASM を使う事で、ブラウザでネイティブに近いパフォーマンスのコードを実行できる様になります。既に色々な開発言語から WASM を生成できる様になっています。Go 言語も WASM を生成できる様になっています。

WASM を実行できる処理系

WASM を実行できる処理系としてはブラウザや、wasmtimeLucetwasm-micro-runtimewasmer 等があります。

wasmer は Rust で実装された WebAssembly ランタイムで、Go 言語からは wasmer-go というバインディングから利用できます。

Go 言語の WASM の残念なところ

しかし Go 言語の WASM は、Rust や他の言語の様に関数を直接呼び出す事ができないのです。

#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
  a + b
}

残念な事に、Go 言語で実装された wasmer-go は Go 言語で生成した WASM を実行できないという事になります。これは Go 言語がランタイムを必要とする処理系だからです。ブラウザであれば js の力を借りて、Go 言語のランタイムを動かす必要があります。詳しくは Go 言語のリポジトリに含まれる wasm_exec.js のソースを参照して下さい。

Go 言語という処理系は関数が呼び出された後も goroutine を回し続ける必要があり、それはつまりランタイムが必要な言語という話ですから致し方ない話です。これが「GoのWASMがライブラリではなくアプリケーションであること」と言われる理由です。

https://www.kabuku.co.jp/developers/annoying-go-wasm

シンボル公開の仕組み

しかしながら Go 言語が生成する WASM でも、自ら js 側にシンボルをエクスポートする事は可能です。

  
package main

import (
	"syscall/js"
)

func add(this js.Value, args []js.Value) interface{} {
	return js.ValueOf(args[0].Int() + args[1].Int())
}

func main() {
	c := make(chan struct{}, 0)
	js.Global().Set("Add", js.FuncOf(add))
	<-c
}

この呼び出しを行うと、Go 言語の WASM は WASM ランタイムに対して _makeFuncWrapper という命令(Go特有です)を経由してシンボルを公開しようとします。この際、WASM はスタックマシンの命令を実行しており、前述の wasm_exec.js は JS で実装したスタックマシンを処理しています。

無いなら作る

つまりこのスタックマシンを Go 言語で実装してやれば、Go 言語で生成した WASM を Go 言語で直接実行できる事になる訳です。そうと分かれば実装しない理由はありません。wasm_exec.js で実装されているスタックマシンを Go 言語で実装しました。

https://github.com/mattn/gowasmer

この gowasmer はコマンドではなく Go 言語で生成した WASM を動かす為のランタイムを提供するパッケージになっています。gowasmer を使う事で、上記に登場した足し算関数 Add を実装した WASM の実行が以下の様に書く事ができます。

package main

import (
	"fmt"
	"io/ioutil"
	"log"

	gowasmer "github.com/mattn/gowasmer"
)

func main() {
	b, err := ioutil.ReadFile("wasm/wasm-example")
	if err != nil {
		log.Fatal(err)
	}

	inst, err := gowasmer.NewInstance(b)
	if err != nil {
		log.Fatal(err)
	}

	m := inst.Get("Add")
	r := m.(func([]interface{}) interface{})([]interface{}{1, 3})
	fmt.Printf("1 + 3 = %v\n", r)
}

まだ動き出してホヤホヤなので幾つか漏れがあるかもしれませんが、ひとまず足し算や、WASM からランタイムの関数を呼び出すコードにも対応しました。

js.Global().Get("console").Call("log", "Hello, World")

おわりに

Go 言語で実装された WASM ランタイムである wasmer のバインディング wasmer-go を使って、Go 言語で生成された WASM を動かす為に、Go 言語向けの WASM ランタイムを実装した、gowasmer をご紹介しました。(長い!!!)

まだ色々と抜けがある可能性もあるので、ぜひ遊んでバグを見付けたらコントリビュートして下さい。

Discussion