-
-
Notifications
You must be signed in to change notification settings - Fork 400
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
* 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 #3770 * Remove unnecessary into_iter() * cargo fmt * cargo clippy * Make IntoJsFunction unsafe * Remove unused code
- Loading branch information
Showing
6 changed files
with
268 additions
and
0 deletions.
There are no files selected for viewing
Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.
Oops, something went wrong.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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 |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,19 @@ | ||
[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 | ||
rustc-hash = { workspace = true, features = ["std"] } | ||
|
||
[lints] | ||
workspace = true |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,138 @@ | ||
//! Interop utilities between Boa and its host. | ||
|
||
use std::cell::RefCell; | ||
|
||
use boa_engine::module::SyntheticModuleInitializer; | ||
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; | ||
} | ||
|
||
impl<T: IntoIterator<Item = (JsString, NativeFunction)> + 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, | ||
) | ||
} | ||
} | ||
|
||
/// A trait to convert a type into a JS function. | ||
/// 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. | ||
/// | ||
/// # 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; | ||
} | ||
|
||
unsafe impl<T: FnMut() + 'static> IntoJsFunctionUnsafe for T { | ||
unsafe fn into_js_function(self, _context: &mut Context) -> NativeFunction { | ||
let cell = RefCell::new(self); | ||
unsafe { | ||
NativeFunction::from_closure(move |_, _, _| { | ||
cell.borrow_mut()(); | ||
Ok(JsValue::undefined()) | ||
}) | ||
} | ||
} | ||
} | ||
|
||
#[test] | ||
#[allow(clippy::missing_panics_doc)] | ||
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() | ||
.module_loader(loader.clone()) | ||
.build() | ||
.unwrap(); | ||
|
||
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"), | ||
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); | ||
|
||
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.load(Ordering::Relaxed), 1); | ||
assert_eq!(bar_count.load(Ordering::Relaxed), 10); | ||
assert_eq!(v, JsValue::undefined()); | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -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; |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,63 @@ | ||
//! 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. | ||
/// After registering modules, this loader will look for the exact name | ||
/// in its internal map to resolve. | ||
#[derive(Debug, Clone)] | ||
pub struct HashMapModuleLoader(GcRefCell<FxHashMap<JsString, Module>>); | ||
|
||
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::default() | ||
} | ||
|
||
/// Registers a module with a given name. | ||
pub fn register(&self, key: impl Into<JsString>, value: Module) { | ||
self.0.borrow_mut().insert(key.into(), value); | ||
} | ||
} | ||
|
||
impl FromIterator<(JsString, Module)> for HashMapModuleLoader { | ||
fn from_iter<T: IntoIterator<Item = (JsString, Module)>>(iter: T) -> Self { | ||
let map = iter.into_iter().collect(); | ||
Self(GcRefCell::new(map)) | ||
} | ||
} | ||
|
||
impl ModuleLoader for HashMapModuleLoader { | ||
fn load_imported_module( | ||
&self, | ||
_referrer: Referrer, | ||
specifier: JsString, | ||
finish_load: Box<dyn FnOnce(JsResult<Module>, &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<Module> { | ||
self.0.borrow().get(&specifier).cloned() | ||
} | ||
} |