Open11

Go1.21で追加されたWASI(WebAssembly System Interface) Preview 1サポートを試してみる

sho-hatasho-hata

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をウェブブラウザ以外の環境でも実行できるようにした仕様のこと。
https://wasi.dev/

Wasmはポータビリティ・セキュリティの観点からシステムコールなどを提供しておらず、Wasmサンドボックス外にアクセスするにはあらかじめ用意されたAPIを呼び出すしかないようになっている。
そこで、ホストのファイルやネットワークなどの資源に安全にアクセスできるよう、OSごとに異なるAPIを抽象化した仕様がWASI。

sho-hatasho-hata

なぜウェブブラウザ以外の文脈でWasmなのか?
ここでは主題と外れるため詳細は載せないが、ざっくり以下のような感じ。

  • プロセッサやOSなどに依存せず、しかも通常のアプリケーションのようにファイルやネットワークなどのリソースにアクセス可能な機能を備えつつ、どこでもネイティブコード並みの実行速度で実行できる、ポータブルで扱いやすいアプリケーション表現形式の実現

  • https://www.publickey1.jp/blog/19/webassemblywebwasimozillanodejs.html

sho-hatasho-hata

チュートリアルは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
sho-hatasho-hata

まず、従来のGOOS=jsでコンパイルしてみる。

GOOS=js GOARCH=wasm go build -o main.wasm

これをwasmランタイムに渡して実行してみる。
今回、使用するのはチュートリアル通りwasmtime

https://wasmtime.dev/

もちろん通らない。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
sho-hatasho-hata

追加された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」?これはチュートリアルにないエラーだ。深掘りしていく。

sho-hatasho-hata

これを踏んだようだ。

https://github.com/golang/go/issues/60732

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には作業ディレクトリという概念はないため)

sho-hatasho-hata

結果

以下のようにすると動く。

$ wasmtime --dir=$PWD --dir=/tmp main.wasm $PWD/test.txt /tmp/somewhere.txt    
$ cat /tmp/somewhere.txt   
hello world

GoのソースコードをWASI対応のwasmバイナリにコンパイルし、ファイル操作を伴う処理が実際にwasmランタイム上で動作することが確認できた。

sho-hatasho-hata

詳しくは追っていないが、バグってしまった挙動については解決の方向性を注視していきたい。

wasmtimeの内容と一部方向性が食い違う気がしているので
https://github.com/bytecodealliance/wasmtime/blob/main/docs/WASI-tutorial.md#executing-in-wasmtime-runtime:~:text=余談ですが、.プログラムに現在のディレクトリへのアクセスを許可するために上記のパスを使用したことに注意してください。これが必要となるのは、パスから関連機能へのマッピングが libc によって実行されるため、libc は WebAssembly プログラムの一部であり、実際の現在の作業ディレクトリを WebAssembly プログラムに公開しないためです。したがって、フルパスを指定しても機能しません。

余談ですが、.プログラムに現在のディレクトリへのアクセスを許可するために上記のパスを使用したことに注意してください。これが必要となるのは、パスから関連機能へのマッピングが libc によって実行されるため、libc は WebAssembly プログラムの一部であり、実際の現在の作業ディレクトリを WebAssembly プログラムに公開しないためです。したがって、フルパスを指定しても機能しません。

sho-hatasho-hata

サイズ比較

ターゲット size
GOOS=js 2239733
GOOS=wasip1 2197925

WASI対応の方がわずかにサイズが小さい。