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