mirror of https://github.com/boa-dev/boa.git
Browse Source
* 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 * Remove unnecessary into_iter() * cargo fmt * cargo clippy * Make IntoJsFunction unsafe * Remove unused codepull/3787/head
Hans Larsen
8 months ago
committed by
GitHub
6 changed files with 268 additions and 0 deletions
@ -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 |
@ -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 |
@ -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()); |
||||||
|
} |
@ -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; |
@ -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() |
||||||
|
} |
||||||
|
} |
Loading…
Reference in new issue