Open d00z3l opened 2 years ago
With the help from Discord it seems I can use Gc<GcCell
use boa::{
gc::{Finalize, Trace},
object::{FunctionBuilder},
property::{Attribute},
Context, JsValue,
};
use gc::{Gc, GcCell};
fn main() -> Result<(), JsValue> {
// We create a new `Context` to create a new Javascript executor.
let mut context = Context::new();
// We have created a closure with moved variables and executed that closure
// inside Javascript!
#[derive(Debug, Trace, Finalize)]
struct MyStuff {
val: i32
}
impl MyStuff {
pub fn do_something(&self, val: i32) -> JsValue {
JsValue::Integer(val + self.val)
}
}
// This struct is passed to a closure as a capture.
#[derive(Debug, Clone, Trace, Finalize)]
struct Api {
stuff: Gc<GcCell<MyStuff>>
}
impl Api {
pub fn do_something(&self, val: i32) -> JsValue {
let stuff = self.stuff.borrow();
stuff.do_something(val)
}
}
// Now, we execute some operations that return a `Clone` type
let clone_variable = Api {
stuff: Gc::new(GcCell::new(MyStuff { val: 1 }))
};
// We can use `FunctionBuilder` to define a closure with additional
// captures.
let js_function = FunctionBuilder::closure_with_captures(
&mut context,
|_, args, captures, context| {
let val = captures.do_something(args[0].to_i32(context).unwrap());
Ok(val)
},
// Here is where we move `clone_variable` into the closure.
clone_variable,
)
// And here we assign `test` to the `name` property of the closure.
.name("test")
// By default all `FunctionBuilder`s set the `length` property to `0` and
// the `constructable` property to `false`.
.build();
// We bind the newly constructed closure as a global property in Javascript.
context.register_global_property(
// We set the key to access the function the same as its name for
// consistency, but it may be different if needed.
"test",
// We pass `js_function` as a property value.
js_function,
// We assign to the "createMessage" property the desired attributes.
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
);
let val = context.eval("test(1)")?;
assert_eq!(
val,
2.into()
);
// We have moved `Clone` variables into a closure and executed that closure
// inside Javascript!
Ok(())
}
Here is a more realistic example:
// This example goes into the details on how to pass closures as functions
// inside Rust and call them from Javascript.
use boa::{
gc::{Finalize, Trace},
object::{FunctionBuilder},
property::{Attribute},
Context, JsValue,
};
use gc::{Gc, GcCell};
#[derive(Debug, Trace, Finalize)]
struct ApiInner {
val: i32
}
impl ApiInner {
pub fn get(&self) -> JsValue {
JsValue::Integer(self.val)
}
pub fn set(&mut self, val: i32) {
self.val = val;
}
pub fn add(&self, val: i32) -> JsValue {
JsValue::Integer(self.val + val)
}
}
// This struct is passed to a closure as a capture.
#[derive(Debug, Clone, Trace, Finalize)]
struct Api {
inner: Gc<GcCell<ApiInner>>
}
impl Api {
pub fn get(&self) -> JsValue {
let inner = self.inner.borrow();
inner.get()
}
pub fn set(&self, val: i32) {
let mut inner = self.inner.borrow_mut();
inner.set(val)
}
pub fn add(&mut self, val: i32) -> JsValue {
let inner = self.inner.borrow();
inner.add(val)
}
}
fn main() -> Result<(), JsValue> {
// We create a new `Context` to create a new Javascript executor.
let mut context = Context::new();
// We have created a closure with moved variables and executed that closure
// inside Javascript!
// Now, we execute some operations that return a `Clone` type
let clone_variable = Api {
inner: Gc::new(GcCell::new(ApiInner { val: 1 }))
};
// We can use `FunctionBuilder` to define a closure with additional
// captures.
let js_function = FunctionBuilder::closure_with_captures(
&mut context,
|_, _, captures, _| {
let val = captures.get();
Ok(val)
},
// Here is where we move `clone_variable` into the closure.
clone_variable.clone(),
)
// And here we assign `test` to the `name` property of the closure.
.name("get")
// By default all `FunctionBuilder`s set the `length` property to `0` and
// the `constructable` property to `false`.
.build();
// We bind the newly constructed closure as a global property in Javascript.
context.register_global_property(
// We set the key to access the function the same as its name for
// consistency, but it may be different if needed.
"get",
// We pass `js_function` as a property value.
js_function,
// We assign to the "createMessage" property the desired attributes.
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
);
// We can use `FunctionBuilder` to define a closure with additional
// captures.
let js_function = FunctionBuilder::closure_with_captures(
&mut context,
|_, args, captures, context| {
let val = args[0].to_i32(context).unwrap();
captures.set(val);
Ok(val.into())
},
// Here is where we move `clone_variable` into the closure.
clone_variable.clone(),
)
// And here we assign `test` to the `name` property of the closure.
.name("set")
// By default all `FunctionBuilder`s set the `length` property to `0` and
// the `constructable` property to `false`.
.build();
// We bind the newly constructed closure as a global property in Javascript.
context.register_global_property(
// We set the key to access the function the same as its name for
// consistency, but it may be different if needed.
"set",
// We pass `js_function` as a property value.
js_function,
// We assign to the "createMessage" property the desired attributes.
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
);
// We can use `FunctionBuilder` to define a closure with additional
// captures.
let js_function = FunctionBuilder::closure_with_captures(
&mut context,
|_, args, captures, context| {
let val = args[0].to_i32(context).unwrap();
let val = captures.add(val);
Ok(val.into())
},
// Here is where we move `clone_variable` into the closure.
clone_variable,
)
// And here we assign `test` to the `name` property of the closure.
.name("add")
// By default all `FunctionBuilder`s set the `length` property to `0` and
// the `constructable` property to `false`.
.build();
// We bind the newly constructed closure as a global property in Javascript.
context.register_global_property(
// We set the key to access the function the same as its name for
// consistency, but it may be different if needed.
"add",
// We pass `js_function` as a property value.
js_function,
// We assign to the "createMessage" property the desired attributes.
Attribute::WRITABLE | Attribute::NON_ENUMERABLE | Attribute::CONFIGURABLE,
);
let val = context.eval("get()")?;
assert_eq!(
val,
1.into()
);
let val = context.eval("set(5)")?;
assert_eq!(
val,
5.into()
);
let val = context.eval("get()")?;
assert_eq!(
val,
5.into()
);
let val = context.eval("add(5)")?;
assert_eq!(
val,
10.into()
);
// We have moved `Clone` variables into a closure and executed that closure
// inside Javascript!
Ok(())
}
Thank you for your snippets! Even if the issue is resolved, I'm gonna keep it open as a reminder to add this snippet as an example of how to pass references between Rust and the JS engine :)
Related: #1161
Follow up question on Gc and Trace. I don't really understand the purpose of Trace and whether I can safely ignore it in the Api struct if it is going to be exclusivity used in closures to create an API?
Take the example below, in the real world example the Vec
#[derive(Debug, Clone, gc::Trace, gc::Finalize)]
struct Api {
#[unsafe_ignore_trace]
tuple: Arc<RwLock<Vec<u32>>>
}
impl Api {
fn new() -> Self {
Self {
tuple: Arc::new(RwLock::new(vec![0, 1, 2]))
}
}
fn get(&self, index: i32) -> u32 {
self.tuple.read()[index as usize]
}
fn set(&self, index: i32, val: u32) {
self.tuple.write()[index as usize] = val
}
}
#[test]
fn test_boa() {
let mut ctx = boa::Context::new();
let api = Api::new();
let mut rng = rand::thread_rng();
let fake_id = rng.gen_range(1..u32::MAX);
api.set(0, fake_id);
// Need to wrap the item in Gc
let api = gc::Gc::new(gc::GcCell::new(api));
let func = boa::object::FunctionBuilder::closure_with_captures(
&mut ctx,
|_, args, api, ctx| {
let val = args[0].to_i32(ctx).unwrap();
let api = api.borrow();
let val = api.get(val);
Ok(val.into())
},
api.clone(),
).name("get").build();
ctx.register_global_property(
"get",
func,
boa::property::Attribute::WRITABLE | boa::property::Attribute::NON_ENUMERABLE | boa::property::Attribute::CONFIGURABLE,
);
let func = boa::object::FunctionBuilder::closure_with_captures(
&mut ctx,
|_, args, api, ctx| {
let index = args[0].to_i32(ctx).unwrap();
let val = args[1].to_u32(ctx).unwrap();
let mut api = api.borrow_mut();
api.set(index, val);
Ok(boa::JsValue::null())
},
api.clone(),
).name("set").build();
ctx.register_global_property(
"set",
func,
boa::property::Attribute::WRITABLE | boa::property::Attribute::NON_ENUMERABLE | boa::property::Attribute::CONFIGURABLE,
);
let val = ctx.eval("get(0)").unwrap();
assert_eq!(val, fake_id.into());
let val = ctx.eval("get(1)").unwrap();
assert_eq!(val, 1.into());
ctx.eval("set(1, 10)").unwrap();
let val = ctx.eval("get(1)").unwrap();
assert_eq!(val, 10.into());
}
Feature I would like to be pass a reference to a rust object to Boa so it can be used in to retrieve data from rust objects or to update values in rust objects.
closure_with_captures
seems the closest to making it happen in the current API. Below is some mocked up code based on the 'closures' example of what I would like to do. Not sure if this possible or my methodology is wrong for how to do it.