Go1.21で追加されたWASI(WebAssembly System Interface) Preview 1サポートを試してみる
Go1.21で、WebAssembly System Interface(WASI)の実験的サポートされた。
Go Blog
WASIとは
今までも、WebAssembly(wasm)バイナリにコンパイルすることは可能だった。ターゲットOSをJavaScriptの実行環境に、コンパイルアーキテクチャをWasmにすることで、以下のようにjsから呼び出し可能なWasmバイナリにコンパイルできた。
GOOS=js GOARCH=wasm go build -o main.wasm
しかし、Go 1.21で追加されたWASI(WebAssembly System Interface) Preview 1サポートにより、このプロセスがさらに拡張され、強化された。
WASIとは、Wasmをウェブブラウザ以外の環境でも実行できるようにした仕様のこと。
Wasmはポータビリティ・セキュリティの観点からシステムコールなどを提供しておらず、Wasmサンドボックス外にアクセスするにはあらかじめ用意されたAPIを呼び出すしかないようになっている。
そこで、ホストのファイルやネットワークなどの資源に安全にアクセスできるよう、OSごとに異なるAPIを抽象化した仕様がWASI。
なぜウェブブラウザ以外の文脈でWasmなのか?
ここでは主題と外れるため詳細は載せないが、ざっくり以下のような感じ。
-
プロセッサやOSなどに依存せず、しかも通常のアプリケーションのようにファイルやネットワークなどのリソースにアクセス可能な機能を備えつつ、どこでもネイティブコード並みの実行速度で実行できる、ポータブルで扱いやすいアプリケーション表現形式の実現
-
https://www.publickey1.jp/blog/19/webassemblywebwasimozillanodejs.html
試す
Wasmの仕様の標準化を進めている団体Bytecode AlianceからWASIチュートリアルが提供されているので、これをGoに置き換えて試してみる。
チュートリアルはC, Rustでサンプルコードが書かれているので、Goに書き換える。
プログラムの内容としては、コマンドライン引数を2つ受け取り、第一引数で受け取った名前のファイルを開き、第二引数で受け取ったファイルに内容をコピーするというもの。
Goのコードは以下になる。
package main
import (
"fmt"
"io"
"os"
)
func main() {
if len(os.Args) != 3 {
fmt.Fprintf(os.Stderr, "usage: %s <from> <to>\n", os.Args[0])
os.Exit(1)
}
fromFile, err := os.Open(os.Args[1])
if err != nil {
fmt.Fprintf(os.Stderr, "error opening input %s: %v\n", os.Args[1], err)
os.Exit(1)
}
defer fromFile.Close()
toFile, err := os.OpenFile(os.Args[2], os.O_WRONLY|os.O_CREATE, 0660)
if err != nil {
fmt.Fprintf(os.Stderr, "error opening output %s: %v\n", os.Args[2], err)
os.Exit(1)
}
defer toFile.Close()
_, err = io.Copy(toFile, fromFile)
if err != nil {
fmt.Fprintf(os.Stderr, "copy error: %v\n", err)
os.Exit(1)
}
}
コードを書いたら、プログラムで使うファイル(コピーもととなるファイル)を作成しておく。
echo "hello world" > text.txt
まず、従来のGOOS=js
でコンパイルしてみる。
GOOS=js GOARCH=wasm go build -o main.wasm
これをwasmランタイムに渡して実行してみる。
今回、使用するのはチュートリアル通りwasmtime
。
もちろん通らない。GOOS=js
で実行環境にJavaScriptを指定しているので。
$ wasmtime main.wasm test.txt /tmp/somewhere.txt
Error: failed to run main module `main.wasm`
Caused by:
0: failed to instantiate "main.wasm"
1: unknown import: `gojs::runtime.scheduleTimeoutEvent` has not been defined
追加されたGOOS=wasip1
でコンパイルしてみる。
GOOS=wasip1 GOARCH=wasm go build -o main.wasm
実行
$wasmtime main.wasm test.txt /tmp/somewhere.txt
error opening input test.txt: failed to find a pre-opened file descriptor through which "test.txt" could be opened
通らない。
これはなぜかというと、wasmはサンドボックス内で実行されるため。プログラムではコピーに必要なtext.txt
ファイルを探しに行くが、アクセスする権限がないため失敗している。
wasmのセキュリティ設計が垣間見える。
これを動くようにするには、wasmtime
ランタイムに必要なディレクトリにアクセスする機能を使う。
--dir
オプションを渡すことで、ディレクトリをプログラム実行前に事前に開き、プログラムから使用できるディレクトリとして登録できる。
実行
$ wasmtime --dir=. --dir=/tmp main.wasm test.txt /tmp/somewhere.txt
error opening input test.txt: open test.txt: Bad file number
通らない。「Bad file number」?これはチュートリアルにないエラーだ。深掘りしていく。
これを踏んだようだ。
issueコメントより
wasmtime --dir=. does not expose a directory with a path equal to $PWD, it exposes a directory named . pointing to the current working directory, which then defeats the current directory emulation that we do on GOOS=wasip1 (due to WASI not having the concept of working directory).>
wasmtime --dir=.
は$PWD
に等しいパスを持つディレクトリを公開するのではなく、現在の作業ディレクトリを指す.という名前のディレクトリを公開する。
(wasiには作業ディレクトリという概念はないため)
結果
以下のようにすると動く。
$ wasmtime --dir=$PWD --dir=/tmp main.wasm $PWD/test.txt /tmp/somewhere.txt
$ cat /tmp/somewhere.txt
hello world
GoのソースコードをWASI対応のwasmバイナリにコンパイルし、ファイル操作を伴う処理が実際にwasmランタイム上で動作することが確認できた。
詳しくは追っていないが、バグってしまった挙動については解決の方向性を注視していきたい。
wasmtimeの内容と一部方向性が食い違う気がしているので
余談ですが、.プログラムに現在のディレクトリへのアクセスを許可するために上記のパスを使用したことに注意してください。これが必要となるのは、パスから関連機能へのマッピングが libc によって実行されるため、libc は WebAssembly プログラムの一部であり、実際の現在の作業ディレクトリを WebAssembly プログラムに公開しないためです。したがって、フルパスを指定しても機能しません。
サイズ比較
ターゲット | size |
---|---|
GOOS=js | 2239733 |
GOOS=wasip1 | 2197925 |
WASI対応の方がわずかにサイズが小さい。