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

How to properly call JavaScript functions from Rust?

Open chase-moskal opened this issue 3 years ago • 1 comments

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:

  1. given this rust code
    use wasm_bindgen::prelude::*;
    
    #[wasm_bindgen]
    extern "C" {
    
      #[wasm_bindgen]
      fn foo();
    }
    
    #[wasm_bindgen]
    pub fn greet() {
      foo();
    }
    
  2. 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.
    
  3. so, next in the generated javascript file's init, we see this:
    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);
    
    we're so close!
    here we can see the (naughty) global reference to foo -- but of course, that's not what we want.
    we need a local reference for foo, to be passed into init.
  4. so, we manually hacked init like this:
    async function init(input, {foo}) { // <-- added ", {foo}"
    
    lo and behold, this allows us to provide foo locally, 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!!"
    
  5. okay, so this works — but it's a horrible hack!
    • it's surely a terrible idea to use sed to inject {foo} into the generated javascript init function 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 for init that includes these javascript functions — this is what the generated .d.ts looks like, it would just need a second argument:
      export default function init (module_or_path?: InitInput | Promise<InitInput>): Promise<InitOutput>;
      
  6. 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):

  1. 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),
      );
    }
    
  2. 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 no call4)
  3. 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)
  4. so, here's our solution for calling more than three parameters on javascript callback functions with js_sys: /src/callers.js
    export 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
    #[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>;
    }
    
    now actually making use of call4 in /src/lib.rs looks 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"),
      );
    }
    
  5. so, of course, this seems awkward to use, but it does work. the idea of all that repeating code, for call4 through to like call25 or 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!

chase-moskal avatar Jun 02 '22 01:06 chase-moskal

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.

jonboj avatar Jul 02 '22 08:07 jonboj