Rust で WebAssembly と戯れてどうにもならなかった
夢の跡
頑張ったけど無理でした。
やりたかったことは WebAssembly を使った音声操作です。
動機
Rust 実装の音声ライブラリについて調べてたところ、cpal に行き当たりました。
んで、何とそこには Emscripten という文字が。
お? ブラウザでも動くの?? となぜか WebAssembly で書いてみようと思ったのです。
制限時間は1日。
いや対応してないやん
それはそうなのですが、WebAssembly で動くと
- 必要な速度が出れば、ブラウザ上で音声規格の配布ができる
- 上記が達成できれば、標準化を狙った規格の試行錯誤ができる
- 標準化にこぎつけブラウザに組み込むとなった段階で、組み込める(かもしれない)コードが出来上がってる
こう書くと、風が吹けば桶屋が儲かる感がすさまじい。おかしいな、こんなはずでは…
それはともかく、速度を優先されてかつ私がすぐ試せるものとなると、大学時代にかじった音声処理でした。
ので、それでお試し。
やったこと
まずはネイティブ実装
#[no_mangle] pub extern "C" fn hoge() -> f64{ let device = rodio::default_output_device().unwrap(); let sink = Sink::new(&device); // Add a dummy source of the sake of the example. let source = rodio::source::SineWave::new(440).take_duration(Duration::from_secs(3)); sink.append(source); sink.sleep_until_end(); 1.0 }
サイン波を3秒再生する実装です。
使用したライブラリは rodio で、ドキュメントとかを見ながら実装しました。
ローカルでの WebAssembly の実行環境
上記ページを参考させてもらいました。
<!DOCTYPE html> <html lang="ja"> <head> <meta charset="utf-8"> <script> fetch('../target/wasm32-unknown-unknown/debug/wasm_sample.wasm') .then(response => response.arrayBuffer()) .then(function(bytes) { return WebAssembly.instantiate(bytes, {}); }) .then(results => { console.log(results.instance.exports.fuga(2)); }); </script> </head> <body> </body> </html>
IntelliJ で HTML を書いて右上にマウスカーソルを動かすと、HTTP サーバーとして動いてくれます。
ので、今回はそれを使いました。お試しなので許して。
WebAssembly 呼び出せてるよなー、の確認で加算関数を作って呼び出し。
無事呼び出せたので次。
ImportObj の定義
いよいよ音声再生だーと呼び出してみたところ、即失敗。
WebAssembly.instantiate
にてエラーが発生していて、エラーメッセージは以下でした。
TypeError: import object field 'env' is not an Object
env とは???
で、エラーメッセージで調べてみると、以下を見つけました。
Rocket - A Rust game running on WASM
なるほどなー、足りない関数の対応表を渡してやる必要があるわけですな。
ということで、そのままパクる。
関数の呼び出しに成功しました。んが!
WebAssembly 環境の追加
動きませんでした。 そらそうだよね!
print デバッグをやって確認したところ、1行目でこけてる模様。
rodio と cpal をローカルにクローンして、書き換えながら確認しました。
#[cfg(not(any(windows, target_os = "linux", target_os = "freebsd", target_os = "macos", target_os = "ios", target_os = "emscripten")))] use null as cpal_impl; use std::error::Error; use std::fmt; use std::iter; use std::ops::{Deref, DerefMut}; mod null; mod samples_formats; #[cfg(any(target_os = "linux", target_os = "freebsd"))] #[path = "alsa/mod.rs"] mod cpal_impl; #[cfg(windows)] #[path = "wasapi/mod.rs"] mod cpal_impl; #[cfg(any(target_os = "macos", target_os = "ios"))] #[path = "coreaudio/mod.rs"] mod cpal_impl; #[cfg(target_os = "emscripten")] #[path = "emscripten/mod.rs"] mod cpal_impl;
引用元 cpal/lib.rs at 77ce5eba06447275ce6a094ac053e6775272ad8c · tomaka/cpal · GitHub
当たり前なのですが WASM の文字がなく、結果として未対応環境の null
に行きついてエラーになってました。そらそうだ。
同じブラウザで動かすし、内部で使われてる stdweb は WebAssembly 対応してるぽいから、emscripten
の記載部分に wasm 追加すればいいかなー、と安直に考えて追加しました。
中を見たところ WebAudio に対応してるかを判定してたので、WebAssembly から API 呼び出しできればいけるかなーってところでした。
追加方法の参考リンクは以下です。
https://stackoverflow.com/questions/48350087/how-do-i-conditionally-compile-for-webassembly-in-rust
https://github.com/koute/stdweb/blob/master/src/webcore/ffi/mod.rs
記載方法はこんな感じ。
#[cfg(any(target_os = "emscripten", all(target_arch = "wasm32", target_os = "unknown")))] #[path = "emscripten/mod.rs"] mod cpal_impl;
Cargo.toml も併せて修正。
[target.'cfg(any(target_os = "emscripten",all(target_arch = "wasm32", target_os = "unknown")))'.dependencies] stdweb = { version = "0.1.3", default-features = false }
んでなんと通ってしまった!
そして次のエラー。
sinf/cosf の補填
突破だーと思ったところ、またもやエラーでした。
LinkError: import object field 'sinf' is not a Function
なるほどな? 関数が見つからん、といってるので、先ほどの関数の対応表に sinf
と cosf
を追加して、突破です。
で、次のエラー。
LinkError: "import object field 'emscripten_asm_const_int' is not a Function" にて挫折・終了
軽く調べて C の header ファイルに行きついた瞬間に、これはちょっとこれまでのやつと違う気がする、となってきました。
C から Javascript の関数を呼び出すための関数、らしいのですが、今そういったことをやってないんですよね。
関数の役割的に sinf
のように補填すればよい、ということでもなさそうです。
んで、stdweb の実装に確かに記載をみつけ、mod.rs
での切り替えや macro.rs
での呼び出しも見つけましたが、よくわからんぞーとなってきて心折れてきたところで夜でした。
難しい
そんな簡単にいくわけがない、とわかっていたものの、中途半端になってしまいました。
いつかリベンジ。いつか…