boa-dev / boa

Boa is an embeddable and experimental Javascript engine written in Rust. Currently, it has support for some of the language.
MIT License
5.01k stars 397 forks source link

Add example of passing Rust reference to Boa for use in instance methods #1748

Open d00z3l opened 2 years ago

d00z3l commented 2 years ago

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.

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, Clone)]
    struct MyApiInner {
        val: i32 // This would actually be a struct or more complex object
    }
    impl MyApiInner {
        pub fn do_something(&self, val: i32) -> JsValue {
            JsValue::Integer(val + self.val)
        }
    }

    #[derive(Debug, Clone)]
    struct MyApi {
        inner: Arc<RwLock<MyApiInner>>
    }
    impl MyApi {
        pub fn new() -> Self {
            Self {
                inner: Arc::new(RwLock::new(MyApiInner{val: 1}))
            }
        }
        pub fn do_something(&self, val: i32) -> JsValue {
            self.inner.read().unwrap().do_something(val)
        }
    }

    // This struct is passed to a closure as a capture.
    #[derive(Debug, Clone, Trace, Finalize)]
    struct BigStruct {
        api: MyApi
    }

    // We create a new `JsObject` with some data
    let object = context.construct_object();
    object.define_property_or_throw(
        "name",
        PropertyDescriptor::builder()
            .value("Boa dev")
            .writable(false)
            .enumerable(false)
            .configurable(false),
        &mut context,
    )?;

    // Now, we execute some operations that return a `Clone` type
    let clone_variable = BigStruct {
        api: MyApi::new() // This would actually take a reference to a object
    };

    // 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.api.do_something(args[0].to_i32(context).unwrap());

            // We convert `message` into `Jsvalue` to be able to return it.
            Ok(val.into())
        },
        // Here is where we move `clone_variable` into the closure.
        clone_variable,
    )
    // And here we assign `createMessage` to the `name` property of the closure.
    .name("createMessage")
    // 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.
        "createMessage",
        // 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,
    );

    assert_eq!(
        context.eval("createMessage(1)")?,
        "message from `Boa dev`: Hello!".into()
    );

    // The data mutates between calls
    assert_eq!(
        context.eval("createMessage(); createMessage();")?,
        "message from `Boa dev`: Hello! Hello! Hello!".into()
    );

    // We have moved `Clone` variables into a closure and executed that closure
    // inside Javascript!

    Ok(())
}
d00z3l commented 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(())
}
d00z3l commented 2 years ago

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(())
}
jedel1043 commented 2 years ago

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 :)

jedel1043 commented 2 years ago

Related: #1161

d00z3l commented 2 years ago

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 would be a struct holding the rust data that is being shared with JavaScript via the closure api.

#[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());

}