本来の 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 しないで済む分使いやすい