wasm-bindgen icon indicating copy to clipboard operation
wasm-bindgen copied to clipboard

Import JS function provided at runtime

Open SIGSTACKFAULT opened this issue 1 year ago • 4 comments

Motivation

I'm embedding a Bevy game in a Vue app. i'd like for them to be able to communicate. since there's no apparent way for a rust function called from JS to interact with the ECS (or, i haven't found it) i'd like to be able to call a JS function from rust which returns a queue of recent events.

The function I want to call will be in a .vue file (and, even if i put it in a .ts file, i can't figure out how to get #[wasm_bindgen(module=...)] to play nice with Vite) which makes it impossible to import. So i'd like to be able to provide it at runtime, presumably while calling __wbg_init

as a bonus, this would make it always possible to link to a function with any bundler.

Proposed Solution

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen(dynamic)]
    fn foo() -> u32;
}
import init, {main} from "my-wasm-whatever";

function whatever() {
    return 1337;
}

init(undefined, {foo: whatever}).then(main);

modify __wbg_init to something like

async function __wbg_init(input, import_overrides = undefined) {
    if (wasm !== undefined) return wasm;

    if (typeof input === 'undefined') {
        input = "/assets/my_wasm_whatever.wasm"
    }
    const imports = __wbg_get_imports();
    if (import_overrides !== undefined){              //   \
        Object.assign(imports, import_overrides);     //    )- the added part
    }                                                 //   /
    
    if (typeof input === 'string' || (typeof Request === 'function' && input instanceof Request) || (typeof URL === 'function' && input instanceof URL)) {
        input = fetch(input);
    }

    __wbg_init_memory(imports);

    const { instance, module } = await __wbg_load(await input, imports);

    return __wbg_finalize_init(instance, module);
}

Alternatives

If this is already possible i couldn't find it in the documentation.

SIGSTACKFAULT avatar Oct 17 '23 03:10 SIGSTACKFAULT

It's my idea. declare foo function in #[wasm_bindgen] extern, wasm_bindgen expose a function for import foo function (like set_foo_ns):

#[wasm_bindgen]
extern "C" {
    pub type NS;

    #[wasm_bindgen(method)]
    pub fn foo(this: &NS) -> u32;
}

#[wasm_bindgen]
pub fn set_foo_ns(ns: &NS) -> u32 {
    ns.foo()
}
import wasmInit, { set_foo_ns } from '../pkg';

wasmInit().then( ()=> { console.log(set_foo_ns( { foo: () => 1 } )); } );

It will print 1 on console.

thy486 avatar Nov 13 '23 04:11 thy486

An alternative would be to define a wrapper that lives in a .ts/.js file and calls the Vue function itself, and then import the wrapper into WASM.

khoover avatar Jan 21 '24 16:01 khoover

I believe the simplest way is

  1. Create a Javascript object
  2. Create a Rust shim for it, along the lines of https://rustwasm.github.io/wasm-bindgen/examples/import-js.html
  3. Add a function that lets you pass in the Javascript object, and store a reference to it. You'll probably need myJsValue.unchecked_into::<MyClass>();
  4. Use it

The documentation could definitely be improved.

stefnotch avatar Feb 02 '24 12:02 stefnotch

@stefnotch I think this is a good way. I'm just commenting an example I created when adding dynamic import to another project. (as it wasn't obvious to me on how to fully complete it)

#[wasm_bindgen]
extern "C" {
    #[wasm_bindgen]
    pub type HelloWorldModule;

    #[wasm_bindgen(method, js_name = "helloWorld")]
    pub fn hello_world(this: &HelloWorldModule);

}

pub async fn load_dynamic_hello_world() -> HelloWorldModule {
    let module_as_str = r#"export function helloWorld() { console.log("Hello World!"); }"#;

    let from_data = Array::new();
    from_data.push(&module_as_str.into());

    let mut type_set: BlobPropertyBag = BlobPropertyBag::new();
    type_set.type_("application/javascript");

    let blob = Blob::new_with_str_sequence_and_options(&from_data, &type_set).unwrap();
    let module_address = web_sys::Url::create_object_url_with_blob(&blob).unwrap();

    let module_promise: Promise = js_sys::eval(&format!(r#"import ("{}")"#, module_address))
        .unwrap()
        .into();

    let module = JsFuture::from(module_promise).await.unwrap();

    let as_hello_world: HelloWorldModule = module.into();
    as_hello_world.hello_world();

    as_hello_world
}

I like it better than inline_js= as I think it's likely this will work more frequently with different rust frameworks. This is because inline_js bindings require the creator of the framework to explicitly support them.

ghost avatar Mar 17 '24 04:03 ghost