wasm-bindgen
wasm-bindgen copied to clipboard
Import JS function provided at runtime
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.
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.
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.
I believe the simplest way is
- Create a Javascript object
- Create a Rust shim for it, along the lines of https://rustwasm.github.io/wasm-bindgen/examples/import-js.html
- 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>();
- Use it
The documentation could definitely be improved.
@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.