Most of the examples I saw copy a string twice. First on the WASM side, in CString or by reducing Vec to its bandwidth, and then on the JS side when decoding UTF-8.
Given that we often use WASM for speed, I tried to implement a version in which the Rust vector would be reused.
use std::collections::HashMap; /// Byte vectors shared with JavaScript. /// /// A map from payload memory location to 'Vec<u8>'. /// /// In order to deallocate memory in Rust we need not just the memory location but also it size. /// In case of strings and vectors the freed size is capacity. /// Keeping the vector around allows us not to change it capacity. /// /// Not thread-safe (assuming that we're running WASM from the single JavaScript thread). static mut SHARED_VECS: Option<HashMap<u32, Vec<u8>>> = None; extern "C" { fn console_log(rs: *const u8); fn console_log_8859_1(rs: *const u8); } #[no_mangle] pub fn init() { unsafe { SHARED_VECS = Some(HashMap::new()) } } #[no_mangle] pub fn vec_len(payload: *const u8) -> u32 { unsafe { SHARED_VECS .as_ref() .unwrap() .get(&(payload as u32)) .unwrap() .len() as u32 } } pub fn vec2js<V: Into<Vec<u8>>>(v: V) -> *const u8 { let v = v.into(); let payload = v.as_ptr(); unsafe { SHARED_VECS.as_mut().unwrap().insert(payload as u32, v); } payload } #[no_mangle] pub extern "C" fn free_vec(payload: *const u8) { unsafe { SHARED_VECS.as_mut().unwrap().remove(&(payload as u32)); } } #[no_mangle] pub fn start() { unsafe { console_log(vec2js(format!("Hello again!"))); console_log_8859_1(vec2js(b"ASCII string." as &[u8])); } }
And the JavaScript part:
(function (iif) { function rs2js (mod, rs, utfLabel = 'utf-8') { const view = new Uint8Array (mod.memory.buffer, rs, mod.vec_len (rs)) const utf8dec = new TextDecoder (utfLabel) const utf8 = utf8dec.decode (view) mod.free_vec (rs) return utf8} function loadWasm (cache) {
The trade-off here is that we use a HashMap in WASM, which can increase size if a HashMap no longer required.
An interesting alternative is to use tables to share a triplet (payload, length, capacity) with JavaScript and return it when the time comes to free the line. But I donβt know yet how to use tables.
PS Sometimes we do not want to allocate Vec in the first place.
In this case, we can move the memory tracking in JavaScript:
extern "C" { fn new_js_string(utf8: *const u8, len: i32) -> i32; fn console_log(js: i32); } fn rs2js(rs: &str) -> i32 { assert!(rs.len() < i32::max_value() as usize); unsafe { new_js_string(rs.as_ptr(), rs.len() as i32) } } #[no_mangle] pub fn start() { unsafe { console_log(rs2js("Hello again!")); } }
(function (iif) { function loadWasm (cache) { WebAssembly.instantiateStreaming (fetch ('main.wasm', {cache: cache ? "default" : "no-cache"}), {env: { new_js_string: function (utf8, len) { const view = new Uint8Array (iif.main.memory.buffer, utf8, len) const utf8dec = new TextDecoder ('utf-8') const decoded = utf8dec.decode (view) let stringId = iif.lastStringId while (typeof iif.strings[stringId] !== 'undefined') stringId += 1 if (stringId > 2147483647) {