rust
WebAssembly

RustでWebAssemblyのライブラリを作るときは配列を引数にとらないように気をつけよう

多分Rust以外でもそうだと思うのですが、WebAssemblyはメモリの扱いが難しくてちょっと詰まったので、その話と私の解決法を書いておこうと思います。

 追記)mallocとfreeをRustに実装することで配列も引数に取れることに気が付きました。なのでmallocを使う方法とここに書いた方法を比較してみました。https://qiita.com/garagara/items/0562ee5a85db5aeeb6b4

 結論を言うとここに書いた方法の方が良いと思います。

Rustを使ってWebAssemblyのライブラリを作るには

ここを参考にしました。http://nmi.jp/2018-03-19-WebAssembly-with-Rust

かいつまんでいうと、rustup add target=wasm32-unknown-unknown でターゲットを追加し、crate-type = ["cdylib"]、 にして、cargo build --lib --release --target=wasm32-unknown-unknown でwasmファイルを作り、htmlなんかがあるディレクトリにwasmファイルを置いて、javascriptから実行します。RustでCライブラリを書くのと同じように書いて、targetをwasm32-unknown-unknownにしてビルドすれば、jsから呼び出してブラウザで実行できるようになります。

どのような関数をエクスポートする必要があるか

C互換のライブラリということで、最初はこんな感じのものを作りました。

///バイト列を受け取るのに必要なバイト数を取得。受け取れない場合-1
#[no_mangle]
pub extern "C" fn get_item_length(p: *const Decoded) -> i32{ ... }

/// バイト列をコピーする。必要な量全てを一度に書ききるので、事前にget_item_lengthで大きさを調べて、
/// 入りきる大きさのメモリ領域を準備する必要がある。成功すると1 失敗すると0
#[no_mangle]
pub extern "C" fn copy_item_bytes(p: *const Decoded, copy_to: *mut u8) -> i32{...}

必要な長さを調べて、その長さの配列を用意し、ライブラリに渡して結果を書き込んでもらうスタイルです。他の環境ではこれでいけたのですが、WebAssemblyではこのスタイルは二重に間違っていました。

第一に、WebAssemblyには配列を渡す事ができません。その代りに、WebAssemblyのコードが使用するメモリ領域をまるごと渡すことが出来ます。そのメモリ領域に値を書き込み、書き込んだアドレスと共にライブラリ側に送ることで、実質配列を送ることも出来ますが、このアプローチには明らかに問題があります。メモリはRust側で使用され、Rustは内部でメモリを確保したり解放したりします。なので書き込んだデータが無事なのか分かりません。

そして、書き換えた配列を受け取ることも出来ません。WebAssemblyの関数に渡せるのは32bitまたは64bitの数値だけで、戻り値として受け取れるのも同じです。Rustが計算した可変長の結果をJS側で受け取るには、メモリアドレスを整数として返してもらい、その番地のメモリにJSからアクセスするしかありません。

なので、ライブラリをこのまま使うことは出来ません。WebAssemblyに対応するには、配列を受け取らなくても済むようにしてやる必要があります。

WebAssemblyと可変長のデータをやりとりするには

 こんな風にやるといいと思います。

pub struct StringBuilder{
    s : String
}

#[no_mangle]
pub extern "C" fn create_string_builder() -> *mut StringBuilder {
    let b = Box::new(StringBuilder{ s : "".to_string()});
    Box::into_raw(b)
}

#[no_mangle]
pub extern "C" fn destroy_string_builder(b : *mut StringBuilder){
    unsafe{ Box::from_raw(b); }
}

/// cはunicodeのcodepointで32bitのunsigned整数。互換性のためにi32にしている。
/// cがcodepointでないと失敗して-1、そうでないと成功して1が返る
#[no_mangle]
pub extern "C" fn string_builder_append_char(b : *mut StringBuilder, c : i32) -> i32{
    unsafe {
        match ::std::char::from_u32(c as u32) {
            Some(c) => {
                (*b).s.push(c);
                return 0;
            }
            _ => return -1,
        }
    }
}

 C互換関数を介してRustのstructを返す方法はここを参考にしました。 https://qiita.com/tatsuya6502/items/b9801d92f71e24874c9d

 たとえばWebAssemblyに文字列を渡したければ、まずStringBuilderオブジェクトを作り、文字を一つ一つ渡して文字列を作ります。

var b = export.create_string_builder();
for(let c of s){
  export.string_builder_append_char(b, c.codePointAt(0));
}

 そしてStringBuilderを取るラッパ関数を呼び出します。js側では、Rustのstructを使い終わったらdestoryを呼び出してメモリを解放することを忘れないようにしなければいけません。

#[no_mangle]
pub extern "C" fn decode_with_builder(builder : *mut StringBuilder) -> *mut Decoded{ 
  let s = &(*b).s;
  decode(s.as_ptr(), s.len() as i32) //ポインタと長さを取るC互換関数を呼び出す
}

 WebAssemblyの関数から可変長のデータを受け取るときは、ポインタを返す関数を用意して、js側からアクセスできるようにします。

#[no_mangle]
pub extern "C" fn get_item_address(p: *const Decoded) -> *const u8{ (*p).vec.as_ptr() }

js側
fetch('hoge.wasm').then(response => response.arrayBuffer())
    .then(bytes => WebAssembly.instantiate(bytes, {}))
    .then(results => {
var export = results.instance.exports;
...
var result_struct = export.do_something(builder);
var addr = export.get_item_address(result_struct);
var len = export.get_item_len(result_struct);
var mem = export.memory.buffer;
var decoder = new TextDecoder('utf-8');
var bytes = new Uint8Array(mem, addr, len);
var result_string = decoder.decode(bytes);

 非常に面倒ですが、他に方法は思いつきませんでした。もっと良い方法をご存知の方はぜひ教えていただきたいです。

 この面倒さはWebAssemblyにGCが追加されたりすればいずれ解消されるものだと思いますが、いまのところはこんな感じにするのがいいんじゃないかと思います。