How to return a string (or similar) from Rust to WebAssembly?

I created a small Wasm file from this Rust code:

#[no_mangle] pub fn hello() -> &'static str { "hello from rust" } 

It builds and the hello function can be called from JS:

 <!DOCTYPE html> <html> <body> <script> fetch('main.wasm') .then(response => response.arrayBuffer()) .then(bytes => WebAssembly.instantiate(bytes, {})) .then(results => { alert(results.instance.exports.hello()); }); </script> </body> </html> 

My problem is that alert displays "undefined". If I return i32 , it works and displays i32 . I also tried returning a String but it does not work (it still displays "undefined").

Is there a way to return a string from Rust to WebAssembly? What type should I use?

+11
rust webassembly
source share
3 answers

WebAssembly supports only a few numeric types that can be returned through an exported function.

When compiling in WebAssembly, your string will be held in the linear memory of the module. To read this line from JavaScript hosting, you need to return a link to its location in memory and the length of the line, i.e. Integers. This allows you to read a string from memory.

You use the same technique, no matter what language you compile in WebAssembly. How to return a JavaScript string from a WebAssembly function provides detailed help on this issue.

Unlike Rust, you need to use an external function interface (FFI) using the CString type as follows:

 use std::ffi::CString; use std::os::raw::c_char; static HELLO: &'static str = "hello from rust"; #[no_mangle] pub fn get_hello() -> *mut c_char { let s = CString::new(HELLO).unwrap(); s.into_raw() } #[no_mangle] pub fn get_hello_len() -> usize { HELLO.len() } 

The above code exports two functions: get_hello , which returns a string reference, and get_hello_len , which returns its length.

With the above code compiled into the wasm module, access to this line can be obtained as follows:

 const res = await fetch('chip8.wasm'); const buffer = await res.arrayBuffer(); const module = await WebAssembly.compile(buffer); const instance = await WebAssembly.instantiate(module); // obtain the module memory const linearMemory = instance.exports.memory; // create a buffer starting at the reference to the exported string const offset = instance.exports.get_hello(); const stringBuffer = new Uint8Array(linearMemory.buffer, offset, instance.exports.get_hello_len()); // create a string from this buffer let str = ''; for (let i=0; i<stringBuffer.length; i++) { str += String.fromCharCode(stringBuffer[i]); } console.log(str); 

The C-equivalent can be seen in action in WasmFiddle .

+13
source share

You cannot directly return Rust String or &str . Instead, select and return the raw byte pointer containing the data, which should then be encoded as a JS string on the JavaScript side.

You can see an example of SHA1 here .

Functions of interest are in

  • demos/bundle.js - copyCStr
  • demos/sha1/sha1-digest.rs - digest

Additional examples: https://www.hellorust.com/demos/sha1/index.html

+4
source share

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) { // https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming WebAssembly.instantiateStreaming (fetch ('main.wasm', {cache: cache ? "default" : "no-cache"}), {env: { console_log: function (rs) {if (window.console) console.log ('main]', rs2js (iif.main, rs))}, console_log_8859_1: function (rs) {if (window.console) console.log ('main]', rs2js (iif.main, rs, 'iso-8859-1'))} }}) .then (results => { const exports = results.instance.exports exports.init() iif.main = exports iif.main.start()})} // Hot code reloading. if (window.location.hostname == '127.0.0.1' && window.location.port == '43080') { window.setInterval ( function() { // Check if the WASM was updated. fetch ('main.wasm.lm', {cache: "no-cache"}) .then (r => r.text()) .then (lm => { lm = lm.trim() if (/^\d+$/.test (lm) && lm != iif.lm) { iif.lm = lm loadWasm (false)}})}, 200) } else loadWasm (true) } (window.iif = window.iif || {})) 

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) { // Can't easily pass more than that through WASM. stringId = -2147483648 while (typeof iif.strings[stringId] !== 'undefined') stringId += 1 if (stringId > 2147483647) throw new Error ('Out of string IDs!')} iif.strings[stringId] = decoded return iif.lastStringId = stringId}, console_log: function (js) { if (window.console) console.log ('main]', iif.strings[js]) delete iif.strings[js]} }}) .then (results => { iif.main = results.instance.exports iif.main.start()})} loadWasm (true) } (window.iif = window.iif || {strings: {}, lastStringId: 1})) 
+2
source share

All Articles