TinyGo で WASI 【失敗編】
前回は Go および TinyGo を使って WebAssembly コードを生成しブラウザ上で実行するところまでやった。
しかし,クライアント側のブラウザ上で動かすだけではあまり面白くないよね。 そこで WASI (WebAssembly System Interface) という POSIX 風の標準規格があるそうな。 WASI に則った WebAssembly コードと,それを駆動するランタイム環境を用意することで “Write Once, Run Anywhere” の夢よもう一度,というわけ1(笑)
実は本家 Go の wasm
アーキテクチャは WASI に対応していない。
ただし TinyGo のほうはイケるみたいなので,今回は TinyGo オンリーでお送りする。
WASI ランタイム
スタンドアロンで動く WASI ランタイムには色々あるようで
といった実装があるらしい。
ただ TinyGo のターゲット定義が
{
"llvm-target": "wasm32--wasi",
"build-tags": ["wasm", "wasi"],
"goos": "linux",
"goarch": "arm",
"compiler": "clang",
"linker": "wasm-ld",
"libc": "wasi-libc",
"cflags": [
"--target=wasm32--wasi",
"--sysroot={root}/lib/wasi-libc/sysroot",
"-Oz"
],
"ldflags": [
"--allow-undefined",
"--stack-first",
"--export-dynamic",
"--no-demangle"
],
"emulator": ["wasmtime"],
"wasm-abi": "generic"
}
と Wasmtime をリファレンスとしているみたいなので,今回はこれを使う。
Wasmtime の導入
Wasmtime のリポジトリでバイナリがリリースされているので,これを取ってきて PATH の通ったディレクトリに放り込んでおけばよい。
あるいは
$ curl https://wasmtime.dev/install.sh -sSf | bash
とすれば $HOME/.wasmtime/bin/
ディレクトリを掘って入れてくれる。
さらに PATH を通すために $HOME/.bashrc
ファイルを書き換えてくれやがるので,ご注意を。
なお Wasmtime 自体のビルドには Rust と C++ (多分 GCC の g++) のビルド環境が必要らしい。 時代は Rust なんだねぇ。
以下,動作確認。
$ wasmtime --help
wasmtime 0.25.0
Wasmtime WebAssembly Runtime
USAGE:
wasmtime <SUBCOMMAND>
FLAGS:
-h, --help Prints help information
-V, --version Prints version information
SUBCOMMANDS:
config Controls Wasmtime configuration settings
help Prints this message or the help of the given subcommand(s)
run Runs a WebAssembly module
wasm2obj Translates a WebAssembly module to native object file
wast Runs a WebAssembly test script file
If a subcommand is not provided, the `run` subcommand will be used.
Usage examples:
Running a WebAssembly module with a start function:
wasmtime example.wasm
Passing command line arguments to a WebAssembly module:
wasmtime example.wasm arg1 arg2 arg3
Invoking a specific function (e.g. `add`) in a WebAssembly module:
wasmtime example.wasm --invoke add 1 2
みんな大好き Hello World
何はともあれ,コードを用意しないとね。 いつものように,みんな大好き Hello World で。
package main
import "fmt"
func main() {
fmt.Println("Hello, World!")
}
これを TinyGo で処理する。
$ tinygo build -o hello.wasm -target wasi ./hello.go
ターゲットが wasi
になっている点に注意。
Wasmtime で WASI コードを動かす
んではビルドした hello.wasm
ファイルを実行してみる。
$ wasmtime run hello.wasm
Hello, World!
よーし,うむうむ,よーし。
wasmtime-go で WASI ランタイムを組み込む【失敗編】
bytecodealliance/wasmtime-go を使うと Wasmtime のランタイム機能を Go のコードとして埋め込めるらしい(要 cgo)。 こんな感じかな。
package main
import (
_ "embed"
"fmt"
"os"
"github.com/bytecodealliance/wasmtime-go"
)
//go:embed hello.wasm
var wasm []byte
func main() {
store := wasmtime.NewStore(wasmtime.NewEngine())
wasiConfig := wasmtime.NewWasiConfig()
wasiConfig.InheritStdout()
wasi, err := wasmtime.NewWasiInstance(store, wasiConfig, "wasi_snapshot_preview1")
if err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in wasmtime.NewWasiInstance() : %w", err))
return
}
linker := wasmtime.NewLinker(store)
if err := linker.DefineWasi(wasi); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in wasmtime.Linker.DefineWasi() : %w", err))
return
}
if err := wasmtime.ModuleValidate(store, wasm); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in wasmtime.ModuleValidate() : %w", err))
return
}
module, err := wasmtime.NewModule(store.Engine, wasm)
if err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in wasmtime.NewModule() : %w", err))
return
}
instance, err := linker.Instantiate(module)
if err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in wasmtime.Linker.Instantiate() : %w", err))
return
}
if _, err := instance.GetExport("_start").Func().Call(); err != nil {
fmt.Fprintln(os.Stderr, fmt.Errorf("error in \"_start\" : %w", err))
return
}
}
では,これを動かしてみよう。
$ go run sample.go
error in wasmtime.Linker.Instantiate() : unknown import: `wasi_unstable::fd_write` has not been defined
おうふ。 なんか足らんと言っている。
TinyGo 側でなにか不備があるのかと思って以下のサンプル・コードもそのまま動かしてみたが
package main
import (
"fmt"
"io/ioutil"
"os"
"path/filepath"
"github.com/bytecodealliance/wasmtime-go"
)
const TextWat = `(module
;; Import the required fd_write WASI function which will write the given io vectors to stdout
;; The function signature for fd_write is:
;; (File Descriptor, *iovs, iovs_len, nwritten) -> Returns number of bytes written
(import "wasi_unstable" "fd_write" (func $fd_write (param i32 i32 i32 i32) (result i32)))
(memory 1)
(export "memory" (memory 0))
;; Write 'hello world\n' to memory at an offset of 8 bytes
;; Note the trailing newline which is required for the text to appear
(data (i32.const 8) "hello world\n")
(func $main (export "_start")
;; Creating a new io vector within linear memory
(i32.store (i32.const 0) (i32.const 8)) ;; iov.iov_base - This is a pointer to the start of the 'hello world\n' string
(i32.store (i32.const 4) (i32.const 12)) ;; iov.iov_len - The length of the 'hello world\n' string
(call $fd_write
(i32.const 1) ;; file_descriptor - 1 for stdout
(i32.const 0) ;; *iovs - The pointer to the iov array, which is stored at memory location 0
(i32.const 1) ;; iovs_len - We're printing 1 string stored in an iov - so one.
(i32.const 20) ;; nwritten - A place in memory to store the number of bytes written
)
drop ;; Discard the number of bytes written from the top of the stack
)
)`
func check(e error) {
if e != nil {
panic(e)
}
}
func main() {
dir, err := ioutil.TempDir("", "out")
check(err)
defer os.RemoveAll(dir)
stdoutPath := filepath.Join(dir, "stdout")
engine := wasmtime.NewEngine()
store := wasmtime.NewStore(engine)
linker := wasmtime.NewLinker(store)
// Configure WASI imports to write stdout into a file.
wasiConfig := wasmtime.NewWasiConfig()
wasiConfig.SetStdoutFile(stdoutPath)
// Set the version to the same as in the WAT.
wasi, err := wasmtime.NewWasiInstance(store, wasiConfig, "wasi_snapshot_preview1")
check(err)
// Link WASI
err = linker.DefineWasi(wasi)
check(err)
// Create our module
wasm, err := wasmtime.Wat2Wasm(TextWat)
check(err)
module, err := wasmtime.NewModule(store.Engine, wasm)
check(err)
instance, err := linker.Instantiate(module)
check(err)
// Run the function
nom := instance.GetExport("_start").Func()
_, err = nom.Call()
check(err)
// Print WASM stdout
out, err := ioutil.ReadFile(stdoutPath)
check(err)
fmt.Print(string(out))
}
結果は同じで wasi_unstable::fd_write
なんぞ知らんと言ってくさる。
えっ? みんなこのサンプルコード動かせるの? どうやんだ? 多分ランタイム側で何か足らないんだろうけど,よく分からん。
wasmtime-c-api
を組み込めばいいのかなと思ったが,違うよなぁ?
というところで挫折した orz
どなたか教えてください 🙇
【2021-03-22 追記】
Twitter で教えていただきました。 感謝!
どうも TinyGo と wasmtime-go との間で WASI Application ABI (Application Binary Interface) が マッチしていない模様。
たしかに $TINYGOROOT/src/runtime/runtime_wasm.go
に
//go:wasm-module wasi_unstable
//export fd_write
func fd_write(id uint32, iovs *__wasi_iovec_t, iovs_len uint, nwritten *uint) (errno uint)
って記述があるわ。
ふむむー。
//go:wasm-module
ディレクティブをキーに調べてみればいいのかな。
参考になった。
ちなみに,アドバイスを参考に
wasi, err := wasmtime.NewWasiInstance(store, wasiConfig, "wasi_snapshot_preview1")
の部分を
wasi, err := wasmtime.NewWasiInstance(store, wasiConfig, "wasi_unstable")
に差し替えたら動き出した。 なるほどねー。
TinyGo 側の PR は受理されてマージされているようなので,次のバージョンでは wasi_snapshot_preview1
で行けるだろう。
【おまけ】 Node.js で WASI を動かす
Node.js は v13 から WASI に対応しているらしい。
$ npm i wasi
でパッケージを組み込めば使えるようだ。 で,こんな感じのコードを書いて
'use strict';
const fs = require('fs');
const { WASI } = require('wasi');
const wasi = new WASI({
args: process.argv,
env: process.env,
preopens: {
}
});
const importObject = { wasi_unstable: wasi.wasiImport };
// const importObject = { wasi_snapshot_preview1: wasi.wasiImport };
(async () => {
try {
const wasm = await WebAssembly.compile(fs.readFileSync('./hello.wasm'));
const instance = await WebAssembly.instantiate(wasm, importObject);
wasi.start(instance);
} catch (e) {
console.error(e)
}
})();
動かしてみると
$ node --experimental-wasi-unstable-preview1 --experimental-wasm-bigint wasi.js
(node:210549) ExperimentalWarning: WASI is an experimental feature. This feature could change at any time
(Use `node --trace-warnings ...` to show where the warning was created)
Hello, World!
おー,動いた動いた。 これで Go のコードを WSAI 経由で JavaScript コードに埋め込めるわけだ。
ブックマーク
- WASI - WebAssembly System Interface with Wasmtime - DEV Community
- コンテナ技術を捨て、 WASIを試す. こんにちは、NTTの藤田です。 | by FUJITA Tomonori | nttlabs | Medium
- TinyGo の開発版のビルド方法と、ビルドせずに開発版バイナリを手に入れる方法 - Qiita
参考図書
- ソフトウェアデザイン 2021年3月号
- 谷本 心 (著), 水島 宏太 (著), 増田 亨 (著), 山本 悠滋 (著), 折原 レオナルド賢 (著), 米田 武 (著), 清水 洋治 (著), 結城 浩 (著), 刀根 諒 (著), 大串 肇 (著), 松本 直人 (著), クラスメソッド 木村(作) (著), エクスデザイン ninnzinn(画) (著), くつなりょうすけ (著), 広木 大地 (著), 中島 明日香 (著), 金谷 拓哉 (著), 高橋 永成 (著), 平岡 正寿 (著), 梶原 直人(監修) (著), 平櫛 貴章 (著), 星川 真麻 (著), けんちょん(大槻 兼資) (著), 大嶋 健容 (著), 職業「戸倉彩」 (著), mattn (著), 小野 輝也 (著), 濱田 康貴 (著), 森若 和雄 (著), 古川 菜摘 (著), 嘉山 陽一 (著), 平野 尚志 (著), 杉山 貴章 (著), Software Design編集部 (編集)
- 技術評論社 2021-02-18 (Release 2021-02-18)
- 雑誌
- B08T7D2LFR (ASIN), 4910058270316 (EAN)
- 評価
第2特集が「WebAssembly 入門」近年の動向を把握するには丁度いいだろう。
- プログラミング言語Go (ADDISON-WESLEY PROFESSIONAL COMPUTING SERIES)
- Alan A.A. Donovan (著), Brian W. Kernighan (著), 柴田 芳樹 (翻訳)
- 丸善出版 2016-06-20
- 単行本(ソフトカバー)
- 4621300253 (ASIN), 9784621300251 (EAN), 4621300253 (ISBN), 9784621300251 (ISBN)
- 評価
著者のひとりは(あの「バイブル」とも呼ばれる)通称 “K&R” の K のほうである。この本は Go 言語の教科書と言ってもいいだろう。
-
“Write Once, Run Anywhere” は初期の Java のキャッチフレーズだった。当時は UNIX 機のハードウェア非互換の問題が酷くて,なんとかバイナリ互換を確保する方法がないかみんな頭を悩ませていた。そこに登場したのが Sun Microsystems の Java だったわけ。でも実際にはプラットフォーム間の差異が微妙に残ってしまい,むしろ “Write Once, Debug Everywhere” などと揶揄されることもあった。それでも Virtual Machine 上で標準化されたバイトコードを駆動させるというアイデアは秀逸だったので Java 以外の処理系でも応用され,特に組み込み用途では重宝されている。 ↩︎