1

I'm trying to pass a struct from Rust to a JavaScript callback with wasm_bindgen. The JavaScript code should be able to call methods defined in Rust.

This is why this guide doesn't work in my case, as there only objects are passed (you cannot call methods defined in Rust on them), not with references to the actual object in "Rust-Memory".

So basically I have a SomeStruct in Rust:

#[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(&mut s, function, vec![JsValue::from(1u32)]).await
    }
}

However, in order to turn SomeStruct into a JsValue I can pass to JavaScript, I need to clone it first (otherwise the JsValue::from call doesn't compile):

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;

Now I want to modify the field_to_be_modified within the callback passed to with_callback, so I'm calling s_instance.modify_field(42); from JavaScript:

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
    // TODO: How to make this work?
    console.log("Field value after callback (should be 42):", s.field());
}

run();

This callback obviously only changes the cloned value that was passed to it, so the final s.field() call yields 0. However, I cannot

  • read from the cloned value in Rust after the function is called because of borrow checker errors
  • pass the actual object I want to modify, as only a copy works

So the question is how I can modify SomeStruct in JavaScript and get it back in Rust. I don't care whether it's unsafe or not, I just want it to work.

I also created a minimum example with the code from this question so you can quickly run it, see the Git repo:

git clone https://github.com/xarantolus/rust-wasm-bindgen-stackoverflow-question

Clone the repo, follow the installation steps for wasm-pack and execute the following to see the problem on your machine:

wasm-pack build --target web && python3 -m http.server

I have also opened an issue for this problem.

xarantolus
  • 1,961
  • 1
  • 11
  • 19
  • Maybe use of `Rc` could solve issue with sharing of struct between wasm and JS, this answer explains how to use `Rc` in `wasm-bindgen` https://stackoverflow.com/a/72881156/5396835. The `wasm-bindgen` example [Paint use Rc](https://github.com/rustwasm/wasm-bindgen/blob/main/examples/paint/src/lib.rs#L20) shows for a full application. – Jonas Bojesen Sep 23 '22 at 05:56

0 Answers0