今更だけどRustでwasmを試す
RustでWebAssemblyしとかないと時代の流れに取り残されそう、という強迫観念にかられ腹をくくって飛び込んだ。
触るだけなら案外いけたので、その気持を忘れないようにここにメモする。
書かないこと
- Rustについて
- Wasmについて
-
環境構築
- WebAssembly 開発環境構築の本 がわかりやすい。
準備
wasm の知識
とくに勉強してない。LLVMがどうとかよくわからんレベル。
触って覚えようスタイル。
最低限 Rust で簡単なコードを書けるようにする
マークアップ生まれ JavaScript
育ちとしては、流石にRustをすっ飛ばして
Rustでwasm
するのは気が早すぎるという警告を受信したので、
なんとなく 言語処理100本ノック 2015 で軽くRustを素振りしておいた。
不まじめなので、12, 13あたりまでやって、なんかやろうとしてることとちょっと違うなと思って止めてしまったけど、
あああああああEarly returnした型が &str なのに、その先で値生成してたせいでStringになって結果戻り値の型が一致しなくて怒られてただけでしたあああああああ
— undefined (@RyoNkmr_) 2019年1月21日
String
と &str
に悩んだり、 usize
と uint
で悩んだり
vec![]
はいいらしい、とか、 変数の mut
と 参照?の mut
はなるほど?
if
, match
, iterator
あたりはなんとなくやっておいて、
関数の書き方、モジュールをまたいだ関数の呼び出しくらいは調べながらできるようになった。
シンプルに試す
とにかく触るという目標なので、 素の rust
開発環境 だけで開発&wasmにビルドして、
jsから呼ぶというのが今回の目標。というかそんなミニマルにできるようになったんだ、と驚いてる。
Rustのコードを書く
cargo new rust-wasm-vanilla --lib
でプロジェクト作って早速書く。
#[no_mangle]
pub fn add(a: i32, b: i32) -> i32 {
a + b
}
#[no_mangle]
pub fn fib(count: u32) -> u32 {
if count < 2 {
return count;
}
let (_, ret) = (0..count-2).fold((0, 1), |(former, ret), _| (ret, ret + former));
ret
}
#[no_mangle]
pub fn fib_array(count: u32) -> Vec<u32> {
match count {
0 => vec![],
1 => vec![0],
2 => vec![0, 1],
_ => (2..count).fold(vec![0, 1], |mut acc, i| {
let new = acc[(i-1) as usize] + acc[(i-2) as usize];
acc.push(new);
acc
})
}
}
fn main() {
println!("{:?}", add(10, -100));
println!("{:?}", fib(40));
println!("{:?}", fib_array(10));
assert!(fib_array(10).len() == 10);
}
#[no_mangle] はアトリビュートと言い, Java のデコレータのようにブロックやメソッドなどを修飾する構文です. #[no_mangle] では「Rust コンパイラに次の関数の名前をマングリングせずにコンパイルせよ」と指示します. これにより, JavaScript から add という名前で関数にアクセスできるようになります. また, 関数を公開して外部から呼び出せるようにするため pub キーワードを付けています.
https://wasm-dev-book.netlify.com/hello-wasm.html#webassembly-%E3%82%92%E8%A9%A6%E3%81%99
こんな感じでざっと関数を定義して ESM的に言うなら export
する。
Rustに詳しくない人も fn main()
みたらなにか感じたかも知れないけど、
せっかくなので rust
として実行してみる。
Rustとして実行&ビルド
$ rustc src/lib.rs # ./lib というバイナリにCompile
$ ./lib # 実行
-90
63245986
[0, 1, 1, 2, 3, 5, 8, 13, 21, 34]
やったぜ🐱
これはただのRustなのでまだ全然wasmしてない。
今度は wasm32-unknown-unknown
というビルドターゲットを指定してBuildする。
$ cargo build --target wasm32-unknown-unknown
Compiling rust-wasm-vanilla v0.1.0 (<PROJECT_PATH>)
warning: function is never used: `main`
--> src/lib.rs:30:1
|
30 | fn main() {
| ^^^^^^^^^
|
= note: #[warn(dead_code)] on by default
Finished dev [unoptimized + debuginfo] target(s) in 3.44s
すると ./target/wasm32-unknown-unknown/debug/rust_wasm_vanilla.wasm
がBuildされる。
Rustで試す用のコードそのまま放置してるので怒られが発生してるけど気にしなくてOK。
(エラーにあるようにattribute使えば回避できる)
JavaScriptからwasmを利用する
生み出した .wasm
を js
のcontextから叩くための準備をする。
<!DOCTYPE html>
<html>
<head>
<meta charset="utf-8">
<title>WasmTest</title>
</head>
<body>
<script>
const wasmPath = './target/wasm32-unknown-unknown/debug/rust_wasm_vanilla.wasm';
WebAssembly.instantiateStreaming(fetch(wasmPath), {})
.then(({ instance }) => {
const fib = 47;
console.log(instance.exports);
console.log('add', instance.exports.add(2, 8));
console.log('fib', fib, instance.exports.fib(fib));
console.log('fib_array', instance.exports.fib_array(47));
console.log(instance.exports.fib(50));
});
</script>
</body>
</html>
ちなみに、WebAssembly.instantiateStreaming 非対応ブラウザならこうする。
fetch(wasmPath)
.then(response => response.arrayBuffer())
.then(bytes => WebAssembly.instantiate(bytes, {}))
.then(({ instance }) => {
// 略
});
fetch()
:.wasm
を取得するResponse
を作る-
Body.arrayBuffer()
:Response
のストリームからwasm
バイナリデータをすべて読み取るArrayBuffer
にresolve
するPromise
が返る
-
WebAssembly.instantiate()
: wasmバイナリのArrayBuffer
をインスタンス化する。その過程で module の Compiling もされるのでそれも返る。-
{ module: コンパイル済みの WebAssembly.Module instance: 上記 module を instance 化したもの }
に
resolve
するPromise
が返る
-
とにかく開ければいいので最低限の .html
をシュッと作る。
ここから、ローカルの .wasm
に向けて fetch
叩いても リクエストしないので、 ローカルのWebサーバを経由で見るようにする。
$ npx http-server . -p <適当なポート番号>
または、 php -S localhost:<適当なポート番号>
でもなんでもいいけど、
wasm 渡すときの MIMEtype 周りにちょっと注意。
実行結果
Chromeでの実行結果はこんな感じ。
-
instance.exports
- いろいろはいってるけど略
-
instance.exports.add(2, 8)
- OK
-
instance.exports.fib(fib)
- OK
-
instance.exports.fib_array(47)
Vec
が都合よくArray
になるなんてことはなくて虚無になる
-
instance.exports.fib(50)
rust
のコード内でpanic
してるstd::u32::MAX
である4294967295
を超える7778742049
になるため-
だからといってrust側で戻り値のtypeを
u64
にするとUncaught (in promise) TypeError: wasm function signature contains illegal type at WebAssembly.instantiateStreaming.then
というRuntime Errorが発生する。
多分u64(rust) -> i64(wasm)
のあとにi64
がNumber
にtypecast出来なくて死んでるっぽい
現時点のまとめ
i32
の範疇でなにかやるくらいなら全然これでいい。
あと、文字(char) はどうなのと思って試した
#[no_mangle]
pub fn get_char_a() -> char {
'a'
}
//...略
const a = instance.exports.get_char_a();
// 97
97...
まず, Rust と WebAssembly のそれぞれでサポートされるプリミティブ型を確認します. Rust は
array
,bool
,char
,i32
などを含む 25 種類のプリミティブ型がサポートされます. 一方, WebAssemblyのプリミティブ型はi32
,i64
,f32
,f64
の 4 種類のみしかサポートされません.
というわけで、Rust における char
も例外ではなく wasm に持っていった段階ですべて int
or float
になっちゃうので、jsで文字(列)として decode する必要がある。
TextDecoder
があるので、そこに Uint8Array
、つまりバイトストリームにして、decode methodに食わせる。
const charAByte = new Uint8Array([instance.exports.get_char_a()];
const decoder = new TextDecoder('utf-8');
decoder.decode(charAByte);
// a
こういった諸々の手続き踏めば色々な型の表現ができるんだけど、これはいちいちやってられないし、
自前で頑張るのはなかなかしんどい、ということで必然的に rustwasm/rust-bindgen
という wasm-js 間の binding 生成ライブラリを使う。これが結構凄い。これについては次回。
やり残し
あとやってないけどjsから環境を渡すことで WebAssembly から JavaScript の関数を呼び出す ことができるっぽい。
Rust 側で予め環境のInterface定義して呼ぶような感じ。
あとせっかくやるならRust のサードパーティ製ライブラリの利用 もやりたい。
適当なタイミングで、この2つは試してみようと思う。