Search code examples
rustwebassemblywasm-bindgen

How to import a WASM module in WASM (Rust) and pass a String parameter


I want to instantiate a Wasm module from inside a Wasm module, following this js-sys example. In the example, the add function is called which passes i32 parameters.

I've created a hello world function, which takes a string as a parameter and returns a string. However, calling this function doesn't work, as it returns undefined.

Normally wasm bindgen generates glue code which creates a context and puts the string on the stack. However, no such code is generated for Rust.

How can I load and execute the hello function from Wasm in Rust?

imported_lib.rs
use wasm_bindgen::prelude::*;

#[wasm_bindgen]
pub fn add(a: i32, b: i32) -> i32 {
 a + b
}

#[wasm_bindgen]
pub fn hello(name: String) -> String {
 format!("hello {:?}", name).into()
}
main_lib.rs
use js_sys::{Function, Object, Reflect, WebAssembly};
use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;
use wasm_bindgen_futures::{spawn_local, JsFuture};

// lifted from the `console_log` example
#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(js_namespace = console)]
    fn log(a: &str);
}

macro_rules! console_log {
    ($($t:tt)*) => (log(&format_args!($($t)*).to_string()))
}

const WASM: &[u8] = include_bytes!("imported_lib.wasm");

async fn run_async() -> Result<(), JsValue> {
 let a = JsFuture::from(WebAssembly::instantiate_buffer(WASM, &Object::new())).await?;
 let b: WebAssembly::Instance = Reflect::get(&a, &"instance".into())?.dyn_into()?;
 let c = b.exports();

 let add = Reflect::get(c.as_ref(), &"add".into())?
  .dyn_into::<Function>()
  .expect("add export wasn't a function");
 let three = add.call2(&JsValue::undefined(), &1.into(), &2.into())?;
 console_log!("1 + 2 = {:?}", three); // 1 + 2 = JsValue(3)

 let hello = Reflect::get(c.as_ref(), &"hello".into())?
  .dyn_into::<Function>()
  .expect("hello export wasn't a function");
 let hello_world = hello.call1(&JsValue::undefined(), &"world".into());
 console_log!("{:?}", hello_world); // JsValue(undefined)

 Ok(())
}

#[wasm_bindgen(start)]
pub fn run() {
 spawn_local(async {
  run_async().await.unwrap_throw();
 });
}

