use std::{
cell::RefCell,
rc::Rc,
sync::mpsc::{self, Sender},
thread::JoinHandle,
time::Duration,
};
#[cfg(feature = "annex-b")]
use boa_engine::builtins::is_html_dda::IsHTMLDDA;
use boa_engine::{
Context, JsArgs, JsNativeError, JsResult, JsValue, Source,
builtins::array_buffer::{ArrayBuffer, SharedArrayBuffer},
js_string,
native_function::NativeFunction,
object::{JsObject, ObjectInitializer, builtins::JsSharedArrayBuffer},
property::Attribute,
};
use bus::BusReader;
#[derive(Debug)]
pub enum WorkerResult {
Ok,
Err(String),
Panic(String),
}
type WorkerHandle = JoinHandle<Result<(), String>>;
#[derive(Debug, Clone)]
pub struct WorkerHandles(Rc<RefCell<Vec<WorkerHandle>>>);
impl Default for WorkerHandles {
fn default() -> Self {
Self::new()
}
}
impl WorkerHandles {
#[must_use]
pub fn new() -> Self {
Self(Rc::default())
}
#[allow(clippy::print_stderr)]
pub fn join_all(&mut self) -> Vec<WorkerResult> {
let handles = std::mem::take(&mut *self.0.borrow_mut());
handles
.into_iter()
.map(|h| {
let result = h.join();
match result {
Ok(Ok(())) => WorkerResult::Ok,
Ok(Err(msg)) => {
eprintln!("Detected error on worker thread: {msg}");
WorkerResult::Err(msg)
}
Err(e) => {
let msg = e
.downcast_ref::<&str>()
.map(|&s| String::from(s))
.unwrap_or_default();
eprintln!("Detected panic on worker thread: {msg}");
WorkerResult::Panic(msg)
}
}
})
.collect()
}
}
impl Drop for WorkerHandles {
fn drop(&mut self) {
self.join_all();
}
}
pub fn register_js262(handles: WorkerHandles, console: bool, context: &mut Context) -> JsObject {
let global_obj = context.global_object();
let agent = agent_obj(handles, console, context);
let js262 = ObjectInitializer::new(context)
.function(
NativeFunction::from_fn_ptr(create_realm),
js_string!("createRealm"),
0,
)
.function(
NativeFunction::from_fn_ptr(detach_array_buffer),
js_string!("detachArrayBuffer"),
2,
)
.function(
NativeFunction::from_fn_ptr(eval_script),
js_string!("evalScript"),
1,
)
.function(NativeFunction::from_fn_ptr(gc), js_string!("gc"), 0)
.property(
js_string!("global"),
global_obj,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)
.property(
js_string!("agent"),
agent,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)
.build();
#[cfg(feature = "annex-b")]
js262
.create_data_property_or_throw(
js_string!("IsHTMLDDA"),
JsObject::from_proto_and_data(None, IsHTMLDDA),
context,
)
.expect("the IsHTMLDDA property must be definable");
context
.register_global_property(
js_string!("$262"),
js262.clone(),
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)
.expect("shouldn't fail with the default global");
js262
}
#[allow(clippy::unnecessary_wraps)]
fn create_realm(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let mut realm = context.create_realm()?;
realm = context.enter_realm(realm);
let js262 = register_js262(WorkerHandles::new(), false, context);
context.enter_realm(realm);
Ok(JsValue::new(js262))
}
fn detach_array_buffer(_: &JsValue, args: &[JsValue], _: &mut Context) -> JsResult<JsValue> {
fn type_err() -> JsNativeError {
JsNativeError::typ().with_message("The provided object was not an ArrayBuffer")
}
let object = args.first().and_then(JsValue::as_object);
let mut array_buffer = object
.as_ref()
.and_then(|o| o.downcast_mut::<ArrayBuffer>())
.ok_or_else(type_err)?;
let key = args.get_or_undefined(1);
array_buffer.detach(key)?;
Ok(JsValue::null())
}
fn eval_script(_this: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
args.first().and_then(JsValue::as_string).map_or_else(
|| Ok(JsValue::undefined()),
|source_text| context.eval(Source::from_bytes(&source_text.to_std_string_escaped())),
)
}
#[allow(clippy::unnecessary_wraps)]
fn gc(_this: &JsValue, _: &[JsValue], _context: &mut Context) -> JsResult<JsValue> {
boa_gc::force_collect();
Ok(JsValue::undefined())
}
fn sleep(_: &JsValue, args: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
let ms = args.get_or_undefined(0).to_number(context)? / 1000.0;
std::thread::sleep(Duration::from_secs_f64(ms));
Ok(JsValue::undefined())
}
#[allow(clippy::unnecessary_wraps)]
fn monotonic_now(_: &JsValue, _: &[JsValue], context: &mut Context) -> JsResult<JsValue> {
#[allow(clippy::cast_precision_loss)]
Ok(JsValue::from(
context.clock().now().millis_since_epoch() as f64
))
}
fn agent_obj(handles: WorkerHandles, console: bool, context: &mut Context) -> JsObject {
let bus = Rc::new(RefCell::new(bus::Bus::new(1)));
let (reports_tx, reports_rx) = mpsc::channel();
let start = unsafe {
let bus = bus.clone();
NativeFunction::from_closure(move |_, args, context| {
let script = args
.get_or_undefined(0)
.to_string(context)?
.to_std_string()
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
let rx = bus.borrow_mut().add_rx();
let tx = reports_tx.clone();
handles.0.borrow_mut().push(std::thread::spawn(move || {
let context = &mut Context::builder()
.can_block(true)
.build()
.map_err(|e| e.to_string())?;
register_js262_worker(rx, tx, context);
if console {
let console = crate::Console::init(context);
context
.register_global_property(crate::Console::NAME, console, Attribute::all())
.expect("the console builtin shouldn't exist");
}
let src = Source::from_bytes(&script);
context.eval(src).map_err(|e| e.to_string())?;
context.run_jobs().map_err(|e| e.to_string())?;
Ok(())
}));
Ok(JsValue::undefined())
})
};
let broadcast = unsafe {
NativeFunction::from_closure(move |_, args, _| {
let buffer = args.get_or_undefined(0).as_object().ok_or_else(|| {
JsNativeError::typ().with_message("argument was not a shared array")
})?;
let buffer = buffer
.downcast_ref::<SharedArrayBuffer>()
.ok_or_else(|| {
JsNativeError::typ().with_message("argument was not a shared array")
})?
.clone();
bus.borrow_mut().broadcast(buffer);
Ok(JsValue::undefined())
})
};
let get_report = unsafe {
NativeFunction::from_closure(move |_, _, _| {
let Ok(msg) = reports_rx.try_recv() else {
return Ok(JsValue::null());
};
Ok(js_string!(&msg[..]).into())
})
};
ObjectInitializer::new(context)
.function(start, js_string!("start"), 1)
.function(broadcast, js_string!("broadcast"), 2)
.function(get_report, js_string!("getReport"), 0)
.function(NativeFunction::from_fn_ptr(sleep), js_string!("sleep"), 1)
.function(
NativeFunction::from_fn_ptr(monotonic_now),
js_string!("monotonicNow"),
0,
)
.build()
}
fn register_js262_worker(
rx: BusReader<SharedArrayBuffer>,
tx: Sender<Vec<u16>>,
context: &mut Context,
) {
let rx = RefCell::new(rx);
let receive_broadcast = unsafe {
NativeFunction::from_closure(move |_, args, context| {
let array = rx.borrow_mut().recv().map_err(|err| {
JsNativeError::typ().with_message(format!("failed to receive buffer: {err}"))
})?;
let callable = args
.get_or_undefined(0)
.as_callable()
.ok_or_else(|| JsNativeError::typ().with_message("argument is not callable"))?;
let buffer = JsSharedArrayBuffer::from_buffer(array, context);
callable.call(&JsValue::undefined(), &[buffer.into()], context)
})
};
let report = unsafe {
NativeFunction::from_closure(move |_, args, context| {
let string = args.get_or_undefined(0).to_string(context)?.to_vec();
tx.send(string)
.map_err(|e| JsNativeError::typ().with_message(e.to_string()))?;
Ok(JsValue::undefined())
})
};
let agent = ObjectInitializer::new(context)
.function(receive_broadcast, js_string!("receiveBroadcast"), 1)
.function(report, js_string!("report"), 1)
.function(NativeFunction::from_fn_ptr(sleep), js_string!("sleep"), 1)
.function(
NativeFunction::from_fn_ptr(|_, _, _| Ok(JsValue::undefined())),
js_string!("leaving"),
0,
)
.function(
NativeFunction::from_fn_ptr(monotonic_now),
js_string!("monotonicNow"),
0,
)
.build();
let js262 = ObjectInitializer::new(context)
.property(
js_string!("agent"),
agent,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)
.build();
context
.register_global_property(
js_string!("$262"),
js262,
Attribute::WRITABLE | Attribute::CONFIGURABLE,
)
.expect("shouldn't fail with the default global");
}