ソモサン

私rohkiによる活動や読書の記録をつらつらと書くページです

Rust で WebAssembly と戯れてどうにもならなかった

夢の跡

github.com

頑張ったけど無理でした。
やりたかったことは WebAssembly を使った音声操作です。

動機

Rust 実装の音声ライブラリについて調べてたところ、cpal に行き当たりました。
んで、何とそこには Emscripten という文字が。
お? ブラウザでも動くの?? となぜか WebAssembly で書いてみようと思ったのです。
制限時間は1日。

いや対応してないやん

それはそうなのですが、WebAssembly で動くと

  1. 必要な速度が出れば、ブラウザ上で音声規格の配布ができる
  2. 上記が達成できれば、標準化を狙った規格の試行錯誤ができる
  3. 標準化にこぎつけブラウザに組み込むとなった段階で、組み込める(かもしれない)コードが出来上がってる

こう書くと、風が吹けば桶屋が儲かる感がすさまじい。おかしいな、こんなはずでは…
それはともかく、速度を優先されてかつ私がすぐ試せるものとなると、大学時代にかじった音声処理でした。
ので、それでお試し。

やったこと

まずはネイティブ実装

#[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 の実行環境

nmi.jp

上記ページを参考させてもらいました。

<!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行目でこけてる模様。
rodiocpal をローカルにクローンして、書き換えながら確認しました。

#[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
なるほどな? 関数が見つからん、といってるので、先ほどの関数の対応表に sinfcosf を追加して、突破です。
で、次のエラー。

LinkError: "import object field 'emscripten_asm_const_int' is not a Function" にて挫折・終了

軽く調べて C の header ファイルに行きついた瞬間に、これはちょっとこれまでのやつと違う気がする、となってきました。
C から Javascript の関数を呼び出すための関数、らしいのですが、今そういったことをやってないんですよね。
関数の役割的に sinf のように補填すればよい、ということでもなさそうです。

んで、stdweb の実装に確かに記載をみつけ、mod.rs での切り替えや macro.rs での呼び出しも見つけましたが、よくわからんぞーとなってきて心折れてきたところで夜でした。

難しい

そんな簡単にいくわけがない、とわかっていたものの、中途半端になってしまいました。
いつかリベンジ。いつか…