Search code examples
javascriptrustwebassembly

Print a string from Rust WASM in JavaScript without wasm_bindgen


Problem

To understand WASM a little better, I want to try passing some string data from a Rust WASM module into JavaScript without using wasm_bindgen. I am attempting to call a Rust function that returns a tuple containing a pointer to the string and its length. The module imports correctly, but I'm getting undefined in the JS when I call the function.

Research

I referred to this SO accepted answer, but it seems to be out-of-date and maybe a little unergonomic. The updated answer, of course, uses wasm_bindgen. (Note: I'm not opposed to using wasm_bindgen, I just want to understand what's going on better.)

Code

My Rust:

use std::ptr;

#[no_mangle]
pub fn greet(name: &str) -> (u8, usize) {
    let s = format!("Hello, {}!", name).to_string();
    let len = s.len();
    let ptr = ptr::addr_of!(s);
    (ptr as u8, len)
}

Abbreviated JS:

let wasm = wasmModule.instance.exports;

//After WASM load:
console.log( wasm?.greet("WebAssembly") );

Expectations and Assumptions

  • New to Rust, new to WASM.
  • My expectation is that I would get an ArrayBuffer from wasm.greet, which I would then need to break apart and use to reference bytes in wasm.memory, but I never get that ArrayBuffer.
  • I would expect those integer values (the pointer and the length) to return (because they are copied in the function response), but I assume my next issue will be that the string itself doesn't exist in the WASM memory anymore by the time I reference it in the JS. I'll deal with that later, unless some kind folks provide some suggestions for how it should be dealt with. :-)
  • Is it possible that the function could return undefined because the integers I'm sending don't exist in memory by the time the function returns?

Solution

  • Based on this answer and confirmed by altering my own code, the problem was returning a tuple. You should return only a single value for the time being (apparently not all environments support returning multiple values).

    So, the "unergonomic" code I mentioned seeing in this SO answer is actually necessary to some degree because you can't return a pointer and a length together.


    JS code to get the string returned from separate function calls for the pointer and length:

    function wasmString(loc, len) {
        const mem = new Uint8Array(wasm?.memory?.buffer, loc, len);
        console.log(mem);
        const dec = new TextDecoder();
        return dec.decode(mem);
    }
    

    Some other notes that may be helpful:

    • Returning the pointer and length from WASM works suspiciously without fault! The lifetime of the string either isn't an issue, or it's an issue that will creep up later when data at that address is overwritten. I don't think I trust it.
    • The printed string is "Hello, !", meaning that the string argument from JS does NOT pass to Rust. Presumably this is more wasm_bindgen magic. Investigating.