Solution

  • It really took me days to solve this problem. I hope it helps! Since it's a lot of info to pack here. I will try to keep it short but If you want to know more let me know and I'll expand on my answer.

    Short explanation of why this is happening

    This actually happens because wasm by default does not return Strings so the smart people at wasm-bindgen did something so when you run wasm-pack build it generates a js code that do this for you. The function hello does not return a string, instead returns a pointer. To proof this, you can check the files generated when you build the imported_lib.rs

    You can see that it generates the file imported_lib.wasm.d.ts that looks something like this:

    export const memory: WebAssembly.Memory;
    export function add(a: number, b: number): number;
    export function hello(a: number, b: number, c: number): void;
    export function popo(a: number): void;
    export function __wbindgen_add_to_stack_pointer(a: number): number;
    export function __wbindgen_malloc(a: number): number;
    export function __wbindgen_realloc(a: number, b: number, c: number): number;
    export function __wbindgen_free(a: number, b: number): void;
    
    • You can see that the function add does match how you declared, 2 parameters and returns a number. In the other hand you can see that the function hello takes 3 parameters and return a void (very different to how you declared)
    • You can also see that the command wasp-pack build generated some extra functions like (__wbindgen_add_to_stack_pointer, __wbindgen_free, etc). With these functions they are able to get the string.

    The other file that the command wasm-pack build generates is imported_lib_bg.js. In this file you can see that they export the function hello. Here it's where JavaScript call the compiled wasm function and "translate" the pointer to the actual string.

    So basically you would have to do something similar to what it is in the file imported_lib_bg.js. This is how I did it:

    Solution

    In your main project create a folder call js, and inside that folder create a file call getString.js. Your project filesystem should look something like this:

    mainProject
    ├── js
        ├── getString.js
    ├── src
        ├── main_lib.rs
        ├── ...
    ├── www
    ├── ...
    

    And the file should have this:

    function getInt32Memory0(wasm_memory_buffer) {
        let cachedInt32Memory0 = new Int32Array(wasm_memory_buffer);
        return cachedInt32Memory0;
    }
    
    function getStringFromWasm(ptr, len, wasm_memory_buffer) {
        const mem = new Uint8Array(wasm_memory_buffer);
        const slice = mem.slice(ptr, ptr + len);
        const ret = new TextDecoder('utf-8').decode(slice);
        return ret;
    }
    
    let WASM_VECTOR_LEN = 0;
    
    function getUint8Memory0(wasm_memory_buffer) {
        let cachedUint8Memory0 = new Uint8Array(wasm_memory_buffer);
        return cachedUint8Memory0;
    }
    
    const lTextEncoder = typeof TextEncoder === 'undefined' ? (0, module.require)('util').TextEncoder : TextEncoder;
    
    let cachedTextEncoder = new lTextEncoder('utf-8');
    
    const encodeString = (typeof cachedTextEncoder.encodeInto === 'function'
        ? function (arg, view) {
        return cachedTextEncoder.encodeInto(arg, view);
    }
        : function (arg, view) {
        const buf = cachedTextEncoder.encode(arg);
        view.set(buf);
        return {
            read: arg.length,
            written: buf.length
        };
    });
    
    function passStringToWasm0(arg, malloc, realloc, wasm_memory_buffer) {
    
        if (realloc === undefined) {
            const buf = cachedTextEncoder.encode(arg);
            const ptr = malloc(buf.length);
            getUint8Memory0(wasm_memory_buffer).subarray(ptr, ptr + buf.length).set(buf);
            WASM_VECTOR_LEN = buf.length;
            return ptr;
        }
    
        let len = arg.length;
        let ptr = malloc(len);
    
        const mem = getUint8Memory0(wasm_memory_buffer);
    
        let offset = 0;
    
        for (; offset < len; offset++) {
            const code = arg.charCodeAt(offset);
            if (code > 0x7F) break;
            mem[ptr + offset] = code;
        }
    
        if (offset !== len) {
            if (offset !== 0) {
                arg = arg.slice(offset);
            }
            ptr = realloc(ptr, len, len = offset + arg.length * 3);
            const view = getUint8Memory0(wasm_memory_buffer).subarray(ptr + offset, ptr + len);
            const ret = encodeString(arg, view);
    
            offset += ret.written;
        }
    
        WASM_VECTOR_LEN = offset;
        return ptr;
    }
    
    
    /**
    * @param {&JsValue} wasm: wasm object
    * @param {string} fn_name: function's name to call in the wasm object
    * @param {string} name: param to give to fn_name
    * @returns {string}
    */
    export function getString(wasm, fn_name, name) {
        try {
            const retptr = wasm.__wbindgen_add_to_stack_pointer(-16);
            const ptr0 = passStringToWasm0(name, wasm.__wbindgen_malloc, wasm.__wbindgen_realloc, wasm.memory.buffer);
            const len0 = WASM_VECTOR_LEN;
            //wasm.hello(retptr, ptr0, len0);
            wasm[fn_name](retptr, ptr0, len0);
            var r0 = getInt32Memory0(wasm.memory.buffer)[retptr / 4 + 0];
            var r1 = getInt32Memory0(wasm.memory.buffer)[retptr / 4 + 1];
            return getStringFromWasm(r0, r1, wasm.memory.buffer);
        } finally {
            wasm.__wbindgen_add_to_stack_pointer(16);
            wasm.__wbindgen_free(r0, r1);
        }
    }
    

    In your main_lib.rs add this:

    
    ...
    
    #[wasm_bindgen(module = "/js/getStrings.js")]
    extern "C" {
        fn getString(wasm: &JsValue, nf_name: &str, name: &str) -> String;
    }
    
    ...
    
        let hello_out= getString(c.as_ref(), &"hello", "Arnold");
        console_log!("# hello returns: {:?}", hello_out);
    ...
    

    That should totally work!