のどあめ

ググってもなかなか出てこなかった情報を垂れ流すブログ。

【Rust×WebAssembly】Rust JavaScript間でデータをやり取りする

概要

  • WebAssemblyを使ってJavaScriptからRustの関数を使うときの話
  • JavaScript<->Rustのデータのやり取りをする方法をまとめました。
  • 数値、String、Arrayで書き方が違うみたいです。

github.com

参考にしたページ

Rust, Webassemblyとは

省略

必要なもの

データやり取りの方法

共通

  1. src/main.rswasm/wasm_test.wasm,wasm/wasm_test.jsコンパイルします。
  2. index.htmlwasm/wasm_test.wasmをロードしたらsrc/main.jsが実行されます。
  3. src/main.jsから、src/main.rsで実装したrustの関数を呼び出します。

index.html

<!DOCTYPE html>
<html lang="ja">
  <head>
    <meta charset="utf-8">
    <title>wasm test</title>
    <script>
      var Module = {};
 
      fetch('wasm/wasm_test.wasm')
        .then((response) => response.arrayBuffer())
        .then((wasm) => {
          Module.wasmBinary = wasm;
 
          const scriptElem = document.createElement('script');
          scriptElem.src = 'wasm/wasm_test.js';
          scriptElem.addEventListener('load', (e) => {
              const mainScriptElem = document.createElement('script');
              mainScriptElem.src = 'src/main.js';
              document.body.appendChild(mainScriptElem);
          });
          document.body.appendChild(scriptElem);
        });
    </script>
  </head>
  <body>
    <div id="number"></div>
    <div id="string"></div>
    <div id="array"></div>
  </body>
</html>

想定する結果

  • JavaScriptからRustに渡された値はprintln!マクロによってconsole.logに出力します。
  • RustからJavaScriptに渡された値はdivに出力されます。
  • githubのコードを動かすと↓のようになります。

f:id:ykicisk:20170430193518p:plain

数値の受け渡し

数値(js<->rust)

数値を受け渡す場合は、JavaScript、Rustともに普通に関数を書けば良いです。 i32f64等数値っぽい型はそのまま渡せますが、usizeなんかは渡せませんでした。

src/main.js

// number: js <-> rust
const wasm_number = Module.cwrap('wasm_number', 'number', ['number', 'number']);
document.getElementById('number').innerText = wasm_number(3, 4.2);

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_number(n: i32, m: f64) -> f64 {
    m + (n as f64) * 2.0
}

文字列の受け渡し

文字列(js->rust)

JavaScriptの文字列はRustで*c_char型として受け取ります。 これをRustのString型に変換する必要があります。

src/main.js

const wasm_string_to_rust = Module.cwrap('wasm_string_to_rust', null, ['string']);
wasm_string_to_rust("string from js");

src/main.rs

use std::os::raw::c_char;
use std::ffi::CStr;
use std::str;
#[no_mangle]
pub extern "C" fn wasm_string_to_rust(c_buf: *const c_char) {
    let c_str: &CStr = unsafe { CStr::from_ptr(c_buf) };
    let buffer: &[u8] = c_str.to_bytes();
    let rust_str: &str = str::from_utf8(buffer).unwrap();
    let rust_string: String = rust_str.to_owned();
    println!("{}", rust_string);
}

文字列(rust->js)

rustの文字列をjsに渡すためには、同様に*c_char型で返せば良いですが、 Rustの関数内で作った文字列のアドレスをas_ptrで返してしまうと、 関数から抜けたときに文字列のメモリが開放されてしまいます。

今回は、lazy_staticを利用してstaticなStringを保持して、 勝手にメモリが開放されないようにしています。

JavaScriptから関数を呼ぶ場合はこの処理をなくしても動作するそうですが、 Rustから関数を呼ぶとメモリ開放されて値を受け取れないので、念のためやっておいた方が良いと思います。

src/main.js

const wasm_string_to_js = Module.cwrap('wasm_string_to_js', 'string', []);
document.getElementById('string').innerText = wasm_string_to_js();

src/main.rs

lazy_static! {
    static ref HEAP_STRING: Mutex<String> = Mutex::new(String::new());
}
#[no_mangle]
pub extern "C" fn wasm_string_to_js() -> *const c_char {
    let s: String = "string from rust".to_string();

    let mut output = HEAP_STRING.lock().unwrap();
    output.clear();
    write!(output, "{}\0", s).unwrap();
    output.as_ptr() as *const c_char
}

配列の受け渡し

配列(js->rust)

配列の受け渡しをする時もメモリ管理は重要です。 JavaScriptの方で配列用のメモリを確保し、Rust側でその値を*f32として受け取ります。

JavaScript側で普通にnew FloatArray(5)みたいな感じで配列を作っちゃうと動きません。

src/main.js

// array: js -> rust
const wasm_array_to_rust = Module.cwrap('wasm_array_to_rust', null, ['number', 'number']);
var len = 5;
var bufsize = len * 4;
var bufptr = Module._malloc(bufsize);
Module._memset(bufptr, 0, bufsize)
var buf = new Float32Array(Module.HEAPF32.buffer, bufptr, len);
for (var i = 0; i < len;i ++){
    buf[i] = 1.1 * i;
}
wasm_array_to_rust(len, buf.byteOffset);
Module._free(bufptr); 

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_array_to_rust(_len: i32, ptr: *const f32) {
    let len = _len as usize;
    let slice: &[f32] = unsafe { std::slice::from_raw_parts(ptr, len) };
    for n in slice {
        println!("{}", n);
    }
}

配列(rust->js)

RustからJavaScriptに配列を渡すときも、配列自体はJavaScriptで確保して、Rust側で値を埋めるようにします。

※Rustが1.16.0だとうまく行かなかったので、動かないときはRustのバージョンを上げるとよいかもです。

src/main.js

// array: rust -> js
const wasm_array_to_js = Module.cwrap('wasm_array_to_js', null, ['number', 'number']);
var len = 6;
var bufsize = len * 4;
var bufptr = Module._malloc(bufsize);
Module._memset(bufptr, 0, bufsize)
var buf = new Float32Array(Module.HEAPF32.buffer, bufptr, len);
wasm_array_to_js(len, buf.byteOffset);
document.getElementById('array').innerText = buf;
Module._free(bufptr); 

src/main.rs

#[no_mangle]
pub extern "C" fn wasm_array_to_js(_len: i32, ptr: *mut f32) {
    let len = _len as usize;
    let mut v: Vec<f32> = vec![0.5; len];
    v[3] = 99.3;
    let mut res: &mut [f32] = unsafe { std::slice::from_raw_parts_mut(ptr, len) };
    res.clone_from_slice(&v.as_slice())
}

感想

  • 就活生にWebAssemblyについて質問されたが、うまく答えられなかったのでWebAssembly勉強したい。
  • Rust×WebAssemblyで遊ぼうと思ったが、ここでかなり詰まってしまった。
  • せっかくJavaScriptとRust使っているのにCみたいにメモリ管理するの辛い。
    • ここを乗り切れば平和な世界が訪れる?
  • JavaScriptもRustも超初心者なので何か間違ってる気がする