Browse Source

Add a boa_interop crate (#3772)

* 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 code
pull/3787/head
Hans Larsen 1 month ago committed by GitHub
parent
commit
37db6a4911
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
  1. 9
      Cargo.lock
  2. 33
      core/interop/ABOUT.md
  3. 19
      core/interop/Cargo.toml
  4. 138
      core/interop/src/lib.rs
  5. 6
      core/interop/src/loaders.rs
  6. 63
      core/interop/src/loaders/hashmap.rs

9
Cargo.lock generated

@ -502,6 +502,15 @@ dependencies = [
"static_assertions",
]
[[package]]
name = "boa_interop"
version = "0.18.0"
dependencies = [
"boa_engine",
"boa_gc",
"rustc-hash",
]
[[package]]
name = "boa_macros"
version = "0.18.0"

33
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

19
core/interop/Cargo.toml

@ -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

138
core/interop/src/lib.rs

@ -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());
}

6
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;

63
core/interop/src/loaders/hashmap.rs

@ -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…
Cancel
Save