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

Passing mutable struct to JavaScript callback and keep modifications in WebAssembly

Open xarantolus opened this issue 3 years ago • 0 comments

Summary

Hi, I'm currently having a problem when passing a struct from Rust to JavaScript, as I want to keep/receive modifications made by a JavaScript callback in Rust. I also asked this on Stack Overflow.

I'll include the code snippets here, but for a full example you can get run quickly check out this Git repo.

As far as I am aware it's not possible to use serde to pass data in this case, because I want to call functions on the JavaScript object. Or did I miss something here?

Additional Details

Basically what I have is the following struct (see lib.rs in example):

#[wasm_bindgen]
#[derive(Debug, Clone, Serialize, Deserialize)]
// Note that SomeStruct must not implement the Copy trait, as in the not-minimal-example I have Vec<>s in the struct
pub struct SomeStruct {
    pub(crate) field_to_be_modified: i32,
}


#[wasm_bindgen]
impl SomeStruct {
    #[wasm_bindgen(constructor)]
    pub fn new() -> Self {
        set_panic_hook();

        Self {
            field_to_be_modified: 0,
        }
    }

    pub fn modify_field(&mut self, value: i32) {
        self.field_to_be_modified = value;
    }

    pub fn field(&self) -> i32 {
        self.field_to_be_modified
    }

    #[wasm_bindgen]
    pub async fn with_callback(&self, function_or_promise: JsValue) -> Result<JsValue, JsValue> {
        let mut s = SomeStruct::new();
        let function = function_or_promise.dyn_into::<Function>().map_err(|_| {
            JsError::new("The provided callback is not a function. Please provide a function.")
        })?;

        // run_any_function runs either a promise or a function
        run_any_function(&mut s, function, vec![JsValue::from(1u32)]).await
    }
}
Open rest of the code & how the JS function is called
pub(crate) async fn run_any_function(
    ax: &mut SomeStruct,
    function_or_promise: js_sys::Function,
    arguments: Vec<JsValue>,
) -> Result<JsValue, JsValue> {
    let result = run_function(ax, function_or_promise, arguments)?;

    // Handle functions defined like "async function(args) {}"
    if result.has_type::<js_sys::Promise>() {
        return run_promise(result).await;
    } else {
        Ok(result)
    }
}

async fn run_promise(promise_arg: JsValue) -> Result<JsValue, JsValue> {
    let promise = js_sys::Promise::from(promise_arg);
    let future = JsFuture::from(promise);
    future.await
}

fn run_function(
    my_struct: &mut SomeStruct,
    function: js_sys::Function,
    arguments: Vec<JsValue>,
) -> Result<JsValue, JsValue> {
    let args = Array::new();

    // This is the reason modifications from JS aren't reflected in Rust, but without it JsValue::from doesn't work
    let clone = my_struct.clone();

    // my_struct is the first function argument
    // TODO: JsValue::from only works when cloned, not on the original struct. Why?
    // Best would be directly passing my_struct, as then modifications would work
    // Passing a pointer to the struct would also be fine, as long as methods can be called on it from JavaScript
    args.push(&JsValue::from(clone));

    for arg in arguments {
        args.push(&arg);
    }

    // Actually call the function
    let result = function.apply(&JsValue::NULL, &args)?;

    // TODO: How to turn result back into a SomeStruct struct?

    // Copying fields manually also doesn't work because of borrow checker:
    // my_struct.field_to_be_modified = clone.field_to_be_modified;

    Ok(result)
}

And I want to use it from JS like the following (see index.html in example):

import * as mve from './pkg/mve.js';

async function run() {
    let module = await mve.default();

    let s = new mve.SomeStruct();

    console.log("Initial value (should be 0):", s.field());

    await s.with_callback(function(s_instance, second_arg, third_arg) {
        // s_instance is of type SomeStruct, and is a COPY of s

        console.log("callback was called with parameter", s_instance, second_arg, third_arg);

        console.log("Current field value (should be 0):", s_instance.field());

        console.log("Setting field to 42");

        // This only modifies the copy
        s_instance.modify_field(42);

        console.log("Field value after setting (should be 42):", s_instance.field());

        console.log("end callback");

        // TODO: Directly calling methods on s also does not work either
        // Error: recursive use of an object detected which would lead to unsafe aliasing in rust
        //
        // s.modify_field(43);
    })

    console.log("This should be after \"end callback\"");

    // TODO: the original s is unchanged, so
    // this does not work, as the callback operated on the cloned s_instance
    // TODO: How to make this work?
    console.log("Field value after callback (should be 42):", s.field());
}

run();

Another problem I ran into is when using s directly in the callback (this would circumvent my problem sufficiently, however it doesn't work): Error: recursive use of an object detected which would lead to unsafe aliasing in rust. So what I'm trying to do seems impossible, but I think it shouldn't be?

The problem is that I can't figure out how to pass a mutable reference of that struct (as a JsValue) to the JavaScript function. This is why in run_function a clone of the struct is passed, but obviously this doesn't keep the modifications the JS Code does to that struct.

Is there a way to pass a reference of the struct (as JsValue) to the JS function directly, without cloning? Or is there another way to keep modifications? I would also be happy about a way to pass a clone, and then copy the modifications to the original struct (but this didn't work because of borrow checker errors -- after all, I can't seem to access clone after it was passed to JsValue::from.

Thank you in advance!

xarantolus avatar Sep 22 '22 09:09 xarantolus