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

1

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();
 });
}
Comate answered 14/6, 2022 at 14:14 Comment(6)
Nitpick: format_args!($($t)*).to_string()? Wow, that is impressive. Just use format!($($t)*).Suziesuzuki
That's just copy/paste from this example rustwasm.github.io/docs/wasm-bindgen/examples/wasm-in-wasm.html. It's not really part of the issue.Comate
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.Formulaic
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...Formulaic
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.Comate
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.Shoreward
R
5

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!

Resinoid answered 21/6, 2022 at 18:38 Comment(4)
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.Shoreward
@Shoreward 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 githubResinoid
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...Shoreward
@frankenapps. That might be...? maybe... If not, here is my repoResinoid

© 2022 - 2024 — McMap. All rights reserved.