From c115223ea798e344984360bbe874b26841b7986e Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 28 Mar 2024 11:32:16 -0700 Subject: [PATCH 1/6] Add a boa_interop crate This crate will contain types and functions to help integrating boa in Rust projects, making it easier to interop between the host and the JavaScript code. See https://github.com/boa-dev/boa/discussions/3770 --- Cargo.lock | 12 +++ core/interop/ABOUT.md | 33 ++++++++ core/interop/Cargo.toml | 22 +++++ core/interop/src/lib.rs | 120 ++++++++++++++++++++++++++++ core/interop/src/loaders.rs | 6 ++ core/interop/src/loaders/hashmap.rs | 46 +++++++++++ 6 files changed, 239 insertions(+) create mode 100644 core/interop/ABOUT.md create mode 100644 core/interop/Cargo.toml create mode 100644 core/interop/src/lib.rs create mode 100644 core/interop/src/loaders.rs create mode 100644 core/interop/src/loaders/hashmap.rs diff --git a/Cargo.lock b/Cargo.lock index 98b1582478f..45dfad54937 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -502,6 +502,18 @@ dependencies = [ "static_assertions", ] +[[package]] +name = "boa_interop" +version = "0.18.0" +dependencies = [ + "boa_engine", + "boa_gc", + "proc-macro2", + "quote", + "syn 2.0.55", + "synstructure", +] + [[package]] name = "boa_macros" version = "0.18.0" diff --git a/core/interop/ABOUT.md b/core/interop/ABOUT.md new file mode 100644 index 00000000000..3cde6cc6036 --- /dev/null +++ b/core/interop/ABOUT.md @@ -0,0 +1,33 @@ +# About Boa + +Boa is an open-source, experimental ECMAScript Engine written in Rust for +lexing, parsing and executing ECMAScript/JavaScript. Currently, Boa supports some +of the [language][boa-conformance]. More information can be viewed at [Boa's +website][boa-web]. + +Try out the most recent release with Boa's live demo +[playground][boa-playground]. + +## Boa Crates + +- [**`boa_ast`**][ast] - Boa's ECMAScript Abstract Syntax Tree. +- [**`boa_engine`**][engine] - Boa's implementation of ECMAScript builtin objects and + execution. +- [**`boa_gc`**][gc] - Boa's garbage collector. +- [**`boa_interner`**][interner] - Boa's string interner. +- [**`boa_parser`**][parser] - Boa's lexer and parser. +- [**`boa_profiler`**][profiler] - Boa's code profiler. +- [**`boa_icu_provider`**][icu] - Boa's ICU4X data provider. +- [**`boa_runtime`**][runtime] - Boa's WebAPI features. + +[boa-conformance]: https://boajs.dev/boa/test262/ +[boa-web]: https://boajs.dev/ +[boa-playground]: https://boajs.dev/boa/playground/ +[ast]: https://boajs.dev/boa/doc/boa_ast/index.html +[engine]: https://boajs.dev/boa/doc/boa_engine/index.html +[gc]: https://boajs.dev/boa/doc/boa_gc/index.html +[interner]: https://boajs.dev/boa/doc/boa_interner/index.html +[parser]: https://boajs.dev/boa/doc/boa_parser/index.html +[profiler]: https://boajs.dev/boa/doc/boa_profiler/index.html +[icu]: https://boajs.dev/boa/doc/boa_icu_provider/index.html +[runtime]: https://boajs.dev/boa/doc/boa_runtime/index.html diff --git a/core/interop/Cargo.toml b/core/interop/Cargo.toml new file mode 100644 index 00000000000..b425e555ac7 --- /dev/null +++ b/core/interop/Cargo.toml @@ -0,0 +1,22 @@ +[package] +name = "boa_interop" +description = "Interop utilities for integrating boa with a Rust host." +keywords = ["javascript", "js", "interop"] +categories = ["api-bindings"] +version.workspace = true +edition.workspace = true +authors.workspace = true +license.workspace = true +repository.workspace = true +rust-version.workspace = true + +[dependencies] +boa_engine.workspace = true +boa_gc.workspace = true +quote = "1.0.34" +syn = { version = "2.0.55", features = ["full"] } +proc-macro2 = "1.0" +synstructure = "0.13" + +[lints] +workspace = true diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs new file mode 100644 index 00000000000..1ad06b8ef1f --- /dev/null +++ b/core/interop/src/lib.rs @@ -0,0 +1,120 @@ +use std::cell::RefCell; + +use boa_engine::{Context, JsString, JsValue, Module, NativeFunction, Source}; +use boa_engine::builtins::promise::PromiseState; +use boa_engine::module::SyntheticModuleInitializer; + +pub mod loaders; + +pub trait IntoJsModule { + fn into_js_module(self, context: &mut Context) -> Module; +} + +impl + Clone> IntoJsModule for T { + fn into_js_module(self, context: &mut Context) -> Module { + let (names, fns): (Vec<_>, Vec<_>) = self.into_iter().unzip(); + let exports = names.clone(); + + Module::synthetic( + exports.as_slice(), + unsafe { + SyntheticModuleInitializer::from_closure(move |module, context| { + for (name, f) in names.iter().zip(fns.iter()) { + module + .set_export(name, f.clone().to_js_function(context.realm()).into())?; + } + Ok(()) + }) + }, + None, + context, + ) + } +} + +pub trait IntoJsFunction { + fn into_js_function(self, context: &mut Context) -> NativeFunction; +} + +impl () + 'static> IntoJsFunction for T { + fn into_js_function(self, _context: &mut Context) -> NativeFunction { + let s = RefCell::new(self); + + unsafe { + NativeFunction::from_closure(move |_, _, _| { + s.borrow_mut()(); + Ok(JsValue::undefined()) + }) + } + } +} + +#[test] +pub fn into_js_module() { + use boa_engine::{js_string, JsValue}; + use std::rc::Rc; + + let loader = Rc::new(loaders::HashMapModuleLoader::new()); + let mut context = Context::builder() + .module_loader(loader.clone()) + .build() + .unwrap(); + + let foo_count = Rc::new(RefCell::new(0)); + let bar_count = Rc::new(RefCell::new(0)); + let module = vec![ + ( + js_string!("foo"), + IntoJsFunction::into_js_function( + { + let foo_count = foo_count.clone(); + move || { + *foo_count.borrow_mut() += 1; + } + }, + &mut context, + ), + ), + ( + js_string!("bar"), + IntoJsFunction::into_js_function( + { + let bar_count = bar_count.clone(); + move || { + *bar_count.borrow_mut() += 1; + } + }, + &mut context, + ), + ), + ] + .into_iter() + .into_js_module(&mut context); + + loader.register(js_string!("test"), module); + + let source = Source::from_bytes( + r" + import * as test from 'test'; + let result = test.foo(); + for (let i = 0; i < 10; i++) { + test.bar(); + } + + result + ", + ); + let root_module = Module::parse(source, None, &mut context).unwrap(); + + let promise_result = root_module.load_link_evaluate(&mut context); + context.run_jobs(); + + // Checking if the final promise didn't return an error. + let PromiseState::Fulfilled(v) = promise_result.state() else { + panic!("module didn't execute successfully!") + }; + + assert_eq!(*foo_count.borrow(), 1); + assert_eq!(*bar_count.borrow(), 10); + assert_eq!(v, JsValue::undefined()); +} diff --git a/core/interop/src/loaders.rs b/core/interop/src/loaders.rs new file mode 100644 index 00000000000..d9e332df89e --- /dev/null +++ b/core/interop/src/loaders.rs @@ -0,0 +1,6 @@ +//! A collection of JS [boa_engine::module::ModuleLoader]s utilities to help in +//! creating custom module loaders. + +pub use hashmap::HashMapModuleLoader; + +pub mod hashmap; diff --git a/core/interop/src/loaders/hashmap.rs b/core/interop/src/loaders/hashmap.rs new file mode 100644 index 00000000000..f626a53a30a --- /dev/null +++ b/core/interop/src/loaders/hashmap.rs @@ -0,0 +1,46 @@ +use std::collections::HashMap; + +use boa_engine::{Context, JsNativeError, JsResult, JsString, Module}; +use boa_engine::module::{ModuleLoader, Referrer}; +use boa_gc::GcRefCell; + +/// A ModuleLoader that loads modules from a HashMap based on the name. +/// After registering modules, this loader will look for the exact name +/// in its internal map to resolve. +#[derive(Debug, Clone)] +pub struct HashMapModuleLoader(GcRefCell>); + +impl HashMapModuleLoader { + pub fn new() -> Self { + Self(GcRefCell::new(HashMap::new())) + } + + pub fn register(&self, key: impl Into, value: Module) { + self.0.borrow_mut().insert(key.into(), value); + } +} + +impl ModuleLoader for HashMapModuleLoader { + fn load_imported_module( + &self, + _referrer: Referrer, + specifier: JsString, + finish_load: Box, &mut Context)>, + context: &mut Context, + ) { + // First, try to resolve from our internal cached. + if let Some(module) = self.0.borrow().get(&specifier) { + finish_load(Ok(module.clone()), context); + } else { + let err = JsNativeError::typ().with_message(format!( + "could not find module `{}`", + specifier.to_std_string_escaped() + )); + finish_load(Err(err.into()), context); + } + } + + fn get_module(&self, specifier: JsString) -> Option { + self.0.borrow().get(&specifier).cloned() + } +} From 1afa66f39040f9653309db09b414c321889a3822 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 28 Mar 2024 14:08:47 -0700 Subject: [PATCH 2/6] Remove unnecessary into_iter() --- core/interop/src/lib.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index 1ad06b8ef1f..8bf38fc8d00 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -88,7 +88,6 @@ pub fn into_js_module() { ), ), ] - .into_iter() .into_js_module(&mut context); loader.register(js_string!("test"), module); From 8261a35c5d8f9b0ad38e0fbdf72a6d34e0be34bb Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 28 Mar 2024 14:09:50 -0700 Subject: [PATCH 3/6] cargo fmt --- core/interop/src/lib.rs | 2 +- core/interop/src/loaders/hashmap.rs | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index 8bf38fc8d00..1f9a7f48abf 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -1,8 +1,8 @@ use std::cell::RefCell; -use boa_engine::{Context, JsString, JsValue, Module, NativeFunction, Source}; use boa_engine::builtins::promise::PromiseState; use boa_engine::module::SyntheticModuleInitializer; +use boa_engine::{Context, JsString, JsValue, Module, NativeFunction, Source}; pub mod loaders; diff --git a/core/interop/src/loaders/hashmap.rs b/core/interop/src/loaders/hashmap.rs index f626a53a30a..c0ac8ee3b89 100644 --- a/core/interop/src/loaders/hashmap.rs +++ b/core/interop/src/loaders/hashmap.rs @@ -1,7 +1,7 @@ use std::collections::HashMap; -use boa_engine::{Context, JsNativeError, JsResult, JsString, Module}; use boa_engine::module::{ModuleLoader, Referrer}; +use boa_engine::{Context, JsNativeError, JsResult, JsString, Module}; use boa_gc::GcRefCell; /// A ModuleLoader that loads modules from a HashMap based on the name. From 79ea9029d4d661a10eb17e8016616dc12232b9cf Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Thu, 28 Mar 2024 14:21:09 -0700 Subject: [PATCH 4/6] cargo clippy --- Cargo.lock | 5 +---- core/interop/Cargo.toml | 5 +---- core/interop/src/lib.rs | 14 ++++++++++---- core/interop/src/loaders.rs | 2 +- core/interop/src/loaders/hashmap.rs | 25 +++++++++++++++++++++---- 5 files changed, 34 insertions(+), 17 deletions(-) diff --git a/Cargo.lock b/Cargo.lock index 45dfad54937..a9a68a02e5f 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -508,10 +508,7 @@ version = "0.18.0" dependencies = [ "boa_engine", "boa_gc", - "proc-macro2", - "quote", - "syn 2.0.55", - "synstructure", + "rustc-hash", ] [[package]] diff --git a/core/interop/Cargo.toml b/core/interop/Cargo.toml index b425e555ac7..454b2b1c5c1 100644 --- a/core/interop/Cargo.toml +++ b/core/interop/Cargo.toml @@ -13,10 +13,7 @@ rust-version.workspace = true [dependencies] boa_engine.workspace = true boa_gc.workspace = true -quote = "1.0.34" -syn = { version = "2.0.55", features = ["full"] } -proc-macro2 = "1.0" -synstructure = "0.13" +rustc-hash = { workspace = true, features = ["std"] } [lints] workspace = true diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index 1f9a7f48abf..fc073f3f1f1 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -1,12 +1,14 @@ +//! Interop utilities between Boa and its host. use std::cell::RefCell; -use boa_engine::builtins::promise::PromiseState; use boa_engine::module::SyntheticModuleInitializer; -use boa_engine::{Context, JsString, JsValue, Module, NativeFunction, Source}; +use boa_engine::{Context, JsString, JsValue, Module, NativeFunction}; pub mod loaders; +/// A trait to convert a type into a JS module. pub trait IntoJsModule { + /// Converts the type into a JS module. fn into_js_module(self, context: &mut Context) -> Module; } @@ -32,11 +34,13 @@ impl + Clone> IntoJsModule fo } } +/// A trait to convert a type into a JS function. pub trait IntoJsFunction { + /// Converts the type into a JS function. fn into_js_function(self, context: &mut Context) -> NativeFunction; } -impl () + 'static> IntoJsFunction for T { +impl IntoJsFunction for T { fn into_js_function(self, _context: &mut Context) -> NativeFunction { let s = RefCell::new(self); @@ -50,8 +54,10 @@ impl () + 'static> IntoJsFunction for T { } #[test] +#[allow(clippy::missing_panics_doc)] pub fn into_js_module() { - use boa_engine::{js_string, JsValue}; + use boa_engine::builtins::promise::PromiseState; + use boa_engine::{js_string, JsValue, Source}; use std::rc::Rc; let loader = Rc::new(loaders::HashMapModuleLoader::new()); diff --git a/core/interop/src/loaders.rs b/core/interop/src/loaders.rs index d9e332df89e..663210dde87 100644 --- a/core/interop/src/loaders.rs +++ b/core/interop/src/loaders.rs @@ -1,4 +1,4 @@ -//! A collection of JS [boa_engine::module::ModuleLoader]s utilities to help in +//! A collection of JS [`boa_engine::module::ModuleLoader`]s utilities to help in //! creating custom module loaders. pub use hashmap::HashMapModuleLoader; diff --git a/core/interop/src/loaders/hashmap.rs b/core/interop/src/loaders/hashmap.rs index c0ac8ee3b89..947c73ff595 100644 --- a/core/interop/src/loaders/hashmap.rs +++ b/core/interop/src/loaders/hashmap.rs @@ -1,25 +1,42 @@ -use std::collections::HashMap; +//! A `ModuleLoader` that loads modules from a `HashMap` based on the name. +use rustc_hash::FxHashMap; use boa_engine::module::{ModuleLoader, Referrer}; use boa_engine::{Context, JsNativeError, JsResult, JsString, Module}; use boa_gc::GcRefCell; -/// A ModuleLoader that loads modules from a HashMap based on the name. +/// A `ModuleLoader` that loads modules from a `HashMap` based on the name. /// After registering modules, this loader will look for the exact name /// in its internal map to resolve. #[derive(Debug, Clone)] -pub struct HashMapModuleLoader(GcRefCell>); +pub struct HashMapModuleLoader(GcRefCell>); + +impl Default for HashMapModuleLoader { + fn default() -> Self { + Self(GcRefCell::new(FxHashMap::default())) + } +} impl HashMapModuleLoader { + /// Creates an empty `HashMapModuleLoader`. + #[must_use] pub fn new() -> Self { - Self(GcRefCell::new(HashMap::new())) + Self::default() } + /// Registers a module with a given name. pub fn register(&self, key: impl Into, value: Module) { self.0.borrow_mut().insert(key.into(), value); } } +impl FromIterator<(JsString, Module)> for HashMapModuleLoader { + fn from_iter>(iter: T) -> Self { + let map = iter.into_iter().collect(); + Self(GcRefCell::new(map)) + } +} + impl ModuleLoader for HashMapModuleLoader { fn load_imported_module( &self, From 0f8b3bee00e514e3e37cc62a2d5ea2bd4bc4f88e Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Fri, 29 Mar 2024 12:01:09 -0700 Subject: [PATCH 5/6] Make IntoJsFunction unsafe --- core/gc/src/trace.rs | 1 + core/interop/src/lib.rs | 83 ++++++++++++++++++++++++----------------- 2 files changed, 49 insertions(+), 35 deletions(-) diff --git a/core/gc/src/trace.rs b/core/gc/src/trace.rs index b28d976e8d1..b05454df45d 100644 --- a/core/gc/src/trace.rs +++ b/core/gc/src/trace.rs @@ -271,6 +271,7 @@ macro_rules! type_arg_tuple_based_finalize_trace_impls { ($(($($args:ident),*);)*) => { $( fn_finalize_trace_group!($($args),*); + // closure_finalize_trace_group!($($args),*); tuple_finalize_trace!($($args),*); )* } diff --git a/core/interop/src/lib.rs b/core/interop/src/lib.rs index fc073f3f1f1..f048bf2f655 100644 --- a/core/interop/src/lib.rs +++ b/core/interop/src/lib.rs @@ -1,4 +1,5 @@ //! Interop utilities between Boa and its host. + use std::cell::RefCell; use boa_engine::module::SyntheticModuleInitializer; @@ -35,18 +36,27 @@ impl + Clone> IntoJsModule fo } /// A trait to convert a type into a JS function. -pub trait IntoJsFunction { +/// This trait does not require the implementing type to be `Copy`, which +/// can lead to undefined behaviour if it contains Garbage Collected objects. +/// +/// # Safety +/// For this trait to be implemented safely, the implementing type must not contain any +/// garbage collected objects (from [`boa_gc`]). +pub unsafe trait IntoJsFunctionUnsafe { /// Converts the type into a JS function. - fn into_js_function(self, context: &mut Context) -> NativeFunction; + /// + /// # Safety + /// This function is unsafe to ensure the callee knows the risks of using this trait. + /// The implementing type must not contain any garbage collected objects. + unsafe fn into_js_function(self, context: &mut Context) -> NativeFunction; } -impl IntoJsFunction for T { - fn into_js_function(self, _context: &mut Context) -> NativeFunction { - let s = RefCell::new(self); - +unsafe impl IntoJsFunctionUnsafe for T { + unsafe fn into_js_function(self, _context: &mut Context) -> NativeFunction { + let cell = RefCell::new(self); unsafe { NativeFunction::from_closure(move |_, _, _| { - s.borrow_mut()(); + cell.borrow_mut()(); Ok(JsValue::undefined()) }) } @@ -59,6 +69,7 @@ pub fn into_js_module() { use boa_engine::builtins::promise::PromiseState; use boa_engine::{js_string, JsValue, Source}; use std::rc::Rc; + use std::sync::atomic::{AtomicU32, Ordering}; let loader = Rc::new(loaders::HashMapModuleLoader::new()); let mut context = Context::builder() @@ -66,34 +77,36 @@ pub fn into_js_module() { .build() .unwrap(); - let foo_count = Rc::new(RefCell::new(0)); - let bar_count = Rc::new(RefCell::new(0)); - let module = vec![ - ( - js_string!("foo"), - IntoJsFunction::into_js_function( - { - let foo_count = foo_count.clone(); - move || { - *foo_count.borrow_mut() += 1; - } - }, - &mut context, + let foo_count = Rc::new(AtomicU32::new(0)); + let bar_count = Rc::new(AtomicU32::new(0)); + let module = unsafe { + vec![ + ( + js_string!("foo"), + IntoJsFunctionUnsafe::into_js_function( + { + let counter = foo_count.clone(); + move || { + counter.fetch_add(1, Ordering::Relaxed); + } + }, + &mut context, + ), ), - ), - ( - js_string!("bar"), - IntoJsFunction::into_js_function( - { - let bar_count = bar_count.clone(); - move || { - *bar_count.borrow_mut() += 1; - } - }, - &mut context, + ( + js_string!("bar"), + IntoJsFunctionUnsafe::into_js_function( + { + let counter = bar_count.clone(); + move || { + counter.fetch_add(1, Ordering::Relaxed); + } + }, + &mut context, + ), ), - ), - ] + ] + } .into_js_module(&mut context); loader.register(js_string!("test"), module); @@ -119,7 +132,7 @@ pub fn into_js_module() { panic!("module didn't execute successfully!") }; - assert_eq!(*foo_count.borrow(), 1); - assert_eq!(*bar_count.borrow(), 10); + assert_eq!(foo_count.load(Ordering::Relaxed), 1); + assert_eq!(bar_count.load(Ordering::Relaxed), 10); assert_eq!(v, JsValue::undefined()); } From 88b3d2747fdf904980ac4ae93c9a4644ea73f150 Mon Sep 17 00:00:00 2001 From: Hans Larsen Date: Mon, 1 Apr 2024 18:59:16 -0700 Subject: [PATCH 6/6] Remove unused code --- core/gc/src/trace.rs | 1 - 1 file changed, 1 deletion(-) diff --git a/core/gc/src/trace.rs b/core/gc/src/trace.rs index b05454df45d..b28d976e8d1 100644 --- a/core/gc/src/trace.rs +++ b/core/gc/src/trace.rs @@ -271,7 +271,6 @@ macro_rules! type_arg_tuple_based_finalize_trace_impls { ($(($($args:ident),*);)*) => { $( fn_finalize_trace_group!($($args),*); - // closure_finalize_trace_group!($($args),*); tuple_finalize_trace!($($args),*); )* }