Skip to content

Commit

Permalink
Add a boa_interop crate (#3772)
Browse files Browse the repository at this point in the history
* 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
hansl committed Apr 2, 2024
1 parent b1f0780 commit 37db6a4
Show file tree
Hide file tree
Showing 6 changed files with 268 additions and 0 deletions.
9 changes: 9 additions & 0 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

33 changes: 33 additions & 0 deletions core/interop/ABOUT.md
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
19 changes: 19 additions & 0 deletions core/interop/Cargo.toml
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
138 changes: 138 additions & 0 deletions core/interop/src/lib.rs
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());
}
6 changes: 6 additions & 0 deletions core/interop/src/loaders.rs
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;
63 changes: 63 additions & 0 deletions core/interop/src/loaders/hashmap.rs
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()
}
}

0 comments on commit 37db6a4

Please sign in to comment.