Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Import JS function provided at runtime #3659

Open
SIGSTACKFAULT opened this issue Oct 17, 2023 · 4 comments
Open

Import JS function provided at runtime #3659

SIGSTACKFAULT opened this issue Oct 17, 2023 · 4 comments

Comments

@SIGSTACKFAULT
Copy link

SIGSTACKFAULT commented Oct 17, 2023

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.

@thy486
Copy link

thy486 commented Nov 13, 2023

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.

@khoover
Copy link

khoover commented Jan 21, 2024

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.

@stefnotch
Copy link

stefnotch commented Feb 2, 2024

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.

@ghost
Copy link

ghost commented Mar 17, 2024

@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.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Projects
None yet
Development

No branches or pull requests

4 participants