LoginSignup
16
10

More than 5 years have passed since last update.

wasm-bindgen で Rust から JS へ JSON を渡す

Last updated at Posted at 2018-08-18

本来の wasm は float しか送れないが、wasm-bindgen を経由すれば自分でTextEncoderを実装することなく文字列の受け渡しができる。

コンパイルやインストールは略。

Cargo.toml
[dependencies]
wasm-bindgen = "0.2"
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"

[lib]
crate-type = ["cdylib"]

#[wasm-bindgen]&str で受け取り、String を返す。
今回は serde を使って struct を JSONに変換して返す。

src/lib.rs
#[macro_use]
extern crate wasm_bindgen;

#[macro_use]
extern crate serde_derive;
extern crate serde;
extern crate serde_json;

#[derive(Serialize)]
pub struct MyObj {
    text: String,
    x: i32,
    y: i32,
}

#[wasm_bindgen]
pub fn xxx(x: &str) -> String {
    let myobj = MyObj {
        text: x.to_string(),
        x: 1,
        y: 2,
    };
    let serilized = serde_json::to_string(&myobj).unwrap();
    serilized
}

JS側で文字列を受け取ってJSON.parseする。

const main = async () => {
  const { xxx } = await import("../gen/rust_wasm");

  const arr = new Array(1000).fill(0);
  console.time("xxx");
  arr.forEach(() => {
    console.log(JSON.parse(xxx("foo")));
  });
  console.timeEnd("xxx");
};

main();

1000回実行して 300ms 程度。あんまりパフォーマンスが良いとは言い難い。
追記: console.log を考慮するのを忘れていた。抜くと 1000回で 6ms。 十分速い。

みっちりチューニングするなら、当然自分でABIのような何かを決めてデコードした方が速い。

ちなみに回数を増やすと線形に増加するので、内部でJITが効いたりはしてない模様。

おまけ: 生成されてるコード

TextEncoder を自分で使うとだるいという話


const TextEncoder = typeof self === 'object' && self.TextEncoder
    ? self.TextEncoder
    : require('util').TextEncoder;

let cachedEncoder = new TextEncoder('utf-8');

let cachegetUint8Memory = null;
function getUint8Memory() {
    if (cachegetUint8Memory === null || cachegetUint8Memory.buffer !== wasm.memory.buffer) {
        cachegetUint8Memory = new Uint8Array(wasm.memory.buffer);
    }
    return cachegetUint8Memory;
}

function passStringToWasm(arg) {

    const buf = cachedEncoder.encode(arg);
    const ptr = wasm.__wbindgen_malloc(buf.length);
    getUint8Memory().set(buf, ptr);
    return [ptr, buf.length];
}

const TextDecoder = typeof self === 'object' && self.TextDecoder
    ? self.TextDecoder
    : require('util').TextDecoder;

let cachedDecoder = new TextDecoder('utf-8');

function getStringFromWasm(ptr, len) {
    return cachedDecoder.decode(getUint8Memory().subarray(ptr, ptr + len));
}

let cachedGlobalArgumentPtr = null;
function globalArgumentPtr() {
    if (cachedGlobalArgumentPtr === null) {
        cachedGlobalArgumentPtr = wasm.__wbindgen_global_argument_ptr();
    }
    return cachedGlobalArgumentPtr;
}

let cachegetUint32Memory = null;
function getUint32Memory() {
    if (cachegetUint32Memory === null || cachegetUint32Memory.buffer !== wasm.memory.buffer) {
        cachegetUint32Memory = new Uint32Array(wasm.memory.buffer);
    }
    return cachegetUint32Memory;
}
/**
* @param {string} arg0
* @returns {string}
*/
export function xxx(arg0) {
    const [ptr0, len0] = passStringToWasm(arg0);
    const retptr = globalArgumentPtr();
    try {
        wasm.xxx(retptr, ptr0, len0);
        const mem = getUint32Memory();
        const rustptr = mem[retptr / 4];
        const rustlen = mem[retptr / 4 + 1];

        const realRet = getStringFromWasm(rustptr, rustlen).slice();
        wasm.__wbindgen_free(rustptr, rustlen * 1);
        return realRet;


    } finally {
        wasm.__wbindgen_free(ptr0, len0 * 1);

    }

}

追記

wasm-bindgen に serde-serialized という feature があった

Cargo.toml
[dependencies]
serde = "1.0"
serde_json = "1.0"
serde_derive = "1.0"

[dependencies.wasm-bindgen]
version = "^0.2"
features = ["serde-serialize"]
#[wasm_bindgen]
pub fn xxx(x: &str) -> JsValue {
    let myobj = MyObj {
        text: x.to_string(),
        x: 1,
        y: 2,
    };
    JsValue::from_serde(&myobj).unwrap()
}

JsValue型で、型もクソもないが、 JS側でJSON.parse しないで済む分使いやすい

16
10
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
16
10