Wasm-bindgen: How to chain method

Created on 8 Jun 2019  路  4Comments  路  Source: rustwasm/wasm-bindgen

Summary

I am trying to implement node like EventEmitter struct with this library. One of the methods requires to return mutable self-reference such as on method so that a user can chain methods to register multiple listeners.

Is there any way to implement this feature with wasm-bindgen?

Additional Details

const eventEmitter = new EventEmitter();
eventEmitter.on('eventA', () => {...})
  .on('eventB', () => {...})

eventEmitter.emit('eventA')

I get this error when I return mutable self-reference.

error: cannot return a borrowed ref with #[wasm_bindgen]
...
impl EventEmitter {
    pub fn(&mut self, event_name: String, callback: js_sys::Function) -> &mut EventEmitter {
        ...
        self
    }
}
...

I also tried returning a new struct with updated fields but it easily causes null pointer error.

question

All 4 comments

I would recommend against copying the JS API 1-to-1, because a lot of JS idioms don't work in Rust (and vice versa). Instead I recommend creating an API that fits in well with Rust.

And there's no need for the chaining functionality, since you can just do this:

let eventEmitter = EventEmitter::new();
eventEmitter.on("eventA", move |_| { ... });
eventEmitter.on("eventB", move |_| { ... });

However, if you really do want the chaining functionality, you should be able to achieve it like this:

use wasm_bindgen::prelude::*;
use wasm_bindgen::JsCast;

mod raw {
    use wasm_bindgen::prelude::*;

    #[wasm_bindgen(module = "events")]
    extern "C" {
        #[wasm_bindgen(js_name = default)]
        pub type EventEmitter;

        #[wasm_bindgen(constructor)]
        pub fn new() -> EventEmitter;

        #[wasm_bindgen(js_class = "default", method)]
        pub fn on(this: &EventEmitter, name: &str, listener: &js_sys::Function);

        #[wasm_bindgen(js_class = "default", method)]
        pub fn emit(this: &EventEmitter, name: &str, value: &JsValue) -> bool;
    }
}

pub struct EventEmitter {
    emitter: raw::EventEmitter,
}

impl EventEmitter {
    pub fn new() -> Self {
        Self {
            emitter: raw::EventEmitter::new(),
        }
    }

    pub fn on<F>(&mut self, name: &str, callback: F) -> &mut Self
        where F: FnMut(JsValue) + 'static {

        let closure = Closure::wrap(Box::new(callback) as Box<dyn FnMut(JsValue)>);
        self.emitter.on(name, closure.as_ref().unchecked_ref());
        self
    }

    pub fn emit(&self, name: &str, value: &JsValue) -> bool {
        self.emitter.emit(name, value)
    }
}

Basically, you create some raw bindings (which don't include chaining), and then you create a Rust struct which wraps the raw bindings (and provides chaining).

This is a quite common technique which is used to create more "Rust-like" APIs on top of JavaScript, rather than exposing the raw JS APIs directly.

Emm, I also faced this problem and finally use some black magic to solve it by modifying js file:

fp = path.resolve(__dirname, './pkg/your_module.js');
const content = fs.readFileSync(fp, {encoding: 'utf8'})
  .replace(/(@returns {void}\n\s+\*\/)\n\s+static (\S+?\([\s\S]+?){\n\s+([\s\S\n]+?)}/g, (_, comm, funh, funb) => {
    return `${comm.replace('void', 'this')}
    static ${funh} {
      ${funb.replace('return ', '')}return this;
    }
    `
  });
fs.writeFileSync(fp, content);

Hope to help you.

I think @Pauan answered this well enough so closing, but feel free to comment if there are still issues!

@Pauan Thank you!

Was this page helpful?
0 / 5 - 0 ratings