wasm-bindgen
wasm-bindgen copied to clipboard
How to properly call JavaScript functions from Rust?
hello folks :wave:
following the hello world example, it's been a pleasure calling rust functions from javascript — but for the other way around, i'm stuck!
for days now, i have re-read the docs many times, upside-down and sideways, asked for help on the rust discord, and read every reddit and stackoverflow post i can find, and even read some of the wasm-bindgen source code — and for the life of me, i cannot figure out how to properly call javascript functions from rust. i'm using wasm-pack and wasm-bindgen, and i'm a javascript developer who is trying to learn rust by integrating it into web applications to improve performance.
:pray: absolutely any guidance or help is much appreciated.
the hello world example shows calling javascript alert from rust, which is neat, but is unfortunately, not a viable technique:
- i have taken an oath, as a javascript library developer, to never ever pollute the global window object.
- so, every time we need to use some rust-wasm widget, we cannot attach a bunch of global properties to the window (that would be obvious madness)
- so we need a better way
additionally, examples like importing js demonstrate calling functions that live in an es module:
- but module-level functions are not useful for our cases
- we need to pass javascript callback functions that are generated at runtime, so they can have an effect on the state of our applications — it's the only way to make a rust-wasm widget actually useful so it can interact with an ongoing web application
okay, so those two techniques don't work. here's where things start to get weird:
- given this rust code
use wasm_bindgen::prelude::*; #[wasm_bindgen] extern "C" { #[wasm_bindgen] fn foo(); } #[wasm_bindgen] pub fn greet() { foo(); } - we spot the following code in the generated javascript file
async function load(module, imports) { //... return await WebAssembly.instantiateStreaming(module, imports); } woah! this `imports` object looks like exactly what we need!! just as the [wasm 'instantiateStreaming' docs](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/WebAssembly/instantiateStreaming) say, this `imports` object here allows us to pass javascript functions into the wasm runtime (as is the wasm standard). so we need to figure out how to pass javascript functions into here. - so, next in the generated javascript file's
init, we see this:
we're so close!async function init(input) { //... const imports = {}; imports.wbg = {}; imports.wbg.__wbg_foo_59bca18a9dbf92cf = typeof foo == 'function' ? foo : notDefined('foo'); //... const { instance, module } = await load(await input, imports);
here we can see the (naughty) global reference tofoo-- but of course, that's not what we want.
we need a local reference forfoo, to be passed intoinit. - so, we manually hacked
initlike this:
lo and behold, this allows us to provideasync function init(input, {foo}) { // <-- added ", {foo}"foolocally, not as an evil global :tada:import init, {greet} from "../rust/pkg/rust.js" const foo = () => console.log("foo!!") await init(undefined, {foo}) greet() //> "foo!!" - okay, so this works — but it's a horrible hack!
- it's surely a terrible idea to use
sedto inject{foo}into the generated javascriptinitfunction as a part of a built routine - we don't get any nifty generated typescript typings in the generated
.d.ts— we really want wasm-bindgen to provide the typings forinitthat includes these javascript functions — this is what the generated.d.tslooks like, it would just need a second argument:export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
- it's surely a terrible idea to use
- so, hacks aside — what's the actual way to do this?
- surely we're simply missing something — surely we don't need to literally regex-find-and-replace hack the generated javascript, just to properly call some javascript functions?
- hopefully, once we find the actual way to do this, we can improve the wasm-pack/wasm-bindgen docs to make it more clear and obvious
yet furthermore, in discussions on the rust discord, we found a different kind of strategy: for rust functions to accept javascript callback functions — this could actually be far better (but we need help here, too):
- we found via this documentation that we could accept javascript callback functions, and call them like this:
#[wasm_bindgen] pub fn greet1(name: &str, callback: &js_sys::Function) { let greeting = &format!("Hello, {}!", name); let _ = callback.call1( &JsValue::null(), &JsValue::from(greeting), ); } - this seemed well and good, until we discovered that the js_sys::Function docs show that js functions can only be called with up to 3 arguments (
call0,call1,call2,call3— there is nocall4) - we don't want to use
apply, because that requires creating a new Array object, which we perceive to be a performance cost (the whole point of using rust here is for performance-sensitive code) - so, here's our solution for calling more than three parameters on javascript callback functions with js_sys:
/src/callers.jsexport function call4(func, context, arg1, arg2, arg3, arg4) { return func.call(context, arg1, arg2, arg3, arg4) } export function call5(func, context, arg1, arg2, arg3, arg4, arg5) { return func.call(context, arg1, arg2, arg3, arg4, arg5) }/src/lib.rs
now actually making use of#[wasm_bindgen(module = "/src/callers.js")] extern "C" { #[wasm_bindgen(catch)] fn call4( this: &js_sys::Function, context: &JsValue, arg1: &JsValue, arg2: &JsValue, arg3: &JsValue, arg4: &JsValue, ) -> Result<JsValue, JsValue>; #[wasm_bindgen(catch)] fn call5( this: &js_sys::Function, context: &JsValue, arg1: &JsValue, arg2: &JsValue, arg3: &JsValue, arg4: &JsValue, arg5: &JsValue, ) -> Result<JsValue, JsValue>; }call4in/src/lib.rslooks like this:#[wasm_bindgen] pub fn greet2(name: &str, callback: &js_sys::Function) { let greeting = &format!("Hello, {}!", name); let _ = call4( &callback, &JsValue::null(), &JsValue::from(greeting), &JsValue::from("argument2"), &JsValue::from("argument3"), &JsValue::from("argument4"), ); } - so, of course, this seems awkward to use, but it does work. the idea of all that repeating code, for
call4through to likecall25or whatever, does makes us a little sad.
all in all, we've feel disgusted and ashamed by any solution we've been able to figure out.
with a little luck, hopefully there's just some plain documentation we've missed, or there's something basic about rust i don't know, and then i could merely feel silly, but continue with my projects :laughing:
:beers: thanks folks, grateful for any help, cheers!
Think the last construct with call1, ... , call3 could solve the design, e.g. receiving-js-closures-in-rust. The background for call4 is given below "Since Rust has no function overloading ...". This is also reflected in the web-sys api e.g. web-sys::Document.