Help us understand the problem. What is going on with this article?

Rust + WASM + JS でパニックメッセージをブラウザのコンソールに表示する

More than 1 year has passed since last update.

追記

近頃は console_error_panic_hook という便利なものがあるのでこれを使ってください:
https://crates.io/crates/console_error_panic_hook

動機

Rust の wasm32-unknown-unknown ターゲットで出力される WASM においては、パニックは単に「到達してはいけないところに到達しました」的なアセンブリ命令に過ぎない。パニックは回復不可能なエラーであるとはいえ、パニックメッセージくらいは見ないと原因がよくわからない。そんなわけで、パニックメッセージを JS のコンソールに表示できれば嬉しい。

方針

おそらく cargo-webstdweb を組み合わせてマクロを使えば話は早いのだろうが、WASM 関係の JS API を見るに、インポートオブジェクトを通じて JS の関数を WASM から呼び出すというのが意図された設計であるように思われるので、ここではそれに従う。

$ rustc --version
rustc 1.25.0-nightly (932c73647 2018-02-08)

実装

Rust

Rust ではパニック時のフックを登録することができる。もちろんパニック時の情報がクロージャの引数で取得できるので、これを文字列に整形し、C 形式の文字列に変換してメモリ上に置き、JS に文字列の先頭のアドレスと長さを渡す。

use std::panic;

// JS から WASM をインスタンス化する際に受け取る
extern {
    fn js_console_log(ptr: *const i8, size: usize);
}

// 上の関数のラッパー
fn console_log(message: &str) {
    unsafe { js_console_log(message.as_ptr(), message.len()) };
}

#[no_mangle]
pub fn init() {
    // パニック時のフックを登録する
    panic::set_hook(Box::new(|panic_info| {
        let payload = panic_info.payload();

        // payload は Any なので String か &str だったら具象型に変換し Some にくるむ
        let payload = if payload.is::<String>() {
            Some(payload.downcast_ref::<String>().unwrap().as_str())
        } else if payload.is::<&str>() {
            Some(*payload.downcast_ref::<&str>().unwrap())
        } else {
            None
        };

        // PC での panic 表示を真似てフォーマット
        if let (Some(payload), Some(location)) = (payload, panic_info.location()) {
            console_log(format!("panicked at {:?}, {}:{}:{}", payload, location.file(), location.line(), location.column()).as_str());
        }
    }));
}

#[no_mangle]
pub fn panic() {
    panic!("Normal panic {}", 42);
}

JS

JS 側はメッセージ文字列のアドレスと長さを受け取るので、メモリバッファから指定の場所を読み出してコンソールに出力する。読み出すべきメモリバッファは関数を定義する時点では用意されていないので、WASM をインスタンス化したあとでメモリオブジェクトを js_console_log からアクセス可能な場所に用意してやる。

let memory = null

const imports = {
  env: {
    js_console_log: (ptr, size) => {
      const buffer = new Uint8Array(memory.buffer, ptr, size)
      const message = String.fromCodePoint(...buffer)
      console.log(message)
    },
  },
}

fetch('path/to/wasm/file')
  .then(response => response.arrayBuffer())
  .then(buffer => WebAssembly.compile(buffer))
  .then(module => WebAssembly.instantiate(module, imports))
  .then(({ exports }) => {
    memory = exports.memory
    exports.init()
    exports.panic()
  })

先に用意してインポートする方法もあるが、メモリの再確保で ArrayBuffer が無効になりうるらしい。https://stackoverflow.com/a/41372484/6826848 を参照。WebAssembly.Memory までは無効にならないのかも知れない。また、Rust でメモリをインポートするにはいくつか属性を指定する必要もある。 https://www.hellorust.com/demos/import-memory/index.html を参照。

ぼやき

  • Any から Debug を作れないの辛い
  • 作成したメッセージがメモリ上に残ることは一切保証されていないと思われる
mizkichan
高専→文学部哲学科→文学研究科哲学専攻。
https://mizkichan.github.io
Why not register and get more from Qiita?
  1. We will deliver articles that match you
    By following users and tags, you can catch up information on technical fields that you are interested in as a whole
  2. you can read useful information later efficiently
    By "stocking" the articles you like, you can search right away