Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add a boa_interop crate #3772

Merged
merged 6 commits into from
Apr 2, 2024
Merged
Show file tree
Hide file tree
Changes from 5 commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
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.

1 change: 1 addition & 0 deletions core/gc/src/trace.rs
Original file line number Diff line number Diff line change
Expand Up @@ -271,6 +271,7 @@ macro_rules! type_arg_tuple_based_finalize_trace_impls {
($(($($args:ident),*);)*) => {
$(
fn_finalize_trace_group!($($args),*);
// closure_finalize_trace_group!($($args),*);
hansl marked this conversation as resolved.
Show resolved Hide resolved
tuple_finalize_trace!($($args),*);
)*
}
Expand Down
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() {
nekevss marked this conversation as resolved.
Show resolved Hide resolved
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 {
Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion/issue: not sure that new is needed here if all it does is call Default

Copy link
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Some people don't like to import default (it's not part of std prelude). I've seen this pattern in places, including the rust std: https://github.com/rust-lang/rust/blob/master/library/std/src/collections/hash/map.rs#L229-L234

I'm okay replacing it with empty() too if you want.

Copy link
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I would say to just leave it. The std uses the same pattern of having a new method that does the same as default, so I think we should do the same.

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