Description
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!