Wasm-bindgen: Get back struct returned by a JS Function js_sys::Function

Created on 5 Jul 2019  路  8Comments  路  Source: rustwasm/wasm-bindgen

Given a js_sys::Function which is a JS closure returning an instance of an exported Rust struct SomeObject, how can I get back the returned instance ?

Rust:

#[wasm_bindgen] 
pub struct SomeObject {}

#[wasm_bindgen] 
impl SomeObject {
    #[wasm_bindgen(constructor)]
    pub fn new() -> SomeObject {
        SomeObject {}
    }
}

#[wasm_bindgen] 
pub fn do_call(f: js_sys::Function) -> Result<(), JsValue> {
    // f returns an instance of SomeObject
    let ret: JsValue = f.call0(&JsValue::NULL)?;

    // XXX how to try_cast ret to its original SomeObject?

    // do something with SomeObject's instance

    Ok(())
}

JS:

wasm.do_call(function() {
    return new wasm.SomeObject();
});

I read the related issue https://github.com/rustwasm/wasm-bindgen/issues/1509 and quoting https://github.com/rustwasm/wasm-bindgen/issues/1509#issuecomment-489215237 :

[...] the only way currently to unwrap it is to call a by-value-self method on the JS object itself, which will do the unwrapping. [...]

However I don't know how to apply it in this situation.

I'm newbie therefore I may have missed something. Do you have any suggestions?

Thanks,
Gawen

help wanted

Most helpful comment

Thanks for the report @Gawen!

Currently this isn't directly supported but @Pauan is right in that we should have this! I don't think we'll be able to use From but what we can probably do is something like:

impl TryFrom<JsValue> for RustType {
    type Error = JsValue;
    // ...
}

internally that would do a check that JsValue has the right class in JS, it'd extract the ptr value, and then unwrap the value in Rust.

All 8 comments

I think it's reasonable for Rust structs to have an impl From<JsValue> for Foo implementation (which already exists for other types, but not Rust structs).

I also have this problem. I've found the following workaround, which involves wrapping the callback function in an object, and telling rust to treat it as a method on a duck-typed interface:

#[wasm_bindgen] 
pub struct SomeObject {}

#[wasm_bindgen] 
impl SomeObject {
    #[wasm_bindgen(constructor)]
    pub fn new() -> SomeObject {
        SomeObject {}
    }
}

#[wasm_bindgen]
extern "C" {
    pub type ExecReturnsSomeObject;

    #[wasm_bindgen(structural, method)]
    pub fn exec(this: &ExecReturnsSomeObject) -> SomeObject;
}

#[wasm_bindgen] 
pub fn do_call(f: &ExecReturnsSomeObject) -> Result<(), JsValue> {
    // f.exec() returns an instance of SomeObject
    let ret: SomeObject = f.exec();

    Ok(())
}
wasm.do_call({
    exec: function() {
        return new wasm.SomeObject();
    }
});

I believe the workaround suggested by https://github.com/rustwasm/wasm-bindgen/issues/1509#issuecomment-489215237 would look something like this (though I couldn't make the unwrap method take this by value, only by reference). It also uses some wrapping, but unlike the previous workaround, it wraps the return value, not the callback function:


#[wasm_bindgen]
pub struct SomeObject {}

#[wasm_bindgen] 
impl SomeObject {
    #[wasm_bindgen(constructor)]
    pub fn new() -> SomeObject {
        SomeObject {}
    }
}

#[wasm_bindgen]
extern "C" {
    pub type WrapperForSomeObject;

    #[wasm_bindgen(structural, method)]
    pub fn get_inner(this: &WrapperForSomeObject) -> SomeObject;
}

#[wasm_bindgen] 
pub fn do_call(f: &js_sys::Function) -> Result<(), JsValue> {
    use wasm_bindgen::JsCast;

    // f returns an instance of WrapperForSomeObject
    let ret: JsValue = f.call0(&JsValue::NULL)?;

    let obj: SomeObject = ret.dyn_into::<WrapperForSomeObject>()?.get_inner();

    Ok(())
}

class WrapperForSomeObject {
  constructor(obj) {
    this.obj = obj;
  }
  get_inner() {
    return this.obj;
  }
}

// ...

wasm.do_call(function() {
    const result = new wasm.SomeObject();
    return new WrapperForSomeObject(result);
});

@mstange thanks! your second workaround is neat. I'll use this pattern in the meantime.

Thanks for the report @Gawen!

Currently this isn't directly supported but @Pauan is right in that we should have this! I don't think we'll be able to use From but what we can probably do is something like:

impl TryFrom<JsValue> for RustType {
    type Error = JsValue;
    // ...
}

internally that would do a check that JsValue has the right class in JS, it'd extract the ptr value, and then unwrap the value in Rust.

This is a relatively meaty addition to wasm-bindgen, and if someone's curious to try it out it may be a good opportunity to learn about the macro and intrinsics! The general structure of this would look like:

  • The codegen (crates/backend/src/codegen.rs) would be updated to have the TryFrom impl (or some similar method)
  • The implementation itself would invoke some sort of intrinsic which will probably have the form fn #the_name(js: u32) -> *mut #the_rust_type;. If a non-null pointer is returned we'd then do the unwrapping process for the type (get it out of the RefCell and such), otherwise the original JsValue is returned.
  • The intrinsic would be recognized in crates/cli-support in the same way instanceof wrappers and such are already recognized.

A impl From<JsValue> for RustStruct implementation would also help interoperability with JS promises: If I have a duck-typed reference to a JS object in my Rust code, and that JS object has a method that returns a promise to a Rust struct, the Rust code cannot get the struct out of the promise. It can only get a JsValue out of the promise.
Luckily, the wrapper object workaround (see above) works for this use case, too.

#[wasm_bindgen] 
pub struct WasmMemBuffer {}

#[wasm_bindgen]
extern "C" {
    pub type FileAndPathHelper;

    /// Returns Promise of BufferWrapper
    /// (but wants to return Promise of WasmMemBuffer)
    #[wasm_bindgen(method)]
    fn readFile(this: &FileAndPathHelper, path: &str) -> Promise;

    pub type BufferWrapper;
    #[wasm_bindgen(method)]
    fn getBuffer(this: &BufferWrapper) -> WasmMemBuffer;
}

Don't mind me, just adding some search terms for this issue:

convert, cast

Was this page helpful?
0 / 5 - 0 ratings