モチベーション
WebAssemblyが面白そうだと思いつつ、HelloWorldだと味気ないし、SQLiteを移植した、というようなものだと複雑すぎるということで、少し複雑なGoのプログラムを、ブラウザで動かすように変更してみて、雰囲気を掴みたいと考えました。
Goのプログラムを書く
文字でHelloWorldのサンプルは味気ないということで、今回選んだのはGoで画像を生成するプログラムです。
以下の記事のプログラムをほぼそのまま利用しています(以下の記事では日本語を出すための内容が書かれていますが、その部分は今回はスルーします)
package main
import (
"bytes"
"fmt"
"image"
"image/png"
"os"
"github.com/golang/freetype/truetype"
"golang.org/x/image/font"
"golang.org/x/image/font/gofont/gobold"
"golang.org/x/image/math/fixed"
)
func main() {
ft, err := truetype.Parse(gobold.TTF)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
opt := truetype.Options{
Size: 90,
DPI: 0,
Hinting: 0,
GlyphCacheEntries: 0,
SubPixelsX: 0,
SubPixelsY: 0,
}
imageWidth := 100
imageHeight := 100
textTopMargin := 90
text := "A"
img := image.NewRGBA(image.Rect(0, 0, imageWidth, imageHeight))
face := truetype.NewFace(ft, &opt)
dr := &font.Drawer{
Dst: img,
Src: image.Black,
Face: face,
Dot: fixed.Point26_6{},
}
dr.Dot.X = (fixed.I(imageWidth) - dr.MeasureString(text)) / 2
dr.Dot.Y = fixed.I(textTopMargin)
dr.DrawString(text)
buf := &bytes.Buffer{}
err = png.Encode(buf, img)
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
//file, err := os.Create(`test.png`) //ここを変更
if err != nil {
fmt.Fprintln(os.Stderr, err)
os.Exit(1)
}
//defer file.Close() //ここを変更
//file.Write(buf.Bytes()) // ここを変更
os.Stdout.Write(buf.Bytes()) //ここを追加
}
ファイル書き出し部分を少し変更し、生成した画像をファイルに書き出すのではなく標準出力に書き出すようにしました。
これはGoのWebAssemblyのインターフェースをシンプルにするためです。
ビルド
ここからは以下の記事を参考にします
goのオプションでWASMを出力するようにして、ビルドします。
# 初回のみ
$ go mod init github.com/inajob/go-image-wasm
$ go mod tidy
# ビルド
$ GOOS=js GOARCH=wasm go build -o main.wasm main.go
これでmain.wasmが生成されました
ブラウザ部分を作る
GoでビルドしたWASMのバイナリを実行するためには、Goの公式で配布しているローダーである以下をダウンロードします
さらに、これらを呼び出すためのindex.htmlを用意します
<html>
<body>
<script src="./wasm_exec.js"></script>
<script>
const go = new Go();
let mod, inst;
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("run").disabled = false;
});
async function run() {
console.clear();
await go.run(inst);
}
</script>
<button onClick="run();" id="run" disabled>Run</button>
</body>
</html>
まずはこんな感じのHTMLを用意します。
上記ページを参考にしただけなので、正直なところそれぞれの行の厳密な動作は追えてないのですが、サーバ上のwasmファイルを読み込んできて、WebAssemblyオブジェクトに渡しているようです。
ここで出てくるGoというオブジェクトはwasm_exec.jsの中で定義されているGoのローダーです。
これを実行するためには、何かしらのWebサーバ必要なので、参考ページのマネをしてサクッとGoで書きます。
package main
import (
"log"
"net/http"
)
func main() {
http.Handle("/", http.FileServer(http.Dir("")))
log.Fatal(http.ListenAndServe(":8080", nil))
}
go run server.go
でWebサーバを実行し、ブラウザからアクセスします。
ブラウザに表示されたボタンをクリックすると、WebAssemblyによりGoで書いたプログラムが実行され、デバッグコンソールに以下のような出力が出ます。
標準出力に出したPNGのバイナリデータのようです。
この出力処理はwasm_exec.jsの中のwrite, writeSyncという関数で行われています。ここには標準出力に出そうとする文字列をコンソールに出力する処理が実装されています。
if (!globalThis.fs) {
let outputBuf = "";
globalThis.fs = {
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputBuf += decoder.decode(buf);
const nl = outputBuf.lastIndexOf("\n");
if (nl != -1) {
console.log(outputBuf.substring(0, nl));
outputBuf = outputBuf.substring(nl + 1);
}
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
if (offset !== 0 || length !== buf.length || position !== null) {
callback(enosys());
return;
}
const n = this.writeSync(fd, buf);
callback(null, n);
},
この部分をうまく乗っ取ることができれば、コンソールに出てきたこのバイナリをJavaScriptから操作できそうです
※Goのプログラムの中にOpenの処理が入っているとここでopen test.png: not implemented on js
というエラーが出ます。
丁度上に貼り付けたソースコードの一番上を見るとglobalにfs
が定義されていればそちらを使うようになっているようなので、それを踏まえて先程のHTMLを修正します
<html>
<body>
<script src="./wasm_exec.js"></script>
<script>
const decoder = new TextDecoder("utf-8");
outputArray = []
globalThis.fs = { // オリジナルの処理を用意
constants: { O_WRONLY: -1, O_RDWR: -1, O_CREAT: -1, O_TRUNC: -1, O_APPEND: -1, O_EXCL: -1 }, // unused
writeSync(fd, buf) {
outputArray.push(buf)
return buf.length;
},
write(fd, buf, offset, length, position, callback) {
const n = this.writeSync(fd, buf);
callback(null, n);
},
}
const go = new Go();
let mod, instance;
WebAssembly.instantiateStreaming(fetch("main.wasm"), go.importObject).then((result) => {
mod = result.module;
inst = result.instance;
document.getElementById("run").disabled = false;
});
async function run() {
console.clear();
await go.run(inst);
img = document.createElement("img")
img.src = "data:image/png;base64," + btoa(String.fromCharCode(...outputArray[0]))
document.body.appendChild(img)
}
</script>
<button onClick="run();" id="run" disabled>Run</button>
</body>
</html>
画像をbase64に変換してData URL スキームにすることでWebページ上にGoで生成した画像を表示できました。
ここで出てくるwriteって何?
これはGoのWebAssemblyビルドがブラウザとのインターフェースとして用意した関数です。
これはシステムコールをもとにしたもののようですが、現在WebAssembly界隈で策定が進んでいるWASIなどに置き換わっていくのでは?と思っています
それっぽいGoのIssueはこちら↓
またWASIをブラウザで動かすためのpolyfillの実装もあるようです。
さらにはWASIで動作するlibcなどの実装もあります
まとめ
Goで作成した画像生成のプログラムをWebAssemmblyにコンパイルすることで、その画像をブラウザから利用できるようにしてみました。
現状はシステムコールに対応する関数をブラウザ側に定義する形でブラウザ対応しましたが、WASIの策定が進むとこのあたりももう少しこなれてくるのでは?と感じました(まぁWASIにブラウザで動かすようなモチベーションがどこまであるかは知らないですが・・)
ブラウザで動かすことにも一定の意義がありますが、WebAssemblyは様々なランタイムが開発されており、軽量でセキュアなコンテナ技術のような使われ方や、組み込み機器での利用なども模索されています。
まだまだどっちに進むかわからないWebAssemblyですが、活発な分野なので、今後の展開に注視したいです
おまけ モバイルレトロゲーム開発とWebAssembly
WebAssemblyとゲームと言うとUnityの事例のような、ブラウザで高速なゲームエンジンを動かす事例が、例に挙げられますが、私の趣味である携帯ゲーム機の開発の分野でも面白い利用例があります。
こちらのミニデバイス向けのゲームライブラリであるcrisp-game-lib-portableは、C/C++でゲームを開発し、M5StickCPLUSのような、組み込みゲーム機(PSPやNintendo DSなどと比べると十分に性能の低い、いうなればレトロゲーム実行環境のようなもの)に向けたゲームを開発するものです。
このライブラリはWebAssemblyを利用して、ブラウザゲームロジックを動かす事ができ、実機と同じ画面やスピーカーのインターフェースをブラウザ側にも用意することで、ブラウザで実機のシミュレーターを実現しています。
これはUnityなどが、ブラウザで高速なゲームエンジンを動かすモチベーションとは少し違い、組み込み機器開発のイテレーションを高速化するために利用されているという点で、個人的に面白いと感じています。
参考