use crate::buf::BuffersManagerArc;
use crate::cli::CliOpt;
use crate::js::err::JsError;
use crate::js::exception::ExceptionState;
use crate::js::hook::module_resolve_cb;
use crate::js::module::{
ImportKind, ImportMap, ModuleMap, ModuleStatus, create_origin, fetch_module_tree, load_import,
resolve_import,
};
use crate::js::msg::{EventLoopToJsRuntimeMessage, JsRuntimeToEventLoopMessage};
use crate::prelude::*;
use crate::state::StateArc;
use crate::ui::tree::TreeArc;
use once_cell::sync::Lazy;
use parking_lot::Mutex;
use std::cell::RefCell;
use std::path::PathBuf;
use std::rc::Rc;
use std::sync::Arc;
use std::sync::Once;
use std::sync::atomic::{AtomicI32, Ordering};
use std::time::Instant;
use tokio::sync::mpsc::{Receiver, Sender};
use tracing::{error, trace};
pub mod binding;
pub mod constant;
pub mod err;
pub mod exception;
pub mod hook;
pub mod loader;
pub mod module;
pub mod msg;
pub mod transpiler;
#[derive(Debug, Default, Clone)]
pub struct JsRuntimeOptions {
pub root: Option<String>,
pub import_map: Option<ImportMap>,
pub test_mode: bool,
pub v8_flags: Vec<String>,
}
pub trait JsFuture {
fn run(&mut self, scope: &mut v8::HandleScope);
}
pub type JsFutureId = i32;
pub fn next_future_id() -> JsFutureId {
static VALUE: AtomicI32 = AtomicI32::new(1);
VALUE.fetch_add(1, Ordering::Relaxed)
}
pub fn v8_version() -> &'static str {
v8::VERSION_STRING
}
pub fn init_v8_platform() {
static V8_INIT: Once = Once::new();
V8_INIT.call_once(move || {
let platform = v8::new_default_platform(0, false).make_shared();
v8::V8::initialize_platform(platform);
v8::V8::initialize();
});
}
static BUILTIN_RUNTIME_MODULES: Lazy<Vec<(&str, &str)>> = Lazy::new(|| {
vec![
("00__web.js", include_str!("./js/runtime/00__web.js")),
("01__rsvim.js", include_str!("./js/runtime/01__rsvim.js")),
]
});
pub struct JsRuntimeStateForSnapshot {
pub context: Option<v8::Global<v8::Context>>,
}
pub struct JsRuntimeForSnapshot {
pub isolate: Option<v8::OwnedIsolate>,
pub state: Rc<RefCell<JsRuntimeStateForSnapshot>>,
}
impl Drop for JsRuntimeForSnapshot {
fn drop(&mut self) {
debug_assert_eq!(Rc::strong_count(&self.state), 1);
}
}
impl JsRuntimeForSnapshot {
#[allow(clippy::new_without_default)]
pub fn new() -> Self {
init_v8_platform();
let (mut isolate, global_context) = Self::create_isolate();
let mut context_scope = v8::HandleScope::with_context(&mut isolate, global_context.clone());
let scope = &mut context_scope;
let _context = v8::Local::new(scope, global_context.clone());
{
for module_src in BUILTIN_RUNTIME_MODULES.iter() {
let filename = module_src.0;
let source = module_src.1;
Self::init_builtin_module(scope, filename, source);
}
}
let state = Rc::new(RefCell::new(JsRuntimeStateForSnapshot {
context: Some(global_context),
}));
scope.set_slot(state.clone());
drop(context_scope);
JsRuntimeForSnapshot {
isolate: Some(isolate),
state,
}
}
fn create_isolate() -> (v8::OwnedIsolate, v8::Global<v8::Context>) {
let mut isolate = v8::Isolate::snapshot_creator(None, Some(v8::CreateParams::default()));
isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10);
isolate.set_promise_reject_callback(hook::promise_reject_cb);
isolate
.set_host_initialize_import_meta_object_callback(hook::host_initialize_import_meta_object_cb);
let global_context = {
let scope = &mut v8::HandleScope::new(&mut isolate);
let context = v8::Context::new(scope, Default::default());
v8::Global::new(scope, context)
};
(isolate, global_context)
}
pub fn create_snapshot(mut self) -> v8::StartupData {
{
let global_context = self.global_context();
let mut scope = self.handle_scope();
let local_context = v8::Local::new(&mut scope, global_context);
scope.set_default_context(local_context);
}
{
let state = self.get_state();
state.borrow_mut().context.take();
}
let snapshot_creator = self.isolate.take().unwrap();
snapshot_creator
.create_blob(v8::FunctionCodeHandling::Keep)
.unwrap()
}
fn init_builtin_module(scope: &mut v8::HandleScope<'_>, name: &str, source: &str) {
let tc_scope = &mut v8::TryCatch::new(scope);
let module = match Self::fetch_module(tc_scope, name, Some(source)) {
Some(module) => module,
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
error!("Failed to import builtin modules: {name}, error: {exception:?}");
eprintln!("Failed to import builtin modules: {name}, error: {exception:?}");
std::process::exit(1);
}
};
if module
.instantiate_module(tc_scope, module_resolve_cb)
.is_none()
{
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
error!("Failed to instantiate builtin modules: {name}, error: {exception:?}");
eprintln!("Failed to instantiate builtin modules: {name}, error: {exception:?}");
std::process::exit(1);
}
let _ = module.evaluate(tc_scope);
if module.get_status() == v8::ModuleStatus::Errored {
let exception = module.get_exception();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
error!("Failed to evaluate builtin modules: {name}, error: {exception:?}");
eprintln!("Failed to evaluate builtin modules: {name}, error: {exception:?}");
std::process::exit(1);
}
}
fn fetch_module<'a>(
scope: &mut v8::HandleScope<'a>,
filename: &str,
source: Option<&str>,
) -> Option<v8::Local<'a, v8::Module>> {
let origin = create_origin(scope, filename, true);
let source = match source {
Some(source) => source.into(),
None => load_import(filename, true).unwrap(),
};
trace!(
"Fetched module filename: {:?}, source: {:?}",
filename,
if source.as_str().len() > 20 {
String::from(&source.as_str()[..20]) + "..."
} else {
String::from(source.as_str())
}
);
let source = v8::String::new(scope, &source).unwrap();
let mut source = v8::script_compiler::Source::new(source, Some(&origin));
let module = v8::script_compiler::compile_module(scope, &mut source)?;
Some(module)
}
}
impl JsRuntimeForSnapshot {
pub fn global_context(&self) -> v8::Global<v8::Context> {
self.get_state().borrow().context.as_ref().unwrap().clone()
}
pub fn state(isolate: &v8::Isolate) -> Rc<RefCell<JsRuntimeStateForSnapshot>> {
isolate
.get_slot::<Rc<RefCell<JsRuntimeStateForSnapshot>>>()
.unwrap()
.clone()
}
pub fn get_state(&self) -> Rc<RefCell<JsRuntimeStateForSnapshot>> {
Self::state(self.isolate.as_ref().unwrap())
}
pub fn handle_scope(&mut self) -> v8::HandleScope {
let context = self.global_context();
v8::HandleScope::with_context(self.isolate.as_mut().unwrap(), context)
}
}
pub struct JsRuntimeState {
pub context: v8::Global<v8::Context>,
pub module_map: ModuleMap,
pub timeout_handles: HashSet<i32>,
pub pending_futures: HashMap<JsFutureId, Box<dyn JsFuture>>,
pub startup_moment: Instant,
pub time_origin: u128,
pub exceptions: ExceptionState,
pub options: JsRuntimeOptions,
pub js_runtime_send_to_master: Sender<JsRuntimeToEventLoopMessage>,
pub js_runtime_recv_from_master: Receiver<EventLoopToJsRuntimeMessage>,
pub cli_opt: CliOpt,
pub runtime_path: Arc<Mutex<Vec<PathBuf>>>,
pub tree: TreeArc,
pub buffers: BuffersManagerArc,
pub editing_state: StateArc,
}
pub struct SnapshotData {
pub value: &'static [u8],
}
impl SnapshotData {
pub fn new(value: &'static [u8]) -> Self {
SnapshotData { value }
}
}
pub struct JsRuntime {
pub isolate: v8::OwnedIsolate,
#[allow(unused)]
pub state: Rc<RefCell<JsRuntimeState>>,
}
impl JsRuntime {
#[allow(clippy::too_many_arguments)]
pub fn new(
options: JsRuntimeOptions,
snapshot: SnapshotData,
startup_moment: Instant,
time_origin: u128,
js_runtime_send_to_master: Sender<JsRuntimeToEventLoopMessage>,
js_runtime_recv_from_master: Receiver<EventLoopToJsRuntimeMessage>,
cli_opt: CliOpt,
runtime_path: Arc<Mutex<Vec<PathBuf>>>,
tree: TreeArc,
buffers: BuffersManagerArc,
editing_state: StateArc,
) -> Self {
init_v8_platform();
let mut isolate = {
let create_params = v8::CreateParams::default();
let create_params = create_params.snapshot_blob(snapshot.value.into());
v8::Isolate::new(create_params)
};
isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10);
isolate.set_promise_reject_callback(hook::promise_reject_cb);
isolate
.set_host_initialize_import_meta_object_callback(hook::host_initialize_import_meta_object_cb);
let context: v8::Global<v8::Context> = {
let scope = &mut v8::HandleScope::new(&mut *isolate);
let context = binding::create_new_context(scope);
v8::Global::new(scope, context)
};
let state = Rc::new(RefCell::new(JsRuntimeState {
context,
module_map: ModuleMap::new(),
timeout_handles: HashSet::new(),
pending_futures: HashMap::new(),
startup_moment,
time_origin,
exceptions: ExceptionState::new(),
options,
js_runtime_send_to_master,
js_runtime_recv_from_master,
cli_opt,
runtime_path,
tree,
buffers,
editing_state,
}));
isolate.set_slot(state.clone());
JsRuntime {
isolate,
state,
}
}
pub fn __execute_script(
&mut self,
filename: &str,
source: &str,
) -> Result<Option<v8::Global<v8::Value>>, AnyErr> {
let scope = &mut self.handle_scope();
let state_rc = JsRuntime::state(scope);
let origin = create_origin(scope, filename, false);
let source = v8::String::new(scope, source).unwrap();
let tc_scope = &mut v8::TryCatch::new(scope);
type ExecuteScriptResult = Result<Option<v8::Global<v8::Value>>, AnyErr>;
let handle_exception =
|scope: &mut v8::TryCatch<'_, v8::HandleScope<'_>>| -> ExecuteScriptResult {
assert!(scope.has_caught());
let exception = scope.exception().unwrap();
let exception = v8::Global::new(scope, exception);
let mut state = state_rc.borrow_mut();
state.exceptions.capture_exception(exception);
drop(state);
if let Some(error) = check_exceptions(scope) {
anyhow::bail!(error)
}
Ok(None)
};
let script = match v8::Script::compile(tc_scope, source, Some(&origin)) {
Some(script) => script,
None => return handle_exception(tc_scope),
};
match script.run(tc_scope) {
Some(value) => Ok(Some(v8::Global::new(tc_scope, value))),
None => handle_exception(tc_scope),
}
}
pub fn execute_module(&mut self, filename: &str, source: Option<&str>) -> Result<(), AnyErr> {
let scope = &mut self.handle_scope();
let path = match source.is_some() {
true => filename.to_string(),
false => match resolve_import(None, filename, false, None) {
Ok(specifier) => specifier,
Err(e) => {
return Err(e);
}
},
};
trace!("Resolved main js module (path): {:?}", path);
let tc_scope = &mut v8::TryCatch::new(scope);
let module = match fetch_module_tree(tc_scope, filename, None) {
Some(module) => module,
None => {
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
let _exception = JsError::from_v8_exception(tc_scope, exception, None);
let e = format!("User config not found: {filename:?}");
error!(e);
eprintln!("{e}");
anyhow::bail!(e);
}
};
if module
.instantiate_module(tc_scope, module_resolve_cb)
.is_none()
{
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
let e = format!("Failed to instantiate user config module {filename:?}: {exception:?}");
error!(e);
eprintln!("{e}");
anyhow::bail!(e);
}
match module.evaluate(tc_scope) {
Some(result) => {
trace!(
"Evaluated user config module result ({:?}): {:?}",
result.type_repr(),
result.to_rust_string_lossy(tc_scope),
);
}
None => trace!("Evaluated user config module result: None"),
}
if module.get_status() == v8::ModuleStatus::Errored {
let exception = module.get_exception();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
let e = format!("Failed to evaluate user config module {filename:?}: {exception:?}");
error!(e);
eprintln!("{e}");
anyhow::bail!(e);
}
Ok(())
}
pub fn tick_event_loop(&mut self) {
let isolate_has_pending_tasks = self.isolate.has_pending_background_tasks();
trace!(
"Tick js runtime, isolate has pending tasks: {:?}",
isolate_has_pending_tasks
);
run_next_tick_callbacks(&mut self.handle_scope());
self.fast_forward_imports();
self.run_pending_futures();
trace!("Tick js runtime - done");
}
fn run_pending_futures(&mut self) {
let scope = &mut self.handle_scope();
let mut futures: Vec<Box<dyn JsFuture>> = Vec::new();
{
let state_rc = Self::state(scope);
let mut state = state_rc.borrow_mut();
while let Ok(msg) = state.js_runtime_recv_from_master.try_recv() {
match msg {
EventLoopToJsRuntimeMessage::TimeoutResp(resp) => {
match state.pending_futures.remove(&resp.future_id) {
Some(timeout_cb) => futures.push(timeout_cb),
None => unreachable!("Failed to get timeout future by ID {:?}", resp.future_id),
}
}
}
}
}
for mut fut in futures {
fut.run(scope);
if let Some(error) = check_exceptions(scope) {
error!("Js runtime timeout error:{error:?}");
eprintln!("Js runtime timeout error:{error:?}");
}
run_next_tick_callbacks(scope);
}
}
fn fast_forward_imports(&mut self) {
let scope = &mut self.handle_scope();
let state_rc = JsRuntime::state(scope);
let mut state = state_rc.borrow_mut();
let mut ready_imports = vec![];
let state_ref = &mut *state;
let pending_graphs = &mut state_ref.module_map.pending;
let seen_modules = &mut state_ref.module_map.seen;
pending_graphs.retain(|graph_rc| {
let graph = graph_rc.borrow();
let mut graph_root = graph.root_rc.borrow_mut();
if let Some(message) = graph_root.exception.borrow_mut().take() {
let exception = v8::String::new(scope, &message).unwrap();
let exception = v8::Exception::error(scope, exception);
match graph.kind.clone() {
ImportKind::Static => unreachable!(),
ImportKind::Dynamic(main_promise) => {
for promise in [main_promise].iter().chain(graph.same_origin.iter()) {
promise.open(scope).reject(scope, exception);
}
}
}
return false;
}
if graph_root.status != ModuleStatus::Ready {
graph_root.fast_forward(seen_modules);
return true;
}
ready_imports.push(Rc::clone(graph_rc));
false
});
drop(state);
for graph_rc in ready_imports {
let tc_scope = &mut v8::TryCatch::new(scope);
let graph = graph_rc.borrow();
let path = graph.root_rc.borrow().path.clone();
let module = state_rc.borrow().module_map.get(&path).unwrap();
let module = v8::Local::new(tc_scope, module);
if module
.instantiate_module(tc_scope, module_resolve_cb)
.is_none()
{
assert!(tc_scope.has_caught());
let exception = tc_scope.exception().unwrap();
let exception = JsError::from_v8_exception(tc_scope, exception, None);
error!("{exception:?}");
eprintln!("{exception:?}");
continue;
}
let _ = module.evaluate(tc_scope);
let is_root_module = !graph.root_rc.borrow().is_dynamic_import;
if module.get_status() == v8::ModuleStatus::Errored && is_root_module {
let mut state = state_rc.borrow_mut();
let exception = module.get_exception();
let exception = v8::Global::new(tc_scope, exception);
state.exceptions.capture_exception(exception.clone());
state.exceptions.remove_promise_rejection_entry(&exception);
drop(state);
if let Some(error) = check_exceptions(tc_scope) {
error!("{error:?}");
eprintln!("{error:?}");
continue;
}
}
if let ImportKind::Dynamic(main_promise) = graph.kind.clone() {
let namespace = module.get_module_namespace();
for promise in [main_promise].iter().chain(graph.same_origin.iter()) {
promise.open(tc_scope).resolve(tc_scope, namespace);
}
}
}
run_next_tick_callbacks(scope);
}
pub fn has_promise_rejections(&mut self) -> bool {
self.get_state().borrow().exceptions.has_promise_rejection()
}
pub fn has_pending_imports(&mut self) -> bool {
self.get_state().borrow().module_map.has_pending_imports()
}
}
impl JsRuntime {
pub fn state(isolate: &v8::Isolate) -> Rc<RefCell<JsRuntimeState>> {
isolate
.get_slot::<Rc<RefCell<JsRuntimeState>>>()
.unwrap()
.clone()
}
pub fn get_state(&self) -> Rc<RefCell<JsRuntimeState>> {
Self::state(&self.isolate)
}
pub fn handle_scope(&mut self) -> v8::HandleScope {
let context = self.context();
v8::HandleScope::with_context(&mut self.isolate, context)
}
pub fn context(&mut self) -> v8::Global<v8::Context> {
let state = self.get_state();
let state = state.borrow();
state.context.clone()
}
}
fn run_next_tick_callbacks(scope: &mut v8::HandleScope) {
let tc_scope = &mut v8::TryCatch::new(scope);
tc_scope.perform_microtask_checkpoint();
}
pub fn check_exceptions(scope: &mut v8::HandleScope) -> Option<JsError> {
let state_rc = JsRuntime::state(scope);
let maybe_exception = state_rc.borrow_mut().exceptions.exception.take();
if let Some(exception) = maybe_exception {
let state = state_rc.borrow();
let exception = v8::Local::new(scope, exception);
if let Some(callback) = state.exceptions.uncaught_exception_cb.as_ref() {
let callback = v8::Local::new(scope, callback);
let undefined = v8::undefined(scope).into();
let origin = v8::String::new(scope, "uncaughtException").unwrap();
let tc_scope = &mut v8::TryCatch::new(scope);
drop(state);
callback.call(tc_scope, undefined, &[exception, origin.into()]);
if tc_scope.has_caught() {
let exception = tc_scope.exception().unwrap();
let exception = v8::Local::new(tc_scope, exception);
let error = JsError::from_v8_exception(tc_scope, exception, None);
return Some(error);
}
return None;
}
let error = JsError::from_v8_exception(scope, exception, None);
return Some(error);
}
None
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn next_future_id1() {
assert!(next_future_id() > 0);
}
}