mod extensions; pub mod message; mod microtasks; mod modules; mod op; mod repl; mod reporting; mod snapshot; mod stack_trace; mod structured_clone; mod tasks; mod wasm_streaming; use std::cell::RefCell; use std::collections::HashMap; use std::rc::Rc; use std::sync::OnceLock; use std::task::Context; use serde_json::json; use v8::inspector::StringView; use serde::de::DeserializeOwned; pub use modules::{ModuleFormat, ModuleLoader, ModuleSource}; pub use op::Op; pub use serde_v8; pub use snapshot::Snapshot; pub use v8; pub use wasm_streaming::{WasmStreamingRegistry, WasmStreamingStart, WasmStreamingStartError}; /// Bootstrap modules with a private graph, separate from guest modules and public builtins. pub struct Extension { pub name: &'static str, /// Extensions that must be present and whose entry points run first. pub dependencies: &'static [&'static str], pub ops: Vec, /// Canonical specifiers and sources, registered before any entry point executes. pub modules: &'static [(&'static str, &'static str)], /// An embedded module to evaluate before guest execution. /// Live setup registered through `ext:core/bootstrap` runs after all entries. pub entry_point: Option<&'static str>, } /// A guest module and the embedded source/declaration pair that defines it. pub struct PublicModule { pub name: &'static str, pub module: (&'static str, &'static str), pub declaration: &'static str, } impl PublicModule { pub fn register(&self, runtime: &mut Runtime) -> Result<(), Error> { runtime.register_extension_module(self.name, self.module.0) } } /// Embed matching JavaScript and declarations from one build output stem. /// Use the row's `module` in `Extension::modules` as well as registering the row. #[macro_export] macro_rules! public_module { ($name:literal, $extension:literal, $output:literal $(,)?) => { $crate::PublicModule { name: $name, module: ( concat!($extension, "/", $output, ".js"), include_str!(concat!(env!("OUT_DIR"), "/", $output, ".js")), ), declaration: include_str!(concat!(env!("OUT_DIR"), "/", $output, ".d.ts")), } }; } #[derive(Default)] pub struct RuntimeOptions { pub extensions: Vec, pub module_loader: Option>, } #[derive(Debug, thiserror::Error)] pub enum Error { #[error("{0}")] JavaScript(String), #[error("Duplicate op: {0}")] DuplicateOp(&'static str), #[error("{0}")] Module(String), #[error("SyntaxError: {0}")] Syntax(String), #[error(transparent)] Serde(#[from] serde_v8::Error), #[error(transparent)] Snapshot(#[from] SnapshotError), } #[derive(Debug, thiserror::Error)] pub enum SnapshotError { #[error("Snapshot has no layout matching these extensions")] IncompatibleLayout, #[error("Snapshot layouts must be distinct")] DuplicateLayout, #[error("Cannot snapshot active runtime state: {0}")] ActiveState(&'static str), #[error("V8 could not create a snapshot")] CreationFailed, #[error("Invalid snapshot artifact: {0}")] InvalidArtifact(&'static str), #[error("Invalid snapshot metadata: {0}")] Metadata(#[from] serde_json::Error), #[error("Invalid snapshot data: {0}")] Data(#[from] v8::DataError), #[error("Live runtimes in this process use a different V8 read-only heap")] ReadOnlyHeapInUse, #[error("Runtimes cannot start in a snapshot generator process")] GeneratorProcess, #[error("Snapshot generation must initialize V8 before any runtime in its process")] RuntimeProcess, } pub struct Runtime { // Global handles must be dropped before their isolate. repl: Option, module_evaluation: Option>, context: v8::Global, isolate: snapshot::IsolateOwner, } pub(crate) fn source<'s>( scope: &mut v8::PinScope<'s, '_>, name: &str, code: &str, is_module: bool, host_options: Option>, ) -> v8::script_compiler::Source { let resource = v8::String::new(scope, name).unwrap(); let origin = v8::ScriptOrigin::new( scope, resource.into(), 0, 0, false, -1, None, false, false, is_module, host_options, ); let code = v8::String::new(scope, code).unwrap(); v8::script_compiler::Source::new(code, Some(&origin)) } #[derive(Clone, Copy, PartialEq, Eq)] pub(crate) enum Process { Runtime, Generator, } /// The first caller decides how this process initializes V8. pub(crate) fn initialize_v8(process: Process) -> Process { static PROCESS: OnceLock = OnceLock::new(); *PROCESS.get_or_init(|| { // Snapshot creators must reuse V8's read-only heap rather than extend it. let mut flags = String::from("--no-extensible-ro-snapshot"); if process == Process::Generator { // Seeds identity hashes and V8's mmap hints, which shape artifact bytes. flags.push_str(" --random-seed=1534"); } v8::V8::set_flags_from_string(&flags); let platform = v8::new_default_platform(0, false).make_shared(); v8::V8::initialize_platform(platform); v8::V8::initialize(); process }) } /// Initialize V8 for reproducible snapshot generation. /// /// This must run before any other avril-core use, in a dedicated process such as a /// build script: it seeds V8's process-wide random number generators, so runtimes /// are refused afterwards with [`SnapshotError::GeneratorProcess`]. pub fn initialize_snapshot_generator() -> Result<(), SnapshotError> { match initialize_v8(Process::Generator) { Process::Generator => Ok(()), Process::Runtime => Err(SnapshotError::RuntimeProcess), } } fn runtime_process() -> Result<(), SnapshotError> { match initialize_v8(Process::Runtime) { Process::Runtime => Ok(()), Process::Generator => Err(SnapshotError::GeneratorProcess), } } pub(crate) enum ContextSource<'a> { /// A new context for cold startup or snapshot generation. Fresh, Layout(&'a snapshot::Layout), } impl Runtime { /// Bootstrap on V8's built-in heap. pub fn new(options: RuntimeOptions) -> Result { runtime_process()?; let isolate = snapshot::IsolateOwner::new(snapshot::Source::BuiltIn)?; Self::bootstrap(isolate, options, ContextSource::Fresh)?.start() } /// Restore the snapshot layout matching these extensions, attach fresh native /// state, then run live initializers. pub fn from_snapshot(options: RuntimeOptions, snapshot: &Snapshot) -> Result { runtime_process()?; let layout = snapshot.layout(&snapshot::fingerprint(&options.extensions)?)?; let isolate = snapshot::IsolateOwner::new(snapshot::Source::Snapshot(snapshot))?; Self::bootstrap(isolate, options, ContextSource::Layout(layout))?.start() } /// Evaluate each layout's static extension graph, without live initializers, /// into one snapshot whose contexts share a read-only heap. /// /// Static entries must not retain native resources, pending work, or global /// handles outside core-owned state: V8 aborts the process rather than /// serialize a heap still referenced by foreign handles. /// /// Run generation in a short-lived build process, after /// [`initialize_snapshot_generator`] for reproducible bytes. rusty_v8 cannot /// dispose an unfinished snapshot creator: on failure, core-owned native state /// is released, but the creator and V8's built-in read-only heap are leaked /// (with a warning) until process exit, so later snapshot restores in this /// process fail with [`SnapshotError::ReadOnlyHeapInUse`]. pub fn create_snapshot(layouts: Vec) -> Result { initialize_v8(Process::Runtime); snapshot::create(layouts) } pub(crate) fn bootstrap( mut isolate: snapshot::IsolateOwner, options: RuntimeOptions, context: ContextSource, ) -> Result { let order = extensions::validate(&options.extensions)?; let mut ops = Vec::new(); let mut sources = HashMap::new(); for extension in &options.extensions { sources.extend(extension.modules.iter().copied()); } sources.extend(extensions::SOURCES.iter().copied()); let entries = order .iter() .filter_map(|index| options.extensions[*index].entry_point) .collect::>(); for extension in options.extensions { ops.extend(extension.ops); } let ops = op::RegisteredOps::new(ops)?; let wasm_streaming = Rc::new(RefCell::new(WasmStreamingRegistry::default())); isolate.set_slot(wasm_streaming.clone()); isolate.set_wasm_streaming_callback(|scope, source, stream| { let wasm_streaming = scope .get_slot::>>() .unwrap() .clone(); let started = wasm_streaming.borrow_mut().start(stream); match started { Ok(start) => { if let Some(callback) = start.callback { let callback = v8::Local::new(scope, callback); let id = v8::Number::new(scope, start.id as f64); let undefined = v8::undefined(scope); v8::tc_scope!(let try_catch, scope); let result = callback.call(try_catch, undefined.into(), &[source, id.into()]); if result.is_none() { let exception = try_catch .exception() .unwrap_or_else(|| v8::undefined(try_catch).into()); try_catch.reset(); let _ = wasm_streaming.borrow_mut().abort(start.id, exception); } } else { let message = v8::String::new(scope, "Wasm streaming callback is unavailable") .unwrap(); let exception = v8::Exception::type_error(scope, message); let _ = wasm_streaming.borrow_mut().abort(start.id, exception); } } Err(start) => { let message = v8::String::new(scope, &start.message).unwrap(); let exception = v8::Exception::type_error(scope, message); start.stream.abort(Some(exception)); } } }); isolate.set_microtasks_policy(v8::MicrotasksPolicy::Explicit); isolate.set_slot(ops.clone()); op::init(&mut isolate); reporting::init(&mut isolate); microtasks::init(&mut isolate); tasks::init(&mut isolate); modules::init(&mut isolate, options.module_loader); let capture = isolate.is_creator(); let restored = matches!(context, ContextSource::Layout(_)); let context = { v8::scope!(let scope, &mut *isolate); match context { ContextSource::Layout(layout) => { let context = v8::Context::from_snapshot(scope, layout.context, Default::default()) .ok_or(SnapshotError::InvalidArtifact("missing layout context"))?; let scope = &mut v8::ContextScope::new(scope, context); extensions::restore(scope, sources, &layout.state)?; v8::Global::new(scope, context) } ContextSource::Fresh => { let context = v8::Context::new(scope, Default::default()); let scope = &mut v8::ContextScope::new(scope, context); if capture { snapshot::define_stack_trace_limit(scope).unwrap(); } let registry = ops.object(scope).unwrap(); let mut source = source( scope, "", include_str!("primordials.js"), false, None, ); let primordials = v8::script_compiler::compile( scope, &mut source, v8::script_compiler::CompileOptions::NoCompileOptions, v8::script_compiler::NoCacheReason::NoReason, ) .unwrap() .run(scope) .unwrap(); let primordial_object = v8::Local::::try_from(primordials).unwrap(); extensions::init(scope, sources, primordial_object, registry).unwrap(); v8::Global::new(scope, context) } } }; let mut runtime = Self { repl: None, module_evaluation: None, context, isolate, }; runtime.with_context(|scope| { let module = extensions::execute(scope, "ext:core/stack_trace.js")?; let namespace = v8::Local::::try_from(module.get_module_namespace()) .expect("Core stack formatter must have a module namespace"); let key = v8::String::new(scope, "formatStackTrace")?; let formatter = v8::Local::::try_from(namespace.get(scope, key.into())?) .expect("Core stack formatter must export formatStackTrace"); stack_trace::init(scope, formatter); extensions::execute(scope, "ext:core/shared_array_buffer")?; Some(()) })?; if !restored { for entry in entries { runtime.with_context(|scope| extensions::execute(scope, entry).map(|_| ()))?; } } runtime .with_context(|scope| extensions::execute(scope, "ext:core/bootstrap").map(|_| ()))?; Ok(runtime) } /// Run the live initializers registered through `ext:core/bootstrap`, exactly once. fn start(mut self) -> Result { self.with_context(|scope| { let module = extensions::execute(scope, "ext:core/bootstrap")?; let namespace = v8::Local::::try_from(module.get_module_namespace()).ok()?; let key = v8::String::new(scope, "startRuntime")?; let start = v8::Local::::try_from(namespace.get(scope, key.into())?).ok()?; start.call(scope, v8::undefined(scope).into(), &[])?; Some(()) })?; Ok(self) } fn with_context( &mut self, execute: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>) -> Option, ) -> Result { v8::scope!(let scope, &mut *self.isolate); let context = v8::Local::new(scope, &self.context); let scope = &mut v8::ContextScope::new(scope, context); v8::tc_scope!(let scope, scope); reporting::clear_processed(scope); let result = execute(scope); let execution_exception = if result.is_none() { scope.exception().map(|error| v8::Global::new(scope, error)) } else { None }; let execution_exception_reported = if let Some(error) = &execution_exception { let error = v8::Local::new(scope, error); reporting::was_processed(scope, error) } else { false }; op::checkpoint(scope); let exception = microtasks::process_exceptions(scope); let rejection = op::process_rejections(scope); let result = result.and_then(|value| { if let Some(exception) = exception { let error = v8::Local::new(scope, exception); reporting::clear_processed(scope); reporting::mark_processed_exception(scope, error); scope.throw_exception(error); None } else if let Some(error) = rejection { let error = v8::Local::new(scope, error); if let Some(execution_exception) = &execution_exception { reporting::clear_processed(scope); scope.throw_exception(v8::Local::new(scope, execution_exception)); } else { scope.throw_exception(error); } None } else { Some(value) } }); if result.is_none() && let Some(error) = scope.exception() && !(execution_exception_reported && execution_exception .as_ref() .is_some_and(|reported| v8::Local::new(scope, reported) == error)) && (execution_exception.is_some() || !reporting::was_processed(scope, error)) { reporting::report_exception_for_value(scope, error); } let result = result.ok_or_else(|| { let exception = scope .stack_trace() .filter(|value| !value.is_undefined()) .or_else(|| scope.exception()); let message = exception .map(|value| value.to_rust_string_lossy(scope)) .unwrap_or_else(|| "JavaScript execution terminated".into()); Error::JavaScript(message) }); reporting::clear_processed(scope); result } /// Register a built-in module, capturing its export values at registration. pub fn register_module( &mut self, name: &str, exports: impl for<'s, 'i> FnOnce(&mut v8::PinScope<'s, 'i>) -> Option>, ) -> Result<(), Error> { self.with_context(|scope| { let exports = exports(scope)?; modules::register(scope, name, exports) }) } /// Register an extension module's exports as a built-in module, capturing /// its export values at registration. The extension module is evaluated if /// it has not been already. pub fn register_extension_module( &mut self, name: &str, extension_module: &str, ) -> Result<(), Error> { self.with_context(|scope| { let module = extensions::execute(scope, extension_module)?; let namespace = v8::Local::::try_from(module.get_module_namespace()).ok()?; modules::register(scope, name, namespace) }) } /// Execute a module graph; the first entry is the runtime's main module. pub fn execute_module(&mut self, name: &str) -> Result<(), Error> { let name = modules::prepare_entry(&mut self.isolate, name)?; self.with_context(|scope| { let promise = modules::execute(scope, &name)?; op::run_event_loop(scope, Some(promise), true) }) } /// Evaluate an entry, then pass its namespace to a trusted extension bootstrap. pub fn execute_module_with_bootstrap( &mut self, name: &str, bootstrap: &str, export: &str, ) -> Result<(), Error> { let name = modules::prepare_entry(&mut self.isolate, name)?; self.with_context(|scope| { let promise = modules::execute(scope, &name)?; op::run_event_loop(scope, Some(promise), false)?; let namespace = modules::namespace(scope, &name)?; let module = extensions::execute(scope, bootstrap)?; let exports = v8::Local::::try_from(module.get_module_namespace()).ok()?; let key = v8::String::new(scope, export)?; let function = v8::Local::::try_from(exports.get(scope, key.into())?).ok()?; let undefined = v8::undefined(scope); let value = function.call(scope, undefined.into(), &[namespace])?; let completion = v8::Local::::try_from(value).ok(); op::run_event_loop(scope, completion, false) }) } /// Start a prepared entry without blocking on top-level await. Polling the event /// loop also monitors its evaluation promise. pub fn start_module(&mut self, name: &str) -> Result<(), Error> { if self.module_evaluation.is_some() { return Err(Error::Module("Module evaluation is already pending".into())); } let name = modules::prepare_entry(&mut self.isolate, name)?; let promise = self.with_context(|scope| { modules::execute(scope, &name).map(|promise| v8::Global::new(scope, promise)) })?; self.module_evaluation = Some(promise); Ok(()) } /// Drive microtasks and native ops until idle, reporting unhandled rejections. pub fn run_event_loop(&mut self) -> Result<(), Error> { let evaluation = self.module_evaluation.take(); self.with_context(|scope| { let promise = evaluation.map(|promise| v8::Local::new(scope, promise)); op::run_event_loop(scope, promise, true) }) } /// Drive one task or native completion and register the caller's wakeup. /// A false result does not imply idle: native work may still be pending. pub fn poll_event_loop(&mut self, context: &mut Context<'_>) -> Result { self.isolate.set_slot(context.waker().clone()); let evaluation = self.module_evaluation.take(); let (progressed, evaluation) = self.with_context(|scope| { let progressed = op::poll_event_loop(scope, context)?; let evaluation = if let Some(promise) = evaluation { let local = v8::Local::new(scope, &promise); match local.state() { v8::PromiseState::Pending => Some(promise), v8::PromiseState::Fulfilled => None, v8::PromiseState::Rejected => { let error = local.result(scope); scope.throw_exception(error); return None; } } } else { None }; Some((progressed, evaluation)) })?; self.module_evaluation = evaluation; if progressed { context.waker().wake_by_ref(); } Ok(progressed) } pub fn isolate_handle(&self) -> v8::IsolateHandle { self.isolate.thread_safe_handle() } pub fn has_pending_background_tasks(&self) -> bool { self.isolate.has_pending_background_tasks() } pub fn has_pending_work(&self) -> bool { self.module_evaluation.is_some() || op::has_pending_work(&self.isolate) || tasks::has_pending(&self.isolate) || self.has_pending_background_tasks() } pub fn enable_repl(&mut self) -> Result<(), Error> { if self.repl.is_some() { return Err(Error::JavaScript( "REPL inspector is already enabled".into(), )); } self.repl = Some(repl::Inspector::new(&mut self.isolate, &self.context)); let id = self.dispatch_repl_message(json!({"method": "Runtime.enable", "params": {}}))?; let _ = self.take_repl_response(id); let inspector = self.repl.as_mut().unwrap(); let Some(context_id) = inspector.context_id() else { return Err(Error::JavaScript( "REPL execution context was not created".into(), )); }; inspector.context_id = context_id; Ok(()) } pub fn evaluate_repl(&mut self, expression: &str, source_url: &str) -> Result { self.set_repl_referrer(Some(source_url))?; let expression = format!("{expression}\n//# sourceURL={source_url}"); self.dispatch_repl_message(json!({ "method": "Runtime.evaluate", "params": { "expression": expression, "contextId": self.repl.as_ref().map(|inspector| inspector.context_id), "awaitPromise": true, "replMode": true, "includeCommandLineAPI": false, "objectGroup": "avril-repl" } })) } /// Call a function in the REPL context with inspector `CallArgument`s, such as /// `{"objectId": ...}` from an earlier result, and resolve to its JSON value. pub fn call_repl_function( &mut self, declaration: &str, arguments: &[serde_json::Value], ) -> Result { self.dispatch_repl_message(json!({ "method": "Runtime.callFunctionOn", "params": { "functionDeclaration": declaration, "executionContextId": self.repl.as_ref().map(|inspector| inspector.context_id), "arguments": arguments, "awaitPromise": true, "returnByValue": true, "objectGroup": "avril-repl" } })) } pub fn has_repl_response(&self, id: i32) -> bool { self.repl .as_ref() .is_some_and(|inspector| inspector.has_response(id)) } pub fn take_repl_response(&mut self, id: i32) -> Option { self.repl.as_mut()?.take_response(id) } fn set_repl_referrer(&mut self, referrer: Option<&str>) -> Result<(), Error> { v8::scope!(let scope, &mut *self.isolate); let context = v8::Local::new(scope, &self.context); let scope = &mut v8::ContextScope::new(scope, context); modules::set_repl_referrer(scope, referrer) .ok_or_else(|| Error::JavaScript("REPL module map is unavailable".into())) } pub fn terminate_repl_execution(&mut self) -> Result<(), Error> { let id = self.dispatch_repl_message(json!({ "method": "Runtime.terminateExecution", "params": {} }))?; let _ = self.take_repl_response(id); Ok(()) } pub fn release_repl_objects(&mut self) -> Result<(), Error> { let id = self.dispatch_repl_message(json!({ "method": "Runtime.releaseObjectGroup", "params": {"objectGroup": "avril-repl"} }))?; let _ = self.take_repl_response(id); Ok(()) } fn dispatch_repl_message(&mut self, mut command: serde_json::Value) -> Result { let inspector = self .repl .as_mut() .ok_or_else(|| Error::JavaScript("REPL inspector is not enabled".into()))?; let id = inspector.next_id; inspector.next_id = inspector .next_id .checked_add(1) .ok_or_else(|| Error::JavaScript("REPL inspector command ID space exhausted".into()))?; command["id"] = json!(id); let message = serde_json::to_string(&command) .map_err(|error| Error::JavaScript(error.to_string()))?; inspector .session .dispatch_protocol_message(StringView::from(message.as_bytes())); Ok(id) } pub fn disable_repl(&mut self) -> Result<(), Error> { self.isolate.remove_slot::(); let result = self.set_repl_referrer(None); self.repl.take(); result } /// Evaluate a script and drain microtasks without waiting for native ops. pub fn execute_script( &mut self, name: &str, code: &str, ) -> Result { self.with_context(|scope| { let mut source = source(scope, name, code, false, None); let value = v8::script_compiler::compile( scope, &mut source, v8::script_compiler::CompileOptions::NoCompileOptions, v8::script_compiler::NoCacheReason::NoReason, )? .run(scope)?; op::checkpoint(scope); Some(serde_v8::from_v8(scope, value).map_err(Error::from)) })? } }