9

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();
 });
}
Arnold Daniels
  • 16,516
  • 4
  • 53
  • 82
  • Nitpick: `format_args!($($t)*).to_string()`? Wow, that is impressive. Just use `format!($($t)*)`. – Chayim Friedman Jun 14 '22 at 23:19
  • That's just copy/paste from this example https://rustwasm.github.io/docs/wasm-bindgen/examples/wasm-in-wasm.html. It's not really part of the issue. – Arnold Daniels Jun 15 '22 at 13:07
  • I think that if you do not use the glue code generated by `wasm-bindgen` you should stick to FFI-safe code, just as if you were interfacing with C. That is, a `#[no_mangle] pub unsafe extern "C" hello(name: *const u8, reply: *mut u8, reply_len: usize)` or something like that. – rodrigo Jun 19 '22 at 10:56
  • But... thinking of it, the called and the callee codes live in separated WASM modules, so they use separated memory spaces and they cannot pass pointers to each other memory. This makes things more _interesting_... – rodrigo Jun 19 '22 at 15:21
  • The sandboxing of WASM modules is important for this solution. The loaded WASM module isn't from a trusted source. Using FFI isn't an option. – Arnold Daniels Jun 20 '22 at 07:35
  • 1
    I am pretty sure you have to rewrite all the auto-generated javascript code of `imported_lib` using `js-sys`. If you look into the corresponding `.wasm.d.ts` file you can see that the actual signature of hello is as follows: `export function hello(a: number, b: number, c: number): void;`. I was getting started with rewriting all the glue code but this turns out to be quite painful. – frankenapps Jun 20 '22 at 12:50

1 Answers1

3

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!

Ricardo
  • 1,308
  • 1
  • 10
  • 21
  • The general idea looks great to me, however when I try to run this in the browser I get a `Uncaught (in promise) TypeError: attempting to access detached ArrayBuffer` in `getString.js:16:30`. – frankenapps Jun 22 '22 at 08:13
  • @frankenapps that is weird.... I tried the code before I wrote the answer. Do you at least get the string printed out or is this happens after the string is printed out? Can you check that the functions called in the `getString` file have the names of the functions in the file `imported_lib.wasm.d.ts`. If you still have problem with it I can upload the code into my github – Ricardo Jun 22 '22 at 13:57
  • That would be great. No the error occurs before the string is printed (it is never printed actually). However I used `wasm-pack build --target web` and then ran the program in the browser so maybe that contributes to the issue... – frankenapps Jun 22 '22 at 17:15
  • 1
    @frankenapps. That might be...? maybe... If not, here is my [repo](https://github.com/SpaceComet/rmStack/tree/master/stackOverflow/06072022-wasm) – Ricardo Jun 22 '22 at 18:51