興味のおもむくままにWASM/WASIらへん
https://zenn.dev/link/comments/09280d40f6fa5a で考えていたけどあまりに脇道に逸れ過ぎなので別のスクラップブックにした。
気になること
- WASIって何? (WASMとの差分)
- Web用 WASMをパッケージングする上でEmscriptenとwasm-packの違い
- 純粋JSとのパフォーマンス差をベンチマークしてみたい
WASIって何? (WASMとの差分)
- WASIはBrowser, JS independent なWASM仕様 -> ランタイムのためのインターフェース?
- BytecodeAllianceが中心となって策定している
- WASI実装としては wasmtime, wasmer, lucetの3つがメジャーっぽい
結局、WASI/WASMの関係性は何なのか?
- 一般的にWASMと呼ばれているものはブラウザ向けのWebAssemblyのこと
- WebAssembly Core に JavaScript APIとWeb APIをのっけたもの
- ランタイム実装としてはV8とかSpiderMonkeyとか
- それこそEmbeddedされている
- 対してWASIはCoreにWASI APIをのっけたもの
- 正確にはInterfaceなのでWASI API仕様だけで、実装としてwasmerやwasmtimeなどがある
- こちらもOSネイティブなランタイムにEmbeddedされている
- 正確にはInterfaceなのでWASI API仕様だけで、実装としてwasmerやwasmtimeなどがある
つまり、ブラウザ向けのWASMとWASI向けのWASMには差がある。積集合に当たる部分はCoreで共通するが使えるcapabilityが異なる。
ブラウザ向けにコンパイルされた(アセンブルされたというべき?)wasmはWASIでは動かないかもしれない=依存するAPIが使えないかもしれない
理解の役に立った文献
メジャーなWASIランタイム
wasmtime
- BytecodeAllienceのリファレンス実装
- 実用性でいうと起動速度、実行速度ともに物足りなくなっている様子
wasmer
- 単純なWASI実装を超えてエコシステム目指してるっぽい
- NPMライクなWAPMとか
- 商業感ある
lucet
- CDNで有名なFastly主管
- Fastlyはエッジコンピューティングの有力な手段としてwasm推し
- BytecodeAllianceのwasmtimeと密な連携してそう
- ...と思ったら開発終了してwasmtimeと合流してる
- もともとwasmtimeの派生でその成果をwasmtimeに還元してプロジェクトを終了したっぽい
チュートリアルやってみた
リポジトリ
$ wasmtime --dir=./tmp ./target/wasm32-wasi/debug/hello.wasm ./tmp/org.txt ./tmp/copy_by_wasmtime.txt
$ wasmer --dir=./tmp ./target/wasm32-wasi/debug/hello.wasm ./tmp/org.txt ./tmp/copy_by_wasmer.txt
当然ながらどちらのランタイムでも同じwasmファイルが動作する
当たり前だけどWindowsに入れたwasmtimeでも動く
PS C:\Users\NewGyu\tmp> wasmtime.exe --dir=. .\hello.wasm .\org.txt copy_by_win.txt
感想(サンドボックス)
なるほど、サンドボックスとして --dir, --mapdirでファイルアクセス範囲をホワイトリストで指定するようになっているのか。
The --dir= option instructs wasmtime to preopen a directory, and make it available to the program as a capability which can be used to open files inside that directory.
wasmerの方が早い傾向(さすがパフォーマンスに注力している)
$ time wasmer --dir=./tmp ./target/wasm32-wasi/debug/hello.wasm ./tmp/org.txt ./tmp/copy_by_wasmtime.txt
real 0m0.008s
user 0m0.000s
sys 0m0.008s
$ time wasmtime --dir=./tmp ./target/wasm32-wasi/debug/hello.wasm ./tmp/org.txt ./tmp/copy_by_wasmtime.txt
real 0m0.016s
user 0m0.017s
sys 0m0.000s
Web向けのWASM
- 一般的にWASMと呼ばれているものはブラウザ向けのWebAssemblyのこと
- WebAssembly Core に JavaScript APIとWeb APIをのっけたもの
- ランタイム実装としてはV8とかSpiderMonkeyとか
- それこそEmbeddedされている
理解の役に立った文献
- MDNのページが最も整理されている
- https://rustwasm.github.io/docs.html
- https://webassembly.org/specs/
- https://eng-blog.iij.ad.jp/archives/10875
WASM with JavaScriptの仕様
JavaScrpit API
JavaScript API: defines JavaScript classes and objects for accessing WebAssembly from within JavaScript, including methods for validation, compilation, instantiation, and classes for representing and manipulating imports and exports as JavaScript objects.
JavaScript <---> WASMのつなぎの仕様
- WASMの中にJSのfunctionをどうやってimportするか
- JS側からWASMのModuleをどうinstantiateするか
- その他にもJSObjectをどうやってWASMの中で扱うかとかそんなことが書かれている(たぶん)
Web API
Web API: defines extensions to the JavaScript API made available specifically in web browsers, in particular, an interface for streaming compilation and instantiation from origin-bound Response types.
ブラウザがWASMを組み込むための仕様っぽい
Web向けWASMをビルドするツール
基本的にはRustでコードを書くスタンスで調べる
wasm-pack
- Rustからブラウザ向けのWASMにするツール
- その実はwasm-binggenをwrapしたcliツール
- RustコードにアノテーションをつけることでJSとのグルーコードを吐いてくれる
- グルーコードを吐く点では後述のEmscriptenとやっていることは近い用に思えるが、暗黙ではなく明示的にアノテートするところが違いなのかな
Emscripten
Emscripten is a complete compiler toolchain to WebAssembly, using LLVM, with a special focus on speed, size, and the Web platform.
APIs
Emscripten converts OpenGL into WebGL, and has support for familiar APIs like SDL, pthreads, and POSIX, as well as Web APIs and JavaScript.
- 基本C/C++をWASMにするツール
- LLVM使える言語なら何でもできそうではある
- ブラウザーで動かすために色々便宜を計らってくれるっぽい(JS APIに置き換えたり)
- Rust勢はEmscriptenから離れていっている様子
WebAssembly Script
Designed for WebAssembly
AssemblyScript targets WebAssembly's feature set specifically, giving developers low-level control over their code.Familiar TypeScript syntax
Its similarity with TypeScript makes it easy to compile to WebAssembly without learning a new language.
WASMにコンパイルするためのTypeScriptライクな専用言語らしい。とりあえずはあまり興味ない。
wasm-pack
チュートリアル
npmパッケージとして扱うパターン
正直webpackが必要になってめんどい。手順は以下。
-
wasm-pack build
でビルドすると以下のようにnpmパッケージ前提の一式が作られる。
$ ls -1 ./pkg/
hello_wasm_bg.js
hello_wasm_bg.wasm
hello_wasm_bg.wasm.d.ts
hello_wasm.d.ts
hello_wasm.js
package.json
README.md
$ cat ./pkg/package.json
{
"name": "hello-wasm",
"version": "0.1.0",
"files": [
"hello_wasm_bg.wasm",
"hello_wasm.js",
"hello_wasm_bg.js",
"hello_wasm.d.ts"
],
"module": "hello_wasm.js",
"types": "hello_wasm.d.ts",
"sideEffects": false
}
- 別途testsite用にwebpackの構成をする
$ ls -1 ./testsite/
index.html
index.js
package.json
package-lock.json
webpack.config.js
- 上記のwasmの入った
hello-wasm
npmパッケージをtestsiteに取り込む
$ cat ./testsite/package.json
{
"name": "testsite",
"dependencies": {
"hello-wasm": "file:../pkg"
},
- そいつをimportして使う
import * as wasm from "hello-wasm";
wasm.greet("WebAssembly!");
ポイント
- WASM入りのnpmパッケージを配布する想定になっている
- node.js向け?
- ブラウザJSにするためにWebpackを前提としている
- wasmモジュールを読み込むところはWebpackの機能
- https://webpack.js.org/configuration/experiments/#experimentsfuturedefaults
- Webpack4.xまでは暗黙で
.wasm
のimportを処理していたが5.xからはexperiment扱いになったらしい
wasm-pack build --target web でビルドするパターン
日本語ページには載ってなくて、英語版にひっそり書いてあった…。こっちのほうがええやん。
というわけでこれも試した。
-
wasm-pack build --target web
とするとpkgディレクトリに色々作られる
$ ls -1 pkg/
hello_wasm_bg.wasm ...コンパイルされたWASM
hello_wasm_bg.wasm.d.ts
hello_wasm.d.ts
hello_wasm.js ...つなぎのJSコード
package.json
README.md
- 上記のうちhello_wasm.jsにinit関数が定義されるのが大きな違い
init関数の中でwasmファイルのロードとinstantiateが行われている - HTML中から扱うのはとてもシンプル
<script type="module">
import init, {greet} from "./pkg/hello_wasm.js";
init()
.then(() => {
greet("WebAssembly")
});
</script>
ポイント
-
--target web
をつけよう - hello_wasm.jsがすべてを覆い隠してjs関数のように扱うことができる!
- いずれにしてもrustcの
wasm32-unknown-unknown
ターゲットとしてコンパイルされる=WASIと共通のwasm-coreに準じたものになる。
Rust + WASM
Emscripten
やろうと思ったけどインストールするものが多いので面倒になってやめた。
Emscripten vs wasm-pack メモ
- wasm32-unknown-unknown - this target, like the WASI one, is focused on producing single *.wasm binaries. The standard library, however, is largely stubbed out since the "unknown" part of the target means libstd can't assume anything. This means that while binaries will likely work in wasmtime, common conveniences like println! or panic! won't work.
- wasm32-unknown-emscripten - this target is intended to work in a web browser and produces a *.wasm file coupled with a *.js file, and it is not compatible with wasmtime.
前者は純粋なwasm(おそらくwasm coreの範囲)でのビルドになるのでポータビリティがある。(WASIランタイムでも動くし、ブラウザ上でも動く)。しかし、libstd相当の部分、例えばprintf!, panic!のようなものは動かない。(別の手段が必要になる)
後者(emscripten)でビルドされたものはprintf!, panic!みたいなシステムライブラリをうまいことJSに依存するようにしてあるので便利だけど、ブラウザを前提としているのでWASIランタイム上では動かない。(たぶんimport されたobjectにメッチャ依存するようになってる)
wasm32-unknown-emscripten
This target uses emscripten to provide the standard library. emscripten tries to provide things like TCP sockets/file io/multithreading/opengl so that you can compile your existing code to wasm and have it ‘just work’. People compile their SDL2 applications, and emscripten will handle making DOM elements etc. that given you your gl canvas.
wasm32-unknown-unknown
This target is much more “bare bones”. There is no attempt to emulate a PC - most system calls like IO will just panic (todo check this). However, the compiled wasm modules tend to be smaller since they don’t need all that emulation, and we don’t have to build and install an enormous C project. We do get an allocator (wee_alloc I believe) so we can use all our favorite heap allocated data structures (Vec, HashMap, …). The awesome wasm-bindgen project provides interop with javascript, and then libraries like web-sys/js-sys provide rust functions that map to ECMAScript and Web API functions, so you can do anything like create HTML nodes.
- Emscriptenは(ブラウザ想定なら)既存のプログラムがほとんどそのまま動く...けれどサイズが大きい
vs
- 「素」のwasmだけなのでコンピューティング以外(IOなど)は何もできないけれど小さい
- 必要ならwasm-bindgenを組み合わせて
という構造で、Emscriptenのモノリシックだった部分を分割して行く方向性か。
wasm-pack deep dive
wasm-packは結局のところwasm-bindgenのWrapperなのでwasm-bindgenの流儀を知る必要がある。
WASM vs JS パフォーマンス対決
一番これがやりたかった。題材としては純粋な計算量勝負のアルゴリズムとしてハノイの塔 O(2n)をチョイス。
手元で動かす場合はwebpack-dev-serverでlocalhost:8080だが、gh-pagesにもしてある。
結果としてはこんな感じで思ったよりも速くない…。 v8が優秀なのか?
- プロセッサ AMD Ryzen 7 3700X 8-Core Processor、3593 Mhz、8 個のコア、16 個のロジカル プロセッサ
- メモリ 32GB
- Windows11上のWSL2 Ubuntu20.04上のVSCode Remote Container上で計測
Node.js vs WASIだとWASIのほうが15%くらい早くて、同じ展開を期待していたのだが・・・
(´・ω・`)
パフォーマンス対決をしようと思ってハマったのは
- ChromeのdevtoolsでWASM関数を実行するとやたらと遅い
- なぜかperformance profilerをONにすると解消してしまう
- Firefoxはなんの問題もない
という謎事象。
Rustのdiscordで聞いてみたら「devtoolsを閉じた状態で計測してみ?」というアドバイスをもらって確認したところ上記の「やや速い」という結果が得られた。
Twitterでも教えていただけたのはdevtoolsのバグなんじゃないか説。
Chrome において、DevTools をただ開くと wasm の実行が極めて遅くなる現象が確認されております。これはバグではなくて仕様なのですが、結構複雑な仕様で、
DevTools を開くと TurboFan の最適化がキャンセルされて遅くなる
しかし Profile タブで Profile を取るときには再度 TurboFan の最適化が適用される
という挙動になっております。Chrome では、ただ DevTools を開いているだけで wasm の実行がかなり遅くなります。大変気づきにくいトリガーなので、wasm 系の開発をされる時にはお気をつけください。
このまとめ超ありがたい。
rust + wasm-packで外部ライブラリのリンク
ソフトウェア作ってるとどうしても外部ライブラリが必要になってくる。他のライブラリに依存するようなWASMをビルドしたい場合どうしたものか。
パターン別に考えてみる。
- Cargoのdependencyでcrateにリンク
- *.soにDynamic Linking
- *.aにStatic Linking
- 別のwasmにDynamic Linking
- 別のwasmとCombine(Static Linking)
結論としては以下のいずれかしか方法がない。
-
- Cargoのdependencyでcrateにリンク
-
- *.aにStatic Linking
下記についてはwasm仕様として検討はされているようだがまだexperimentalな機能(emscripten固有?)として捉えたほうが良さそう。
-
- 別のwasmにDynamic Linking
1.Cargoのdependencyでcrateにリンク
依存するcrateはCargoがソースコードをダウンロードしてきて*.rlibとしてコンパイルされる。
(.rlibは c/c++における.aと同じでスタティックリンクを意図した形式のRust版)
その後にリンカーによってそれらがスタティックリンクされて一つのwasmにまとめられる。
(このあたりはwasm-pack buildの内部挙動で良くは分かっていないが)
つまりは依存ライブラリがRust製のcrateなら特に気にすることなくwasm-packできる。
2. *.soにDynamic Linking
結論から言うと無理。
wasmはwasm runtime上で動くわけでruntimeは色々な環境で動く。
Webブラウザ上のwasm runtimeから*.soにアクセスできるわけが無い。そもそもwasm runtimeは隔離性を重視しているのでruntimeの下回りのOSに直接アクセスと言うのは無い。
3.*.aにStatic Linking
C/C++で書かれたライブラリをリンクするにはこの方法しかない(たぶん)
4. 別のwasmにDynamic Linking
- https://github.com/WebAssembly/tool-conventions/blob/main/DynamicLinking.md
- https://pspdfkit.com/blog/2022/dynamic-linking-in-webassembly/
Emscriptenだとメインモジュール、サイドモジュールという考え方があってメインのwasmから別のwasmを動的にリンクできるように見える。
WebAssembly公式のGitHubでは以下のように述べられているのでまだexperimentalな機能と考えて良さそう。
Note: This ABI is still a work in progress. There is no stable ABI yet.
5.別のwasmとCombine(Static Linking)
コンパイル済みのwasmファイルを取り込む(static linkingのようなイメージ)事ができないかとおもったが、noと言われているのでだめなんだろうな。
you are asking about linking a SIDE_MODULE into the MAIN_MODULE ahead of time, its not something that is supported no.
過去にはwasm-mergeというツールがあった??
補足:この考え方はComponent ModelではComposeという概念で実現される様子。
What is composition?
When you compose components, you wire up the imports of one "primary" component to the exports of one or more other "dependency" components, creating a new component.