Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Passing mutable struct to JavaScript callback and keep modifications in WebAssembly #3091

Open
xarantolus opened this issue Sep 22, 2022 · 0 comments
Labels

Comments

@xarantolus
Copy link

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!

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
Projects
None yet
Development

No branches or pull requests

1 participant