WASM / WASIに興味が最近ある
作ったアプリにwasm
が使われていたライブラリがあったので(そのために意図的に私が採用したんですが)、最近になってWebAssemblyに急激に興味が出てきた。
ただ、ちょっとこの技術の認識はフワフワしていて、まだ良くわかってない。 とりあえず感じることとしては、
- 同一のバイナリはOSおよびCPUアーキテクチャに左右されることはない (これは中々画期的だと思っていて、私が興味を持ってる理由)
- サンドボックス的実行(Dockerの代替とか言われている)
- 様々な言語からバイナリを吐ける
- なんかJVM的な感じ?
- Webブラウザで共通仕様化し、javascriptに次ぐ実行環境として席巻
みたいなイメージではある。うん、理解してない。
とりあえず、.NETではBlazorっていうのがあって、
C#上でReactっぽいことが出来る、厳密にはフロントエンドでコードをC#とjavascriptで相互に行き来できる的技術、という.NET発のWebAssemblyに対するアプローチがあり、1回公式チュートリアルやったことあるんですが、ちょっとよく分からなかった。
私自身がASP.NETに全く慣れてない為で、全てを.razor
ファイルに組み込めることを考えると、結構良いのかも、なんて浅いことを考えている。
こういう系のメリット把握できないのは実際仕様理解が足りないのが理由で、何かしらあるものだ。もうちょっと理解できたらなあと思う。
と言うことで、Hello Worldくらいはしてみろよ、って思ったのでしてみた。
とりあえず動かしてみる
とりあえずwasm
をOSで動かす技術としてwasi
があるのだと言う。今回はそれでHello Worldをしてみる。
WASI実行環境としてはWasmtimeとWasmerがあり、
とりあえずMac/Linux環境以外で実行できるかは不明。どちらも$HOME
フォルダに実行フォルダを作成するところはラクだ。
うん、最近Ubuntu Desktop入れてたけどかなり良かったな!こーゆーの入れたくなったらすぐ入れられる。
とりあえずこれでWebAssembly実行してみる。
まずWebAssemblyといったらRust。Rustは以前ちょい文法を触って学んで久々に。正直、大分分かっていない。 WebAssemblyのおかげで興味が急上昇中である。
なんとなくだが、単純な配列処理関数をやって表示するだけのものとした。
fn for_each<T>(arr: Vec<T>, f: fn(T)) {
for i in arr {
f(i);
}
}
fn filter<T: Copy>(arr: Vec<T>, f: fn(T) -> bool) -> Vec<T> {
let mut result = Vec::new();
for i in arr {
if f(i) {
result.push(i);
}
}
result
}
fn main() {
println!("filter demo: ");
let arr: Vec<i32> = vec![1, 2, 3, 4, 5];
let result = filter(arr, |i| i % 2 == 0);
for_each(result, |i| println!("{}", i));
}
wasm32-wasi
ターゲットを追加してビルドする。
rustup target add wasm32-wasi
rustc ./src/main.rs --target wasm32-wasi
で、実行。
wasmtime ./main.wasm
出力結果
filter demo:
2
4
Goの場合だが、GOOS=wasip1 GOARCH=wasm
を選択するとビルドできる模様。
Rustと同様のコードを書いてみる。
package main
import "fmt"
func forEach[T any](arr []T, f func(T)) {
for _, v := range arr {
f(v)
}
}
func filter[T any](arr []T, f func(T) bool) []T {
var res []T
for _, v := range arr {
if f(v) {
res = append(res, v)
}
}
return res
}
func main() {
fmt.Println("filter demo: ")
arr := []int{1, 2, 3, 4, 5}
result := filter(arr, func(n int) bool {
return n%2 == 0
})
forEach(result, func(n int) {
fmt.Println(n)
})
}
ビルド方法は以下の通り。
GOOS=wasip1 GOARCH=wasm go build -o main.wasm main.go
今度はwasmer
での例を示そう。
wasmer ./main.wasm
出力結果は次の通り。
filter demo:
2
4
ちなみに出力されるバイナリはls -l -h
で見ると、Rust vs Goで1.7MB vs 2.1MBであった。もちろん言語ごとに仕様が違うのもある。一見等価っぽい処理があったとしても、どの言語を入力にしてコンパイルするかで色々サイズや速度が違ったりする、と言うが、実際そうではあるみたいだ。
とりあえず、複数の言語から入力して、同じ感じの実行ファイルが作れるよーってことだけはまず理解した。
Node.jsでGoプログラムを実行
次は簡易にNode.jsでGoプログラムを実行してみる。次のような簡単なものを用意する。
package main
import "fmt"
func add(a, b int) int {
return a + b
}
func main() {
var a int = 1
var b int = 2
fmt.Printf("%d + %d = %d\n", a, b, add(a, b))
}
これを、次のような形でビルドする
GOOS=js GOARCH=wasm go build -o main.wasm main.go
浅い調査だが、これで生成したwasm
を実行するには、専用のスクリプトを経由しないといけないようだ。
これは$GOROOT/misc/wasm
に存在するスクリプトで、単純なブラウザ用スクリプト+Node.js実行用スクリプトの2段構えになっている。ちなみに$GOROOT
の場所は、snapパッケージでインストールしたGoなら/snap/go/xxxxx
みたいな、ホームディレクトリの場所とは違うから注意が必要である。
これらのファイルをローカルにまずコピーする。wasm_exec_node.js
は内部でrequire("./wasm_exec");
してるため、両者が必須で、ディレクトリ位置も重要である。
cp $(go env GOROOT)/misc/wasm/wasm_exec_node.js ./
cp $(go env GOROOT)/misc/wasm/wasm_exec.js ./
そして、wasm
ファイルを引数に渡す。
node ./wasm_exec_node.js ./main.wasm
実行される。
1 + 2 = 3
とりあえず、メインプログラムを実行する、ということができたので良しとしよう。
Go関数を呼び出す
ではNode.js上でGo関数を使うことを考えてみる。とりあえず、TinyGoというコンパイラの方が本家よりWasm領域は特化してるようだ。
導入はとりあえずDockerで。compose.yml
を作っておく。
services:
go:
image: tinygo/tinygo:0.31.2
tty: true
command: /bin/bash
volumes:
- .:/src
パッケージはmain
にしておき、メイン関数は空にしておく。大事なのが//export
コメントで、これを書くとjavascript側のWebAssembly.Instance
オブジェクトから関数がアクセスできるようになるっぽい。
package main
//export add
func Add(a, b int) int {
return a + b
}
func main() {
}
先に似ているが、次のグルーファイルをルートにコピーしなければならない。
cp "$(tinygo env TINYGOROOT)/targets/wasm_exec.js" ./wasm_exec.js
そんなこんなでビルド。
tinygo build -o main.wasm -target wasm ./main.go
javascriptでの書き方だが、こんな感じでいけたっぽい。Node.jsではTinyGoでビルドしたwasm
は以下のように呼び出してモジュール利用できる。
const fs = require("fs");
require("./wasm_exec");
const go = new Go();
async function main() {
const buf = fs.readFileSync("./main.wasm");
const { instance } = await WebAssembly.instantiate(buf, go.importObject);
// Goはこれ必須みたい
go.run(instance);
const a = instance.exports?.add(1, 2);
console.log(a); // -> 3
}
main();
とりあえず、少しずつやれることが増えてきた気がするが、Goめんどいなァ、、 データのやりとりの型とか、どうすれば正しく期待しているものが入出力できるかなど、もっと理解を増やしたいところだ。