From ec8e9d4f5bd2c8eed5f086356c1c6dd7c8b40b7f Mon Sep 17 00:00:00 2001 From: Matt Mastracci Date: Tue, 13 Jun 2023 20:03:10 -0600 Subject: chore(core): Refactor runtime and split out tests (#19491) This is a quick first refactoring to split the tests out of runtime and move runtime-related code to a top-level runtime module. There will be a followup to refactor imports a bit, but this is the major change that will most likely conflict with other work and I want to merge it early. --- core/bindings.js | 51 - core/bindings.rs | 617 ---- core/encode_decode_test.js | 69 - core/error.rs | 56 +- core/error_builder_test.js | 32 - core/icudtl.dat | Bin 10541264 -> 0 bytes core/lib.rs | 32 +- core/modules/map.rs | 26 +- core/modules/tests.rs | 4 +- core/ops.rs | 4 +- core/ops_builtin_v8.rs | 2 +- core/realm.rs | 368 --- core/runtime.rs | 4934 ---------------------------- core/runtime/bindings.js | 51 + core/runtime/bindings.rs | 598 ++++ core/runtime/encode_decode_test.js | 69 + core/runtime/error_builder_test.js | 32 + core/runtime/icudtl.dat | Bin 0 -> 10541264 bytes core/runtime/jsrealm.rs | 367 +++ core/runtime/jsruntime.rs | 2416 ++++++++++++++ core/runtime/mod.rs | 35 + core/runtime/ops.rs | 156 + core/runtime/serialize_deserialize_test.js | 74 + core/runtime/snapshot_util.rs | 252 ++ core/runtime/tests.rs | 2306 +++++++++++++ core/serialize_deserialize_test.js | 74 - core/snapshot_util.rs | 252 -- 27 files changed, 6456 insertions(+), 6421 deletions(-) delete mode 100644 core/bindings.js delete mode 100644 core/bindings.rs delete mode 100644 core/encode_decode_test.js delete mode 100644 core/error_builder_test.js delete mode 100644 core/icudtl.dat delete mode 100644 core/realm.rs delete mode 100644 core/runtime.rs create mode 100644 core/runtime/bindings.js create mode 100644 core/runtime/bindings.rs create mode 100644 core/runtime/encode_decode_test.js create mode 100644 core/runtime/error_builder_test.js create mode 100644 core/runtime/icudtl.dat create mode 100644 core/runtime/jsrealm.rs create mode 100644 core/runtime/jsruntime.rs create mode 100644 core/runtime/mod.rs create mode 100644 core/runtime/ops.rs create mode 100644 core/runtime/serialize_deserialize_test.js create mode 100644 core/runtime/snapshot_util.rs create mode 100644 core/runtime/tests.rs delete mode 100644 core/serialize_deserialize_test.js delete mode 100644 core/snapshot_util.rs (limited to 'core') diff --git a/core/bindings.js b/core/bindings.js deleted file mode 100644 index 21d27a2c3..000000000 --- a/core/bindings.js +++ /dev/null @@ -1,51 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -if (!globalThis.Deno) { - globalThis.Deno = { - core: { - ops: {}, - asyncOps: {}, - }, - }; -} - -Deno.__op__console = function (callConsole, console) { - Deno.core.callConsole = callConsole; - Deno.core.console = console; -}; - -Deno.__op__registerOp = function (isAsync, op, opName) { - const core = Deno.core; - if (isAsync) { - if (core.ops[opName] !== undefined) { - return; - } - core.asyncOps[opName] = op; - const fn = function (...args) { - if (this !== core.ops) { - // deno-lint-ignore prefer-primordials - throw new Error( - "An async stub cannot be separated from Deno.core.ops. Use ???", - ); - } - return core.asyncStub(opName, args); - }; - fn.name = opName; - core.ops[opName] = fn; - } else { - core.ops[opName] = op; - } -}; - -Deno.__op__unregisterOp = function (isAsync, opName) { - if (isAsync) { - delete Deno.core.asyncOps[opName]; - } - delete Deno.core.ops[opName]; -}; - -Deno.__op__cleanup = function () { - delete Deno.__op__console; - delete Deno.__op__registerOp; - delete Deno.__op__unregisterOp; - delete Deno.__op__cleanup; -}; diff --git a/core/bindings.rs b/core/bindings.rs deleted file mode 100644 index 2be9b35b6..000000000 --- a/core/bindings.rs +++ /dev/null @@ -1,617 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use log::debug; -use std::fmt::Write; -use std::option::Option; -use std::os::raw::c_void; -use v8::MapFnTo; - -use crate::error::is_instance_of_error; -use crate::error::JsStackFrame; -use crate::modules::get_asserted_module_type_from_assertions; -use crate::modules::parse_import_assertions; -use crate::modules::validate_import_assertions; -use crate::modules::ImportAssertionsKind; -use crate::modules::ModuleMap; -use crate::modules::ResolutionKind; -use crate::ops::OpCtx; -use crate::runtime::InitMode; -use crate::JsRealm; -use crate::JsRuntime; - -pub(crate) fn external_references(ops: &[OpCtx]) -> v8::ExternalReferences { - // Overallocate a bit, it's better than having to resize the vector. - let mut references = Vec::with_capacity(4 + ops.len() * 4); - - references.push(v8::ExternalReference { - function: call_console.map_fn_to(), - }); - references.push(v8::ExternalReference { - function: import_meta_resolve.map_fn_to(), - }); - references.push(v8::ExternalReference { - function: catch_dynamic_import_promise_error.map_fn_to(), - }); - references.push(v8::ExternalReference { - function: empty_fn.map_fn_to(), - }); - - for ctx in ops { - let ctx_ptr = ctx as *const OpCtx as _; - references.push(v8::ExternalReference { pointer: ctx_ptr }); - references.push(v8::ExternalReference { - function: ctx.decl.v8_fn_ptr, - }); - if let Some(fast_fn) = &ctx.decl.fast_fn { - references.push(v8::ExternalReference { - pointer: fast_fn.function as _, - }); - references.push(v8::ExternalReference { - pointer: ctx.fast_fn_c_info.unwrap().as_ptr() as _, - }); - } - } - - let refs = v8::ExternalReferences::new(&references); - // Leak, V8 takes ownership of the references. - std::mem::forget(references); - refs -} - -// TODO(nayeemrmn): Move to runtime and/or make `pub(crate)`. -pub fn script_origin<'a>( - s: &mut v8::HandleScope<'a>, - resource_name: v8::Local<'a, v8::String>, -) -> v8::ScriptOrigin<'a> { - let source_map_url = v8::String::empty(s); - v8::ScriptOrigin::new( - s, - resource_name.into(), - 0, - 0, - false, - 123, - source_map_url.into(), - true, - false, - false, - ) -} - -pub fn module_origin<'a>( - s: &mut v8::HandleScope<'a>, - resource_name: v8::Local<'a, v8::String>, -) -> v8::ScriptOrigin<'a> { - let source_map_url = v8::String::empty(s); - v8::ScriptOrigin::new( - s, - resource_name.into(), - 0, - 0, - false, - 123, - source_map_url.into(), - true, - false, - true, - ) -} - -fn get<'s, T>( - scope: &mut v8::HandleScope<'s>, - from: v8::Local, - key: &'static [u8], - path: &'static str, -) -> T -where - v8::Local<'s, v8::Value>: TryInto, -{ - let key = v8::String::new_external_onebyte_static(scope, key).unwrap(); - from - .get(scope, key.into()) - .unwrap_or_else(|| panic!("{path} exists")) - .try_into() - .unwrap_or_else(|_| panic!("unable to convert")) -} - -pub(crate) fn initialize_context<'s>( - scope: &mut v8::HandleScope<'s>, - context: v8::Local<'s, v8::Context>, - op_ctxs: &[OpCtx], - init_mode: InitMode, -) -> v8::Local<'s, v8::Context> { - let global = context.global(scope); - - let mut codegen = String::with_capacity(op_ctxs.len() * 200); - codegen.push_str(include_str!("bindings.js")); - _ = writeln!( - codegen, - "Deno.__op__ = function(opFns, callConsole, console) {{" - ); - if init_mode == InitMode::New { - _ = writeln!(codegen, "Deno.__op__console(callConsole, console);"); - } - for op_ctx in op_ctxs { - if op_ctx.decl.enabled { - _ = writeln!( - codegen, - "Deno.__op__registerOp({}, opFns[{}], \"{}\");", - op_ctx.decl.is_async, op_ctx.id, op_ctx.decl.name - ); - } else { - _ = writeln!( - codegen, - "Deno.__op__unregisterOp({}, \"{}\");", - op_ctx.decl.is_async, op_ctx.decl.name - ); - } - } - codegen.push_str("Deno.__op__cleanup();"); - _ = writeln!(codegen, "}}"); - - let script = v8::String::new_from_one_byte( - scope, - codegen.as_bytes(), - v8::NewStringType::Normal, - ) - .unwrap(); - let script = v8::Script::compile(scope, script, None).unwrap(); - script.run(scope); - - let deno = get(scope, global, b"Deno", "Deno"); - let op_fn: v8::Local = - get(scope, deno, b"__op__", "Deno.__op__"); - let recv = v8::undefined(scope); - let op_fns = v8::Array::new(scope, op_ctxs.len() as i32); - for op_ctx in op_ctxs { - let op_fn = op_ctx_function(scope, op_ctx); - op_fns.set_index(scope, op_ctx.id as u32, op_fn.into()); - } - if init_mode == InitMode::FromSnapshot { - op_fn.call(scope, recv.into(), &[op_fns.into()]); - } else { - // Bind functions to Deno.core.* - let call_console_fn = v8::Function::new(scope, call_console).unwrap(); - - // Bind v8 console object to Deno.core.console - let extra_binding_obj = context.get_extras_binding_object(scope); - let console_obj: v8::Local = get( - scope, - extra_binding_obj, - b"console", - "ExtrasBindingObject.console", - ); - - op_fn.call( - scope, - recv.into(), - &[op_fns.into(), call_console_fn.into(), console_obj.into()], - ); - } - - context -} - -fn op_ctx_function<'s>( - scope: &mut v8::HandleScope<'s>, - op_ctx: &OpCtx, -) -> v8::Local<'s, v8::Function> { - let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void; - let external = v8::External::new(scope, op_ctx_ptr as *mut c_void); - let v8name = - v8::String::new_external_onebyte_static(scope, op_ctx.decl.name.as_bytes()) - .unwrap(); - let builder: v8::FunctionBuilder = - v8::FunctionTemplate::builder_raw(op_ctx.decl.v8_fn_ptr) - .data(external.into()) - .length(op_ctx.decl.arg_count as i32); - - let template = if let Some(fast_function) = &op_ctx.decl.fast_fn { - builder.build_fast( - scope, - fast_function, - Some(op_ctx.fast_fn_c_info.unwrap().as_ptr()), - None, - None, - ) - } else { - builder.build(scope) - }; - - let v8fn = template.get_function(scope).unwrap(); - v8fn.set_name(v8name); - v8fn -} - -pub extern "C" fn wasm_async_resolve_promise_callback( - _isolate: *mut v8::Isolate, - context: v8::Local, - resolver: v8::Local, - compilation_result: v8::Local, - success: v8::WasmAsyncSuccess, -) { - // SAFETY: `CallbackScope` can be safely constructed from `Local` - let scope = &mut unsafe { v8::CallbackScope::new(context) }; - if success == v8::WasmAsyncSuccess::Success { - resolver.resolve(scope, compilation_result).unwrap(); - } else { - resolver.reject(scope, compilation_result).unwrap(); - } -} - -pub fn host_import_module_dynamically_callback<'s>( - scope: &mut v8::HandleScope<'s>, - _host_defined_options: v8::Local<'s, v8::Data>, - resource_name: v8::Local<'s, v8::Value>, - specifier: v8::Local<'s, v8::String>, - import_assertions: v8::Local<'s, v8::FixedArray>, -) -> Option> { - // NOTE(bartlomieju): will crash for non-UTF-8 specifier - let specifier_str = specifier - .to_string(scope) - .unwrap() - .to_rust_string_lossy(scope); - let referrer_name_str = resource_name - .to_string(scope) - .unwrap() - .to_rust_string_lossy(scope); - - let resolver = v8::PromiseResolver::new(scope).unwrap(); - let promise = resolver.get_promise(scope); - - let assertions = parse_import_assertions( - scope, - import_assertions, - ImportAssertionsKind::DynamicImport, - ); - - { - let tc_scope = &mut v8::TryCatch::new(scope); - validate_import_assertions(tc_scope, &assertions); - if tc_scope.has_caught() { - let e = tc_scope.exception().unwrap(); - resolver.reject(tc_scope, e); - } - } - let asserted_module_type = - get_asserted_module_type_from_assertions(&assertions); - - let resolver_handle = v8::Global::new(scope, resolver); - { - let state_rc = JsRuntime::state_from(scope); - let module_map_rc = JsRuntime::module_map_from(scope); - - debug!( - "dyn_import specifier {} referrer {} ", - specifier_str, referrer_name_str - ); - ModuleMap::load_dynamic_import( - module_map_rc, - &specifier_str, - &referrer_name_str, - asserted_module_type, - resolver_handle, - ); - state_rc.borrow_mut().notify_new_dynamic_import(); - } - // Map errors from module resolution (not JS errors from module execution) to - // ones rethrown from this scope, so they include the call stack of the - // dynamic import site. Error objects without any stack frames are assumed to - // be module resolution errors, other exception values are left as they are. - let builder = v8::FunctionBuilder::new(catch_dynamic_import_promise_error); - - let map_err = - v8::FunctionBuilder::::build(builder, scope).unwrap(); - - let promise = promise.catch(scope, map_err).unwrap(); - - Some(promise) -} - -pub extern "C" fn host_initialize_import_meta_object_callback( - context: v8::Local, - module: v8::Local, - meta: v8::Local, -) { - // SAFETY: `CallbackScope` can be safely constructed from `Local` - let scope = &mut unsafe { v8::CallbackScope::new(context) }; - let module_map_rc = JsRuntime::module_map_from(scope); - let module_map = module_map_rc.borrow(); - - let module_global = v8::Global::new(scope, module); - let info = module_map - .get_info(&module_global) - .expect("Module not found"); - - let url_key = v8::String::new_external_onebyte_static(scope, b"url").unwrap(); - let url_val = info.name.v8(scope); - meta.create_data_property(scope, url_key.into(), url_val.into()); - - let main_key = - v8::String::new_external_onebyte_static(scope, b"main").unwrap(); - let main_val = v8::Boolean::new(scope, info.main); - meta.create_data_property(scope, main_key.into(), main_val.into()); - - let builder = - v8::FunctionBuilder::new(import_meta_resolve).data(url_val.into()); - let val = v8::FunctionBuilder::::build(builder, scope).unwrap(); - let resolve_key = - v8::String::new_external_onebyte_static(scope, b"resolve").unwrap(); - meta.set(scope, resolve_key.into(), val.into()); -} - -fn import_meta_resolve( - scope: &mut v8::HandleScope, - args: v8::FunctionCallbackArguments, - mut rv: v8::ReturnValue, -) { - if args.length() > 1 { - return throw_type_error(scope, "Invalid arguments"); - } - - let maybe_arg_str = args.get(0).to_string(scope); - if maybe_arg_str.is_none() { - return throw_type_error(scope, "Invalid arguments"); - } - let specifier = maybe_arg_str.unwrap(); - let referrer = { - let url_prop = args.data(); - url_prop.to_rust_string_lossy(scope) - }; - let module_map_rc = JsRuntime::module_map_from(scope); - let loader = module_map_rc.borrow().loader.clone(); - let specifier_str = specifier.to_rust_string_lossy(scope); - - if specifier_str.starts_with("npm:") { - throw_type_error(scope, "\"npm:\" specifiers are currently not supported in import.meta.resolve()"); - return; - } - - match loader.resolve(&specifier_str, &referrer, ResolutionKind::DynamicImport) - { - Ok(resolved) => { - let resolved_val = serde_v8::to_v8(scope, resolved.as_str()).unwrap(); - rv.set(resolved_val); - } - Err(err) => { - throw_type_error(scope, &err.to_string()); - } - }; -} - -fn empty_fn( - _scope: &mut v8::HandleScope, - _args: v8::FunctionCallbackArguments, - _rv: v8::ReturnValue, -) { - //Do Nothing -} - -//It creates a reference to an empty function which can be mantained after the snapshots -pub fn create_empty_fn<'s>( - scope: &mut v8::HandleScope<'s>, -) -> Option> { - let empty_fn = v8::FunctionTemplate::new(scope, empty_fn); - empty_fn.get_function(scope) -} - -fn catch_dynamic_import_promise_error( - scope: &mut v8::HandleScope, - args: v8::FunctionCallbackArguments, - _rv: v8::ReturnValue, -) { - let arg = args.get(0); - if is_instance_of_error(scope, arg) { - let e: crate::error::NativeJsError = serde_v8::from_v8(scope, arg).unwrap(); - let name = e.name.unwrap_or_else(|| "Error".to_string()); - let msg = v8::Exception::create_message(scope, arg); - if msg.get_stack_trace(scope).unwrap().get_frame_count() == 0 { - let arg: v8::Local = arg.try_into().unwrap(); - let message_key = - v8::String::new_external_onebyte_static(scope, b"message").unwrap(); - let message = arg.get(scope, message_key.into()).unwrap(); - let mut message: v8::Local = message.try_into().unwrap(); - if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) { - if let Some(location) = stack_frame.maybe_format_location() { - let str = - format!("{} at {location}", message.to_rust_string_lossy(scope)); - message = v8::String::new(scope, &str).unwrap(); - } - } - let exception = match name.as_str() { - "RangeError" => v8::Exception::range_error(scope, message), - "TypeError" => v8::Exception::type_error(scope, message), - "SyntaxError" => v8::Exception::syntax_error(scope, message), - "ReferenceError" => v8::Exception::reference_error(scope, message), - _ => v8::Exception::error(scope, message), - }; - let code_key = - v8::String::new_external_onebyte_static(scope, b"code").unwrap(); - let code_value = - v8::String::new_external_onebyte_static(scope, b"ERR_MODULE_NOT_FOUND") - .unwrap(); - let exception_obj = exception.to_object(scope).unwrap(); - exception_obj.set(scope, code_key.into(), code_value.into()); - scope.throw_exception(exception); - return; - } - } - scope.throw_exception(arg); -} - -pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { - use v8::PromiseRejectEvent::*; - - // SAFETY: `CallbackScope` can be safely constructed from `&PromiseRejectMessage` - let scope = &mut unsafe { v8::CallbackScope::new(&message) }; - - let context_state_rc = JsRealm::state_from_scope(scope); - let mut context_state = context_state_rc.borrow_mut(); - - if let Some(js_promise_reject_cb) = context_state.js_promise_reject_cb.clone() - { - drop(context_state); - - let tc_scope = &mut v8::TryCatch::new(scope); - let undefined: v8::Local = v8::undefined(tc_scope).into(); - let type_ = v8::Integer::new(tc_scope, message.get_event() as i32); - let promise = message.get_promise(); - - let reason = match message.get_event() { - PromiseRejectWithNoHandler - | PromiseRejectAfterResolved - | PromiseResolveAfterResolved => message.get_value().unwrap_or(undefined), - PromiseHandlerAddedAfterReject => undefined, - }; - - let promise_global = v8::Global::new(tc_scope, promise); - let args = &[type_.into(), promise.into(), reason]; - let maybe_has_unhandled_rejection_handler = js_promise_reject_cb - .open(tc_scope) - .call(tc_scope, undefined, args); - - let has_unhandled_rejection_handler = - if let Some(value) = maybe_has_unhandled_rejection_handler { - value.is_true() - } else { - false - }; - - if has_unhandled_rejection_handler { - let state_rc = JsRuntime::state_from(tc_scope); - let mut state = state_rc.borrow_mut(); - if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() { - if !pending_mod_evaluate.has_evaluated { - pending_mod_evaluate - .handled_promise_rejections - .push(promise_global); - } - } - } - } else { - let promise = message.get_promise(); - let promise_global = v8::Global::new(scope, promise); - match message.get_event() { - PromiseRejectWithNoHandler => { - let error = message.get_value().unwrap(); - let error_global = v8::Global::new(scope, error); - context_state - .pending_promise_rejections - .push_back((promise_global, error_global)); - } - PromiseHandlerAddedAfterReject => { - context_state - .pending_promise_rejections - .retain(|(key, _)| key != &promise_global); - } - PromiseRejectAfterResolved => {} - PromiseResolveAfterResolved => { - // Should not warn. See #1272 - } - } - } -} - -/// This binding should be used if there's a custom console implementation -/// available. Using it will make sure that proper stack frames are displayed -/// in the inspector console. -/// -/// Each method on console object should be bound to this function, eg: -/// ```ignore -/// function wrapConsole(consoleFromDeno, consoleFromV8) { -/// const callConsole = core.callConsole; -/// -/// for (const key of Object.keys(consoleFromV8)) { -/// if (consoleFromDeno.hasOwnProperty(key)) { -/// consoleFromDeno[key] = callConsole.bind( -/// consoleFromDeno, -/// consoleFromV8[key], -/// consoleFromDeno[key], -/// ); -/// } -/// } -/// } -/// ``` -/// -/// Inspired by: -/// https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/src/inspector_js_api.cc#L194-L222 -fn call_console( - scope: &mut v8::HandleScope, - args: v8::FunctionCallbackArguments, - _rv: v8::ReturnValue, -) { - if args.length() < 2 - || !args.get(0).is_function() - || !args.get(1).is_function() - { - return throw_type_error(scope, "Invalid arguments"); - } - - let mut call_args = vec![]; - for i in 2..args.length() { - call_args.push(args.get(i)); - } - - let receiver = args.this(); - let inspector_console_method = - v8::Local::::try_from(args.get(0)).unwrap(); - let deno_console_method = - v8::Local::::try_from(args.get(1)).unwrap(); - - inspector_console_method.call(scope, receiver.into(), &call_args); - deno_console_method.call(scope, receiver.into(), &call_args); -} - -/// Called by V8 during `JsRuntime::instantiate_module`. -/// -/// This function borrows `ModuleMap` from the isolate slot, -/// so it is crucial to ensure there are no existing borrows -/// of `ModuleMap` when `JsRuntime::instantiate_module` is called. -pub fn module_resolve_callback<'s>( - context: v8::Local<'s, v8::Context>, - specifier: v8::Local<'s, v8::String>, - import_assertions: v8::Local<'s, v8::FixedArray>, - referrer: v8::Local<'s, v8::Module>, -) -> Option> { - // SAFETY: `CallbackScope` can be safely constructed from `Local` - let scope = &mut unsafe { v8::CallbackScope::new(context) }; - - let module_map_rc = JsRuntime::module_map_from(scope); - let module_map = module_map_rc.borrow(); - - let referrer_global = v8::Global::new(scope, referrer); - - let referrer_info = module_map - .get_info(&referrer_global) - .expect("ModuleInfo not found"); - let referrer_name = referrer_info.name.as_str(); - - let specifier_str = specifier.to_rust_string_lossy(scope); - - let assertions = parse_import_assertions( - scope, - import_assertions, - ImportAssertionsKind::StaticImport, - ); - let maybe_module = module_map.resolve_callback( - scope, - &specifier_str, - referrer_name, - assertions, - ); - if let Some(module) = maybe_module { - return Some(module); - } - - let msg = format!( - r#"Cannot resolve module "{specifier_str}" from "{referrer_name}""# - ); - throw_type_error(scope, msg); - None -} - -pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef) { - let message = v8::String::new(scope, message.as_ref()).unwrap(); - let exception = v8::Exception::type_error(scope, message); - scope.throw_exception(exception); -} diff --git a/core/encode_decode_test.js b/core/encode_decode_test.js deleted file mode 100644 index 0f4668765..000000000 --- a/core/encode_decode_test.js +++ /dev/null @@ -1,69 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -"use strict"; -function assert(cond) { - if (!cond) { - throw Error("assert"); - } -} - -function assertArrayEquals(a1, a2) { - if (a1.length !== a2.length) throw Error("assert"); - - for (const index in a1) { - if (a1[index] !== a2[index]) { - throw Error("assert"); - } - } -} - -function main() { - // deno-fmt-ignore - const fixture1 = [ - 0xf0, 0x9d, 0x93, 0xbd, - 0xf0, 0x9d, 0x93, 0xae, - 0xf0, 0x9d, 0x94, 0x81, - 0xf0, 0x9d, 0x93, 0xbd - ]; - // deno-fmt-ignore - const fixture2 = [ - 72, 101, 108, 108, - 111, 32, 239, 191, - 189, 239, 191, 189, - 32, 87, 111, 114, - 108, 100 - ]; - - const empty = Deno.core.ops.op_encode(""); - if (empty.length !== 0) throw new Error("assert"); - - assertArrayEquals( - Array.from(Deno.core.ops.op_encode("𝓽𝓮𝔁𝓽")), - fixture1, - ); - assertArrayEquals( - Array.from(Deno.core.ops.op_encode("Hello \udc12\ud834 World")), - fixture2, - ); - - const emptyBuf = Deno.core.ops.op_decode(new Uint8Array(0)); - if (emptyBuf !== "") throw new Error("assert"); - - assert(Deno.core.ops.op_decode(new Uint8Array(fixture1)) === "𝓽𝓮𝔁𝓽"); - assert( - Deno.core.ops.op_decode(new Uint8Array(fixture2)) === - "Hello �� World", - ); - - // See https://github.com/denoland/deno/issues/6649 - let thrown = false; - try { - Deno.core.ops.op_decode(new Uint8Array(2 ** 29)); - } catch (e) { - thrown = true; - assert(e instanceof RangeError); - assert(e.message === "string too long"); - } - assert(thrown); -} - -main(); diff --git a/core/error.rs b/core/error.rs index 16f813b89..07dc98a22 100644 --- a/core/error.rs +++ b/core/error.rs @@ -9,8 +9,7 @@ use std::fmt::Formatter; use anyhow::Error; -use crate::realm::JsRealm; -use crate::runtime::GetErrorClassFn; +use crate::runtime::JsRealm; use crate::runtime::JsRuntime; use crate::source_map::apply_source_map; use crate::source_map::get_source_line; @@ -20,6 +19,9 @@ use crate::url::Url; // TODO(ry) Deprecate AnyError and encourage deno_core::anyhow::Error instead. pub type AnyError = anyhow::Error; +pub type JsErrorCreateFn = dyn Fn(JsError) -> Error; +pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str; + /// Creates a new error with a caller-specified error class name and message. pub fn custom_error( class: &'static str, @@ -643,6 +645,56 @@ fn abbrev_file_name(file_name: &str) -> Option { Some(format!("{}:{},{}......{}", url.scheme(), head, start, end)) } +pub(crate) fn exception_to_err_result( + scope: &mut v8::HandleScope, + exception: v8::Local, + in_promise: bool, +) -> Result { + let state_rc = JsRuntime::state_from(scope); + + let was_terminating_execution = scope.is_execution_terminating(); + // Disable running microtasks for a moment. When upgrading to V8 v11.4 + // we discovered that canceling termination here will cause the queued + // microtasks to run which breaks some tests. + scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit); + // If TerminateExecution was called, cancel isolate termination so that the + // exception can be created. Note that `scope.is_execution_terminating()` may + // have returned false if TerminateExecution was indeed called but there was + // no JS to execute after the call. + scope.cancel_terminate_execution(); + let mut exception = exception; + { + // If termination is the result of a `op_dispatch_exception` call, we want + // to use the exception that was passed to it rather than the exception that + // was passed to this function. + let state = state_rc.borrow(); + exception = if let Some(exception) = &state.dispatched_exception { + v8::Local::new(scope, exception.clone()) + } else if was_terminating_execution && exception.is_null_or_undefined() { + let message = v8::String::new(scope, "execution terminated").unwrap(); + v8::Exception::error(scope, message) + } else { + exception + }; + } + + let mut js_error = JsError::from_v8_exception(scope, exception); + if in_promise { + js_error.exception_message = format!( + "Uncaught (in promise) {}", + js_error.exception_message.trim_start_matches("Uncaught ") + ); + } + + if was_terminating_execution { + // Resume exception termination. + scope.terminate_execution(); + } + scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto); + + Err(js_error.into()) +} + #[cfg(test)] mod tests { use super::*; diff --git a/core/error_builder_test.js b/core/error_builder_test.js deleted file mode 100644 index f442819cb..000000000 --- a/core/error_builder_test.js +++ /dev/null @@ -1,32 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -const { core } = Deno; -const { ops } = core; - -class DOMException { - constructor(message, code) { - this.msg = message; - this.code = code; - } -} - -core.registerErrorBuilder( - "DOMExceptionOperationError", - function DOMExceptionOperationError(msg) { - return new DOMException(msg, "OperationError"); - }, -); - -try { - ops.op_err(); - throw new Error("op_err didn't throw!"); -} catch (err) { - if (!(err instanceof DOMException)) { - throw new Error("err not DOMException"); - } - if (err.msg !== "abc") { - throw new Error("err.message is incorrect"); - } - if (err.code !== "OperationError") { - throw new Error("err.code is incorrect"); - } -} diff --git a/core/icudtl.dat b/core/icudtl.dat deleted file mode 100644 index d1f10917a..000000000 Binary files a/core/icudtl.dat and /dev/null differ diff --git a/core/lib.rs b/core/lib.rs index 336d9c2b9..82cd1dd43 100644 --- a/core/lib.rs +++ b/core/lib.rs @@ -1,7 +1,6 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. mod async_cancel; mod async_cell; -mod bindings; pub mod error; mod error_codes; mod extensions; @@ -18,10 +17,8 @@ mod ops_builtin; mod ops_builtin_v8; mod ops_metrics; mod path; -mod realm; mod resources; mod runtime; -pub mod snapshot_util; mod source_map; pub mod task; mod task_queue; @@ -57,6 +54,8 @@ pub use crate::async_cell::AsyncRefCell; pub use crate::async_cell::AsyncRefFuture; pub use crate::async_cell::RcLike; pub use crate::async_cell::RcRef; +pub use crate::error::GetErrorClassFn; +pub use crate::error::JsErrorCreateFn; pub use crate::extensions::Extension; pub use crate::extensions::ExtensionBuilder; pub use crate::extensions::ExtensionFileSource; @@ -103,15 +102,13 @@ pub use crate::ops_builtin::op_void_async; pub use crate::ops_builtin::op_void_sync; pub use crate::ops_metrics::OpsTracker; pub use crate::path::strip_unc_prefix; -pub use crate::realm::JsRealm; pub use crate::resources::AsyncResult; pub use crate::resources::Resource; pub use crate::resources::ResourceId; pub use crate::resources::ResourceTable; pub use crate::runtime::CompiledWasmModuleStore; pub use crate::runtime::CrossIsolateStore; -pub use crate::runtime::GetErrorClassFn; -pub use crate::runtime::JsErrorCreateFn; +pub use crate::runtime::JsRealm; pub use crate::runtime::JsRuntime; pub use crate::runtime::JsRuntimeForSnapshot; pub use crate::runtime::RuntimeOptions; @@ -130,21 +127,30 @@ pub fn v8_version() -> &'static str { /// An internal module re-exporting functions used by the #[op] (`deno_ops`) macro #[doc(hidden)] pub mod _ops { - pub use super::bindings::throw_type_error; pub use super::error_codes::get_error_code; pub use super::ops::to_op_result; pub use super::ops::OpCtx; pub use super::ops::OpResult; - pub use super::runtime::map_async_op1; - pub use super::runtime::map_async_op2; - pub use super::runtime::map_async_op3; - pub use super::runtime::map_async_op4; - pub use super::runtime::queue_async_op; - pub use super::runtime::queue_fast_async_op; + pub use super::runtime::ops::map_async_op1; + pub use super::runtime::ops::map_async_op2; + pub use super::runtime::ops::map_async_op3; + pub use super::runtime::ops::map_async_op4; + pub use super::runtime::ops::queue_async_op; + pub use super::runtime::ops::queue_fast_async_op; + pub use super::runtime::throw_type_error; pub use super::runtime::V8_WRAPPER_OBJECT_INDEX; pub use super::runtime::V8_WRAPPER_TYPE_INDEX; } +// TODO(mmastrac): Temporary while we move code around +pub mod snapshot_util { + pub use crate::runtime::create_snapshot; + pub use crate::runtime::get_js_files; + pub use crate::runtime::CreateSnapshotOptions; + pub use crate::runtime::CreateSnapshotOutput; + pub use crate::runtime::FilterFn; +} + /// A helper macro that will return a call site in Rust code. Should be /// used when executing internal one-line scripts for JsRuntime lifecycle. /// diff --git a/core/modules/map.rs b/core/modules/map.rs index 4ab146659..828d5888b 100644 --- a/core/modules/map.rs +++ b/core/modules/map.rs @@ -1,6 +1,4 @@ -use crate::JsRuntime; // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::bindings; use crate::error::generic_error; use crate::fast_string::FastString; use crate::modules::get_asserted_module_type_from_assertions; @@ -20,7 +18,8 @@ use crate::modules::NoopModuleLoader; use crate::modules::PrepareLoadFuture; use crate::modules::RecursiveModuleLoad; use crate::modules::ResolutionKind; -use crate::snapshot_util::SnapshottedData; +use crate::runtime::JsRuntime; +use crate::runtime::SnapshottedData; use anyhow::Error; use futures::future::FutureExt; use futures::stream::FuturesUnordered; @@ -467,7 +466,7 @@ impl ModuleMap { let name_str = name.v8(scope); let source_str = source.v8(scope); - let origin = bindings::module_origin(scope, name_str); + let origin = module_origin(scope, name_str); let source = v8::script_compiler::Source::new(source_str, Some(&origin)); let tc_scope = &mut v8::TryCatch::new(scope); @@ -820,3 +819,22 @@ fn json_module_evaluation_steps<'a>( resolver.resolve(tc_scope, undefined.into()); Some(resolver.get_promise(tc_scope).into()) } + +pub fn module_origin<'a>( + s: &mut v8::HandleScope<'a>, + resource_name: v8::Local<'a, v8::String>, +) -> v8::ScriptOrigin<'a> { + let source_map_url = v8::String::empty(s); + v8::ScriptOrigin::new( + s, + resource_name.into(), + 0, + 0, + false, + 123, + source_map_url.into(), + true, + false, + true, + ) +} diff --git a/core/modules/tests.rs b/core/modules/tests.rs index d32d7244e..0eb7ce514 100644 --- a/core/modules/tests.rs +++ b/core/modules/tests.rs @@ -1,8 +1,8 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::ascii_str; use crate::resolve_import; -use crate::JsRuntime; -use crate::JsRuntimeForSnapshot; +use crate::runtime::JsRuntime; +use crate::runtime::JsRuntimeForSnapshot; use crate::RuntimeOptions; use crate::Snapshot; use deno_ops::op; diff --git a/core/ops.rs b/core/ops.rs index b766eb60d..372ffe5b2 100644 --- a/core/ops.rs +++ b/core/ops.rs @@ -1,10 +1,10 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. use crate::error::AnyError; +use crate::error::GetErrorClassFn; use crate::gotham_state::GothamState; -use crate::realm::ContextState; use crate::resources::ResourceTable; -use crate::runtime::GetErrorClassFn; +use crate::runtime::ContextState; use crate::runtime::JsRuntimeState; use crate::OpDecl; use crate::OpsTracker; diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs index 8416546cb..9fd906291 100644 --- a/core/ops_builtin_v8.rs +++ b/core/ops_builtin_v8.rs @@ -1,5 +1,4 @@ // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -use crate::bindings::script_origin; use crate::error::custom_error; use crate::error::is_instance_of_error; use crate::error::range_error; @@ -7,6 +6,7 @@ use crate::error::type_error; use crate::error::JsError; use crate::ops_builtin::WasmStreamingResource; use crate::resolve_url; +use crate::runtime::script_origin; use crate::serde_v8::from_v8; use crate::source_map::apply_source_map; use crate::JsRealm; diff --git a/core/realm.rs b/core/realm.rs deleted file mode 100644 index d18f41e66..000000000 --- a/core/realm.rs +++ /dev/null @@ -1,368 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use crate::bindings; -use crate::modules::ModuleCode; -use crate::ops::OpCtx; -use crate::runtime::exception_to_err_result; -use crate::runtime::JsRuntimeState; -use crate::task::MaskResultAsSend; -use crate::JsRuntime; -use crate::OpId; -use crate::OpResult; -use crate::PromiseId; -use anyhow::Error; -use std::cell::RefCell; -use std::collections::HashSet; -use std::collections::VecDeque; -use std::hash::BuildHasherDefault; -use std::hash::Hasher; -use std::option::Option; -use std::rc::Rc; -use tokio::task::JoinSet; -use v8::HandleScope; -use v8::Local; - -// Hasher used for `unrefed_ops`. Since these are rolling i32, there's no -// need to actually hash them. -#[derive(Default)] -pub(crate) struct IdentityHasher(u64); - -impl Hasher for IdentityHasher { - fn write_i32(&mut self, i: i32) { - self.0 = i as u64; - } - - fn finish(&self) -> u64 { - self.0 - } - - fn write(&mut self, _bytes: &[u8]) { - unreachable!() - } -} - -#[derive(Default)] -pub(crate) struct ContextState { - pub(crate) js_event_loop_tick_cb: Option>>, - pub(crate) js_build_custom_error_cb: Option>>, - pub(crate) js_promise_reject_cb: Option>>, - pub(crate) js_format_exception_cb: Option>>, - pub(crate) js_wasm_streaming_cb: Option>>, - pub(crate) pending_promise_rejections: - VecDeque<(v8::Global, v8::Global)>, - pub(crate) unrefed_ops: HashSet>, - pub(crate) pending_ops: - JoinSet>, - // We don't explicitly re-read this prop but need the slice to live alongside - // the context - pub(crate) op_ctxs: Box<[OpCtx]>, - pub(crate) isolate: Option<*mut v8::OwnedIsolate>, -} - -/// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows -/// execution in the realm's context. -/// -/// A [`JsRealm`] instance is a reference to an already existing realm, which -/// does not hold ownership of it, so instances can be created and dropped as -/// needed. As such, calling [`JsRealm::new`] doesn't create a new realm, and -/// cloning a [`JsRealm`] only creates a new reference. See -/// [`JsRuntime::create_realm`] to create new realms instead. -/// -/// Despite [`JsRealm`] instances being references, multiple instances that -/// point to the same realm won't overlap because every operation requires -/// passing a mutable reference to the [`v8::Isolate`]. Therefore, no operation -/// on two [`JsRealm`] instances tied to the same isolate can be run at the same -/// time, regardless of whether they point to the same realm. -/// -/// # Panics -/// -/// Every method of [`JsRealm`] will panic if you call it with a reference to a -/// [`v8::Isolate`] other than the one that corresponds to the current context. -/// -/// In other words, the [`v8::Isolate`] parameter for all the related [`JsRealm`] methods -/// must be extracted from the pre-existing [`JsRuntime`]. -/// -/// Example usage with the [`JsRealm::execute_script`] method: -/// ``` -/// use deno_core::JsRuntime; -/// use deno_core::RuntimeOptions; -/// -/// let mut runtime = JsRuntime::new(RuntimeOptions::default()); -/// let new_realm = runtime -/// .create_realm() -/// .expect("Handle the error properly"); -/// let source_code = "var a = 0; a + 1"; -/// let result = new_realm -/// .execute_script_static(runtime.v8_isolate(), "", source_code) -/// .expect("Handle the error properly"); -/// # drop(result); -/// ``` -/// -/// # Lifetime of the realm -/// -/// As long as the corresponding isolate is alive, a [`JsRealm`] instance will -/// keep the underlying V8 context alive even if it would have otherwise been -/// garbage collected. -#[derive(Clone)] -#[repr(transparent)] -pub struct JsRealm(pub(crate) JsRealmInner); - -#[derive(Clone)] -pub(crate) struct JsRealmInner { - context_state: Rc>, - context: Rc>, - runtime_state: Rc>, - is_global: bool, -} - -impl JsRealmInner { - pub(crate) fn new( - context_state: Rc>, - context: v8::Global, - runtime_state: Rc>, - is_global: bool, - ) -> Self { - Self { - context_state, - context: context.into(), - runtime_state, - is_global, - } - } - - pub fn num_pending_ops(&self) -> usize { - self.context_state.borrow().pending_ops.len() - } - - pub fn num_unrefed_ops(&self) -> usize { - self.context_state.borrow().unrefed_ops.len() - } - - #[inline(always)] - pub fn context(&self) -> &v8::Global { - &self.context - } - - #[inline(always)] - pub(crate) fn state(&self) -> Rc> { - self.context_state.clone() - } - - /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. - #[inline(always)] - pub fn handle_scope<'s>( - &self, - isolate: &'s mut v8::Isolate, - ) -> v8::HandleScope<'s> { - v8::HandleScope::with_context(isolate, &*self.context) - } - - pub(crate) fn check_promise_rejections( - &self, - scope: &mut v8::HandleScope, - ) -> Result<(), Error> { - let Some((_, handle)) = self.context_state.borrow_mut().pending_promise_rejections.pop_front() else { - return Ok(()); - }; - - let exception = v8::Local::new(scope, handle); - let state_rc = JsRuntime::state_from(scope); - let state = state_rc.borrow(); - if let Some(inspector) = &state.inspector { - let inspector = inspector.borrow(); - inspector.exception_thrown(scope, exception, true); - if inspector.has_blocking_sessions() { - return Ok(()); - } - } - exception_to_err_result(scope, exception, true) - } - - pub(crate) fn is_same(&self, other: &Rc>) -> bool { - Rc::ptr_eq(&self.context, other) - } - - pub fn destroy(self) { - let state = self.state(); - let raw_ptr = self.state().borrow().isolate.unwrap(); - // SAFETY: We know the isolate outlives the realm - let isolate = unsafe { raw_ptr.as_mut().unwrap() }; - let mut realm_state = state.borrow_mut(); - // These globals will prevent snapshots from completing, take them - std::mem::take(&mut realm_state.js_event_loop_tick_cb); - std::mem::take(&mut realm_state.js_build_custom_error_cb); - std::mem::take(&mut realm_state.js_promise_reject_cb); - std::mem::take(&mut realm_state.js_format_exception_cb); - std::mem::take(&mut realm_state.js_wasm_streaming_cb); - // The OpCtx slice may contain a circular reference - std::mem::take(&mut realm_state.op_ctxs); - - self.context().open(isolate).clear_all_slots(isolate); - - // Expect that this context is dead (we only check this in debug mode) - // TODO(mmastrac): This check fails for some tests, will need to fix this - // debug_assert_eq!(Rc::strong_count(&self.context), 1, "Realm was still alive when we wanted to destory it. Not dropped?"); - } -} - -impl JsRealm { - pub(crate) fn new(inner: JsRealmInner) -> Self { - Self(inner) - } - - #[inline(always)] - pub(crate) fn state_from_scope( - scope: &mut v8::HandleScope, - ) -> Rc> { - let context = scope.get_current_context(); - context - .get_slot::>>(scope) - .unwrap() - .clone() - } - - #[inline(always)] - pub fn num_pending_ops(&self) -> usize { - self.0.num_pending_ops() - } - - #[inline(always)] - pub fn num_unrefed_ops(&self) -> usize { - self.0.num_unrefed_ops() - } - - /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. - #[inline(always)] - pub fn handle_scope<'s>( - &self, - isolate: &'s mut v8::Isolate, - ) -> v8::HandleScope<'s> { - self.0.handle_scope(isolate) - } - - #[inline(always)] - pub fn context(&self) -> &v8::Global { - self.0.context() - } - - /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. - pub fn global_object<'s>( - &self, - isolate: &'s mut v8::Isolate, - ) -> v8::Local<'s, v8::Object> { - let scope = &mut self.0.handle_scope(isolate); - self.0.context.open(scope).global(scope) - } - - fn string_from_code<'a>( - scope: &mut HandleScope<'a>, - code: &ModuleCode, - ) -> Option> { - if let Some(code) = code.try_static_ascii() { - v8::String::new_external_onebyte_static(scope, code) - } else { - v8::String::new_from_utf8( - scope, - code.as_bytes(), - v8::NewStringType::Normal, - ) - } - } - - /// Executes traditional JavaScript code (traditional = not ES modules) in the - /// realm's context. - /// - /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. - /// - /// The `name` parameter can be a filepath or any other string. E.g.: - /// - /// - "/some/file/path.js" - /// - "" - /// - "[native code]" - /// - /// The same `name` value can be used for multiple executions. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn execute_script_static( - &self, - isolate: &mut v8::Isolate, - name: &'static str, - source_code: &'static str, - ) -> Result, Error> { - self.execute_script(isolate, name, ModuleCode::from_static(source_code)) - } - - /// Executes traditional JavaScript code (traditional = not ES modules) in the - /// realm's context. - /// - /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. - /// - /// The `name` parameter can be a filepath or any other string. E.g.: - /// - /// - "/some/file/path.js" - /// - "" - /// - "[native code]" - /// - /// The same `name` value can be used for multiple executions. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn execute_script( - &self, - isolate: &mut v8::Isolate, - name: &'static str, - source_code: ModuleCode, - ) -> Result, Error> { - let scope = &mut self.0.handle_scope(isolate); - - let source = Self::string_from_code(scope, &source_code).unwrap(); - debug_assert!(name.is_ascii()); - let name = - v8::String::new_external_onebyte_static(scope, name.as_bytes()).unwrap(); - let origin = bindings::script_origin(scope, name); - - let tc_scope = &mut v8::TryCatch::new(scope); - - let script = match v8::Script::compile(tc_scope, source, Some(&origin)) { - Some(script) => script, - None => { - let exception = tc_scope.exception().unwrap(); - return exception_to_err_result(tc_scope, exception, false); - } - }; - - match script.run(tc_scope) { - Some(value) => { - let value_handle = v8::Global::new(tc_scope, value); - Ok(value_handle) - } - None => { - assert!(tc_scope.has_caught()); - let exception = tc_scope.exception().unwrap(); - exception_to_err_result(tc_scope, exception, false) - } - } - } - - // TODO(andreubotella): `mod_evaluate`, `load_main_module`, `load_side_module` -} - -impl Drop for JsRealm { - fn drop(&mut self) { - // Don't do anything special with the global realm - if self.0.is_global { - return; - } - - // There's us and there's the runtime - if Rc::strong_count(&self.0.context) == 2 { - self - .0 - .runtime_state - .borrow_mut() - .remove_realm(&self.0.context); - assert_eq!(Rc::strong_count(&self.0.context), 1); - self.0.clone().destroy(); - assert_eq!(Rc::strong_count(&self.0.context_state), 1); - } - } -} diff --git a/core/runtime.rs b/core/runtime.rs deleted file mode 100644 index ecfd0bd57..000000000 --- a/core/runtime.rs +++ /dev/null @@ -1,4934 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use crate::bindings; -use crate::error::generic_error; -use crate::error::to_v8_type_error; -use crate::error::JsError; -use crate::extensions::OpDecl; -use crate::extensions::OpEventLoopFn; -use crate::inspector::JsRuntimeInspector; -use crate::module_specifier::ModuleSpecifier; -use crate::modules::AssertedModuleType; -use crate::modules::ExtModuleLoader; -use crate::modules::ExtModuleLoaderCb; -use crate::modules::ModuleCode; -use crate::modules::ModuleError; -use crate::modules::ModuleId; -use crate::modules::ModuleLoadId; -use crate::modules::ModuleLoader; -use crate::modules::ModuleMap; -use crate::modules::ModuleName; -use crate::ops::*; -use crate::realm::ContextState; -use crate::realm::JsRealm; -use crate::realm::JsRealmInner; -use crate::snapshot_util; -use crate::source_map::SourceMapCache; -use crate::source_map::SourceMapGetter; -use crate::Extension; -use crate::ModuleType; -use crate::NoopModuleLoader; -use crate::OpMiddlewareFn; -use crate::OpResult; -use crate::OpState; -use crate::PromiseId; -use anyhow::Context as AnyhowContext; -use anyhow::Error; -use futures::channel::oneshot; -use futures::future::poll_fn; -use futures::future::Future; -use futures::future::FutureExt; -use futures::future::MaybeDone; -use futures::stream::StreamExt; -use futures::task::noop_waker; -use smallvec::SmallVec; -use std::any::Any; -use std::cell::RefCell; -use std::collections::HashMap; -use std::ffi::c_void; -use std::mem::ManuallyDrop; -use std::ops::Deref; -use std::ops::DerefMut; -use std::option::Option; -use std::pin::Pin; -use std::rc::Rc; -use std::sync::atomic::AtomicBool; -use std::sync::atomic::Ordering; -use std::sync::Arc; -use std::sync::Mutex; -use std::sync::Once; -use std::task::Context; -use std::task::Poll; - -const STATE_DATA_OFFSET: u32 = 0; -const MODULE_MAP_DATA_OFFSET: u32 = 1; - -pub enum Snapshot { - Static(&'static [u8]), - JustCreated(v8::StartupData), - Boxed(Box<[u8]>), -} - -pub type JsErrorCreateFn = dyn Fn(JsError) -> Error; - -pub type GetErrorClassFn = &'static dyn for<'e> Fn(&'e Error) -> &'static str; - -/// Objects that need to live as long as the isolate -#[derive(Default)] -struct IsolateAllocations { - near_heap_limit_callback_data: - Option<(Box>, v8::NearHeapLimitCallback)>, -} - -/// ManuallyDrop> is clone, but it returns a ManuallyDrop> which is a massive -/// memory-leak footgun. -struct ManuallyDropRc(ManuallyDrop>); - -impl ManuallyDropRc { - pub fn clone(&self) -> Rc { - self.0.deref().clone() - } -} - -impl Deref for ManuallyDropRc { - type Target = Rc; - fn deref(&self) -> &Self::Target { - self.0.deref() - } -} - -impl DerefMut for ManuallyDropRc { - fn deref_mut(&mut self) -> &mut Self::Target { - self.0.deref_mut() - } -} - -/// This struct contains the [`JsRuntimeState`] and [`v8::OwnedIsolate`] that are required -/// to do an orderly shutdown of V8. We keep these in a separate struct to allow us to control -/// the destruction more closely, as snapshots require the isolate to be destroyed by the -/// snapshot process, not the destructor. -/// -/// The way rusty_v8 works w/snapshots is that the [`v8::OwnedIsolate`] gets consumed by a -/// [`v8::snapshot::SnapshotCreator`] that is stored in its annex. It's a bit awkward, because this -/// means we cannot let it drop (because we don't have it after a snapshot). On top of that, we have -/// to consume it in the snapshot creator because otherwise it panics. -/// -/// This inner struct allows us to let the outer JsRuntime drop normally without a Drop impl, while we -/// control dropping more closely here using ManuallyDrop. -struct InnerIsolateState { - will_snapshot: bool, - state: ManuallyDropRc>, - v8_isolate: ManuallyDrop, -} - -impl InnerIsolateState { - /// Clean out the opstate and take the inspector to prevent the inspector from getting destroyed - /// after we've torn down the contexts. If the inspector is not correctly torn down, random crashes - /// happen in tests (and possibly for users using the inspector). - pub fn prepare_for_cleanup(&mut self) { - let mut state = self.state.borrow_mut(); - let inspector = state.inspector.take(); - state.op_state.borrow_mut().clear(); - if let Some(inspector) = inspector { - assert_eq!( - Rc::strong_count(&inspector), - 1, - "The inspector must be dropped before the runtime" - ); - } - } - - pub fn cleanup(&mut self) { - self.prepare_for_cleanup(); - - let state_ptr = self.v8_isolate.get_data(STATE_DATA_OFFSET); - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - _ = unsafe { Rc::from_raw(state_ptr as *const RefCell) }; - - let module_map_ptr = self.v8_isolate.get_data(MODULE_MAP_DATA_OFFSET); - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - _ = unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; - - self.state.borrow_mut().destroy_all_realms(); - - debug_assert_eq!(Rc::strong_count(&self.state), 1); - } - - pub fn prepare_for_snapshot(mut self) -> v8::OwnedIsolate { - self.cleanup(); - // SAFETY: We're copying out of self and then immediately forgetting self - let (state, isolate) = unsafe { - ( - ManuallyDrop::take(&mut self.state.0), - ManuallyDrop::take(&mut self.v8_isolate), - ) - }; - std::mem::forget(self); - drop(state); - isolate - } -} - -impl Drop for InnerIsolateState { - fn drop(&mut self) { - self.cleanup(); - // SAFETY: We gotta drop these - unsafe { - ManuallyDrop::drop(&mut self.state.0); - if self.will_snapshot { - // Create the snapshot and just drop it. - eprintln!("WARNING: v8::OwnedIsolate for snapshot was leaked"); - } else { - ManuallyDrop::drop(&mut self.v8_isolate); - } - } - } -} - -#[derive(Copy, Clone, Debug, Eq, PartialEq)] -pub(crate) enum InitMode { - /// We have no snapshot -- this is a pristine context. - New, - /// We are using a snapshot, thus certain initialization steps are skipped. - FromSnapshot, -} - -impl InitMode { - fn from_options(options: &RuntimeOptions) -> Self { - match options.startup_snapshot { - None => Self::New, - Some(_) => Self::FromSnapshot, - } - } -} - -/// A single execution context of JavaScript. Corresponds roughly to the "Web -/// Worker" concept in the DOM. -//// -/// The JsRuntime future completes when there is an error or when all -/// pending ops have completed. -/// -/// Use [`JsRuntimeForSnapshot`] to be able to create a snapshot. -pub struct JsRuntime { - inner: InnerIsolateState, - module_map: Rc>, - allocations: IsolateAllocations, - extensions: Vec, - event_loop_middlewares: Vec>, - init_mode: InitMode, - // Marks if this is considered the top-level runtime. Used only be inspector. - is_main: bool, -} - -/// The runtime type used for snapshot creation. -pub struct JsRuntimeForSnapshot(JsRuntime); - -impl Deref for JsRuntimeForSnapshot { - type Target = JsRuntime; - - fn deref(&self) -> &Self::Target { - &self.0 - } -} - -impl DerefMut for JsRuntimeForSnapshot { - fn deref_mut(&mut self) -> &mut Self::Target { - &mut self.0 - } -} - -pub(crate) struct DynImportModEvaluate { - load_id: ModuleLoadId, - module_id: ModuleId, - promise: v8::Global, - module: v8::Global, -} - -pub(crate) struct ModEvaluate { - pub(crate) promise: Option>, - pub(crate) has_evaluated: bool, - pub(crate) handled_promise_rejections: Vec>, - sender: oneshot::Sender>, -} - -pub struct CrossIsolateStore(Arc>>); - -struct CrossIsolateStoreInner { - map: HashMap, - last_id: u32, -} - -impl CrossIsolateStore { - pub(crate) fn insert(&self, value: T) -> u32 { - let mut store = self.0.lock().unwrap(); - let last_id = store.last_id; - store.map.insert(last_id, value); - store.last_id += 1; - last_id - } - - pub(crate) fn take(&self, id: u32) -> Option { - let mut store = self.0.lock().unwrap(); - store.map.remove(&id) - } -} - -impl Default for CrossIsolateStore { - fn default() -> Self { - CrossIsolateStore(Arc::new(Mutex::new(CrossIsolateStoreInner { - map: Default::default(), - last_id: 0, - }))) - } -} - -impl Clone for CrossIsolateStore { - fn clone(&self) -> Self { - Self(self.0.clone()) - } -} - -pub type SharedArrayBufferStore = - CrossIsolateStore>; - -pub type CompiledWasmModuleStore = CrossIsolateStore; - -/// Internal state for JsRuntime which is stored in one of v8::Isolate's -/// embedder slots. -pub struct JsRuntimeState { - global_realm: Option, - known_realms: Vec, - pub(crate) has_tick_scheduled: bool, - pub(crate) pending_dyn_mod_evaluate: Vec, - pub(crate) pending_mod_evaluate: Option, - /// A counter used to delay our dynamic import deadlock detection by one spin - /// of the event loop. - dyn_module_evaluate_idle_counter: u32, - pub(crate) source_map_getter: Option>>, - pub(crate) source_map_cache: Rc>, - pub(crate) op_state: Rc>, - pub(crate) shared_array_buffer_store: Option, - pub(crate) compiled_wasm_module_store: Option, - /// The error that was passed to an `op_dispatch_exception` call. - /// It will be retrieved by `exception_to_err_result` and used as an error - /// instead of any other exceptions. - // TODO(nayeemrmn): This is polled in `exception_to_err_result()` which is - // flimsy. Try to poll it similarly to `pending_promise_rejections`. - pub(crate) dispatched_exception: Option>, - pub(crate) inspector: Option>>, -} - -impl JsRuntimeState { - pub(crate) fn destroy_all_realms(&mut self) { - self.global_realm.take(); - for realm in self.known_realms.drain(..) { - realm.destroy() - } - } - - pub(crate) fn remove_realm( - &mut self, - realm_context: &Rc>, - ) { - self - .known_realms - .retain(|realm| !realm.is_same(realm_context)); - } -} - -fn v8_init( - v8_platform: Option>, - predictable: bool, -) { - // Include 10MB ICU data file. - #[repr(C, align(16))] - struct IcuData([u8; 10541264]); - static ICU_DATA: IcuData = IcuData(*include_bytes!("icudtl.dat")); - v8::icu::set_common_data_72(&ICU_DATA.0).unwrap(); - - let flags = concat!( - " --wasm-test-streaming", - " --harmony-import-assertions", - " --no-validate-asm", - " --turbo_fast_api_calls", - " --harmony-change-array-by-copy", - ); - - if predictable { - v8::V8::set_flags_from_string(&format!( - "{}{}", - flags, " --predictable --random-seed=42" - )); - } else { - v8::V8::set_flags_from_string(flags); - } - - let v8_platform = v8_platform - .unwrap_or_else(|| v8::new_default_platform(0, false).make_shared()); - v8::V8::initialize_platform(v8_platform); - v8::V8::initialize(); -} - -pub const V8_WRAPPER_TYPE_INDEX: i32 = 0; -pub const V8_WRAPPER_OBJECT_INDEX: i32 = 1; - -#[derive(Default)] -pub struct RuntimeOptions { - /// Source map reference for errors. - pub source_map_getter: Option>, - - /// Allows to map error type to a string "class" used to represent - /// error in JavaScript. - pub get_error_class_fn: Option, - - /// Implementation of `ModuleLoader` which will be - /// called when V8 requests to load ES modules. - /// - /// If not provided runtime will error if code being - /// executed tries to load modules. - pub module_loader: Option>, - - /// JsRuntime extensions, not to be confused with ES modules. - /// Only ops registered by extensions will be initialized. If you need - /// to execute JS code from extensions, pass source files in `js` or `esm` - /// option on `ExtensionBuilder`. - /// - /// If you are creating a runtime from a snapshot take care not to include - /// JavaScript sources in the extensions. - pub extensions: Vec, - - /// V8 snapshot that should be loaded on startup. - pub startup_snapshot: Option, - - /// Isolate creation parameters. - pub create_params: Option, - - /// V8 platform instance to use. Used when Deno initializes V8 - /// (which it only does once), otherwise it's silenty dropped. - pub v8_platform: Option>, - - /// The store to use for transferring SharedArrayBuffers between isolates. - /// If multiple isolates should have the possibility of sharing - /// SharedArrayBuffers, they should use the same [SharedArrayBufferStore]. If - /// no [SharedArrayBufferStore] is specified, SharedArrayBuffer can not be - /// serialized. - pub shared_array_buffer_store: Option, - - /// The store to use for transferring `WebAssembly.Module` objects between - /// isolates. - /// If multiple isolates should have the possibility of sharing - /// `WebAssembly.Module` objects, they should use the same - /// [CompiledWasmModuleStore]. If no [CompiledWasmModuleStore] is specified, - /// `WebAssembly.Module` objects cannot be serialized. - pub compiled_wasm_module_store: Option, - - /// Start inspector instance to allow debuggers to connect. - pub inspector: bool, - - /// Describe if this is the main runtime instance, used by debuggers in some - /// situation - like disconnecting when program finishes running. - pub is_main: bool, -} - -#[derive(Default)] -pub struct RuntimeSnapshotOptions { - /// An optional callback that will be called for each module that is loaded - /// during snapshotting. This callback can be used to transpile source on the - /// fly, during snapshotting, eg. to transpile TypeScript to JavaScript. - pub snapshot_module_load_cb: Option, -} - -impl JsRuntime { - /// Only constructor, configuration is done through `options`. - pub fn new(mut options: RuntimeOptions) -> JsRuntime { - JsRuntime::init_v8(options.v8_platform.take(), cfg!(test)); - JsRuntime::new_inner(options, false, None) - } - - pub(crate) fn state_from( - isolate: &v8::Isolate, - ) -> Rc> { - let state_ptr = isolate.get_data(STATE_DATA_OFFSET); - let state_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(state_ptr as *const RefCell) }; - let state = state_rc.clone(); - std::mem::forget(state_rc); - state - } - - pub(crate) fn module_map_from( - isolate: &v8::Isolate, - ) -> Rc> { - let module_map_ptr = isolate.get_data(MODULE_MAP_DATA_OFFSET); - let module_map_rc = - // SAFETY: We are sure that it's a valid pointer for whole lifetime of - // the runtime. - unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; - let module_map = module_map_rc.clone(); - std::mem::forget(module_map_rc); - module_map - } - - pub(crate) fn event_loop_pending_state_from_scope( - scope: &mut v8::HandleScope, - ) -> EventLoopPendingState { - let state = JsRuntime::state_from(scope); - let module_map = JsRuntime::module_map_from(scope); - let state = EventLoopPendingState::new( - scope, - &mut state.borrow_mut(), - &module_map.borrow(), - ); - state - } - - fn init_v8( - v8_platform: Option>, - predictable: bool, - ) { - static DENO_INIT: Once = Once::new(); - static DENO_PREDICTABLE: AtomicBool = AtomicBool::new(false); - static DENO_PREDICTABLE_SET: AtomicBool = AtomicBool::new(false); - - if DENO_PREDICTABLE_SET.load(Ordering::SeqCst) { - let current = DENO_PREDICTABLE.load(Ordering::SeqCst); - assert_eq!(current, predictable, "V8 may only be initialized once in either snapshotting or non-snapshotting mode. Either snapshotting or non-snapshotting mode may be used in a single process, not both."); - DENO_PREDICTABLE_SET.store(true, Ordering::SeqCst); - DENO_PREDICTABLE.store(predictable, Ordering::SeqCst); - } - - DENO_INIT.call_once(move || v8_init(v8_platform, predictable)); - } - - fn new_inner( - mut options: RuntimeOptions, - will_snapshot: bool, - maybe_load_callback: Option, - ) -> JsRuntime { - let init_mode = InitMode::from_options(&options); - let (op_state, ops) = Self::create_opstate(&mut options, init_mode); - let op_state = Rc::new(RefCell::new(op_state)); - - // Collect event-loop middleware - let mut event_loop_middlewares = - Vec::with_capacity(options.extensions.len()); - for extension in &mut options.extensions { - if let Some(middleware) = extension.init_event_loop_middleware() { - event_loop_middlewares.push(middleware); - } - } - - let align = std::mem::align_of::(); - let layout = std::alloc::Layout::from_size_align( - std::mem::size_of::<*mut v8::OwnedIsolate>(), - align, - ) - .unwrap(); - assert!(layout.size() > 0); - let isolate_ptr: *mut v8::OwnedIsolate = - // SAFETY: we just asserted that layout has non-0 size. - unsafe { std::alloc::alloc(layout) as *mut _ }; - - let state_rc = Rc::new(RefCell::new(JsRuntimeState { - pending_dyn_mod_evaluate: vec![], - pending_mod_evaluate: None, - dyn_module_evaluate_idle_counter: 0, - has_tick_scheduled: false, - source_map_getter: options.source_map_getter.map(Rc::new), - source_map_cache: Default::default(), - shared_array_buffer_store: options.shared_array_buffer_store, - compiled_wasm_module_store: options.compiled_wasm_module_store, - op_state: op_state.clone(), - dispatched_exception: None, - // Some fields are initialized later after isolate is created - inspector: None, - global_realm: None, - known_realms: Vec::with_capacity(1), - })); - - let weak = Rc::downgrade(&state_rc); - let context_state = Rc::new(RefCell::new(ContextState::default())); - let op_ctxs = ops - .into_iter() - .enumerate() - .map(|(id, decl)| { - OpCtx::new( - id as u16, - context_state.clone(), - Rc::new(decl), - op_state.clone(), - weak.clone(), - ) - }) - .collect::>() - .into_boxed_slice(); - context_state.borrow_mut().op_ctxs = op_ctxs; - context_state.borrow_mut().isolate = Some(isolate_ptr); - - let refs = bindings::external_references(&context_state.borrow().op_ctxs); - // V8 takes ownership of external_references. - let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs)); - - let mut isolate = if will_snapshot { - snapshot_util::create_snapshot_creator( - refs, - options.startup_snapshot.take(), - ) - } else { - let mut params = options - .create_params - .take() - .unwrap_or_default() - .embedder_wrapper_type_info_offsets( - V8_WRAPPER_TYPE_INDEX, - V8_WRAPPER_OBJECT_INDEX, - ) - .external_references(&**refs); - if let Some(snapshot) = options.startup_snapshot.take() { - params = match snapshot { - Snapshot::Static(data) => params.snapshot_blob(data), - Snapshot::JustCreated(data) => params.snapshot_blob(data), - Snapshot::Boxed(data) => params.snapshot_blob(data), - }; - } - v8::Isolate::new(params) - }; - isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10); - isolate.set_promise_reject_callback(bindings::promise_reject_callback); - isolate.set_host_initialize_import_meta_object_callback( - bindings::host_initialize_import_meta_object_callback, - ); - isolate.set_host_import_module_dynamically_callback( - bindings::host_import_module_dynamically_callback, - ); - isolate.set_wasm_async_resolve_promise_callback( - bindings::wasm_async_resolve_promise_callback, - ); - - let (global_context, snapshotted_data) = { - let scope = &mut v8::HandleScope::new(&mut isolate); - let context = v8::Context::new(scope); - - // Get module map data from the snapshot - let snapshotted_data = if init_mode == InitMode::FromSnapshot { - Some(snapshot_util::get_snapshotted_data(scope, context)) - } else { - None - }; - - (v8::Global::new(scope, context), snapshotted_data) - }; - - // SAFETY: this is first use of `isolate_ptr` so we are sure we're - // not overwriting an existing pointer. - isolate = unsafe { - isolate_ptr.write(isolate); - isolate_ptr.read() - }; - - let mut context_scope: v8::HandleScope = - v8::HandleScope::with_context(&mut isolate, global_context.clone()); - let scope = &mut context_scope; - let context = v8::Local::new(scope, global_context.clone()); - - bindings::initialize_context( - scope, - context, - &context_state.borrow().op_ctxs, - init_mode, - ); - - context.set_slot(scope, context_state.clone()); - - op_state.borrow_mut().put(isolate_ptr); - let inspector = if options.inspector { - Some(JsRuntimeInspector::new(scope, context, options.is_main)) - } else { - None - }; - - let loader = options - .module_loader - .unwrap_or_else(|| Rc::new(NoopModuleLoader)); - - { - let global_realm = JsRealmInner::new( - context_state, - global_context, - state_rc.clone(), - true, - ); - let mut state = state_rc.borrow_mut(); - state.global_realm = Some(JsRealm::new(global_realm.clone())); - state.inspector = inspector; - state.known_realms.push(global_realm); - } - scope.set_data( - STATE_DATA_OFFSET, - Rc::into_raw(state_rc.clone()) as *mut c_void, - ); - let module_map_rc = Rc::new(RefCell::new(ModuleMap::new(loader))); - if let Some(snapshotted_data) = snapshotted_data { - let mut module_map = module_map_rc.borrow_mut(); - module_map.update_with_snapshotted_data(scope, snapshotted_data); - } - scope.set_data( - MODULE_MAP_DATA_OFFSET, - Rc::into_raw(module_map_rc.clone()) as *mut c_void, - ); - - drop(context_scope); - - let mut js_runtime = JsRuntime { - inner: InnerIsolateState { - will_snapshot, - state: ManuallyDropRc(ManuallyDrop::new(state_rc)), - v8_isolate: ManuallyDrop::new(isolate), - }, - init_mode, - allocations: IsolateAllocations::default(), - event_loop_middlewares, - extensions: options.extensions, - module_map: module_map_rc, - is_main: options.is_main, - }; - - let realm = js_runtime.global_realm(); - // TODO(mmastrac): We should thread errors back out of the runtime - js_runtime - .init_extension_js(&realm, maybe_load_callback) - .unwrap(); - js_runtime - } - - #[cfg(test)] - #[inline] - pub(crate) fn module_map(&self) -> &Rc> { - &self.module_map - } - - #[inline] - pub fn global_context(&self) -> v8::Global { - self - .inner - .state - .borrow() - .known_realms - .get(0) - .unwrap() - .context() - .clone() - } - - #[inline] - pub fn v8_isolate(&mut self) -> &mut v8::OwnedIsolate { - &mut self.inner.v8_isolate - } - - #[inline] - pub fn inspector(&mut self) -> Rc> { - self.inner.state.borrow().inspector() - } - - #[inline] - pub fn global_realm(&mut self) -> JsRealm { - let state = self.inner.state.borrow(); - state.global_realm.clone().unwrap() - } - - /// Returns the extensions that this runtime is using (including internal ones). - pub fn extensions(&self) -> &Vec { - &self.extensions - } - - /// Creates a new realm (V8 context) in this JS execution context, - /// pre-initialized with all of the extensions that were passed in - /// [`RuntimeOptions::extensions`] when the [`JsRuntime`] was - /// constructed. - pub fn create_realm(&mut self) -> Result { - let realm = { - let context_state = Rc::new(RefCell::new(ContextState::default())); - let op_ctxs: Box<[OpCtx]> = self - .global_realm() - .0 - .state() - .borrow() - .op_ctxs - .iter() - .map(|op_ctx| { - OpCtx::new( - op_ctx.id, - context_state.clone(), - op_ctx.decl.clone(), - op_ctx.state.clone(), - op_ctx.runtime_state.clone(), - ) - }) - .collect(); - context_state.borrow_mut().op_ctxs = op_ctxs; - context_state.borrow_mut().isolate = Some(self.v8_isolate() as _); - - let raw_ptr = self.v8_isolate() as *mut v8::OwnedIsolate; - // SAFETY: Having the scope tied to self's lifetime makes it impossible to - // reference JsRuntimeState::op_ctxs while the scope is alive. Here we - // turn it into an unbound lifetime, which is sound because 1. it only - // lives until the end of this block, and 2. the HandleScope only has - // access to the isolate, and nothing else we're accessing from self does. - let isolate = unsafe { raw_ptr.as_mut() }.unwrap(); - let scope = &mut v8::HandleScope::new(isolate); - let context = v8::Context::new(scope); - let scope = &mut v8::ContextScope::new(scope, context); - - let context = bindings::initialize_context( - scope, - context, - &context_state.borrow().op_ctxs, - self.init_mode, - ); - context.set_slot(scope, context_state.clone()); - let realm = JsRealmInner::new( - context_state, - v8::Global::new(scope, context), - self.inner.state.clone(), - false, - ); - let mut state = self.inner.state.borrow_mut(); - state.known_realms.push(realm.clone()); - JsRealm::new(realm) - }; - - self.init_extension_js(&realm, None)?; - Ok(realm) - } - - #[inline] - pub fn handle_scope(&mut self) -> v8::HandleScope { - self.global_realm().handle_scope(self.v8_isolate()) - } - - /// Initializes JS of provided Extensions in the given realm. - fn init_extension_js( - &mut self, - realm: &JsRealm, - maybe_load_callback: Option, - ) -> Result<(), Error> { - // Initialization of JS happens in phases: - // 1. Iterate through all extensions: - // a. Execute all extension "script" JS files - // b. Load all extension "module" JS files (but do not execute them yet) - // 2. Iterate through all extensions: - // a. If an extension has a `esm_entry_point`, execute it. - - // Take extensions temporarily so we can avoid have a mutable reference to self - let extensions = std::mem::take(&mut self.extensions); - - // TODO(nayeemrmn): Module maps should be per-realm. - let loader = self.module_map.borrow().loader.clone(); - let ext_loader = Rc::new(ExtModuleLoader::new( - &extensions, - maybe_load_callback.map(Rc::new), - )); - self.module_map.borrow_mut().loader = ext_loader; - - let mut esm_entrypoints = vec![]; - - futures::executor::block_on(async { - for extension in &extensions { - let maybe_esm_entry_point = extension.get_esm_entry_point(); - - if let Some(esm_files) = extension.get_esm_sources() { - for file_source in esm_files { - self - .load_side_module( - &ModuleSpecifier::parse(file_source.specifier)?, - None, - ) - .await?; - } - } - - if let Some(entry_point) = maybe_esm_entry_point { - esm_entrypoints.push(entry_point); - } - - if let Some(js_files) = extension.get_js_sources() { - for file_source in js_files { - realm.execute_script( - self.v8_isolate(), - file_source.specifier, - file_source.load()?, - )?; - } - } - - if extension.is_core { - self.init_cbs(realm); - } - } - - for specifier in esm_entrypoints { - let mod_id = { - self - .module_map - .borrow() - .get_id(specifier, AssertedModuleType::JavaScriptOrWasm) - .unwrap_or_else(|| { - panic!("{} not present in the module map", specifier) - }) - }; - let receiver = self.mod_evaluate(mod_id); - self.run_event_loop(false).await?; - receiver - .await? - .with_context(|| format!("Couldn't execute '{specifier}'"))?; - } - - #[cfg(debug_assertions)] - { - let module_map_rc = self.module_map.clone(); - let mut scope = realm.handle_scope(self.v8_isolate()); - let module_map = module_map_rc.borrow(); - module_map.assert_all_modules_evaluated(&mut scope); - } - - Ok::<_, anyhow::Error>(()) - })?; - - self.extensions = extensions; - self.module_map.borrow_mut().loader = loader; - Ok(()) - } - - /// Collects ops from extensions & applies middleware - fn collect_ops(exts: &mut [Extension]) -> Vec { - for (ext, previous_exts) in - exts.iter().enumerate().map(|(i, ext)| (ext, &exts[..i])) - { - ext.check_dependencies(previous_exts); - } - - // Middleware - let middleware: Vec> = exts - .iter_mut() - .filter_map(|e| e.init_middleware()) - .collect(); - - // macroware wraps an opfn in all the middleware - let macroware = move |d| middleware.iter().fold(d, |d, m| m(d)); - - // Flatten ops, apply middlware & override disabled ops - let ops: Vec<_> = exts - .iter_mut() - .filter_map(|e| e.init_ops()) - .flatten() - .map(|d| OpDecl { - name: d.name, - ..macroware(d) - }) - .collect(); - - // In debug build verify there are no duplicate ops. - #[cfg(debug_assertions)] - { - let mut count_by_name = HashMap::new(); - - for op in ops.iter() { - count_by_name - .entry(&op.name) - .or_insert(vec![]) - .push(op.name.to_string()); - } - - let mut duplicate_ops = vec![]; - for (op_name, _count) in - count_by_name.iter().filter(|(_k, v)| v.len() > 1) - { - duplicate_ops.push(op_name.to_string()); - } - if !duplicate_ops.is_empty() { - let mut msg = "Found ops with duplicate names:\n".to_string(); - for op_name in duplicate_ops { - msg.push_str(&format!(" - {}\n", op_name)); - } - msg.push_str("Op names need to be unique."); - panic!("{}", msg); - } - } - - ops - } - - /// Initializes ops of provided Extensions - fn create_opstate( - options: &mut RuntimeOptions, - init_mode: InitMode, - ) -> (OpState, Vec) { - // Add built-in extension - if init_mode == InitMode::FromSnapshot { - options - .extensions - .insert(0, crate::ops_builtin::core::init_ops()); - } else { - options - .extensions - .insert(0, crate::ops_builtin::core::init_ops_and_esm()); - } - - let ops = Self::collect_ops(&mut options.extensions); - - let mut op_state = OpState::new(ops.len()); - - if let Some(get_error_class_fn) = options.get_error_class_fn { - op_state.get_error_class_fn = get_error_class_fn; - } - - // Setup state - for e in &mut options.extensions { - // ops are already registered during in bindings::initialize_context(); - e.init_state(&mut op_state); - } - - (op_state, ops) - } - - pub fn eval<'s, T>( - scope: &mut v8::HandleScope<'s>, - code: &str, - ) -> Option> - where - v8::Local<'s, T>: TryFrom, Error = v8::DataError>, - { - let scope = &mut v8::EscapableHandleScope::new(scope); - let source = v8::String::new(scope, code).unwrap(); - let script = v8::Script::compile(scope, source, None).unwrap(); - let v = script.run(scope)?; - scope.escape(v).try_into().ok() - } - - /// Grabs a reference to core.js' eventLoopTick & buildCustomError - fn init_cbs(&mut self, realm: &JsRealm) { - let (event_loop_tick_cb, build_custom_error_cb) = { - let scope = &mut realm.handle_scope(self.v8_isolate()); - let context = realm.context(); - let context_local = v8::Local::new(scope, context); - let global = context_local.global(scope); - let deno_str = - v8::String::new_external_onebyte_static(scope, b"Deno").unwrap(); - let core_str = - v8::String::new_external_onebyte_static(scope, b"core").unwrap(); - let event_loop_tick_str = - v8::String::new_external_onebyte_static(scope, b"eventLoopTick") - .unwrap(); - let build_custom_error_str = - v8::String::new_external_onebyte_static(scope, b"buildCustomError") - .unwrap(); - - let deno_obj: v8::Local = global - .get(scope, deno_str.into()) - .unwrap() - .try_into() - .unwrap(); - let core_obj: v8::Local = deno_obj - .get(scope, core_str.into()) - .unwrap() - .try_into() - .unwrap(); - - let event_loop_tick_cb: v8::Local = core_obj - .get(scope, event_loop_tick_str.into()) - .unwrap() - .try_into() - .unwrap(); - let build_custom_error_cb: v8::Local = core_obj - .get(scope, build_custom_error_str.into()) - .unwrap() - .try_into() - .unwrap(); - ( - v8::Global::new(scope, event_loop_tick_cb), - v8::Global::new(scope, build_custom_error_cb), - ) - }; - - // Put global handles in the realm's ContextState - let state_rc = realm.0.state(); - let mut state = state_rc.borrow_mut(); - state - .js_event_loop_tick_cb - .replace(Rc::new(event_loop_tick_cb)); - state - .js_build_custom_error_cb - .replace(Rc::new(build_custom_error_cb)); - } - - /// Returns the runtime's op state, which can be used to maintain ops - /// and access resources between op calls. - pub fn op_state(&mut self) -> Rc> { - let state = self.inner.state.borrow(); - state.op_state.clone() - } - - /// Executes traditional JavaScript code (traditional = not ES modules). - /// - /// The execution takes place on the current global context, so it is possible - /// to maintain local JS state and invoke this method multiple times. - /// - /// `name` can be a filepath or any other string, but it is required to be 7-bit ASCII, eg. - /// - /// - "/some/file/path.js" - /// - "" - /// - "[native code]" - /// - /// The same `name` value can be used for multiple executions. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn execute_script( - &mut self, - name: &'static str, - source_code: ModuleCode, - ) -> Result, Error> { - self - .global_realm() - .execute_script(self.v8_isolate(), name, source_code) - } - - /// Executes traditional JavaScript code (traditional = not ES modules). - /// - /// The execution takes place on the current global context, so it is possible - /// to maintain local JS state and invoke this method multiple times. - /// - /// `name` can be a filepath or any other string, but it is required to be 7-bit ASCII, eg. - /// - /// - "/some/file/path.js" - /// - "" - /// - "[native code]" - /// - /// The same `name` value can be used for multiple executions. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn execute_script_static( - &mut self, - name: &'static str, - source_code: &'static str, - ) -> Result, Error> { - self.global_realm().execute_script( - self.v8_isolate(), - name, - ModuleCode::from_static(source_code), - ) - } - - /// Call a function. If it returns a promise, run the event loop until that - /// promise is settled. If the promise rejects or there is an uncaught error - /// in the event loop, return `Err(error)`. Or return `Ok()`. - pub async fn call_and_await( - &mut self, - function: &v8::Global, - ) -> Result, Error> { - let promise = { - let scope = &mut self.handle_scope(); - let cb = function.open(scope); - let this = v8::undefined(scope).into(); - let promise = cb.call(scope, this, &[]); - if promise.is_none() || scope.is_execution_terminating() { - let undefined = v8::undefined(scope).into(); - return exception_to_err_result(scope, undefined, false); - } - v8::Global::new(scope, promise.unwrap()) - }; - self.resolve_value(promise).await - } - - /// Returns the namespace object of a module. - /// - /// This is only available after module evaluation has completed. - /// This function panics if module has not been instantiated. - pub fn get_module_namespace( - &mut self, - module_id: ModuleId, - ) -> Result, Error> { - let module_handle = self - .module_map - .borrow() - .get_handle(module_id) - .expect("ModuleInfo not found"); - - let scope = &mut self.handle_scope(); - - let module = module_handle.open(scope); - - if module.get_status() == v8::ModuleStatus::Errored { - let exception = module.get_exception(); - return exception_to_err_result(scope, exception, false); - } - - assert!(matches!( - module.get_status(), - v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated - )); - - let module_namespace: v8::Local = - v8::Local::try_from(module.get_module_namespace()) - .map_err(|err: v8::DataError| generic_error(err.to_string()))?; - - Ok(v8::Global::new(scope, module_namespace)) - } - - /// Registers a callback on the isolate when the memory limits are approached. - /// Use this to prevent V8 from crashing the process when reaching the limit. - /// - /// Calls the closure with the current heap limit and the initial heap limit. - /// The return value of the closure is set as the new limit. - pub fn add_near_heap_limit_callback(&mut self, cb: C) - where - C: FnMut(usize, usize) -> usize + 'static, - { - let boxed_cb = Box::new(RefCell::new(cb)); - let data = boxed_cb.as_ptr() as *mut c_void; - - let prev = self - .allocations - .near_heap_limit_callback_data - .replace((boxed_cb, near_heap_limit_callback::)); - if let Some((_, prev_cb)) = prev { - self - .v8_isolate() - .remove_near_heap_limit_callback(prev_cb, 0); - } - - self - .v8_isolate() - .add_near_heap_limit_callback(near_heap_limit_callback::, data); - } - - pub fn remove_near_heap_limit_callback(&mut self, heap_limit: usize) { - if let Some((_, cb)) = self.allocations.near_heap_limit_callback_data.take() - { - self - .v8_isolate() - .remove_near_heap_limit_callback(cb, heap_limit); - } - } - - fn pump_v8_message_loop(&mut self) -> Result<(), Error> { - let scope = &mut self.handle_scope(); - while v8::Platform::pump_message_loop( - &v8::V8::get_current_platform(), - scope, - false, // don't block if there are no tasks - ) { - // do nothing - } - - let tc_scope = &mut v8::TryCatch::new(scope); - tc_scope.perform_microtask_checkpoint(); - match tc_scope.exception() { - None => Ok(()), - Some(exception) => exception_to_err_result(tc_scope, exception, false), - } - } - - pub fn maybe_init_inspector(&mut self) { - if self.inner.state.borrow().inspector.is_some() { - return; - } - - let context = self.global_context(); - let scope = &mut v8::HandleScope::with_context( - self.inner.v8_isolate.as_mut(), - context.clone(), - ); - let context = v8::Local::new(scope, context); - - let mut state = self.inner.state.borrow_mut(); - state.inspector = - Some(JsRuntimeInspector::new(scope, context, self.is_main)); - } - - pub fn poll_value( - &mut self, - global: &v8::Global, - cx: &mut Context, - ) -> Poll, Error>> { - let state = self.poll_event_loop(cx, false); - - let mut scope = self.handle_scope(); - let local = v8::Local::::new(&mut scope, global); - - if let Ok(promise) = v8::Local::::try_from(local) { - match promise.state() { - v8::PromiseState::Pending => match state { - Poll::Ready(Ok(_)) => { - let msg = "Promise resolution is still pending but the event loop has already resolved."; - Poll::Ready(Err(generic_error(msg))) - } - Poll::Ready(Err(e)) => Poll::Ready(Err(e)), - Poll::Pending => Poll::Pending, - }, - v8::PromiseState::Fulfilled => { - let value = promise.result(&mut scope); - let value_handle = v8::Global::new(&mut scope, value); - Poll::Ready(Ok(value_handle)) - } - v8::PromiseState::Rejected => { - let exception = promise.result(&mut scope); - Poll::Ready(exception_to_err_result(&mut scope, exception, false)) - } - } - } else { - let value_handle = v8::Global::new(&mut scope, local); - Poll::Ready(Ok(value_handle)) - } - } - - /// Waits for the given value to resolve while polling the event loop. - /// - /// This future resolves when either the value is resolved or the event loop runs to - /// completion. - pub async fn resolve_value( - &mut self, - global: v8::Global, - ) -> Result, Error> { - poll_fn(|cx| self.poll_value(&global, cx)).await - } - - /// Runs event loop to completion - /// - /// This future resolves when: - /// - there are no more pending dynamic imports - /// - there are no more pending ops - /// - there are no more active inspector sessions (only if `wait_for_inspector` is set to true) - pub async fn run_event_loop( - &mut self, - wait_for_inspector: bool, - ) -> Result<(), Error> { - poll_fn(|cx| self.poll_event_loop(cx, wait_for_inspector)).await - } - - /// Runs a single tick of event loop - /// - /// If `wait_for_inspector` is set to true event loop - /// will return `Poll::Pending` if there are active inspector sessions. - pub fn poll_event_loop( - &mut self, - cx: &mut Context, - wait_for_inspector: bool, - ) -> Poll> { - let has_inspector: bool; - - { - let state = self.inner.state.borrow(); - has_inspector = state.inspector.is_some(); - state.op_state.borrow().waker.register(cx.waker()); - } - - if has_inspector { - // We poll the inspector first. - let _ = self.inspector().borrow().poll_sessions(Some(cx)).unwrap(); - } - - self.pump_v8_message_loop()?; - - // Dynamic module loading - ie. modules loaded using "import()" - { - // Run in a loop so that dynamic imports that only depend on another - // dynamic import can be resolved in this event loop iteration. - // - // For example, a dynamically imported module like the following can be - // immediately resolved after `dependency.ts` is fully evaluated, but it - // wouldn't if not for this loop. - // - // await delay(1000); - // await import("./dependency.ts"); - // console.log("test") - // - loop { - let poll_imports = self.prepare_dyn_imports(cx)?; - assert!(poll_imports.is_ready()); - - let poll_imports = self.poll_dyn_imports(cx)?; - assert!(poll_imports.is_ready()); - - if !self.evaluate_dyn_imports() { - break; - } - } - } - - // Resolve async ops, run all next tick callbacks and macrotasks callbacks - // and only then check for any promise exceptions (`unhandledrejection` - // handlers are run in macrotasks callbacks so we need to let them run - // first). - self.do_js_event_loop_tick(cx)?; - self.check_promise_rejections()?; - - // Event loop middlewares - let mut maybe_scheduling = false; - { - let op_state = self.inner.state.borrow().op_state.clone(); - for f in &self.event_loop_middlewares { - if f(op_state.clone(), cx) { - maybe_scheduling = true; - } - } - } - - // Top level module - self.evaluate_pending_module(); - - let pending_state = self.event_loop_pending_state(); - if !pending_state.is_pending() && !maybe_scheduling { - if has_inspector { - let inspector = self.inspector(); - let has_active_sessions = inspector.borrow().has_active_sessions(); - let has_blocking_sessions = inspector.borrow().has_blocking_sessions(); - - if wait_for_inspector && has_active_sessions { - // If there are no blocking sessions (eg. REPL) we can now notify - // debugger that the program has finished running and we're ready - // to exit the process once debugger disconnects. - if !has_blocking_sessions { - let context = self.global_context(); - let scope = &mut self.handle_scope(); - inspector.borrow_mut().context_destroyed(scope, context); - println!("Program finished. Waiting for inspector to disconnect to exit the process..."); - } - - return Poll::Pending; - } - } - - return Poll::Ready(Ok(())); - } - - let state = self.inner.state.borrow(); - - // Check if more async ops have been dispatched - // during this turn of event loop. - // If there are any pending background tasks, we also wake the runtime to - // make sure we don't miss them. - // TODO(andreubotella) The event loop will spin as long as there are pending - // background tasks. We should look into having V8 notify us when a - // background task is done. - if pending_state.has_pending_background_tasks - || pending_state.has_tick_scheduled - || maybe_scheduling - { - state.op_state.borrow().waker.wake(); - } - - drop(state); - - if pending_state.has_pending_module_evaluation { - if pending_state.has_pending_refed_ops - || pending_state.has_pending_dyn_imports - || pending_state.has_pending_dyn_module_evaluation - || pending_state.has_pending_background_tasks - || pending_state.has_tick_scheduled - || maybe_scheduling - { - // pass, will be polled again - } else { - let scope = &mut self.handle_scope(); - let messages = find_stalled_top_level_await(scope); - // We are gonna print only a single message to provide a nice formatting - // with source line of offending promise shown. Once user fixed it, then - // they will get another error message for the next promise (but this - // situation is gonna be very rare, if ever happening). - assert!(!messages.is_empty()); - let msg = v8::Local::new(scope, messages[0].clone()); - let js_error = JsError::from_v8_message(scope, msg); - return Poll::Ready(Err(js_error.into())); - } - } - - if pending_state.has_pending_dyn_module_evaluation { - if pending_state.has_pending_refed_ops - || pending_state.has_pending_dyn_imports - || pending_state.has_pending_background_tasks - || pending_state.has_tick_scheduled - { - // pass, will be polled again - } else if self.inner.state.borrow().dyn_module_evaluate_idle_counter >= 1 - { - let scope = &mut self.handle_scope(); - let messages = find_stalled_top_level_await(scope); - // We are gonna print only a single message to provide a nice formatting - // with source line of offending promise shown. Once user fixed it, then - // they will get another error message for the next promise (but this - // situation is gonna be very rare, if ever happening). - assert!(!messages.is_empty()); - let msg = v8::Local::new(scope, messages[0].clone()); - let js_error = JsError::from_v8_message(scope, msg); - return Poll::Ready(Err(js_error.into())); - } else { - let mut state = self.inner.state.borrow_mut(); - // Delay the above error by one spin of the event loop. A dynamic import - // evaluation may complete during this, in which case the counter will - // reset. - state.dyn_module_evaluate_idle_counter += 1; - state.op_state.borrow().waker.wake(); - } - } - - Poll::Pending - } - - fn event_loop_pending_state(&mut self) -> EventLoopPendingState { - let mut scope = v8::HandleScope::new(self.inner.v8_isolate.as_mut()); - EventLoopPendingState::new( - &mut scope, - &mut self.inner.state.borrow_mut(), - &self.module_map.borrow(), - ) - } -} - -impl JsRuntimeForSnapshot { - pub fn new( - mut options: RuntimeOptions, - runtime_snapshot_options: RuntimeSnapshotOptions, - ) -> JsRuntimeForSnapshot { - JsRuntime::init_v8(options.v8_platform.take(), true); - JsRuntimeForSnapshot(JsRuntime::new_inner( - options, - true, - runtime_snapshot_options.snapshot_module_load_cb, - )) - } - - /// Takes a snapshot and consumes the runtime. - /// - /// `Error` can usually be downcast to `JsError`. - pub fn snapshot(mut self) -> v8::StartupData { - // Ensure there are no live inspectors to prevent crashes. - self.inner.prepare_for_cleanup(); - - // Set the context to be snapshot's default context - { - let context = self.global_context(); - let mut scope = self.handle_scope(); - let local_context = v8::Local::new(&mut scope, context); - scope.set_default_context(local_context); - } - - // Serialize the module map and store its data in the snapshot. - { - let snapshotted_data = { - // `self.module_map` points directly to the v8 isolate data slot, which - // we must explicitly drop before destroying the isolate. We have to - // take and drop this `Rc` before that. - let module_map_rc = std::mem::take(&mut self.module_map); - let module_map = module_map_rc.borrow(); - module_map.serialize_for_snapshotting(&mut self.handle_scope()) - }; - - let context = self.global_context(); - let mut scope = self.handle_scope(); - snapshot_util::set_snapshotted_data( - &mut scope, - context, - snapshotted_data, - ); - } - - self - .0 - .inner - .prepare_for_snapshot() - .create_blob(v8::FunctionCodeHandling::Keep) - .unwrap() - } -} - -fn get_stalled_top_level_await_message_for_module( - scope: &mut v8::HandleScope, - module_id: ModuleId, -) -> Vec> { - let module_map = JsRuntime::module_map_from(scope); - let module_map = module_map.borrow(); - let module_handle = module_map.handles.get(module_id).unwrap(); - - let module = v8::Local::new(scope, module_handle); - let stalled = module.get_stalled_top_level_await_message(scope); - let mut messages = vec![]; - for (_, message) in stalled { - messages.push(v8::Global::new(scope, message)); - } - messages -} - -fn find_stalled_top_level_await( - scope: &mut v8::HandleScope, -) -> Vec> { - let module_map = JsRuntime::module_map_from(scope); - let module_map = module_map.borrow(); - - // First check if that's root module - let root_module_id = module_map - .info - .iter() - .filter(|m| m.main) - .map(|m| m.id) - .next(); - - if let Some(root_module_id) = root_module_id { - let messages = - get_stalled_top_level_await_message_for_module(scope, root_module_id); - if !messages.is_empty() { - return messages; - } - } - - // It wasn't a top module, so iterate over all modules and try to find - // any with stalled top level await - for module_id in 0..module_map.handles.len() { - let messages = - get_stalled_top_level_await_message_for_module(scope, module_id); - if !messages.is_empty() { - return messages; - } - } - - unreachable!() -} - -#[derive(Clone, Copy, PartialEq, Eq, Debug)] -pub(crate) struct EventLoopPendingState { - has_pending_refed_ops: bool, - has_pending_dyn_imports: bool, - has_pending_dyn_module_evaluation: bool, - has_pending_module_evaluation: bool, - has_pending_background_tasks: bool, - has_tick_scheduled: bool, -} -impl EventLoopPendingState { - pub fn new( - scope: &mut v8::HandleScope<()>, - state: &mut JsRuntimeState, - module_map: &ModuleMap, - ) -> EventLoopPendingState { - let mut num_unrefed_ops = 0; - let mut num_pending_ops = 0; - for realm in &state.known_realms { - num_unrefed_ops += realm.num_unrefed_ops(); - num_pending_ops += realm.num_pending_ops(); - } - - EventLoopPendingState { - has_pending_refed_ops: num_pending_ops > num_unrefed_ops, - has_pending_dyn_imports: module_map.has_pending_dynamic_imports(), - has_pending_dyn_module_evaluation: !state - .pending_dyn_mod_evaluate - .is_empty(), - has_pending_module_evaluation: state.pending_mod_evaluate.is_some(), - has_pending_background_tasks: scope.has_pending_background_tasks(), - has_tick_scheduled: state.has_tick_scheduled, - } - } - - pub fn is_pending(&self) -> bool { - self.has_pending_refed_ops - || self.has_pending_dyn_imports - || self.has_pending_dyn_module_evaluation - || self.has_pending_module_evaluation - || self.has_pending_background_tasks - || self.has_tick_scheduled - } -} - -extern "C" fn near_heap_limit_callback( - data: *mut c_void, - current_heap_limit: usize, - initial_heap_limit: usize, -) -> usize -where - F: FnMut(usize, usize) -> usize, -{ - // SAFETY: The data is a pointer to the Rust callback function. It is stored - // in `JsRuntime::allocations` and thus is guaranteed to outlive the isolate. - let callback = unsafe { &mut *(data as *mut F) }; - callback(current_heap_limit, initial_heap_limit) -} - -impl JsRuntimeState { - pub(crate) fn inspector(&self) -> Rc> { - self.inspector.as_ref().unwrap().clone() - } - - /// Called by `bindings::host_import_module_dynamically_callback` - /// after initiating new dynamic import load. - pub fn notify_new_dynamic_import(&mut self) { - // Notify event loop to poll again soon. - self.op_state.borrow().waker.wake(); - } -} - -pub(crate) fn exception_to_err_result( - scope: &mut v8::HandleScope, - exception: v8::Local, - in_promise: bool, -) -> Result { - let state_rc = JsRuntime::state_from(scope); - - let was_terminating_execution = scope.is_execution_terminating(); - // Disable running microtasks for a moment. When upgrading to V8 v11.4 - // we discovered that canceling termination here will cause the queued - // microtasks to run which breaks some tests. - scope.set_microtasks_policy(v8::MicrotasksPolicy::Explicit); - // If TerminateExecution was called, cancel isolate termination so that the - // exception can be created. Note that `scope.is_execution_terminating()` may - // have returned false if TerminateExecution was indeed called but there was - // no JS to execute after the call. - scope.cancel_terminate_execution(); - let mut exception = exception; - { - // If termination is the result of a `op_dispatch_exception` call, we want - // to use the exception that was passed to it rather than the exception that - // was passed to this function. - let state = state_rc.borrow(); - exception = if let Some(exception) = &state.dispatched_exception { - v8::Local::new(scope, exception.clone()) - } else if was_terminating_execution && exception.is_null_or_undefined() { - let message = v8::String::new(scope, "execution terminated").unwrap(); - v8::Exception::error(scope, message) - } else { - exception - }; - } - - let mut js_error = JsError::from_v8_exception(scope, exception); - if in_promise { - js_error.exception_message = format!( - "Uncaught (in promise) {}", - js_error.exception_message.trim_start_matches("Uncaught ") - ); - } - - if was_terminating_execution { - // Resume exception termination. - scope.terminate_execution(); - } - scope.set_microtasks_policy(v8::MicrotasksPolicy::Auto); - - Err(js_error.into()) -} - -// Related to module loading -impl JsRuntime { - pub(crate) fn instantiate_module( - &mut self, - id: ModuleId, - ) -> Result<(), v8::Global> { - let module_map_rc = self.module_map.clone(); - let scope = &mut self.handle_scope(); - let tc_scope = &mut v8::TryCatch::new(scope); - - let module = module_map_rc - .borrow() - .get_handle(id) - .map(|handle| v8::Local::new(tc_scope, handle)) - .expect("ModuleInfo not found"); - - if module.get_status() == v8::ModuleStatus::Errored { - return Err(v8::Global::new(tc_scope, module.get_exception())); - } - - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // `module_resolve_callback` will be calling into `ModuleMap` from within - // the isolate. - let instantiate_result = - module.instantiate_module(tc_scope, bindings::module_resolve_callback); - - if instantiate_result.is_none() { - let exception = tc_scope.exception().unwrap(); - return Err(v8::Global::new(tc_scope, exception)); - } - - Ok(()) - } - - fn dynamic_import_module_evaluate( - &mut self, - load_id: ModuleLoadId, - id: ModuleId, - ) -> Result<(), Error> { - let module_handle = self - .module_map - .borrow() - .get_handle(id) - .expect("ModuleInfo not found"); - - let status = { - let scope = &mut self.handle_scope(); - let module = module_handle.open(scope); - module.get_status() - }; - - match status { - v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {} - _ => return Ok(()), - } - - // IMPORTANT: Top-level-await is enabled, which means that return value - // of module evaluation is a promise. - // - // This promise is internal, and not the same one that gets returned to - // the user. We add an empty `.catch()` handler so that it does not result - // in an exception if it rejects. That will instead happen for the other - // promise if not handled by the user. - // - // For more details see: - // https://github.com/denoland/deno/issues/4908 - // https://v8.dev/features/top-level-await#module-execution-order - let global_realm = - self.inner.state.borrow_mut().global_realm.clone().unwrap(); - let scope = &mut global_realm.handle_scope(&mut self.inner.v8_isolate); - let tc_scope = &mut v8::TryCatch::new(scope); - let module = v8::Local::new(tc_scope, &module_handle); - let maybe_value = module.evaluate(tc_scope); - - // Update status after evaluating. - let status = module.get_status(); - - if let Some(value) = maybe_value { - assert!( - status == v8::ModuleStatus::Evaluated - || status == v8::ModuleStatus::Errored - ); - let promise = v8::Local::::try_from(value) - .expect("Expected to get promise as module evaluation result"); - let empty_fn = bindings::create_empty_fn(tc_scope).unwrap(); - promise.catch(tc_scope, empty_fn); - let promise_global = v8::Global::new(tc_scope, promise); - let module_global = v8::Global::new(tc_scope, module); - - let dyn_import_mod_evaluate = DynImportModEvaluate { - load_id, - module_id: id, - promise: promise_global, - module: module_global, - }; - - self - .inner - .state - .borrow_mut() - .pending_dyn_mod_evaluate - .push(dyn_import_mod_evaluate); - } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Err( - generic_error("Cannot evaluate dynamically imported module, because JavaScript execution has been terminated.") - ); - } else { - assert!(status == v8::ModuleStatus::Errored); - } - - Ok(()) - } - - // TODO(bartlomieju): make it return `ModuleEvaluationFuture`? - /// Evaluates an already instantiated ES module. - /// - /// Returns a receiver handle that resolves when module promise resolves. - /// Implementors must manually call [`JsRuntime::run_event_loop`] to drive - /// module evaluation future. - /// - /// `Error` can usually be downcast to `JsError` and should be awaited and - /// checked after [`JsRuntime::run_event_loop`] completion. - /// - /// This function panics if module has not been instantiated. - pub fn mod_evaluate( - &mut self, - id: ModuleId, - ) -> oneshot::Receiver> { - let global_realm = self.global_realm(); - let state_rc = self.inner.state.clone(); - let module_map_rc = self.module_map.clone(); - let scope = &mut self.handle_scope(); - let tc_scope = &mut v8::TryCatch::new(scope); - - let module = module_map_rc - .borrow() - .get_handle(id) - .map(|handle| v8::Local::new(tc_scope, handle)) - .expect("ModuleInfo not found"); - let mut status = module.get_status(); - assert_eq!( - status, - v8::ModuleStatus::Instantiated, - "Module not instantiated {id}" - ); - - let (sender, receiver) = oneshot::channel(); - - // IMPORTANT: Top-level-await is enabled, which means that return value - // of module evaluation is a promise. - // - // Because that promise is created internally by V8, when error occurs during - // module evaluation the promise is rejected, and since the promise has no rejection - // handler it will result in call to `bindings::promise_reject_callback` adding - // the promise to pending promise rejection table - meaning JsRuntime will return - // error on next poll(). - // - // This situation is not desirable as we want to manually return error at the - // end of this function to handle it further. It means we need to manually - // remove this promise from pending promise rejection table. - // - // For more details see: - // https://github.com/denoland/deno/issues/4908 - // https://v8.dev/features/top-level-await#module-execution-order - { - let mut state = state_rc.borrow_mut(); - assert!( - state.pending_mod_evaluate.is_none(), - "There is already pending top level module evaluation" - ); - state.pending_mod_evaluate = Some(ModEvaluate { - promise: None, - has_evaluated: false, - handled_promise_rejections: vec![], - sender, - }); - } - - let maybe_value = module.evaluate(tc_scope); - { - let mut state = state_rc.borrow_mut(); - let pending_mod_evaluate = state.pending_mod_evaluate.as_mut().unwrap(); - pending_mod_evaluate.has_evaluated = true; - } - - // Update status after evaluating. - status = module.get_status(); - - let has_dispatched_exception = - state_rc.borrow_mut().dispatched_exception.is_some(); - if has_dispatched_exception { - // This will be overrided in `exception_to_err_result()`. - let exception = v8::undefined(tc_scope).into(); - let pending_mod_evaluate = { - let mut state = state_rc.borrow_mut(); - state.pending_mod_evaluate.take().unwrap() - }; - pending_mod_evaluate - .sender - .send(exception_to_err_result(tc_scope, exception, false)) - .expect("Failed to send module evaluation error."); - } else if let Some(value) = maybe_value { - assert!( - status == v8::ModuleStatus::Evaluated - || status == v8::ModuleStatus::Errored - ); - let promise = v8::Local::::try_from(value) - .expect("Expected to get promise as module evaluation result"); - let promise_global = v8::Global::new(tc_scope, promise); - let mut state = state_rc.borrow_mut(); - { - let pending_mod_evaluate = state.pending_mod_evaluate.as_ref().unwrap(); - let pending_rejection_was_already_handled = pending_mod_evaluate - .handled_promise_rejections - .contains(&promise_global); - if !pending_rejection_was_already_handled { - global_realm - .0 - .state() - .borrow_mut() - .pending_promise_rejections - .retain(|(key, _)| key != &promise_global); - } - } - let promise_global = v8::Global::new(tc_scope, promise); - state.pending_mod_evaluate.as_mut().unwrap().promise = - Some(promise_global); - tc_scope.perform_microtask_checkpoint(); - } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - let pending_mod_evaluate = { - let mut state = state_rc.borrow_mut(); - state.pending_mod_evaluate.take().unwrap() - }; - pending_mod_evaluate.sender.send(Err( - generic_error("Cannot evaluate module, because JavaScript execution has been terminated.") - )).expect("Failed to send module evaluation error."); - } else { - assert!(status == v8::ModuleStatus::Errored); - } - - receiver - } - - /// Clear the module map, meant to be used after initializing extensions. - /// Optionally pass a list of exceptions `(old_name, new_name)` representing - /// specifiers which will be renamed and preserved in the module map. - pub fn clear_module_map( - &self, - exceptions: impl Iterator, - ) { - let mut module_map = self.module_map.borrow_mut(); - let handles = exceptions - .map(|(old_name, new_name)| { - (module_map.get_handle_by_name(old_name).unwrap(), new_name) - }) - .collect::>(); - module_map.clear(); - for (handle, new_name) in handles { - module_map.inject_handle( - ModuleName::from_static(new_name), - ModuleType::JavaScript, - handle, - ) - } - } - - fn dynamic_import_reject( - &mut self, - id: ModuleLoadId, - exception: v8::Global, - ) { - let module_map_rc = self.module_map.clone(); - let scope = &mut self.handle_scope(); - - let resolver_handle = module_map_rc - .borrow_mut() - .dynamic_import_map - .remove(&id) - .expect("Invalid dynamic import id"); - let resolver = resolver_handle.open(scope); - - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // rejecting the promise might initiate another `import()` which will - // in turn call `bindings::host_import_module_dynamically_callback` which - // will reach into `ModuleMap` from within the isolate. - let exception = v8::Local::new(scope, exception); - resolver.reject(scope, exception).unwrap(); - scope.perform_microtask_checkpoint(); - } - - fn dynamic_import_resolve(&mut self, id: ModuleLoadId, mod_id: ModuleId) { - let state_rc = self.inner.state.clone(); - let module_map_rc = self.module_map.clone(); - let scope = &mut self.handle_scope(); - - let resolver_handle = module_map_rc - .borrow_mut() - .dynamic_import_map - .remove(&id) - .expect("Invalid dynamic import id"); - let resolver = resolver_handle.open(scope); - - let module = { - module_map_rc - .borrow() - .get_handle(mod_id) - .map(|handle| v8::Local::new(scope, handle)) - .expect("Dyn import module info not found") - }; - // Resolution success - assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated); - - // IMPORTANT: No borrows to `ModuleMap` can be held at this point because - // resolving the promise might initiate another `import()` which will - // in turn call `bindings::host_import_module_dynamically_callback` which - // will reach into `ModuleMap` from within the isolate. - let module_namespace = module.get_module_namespace(); - resolver.resolve(scope, module_namespace).unwrap(); - state_rc.borrow_mut().dyn_module_evaluate_idle_counter = 0; - scope.perform_microtask_checkpoint(); - } - - fn prepare_dyn_imports( - &mut self, - cx: &mut Context, - ) -> Poll> { - if self - .module_map - .borrow() - .preparing_dynamic_imports - .is_empty() - { - return Poll::Ready(Ok(())); - } - - loop { - let poll_result = self - .module_map - .borrow_mut() - .preparing_dynamic_imports - .poll_next_unpin(cx); - - if let Poll::Ready(Some(prepare_poll)) = poll_result { - let dyn_import_id = prepare_poll.0; - let prepare_result = prepare_poll.1; - - match prepare_result { - Ok(load) => { - self - .module_map - .borrow_mut() - .pending_dynamic_imports - .push(load.into_future()); - } - Err(err) => { - let exception = to_v8_type_error(&mut self.handle_scope(), err); - self.dynamic_import_reject(dyn_import_id, exception); - } - } - // Continue polling for more prepared dynamic imports. - continue; - } - - // There are no active dynamic import loads, or none are ready. - return Poll::Ready(Ok(())); - } - } - - fn poll_dyn_imports(&mut self, cx: &mut Context) -> Poll> { - if self.module_map.borrow().pending_dynamic_imports.is_empty() { - return Poll::Ready(Ok(())); - } - - loop { - let poll_result = self - .module_map - .borrow_mut() - .pending_dynamic_imports - .poll_next_unpin(cx); - - if let Poll::Ready(Some(load_stream_poll)) = poll_result { - let maybe_result = load_stream_poll.0; - let mut load = load_stream_poll.1; - let dyn_import_id = load.id; - - if let Some(load_stream_result) = maybe_result { - match load_stream_result { - Ok((request, info)) => { - // A module (not necessarily the one dynamically imported) has been - // fetched. Create and register it, and if successful, poll for the - // next recursive-load event related to this dynamic import. - let register_result = load.register_and_recurse( - &mut self.handle_scope(), - &request, - info, - ); - - match register_result { - Ok(()) => { - // Keep importing until it's fully drained - self - .module_map - .borrow_mut() - .pending_dynamic_imports - .push(load.into_future()); - } - Err(err) => { - let exception = match err { - ModuleError::Exception(e) => e, - ModuleError::Other(e) => { - to_v8_type_error(&mut self.handle_scope(), e) - } - }; - self.dynamic_import_reject(dyn_import_id, exception) - } - } - } - Err(err) => { - // A non-javascript error occurred; this could be due to a an invalid - // module specifier, or a problem with the source map, or a failure - // to fetch the module source code. - let exception = to_v8_type_error(&mut self.handle_scope(), err); - self.dynamic_import_reject(dyn_import_id, exception); - } - } - } else { - // The top-level module from a dynamic import has been instantiated. - // Load is done. - let module_id = - load.root_module_id.expect("Root module should be loaded"); - let result = self.instantiate_module(module_id); - if let Err(exception) = result { - self.dynamic_import_reject(dyn_import_id, exception); - } - self.dynamic_import_module_evaluate(dyn_import_id, module_id)?; - } - - // Continue polling for more ready dynamic imports. - continue; - } - - // There are no active dynamic import loads, or none are ready. - return Poll::Ready(Ok(())); - } - } - - /// "deno_core" runs V8 with Top Level Await enabled. It means that each - /// module evaluation returns a promise from V8. - /// Feature docs: https://v8.dev/features/top-level-await - /// - /// This promise resolves after all dependent modules have also - /// resolved. Each dependent module may perform calls to "import()" and APIs - /// using async ops will add futures to the runtime's event loop. - /// It means that the promise returned from module evaluation will - /// resolve only after all futures in the event loop are done. - /// - /// Thus during turn of event loop we need to check if V8 has - /// resolved or rejected the promise. If the promise is still pending - /// then another turn of event loop must be performed. - fn evaluate_pending_module(&mut self) { - let maybe_module_evaluation = - self.inner.state.borrow_mut().pending_mod_evaluate.take(); - - if maybe_module_evaluation.is_none() { - return; - } - - let mut module_evaluation = maybe_module_evaluation.unwrap(); - let state_rc = self.inner.state.clone(); - let scope = &mut self.handle_scope(); - - let promise_global = module_evaluation.promise.clone().unwrap(); - let promise = promise_global.open(scope); - let promise_state = promise.state(); - - match promise_state { - v8::PromiseState::Pending => { - // NOTE: `poll_event_loop` will decide if - // runtime would be woken soon - state_rc.borrow_mut().pending_mod_evaluate = Some(module_evaluation); - } - v8::PromiseState::Fulfilled => { - scope.perform_microtask_checkpoint(); - // Receiver end might have been already dropped, ignore the result - let _ = module_evaluation.sender.send(Ok(())); - module_evaluation.handled_promise_rejections.clear(); - } - v8::PromiseState::Rejected => { - let exception = promise.result(scope); - scope.perform_microtask_checkpoint(); - - // Receiver end might have been already dropped, ignore the result - if module_evaluation - .handled_promise_rejections - .contains(&promise_global) - { - let _ = module_evaluation.sender.send(Ok(())); - module_evaluation.handled_promise_rejections.clear(); - } else { - let _ = module_evaluation - .sender - .send(exception_to_err_result(scope, exception, false)); - } - } - } - } - - // Returns true if some dynamic import was resolved. - fn evaluate_dyn_imports(&mut self) -> bool { - let pending = std::mem::take( - &mut self.inner.state.borrow_mut().pending_dyn_mod_evaluate, - ); - if pending.is_empty() { - return false; - } - let mut resolved_any = false; - let mut still_pending = vec![]; - for pending_dyn_evaluate in pending { - let maybe_result = { - let scope = &mut self.handle_scope(); - - let module_id = pending_dyn_evaluate.module_id; - let promise = pending_dyn_evaluate.promise.open(scope); - let _module = pending_dyn_evaluate.module.open(scope); - let promise_state = promise.state(); - - match promise_state { - v8::PromiseState::Pending => { - still_pending.push(pending_dyn_evaluate); - None - } - v8::PromiseState::Fulfilled => { - Some(Ok((pending_dyn_evaluate.load_id, module_id))) - } - v8::PromiseState::Rejected => { - let exception = promise.result(scope); - let exception = v8::Global::new(scope, exception); - Some(Err((pending_dyn_evaluate.load_id, exception))) - } - } - }; - - if let Some(result) = maybe_result { - resolved_any = true; - match result { - Ok((dyn_import_id, module_id)) => { - self.dynamic_import_resolve(dyn_import_id, module_id); - } - Err((dyn_import_id, exception)) => { - self.dynamic_import_reject(dyn_import_id, exception); - } - } - } - } - self.inner.state.borrow_mut().pending_dyn_mod_evaluate = still_pending; - resolved_any - } - - /// Asynchronously load specified module and all of its dependencies. - /// - /// The module will be marked as "main", and because of that - /// "import.meta.main" will return true when checked inside that module. - /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` - /// manually after load is finished. - pub async fn load_main_module( - &mut self, - specifier: &ModuleSpecifier, - code: Option, - ) -> Result { - let module_map_rc = self.module_map.clone(); - if let Some(code) = code { - let specifier = specifier.as_str().to_owned().into(); - let scope = &mut self.handle_scope(); - // true for main module - module_map_rc - .borrow_mut() - .new_es_module(scope, true, specifier, code, false) - .map_err(|e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - })?; - } - - let mut load = - ModuleMap::load_main(module_map_rc.clone(), &specifier).await?; - - while let Some(load_result) = load.next().await { - let (request, info) = load_result?; - let scope = &mut self.handle_scope(); - load.register_and_recurse(scope, &request, info).map_err( - |e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - }, - )?; - } - - let root_id = load.root_module_id.expect("Root module should be loaded"); - self.instantiate_module(root_id).map_err(|e| { - let scope = &mut self.handle_scope(); - let exception = v8::Local::new(scope, e); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - })?; - Ok(root_id) - } - - /// Asynchronously load specified ES module and all of its dependencies. - /// - /// This method is meant to be used when loading some utility code that - /// might be later imported by the main module (ie. an entry point module). - /// - /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` - /// manually after load is finished. - pub async fn load_side_module( - &mut self, - specifier: &ModuleSpecifier, - code: Option, - ) -> Result { - let module_map_rc = self.module_map.clone(); - if let Some(code) = code { - let specifier = specifier.as_str().to_owned().into(); - let scope = &mut self.handle_scope(); - // false for side module (not main module) - module_map_rc - .borrow_mut() - .new_es_module(scope, false, specifier, code, false) - .map_err(|e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - })?; - } - - let mut load = - ModuleMap::load_side(module_map_rc.clone(), &specifier).await?; - - while let Some(load_result) = load.next().await { - let (request, info) = load_result?; - let scope = &mut self.handle_scope(); - load.register_and_recurse(scope, &request, info).map_err( - |e| match e { - ModuleError::Exception(exception) => { - let exception = v8::Local::new(scope, exception); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - } - ModuleError::Other(error) => error, - }, - )?; - } - - let root_id = load.root_module_id.expect("Root module should be loaded"); - self.instantiate_module(root_id).map_err(|e| { - let scope = &mut self.handle_scope(); - let exception = v8::Local::new(scope, e); - exception_to_err_result::<()>(scope, exception, false).unwrap_err() - })?; - Ok(root_id) - } - - fn check_promise_rejections(&mut self) -> Result<(), Error> { - let state = self.inner.state.clone(); - let scope = &mut self.handle_scope(); - let state = state.borrow(); - for realm in &state.known_realms { - realm.check_promise_rejections(scope)?; - } - Ok(()) - } - - // Polls pending ops and then runs `Deno.core.eventLoopTick` callback. - fn do_js_event_loop_tick(&mut self, cx: &mut Context) -> Result<(), Error> { - // Handle responses for each realm. - let state = self.inner.state.clone(); - let isolate = &mut self.inner.v8_isolate; - let realm_count = state.borrow().known_realms.len(); - for realm_idx in 0..realm_count { - let realm = state.borrow().known_realms.get(realm_idx).unwrap().clone(); - let context_state = realm.state(); - let mut context_state = context_state.borrow_mut(); - let scope = &mut realm.handle_scope(isolate); - - // We return async responses to JS in unbounded batches (may change), - // each batch is a flat vector of tuples: - // `[promise_id1, op_result1, promise_id2, op_result2, ...]` - // promise_id is a simple integer, op_result is an ops::OpResult - // which contains a value OR an error, encoded as a tuple. - // This batch is received in JS via the special `arguments` variable - // and then each tuple is used to resolve or reject promises - // - // This can handle 15 promises futures in a single batch without heap - // allocations. - let mut args: SmallVec<[v8::Local; 32]> = - SmallVec::with_capacity(32); - - loop { - let item = { - let next = std::pin::pin!(context_state.pending_ops.join_next()); - let Poll::Ready(Some(item)) = next.poll(cx) else { - break; - }; - item - }; - let (promise_id, op_id, mut resp) = item.unwrap().into_inner(); - state - .borrow() - .op_state - .borrow() - .tracker - .track_async_completed(op_id); - context_state.unrefed_ops.remove(&promise_id); - args.push(v8::Integer::new(scope, promise_id).into()); - args.push(match resp.to_v8(scope) { - Ok(v) => v, - Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into())) - .to_v8(scope) - .unwrap(), - }); - } - - let has_tick_scheduled = - v8::Boolean::new(scope, self.inner.state.borrow().has_tick_scheduled); - args.push(has_tick_scheduled.into()); - - let js_event_loop_tick_cb_handle = - context_state.js_event_loop_tick_cb.clone().unwrap(); - let tc_scope = &mut v8::TryCatch::new(scope); - let js_event_loop_tick_cb = js_event_loop_tick_cb_handle.open(tc_scope); - let this = v8::undefined(tc_scope).into(); - drop(context_state); - js_event_loop_tick_cb.call(tc_scope, this, args.as_slice()); - - if let Some(exception) = tc_scope.exception() { - // TODO(@andreubotella): Returning here can cause async ops in other - // realms to never resolve. - return exception_to_err_result(tc_scope, exception, false); - } - - if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { - return Ok(()); - } - } - - Ok(()) - } -} - -#[inline] -pub fn queue_fast_async_op( - ctx: &OpCtx, - promise_id: PromiseId, - op: impl Future> + 'static, -) { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - let fut = op - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(); - // SAFETY: this this is guaranteed to be running on a current-thread executor - ctx.context_state.borrow_mut().pending_ops.spawn(unsafe { - crate::task::MaskFutureAsSend::new(OpCall::pending(ctx, promise_id, fut)) - }); -} - -#[inline] -pub fn map_async_op1( - ctx: &OpCtx, - op: impl Future> + 'static, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - let fut = op - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(); - MaybeDone::Future(fut) -} - -#[inline] -pub fn map_async_op2( - ctx: &OpCtx, - op: impl Future + 'static, -) -> MaybeDone>>> { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - - let fut = op.map(|result| OpResult::Ok(result.into())).boxed_local(); - MaybeDone::Future(fut) -} - -#[inline] -pub fn map_async_op3( - ctx: &OpCtx, - op: Result> + 'static, Error>, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - match op { - Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), - Ok(fut) => MaybeDone::Future( - fut - .map(|result| crate::_ops::to_op_result(get_class, result)) - .boxed_local(), - ), - } -} - -#[inline] -pub fn map_async_op4( - ctx: &OpCtx, - op: Result + 'static, Error>, -) -> MaybeDone>>> { - let get_class = { - let state = RefCell::borrow(&ctx.state); - state.tracker.track_async(ctx.id); - state.get_error_class_fn - }; - - match op { - Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), - Ok(fut) => MaybeDone::Future( - fut.map(|result| OpResult::Ok(result.into())).boxed_local(), - ), - } -} - -pub fn queue_async_op<'s>( - ctx: &OpCtx, - scope: &'s mut v8::HandleScope, - deferred: bool, - promise_id: PromiseId, - mut op: MaybeDone>>>, -) -> Option> { - // An op's realm (as given by `OpCtx::realm_idx`) must match the realm in - // which it is invoked. Otherwise, we might have cross-realm object exposure. - // deno_core doesn't currently support such exposure, even though embedders - // can cause them, so we panic in debug mode (since the check is expensive). - // TODO(mmastrac): Restore this - // debug_assert_eq!( - // runtime_state.borrow().context(ctx.realm_idx as usize, scope), - // Some(scope.get_current_context()) - // ); - - // All ops are polled immediately - let waker = noop_waker(); - let mut cx = Context::from_waker(&waker); - - // Note that MaybeDone returns () from the future - let op_call = match op.poll_unpin(&mut cx) { - Poll::Pending => { - let MaybeDone::Future(fut) = op else { - unreachable!() - }; - OpCall::pending(ctx, promise_id, fut) - } - Poll::Ready(_) => { - let mut op_result = Pin::new(&mut op).take_output().unwrap(); - // If the op is ready and is not marked as deferred we can immediately return - // the result. - if !deferred { - ctx.state.borrow_mut().tracker.track_async_completed(ctx.id); - return Some(op_result.to_v8(scope).unwrap()); - } - - OpCall::ready(ctx, promise_id, op_result) - } - }; - - // Otherwise we will push it to the `pending_ops` and let it be polled again - // or resolved on the next tick of the event loop. - ctx - .context_state - .borrow_mut() - .pending_ops - // SAFETY: this this is guaranteed to be running on a current-thread executor - .spawn(unsafe { crate::task::MaskFutureAsSend::new(op_call) }); - None -} - -#[cfg(test)] -pub mod tests { - use super::*; - use crate::ascii_str; - use crate::error::custom_error; - use crate::error::AnyError; - use crate::include_ascii_string; - use crate::modules::AssertedModuleType; - use crate::modules::ModuleInfo; - use crate::modules::ModuleSource; - use crate::modules::ModuleSourceFuture; - use crate::modules::ModuleType; - use crate::modules::ResolutionKind; - use crate::modules::SymbolicModule; - use crate::ZeroCopyBuf; - use deno_ops::op; - use std::pin::Pin; - use std::rc::Rc; - use std::sync::atomic::AtomicUsize; - use std::sync::atomic::Ordering; - use std::sync::Arc; - - // deno_ops macros generate code assuming deno_core in scope. - mod deno_core { - pub use crate::*; - } - - #[derive(Copy, Clone)] - pub enum Mode { - Async, - AsyncDeferred, - AsyncZeroCopy(bool), - } - - struct TestState { - mode: Mode, - dispatch_count: Arc, - } - - #[op] - async fn op_test( - rc_op_state: Rc>, - control: u8, - buf: Option, - ) -> Result { - #![allow(clippy::await_holding_refcell_ref)] // False positive. - let op_state_ = rc_op_state.borrow(); - let test_state = op_state_.borrow::(); - test_state.dispatch_count.fetch_add(1, Ordering::Relaxed); - let mode = test_state.mode; - drop(op_state_); - match mode { - Mode::Async => { - assert_eq!(control, 42); - Ok(43) - } - Mode::AsyncDeferred => { - tokio::task::yield_now().await; - assert_eq!(control, 42); - Ok(43) - } - Mode::AsyncZeroCopy(has_buffer) => { - assert_eq!(buf.is_some(), has_buffer); - if let Some(buf) = buf { - assert_eq!(buf.len(), 1); - } - Ok(43) - } - } - } - - fn setup(mode: Mode) -> (JsRuntime, Arc) { - let dispatch_count = Arc::new(AtomicUsize::new(0)); - deno_core::extension!( - test_ext, - ops = [op_test], - options = { - mode: Mode, - dispatch_count: Arc, - }, - state = |state, options| { - state.put(TestState { - mode: options.mode, - dispatch_count: options.dispatch_count - }) - } - ); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops(mode, dispatch_count.clone())], - get_error_class_fn: Some(&|error| { - crate::error::get_custom_error_class(error).unwrap() - }), - ..Default::default() - }); - - runtime - .execute_script_static( - "setup.js", - r#" - function assert(cond) { - if (!cond) { - throw Error("assert"); - } - } - "#, - ) - .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); - (runtime, dispatch_count) - } - - #[tokio::test] - async fn test_ref_unref_ops() { - let (mut runtime, _dispatch_count) = setup(Mode::AsyncDeferred); - runtime - .execute_script_static( - "filename.js", - r#" - - var promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); - var p1 = Deno.core.opAsync("op_test", 42); - var p2 = Deno.core.opAsync("op_test", 42); - "#, - ) - .unwrap(); - { - let realm = runtime.global_realm(); - assert_eq!(realm.num_pending_ops(), 2); - assert_eq!(realm.num_unrefed_ops(), 0); - } - runtime - .execute_script_static( - "filename.js", - r#" - Deno.core.ops.op_unref_op(p1[promiseIdSymbol]); - Deno.core.ops.op_unref_op(p2[promiseIdSymbol]); - "#, - ) - .unwrap(); - { - let realm = runtime.global_realm(); - assert_eq!(realm.num_pending_ops(), 2); - assert_eq!(realm.num_unrefed_ops(), 2); - } - runtime - .execute_script_static( - "filename.js", - r#" - Deno.core.ops.op_ref_op(p1[promiseIdSymbol]); - Deno.core.ops.op_ref_op(p2[promiseIdSymbol]); - "#, - ) - .unwrap(); - { - let realm = runtime.global_realm(); - assert_eq!(realm.num_pending_ops(), 2); - assert_eq!(realm.num_unrefed_ops(), 0); - } - } - - #[test] - fn test_dispatch() { - let (mut runtime, dispatch_count) = setup(Mode::Async); - runtime - .execute_script_static( - "filename.js", - r#" - let control = 42; - - Deno.core.opAsync("op_test", control); - async function main() { - Deno.core.opAsync("op_test", control); - } - main(); - "#, - ) - .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 2); - } - - #[test] - fn test_op_async_promise_id() { - let (mut runtime, _dispatch_count) = setup(Mode::Async); - runtime - .execute_script_static( - "filename.js", - r#" - - const p = Deno.core.opAsync("op_test", 42); - if (p[Symbol.for("Deno.core.internalPromiseId")] == undefined) { - throw new Error("missing id on returned promise"); - } - "#, - ) - .unwrap(); - } - - #[test] - fn test_dispatch_no_zero_copy_buf() { - let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(false)); - runtime - .execute_script_static( - "filename.js", - r#" - - Deno.core.opAsync("op_test"); - "#, - ) - .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 1); - } - - #[test] - fn test_dispatch_stack_zero_copy_bufs() { - let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(true)); - runtime - .execute_script_static( - "filename.js", - r#" - const { op_test } = Deno.core.ensureFastOps(); - let zero_copy_a = new Uint8Array([0]); - op_test(null, zero_copy_a); - "#, - ) - .unwrap(); - assert_eq!(dispatch_count.load(Ordering::Relaxed), 1); - } - - #[test] - fn test_execute_script_return_value() { - let mut runtime = JsRuntime::new(Default::default()); - let value_global = - runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); - { - let scope = &mut runtime.handle_scope(); - let value = value_global.open(scope); - assert_eq!(value.integer_value(scope).unwrap(), 3); - } - let value_global = runtime - .execute_script_static("b.js", "b = 'foobar'") - .unwrap(); - { - let scope = &mut runtime.handle_scope(); - let value = value_global.open(scope); - assert!(value.is_string()); - assert_eq!( - value.to_string(scope).unwrap().to_rust_string_lossy(scope), - "foobar" - ); - } - } - - #[tokio::test] - async fn test_poll_value() { - let mut runtime = JsRuntime::new(Default::default()); - poll_fn(move |cx| { - let value_global = runtime - .execute_script_static("a.js", "Promise.resolve(1 + 2)") - .unwrap(); - let v = runtime.poll_value(&value_global, cx); - { - let scope = &mut runtime.handle_scope(); - assert!( - matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 3) - ); - } - - let value_global = runtime - .execute_script_static( - "a.js", - "Promise.resolve(new Promise(resolve => resolve(2 + 2)))", - ) - .unwrap(); - let v = runtime.poll_value(&value_global, cx); - { - let scope = &mut runtime.handle_scope(); - assert!( - matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 4) - ); - } - - let value_global = runtime - .execute_script_static("a.js", "Promise.reject(new Error('fail'))") - .unwrap(); - let v = runtime.poll_value(&value_global, cx); - assert!( - matches!(v, Poll::Ready(Err(e)) if e.downcast_ref::().unwrap().exception_message == "Uncaught Error: fail") - ); - - let value_global = runtime - .execute_script_static("a.js", "new Promise(resolve => {})") - .unwrap(); - let v = runtime.poll_value(&value_global, cx); - matches!(v, Poll::Ready(Err(e)) if e.to_string() == "Promise resolution is still pending but the event loop has already resolved."); - Poll::Ready(()) - }).await; - } - - #[tokio::test] - async fn test_resolve_value() { - let mut runtime = JsRuntime::new(Default::default()); - let value_global = runtime - .execute_script_static("a.js", "Promise.resolve(1 + 2)") - .unwrap(); - let result_global = runtime.resolve_value(value_global).await.unwrap(); - { - let scope = &mut runtime.handle_scope(); - let value = result_global.open(scope); - assert_eq!(value.integer_value(scope).unwrap(), 3); - } - - let value_global = runtime - .execute_script_static( - "a.js", - "Promise.resolve(new Promise(resolve => resolve(2 + 2)))", - ) - .unwrap(); - let result_global = runtime.resolve_value(value_global).await.unwrap(); - { - let scope = &mut runtime.handle_scope(); - let value = result_global.open(scope); - assert_eq!(value.integer_value(scope).unwrap(), 4); - } - - let value_global = runtime - .execute_script_static("a.js", "Promise.reject(new Error('fail'))") - .unwrap(); - let err = runtime.resolve_value(value_global).await.unwrap_err(); - assert_eq!( - "Uncaught Error: fail", - err.downcast::().unwrap().exception_message - ); - - let value_global = runtime - .execute_script_static("a.js", "new Promise(resolve => {})") - .unwrap(); - let error_string = runtime - .resolve_value(value_global) - .await - .unwrap_err() - .to_string(); - assert_eq!( - "Promise resolution is still pending but the event loop has already resolved.", - error_string, - ); - } - - #[test] - fn terminate_execution_webassembly() { - let (mut runtime, _dispatch_count) = setup(Mode::Async); - let v8_isolate_handle = runtime.v8_isolate().thread_safe_handle(); - - // Run an infinite loop in Webassemby code, which should be terminated. - let promise = runtime.execute_script_static("infinite_wasm_loop.js", - r#" - (async () => { - const wasmCode = new Uint8Array([ - 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, - 96, 0, 0, 3, 2, 1, 0, 7, 17, 1, 13, - 105, 110, 102, 105, 110, 105, 116, 101, 95, 108, 111, - 111, 112, 0, 0, 10, 9, 1, 7, 0, 3, 64, - 12, 0, 11, 11, - ]); - const wasmModule = await WebAssembly.compile(wasmCode); - globalThis.wasmInstance = new WebAssembly.Instance(wasmModule); - })() - "#).unwrap(); - futures::executor::block_on(runtime.resolve_value(promise)).unwrap(); - let terminator_thread = std::thread::spawn(move || { - std::thread::sleep(std::time::Duration::from_millis(1000)); - - // terminate execution - let ok = v8_isolate_handle.terminate_execution(); - assert!(ok); - }); - let err = runtime - .execute_script_static( - "infinite_wasm_loop2.js", - "globalThis.wasmInstance.exports.infinite_loop();", - ) - .unwrap_err(); - assert_eq!(err.to_string(), "Uncaught Error: execution terminated"); - // Cancel the execution-terminating exception in order to allow script - // execution again. - let ok = runtime.v8_isolate().cancel_terminate_execution(); - assert!(ok); - - // Verify that the isolate usable again. - runtime - .execute_script_static("simple.js", "1 + 1") - .expect("execution should be possible again"); - - terminator_thread.join().unwrap(); - } - - #[test] - fn terminate_execution() { - let (mut isolate, _dispatch_count) = setup(Mode::Async); - let v8_isolate_handle = isolate.v8_isolate().thread_safe_handle(); - - let terminator_thread = std::thread::spawn(move || { - // allow deno to boot and run - std::thread::sleep(std::time::Duration::from_millis(100)); - - // terminate execution - let ok = v8_isolate_handle.terminate_execution(); - assert!(ok); - }); - - // Rn an infinite loop, which should be terminated. - match isolate.execute_script_static("infinite_loop.js", "for(;;) {}") { - Ok(_) => panic!("execution should be terminated"), - Err(e) => { - assert_eq!(e.to_string(), "Uncaught Error: execution terminated") - } - }; - - // Cancel the execution-terminating exception in order to allow script - // execution again. - let ok = isolate.v8_isolate().cancel_terminate_execution(); - assert!(ok); - - // Verify that the isolate usable again. - isolate - .execute_script_static("simple.js", "1 + 1") - .expect("execution should be possible again"); - - terminator_thread.join().unwrap(); - } - - #[test] - fn dangling_shared_isolate() { - let v8_isolate_handle = { - // isolate is dropped at the end of this block - let (mut runtime, _dispatch_count) = setup(Mode::Async); - runtime.v8_isolate().thread_safe_handle() - }; - - // this should not SEGFAULT - v8_isolate_handle.terminate_execution(); - } - - #[test] - fn syntax_error() { - let mut runtime = JsRuntime::new(Default::default()); - let src = "hocuspocus("; - let r = runtime.execute_script_static("i.js", src); - let e = r.unwrap_err(); - let js_error = e.downcast::().unwrap(); - let frame = js_error.frames.first().unwrap(); - assert_eq!(frame.column_number, Some(12)); - } - - #[tokio::test] - async fn test_encode_decode() { - let (mut runtime, _dispatch_count) = setup(Mode::Async); - poll_fn(move |cx| { - runtime - .execute_script( - "encode_decode_test.js", - // Note: We make this to_owned because it contains non-ASCII chars - include_str!("encode_decode_test.js").to_owned().into(), - ) - .unwrap(); - if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { - unreachable!(); - } - Poll::Ready(()) - }) - .await; - } - - #[tokio::test] - async fn test_serialize_deserialize() { - let (mut runtime, _dispatch_count) = setup(Mode::Async); - poll_fn(move |cx| { - runtime - .execute_script( - "serialize_deserialize_test.js", - include_ascii_string!("serialize_deserialize_test.js"), - ) - .unwrap(); - if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { - unreachable!(); - } - Poll::Ready(()) - }) - .await; - } - - #[tokio::test] - async fn test_error_builder() { - #[op] - fn op_err() -> Result<(), Error> { - Err(custom_error("DOMExceptionOperationError", "abc")) - } - - pub fn get_error_class_name(_: &Error) -> &'static str { - "DOMExceptionOperationError" - } - - deno_core::extension!(test_ext, ops = [op_err]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - get_error_class_fn: Some(&get_error_class_name), - ..Default::default() - }); - poll_fn(move |cx| { - runtime - .execute_script_static( - "error_builder_test.js", - include_str!("error_builder_test.js"), - ) - .unwrap(); - if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { - unreachable!(); - } - Poll::Ready(()) - }) - .await; - } - - /// Ensure that putting the inspector into OpState doesn't cause crashes. The only valid place we currently allow - /// the inspector to be stashed without cleanup is the OpState, and this should not actually cause crashes. - #[test] - fn inspector() { - let mut runtime = JsRuntime::new(RuntimeOptions { - inspector: true, - ..Default::default() - }); - // This was causing a crash - runtime.op_state().borrow_mut().put(runtime.inspector()); - runtime.execute_script_static("check.js", "null").unwrap(); - } - - #[test] - fn will_snapshot() { - let snapshot = { - let mut runtime = - JsRuntimeForSnapshot::new(Default::default(), Default::default()); - runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); - runtime.snapshot() - }; - - let snapshot = Snapshot::JustCreated(snapshot); - let mut runtime2 = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(snapshot), - ..Default::default() - }); - runtime2 - .execute_script_static("check.js", "if (a != 3) throw Error('x')") - .unwrap(); - } - - #[test] - fn will_snapshot2() { - let startup_data = { - let mut runtime = - JsRuntimeForSnapshot::new(Default::default(), Default::default()); - runtime - .execute_script_static("a.js", "let a = 1 + 2") - .unwrap(); - runtime.snapshot() - }; - - let snapshot = Snapshot::JustCreated(startup_data); - let mut runtime = JsRuntimeForSnapshot::new( - RuntimeOptions { - startup_snapshot: Some(snapshot), - ..Default::default() - }, - Default::default(), - ); - - let startup_data = { - runtime - .execute_script_static("check_a.js", "if (a != 3) throw Error('x')") - .unwrap(); - runtime.execute_script_static("b.js", "b = 2 + 3").unwrap(); - runtime.snapshot() - }; - - let snapshot = Snapshot::JustCreated(startup_data); - { - let mut runtime = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(snapshot), - ..Default::default() - }); - runtime - .execute_script_static("check_b.js", "if (b != 5) throw Error('x')") - .unwrap(); - runtime - .execute_script_static("check2.js", "if (!Deno.core) throw Error('x')") - .unwrap(); - } - } - - #[test] - fn test_snapshot_callbacks() { - let snapshot = { - let mut runtime = - JsRuntimeForSnapshot::new(Default::default(), Default::default()); - runtime - .execute_script_static( - "a.js", - r#" - Deno.core.setMacrotaskCallback(() => { - return true; - }); - Deno.core.ops.op_set_format_exception_callback(()=> { - return null; - }) - Deno.core.setPromiseRejectCallback(() => { - return false; - }); - a = 1 + 2; - "#, - ) - .unwrap(); - runtime.snapshot() - }; - - let snapshot = Snapshot::JustCreated(snapshot); - let mut runtime2 = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(snapshot), - ..Default::default() - }); - runtime2 - .execute_script_static("check.js", "if (a != 3) throw Error('x')") - .unwrap(); - } - - #[test] - fn test_from_boxed_snapshot() { - let snapshot = { - let mut runtime = - JsRuntimeForSnapshot::new(Default::default(), Default::default()); - runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); - let snap: &[u8] = &runtime.snapshot(); - Vec::from(snap).into_boxed_slice() - }; - - let snapshot = Snapshot::Boxed(snapshot); - let mut runtime2 = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(snapshot), - ..Default::default() - }); - runtime2 - .execute_script_static("check.js", "if (a != 3) throw Error('x')") - .unwrap(); - } - - #[test] - fn test_get_module_namespace() { - #[derive(Default)] - struct ModsLoader; - - impl ModuleLoader for ModsLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _kind: ResolutionKind, - ) -> Result { - assert_eq!(specifier, "file:///main.js"); - assert_eq!(referrer, "."); - let s = crate::resolve_import(specifier, referrer).unwrap(); - Ok(s) - } - - fn load( - &self, - _module_specifier: &ModuleSpecifier, - _maybe_referrer: Option<&ModuleSpecifier>, - _is_dyn_import: bool, - ) -> Pin> { - async { Err(generic_error("Module loading is not supported")) } - .boxed_local() - } - } - - let loader = std::rc::Rc::new(ModsLoader::default()); - let mut runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader), - ..Default::default() - }); - - let specifier = crate::resolve_url("file:///main.js").unwrap(); - let source_code = ascii_str!( - r#" - export const a = "b"; - export default 1 + 2; - "# - ); - - let module_id = futures::executor::block_on( - runtime.load_main_module(&specifier, Some(source_code)), - ) - .unwrap(); - - #[allow(clippy::let_underscore_future)] - let _ = runtime.mod_evaluate(module_id); - - let module_namespace = runtime.get_module_namespace(module_id).unwrap(); - - let scope = &mut runtime.handle_scope(); - - let module_namespace = - v8::Local::::new(scope, module_namespace); - - assert!(module_namespace.is_module_namespace_object()); - - let unknown_export_name = v8::String::new(scope, "none").unwrap(); - let binding = module_namespace.get(scope, unknown_export_name.into()); - - assert!(binding.is_some()); - assert!(binding.unwrap().is_undefined()); - - let empty_export_name = v8::String::new(scope, "").unwrap(); - let binding = module_namespace.get(scope, empty_export_name.into()); - - assert!(binding.is_some()); - assert!(binding.unwrap().is_undefined()); - - let a_export_name = v8::String::new(scope, "a").unwrap(); - let binding = module_namespace.get(scope, a_export_name.into()); - - assert!(binding.unwrap().is_string()); - assert_eq!(binding.unwrap(), v8::String::new(scope, "b").unwrap()); - - let default_export_name = v8::String::new(scope, "default").unwrap(); - let binding = module_namespace.get(scope, default_export_name.into()); - - assert!(binding.unwrap().is_number()); - assert_eq!(binding.unwrap(), v8::Number::new(scope, 3_f64)); - } - - #[test] - fn test_heap_limits() { - let create_params = - v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024); - let mut runtime = JsRuntime::new(RuntimeOptions { - create_params: Some(create_params), - ..Default::default() - }); - let cb_handle = runtime.v8_isolate().thread_safe_handle(); - - let callback_invoke_count = Rc::new(AtomicUsize::new(0)); - let inner_invoke_count = Rc::clone(&callback_invoke_count); - - runtime.add_near_heap_limit_callback( - move |current_limit, _initial_limit| { - inner_invoke_count.fetch_add(1, Ordering::SeqCst); - cb_handle.terminate_execution(); - current_limit * 2 - }, - ); - let err = runtime - .execute_script_static( - "script name", - r#"let s = ""; while(true) { s += "Hello"; }"#, - ) - .expect_err("script should fail"); - assert_eq!( - "Uncaught Error: execution terminated", - err.downcast::().unwrap().exception_message - ); - assert!(callback_invoke_count.load(Ordering::SeqCst) > 0) - } - - #[test] - fn test_heap_limit_cb_remove() { - let mut runtime = JsRuntime::new(Default::default()); - - runtime.add_near_heap_limit_callback(|current_limit, _initial_limit| { - current_limit * 2 - }); - runtime.remove_near_heap_limit_callback(3 * 1024 * 1024); - assert!(runtime.allocations.near_heap_limit_callback_data.is_none()); - } - - #[test] - fn test_heap_limit_cb_multiple() { - let create_params = - v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024); - let mut runtime = JsRuntime::new(RuntimeOptions { - create_params: Some(create_params), - ..Default::default() - }); - let cb_handle = runtime.v8_isolate().thread_safe_handle(); - - let callback_invoke_count_first = Rc::new(AtomicUsize::new(0)); - let inner_invoke_count_first = Rc::clone(&callback_invoke_count_first); - runtime.add_near_heap_limit_callback( - move |current_limit, _initial_limit| { - inner_invoke_count_first.fetch_add(1, Ordering::SeqCst); - current_limit * 2 - }, - ); - - let callback_invoke_count_second = Rc::new(AtomicUsize::new(0)); - let inner_invoke_count_second = Rc::clone(&callback_invoke_count_second); - runtime.add_near_heap_limit_callback( - move |current_limit, _initial_limit| { - inner_invoke_count_second.fetch_add(1, Ordering::SeqCst); - cb_handle.terminate_execution(); - current_limit * 2 - }, - ); - - let err = runtime - .execute_script_static( - "script name", - r#"let s = ""; while(true) { s += "Hello"; }"#, - ) - .expect_err("script should fail"); - assert_eq!( - "Uncaught Error: execution terminated", - err.downcast::().unwrap().exception_message - ); - assert_eq!(0, callback_invoke_count_first.load(Ordering::SeqCst)); - assert!(callback_invoke_count_second.load(Ordering::SeqCst) > 0); - } - - #[test] - fn es_snapshot() { - #[derive(Default)] - struct ModsLoader; - - impl ModuleLoader for ModsLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _kind: ResolutionKind, - ) -> Result { - let s = crate::resolve_import(specifier, referrer).unwrap(); - Ok(s) - } - - fn load( - &self, - _module_specifier: &ModuleSpecifier, - _maybe_referrer: Option<&ModuleSpecifier>, - _is_dyn_import: bool, - ) -> Pin> { - eprintln!("load() should not be called"); - unreachable!() - } - } - - fn create_module( - runtime: &mut JsRuntime, - i: usize, - main: bool, - ) -> ModuleInfo { - let specifier = crate::resolve_url(&format!("file:///{i}.js")).unwrap(); - let prev = i - 1; - let source_code = format!( - r#" - import {{ f{prev} }} from "file:///{prev}.js"; - export function f{i}() {{ return f{prev}() }} - "# - ) - .into(); - - let id = if main { - futures::executor::block_on( - runtime.load_main_module(&specifier, Some(source_code)), - ) - .unwrap() - } else { - futures::executor::block_on( - runtime.load_side_module(&specifier, Some(source_code)), - ) - .unwrap() - }; - assert_eq!(i, id); - - #[allow(clippy::let_underscore_future)] - let _ = runtime.mod_evaluate(id); - futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); - - ModuleInfo { - id, - main, - name: specifier.into(), - requests: vec![crate::modules::ModuleRequest { - specifier: format!("file:///{prev}.js"), - asserted_module_type: AssertedModuleType::JavaScriptOrWasm, - }], - module_type: ModuleType::JavaScript, - } - } - - fn assert_module_map(runtime: &mut JsRuntime, modules: &Vec) { - let module_map = runtime.module_map.borrow(); - assert_eq!(module_map.handles.len(), modules.len()); - assert_eq!(module_map.info.len(), modules.len()); - assert_eq!( - module_map.by_name(AssertedModuleType::Json).len() - + module_map - .by_name(AssertedModuleType::JavaScriptOrWasm) - .len(), - modules.len() - ); - - assert_eq!(module_map.next_load_id, (modules.len() + 1) as ModuleLoadId); - - for info in modules { - assert!(module_map.handles.get(info.id).is_some()); - assert_eq!(module_map.info.get(info.id).unwrap(), info); - assert_eq!( - module_map - .by_name(AssertedModuleType::JavaScriptOrWasm) - .get(&info.name) - .unwrap(), - &SymbolicModule::Mod(info.id) - ); - } - } - - #[op] - fn op_test() -> Result { - Ok(String::from("test")) - } - - let loader = Rc::new(ModsLoader::default()); - let mut runtime = JsRuntimeForSnapshot::new( - RuntimeOptions { - module_loader: Some(loader.clone()), - extensions: vec![Extension::builder("text_ext") - .ops(vec![op_test::decl()]) - .build()], - ..Default::default() - }, - Default::default(), - ); - - let specifier = crate::resolve_url("file:///0.js").unwrap(); - let source_code = - ascii_str!(r#"export function f0() { return "hello world" }"#); - let id = futures::executor::block_on( - runtime.load_side_module(&specifier, Some(source_code)), - ) - .unwrap(); - - #[allow(clippy::let_underscore_future)] - let _ = runtime.mod_evaluate(id); - futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); - - let mut modules = vec![]; - modules.push(ModuleInfo { - id, - main: false, - name: specifier.into(), - requests: vec![], - module_type: ModuleType::JavaScript, - }); - - modules.extend((1..200).map(|i| create_module(&mut runtime, i, false))); - - assert_module_map(&mut runtime, &modules); - - let snapshot = runtime.snapshot(); - - let mut runtime2 = JsRuntimeForSnapshot::new( - RuntimeOptions { - module_loader: Some(loader.clone()), - startup_snapshot: Some(Snapshot::JustCreated(snapshot)), - extensions: vec![Extension::builder("text_ext") - .ops(vec![op_test::decl()]) - .build()], - ..Default::default() - }, - Default::default(), - ); - - assert_module_map(&mut runtime2, &modules); - - modules.extend((200..400).map(|i| create_module(&mut runtime2, i, false))); - modules.push(create_module(&mut runtime2, 400, true)); - - assert_module_map(&mut runtime2, &modules); - - let snapshot2 = runtime2.snapshot(); - - let mut runtime3 = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader), - startup_snapshot: Some(Snapshot::JustCreated(snapshot2)), - extensions: vec![Extension::builder("text_ext") - .ops(vec![op_test::decl()]) - .build()], - ..Default::default() - }); - - assert_module_map(&mut runtime3, &modules); - - let source_code = r#"(async () => { - const mod = await import("file:///400.js"); - return mod.f400() + " " + Deno.core.ops.op_test(); - })();"#; - let val = runtime3.execute_script_static(".", source_code).unwrap(); - let val = futures::executor::block_on(runtime3.resolve_value(val)).unwrap(); - { - let scope = &mut runtime3.handle_scope(); - let value = v8::Local::new(scope, val); - let str_ = value.to_string(scope).unwrap().to_rust_string_lossy(scope); - assert_eq!(str_, "hello world test"); - } - } - - #[test] - fn test_error_without_stack() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - // SyntaxError - let result = runtime.execute_script_static( - "error_without_stack.js", - r#" -function main() { - console.log("asdf); -} -main(); -"#, - ); - let expected_error = r#"Uncaught SyntaxError: Invalid or unexpected token - at error_without_stack.js:3:15"#; - assert_eq!(result.unwrap_err().to_string(), expected_error); - } - - #[test] - fn test_error_stack() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - let result = runtime.execute_script_static( - "error_stack.js", - r#" -function assert(cond) { - if (!cond) { - throw Error("assert"); - } -} -function main() { - assert(false); -} -main(); - "#, - ); - let expected_error = r#"Error: assert - at assert (error_stack.js:4:11) - at main (error_stack.js:8:3) - at error_stack.js:10:1"#; - assert_eq!(result.unwrap_err().to_string(), expected_error); - } - - #[tokio::test] - async fn test_error_async_stack() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - poll_fn(move |cx| { - runtime - .execute_script_static( - "error_async_stack.js", - r#" -(async () => { - const p = (async () => { - await Promise.resolve().then(() => { - throw new Error("async"); - }); - })(); - try { - await p; - } catch (error) { - console.log(error.stack); - throw error; - } -})();"#, - ) - .unwrap(); - let expected_error = r#"Error: async - at error_async_stack.js:5:13 - at async error_async_stack.js:4:5 - at async error_async_stack.js:9:5"#; - - match runtime.poll_event_loop(cx, false) { - Poll::Ready(Err(e)) => { - assert_eq!(e.to_string(), expected_error); - } - _ => panic!(), - }; - Poll::Ready(()) - }) - .await; - } - - #[tokio::test] - async fn test_error_context() { - use anyhow::anyhow; - - #[op] - fn op_err_sync() -> Result<(), Error> { - Err(anyhow!("original sync error").context("higher-level sync error")) - } - - #[op] - async fn op_err_async() -> Result<(), Error> { - Err(anyhow!("original async error").context("higher-level async error")) - } - - deno_core::extension!(test_ext, ops = [op_err_sync, op_err_async]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - poll_fn(move |cx| { - runtime - .execute_script_static( - "test_error_context_sync.js", - r#" -let errMessage; -try { - Deno.core.ops.op_err_sync(); -} catch (err) { - errMessage = err.message; -} -if (errMessage !== "higher-level sync error: original sync error") { - throw new Error("unexpected error message from op_err_sync: " + errMessage); -} -"#, - ) - .unwrap(); - - let promise = runtime - .execute_script_static( - "test_error_context_async.js", - r#" - -(async () => { - let errMessage; - try { - await Deno.core.opAsync("op_err_async"); - } catch (err) { - errMessage = err.message; - } - if (errMessage !== "higher-level async error: original async error") { - throw new Error("unexpected error message from op_err_async: " + errMessage); - } -})() -"#, - ) - .unwrap(); - - match runtime.poll_value(&promise, cx) { - Poll::Ready(Ok(_)) => {} - Poll::Ready(Err(err)) => panic!("{err:?}"), - _ => panic!(), - } - Poll::Ready(()) - }).await; - } - - #[tokio::test] - async fn test_pump_message_loop() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - poll_fn(move |cx| { - runtime - .execute_script_static( - "pump_message_loop.js", - r#" -function assertEquals(a, b) { - if (a === b) return; - throw a + " does not equal " + b; -} -const sab = new SharedArrayBuffer(16); -const i32a = new Int32Array(sab); -globalThis.resolved = false; -(function() { - const result = Atomics.waitAsync(i32a, 0, 0); - result.value.then( - (value) => { assertEquals("ok", value); globalThis.resolved = true; }, - () => { assertUnreachable(); - }); -})(); -const notify_return_value = Atomics.notify(i32a, 0, 1); -assertEquals(1, notify_return_value); -"#, - ) - .unwrap(); - - match runtime.poll_event_loop(cx, false) { - Poll::Ready(Ok(())) => {} - _ => panic!(), - }; - - // noop script, will resolve promise from first script - runtime - .execute_script_static( - "pump_message_loop2.js", - r#"assertEquals(1, 1);"#, - ) - .unwrap(); - - // check that promise from `Atomics.waitAsync` has been resolved - runtime - .execute_script_static( - "pump_message_loop3.js", - r#"assertEquals(globalThis.resolved, true);"#, - ) - .unwrap(); - Poll::Ready(()) - }) - .await; - } - - #[test] - fn test_v8_platform() { - let options = RuntimeOptions { - v8_platform: Some(v8::new_default_platform(0, false).make_shared()), - ..Default::default() - }; - let mut runtime = JsRuntime::new(options); - runtime.execute_script_static("", "").unwrap(); - } - - #[ignore] // TODO(@littledivy): Fast API ops when snapshot is not loaded. - #[test] - fn test_is_proxy() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - let all_true: v8::Global = runtime - .execute_script_static( - "is_proxy.js", - r#" - (function () { - const o = { a: 1, b: 2}; - const p = new Proxy(o, {}); - return Deno.core.ops.op_is_proxy(p) && !Deno.core.ops.op_is_proxy(o) && !Deno.core.ops.op_is_proxy(42); - })() - "#, - ) - .unwrap(); - let mut scope = runtime.handle_scope(); - let all_true = v8::Local::::new(&mut scope, &all_true); - assert!(all_true.is_true()); - } - - #[tokio::test] - async fn test_async_opstate_borrow() { - struct InnerState(u64); - - #[op] - async fn op_async_borrow( - op_state: Rc>, - ) -> Result<(), Error> { - let n = { - let op_state = op_state.borrow(); - let inner_state = op_state.borrow::(); - inner_state.0 - }; - // Future must be Poll::Pending on first call - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - if n != 42 { - unreachable!(); - } - Ok(()) - } - - deno_core::extension!( - test_ext, - ops = [op_async_borrow], - state = |state| state.put(InnerState(42)) - ); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "op_async_borrow.js", - "Deno.core.opAsync(\"op_async_borrow\")", - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap(); - } - - #[tokio::test] - async fn test_sync_op_serialize_object_with_numbers_as_keys() { - #[op] - fn op_sync_serialize_object_with_numbers_as_keys( - value: serde_json::Value, - ) -> Result<(), Error> { - assert_eq!( - value.to_string(), - r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"# - ); - Ok(()) - } - - deno_core::extension!( - test_ext, - ops = [op_sync_serialize_object_with_numbers_as_keys] - ); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "op_sync_serialize_object_with_numbers_as_keys.js", - r#" -Deno.core.ops.op_sync_serialize_object_with_numbers_as_keys({ - lines: { - 100: { - unit: "m" - }, - 200: { - unit: "cm" - } - } -}) -"#, - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap(); - } - - #[tokio::test] - async fn test_async_op_serialize_object_with_numbers_as_keys() { - #[op] - async fn op_async_serialize_object_with_numbers_as_keys( - value: serde_json::Value, - ) -> Result<(), Error> { - assert_eq!( - value.to_string(), - r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"# - ); - Ok(()) - } - - deno_core::extension!( - test_ext, - ops = [op_async_serialize_object_with_numbers_as_keys] - ); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "op_async_serialize_object_with_numbers_as_keys.js", - r#" - -Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", { - lines: { - 100: { - unit: "m" - }, - 200: { - unit: "cm" - } - } -}) -"#, - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap(); - } - - #[tokio::test] - async fn test_set_macrotask_callback_set_next_tick_callback() { - #[op] - async fn op_async_sleep() -> Result<(), Error> { - // Future must be Poll::Pending on first call - tokio::time::sleep(std::time::Duration::from_millis(1)).await; - Ok(()) - } - - deno_core::extension!(test_ext, ops = [op_async_sleep]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "macrotasks_and_nextticks.js", - r#" - - (async function () { - const results = []; - Deno.core.setMacrotaskCallback(() => { - results.push("macrotask"); - return true; - }); - Deno.core.setNextTickCallback(() => { - results.push("nextTick"); - Deno.core.ops.op_set_has_tick_scheduled(false); - }); - Deno.core.ops.op_set_has_tick_scheduled(true); - await Deno.core.opAsync('op_async_sleep'); - if (results[0] != "nextTick") { - throw new Error(`expected nextTick, got: ${results[0]}`); - } - if (results[1] != "macrotask") { - throw new Error(`expected macrotask, got: ${results[1]}`); - } - })(); - "#, - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap(); - } - - #[test] - fn test_has_tick_scheduled() { - use futures::task::ArcWake; - - static MACROTASK: AtomicUsize = AtomicUsize::new(0); - static NEXT_TICK: AtomicUsize = AtomicUsize::new(0); - - #[op] - fn op_macrotask() -> Result<(), AnyError> { - MACROTASK.fetch_add(1, Ordering::Relaxed); - Ok(()) - } - - #[op] - fn op_next_tick() -> Result<(), AnyError> { - NEXT_TICK.fetch_add(1, Ordering::Relaxed); - Ok(()) - } - - deno_core::extension!(test_ext, ops = [op_macrotask, op_next_tick]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "has_tick_scheduled.js", - r#" - Deno.core.setMacrotaskCallback(() => { - Deno.core.ops.op_macrotask(); - return true; // We're done. - }); - Deno.core.setNextTickCallback(() => Deno.core.ops.op_next_tick()); - Deno.core.ops.op_set_has_tick_scheduled(true); - "#, - ) - .unwrap(); - - struct ArcWakeImpl(Arc); - impl ArcWake for ArcWakeImpl { - fn wake_by_ref(arc_self: &Arc) { - arc_self.0.fetch_add(1, Ordering::Relaxed); - } - } - - let awoken_times = Arc::new(AtomicUsize::new(0)); - let waker = - futures::task::waker(Arc::new(ArcWakeImpl(awoken_times.clone()))); - let cx = &mut Context::from_waker(&waker); - - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - assert_eq!(1, MACROTASK.load(Ordering::Relaxed)); - assert_eq!(1, NEXT_TICK.load(Ordering::Relaxed)); - assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); - - runtime.inner.state.borrow_mut().has_tick_scheduled = false; - assert!(matches!( - runtime.poll_event_loop(cx, false), - Poll::Ready(Ok(())) - )); - assert_eq!(awoken_times.load(Ordering::Relaxed), 0); - assert!(matches!( - runtime.poll_event_loop(cx, false), - Poll::Ready(Ok(())) - )); - assert_eq!(awoken_times.load(Ordering::Relaxed), 0); - } - - #[test] - fn terminate_during_module_eval() { - #[derive(Default)] - struct ModsLoader; - - impl ModuleLoader for ModsLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _kind: ResolutionKind, - ) -> Result { - assert_eq!(specifier, "file:///main.js"); - assert_eq!(referrer, "."); - let s = crate::resolve_import(specifier, referrer).unwrap(); - Ok(s) - } - - fn load( - &self, - _module_specifier: &ModuleSpecifier, - _maybe_referrer: Option<&ModuleSpecifier>, - _is_dyn_import: bool, - ) -> Pin> { - async move { - Ok(ModuleSource::for_test( - "console.log('hello world');", - "file:///main.js", - )) - } - .boxed_local() - } - } - - let loader = std::rc::Rc::new(ModsLoader::default()); - let mut runtime = JsRuntime::new(RuntimeOptions { - module_loader: Some(loader), - ..Default::default() - }); - - let specifier = crate::resolve_url("file:///main.js").unwrap(); - let source_code = ascii_str!("Deno.core.print('hello\\n')"); - - let module_id = futures::executor::block_on( - runtime.load_main_module(&specifier, Some(source_code)), - ) - .unwrap(); - - runtime.v8_isolate().terminate_execution(); - - let mod_result = - futures::executor::block_on(runtime.mod_evaluate(module_id)).unwrap(); - assert!(mod_result - .unwrap_err() - .to_string() - .contains("JavaScript execution has been terminated")); - } - - #[tokio::test] - async fn test_unhandled_rejection_order() { - let mut runtime = JsRuntime::new(Default::default()); - runtime - .execute_script_static( - "", - r#" - for (let i = 0; i < 100; i++) { - Promise.reject(i); - } - "#, - ) - .unwrap(); - let err = runtime.run_event_loop(false).await.unwrap_err(); - assert_eq!(err.to_string(), "Uncaught (in promise) 0"); - } - - #[tokio::test] - async fn test_set_promise_reject_callback() { - static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0); - - #[op] - fn op_promise_reject() -> Result<(), AnyError> { - PROMISE_REJECT.fetch_add(1, Ordering::Relaxed); - Ok(()) - } - - deno_core::extension!(test_ext, ops = [op_promise_reject]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "promise_reject_callback.js", - r#" - // Note: |promise| is not the promise created below, it's a child. - Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => { - if (type !== /* PromiseRejectWithNoHandler */ 0) { - throw Error("unexpected type: " + type); - } - if (reason.message !== "reject") { - throw Error("unexpected reason: " + reason); - } - Deno.core.ops.op_store_pending_promise_rejection(promise); - Deno.core.ops.op_promise_reject(); - }); - new Promise((_, reject) => reject(Error("reject"))); - "#, - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap_err(); - - assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed)); - - runtime - .execute_script_static( - "promise_reject_callback.js", - r#" - { - const prev = Deno.core.ops.op_set_promise_reject_callback((...args) => { - prev(...args); - }); - } - new Promise((_, reject) => reject(Error("reject"))); - "#, - ) - .unwrap(); - runtime.run_event_loop(false).await.unwrap_err(); - - assert_eq!(2, PROMISE_REJECT.load(Ordering::Relaxed)); - } - - #[tokio::test] - async fn test_set_promise_reject_callback_realms() { - let mut runtime = JsRuntime::new(RuntimeOptions::default()); - let global_realm = runtime.global_realm(); - let realm1 = runtime.create_realm().unwrap(); - let realm2 = runtime.create_realm().unwrap(); - - let realm_expectations = &[ - (&global_realm, "global_realm", 42), - (&realm1, "realm1", 140), - (&realm2, "realm2", 720), - ]; - - // Set up promise reject callbacks. - for (realm, realm_name, number) in realm_expectations { - realm - .execute_script( - runtime.v8_isolate(), - "", - format!( - r#" - - globalThis.rejectValue = undefined; - Deno.core.setPromiseRejectCallback((_type, _promise, reason) => {{ - globalThis.rejectValue = `{realm_name}/${{reason}}`; - }}); - Deno.core.opAsync("op_void_async").then(() => Promise.reject({number})); - "# - ).into() - ) - .unwrap(); - } - - runtime.run_event_loop(false).await.unwrap(); - - for (realm, realm_name, number) in realm_expectations { - let reject_value = realm - .execute_script_static( - runtime.v8_isolate(), - "", - "globalThis.rejectValue", - ) - .unwrap(); - let scope = &mut realm.handle_scope(runtime.v8_isolate()); - let reject_value = v8::Local::new(scope, reject_value); - assert!(reject_value.is_string()); - let reject_value_string = reject_value.to_rust_string_lossy(scope); - assert_eq!(reject_value_string, format!("{realm_name}/{number}")); - } - } - - #[tokio::test] - async fn test_set_promise_reject_callback_top_level_await() { - static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0); - - #[op] - fn op_promise_reject() -> Result<(), AnyError> { - PROMISE_REJECT.fetch_add(1, Ordering::Relaxed); - Ok(()) - } - - deno_core::extension!(test_ext, ops = [op_promise_reject]); - - #[derive(Default)] - struct ModsLoader; - - impl ModuleLoader for ModsLoader { - fn resolve( - &self, - specifier: &str, - referrer: &str, - _kind: ResolutionKind, - ) -> Result { - assert_eq!(specifier, "file:///main.js"); - assert_eq!(referrer, "."); - let s = crate::resolve_import(specifier, referrer).unwrap(); - Ok(s) - } - - fn load( - &self, - _module_specifier: &ModuleSpecifier, - _maybe_referrer: Option<&ModuleSpecifier>, - _is_dyn_import: bool, - ) -> Pin> { - let code = r#" - Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => { - Deno.core.ops.op_promise_reject(); - }); - throw new Error('top level throw'); - "#; - - async move { Ok(ModuleSource::for_test(code, "file:///main.js")) } - .boxed_local() - } - } - - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - module_loader: Some(Rc::new(ModsLoader)), - ..Default::default() - }); - - let id = runtime - .load_main_module(&crate::resolve_url("file:///main.js").unwrap(), None) - .await - .unwrap(); - let receiver = runtime.mod_evaluate(id); - runtime.run_event_loop(false).await.unwrap(); - receiver.await.unwrap().unwrap_err(); - - assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed)); - } - - #[test] - fn test_op_return_serde_v8_error() { - #[op] - fn op_err() -> Result, anyhow::Error> { - Ok([(1, 2), (3, 4)].into_iter().collect()) // Maps can't have non-string keys in serde_v8 - } - - deno_core::extension!(test_ext, ops = [op_err]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - assert!(runtime - .execute_script_static( - "test_op_return_serde_v8_error.js", - "Deno.core.ops.op_err()" - ) - .is_err()); - } - - #[test] - fn test_op_high_arity() { - #[op] - fn op_add_4( - x1: i64, - x2: i64, - x3: i64, - x4: i64, - ) -> Result { - Ok(x1 + x2 + x3 + x4) - } - - deno_core::extension!(test_ext, ops = [op_add_4]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - let r = runtime - .execute_script_static("test.js", "Deno.core.ops.op_add_4(1, 2, 3, 4)") - .unwrap(); - let scope = &mut runtime.handle_scope(); - assert_eq!(r.open(scope).integer_value(scope), Some(10)); - } - - #[test] - fn test_op_disabled() { - #[op] - fn op_foo() -> Result { - Ok(42) - } - - fn ops() -> Vec { - vec![op_foo::decl().disable()] - } - - deno_core::extension!(test_ext, ops_fn = ops); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - let err = runtime - .execute_script_static("test.js", "Deno.core.ops.op_foo()") - .unwrap_err(); - assert!(err - .to_string() - .contains("TypeError: Deno.core.ops.op_foo is not a function")); - } - - #[test] - fn test_op_detached_buffer() { - use serde_v8::DetachedBuffer; - - #[op] - fn op_sum_take(b: DetachedBuffer) -> Result { - Ok(b.as_ref().iter().clone().map(|x| *x as u64).sum()) - } - - #[op] - fn op_boomerang( - b: DetachedBuffer, - ) -> Result { - Ok(b) - } - - deno_core::extension!(test_ext, ops = [op_sum_take, op_boomerang]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - runtime - .execute_script_static( - "test.js", - r#" - const a1 = new Uint8Array([1,2,3]); - const a1b = a1.subarray(0, 3); - const a2 = new Uint8Array([5,10,15]); - const a2b = a2.subarray(0, 3); - if (!(a1.length > 0 && a1b.length > 0)) { - throw new Error("a1 & a1b should have a length"); - } - let sum = Deno.core.ops.op_sum_take(a1b); - if (sum !== 6) { - throw new Error(`Bad sum: ${sum}`); - } - if (a1.length > 0 || a1b.length > 0) { - throw new Error("expecting a1 & a1b to be detached"); - } - const a3 = Deno.core.ops.op_boomerang(a2b); - if (a3.byteLength != 3) { - throw new Error(`Expected a3.byteLength === 3, got ${a3.byteLength}`); - } - if (a3[0] !== 5 || a3[1] !== 10) { - throw new Error(`Invalid a3: ${a3[0]}, ${a3[1]}`); - } - if (a2.byteLength > 0 || a2b.byteLength > 0) { - throw new Error("expecting a2 & a2b to be detached, a3 re-attached"); - } - const wmem = new WebAssembly.Memory({ initial: 1, maximum: 2 }); - const w32 = new Uint32Array(wmem.buffer); - w32[0] = 1; w32[1] = 2; w32[2] = 3; - const assertWasmThrow = (() => { - try { - let sum = Deno.core.ops.op_sum_take(w32.subarray(0, 2)); - return false; - } catch(e) { - return e.message.includes('invalid type; expected: detachable'); - } - }); - if (!assertWasmThrow()) { - throw new Error("expected wasm mem to not be detachable"); - } - "#, - ) - .unwrap(); - } - - #[test] - fn test_op_unstable_disabling() { - #[op] - fn op_foo() -> Result { - Ok(42) - } - - #[op(unstable)] - fn op_bar() -> Result { - Ok(42) - } - - deno_core::extension!( - test_ext, - ops = [op_foo, op_bar], - middleware = |op| if op.is_unstable { op.disable() } else { op } - ); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - runtime - .execute_script_static( - "test.js", - r#" - if (Deno.core.ops.op_foo() !== 42) { - throw new Error("Exptected op_foo() === 42"); - } - if (typeof Deno.core.ops.op_bar !== "undefined") { - throw new Error("Expected op_bar to be disabled") - } - "#, - ) - .unwrap(); - } - - #[test] - fn js_realm_simple() { - let mut runtime = JsRuntime::new(Default::default()); - let main_context = runtime.global_context(); - let main_global = { - let scope = &mut runtime.handle_scope(); - let local_global = main_context.open(scope).global(scope); - v8::Global::new(scope, local_global) - }; - - let realm = runtime.create_realm().unwrap(); - assert_ne!(realm.context(), &main_context); - assert_ne!(realm.global_object(runtime.v8_isolate()), main_global); - - let main_object = runtime.execute_script_static("", "Object").unwrap(); - let realm_object = realm - .execute_script_static(runtime.v8_isolate(), "", "Object") - .unwrap(); - assert_ne!(main_object, realm_object); - } - - #[test] - fn js_realm_init() { - #[op] - fn op_test() -> Result { - Ok(String::from("Test")) - } - - deno_core::extension!(test_ext, ops = [op_test]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - let realm = runtime.create_realm().unwrap(); - let ret = realm - .execute_script_static( - runtime.v8_isolate(), - "", - "Deno.core.ops.op_test()", - ) - .unwrap(); - - let scope = &mut realm.handle_scope(runtime.v8_isolate()); - assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap()); - } - - #[test] - fn js_realm_init_snapshot() { - let snapshot = { - let runtime = - JsRuntimeForSnapshot::new(Default::default(), Default::default()); - let snap: &[u8] = &runtime.snapshot(); - Vec::from(snap).into_boxed_slice() - }; - - #[op] - fn op_test() -> Result { - Ok(String::from("Test")) - } - - deno_core::extension!(test_ext, ops = [op_test]); - let mut runtime = JsRuntime::new(RuntimeOptions { - startup_snapshot: Some(Snapshot::Boxed(snapshot)), - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - let realm = runtime.create_realm().unwrap(); - let ret = realm - .execute_script_static( - runtime.v8_isolate(), - "", - "Deno.core.ops.op_test()", - ) - .unwrap(); - - let scope = &mut realm.handle_scope(runtime.v8_isolate()); - assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap()); - } - - #[test] - fn js_realm_sync_ops() { - // Test that returning a ZeroCopyBuf and throwing an exception from a sync - // op result in objects with prototypes from the right realm. Note that we - // don't test the result of returning structs, because they will be - // serialized to objects with null prototype. - - #[op] - fn op_test(fail: bool) -> Result { - if !fail { - Ok(ZeroCopyBuf::empty()) - } else { - Err(crate::error::type_error("Test")) - } - } - - deno_core::extension!(test_ext, ops = [op_test]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - get_error_class_fn: Some(&|error| { - crate::error::get_custom_error_class(error).unwrap() - }), - ..Default::default() - }); - let new_realm = runtime.create_realm().unwrap(); - - // Test in both realms - for realm in [runtime.global_realm(), new_realm].into_iter() { - let ret = realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - const buf = Deno.core.ops.op_test(false); - try { - Deno.core.ops.op_test(true); - } catch(e) { - err = e; - } - buf instanceof Uint8Array && buf.byteLength === 0 && - err instanceof TypeError && err.message === "Test" - "#, - ) - .unwrap(); - assert!(ret.open(runtime.v8_isolate()).is_true()); - } - } - - #[tokio::test] - async fn js_realm_async_ops() { - // Test that returning a ZeroCopyBuf and throwing an exception from a async - // op result in objects with prototypes from the right realm. Note that we - // don't test the result of returning structs, because they will be - // serialized to objects with null prototype. - - #[op] - async fn op_test(fail: bool) -> Result { - if !fail { - Ok(ZeroCopyBuf::empty()) - } else { - Err(crate::error::type_error("Test")) - } - } - - deno_core::extension!(test_ext, ops = [op_test]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - get_error_class_fn: Some(&|error| { - crate::error::get_custom_error_class(error).unwrap() - }), - ..Default::default() - }); - - let global_realm = runtime.global_realm(); - let new_realm = runtime.create_realm().unwrap(); - - let mut rets = vec![]; - - // Test in both realms - for realm in [global_realm, new_realm].into_iter() { - let ret = realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - - (async function () { - const buf = await Deno.core.opAsync("op_test", false); - let err; - try { - await Deno.core.opAsync("op_test", true); - } catch(e) { - err = e; - } - return buf instanceof Uint8Array && buf.byteLength === 0 && - err instanceof TypeError && err.message === "Test" ; - })(); - "#, - ) - .unwrap(); - rets.push((realm, ret)); - } - - runtime.run_event_loop(false).await.unwrap(); - - for ret in rets { - let scope = &mut ret.0.handle_scope(runtime.v8_isolate()); - let value = v8::Local::new(scope, ret.1); - let promise = v8::Local::::try_from(value).unwrap(); - let result = promise.result(scope); - - assert!(result.is_boolean() && result.is_true()); - } - } - - #[ignore] - #[tokio::test] - async fn js_realm_gc() { - static INVOKE_COUNT: AtomicUsize = AtomicUsize::new(0); - struct PendingFuture {} - - impl Future for PendingFuture { - type Output = (); - fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> { - Poll::Pending - } - } - - impl Drop for PendingFuture { - fn drop(&mut self) { - assert_eq!(INVOKE_COUNT.fetch_sub(1, Ordering::SeqCst), 1); - } - } - - // Never resolves. - #[op] - async fn op_pending() { - assert_eq!(INVOKE_COUNT.fetch_add(1, Ordering::SeqCst), 0); - PendingFuture {}.await - } - - deno_core::extension!(test_ext, ops = [op_pending]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - // Detect a drop in OpState - let opstate_drop_detect = Rc::new(()); - runtime - .op_state() - .borrow_mut() - .put(opstate_drop_detect.clone()); - assert_eq!(Rc::strong_count(&opstate_drop_detect), 2); - - let other_realm = runtime.create_realm().unwrap(); - other_realm - .execute_script( - runtime.v8_isolate(), - "future", - ModuleCode::from_static("Deno.core.opAsync('op_pending')"), - ) - .unwrap(); - while INVOKE_COUNT.load(Ordering::SeqCst) == 0 { - poll_fn(|cx: &mut Context| runtime.poll_event_loop(cx, false)) - .await - .unwrap(); - } - drop(other_realm); - while INVOKE_COUNT.load(Ordering::SeqCst) == 1 { - poll_fn(|cx| runtime.poll_event_loop(cx, false)) - .await - .unwrap(); - } - drop(runtime); - - // Make sure the OpState was dropped properly when the runtime dropped - assert_eq!(Rc::strong_count(&opstate_drop_detect), 1); - } - - #[tokio::test] - async fn js_realm_ref_unref_ops() { - // Never resolves. - #[op] - async fn op_pending() { - futures::future::pending().await - } - - deno_core::extension!(test_ext, ops = [op_pending]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - poll_fn(move |cx| { - let main_realm = runtime.global_realm(); - let other_realm = runtime.create_realm().unwrap(); - - main_realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - - var promise = Deno.core.opAsync("op_pending"); - "#, - ) - .unwrap(); - other_realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - - var promise = Deno.core.opAsync("op_pending"); - "#, - ) - .unwrap(); - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - - main_realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); - Deno.core.unrefOp(promise[promiseIdSymbol]); - "#, - ) - .unwrap(); - assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); - - other_realm - .execute_script_static( - runtime.v8_isolate(), - "", - r#" - let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); - Deno.core.unrefOp(promise[promiseIdSymbol]); - "#, - ) - .unwrap(); - assert!(matches!( - runtime.poll_event_loop(cx, false), - Poll::Ready(Ok(())) - )); - Poll::Ready(()) - }) - .await; - } - - #[test] - fn test_array_by_copy() { - // Verify that "array by copy" proposal is enabled (https://github.com/tc39/proposal-change-array-by-copy) - let mut runtime = JsRuntime::new(Default::default()); - assert!(runtime - .execute_script_static( - "test_array_by_copy.js", - "const a = [1, 2, 3]; - const b = a.toReversed(); - if (!(a[0] === 1 && a[1] === 2 && a[2] === 3)) { - throw new Error('Expected a to be intact'); - } - if (!(b[0] === 3 && b[1] === 2 && b[2] === 1)) { - throw new Error('Expected b to be reversed'); - }", - ) - .is_ok()); - } - - #[cfg(debug_assertions)] - #[test] - #[should_panic(expected = "Found ops with duplicate names:")] - fn duplicate_op_names() { - mod a { - use super::*; - - #[op] - fn op_test() -> Result { - Ok(String::from("Test")) - } - } - - #[op] - fn op_test() -> Result { - Ok(String::from("Test")) - } - - deno_core::extension!(test_ext, ops = [a::op_test, op_test]); - JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - } - - #[test] - fn ops_in_js_have_proper_names() { - #[op] - fn op_test_sync() -> Result { - Ok(String::from("Test")) - } - - #[op] - async fn op_test_async() -> Result { - Ok(String::from("Test")) - } - - deno_core::extension!(test_ext, ops = [op_test_sync, op_test_async]); - let mut runtime = JsRuntime::new(RuntimeOptions { - extensions: vec![test_ext::init_ops()], - ..Default::default() - }); - - let src = r#" - if (Deno.core.ops.op_test_sync.name !== "op_test_sync") { - throw new Error(); - } - - if (Deno.core.ops.op_test_async.name !== "op_test_async") { - throw new Error(); - } - - const { op_test_async } = Deno.core.ensureFastOps(); - if (op_test_async.name !== "op_test_async") { - throw new Error(); - } - "#; - runtime.execute_script_static("test", src).unwrap(); - } -} diff --git a/core/runtime/bindings.js b/core/runtime/bindings.js new file mode 100644 index 000000000..21d27a2c3 --- /dev/null +++ b/core/runtime/bindings.js @@ -0,0 +1,51 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +if (!globalThis.Deno) { + globalThis.Deno = { + core: { + ops: {}, + asyncOps: {}, + }, + }; +} + +Deno.__op__console = function (callConsole, console) { + Deno.core.callConsole = callConsole; + Deno.core.console = console; +}; + +Deno.__op__registerOp = function (isAsync, op, opName) { + const core = Deno.core; + if (isAsync) { + if (core.ops[opName] !== undefined) { + return; + } + core.asyncOps[opName] = op; + const fn = function (...args) { + if (this !== core.ops) { + // deno-lint-ignore prefer-primordials + throw new Error( + "An async stub cannot be separated from Deno.core.ops. Use ???", + ); + } + return core.asyncStub(opName, args); + }; + fn.name = opName; + core.ops[opName] = fn; + } else { + core.ops[opName] = op; + } +}; + +Deno.__op__unregisterOp = function (isAsync, opName) { + if (isAsync) { + delete Deno.core.asyncOps[opName]; + } + delete Deno.core.ops[opName]; +}; + +Deno.__op__cleanup = function () { + delete Deno.__op__console; + delete Deno.__op__registerOp; + delete Deno.__op__unregisterOp; + delete Deno.__op__cleanup; +}; diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs new file mode 100644 index 000000000..4cc27592f --- /dev/null +++ b/core/runtime/bindings.rs @@ -0,0 +1,598 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use log::debug; +use std::fmt::Write; +use std::option::Option; +use std::os::raw::c_void; +use v8::MapFnTo; + +use crate::error::is_instance_of_error; +use crate::error::JsStackFrame; +use crate::modules::get_asserted_module_type_from_assertions; +use crate::modules::parse_import_assertions; +use crate::modules::validate_import_assertions; +use crate::modules::ImportAssertionsKind; +use crate::modules::ModuleMap; +use crate::modules::ResolutionKind; +use crate::ops::OpCtx; +use crate::runtime::InitMode; +use crate::JsRealm; +use crate::JsRuntime; + +pub(crate) fn external_references(ops: &[OpCtx]) -> v8::ExternalReferences { + // Overallocate a bit, it's better than having to resize the vector. + let mut references = Vec::with_capacity(4 + ops.len() * 4); + + references.push(v8::ExternalReference { + function: call_console.map_fn_to(), + }); + references.push(v8::ExternalReference { + function: import_meta_resolve.map_fn_to(), + }); + references.push(v8::ExternalReference { + function: catch_dynamic_import_promise_error.map_fn_to(), + }); + references.push(v8::ExternalReference { + function: empty_fn.map_fn_to(), + }); + + for ctx in ops { + let ctx_ptr = ctx as *const OpCtx as _; + references.push(v8::ExternalReference { pointer: ctx_ptr }); + references.push(v8::ExternalReference { + function: ctx.decl.v8_fn_ptr, + }); + if let Some(fast_fn) = &ctx.decl.fast_fn { + references.push(v8::ExternalReference { + pointer: fast_fn.function as _, + }); + references.push(v8::ExternalReference { + pointer: ctx.fast_fn_c_info.unwrap().as_ptr() as _, + }); + } + } + + let refs = v8::ExternalReferences::new(&references); + // Leak, V8 takes ownership of the references. + std::mem::forget(references); + refs +} + +// TODO(nayeemrmn): Move to runtime and/or make `pub(crate)`. +pub fn script_origin<'a>( + s: &mut v8::HandleScope<'a>, + resource_name: v8::Local<'a, v8::String>, +) -> v8::ScriptOrigin<'a> { + let source_map_url = v8::String::empty(s); + v8::ScriptOrigin::new( + s, + resource_name.into(), + 0, + 0, + false, + 123, + source_map_url.into(), + true, + false, + false, + ) +} + +fn get<'s, T>( + scope: &mut v8::HandleScope<'s>, + from: v8::Local, + key: &'static [u8], + path: &'static str, +) -> T +where + v8::Local<'s, v8::Value>: TryInto, +{ + let key = v8::String::new_external_onebyte_static(scope, key).unwrap(); + from + .get(scope, key.into()) + .unwrap_or_else(|| panic!("{path} exists")) + .try_into() + .unwrap_or_else(|_| panic!("unable to convert")) +} + +pub(crate) fn initialize_context<'s>( + scope: &mut v8::HandleScope<'s>, + context: v8::Local<'s, v8::Context>, + op_ctxs: &[OpCtx], + init_mode: InitMode, +) -> v8::Local<'s, v8::Context> { + let global = context.global(scope); + + let mut codegen = String::with_capacity(op_ctxs.len() * 200); + codegen.push_str(include_str!("bindings.js")); + _ = writeln!( + codegen, + "Deno.__op__ = function(opFns, callConsole, console) {{" + ); + if init_mode == InitMode::New { + _ = writeln!(codegen, "Deno.__op__console(callConsole, console);"); + } + for op_ctx in op_ctxs { + if op_ctx.decl.enabled { + _ = writeln!( + codegen, + "Deno.__op__registerOp({}, opFns[{}], \"{}\");", + op_ctx.decl.is_async, op_ctx.id, op_ctx.decl.name + ); + } else { + _ = writeln!( + codegen, + "Deno.__op__unregisterOp({}, \"{}\");", + op_ctx.decl.is_async, op_ctx.decl.name + ); + } + } + codegen.push_str("Deno.__op__cleanup();"); + _ = writeln!(codegen, "}}"); + + let script = v8::String::new_from_one_byte( + scope, + codegen.as_bytes(), + v8::NewStringType::Normal, + ) + .unwrap(); + let script = v8::Script::compile(scope, script, None).unwrap(); + script.run(scope); + + let deno = get(scope, global, b"Deno", "Deno"); + let op_fn: v8::Local = + get(scope, deno, b"__op__", "Deno.__op__"); + let recv = v8::undefined(scope); + let op_fns = v8::Array::new(scope, op_ctxs.len() as i32); + for op_ctx in op_ctxs { + let op_fn = op_ctx_function(scope, op_ctx); + op_fns.set_index(scope, op_ctx.id as u32, op_fn.into()); + } + if init_mode == InitMode::FromSnapshot { + op_fn.call(scope, recv.into(), &[op_fns.into()]); + } else { + // Bind functions to Deno.core.* + let call_console_fn = v8::Function::new(scope, call_console).unwrap(); + + // Bind v8 console object to Deno.core.console + let extra_binding_obj = context.get_extras_binding_object(scope); + let console_obj: v8::Local = get( + scope, + extra_binding_obj, + b"console", + "ExtrasBindingObject.console", + ); + + op_fn.call( + scope, + recv.into(), + &[op_fns.into(), call_console_fn.into(), console_obj.into()], + ); + } + + context +} + +fn op_ctx_function<'s>( + scope: &mut v8::HandleScope<'s>, + op_ctx: &OpCtx, +) -> v8::Local<'s, v8::Function> { + let op_ctx_ptr = op_ctx as *const OpCtx as *const c_void; + let external = v8::External::new(scope, op_ctx_ptr as *mut c_void); + let v8name = + v8::String::new_external_onebyte_static(scope, op_ctx.decl.name.as_bytes()) + .unwrap(); + let builder: v8::FunctionBuilder = + v8::FunctionTemplate::builder_raw(op_ctx.decl.v8_fn_ptr) + .data(external.into()) + .length(op_ctx.decl.arg_count as i32); + + let template = if let Some(fast_function) = &op_ctx.decl.fast_fn { + builder.build_fast( + scope, + fast_function, + Some(op_ctx.fast_fn_c_info.unwrap().as_ptr()), + None, + None, + ) + } else { + builder.build(scope) + }; + + let v8fn = template.get_function(scope).unwrap(); + v8fn.set_name(v8name); + v8fn +} + +pub extern "C" fn wasm_async_resolve_promise_callback( + _isolate: *mut v8::Isolate, + context: v8::Local, + resolver: v8::Local, + compilation_result: v8::Local, + success: v8::WasmAsyncSuccess, +) { + // SAFETY: `CallbackScope` can be safely constructed from `Local` + let scope = &mut unsafe { v8::CallbackScope::new(context) }; + if success == v8::WasmAsyncSuccess::Success { + resolver.resolve(scope, compilation_result).unwrap(); + } else { + resolver.reject(scope, compilation_result).unwrap(); + } +} + +pub fn host_import_module_dynamically_callback<'s>( + scope: &mut v8::HandleScope<'s>, + _host_defined_options: v8::Local<'s, v8::Data>, + resource_name: v8::Local<'s, v8::Value>, + specifier: v8::Local<'s, v8::String>, + import_assertions: v8::Local<'s, v8::FixedArray>, +) -> Option> { + // NOTE(bartlomieju): will crash for non-UTF-8 specifier + let specifier_str = specifier + .to_string(scope) + .unwrap() + .to_rust_string_lossy(scope); + let referrer_name_str = resource_name + .to_string(scope) + .unwrap() + .to_rust_string_lossy(scope); + + let resolver = v8::PromiseResolver::new(scope).unwrap(); + let promise = resolver.get_promise(scope); + + let assertions = parse_import_assertions( + scope, + import_assertions, + ImportAssertionsKind::DynamicImport, + ); + + { + let tc_scope = &mut v8::TryCatch::new(scope); + validate_import_assertions(tc_scope, &assertions); + if tc_scope.has_caught() { + let e = tc_scope.exception().unwrap(); + resolver.reject(tc_scope, e); + } + } + let asserted_module_type = + get_asserted_module_type_from_assertions(&assertions); + + let resolver_handle = v8::Global::new(scope, resolver); + { + let state_rc = JsRuntime::state_from(scope); + let module_map_rc = JsRuntime::module_map_from(scope); + + debug!( + "dyn_import specifier {} referrer {} ", + specifier_str, referrer_name_str + ); + ModuleMap::load_dynamic_import( + module_map_rc, + &specifier_str, + &referrer_name_str, + asserted_module_type, + resolver_handle, + ); + state_rc.borrow_mut().notify_new_dynamic_import(); + } + // Map errors from module resolution (not JS errors from module execution) to + // ones rethrown from this scope, so they include the call stack of the + // dynamic import site. Error objects without any stack frames are assumed to + // be module resolution errors, other exception values are left as they are. + let builder = v8::FunctionBuilder::new(catch_dynamic_import_promise_error); + + let map_err = + v8::FunctionBuilder::::build(builder, scope).unwrap(); + + let promise = promise.catch(scope, map_err).unwrap(); + + Some(promise) +} + +pub extern "C" fn host_initialize_import_meta_object_callback( + context: v8::Local, + module: v8::Local, + meta: v8::Local, +) { + // SAFETY: `CallbackScope` can be safely constructed from `Local` + let scope = &mut unsafe { v8::CallbackScope::new(context) }; + let module_map_rc = JsRuntime::module_map_from(scope); + let module_map = module_map_rc.borrow(); + + let module_global = v8::Global::new(scope, module); + let info = module_map + .get_info(&module_global) + .expect("Module not found"); + + let url_key = v8::String::new_external_onebyte_static(scope, b"url").unwrap(); + let url_val = info.name.v8(scope); + meta.create_data_property(scope, url_key.into(), url_val.into()); + + let main_key = + v8::String::new_external_onebyte_static(scope, b"main").unwrap(); + let main_val = v8::Boolean::new(scope, info.main); + meta.create_data_property(scope, main_key.into(), main_val.into()); + + let builder = + v8::FunctionBuilder::new(import_meta_resolve).data(url_val.into()); + let val = v8::FunctionBuilder::::build(builder, scope).unwrap(); + let resolve_key = + v8::String::new_external_onebyte_static(scope, b"resolve").unwrap(); + meta.set(scope, resolve_key.into(), val.into()); +} + +fn import_meta_resolve( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, +) { + if args.length() > 1 { + return throw_type_error(scope, "Invalid arguments"); + } + + let maybe_arg_str = args.get(0).to_string(scope); + if maybe_arg_str.is_none() { + return throw_type_error(scope, "Invalid arguments"); + } + let specifier = maybe_arg_str.unwrap(); + let referrer = { + let url_prop = args.data(); + url_prop.to_rust_string_lossy(scope) + }; + let module_map_rc = JsRuntime::module_map_from(scope); + let loader = module_map_rc.borrow().loader.clone(); + let specifier_str = specifier.to_rust_string_lossy(scope); + + if specifier_str.starts_with("npm:") { + throw_type_error(scope, "\"npm:\" specifiers are currently not supported in import.meta.resolve()"); + return; + } + + match loader.resolve(&specifier_str, &referrer, ResolutionKind::DynamicImport) + { + Ok(resolved) => { + let resolved_val = serde_v8::to_v8(scope, resolved.as_str()).unwrap(); + rv.set(resolved_val); + } + Err(err) => { + throw_type_error(scope, &err.to_string()); + } + }; +} + +fn empty_fn( + _scope: &mut v8::HandleScope, + _args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + //Do Nothing +} + +//It creates a reference to an empty function which can be mantained after the snapshots +pub fn create_empty_fn<'s>( + scope: &mut v8::HandleScope<'s>, +) -> Option> { + let empty_fn = v8::FunctionTemplate::new(scope, empty_fn); + empty_fn.get_function(scope) +} + +fn catch_dynamic_import_promise_error( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + let arg = args.get(0); + if is_instance_of_error(scope, arg) { + let e: crate::error::NativeJsError = serde_v8::from_v8(scope, arg).unwrap(); + let name = e.name.unwrap_or_else(|| "Error".to_string()); + let msg = v8::Exception::create_message(scope, arg); + if msg.get_stack_trace(scope).unwrap().get_frame_count() == 0 { + let arg: v8::Local = arg.try_into().unwrap(); + let message_key = + v8::String::new_external_onebyte_static(scope, b"message").unwrap(); + let message = arg.get(scope, message_key.into()).unwrap(); + let mut message: v8::Local = message.try_into().unwrap(); + if let Some(stack_frame) = JsStackFrame::from_v8_message(scope, msg) { + if let Some(location) = stack_frame.maybe_format_location() { + let str = + format!("{} at {location}", message.to_rust_string_lossy(scope)); + message = v8::String::new(scope, &str).unwrap(); + } + } + let exception = match name.as_str() { + "RangeError" => v8::Exception::range_error(scope, message), + "TypeError" => v8::Exception::type_error(scope, message), + "SyntaxError" => v8::Exception::syntax_error(scope, message), + "ReferenceError" => v8::Exception::reference_error(scope, message), + _ => v8::Exception::error(scope, message), + }; + let code_key = + v8::String::new_external_onebyte_static(scope, b"code").unwrap(); + let code_value = + v8::String::new_external_onebyte_static(scope, b"ERR_MODULE_NOT_FOUND") + .unwrap(); + let exception_obj = exception.to_object(scope).unwrap(); + exception_obj.set(scope, code_key.into(), code_value.into()); + scope.throw_exception(exception); + return; + } + } + scope.throw_exception(arg); +} + +pub extern "C" fn promise_reject_callback(message: v8::PromiseRejectMessage) { + use v8::PromiseRejectEvent::*; + + // SAFETY: `CallbackScope` can be safely constructed from `&PromiseRejectMessage` + let scope = &mut unsafe { v8::CallbackScope::new(&message) }; + + let context_state_rc = JsRealm::state_from_scope(scope); + let mut context_state = context_state_rc.borrow_mut(); + + if let Some(js_promise_reject_cb) = context_state.js_promise_reject_cb.clone() + { + drop(context_state); + + let tc_scope = &mut v8::TryCatch::new(scope); + let undefined: v8::Local = v8::undefined(tc_scope).into(); + let type_ = v8::Integer::new(tc_scope, message.get_event() as i32); + let promise = message.get_promise(); + + let reason = match message.get_event() { + PromiseRejectWithNoHandler + | PromiseRejectAfterResolved + | PromiseResolveAfterResolved => message.get_value().unwrap_or(undefined), + PromiseHandlerAddedAfterReject => undefined, + }; + + let promise_global = v8::Global::new(tc_scope, promise); + let args = &[type_.into(), promise.into(), reason]; + let maybe_has_unhandled_rejection_handler = js_promise_reject_cb + .open(tc_scope) + .call(tc_scope, undefined, args); + + let has_unhandled_rejection_handler = + if let Some(value) = maybe_has_unhandled_rejection_handler { + value.is_true() + } else { + false + }; + + if has_unhandled_rejection_handler { + let state_rc = JsRuntime::state_from(tc_scope); + let mut state = state_rc.borrow_mut(); + if let Some(pending_mod_evaluate) = state.pending_mod_evaluate.as_mut() { + if !pending_mod_evaluate.has_evaluated { + pending_mod_evaluate + .handled_promise_rejections + .push(promise_global); + } + } + } + } else { + let promise = message.get_promise(); + let promise_global = v8::Global::new(scope, promise); + match message.get_event() { + PromiseRejectWithNoHandler => { + let error = message.get_value().unwrap(); + let error_global = v8::Global::new(scope, error); + context_state + .pending_promise_rejections + .push_back((promise_global, error_global)); + } + PromiseHandlerAddedAfterReject => { + context_state + .pending_promise_rejections + .retain(|(key, _)| key != &promise_global); + } + PromiseRejectAfterResolved => {} + PromiseResolveAfterResolved => { + // Should not warn. See #1272 + } + } + } +} + +/// This binding should be used if there's a custom console implementation +/// available. Using it will make sure that proper stack frames are displayed +/// in the inspector console. +/// +/// Each method on console object should be bound to this function, eg: +/// ```ignore +/// function wrapConsole(consoleFromDeno, consoleFromV8) { +/// const callConsole = core.callConsole; +/// +/// for (const key of Object.keys(consoleFromV8)) { +/// if (consoleFromDeno.hasOwnProperty(key)) { +/// consoleFromDeno[key] = callConsole.bind( +/// consoleFromDeno, +/// consoleFromV8[key], +/// consoleFromDeno[key], +/// ); +/// } +/// } +/// } +/// ``` +/// +/// Inspired by: +/// https://github.com/nodejs/node/blob/1317252dfe8824fd9cfee125d2aaa94004db2f3b/src/inspector_js_api.cc#L194-L222 +fn call_console( + scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + _rv: v8::ReturnValue, +) { + if args.length() < 2 + || !args.get(0).is_function() + || !args.get(1).is_function() + { + return throw_type_error(scope, "Invalid arguments"); + } + + let mut call_args = vec![]; + for i in 2..args.length() { + call_args.push(args.get(i)); + } + + let receiver = args.this(); + let inspector_console_method = + v8::Local::::try_from(args.get(0)).unwrap(); + let deno_console_method = + v8::Local::::try_from(args.get(1)).unwrap(); + + inspector_console_method.call(scope, receiver.into(), &call_args); + deno_console_method.call(scope, receiver.into(), &call_args); +} + +/// Called by V8 during `JsRuntime::instantiate_module`. +/// +/// This function borrows `ModuleMap` from the isolate slot, +/// so it is crucial to ensure there are no existing borrows +/// of `ModuleMap` when `JsRuntime::instantiate_module` is called. +pub fn module_resolve_callback<'s>( + context: v8::Local<'s, v8::Context>, + specifier: v8::Local<'s, v8::String>, + import_assertions: v8::Local<'s, v8::FixedArray>, + referrer: v8::Local<'s, v8::Module>, +) -> Option> { + // SAFETY: `CallbackScope` can be safely constructed from `Local` + let scope = &mut unsafe { v8::CallbackScope::new(context) }; + + let module_map_rc = JsRuntime::module_map_from(scope); + let module_map = module_map_rc.borrow(); + + let referrer_global = v8::Global::new(scope, referrer); + + let referrer_info = module_map + .get_info(&referrer_global) + .expect("ModuleInfo not found"); + let referrer_name = referrer_info.name.as_str(); + + let specifier_str = specifier.to_rust_string_lossy(scope); + + let assertions = parse_import_assertions( + scope, + import_assertions, + ImportAssertionsKind::StaticImport, + ); + let maybe_module = module_map.resolve_callback( + scope, + &specifier_str, + referrer_name, + assertions, + ); + if let Some(module) = maybe_module { + return Some(module); + } + + let msg = format!( + r#"Cannot resolve module "{specifier_str}" from "{referrer_name}""# + ); + throw_type_error(scope, msg); + None +} + +pub fn throw_type_error(scope: &mut v8::HandleScope, message: impl AsRef) { + let message = v8::String::new(scope, message.as_ref()).unwrap(); + let exception = v8::Exception::type_error(scope, message); + scope.throw_exception(exception); +} diff --git a/core/runtime/encode_decode_test.js b/core/runtime/encode_decode_test.js new file mode 100644 index 000000000..0f4668765 --- /dev/null +++ b/core/runtime/encode_decode_test.js @@ -0,0 +1,69 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +"use strict"; +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} + +function assertArrayEquals(a1, a2) { + if (a1.length !== a2.length) throw Error("assert"); + + for (const index in a1) { + if (a1[index] !== a2[index]) { + throw Error("assert"); + } + } +} + +function main() { + // deno-fmt-ignore + const fixture1 = [ + 0xf0, 0x9d, 0x93, 0xbd, + 0xf0, 0x9d, 0x93, 0xae, + 0xf0, 0x9d, 0x94, 0x81, + 0xf0, 0x9d, 0x93, 0xbd + ]; + // deno-fmt-ignore + const fixture2 = [ + 72, 101, 108, 108, + 111, 32, 239, 191, + 189, 239, 191, 189, + 32, 87, 111, 114, + 108, 100 + ]; + + const empty = Deno.core.ops.op_encode(""); + if (empty.length !== 0) throw new Error("assert"); + + assertArrayEquals( + Array.from(Deno.core.ops.op_encode("𝓽𝓮𝔁𝓽")), + fixture1, + ); + assertArrayEquals( + Array.from(Deno.core.ops.op_encode("Hello \udc12\ud834 World")), + fixture2, + ); + + const emptyBuf = Deno.core.ops.op_decode(new Uint8Array(0)); + if (emptyBuf !== "") throw new Error("assert"); + + assert(Deno.core.ops.op_decode(new Uint8Array(fixture1)) === "𝓽𝓮𝔁𝓽"); + assert( + Deno.core.ops.op_decode(new Uint8Array(fixture2)) === + "Hello �� World", + ); + + // See https://github.com/denoland/deno/issues/6649 + let thrown = false; + try { + Deno.core.ops.op_decode(new Uint8Array(2 ** 29)); + } catch (e) { + thrown = true; + assert(e instanceof RangeError); + assert(e.message === "string too long"); + } + assert(thrown); +} + +main(); diff --git a/core/runtime/error_builder_test.js b/core/runtime/error_builder_test.js new file mode 100644 index 000000000..f442819cb --- /dev/null +++ b/core/runtime/error_builder_test.js @@ -0,0 +1,32 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +const { core } = Deno; +const { ops } = core; + +class DOMException { + constructor(message, code) { + this.msg = message; + this.code = code; + } +} + +core.registerErrorBuilder( + "DOMExceptionOperationError", + function DOMExceptionOperationError(msg) { + return new DOMException(msg, "OperationError"); + }, +); + +try { + ops.op_err(); + throw new Error("op_err didn't throw!"); +} catch (err) { + if (!(err instanceof DOMException)) { + throw new Error("err not DOMException"); + } + if (err.msg !== "abc") { + throw new Error("err.message is incorrect"); + } + if (err.code !== "OperationError") { + throw new Error("err.code is incorrect"); + } +} diff --git a/core/runtime/icudtl.dat b/core/runtime/icudtl.dat new file mode 100644 index 000000000..d1f10917a Binary files /dev/null and b/core/runtime/icudtl.dat differ diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs new file mode 100644 index 000000000..970b3f5d5 --- /dev/null +++ b/core/runtime/jsrealm.rs @@ -0,0 +1,367 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use super::bindings; +use crate::error::exception_to_err_result; +use crate::modules::ModuleCode; +use crate::ops::OpCtx; +use crate::runtime::JsRuntimeState; +use crate::task::MaskResultAsSend; +use crate::JsRuntime; +use crate::OpId; +use crate::OpResult; +use crate::PromiseId; +use anyhow::Error; +use std::cell::RefCell; +use std::collections::HashSet; +use std::collections::VecDeque; +use std::hash::BuildHasherDefault; +use std::hash::Hasher; +use std::option::Option; +use std::rc::Rc; +use tokio::task::JoinSet; +use v8::HandleScope; +use v8::Local; + +// Hasher used for `unrefed_ops`. Since these are rolling i32, there's no +// need to actually hash them. +#[derive(Default)] +pub(crate) struct IdentityHasher(u64); + +impl Hasher for IdentityHasher { + fn write_i32(&mut self, i: i32) { + self.0 = i as u64; + } + + fn finish(&self) -> u64 { + self.0 + } + + fn write(&mut self, _bytes: &[u8]) { + unreachable!() + } +} + +#[derive(Default)] +pub(crate) struct ContextState { + pub(crate) js_event_loop_tick_cb: Option>>, + pub(crate) js_build_custom_error_cb: Option>>, + pub(crate) js_promise_reject_cb: Option>>, + pub(crate) js_format_exception_cb: Option>>, + pub(crate) js_wasm_streaming_cb: Option>>, + pub(crate) pending_promise_rejections: + VecDeque<(v8::Global, v8::Global)>, + pub(crate) unrefed_ops: HashSet>, + pub(crate) pending_ops: + JoinSet>, + // We don't explicitly re-read this prop but need the slice to live alongside + // the context + pub(crate) op_ctxs: Box<[OpCtx]>, + pub(crate) isolate: Option<*mut v8::OwnedIsolate>, +} + +/// A representation of a JavaScript realm tied to a [`JsRuntime`], that allows +/// execution in the realm's context. +/// +/// A [`JsRealm`] instance is a reference to an already existing realm, which +/// does not hold ownership of it, so instances can be created and dropped as +/// needed. As such, calling [`JsRealm::new`] doesn't create a new realm, and +/// cloning a [`JsRealm`] only creates a new reference. See +/// [`JsRuntime::create_realm`] to create new realms instead. +/// +/// Despite [`JsRealm`] instances being references, multiple instances that +/// point to the same realm won't overlap because every operation requires +/// passing a mutable reference to the [`v8::Isolate`]. Therefore, no operation +/// on two [`JsRealm`] instances tied to the same isolate can be run at the same +/// time, regardless of whether they point to the same realm. +/// +/// # Panics +/// +/// Every method of [`JsRealm`] will panic if you call it with a reference to a +/// [`v8::Isolate`] other than the one that corresponds to the current context. +/// +/// In other words, the [`v8::Isolate`] parameter for all the related [`JsRealm`] methods +/// must be extracted from the pre-existing [`JsRuntime`]. +/// +/// Example usage with the [`JsRealm::execute_script`] method: +/// ``` +/// use deno_core::JsRuntime; +/// use deno_core::RuntimeOptions; +/// +/// let mut runtime = JsRuntime::new(RuntimeOptions::default()); +/// let new_realm = runtime +/// .create_realm() +/// .expect("Handle the error properly"); +/// let source_code = "var a = 0; a + 1"; +/// let result = new_realm +/// .execute_script_static(runtime.v8_isolate(), "", source_code) +/// .expect("Handle the error properly"); +/// # drop(result); +/// ``` +/// +/// # Lifetime of the realm +/// +/// As long as the corresponding isolate is alive, a [`JsRealm`] instance will +/// keep the underlying V8 context alive even if it would have otherwise been +/// garbage collected. +#[derive(Clone)] +#[repr(transparent)] +pub struct JsRealm(pub(crate) JsRealmInner); + +#[derive(Clone)] +pub(crate) struct JsRealmInner { + context_state: Rc>, + context: Rc>, + runtime_state: Rc>, + is_global: bool, +} + +impl JsRealmInner { + pub(crate) fn new( + context_state: Rc>, + context: v8::Global, + runtime_state: Rc>, + is_global: bool, + ) -> Self { + Self { + context_state, + context: context.into(), + runtime_state, + is_global, + } + } + + pub fn num_pending_ops(&self) -> usize { + self.context_state.borrow().pending_ops.len() + } + + pub fn num_unrefed_ops(&self) -> usize { + self.context_state.borrow().unrefed_ops.len() + } + + #[inline(always)] + pub fn context(&self) -> &v8::Global { + &self.context + } + + #[inline(always)] + pub(crate) fn state(&self) -> Rc> { + self.context_state.clone() + } + + /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. + #[inline(always)] + pub fn handle_scope<'s>( + &self, + isolate: &'s mut v8::Isolate, + ) -> v8::HandleScope<'s> { + v8::HandleScope::with_context(isolate, &*self.context) + } + + pub(crate) fn check_promise_rejections( + &self, + scope: &mut v8::HandleScope, + ) -> Result<(), Error> { + let Some((_, handle)) = self.context_state.borrow_mut().pending_promise_rejections.pop_front() else { + return Ok(()); + }; + + let exception = v8::Local::new(scope, handle); + let state_rc = JsRuntime::state_from(scope); + let state = state_rc.borrow(); + if let Some(inspector) = &state.inspector { + let inspector = inspector.borrow(); + inspector.exception_thrown(scope, exception, true); + if inspector.has_blocking_sessions() { + return Ok(()); + } + } + exception_to_err_result(scope, exception, true) + } + + pub(crate) fn is_same(&self, other: &Rc>) -> bool { + Rc::ptr_eq(&self.context, other) + } + + pub fn destroy(self) { + let state = self.state(); + let raw_ptr = self.state().borrow().isolate.unwrap(); + // SAFETY: We know the isolate outlives the realm + let isolate = unsafe { raw_ptr.as_mut().unwrap() }; + let mut realm_state = state.borrow_mut(); + // These globals will prevent snapshots from completing, take them + std::mem::take(&mut realm_state.js_event_loop_tick_cb); + std::mem::take(&mut realm_state.js_build_custom_error_cb); + std::mem::take(&mut realm_state.js_promise_reject_cb); + std::mem::take(&mut realm_state.js_format_exception_cb); + std::mem::take(&mut realm_state.js_wasm_streaming_cb); + // The OpCtx slice may contain a circular reference + std::mem::take(&mut realm_state.op_ctxs); + + self.context().open(isolate).clear_all_slots(isolate); + + // Expect that this context is dead (we only check this in debug mode) + // TODO(mmastrac): This check fails for some tests, will need to fix this + // debug_assert_eq!(Rc::strong_count(&self.context), 1, "Realm was still alive when we wanted to destory it. Not dropped?"); + } +} + +impl JsRealm { + pub(crate) fn new(inner: JsRealmInner) -> Self { + Self(inner) + } + + #[inline(always)] + pub(crate) fn state_from_scope( + scope: &mut v8::HandleScope, + ) -> Rc> { + let context = scope.get_current_context(); + context + .get_slot::>>(scope) + .unwrap() + .clone() + } + + #[inline(always)] + pub fn num_pending_ops(&self) -> usize { + self.0.num_pending_ops() + } + + #[inline(always)] + pub fn num_unrefed_ops(&self) -> usize { + self.0.num_unrefed_ops() + } + + /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. + #[inline(always)] + pub fn handle_scope<'s>( + &self, + isolate: &'s mut v8::Isolate, + ) -> v8::HandleScope<'s> { + self.0.handle_scope(isolate) + } + + #[inline(always)] + pub fn context(&self) -> &v8::Global { + self.0.context() + } + + /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. + pub fn global_object<'s>( + &self, + isolate: &'s mut v8::Isolate, + ) -> v8::Local<'s, v8::Object> { + let scope = &mut self.0.handle_scope(isolate); + self.0.context.open(scope).global(scope) + } + + fn string_from_code<'a>( + scope: &mut HandleScope<'a>, + code: &ModuleCode, + ) -> Option> { + if let Some(code) = code.try_static_ascii() { + v8::String::new_external_onebyte_static(scope, code) + } else { + v8::String::new_from_utf8( + scope, + code.as_bytes(), + v8::NewStringType::Normal, + ) + } + } + + /// Executes traditional JavaScript code (traditional = not ES modules) in the + /// realm's context. + /// + /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. + /// + /// The `name` parameter can be a filepath or any other string. E.g.: + /// + /// - "/some/file/path.js" + /// - "" + /// - "[native code]" + /// + /// The same `name` value can be used for multiple executions. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn execute_script_static( + &self, + isolate: &mut v8::Isolate, + name: &'static str, + source_code: &'static str, + ) -> Result, Error> { + self.execute_script(isolate, name, ModuleCode::from_static(source_code)) + } + + /// Executes traditional JavaScript code (traditional = not ES modules) in the + /// realm's context. + /// + /// For info on the [`v8::Isolate`] parameter, check [`JsRealm#panics`]. + /// + /// The `name` parameter can be a filepath or any other string. E.g.: + /// + /// - "/some/file/path.js" + /// - "" + /// - "[native code]" + /// + /// The same `name` value can be used for multiple executions. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn execute_script( + &self, + isolate: &mut v8::Isolate, + name: &'static str, + source_code: ModuleCode, + ) -> Result, Error> { + let scope = &mut self.0.handle_scope(isolate); + + let source = Self::string_from_code(scope, &source_code).unwrap(); + debug_assert!(name.is_ascii()); + let name = + v8::String::new_external_onebyte_static(scope, name.as_bytes()).unwrap(); + let origin = bindings::script_origin(scope, name); + + let tc_scope = &mut v8::TryCatch::new(scope); + + let script = match v8::Script::compile(tc_scope, source, Some(&origin)) { + Some(script) => script, + None => { + let exception = tc_scope.exception().unwrap(); + return exception_to_err_result(tc_scope, exception, false); + } + }; + + match script.run(tc_scope) { + Some(value) => { + let value_handle = v8::Global::new(tc_scope, value); + Ok(value_handle) + } + None => { + assert!(tc_scope.has_caught()); + let exception = tc_scope.exception().unwrap(); + exception_to_err_result(tc_scope, exception, false) + } + } + } + + // TODO(andreubotella): `mod_evaluate`, `load_main_module`, `load_side_module` +} + +impl Drop for JsRealm { + fn drop(&mut self) { + // Don't do anything special with the global realm + if self.0.is_global { + return; + } + + // There's us and there's the runtime + if Rc::strong_count(&self.0.context) == 2 { + self + .0 + .runtime_state + .borrow_mut() + .remove_realm(&self.0.context); + assert_eq!(Rc::strong_count(&self.0.context), 1); + self.0.clone().destroy(); + assert_eq!(Rc::strong_count(&self.0.context_state), 1); + } + } +} diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs new file mode 100644 index 000000000..3b41a90f1 --- /dev/null +++ b/core/runtime/jsruntime.rs @@ -0,0 +1,2416 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use super::bindings; +use super::jsrealm::JsRealmInner; +use super::snapshot_util; +use crate::error::exception_to_err_result; +use crate::error::generic_error; +use crate::error::to_v8_type_error; +use crate::error::GetErrorClassFn; +use crate::error::JsError; +use crate::extensions::OpDecl; +use crate::extensions::OpEventLoopFn; +use crate::inspector::JsRuntimeInspector; +use crate::module_specifier::ModuleSpecifier; +use crate::modules::AssertedModuleType; +use crate::modules::ExtModuleLoader; +use crate::modules::ExtModuleLoaderCb; +use crate::modules::ModuleCode; +use crate::modules::ModuleError; +use crate::modules::ModuleId; +use crate::modules::ModuleLoadId; +use crate::modules::ModuleLoader; +use crate::modules::ModuleMap; +use crate::modules::ModuleName; +use crate::ops::*; +use crate::runtime::ContextState; +use crate::runtime::JsRealm; +use crate::source_map::SourceMapCache; +use crate::source_map::SourceMapGetter; +use crate::Extension; +use crate::ModuleType; +use crate::NoopModuleLoader; +use crate::OpMiddlewareFn; +use crate::OpResult; +use crate::OpState; +use crate::V8_WRAPPER_OBJECT_INDEX; +use crate::V8_WRAPPER_TYPE_INDEX; +use anyhow::Context as AnyhowContext; +use anyhow::Error; +use futures::channel::oneshot; +use futures::future::poll_fn; +use futures::future::Future; +use futures::stream::StreamExt; +use smallvec::SmallVec; +use std::any::Any; +use std::cell::RefCell; +use std::collections::HashMap; +use std::ffi::c_void; +use std::mem::ManuallyDrop; +use std::ops::Deref; +use std::ops::DerefMut; +use std::option::Option; +use std::rc::Rc; +use std::sync::atomic::AtomicBool; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::sync::Mutex; +use std::sync::Once; +use std::task::Context; +use std::task::Poll; + +const STATE_DATA_OFFSET: u32 = 0; +const MODULE_MAP_DATA_OFFSET: u32 = 1; + +pub enum Snapshot { + Static(&'static [u8]), + JustCreated(v8::StartupData), + Boxed(Box<[u8]>), +} + +/// Objects that need to live as long as the isolate +#[derive(Default)] +pub(crate) struct IsolateAllocations { + pub(crate) near_heap_limit_callback_data: + Option<(Box>, v8::NearHeapLimitCallback)>, +} + +/// ManuallyDrop> is clone, but it returns a ManuallyDrop> which is a massive +/// memory-leak footgun. +pub(crate) struct ManuallyDropRc(ManuallyDrop>); + +impl ManuallyDropRc { + pub fn clone(&self) -> Rc { + self.0.deref().clone() + } +} + +impl Deref for ManuallyDropRc { + type Target = Rc; + fn deref(&self) -> &Self::Target { + self.0.deref() + } +} + +impl DerefMut for ManuallyDropRc { + fn deref_mut(&mut self) -> &mut Self::Target { + self.0.deref_mut() + } +} + +/// This struct contains the [`JsRuntimeState`] and [`v8::OwnedIsolate`] that are required +/// to do an orderly shutdown of V8. We keep these in a separate struct to allow us to control +/// the destruction more closely, as snapshots require the isolate to be destroyed by the +/// snapshot process, not the destructor. +/// +/// The way rusty_v8 works w/snapshots is that the [`v8::OwnedIsolate`] gets consumed by a +/// [`v8::snapshot::SnapshotCreator`] that is stored in its annex. It's a bit awkward, because this +/// means we cannot let it drop (because we don't have it after a snapshot). On top of that, we have +/// to consume it in the snapshot creator because otherwise it panics. +/// +/// This inner struct allows us to let the outer JsRuntime drop normally without a Drop impl, while we +/// control dropping more closely here using ManuallyDrop. +pub(crate) struct InnerIsolateState { + will_snapshot: bool, + pub(crate) state: ManuallyDropRc>, + v8_isolate: ManuallyDrop, +} + +impl InnerIsolateState { + /// Clean out the opstate and take the inspector to prevent the inspector from getting destroyed + /// after we've torn down the contexts. If the inspector is not correctly torn down, random crashes + /// happen in tests (and possibly for users using the inspector). + pub fn prepare_for_cleanup(&mut self) { + let mut state = self.state.borrow_mut(); + let inspector = state.inspector.take(); + state.op_state.borrow_mut().clear(); + if let Some(inspector) = inspector { + assert_eq!( + Rc::strong_count(&inspector), + 1, + "The inspector must be dropped before the runtime" + ); + } + } + + pub fn cleanup(&mut self) { + self.prepare_for_cleanup(); + + let state_ptr = self.v8_isolate.get_data(STATE_DATA_OFFSET); + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + _ = unsafe { Rc::from_raw(state_ptr as *const RefCell) }; + + let module_map_ptr = self.v8_isolate.get_data(MODULE_MAP_DATA_OFFSET); + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + _ = unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; + + self.state.borrow_mut().destroy_all_realms(); + + debug_assert_eq!(Rc::strong_count(&self.state), 1); + } + + pub fn prepare_for_snapshot(mut self) -> v8::OwnedIsolate { + self.cleanup(); + // SAFETY: We're copying out of self and then immediately forgetting self + let (state, isolate) = unsafe { + ( + ManuallyDrop::take(&mut self.state.0), + ManuallyDrop::take(&mut self.v8_isolate), + ) + }; + std::mem::forget(self); + drop(state); + isolate + } +} + +impl Drop for InnerIsolateState { + fn drop(&mut self) { + self.cleanup(); + // SAFETY: We gotta drop these + unsafe { + ManuallyDrop::drop(&mut self.state.0); + if self.will_snapshot { + // Create the snapshot and just drop it. + eprintln!("WARNING: v8::OwnedIsolate for snapshot was leaked"); + } else { + ManuallyDrop::drop(&mut self.v8_isolate); + } + } + } +} + +#[derive(Copy, Clone, Debug, Eq, PartialEq)] +pub(crate) enum InitMode { + /// We have no snapshot -- this is a pristine context. + New, + /// We are using a snapshot, thus certain initialization steps are skipped. + FromSnapshot, +} + +impl InitMode { + fn from_options(options: &RuntimeOptions) -> Self { + match options.startup_snapshot { + None => Self::New, + Some(_) => Self::FromSnapshot, + } + } +} + +/// A single execution context of JavaScript. Corresponds roughly to the "Web +/// Worker" concept in the DOM. +//// +/// The JsRuntime future completes when there is an error or when all +/// pending ops have completed. +/// +/// Use [`JsRuntimeForSnapshot`] to be able to create a snapshot. +pub struct JsRuntime { + pub(crate) inner: InnerIsolateState, + pub(crate) module_map: Rc>, + pub(crate) allocations: IsolateAllocations, + extensions: Vec, + event_loop_middlewares: Vec>, + init_mode: InitMode, + // Marks if this is considered the top-level runtime. Used only be inspector. + is_main: bool, +} + +/// The runtime type used for snapshot creation. +pub struct JsRuntimeForSnapshot(JsRuntime); + +impl Deref for JsRuntimeForSnapshot { + type Target = JsRuntime; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for JsRuntimeForSnapshot { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +pub(crate) struct DynImportModEvaluate { + load_id: ModuleLoadId, + module_id: ModuleId, + promise: v8::Global, + module: v8::Global, +} + +pub(crate) struct ModEvaluate { + pub(crate) promise: Option>, + pub(crate) has_evaluated: bool, + pub(crate) handled_promise_rejections: Vec>, + sender: oneshot::Sender>, +} + +pub struct CrossIsolateStore(Arc>>); + +struct CrossIsolateStoreInner { + map: HashMap, + last_id: u32, +} + +impl CrossIsolateStore { + pub(crate) fn insert(&self, value: T) -> u32 { + let mut store = self.0.lock().unwrap(); + let last_id = store.last_id; + store.map.insert(last_id, value); + store.last_id += 1; + last_id + } + + pub(crate) fn take(&self, id: u32) -> Option { + let mut store = self.0.lock().unwrap(); + store.map.remove(&id) + } +} + +impl Default for CrossIsolateStore { + fn default() -> Self { + CrossIsolateStore(Arc::new(Mutex::new(CrossIsolateStoreInner { + map: Default::default(), + last_id: 0, + }))) + } +} + +impl Clone for CrossIsolateStore { + fn clone(&self) -> Self { + Self(self.0.clone()) + } +} + +pub type SharedArrayBufferStore = + CrossIsolateStore>; + +pub type CompiledWasmModuleStore = CrossIsolateStore; + +/// Internal state for JsRuntime which is stored in one of v8::Isolate's +/// embedder slots. +pub struct JsRuntimeState { + global_realm: Option, + known_realms: Vec, + pub(crate) has_tick_scheduled: bool, + pub(crate) pending_dyn_mod_evaluate: Vec, + pub(crate) pending_mod_evaluate: Option, + /// A counter used to delay our dynamic import deadlock detection by one spin + /// of the event loop. + dyn_module_evaluate_idle_counter: u32, + pub(crate) source_map_getter: Option>>, + pub(crate) source_map_cache: Rc>, + pub(crate) op_state: Rc>, + pub(crate) shared_array_buffer_store: Option, + pub(crate) compiled_wasm_module_store: Option, + /// The error that was passed to an `op_dispatch_exception` call. + /// It will be retrieved by `exception_to_err_result` and used as an error + /// instead of any other exceptions. + // TODO(nayeemrmn): This is polled in `exception_to_err_result()` which is + // flimsy. Try to poll it similarly to `pending_promise_rejections`. + pub(crate) dispatched_exception: Option>, + pub(crate) inspector: Option>>, +} + +impl JsRuntimeState { + pub(crate) fn destroy_all_realms(&mut self) { + self.global_realm.take(); + for realm in self.known_realms.drain(..) { + realm.destroy() + } + } + + pub(crate) fn remove_realm( + &mut self, + realm_context: &Rc>, + ) { + self + .known_realms + .retain(|realm| !realm.is_same(realm_context)); + } +} + +fn v8_init( + v8_platform: Option>, + predictable: bool, +) { + // Include 10MB ICU data file. + #[repr(C, align(16))] + struct IcuData([u8; 10541264]); + static ICU_DATA: IcuData = IcuData(*include_bytes!("icudtl.dat")); + v8::icu::set_common_data_72(&ICU_DATA.0).unwrap(); + + let flags = concat!( + " --wasm-test-streaming", + " --harmony-import-assertions", + " --no-validate-asm", + " --turbo_fast_api_calls", + " --harmony-change-array-by-copy", + ); + + if predictable { + v8::V8::set_flags_from_string(&format!( + "{}{}", + flags, " --predictable --random-seed=42" + )); + } else { + v8::V8::set_flags_from_string(flags); + } + + let v8_platform = v8_platform + .unwrap_or_else(|| v8::new_default_platform(0, false).make_shared()); + v8::V8::initialize_platform(v8_platform); + v8::V8::initialize(); +} + +#[derive(Default)] +pub struct RuntimeOptions { + /// Source map reference for errors. + pub source_map_getter: Option>, + + /// Allows to map error type to a string "class" used to represent + /// error in JavaScript. + pub get_error_class_fn: Option, + + /// Implementation of `ModuleLoader` which will be + /// called when V8 requests to load ES modules. + /// + /// If not provided runtime will error if code being + /// executed tries to load modules. + pub module_loader: Option>, + + /// JsRuntime extensions, not to be confused with ES modules. + /// Only ops registered by extensions will be initialized. If you need + /// to execute JS code from extensions, pass source files in `js` or `esm` + /// option on `ExtensionBuilder`. + /// + /// If you are creating a runtime from a snapshot take care not to include + /// JavaScript sources in the extensions. + pub extensions: Vec, + + /// V8 snapshot that should be loaded on startup. + pub startup_snapshot: Option, + + /// Isolate creation parameters. + pub create_params: Option, + + /// V8 platform instance to use. Used when Deno initializes V8 + /// (which it only does once), otherwise it's silenty dropped. + pub v8_platform: Option>, + + /// The store to use for transferring SharedArrayBuffers between isolates. + /// If multiple isolates should have the possibility of sharing + /// SharedArrayBuffers, they should use the same [SharedArrayBufferStore]. If + /// no [SharedArrayBufferStore] is specified, SharedArrayBuffer can not be + /// serialized. + pub shared_array_buffer_store: Option, + + /// The store to use for transferring `WebAssembly.Module` objects between + /// isolates. + /// If multiple isolates should have the possibility of sharing + /// `WebAssembly.Module` objects, they should use the same + /// [CompiledWasmModuleStore]. If no [CompiledWasmModuleStore] is specified, + /// `WebAssembly.Module` objects cannot be serialized. + pub compiled_wasm_module_store: Option, + + /// Start inspector instance to allow debuggers to connect. + pub inspector: bool, + + /// Describe if this is the main runtime instance, used by debuggers in some + /// situation - like disconnecting when program finishes running. + pub is_main: bool, +} + +#[derive(Default)] +pub struct RuntimeSnapshotOptions { + /// An optional callback that will be called for each module that is loaded + /// during snapshotting. This callback can be used to transpile source on the + /// fly, during snapshotting, eg. to transpile TypeScript to JavaScript. + pub snapshot_module_load_cb: Option, +} + +impl JsRuntime { + /// Only constructor, configuration is done through `options`. + pub fn new(mut options: RuntimeOptions) -> JsRuntime { + JsRuntime::init_v8(options.v8_platform.take(), cfg!(test)); + JsRuntime::new_inner(options, false, None) + } + + pub(crate) fn state_from( + isolate: &v8::Isolate, + ) -> Rc> { + let state_ptr = isolate.get_data(STATE_DATA_OFFSET); + let state_rc = + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + unsafe { Rc::from_raw(state_ptr as *const RefCell) }; + let state = state_rc.clone(); + std::mem::forget(state_rc); + state + } + + pub(crate) fn module_map_from( + isolate: &v8::Isolate, + ) -> Rc> { + let module_map_ptr = isolate.get_data(MODULE_MAP_DATA_OFFSET); + let module_map_rc = + // SAFETY: We are sure that it's a valid pointer for whole lifetime of + // the runtime. + unsafe { Rc::from_raw(module_map_ptr as *const RefCell) }; + let module_map = module_map_rc.clone(); + std::mem::forget(module_map_rc); + module_map + } + + pub(crate) fn event_loop_pending_state_from_scope( + scope: &mut v8::HandleScope, + ) -> EventLoopPendingState { + let state = JsRuntime::state_from(scope); + let module_map = JsRuntime::module_map_from(scope); + let state = EventLoopPendingState::new( + scope, + &mut state.borrow_mut(), + &module_map.borrow(), + ); + state + } + + fn init_v8( + v8_platform: Option>, + predictable: bool, + ) { + static DENO_INIT: Once = Once::new(); + static DENO_PREDICTABLE: AtomicBool = AtomicBool::new(false); + static DENO_PREDICTABLE_SET: AtomicBool = AtomicBool::new(false); + + if DENO_PREDICTABLE_SET.load(Ordering::SeqCst) { + let current = DENO_PREDICTABLE.load(Ordering::SeqCst); + assert_eq!(current, predictable, "V8 may only be initialized once in either snapshotting or non-snapshotting mode. Either snapshotting or non-snapshotting mode may be used in a single process, not both."); + DENO_PREDICTABLE_SET.store(true, Ordering::SeqCst); + DENO_PREDICTABLE.store(predictable, Ordering::SeqCst); + } + + DENO_INIT.call_once(move || v8_init(v8_platform, predictable)); + } + + fn new_inner( + mut options: RuntimeOptions, + will_snapshot: bool, + maybe_load_callback: Option, + ) -> JsRuntime { + let init_mode = InitMode::from_options(&options); + let (op_state, ops) = Self::create_opstate(&mut options, init_mode); + let op_state = Rc::new(RefCell::new(op_state)); + + // Collect event-loop middleware + let mut event_loop_middlewares = + Vec::with_capacity(options.extensions.len()); + for extension in &mut options.extensions { + if let Some(middleware) = extension.init_event_loop_middleware() { + event_loop_middlewares.push(middleware); + } + } + + let align = std::mem::align_of::(); + let layout = std::alloc::Layout::from_size_align( + std::mem::size_of::<*mut v8::OwnedIsolate>(), + align, + ) + .unwrap(); + assert!(layout.size() > 0); + let isolate_ptr: *mut v8::OwnedIsolate = + // SAFETY: we just asserted that layout has non-0 size. + unsafe { std::alloc::alloc(layout) as *mut _ }; + + let state_rc = Rc::new(RefCell::new(JsRuntimeState { + pending_dyn_mod_evaluate: vec![], + pending_mod_evaluate: None, + dyn_module_evaluate_idle_counter: 0, + has_tick_scheduled: false, + source_map_getter: options.source_map_getter.map(Rc::new), + source_map_cache: Default::default(), + shared_array_buffer_store: options.shared_array_buffer_store, + compiled_wasm_module_store: options.compiled_wasm_module_store, + op_state: op_state.clone(), + dispatched_exception: None, + // Some fields are initialized later after isolate is created + inspector: None, + global_realm: None, + known_realms: Vec::with_capacity(1), + })); + + let weak = Rc::downgrade(&state_rc); + let context_state = Rc::new(RefCell::new(ContextState::default())); + let op_ctxs = ops + .into_iter() + .enumerate() + .map(|(id, decl)| { + OpCtx::new( + id as u16, + context_state.clone(), + Rc::new(decl), + op_state.clone(), + weak.clone(), + ) + }) + .collect::>() + .into_boxed_slice(); + context_state.borrow_mut().op_ctxs = op_ctxs; + context_state.borrow_mut().isolate = Some(isolate_ptr); + + let refs = bindings::external_references(&context_state.borrow().op_ctxs); + // V8 takes ownership of external_references. + let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs)); + + let mut isolate = if will_snapshot { + snapshot_util::create_snapshot_creator( + refs, + options.startup_snapshot.take(), + ) + } else { + let mut params = options + .create_params + .take() + .unwrap_or_default() + .embedder_wrapper_type_info_offsets( + V8_WRAPPER_TYPE_INDEX, + V8_WRAPPER_OBJECT_INDEX, + ) + .external_references(&**refs); + if let Some(snapshot) = options.startup_snapshot.take() { + params = match snapshot { + Snapshot::Static(data) => params.snapshot_blob(data), + Snapshot::JustCreated(data) => params.snapshot_blob(data), + Snapshot::Boxed(data) => params.snapshot_blob(data), + }; + } + v8::Isolate::new(params) + }; + isolate.set_capture_stack_trace_for_uncaught_exceptions(true, 10); + isolate.set_promise_reject_callback(bindings::promise_reject_callback); + isolate.set_host_initialize_import_meta_object_callback( + bindings::host_initialize_import_meta_object_callback, + ); + isolate.set_host_import_module_dynamically_callback( + bindings::host_import_module_dynamically_callback, + ); + isolate.set_wasm_async_resolve_promise_callback( + bindings::wasm_async_resolve_promise_callback, + ); + + let (global_context, snapshotted_data) = { + let scope = &mut v8::HandleScope::new(&mut isolate); + let context = v8::Context::new(scope); + + // Get module map data from the snapshot + let snapshotted_data = if init_mode == InitMode::FromSnapshot { + Some(snapshot_util::get_snapshotted_data(scope, context)) + } else { + None + }; + + (v8::Global::new(scope, context), snapshotted_data) + }; + + // SAFETY: this is first use of `isolate_ptr` so we are sure we're + // not overwriting an existing pointer. + isolate = unsafe { + isolate_ptr.write(isolate); + isolate_ptr.read() + }; + + let mut context_scope: v8::HandleScope = + v8::HandleScope::with_context(&mut isolate, global_context.clone()); + let scope = &mut context_scope; + let context = v8::Local::new(scope, global_context.clone()); + + bindings::initialize_context( + scope, + context, + &context_state.borrow().op_ctxs, + init_mode, + ); + + context.set_slot(scope, context_state.clone()); + + op_state.borrow_mut().put(isolate_ptr); + let inspector = if options.inspector { + Some(JsRuntimeInspector::new(scope, context, options.is_main)) + } else { + None + }; + + let loader = options + .module_loader + .unwrap_or_else(|| Rc::new(NoopModuleLoader)); + + { + let global_realm = JsRealmInner::new( + context_state, + global_context, + state_rc.clone(), + true, + ); + let mut state = state_rc.borrow_mut(); + state.global_realm = Some(JsRealm::new(global_realm.clone())); + state.inspector = inspector; + state.known_realms.push(global_realm); + } + scope.set_data( + STATE_DATA_OFFSET, + Rc::into_raw(state_rc.clone()) as *mut c_void, + ); + let module_map_rc = Rc::new(RefCell::new(ModuleMap::new(loader))); + if let Some(snapshotted_data) = snapshotted_data { + let mut module_map = module_map_rc.borrow_mut(); + module_map.update_with_snapshotted_data(scope, snapshotted_data); + } + scope.set_data( + MODULE_MAP_DATA_OFFSET, + Rc::into_raw(module_map_rc.clone()) as *mut c_void, + ); + + drop(context_scope); + + let mut js_runtime = JsRuntime { + inner: InnerIsolateState { + will_snapshot, + state: ManuallyDropRc(ManuallyDrop::new(state_rc)), + v8_isolate: ManuallyDrop::new(isolate), + }, + init_mode, + allocations: IsolateAllocations::default(), + event_loop_middlewares, + extensions: options.extensions, + module_map: module_map_rc, + is_main: options.is_main, + }; + + let realm = js_runtime.global_realm(); + // TODO(mmastrac): We should thread errors back out of the runtime + js_runtime + .init_extension_js(&realm, maybe_load_callback) + .unwrap(); + js_runtime + } + + #[cfg(test)] + #[inline] + pub(crate) fn module_map(&self) -> &Rc> { + &self.module_map + } + + #[inline] + pub fn global_context(&self) -> v8::Global { + self + .inner + .state + .borrow() + .known_realms + .get(0) + .unwrap() + .context() + .clone() + } + + #[inline] + pub fn v8_isolate(&mut self) -> &mut v8::OwnedIsolate { + &mut self.inner.v8_isolate + } + + #[inline] + pub fn inspector(&mut self) -> Rc> { + self.inner.state.borrow().inspector() + } + + #[inline] + pub fn global_realm(&mut self) -> JsRealm { + let state = self.inner.state.borrow(); + state.global_realm.clone().unwrap() + } + + /// Returns the extensions that this runtime is using (including internal ones). + pub fn extensions(&self) -> &Vec { + &self.extensions + } + + /// Creates a new realm (V8 context) in this JS execution context, + /// pre-initialized with all of the extensions that were passed in + /// [`RuntimeOptions::extensions`] when the [`JsRuntime`] was + /// constructed. + pub fn create_realm(&mut self) -> Result { + let realm = { + let context_state = Rc::new(RefCell::new(ContextState::default())); + let op_ctxs: Box<[OpCtx]> = self + .global_realm() + .0 + .state() + .borrow() + .op_ctxs + .iter() + .map(|op_ctx| { + OpCtx::new( + op_ctx.id, + context_state.clone(), + op_ctx.decl.clone(), + op_ctx.state.clone(), + op_ctx.runtime_state.clone(), + ) + }) + .collect(); + context_state.borrow_mut().op_ctxs = op_ctxs; + context_state.borrow_mut().isolate = Some(self.v8_isolate() as _); + + let raw_ptr = self.v8_isolate() as *mut v8::OwnedIsolate; + // SAFETY: Having the scope tied to self's lifetime makes it impossible to + // reference JsRuntimeState::op_ctxs while the scope is alive. Here we + // turn it into an unbound lifetime, which is sound because 1. it only + // lives until the end of this block, and 2. the HandleScope only has + // access to the isolate, and nothing else we're accessing from self does. + let isolate = unsafe { raw_ptr.as_mut() }.unwrap(); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let context = bindings::initialize_context( + scope, + context, + &context_state.borrow().op_ctxs, + self.init_mode, + ); + context.set_slot(scope, context_state.clone()); + let realm = JsRealmInner::new( + context_state, + v8::Global::new(scope, context), + self.inner.state.clone(), + false, + ); + let mut state = self.inner.state.borrow_mut(); + state.known_realms.push(realm.clone()); + JsRealm::new(realm) + }; + + self.init_extension_js(&realm, None)?; + Ok(realm) + } + + #[inline] + pub fn handle_scope(&mut self) -> v8::HandleScope { + self.global_realm().handle_scope(self.v8_isolate()) + } + + /// Initializes JS of provided Extensions in the given realm. + fn init_extension_js( + &mut self, + realm: &JsRealm, + maybe_load_callback: Option, + ) -> Result<(), Error> { + // Initialization of JS happens in phases: + // 1. Iterate through all extensions: + // a. Execute all extension "script" JS files + // b. Load all extension "module" JS files (but do not execute them yet) + // 2. Iterate through all extensions: + // a. If an extension has a `esm_entry_point`, execute it. + + // Take extensions temporarily so we can avoid have a mutable reference to self + let extensions = std::mem::take(&mut self.extensions); + + // TODO(nayeemrmn): Module maps should be per-realm. + let loader = self.module_map.borrow().loader.clone(); + let ext_loader = Rc::new(ExtModuleLoader::new( + &extensions, + maybe_load_callback.map(Rc::new), + )); + self.module_map.borrow_mut().loader = ext_loader; + + let mut esm_entrypoints = vec![]; + + futures::executor::block_on(async { + for extension in &extensions { + let maybe_esm_entry_point = extension.get_esm_entry_point(); + + if let Some(esm_files) = extension.get_esm_sources() { + for file_source in esm_files { + self + .load_side_module( + &ModuleSpecifier::parse(file_source.specifier)?, + None, + ) + .await?; + } + } + + if let Some(entry_point) = maybe_esm_entry_point { + esm_entrypoints.push(entry_point); + } + + if let Some(js_files) = extension.get_js_sources() { + for file_source in js_files { + realm.execute_script( + self.v8_isolate(), + file_source.specifier, + file_source.load()?, + )?; + } + } + + if extension.is_core { + self.init_cbs(realm); + } + } + + for specifier in esm_entrypoints { + let mod_id = { + self + .module_map + .borrow() + .get_id(specifier, AssertedModuleType::JavaScriptOrWasm) + .unwrap_or_else(|| { + panic!("{} not present in the module map", specifier) + }) + }; + let receiver = self.mod_evaluate(mod_id); + self.run_event_loop(false).await?; + receiver + .await? + .with_context(|| format!("Couldn't execute '{specifier}'"))?; + } + + #[cfg(debug_assertions)] + { + let module_map_rc = self.module_map.clone(); + let mut scope = realm.handle_scope(self.v8_isolate()); + let module_map = module_map_rc.borrow(); + module_map.assert_all_modules_evaluated(&mut scope); + } + + Ok::<_, anyhow::Error>(()) + })?; + + self.extensions = extensions; + self.module_map.borrow_mut().loader = loader; + Ok(()) + } + + /// Collects ops from extensions & applies middleware + fn collect_ops(exts: &mut [Extension]) -> Vec { + for (ext, previous_exts) in + exts.iter().enumerate().map(|(i, ext)| (ext, &exts[..i])) + { + ext.check_dependencies(previous_exts); + } + + // Middleware + let middleware: Vec> = exts + .iter_mut() + .filter_map(|e| e.init_middleware()) + .collect(); + + // macroware wraps an opfn in all the middleware + let macroware = move |d| middleware.iter().fold(d, |d, m| m(d)); + + // Flatten ops, apply middlware & override disabled ops + let ops: Vec<_> = exts + .iter_mut() + .filter_map(|e| e.init_ops()) + .flatten() + .map(|d| OpDecl { + name: d.name, + ..macroware(d) + }) + .collect(); + + // In debug build verify there are no duplicate ops. + #[cfg(debug_assertions)] + { + let mut count_by_name = HashMap::new(); + + for op in ops.iter() { + count_by_name + .entry(&op.name) + .or_insert(vec![]) + .push(op.name.to_string()); + } + + let mut duplicate_ops = vec![]; + for (op_name, _count) in + count_by_name.iter().filter(|(_k, v)| v.len() > 1) + { + duplicate_ops.push(op_name.to_string()); + } + if !duplicate_ops.is_empty() { + let mut msg = "Found ops with duplicate names:\n".to_string(); + for op_name in duplicate_ops { + msg.push_str(&format!(" - {}\n", op_name)); + } + msg.push_str("Op names need to be unique."); + panic!("{}", msg); + } + } + + ops + } + + /// Initializes ops of provided Extensions + fn create_opstate( + options: &mut RuntimeOptions, + init_mode: InitMode, + ) -> (OpState, Vec) { + // Add built-in extension + if init_mode == InitMode::FromSnapshot { + options + .extensions + .insert(0, crate::ops_builtin::core::init_ops()); + } else { + options + .extensions + .insert(0, crate::ops_builtin::core::init_ops_and_esm()); + } + + let ops = Self::collect_ops(&mut options.extensions); + + let mut op_state = OpState::new(ops.len()); + + if let Some(get_error_class_fn) = options.get_error_class_fn { + op_state.get_error_class_fn = get_error_class_fn; + } + + // Setup state + for e in &mut options.extensions { + // ops are already registered during in bindings::initialize_context(); + e.init_state(&mut op_state); + } + + (op_state, ops) + } + + pub fn eval<'s, T>( + scope: &mut v8::HandleScope<'s>, + code: &str, + ) -> Option> + where + v8::Local<'s, T>: TryFrom, Error = v8::DataError>, + { + let scope = &mut v8::EscapableHandleScope::new(scope); + let source = v8::String::new(scope, code).unwrap(); + let script = v8::Script::compile(scope, source, None).unwrap(); + let v = script.run(scope)?; + scope.escape(v).try_into().ok() + } + + /// Grabs a reference to core.js' eventLoopTick & buildCustomError + fn init_cbs(&mut self, realm: &JsRealm) { + let (event_loop_tick_cb, build_custom_error_cb) = { + let scope = &mut realm.handle_scope(self.v8_isolate()); + let context = realm.context(); + let context_local = v8::Local::new(scope, context); + let global = context_local.global(scope); + let deno_str = + v8::String::new_external_onebyte_static(scope, b"Deno").unwrap(); + let core_str = + v8::String::new_external_onebyte_static(scope, b"core").unwrap(); + let event_loop_tick_str = + v8::String::new_external_onebyte_static(scope, b"eventLoopTick") + .unwrap(); + let build_custom_error_str = + v8::String::new_external_onebyte_static(scope, b"buildCustomError") + .unwrap(); + + let deno_obj: v8::Local = global + .get(scope, deno_str.into()) + .unwrap() + .try_into() + .unwrap(); + let core_obj: v8::Local = deno_obj + .get(scope, core_str.into()) + .unwrap() + .try_into() + .unwrap(); + + let event_loop_tick_cb: v8::Local = core_obj + .get(scope, event_loop_tick_str.into()) + .unwrap() + .try_into() + .unwrap(); + let build_custom_error_cb: v8::Local = core_obj + .get(scope, build_custom_error_str.into()) + .unwrap() + .try_into() + .unwrap(); + ( + v8::Global::new(scope, event_loop_tick_cb), + v8::Global::new(scope, build_custom_error_cb), + ) + }; + + // Put global handles in the realm's ContextState + let state_rc = realm.0.state(); + let mut state = state_rc.borrow_mut(); + state + .js_event_loop_tick_cb + .replace(Rc::new(event_loop_tick_cb)); + state + .js_build_custom_error_cb + .replace(Rc::new(build_custom_error_cb)); + } + + /// Returns the runtime's op state, which can be used to maintain ops + /// and access resources between op calls. + pub fn op_state(&mut self) -> Rc> { + let state = self.inner.state.borrow(); + state.op_state.clone() + } + + /// Executes traditional JavaScript code (traditional = not ES modules). + /// + /// The execution takes place on the current global context, so it is possible + /// to maintain local JS state and invoke this method multiple times. + /// + /// `name` can be a filepath or any other string, but it is required to be 7-bit ASCII, eg. + /// + /// - "/some/file/path.js" + /// - "" + /// - "[native code]" + /// + /// The same `name` value can be used for multiple executions. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn execute_script( + &mut self, + name: &'static str, + source_code: ModuleCode, + ) -> Result, Error> { + self + .global_realm() + .execute_script(self.v8_isolate(), name, source_code) + } + + /// Executes traditional JavaScript code (traditional = not ES modules). + /// + /// The execution takes place on the current global context, so it is possible + /// to maintain local JS state and invoke this method multiple times. + /// + /// `name` can be a filepath or any other string, but it is required to be 7-bit ASCII, eg. + /// + /// - "/some/file/path.js" + /// - "" + /// - "[native code]" + /// + /// The same `name` value can be used for multiple executions. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn execute_script_static( + &mut self, + name: &'static str, + source_code: &'static str, + ) -> Result, Error> { + self.global_realm().execute_script( + self.v8_isolate(), + name, + ModuleCode::from_static(source_code), + ) + } + + /// Call a function. If it returns a promise, run the event loop until that + /// promise is settled. If the promise rejects or there is an uncaught error + /// in the event loop, return `Err(error)`. Or return `Ok()`. + pub async fn call_and_await( + &mut self, + function: &v8::Global, + ) -> Result, Error> { + let promise = { + let scope = &mut self.handle_scope(); + let cb = function.open(scope); + let this = v8::undefined(scope).into(); + let promise = cb.call(scope, this, &[]); + if promise.is_none() || scope.is_execution_terminating() { + let undefined = v8::undefined(scope).into(); + return exception_to_err_result(scope, undefined, false); + } + v8::Global::new(scope, promise.unwrap()) + }; + self.resolve_value(promise).await + } + + /// Returns the namespace object of a module. + /// + /// This is only available after module evaluation has completed. + /// This function panics if module has not been instantiated. + pub fn get_module_namespace( + &mut self, + module_id: ModuleId, + ) -> Result, Error> { + let module_handle = self + .module_map + .borrow() + .get_handle(module_id) + .expect("ModuleInfo not found"); + + let scope = &mut self.handle_scope(); + + let module = module_handle.open(scope); + + if module.get_status() == v8::ModuleStatus::Errored { + let exception = module.get_exception(); + return exception_to_err_result(scope, exception, false); + } + + assert!(matches!( + module.get_status(), + v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated + )); + + let module_namespace: v8::Local = + v8::Local::try_from(module.get_module_namespace()) + .map_err(|err: v8::DataError| generic_error(err.to_string()))?; + + Ok(v8::Global::new(scope, module_namespace)) + } + + /// Registers a callback on the isolate when the memory limits are approached. + /// Use this to prevent V8 from crashing the process when reaching the limit. + /// + /// Calls the closure with the current heap limit and the initial heap limit. + /// The return value of the closure is set as the new limit. + pub fn add_near_heap_limit_callback(&mut self, cb: C) + where + C: FnMut(usize, usize) -> usize + 'static, + { + let boxed_cb = Box::new(RefCell::new(cb)); + let data = boxed_cb.as_ptr() as *mut c_void; + + let prev = self + .allocations + .near_heap_limit_callback_data + .replace((boxed_cb, near_heap_limit_callback::)); + if let Some((_, prev_cb)) = prev { + self + .v8_isolate() + .remove_near_heap_limit_callback(prev_cb, 0); + } + + self + .v8_isolate() + .add_near_heap_limit_callback(near_heap_limit_callback::, data); + } + + pub fn remove_near_heap_limit_callback(&mut self, heap_limit: usize) { + if let Some((_, cb)) = self.allocations.near_heap_limit_callback_data.take() + { + self + .v8_isolate() + .remove_near_heap_limit_callback(cb, heap_limit); + } + } + + fn pump_v8_message_loop(&mut self) -> Result<(), Error> { + let scope = &mut self.handle_scope(); + while v8::Platform::pump_message_loop( + &v8::V8::get_current_platform(), + scope, + false, // don't block if there are no tasks + ) { + // do nothing + } + + let tc_scope = &mut v8::TryCatch::new(scope); + tc_scope.perform_microtask_checkpoint(); + match tc_scope.exception() { + None => Ok(()), + Some(exception) => exception_to_err_result(tc_scope, exception, false), + } + } + + pub fn maybe_init_inspector(&mut self) { + if self.inner.state.borrow().inspector.is_some() { + return; + } + + let context = self.global_context(); + let scope = &mut v8::HandleScope::with_context( + self.inner.v8_isolate.as_mut(), + context.clone(), + ); + let context = v8::Local::new(scope, context); + + let mut state = self.inner.state.borrow_mut(); + state.inspector = + Some(JsRuntimeInspector::new(scope, context, self.is_main)); + } + + pub fn poll_value( + &mut self, + global: &v8::Global, + cx: &mut Context, + ) -> Poll, Error>> { + let state = self.poll_event_loop(cx, false); + + let mut scope = self.handle_scope(); + let local = v8::Local::::new(&mut scope, global); + + if let Ok(promise) = v8::Local::::try_from(local) { + match promise.state() { + v8::PromiseState::Pending => match state { + Poll::Ready(Ok(_)) => { + let msg = "Promise resolution is still pending but the event loop has already resolved."; + Poll::Ready(Err(generic_error(msg))) + } + Poll::Ready(Err(e)) => Poll::Ready(Err(e)), + Poll::Pending => Poll::Pending, + }, + v8::PromiseState::Fulfilled => { + let value = promise.result(&mut scope); + let value_handle = v8::Global::new(&mut scope, value); + Poll::Ready(Ok(value_handle)) + } + v8::PromiseState::Rejected => { + let exception = promise.result(&mut scope); + Poll::Ready(exception_to_err_result(&mut scope, exception, false)) + } + } + } else { + let value_handle = v8::Global::new(&mut scope, local); + Poll::Ready(Ok(value_handle)) + } + } + + /// Waits for the given value to resolve while polling the event loop. + /// + /// This future resolves when either the value is resolved or the event loop runs to + /// completion. + pub async fn resolve_value( + &mut self, + global: v8::Global, + ) -> Result, Error> { + poll_fn(|cx| self.poll_value(&global, cx)).await + } + + /// Runs event loop to completion + /// + /// This future resolves when: + /// - there are no more pending dynamic imports + /// - there are no more pending ops + /// - there are no more active inspector sessions (only if `wait_for_inspector` is set to true) + pub async fn run_event_loop( + &mut self, + wait_for_inspector: bool, + ) -> Result<(), Error> { + poll_fn(|cx| self.poll_event_loop(cx, wait_for_inspector)).await + } + + /// Runs a single tick of event loop + /// + /// If `wait_for_inspector` is set to true event loop + /// will return `Poll::Pending` if there are active inspector sessions. + pub fn poll_event_loop( + &mut self, + cx: &mut Context, + wait_for_inspector: bool, + ) -> Poll> { + let has_inspector: bool; + + { + let state = self.inner.state.borrow(); + has_inspector = state.inspector.is_some(); + state.op_state.borrow().waker.register(cx.waker()); + } + + if has_inspector { + // We poll the inspector first. + let _ = self.inspector().borrow().poll_sessions(Some(cx)).unwrap(); + } + + self.pump_v8_message_loop()?; + + // Dynamic module loading - ie. modules loaded using "import()" + { + // Run in a loop so that dynamic imports that only depend on another + // dynamic import can be resolved in this event loop iteration. + // + // For example, a dynamically imported module like the following can be + // immediately resolved after `dependency.ts` is fully evaluated, but it + // wouldn't if not for this loop. + // + // await delay(1000); + // await import("./dependency.ts"); + // console.log("test") + // + loop { + let poll_imports = self.prepare_dyn_imports(cx)?; + assert!(poll_imports.is_ready()); + + let poll_imports = self.poll_dyn_imports(cx)?; + assert!(poll_imports.is_ready()); + + if !self.evaluate_dyn_imports() { + break; + } + } + } + + // Resolve async ops, run all next tick callbacks and macrotasks callbacks + // and only then check for any promise exceptions (`unhandledrejection` + // handlers are run in macrotasks callbacks so we need to let them run + // first). + self.do_js_event_loop_tick(cx)?; + self.check_promise_rejections()?; + + // Event loop middlewares + let mut maybe_scheduling = false; + { + let op_state = self.inner.state.borrow().op_state.clone(); + for f in &self.event_loop_middlewares { + if f(op_state.clone(), cx) { + maybe_scheduling = true; + } + } + } + + // Top level module + self.evaluate_pending_module(); + + let pending_state = self.event_loop_pending_state(); + if !pending_state.is_pending() && !maybe_scheduling { + if has_inspector { + let inspector = self.inspector(); + let has_active_sessions = inspector.borrow().has_active_sessions(); + let has_blocking_sessions = inspector.borrow().has_blocking_sessions(); + + if wait_for_inspector && has_active_sessions { + // If there are no blocking sessions (eg. REPL) we can now notify + // debugger that the program has finished running and we're ready + // to exit the process once debugger disconnects. + if !has_blocking_sessions { + let context = self.global_context(); + let scope = &mut self.handle_scope(); + inspector.borrow_mut().context_destroyed(scope, context); + println!("Program finished. Waiting for inspector to disconnect to exit the process..."); + } + + return Poll::Pending; + } + } + + return Poll::Ready(Ok(())); + } + + let state = self.inner.state.borrow(); + + // Check if more async ops have been dispatched + // during this turn of event loop. + // If there are any pending background tasks, we also wake the runtime to + // make sure we don't miss them. + // TODO(andreubotella) The event loop will spin as long as there are pending + // background tasks. We should look into having V8 notify us when a + // background task is done. + if pending_state.has_pending_background_tasks + || pending_state.has_tick_scheduled + || maybe_scheduling + { + state.op_state.borrow().waker.wake(); + } + + drop(state); + + if pending_state.has_pending_module_evaluation { + if pending_state.has_pending_refed_ops + || pending_state.has_pending_dyn_imports + || pending_state.has_pending_dyn_module_evaluation + || pending_state.has_pending_background_tasks + || pending_state.has_tick_scheduled + || maybe_scheduling + { + // pass, will be polled again + } else { + let scope = &mut self.handle_scope(); + let messages = find_stalled_top_level_await(scope); + // We are gonna print only a single message to provide a nice formatting + // with source line of offending promise shown. Once user fixed it, then + // they will get another error message for the next promise (but this + // situation is gonna be very rare, if ever happening). + assert!(!messages.is_empty()); + let msg = v8::Local::new(scope, messages[0].clone()); + let js_error = JsError::from_v8_message(scope, msg); + return Poll::Ready(Err(js_error.into())); + } + } + + if pending_state.has_pending_dyn_module_evaluation { + if pending_state.has_pending_refed_ops + || pending_state.has_pending_dyn_imports + || pending_state.has_pending_background_tasks + || pending_state.has_tick_scheduled + { + // pass, will be polled again + } else if self.inner.state.borrow().dyn_module_evaluate_idle_counter >= 1 + { + let scope = &mut self.handle_scope(); + let messages = find_stalled_top_level_await(scope); + // We are gonna print only a single message to provide a nice formatting + // with source line of offending promise shown. Once user fixed it, then + // they will get another error message for the next promise (but this + // situation is gonna be very rare, if ever happening). + assert!(!messages.is_empty()); + let msg = v8::Local::new(scope, messages[0].clone()); + let js_error = JsError::from_v8_message(scope, msg); + return Poll::Ready(Err(js_error.into())); + } else { + let mut state = self.inner.state.borrow_mut(); + // Delay the above error by one spin of the event loop. A dynamic import + // evaluation may complete during this, in which case the counter will + // reset. + state.dyn_module_evaluate_idle_counter += 1; + state.op_state.borrow().waker.wake(); + } + } + + Poll::Pending + } + + fn event_loop_pending_state(&mut self) -> EventLoopPendingState { + let mut scope = v8::HandleScope::new(self.inner.v8_isolate.as_mut()); + EventLoopPendingState::new( + &mut scope, + &mut self.inner.state.borrow_mut(), + &self.module_map.borrow(), + ) + } +} + +impl JsRuntimeForSnapshot { + pub fn new( + mut options: RuntimeOptions, + runtime_snapshot_options: RuntimeSnapshotOptions, + ) -> JsRuntimeForSnapshot { + JsRuntime::init_v8(options.v8_platform.take(), true); + JsRuntimeForSnapshot(JsRuntime::new_inner( + options, + true, + runtime_snapshot_options.snapshot_module_load_cb, + )) + } + + /// Takes a snapshot and consumes the runtime. + /// + /// `Error` can usually be downcast to `JsError`. + pub fn snapshot(mut self) -> v8::StartupData { + // Ensure there are no live inspectors to prevent crashes. + self.inner.prepare_for_cleanup(); + + // Set the context to be snapshot's default context + { + let context = self.global_context(); + let mut scope = self.handle_scope(); + let local_context = v8::Local::new(&mut scope, context); + scope.set_default_context(local_context); + } + + // Serialize the module map and store its data in the snapshot. + { + let snapshotted_data = { + // `self.module_map` points directly to the v8 isolate data slot, which + // we must explicitly drop before destroying the isolate. We have to + // take and drop this `Rc` before that. + let module_map_rc = std::mem::take(&mut self.module_map); + let module_map = module_map_rc.borrow(); + module_map.serialize_for_snapshotting(&mut self.handle_scope()) + }; + + let context = self.global_context(); + let mut scope = self.handle_scope(); + snapshot_util::set_snapshotted_data( + &mut scope, + context, + snapshotted_data, + ); + } + + self + .0 + .inner + .prepare_for_snapshot() + .create_blob(v8::FunctionCodeHandling::Keep) + .unwrap() + } +} + +fn get_stalled_top_level_await_message_for_module( + scope: &mut v8::HandleScope, + module_id: ModuleId, +) -> Vec> { + let module_map = JsRuntime::module_map_from(scope); + let module_map = module_map.borrow(); + let module_handle = module_map.handles.get(module_id).unwrap(); + + let module = v8::Local::new(scope, module_handle); + let stalled = module.get_stalled_top_level_await_message(scope); + let mut messages = vec![]; + for (_, message) in stalled { + messages.push(v8::Global::new(scope, message)); + } + messages +} + +fn find_stalled_top_level_await( + scope: &mut v8::HandleScope, +) -> Vec> { + let module_map = JsRuntime::module_map_from(scope); + let module_map = module_map.borrow(); + + // First check if that's root module + let root_module_id = module_map + .info + .iter() + .filter(|m| m.main) + .map(|m| m.id) + .next(); + + if let Some(root_module_id) = root_module_id { + let messages = + get_stalled_top_level_await_message_for_module(scope, root_module_id); + if !messages.is_empty() { + return messages; + } + } + + // It wasn't a top module, so iterate over all modules and try to find + // any with stalled top level await + for module_id in 0..module_map.handles.len() { + let messages = + get_stalled_top_level_await_message_for_module(scope, module_id); + if !messages.is_empty() { + return messages; + } + } + + unreachable!() +} + +#[derive(Clone, Copy, PartialEq, Eq, Debug)] +pub(crate) struct EventLoopPendingState { + has_pending_refed_ops: bool, + has_pending_dyn_imports: bool, + has_pending_dyn_module_evaluation: bool, + has_pending_module_evaluation: bool, + has_pending_background_tasks: bool, + has_tick_scheduled: bool, +} +impl EventLoopPendingState { + pub fn new( + scope: &mut v8::HandleScope<()>, + state: &mut JsRuntimeState, + module_map: &ModuleMap, + ) -> EventLoopPendingState { + let mut num_unrefed_ops = 0; + let mut num_pending_ops = 0; + for realm in &state.known_realms { + num_unrefed_ops += realm.num_unrefed_ops(); + num_pending_ops += realm.num_pending_ops(); + } + + EventLoopPendingState { + has_pending_refed_ops: num_pending_ops > num_unrefed_ops, + has_pending_dyn_imports: module_map.has_pending_dynamic_imports(), + has_pending_dyn_module_evaluation: !state + .pending_dyn_mod_evaluate + .is_empty(), + has_pending_module_evaluation: state.pending_mod_evaluate.is_some(), + has_pending_background_tasks: scope.has_pending_background_tasks(), + has_tick_scheduled: state.has_tick_scheduled, + } + } + + pub fn is_pending(&self) -> bool { + self.has_pending_refed_ops + || self.has_pending_dyn_imports + || self.has_pending_dyn_module_evaluation + || self.has_pending_module_evaluation + || self.has_pending_background_tasks + || self.has_tick_scheduled + } +} + +extern "C" fn near_heap_limit_callback( + data: *mut c_void, + current_heap_limit: usize, + initial_heap_limit: usize, +) -> usize +where + F: FnMut(usize, usize) -> usize, +{ + // SAFETY: The data is a pointer to the Rust callback function. It is stored + // in `JsRuntime::allocations` and thus is guaranteed to outlive the isolate. + let callback = unsafe { &mut *(data as *mut F) }; + callback(current_heap_limit, initial_heap_limit) +} + +impl JsRuntimeState { + pub(crate) fn inspector(&self) -> Rc> { + self.inspector.as_ref().unwrap().clone() + } + + /// Called by `bindings::host_import_module_dynamically_callback` + /// after initiating new dynamic import load. + pub fn notify_new_dynamic_import(&mut self) { + // Notify event loop to poll again soon. + self.op_state.borrow().waker.wake(); + } +} + +// Related to module loading +impl JsRuntime { + pub(crate) fn instantiate_module( + &mut self, + id: ModuleId, + ) -> Result<(), v8::Global> { + let module_map_rc = self.module_map.clone(); + let scope = &mut self.handle_scope(); + let tc_scope = &mut v8::TryCatch::new(scope); + + let module = module_map_rc + .borrow() + .get_handle(id) + .map(|handle| v8::Local::new(tc_scope, handle)) + .expect("ModuleInfo not found"); + + if module.get_status() == v8::ModuleStatus::Errored { + return Err(v8::Global::new(tc_scope, module.get_exception())); + } + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // `module_resolve_callback` will be calling into `ModuleMap` from within + // the isolate. + let instantiate_result = + module.instantiate_module(tc_scope, bindings::module_resolve_callback); + + if instantiate_result.is_none() { + let exception = tc_scope.exception().unwrap(); + return Err(v8::Global::new(tc_scope, exception)); + } + + Ok(()) + } + + fn dynamic_import_module_evaluate( + &mut self, + load_id: ModuleLoadId, + id: ModuleId, + ) -> Result<(), Error> { + let module_handle = self + .module_map + .borrow() + .get_handle(id) + .expect("ModuleInfo not found"); + + let status = { + let scope = &mut self.handle_scope(); + let module = module_handle.open(scope); + module.get_status() + }; + + match status { + v8::ModuleStatus::Instantiated | v8::ModuleStatus::Evaluated => {} + _ => return Ok(()), + } + + // IMPORTANT: Top-level-await is enabled, which means that return value + // of module evaluation is a promise. + // + // This promise is internal, and not the same one that gets returned to + // the user. We add an empty `.catch()` handler so that it does not result + // in an exception if it rejects. That will instead happen for the other + // promise if not handled by the user. + // + // For more details see: + // https://github.com/denoland/deno/issues/4908 + // https://v8.dev/features/top-level-await#module-execution-order + let global_realm = + self.inner.state.borrow_mut().global_realm.clone().unwrap(); + let scope = &mut global_realm.handle_scope(&mut self.inner.v8_isolate); + let tc_scope = &mut v8::TryCatch::new(scope); + let module = v8::Local::new(tc_scope, &module_handle); + let maybe_value = module.evaluate(tc_scope); + + // Update status after evaluating. + let status = module.get_status(); + + if let Some(value) = maybe_value { + assert!( + status == v8::ModuleStatus::Evaluated + || status == v8::ModuleStatus::Errored + ); + let promise = v8::Local::::try_from(value) + .expect("Expected to get promise as module evaluation result"); + let empty_fn = bindings::create_empty_fn(tc_scope).unwrap(); + promise.catch(tc_scope, empty_fn); + let promise_global = v8::Global::new(tc_scope, promise); + let module_global = v8::Global::new(tc_scope, module); + + let dyn_import_mod_evaluate = DynImportModEvaluate { + load_id, + module_id: id, + promise: promise_global, + module: module_global, + }; + + self + .inner + .state + .borrow_mut() + .pending_dyn_mod_evaluate + .push(dyn_import_mod_evaluate); + } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Err( + generic_error("Cannot evaluate dynamically imported module, because JavaScript execution has been terminated.") + ); + } else { + assert!(status == v8::ModuleStatus::Errored); + } + + Ok(()) + } + + // TODO(bartlomieju): make it return `ModuleEvaluationFuture`? + /// Evaluates an already instantiated ES module. + /// + /// Returns a receiver handle that resolves when module promise resolves. + /// Implementors must manually call [`JsRuntime::run_event_loop`] to drive + /// module evaluation future. + /// + /// `Error` can usually be downcast to `JsError` and should be awaited and + /// checked after [`JsRuntime::run_event_loop`] completion. + /// + /// This function panics if module has not been instantiated. + pub fn mod_evaluate( + &mut self, + id: ModuleId, + ) -> oneshot::Receiver> { + let global_realm = self.global_realm(); + let state_rc = self.inner.state.clone(); + let module_map_rc = self.module_map.clone(); + let scope = &mut self.handle_scope(); + let tc_scope = &mut v8::TryCatch::new(scope); + + let module = module_map_rc + .borrow() + .get_handle(id) + .map(|handle| v8::Local::new(tc_scope, handle)) + .expect("ModuleInfo not found"); + let mut status = module.get_status(); + assert_eq!( + status, + v8::ModuleStatus::Instantiated, + "Module not instantiated {id}" + ); + + let (sender, receiver) = oneshot::channel(); + + // IMPORTANT: Top-level-await is enabled, which means that return value + // of module evaluation is a promise. + // + // Because that promise is created internally by V8, when error occurs during + // module evaluation the promise is rejected, and since the promise has no rejection + // handler it will result in call to `bindings::promise_reject_callback` adding + // the promise to pending promise rejection table - meaning JsRuntime will return + // error on next poll(). + // + // This situation is not desirable as we want to manually return error at the + // end of this function to handle it further. It means we need to manually + // remove this promise from pending promise rejection table. + // + // For more details see: + // https://github.com/denoland/deno/issues/4908 + // https://v8.dev/features/top-level-await#module-execution-order + { + let mut state = state_rc.borrow_mut(); + assert!( + state.pending_mod_evaluate.is_none(), + "There is already pending top level module evaluation" + ); + state.pending_mod_evaluate = Some(ModEvaluate { + promise: None, + has_evaluated: false, + handled_promise_rejections: vec![], + sender, + }); + } + + let maybe_value = module.evaluate(tc_scope); + { + let mut state = state_rc.borrow_mut(); + let pending_mod_evaluate = state.pending_mod_evaluate.as_mut().unwrap(); + pending_mod_evaluate.has_evaluated = true; + } + + // Update status after evaluating. + status = module.get_status(); + + let has_dispatched_exception = + state_rc.borrow_mut().dispatched_exception.is_some(); + if has_dispatched_exception { + // This will be overrided in `exception_to_err_result()`. + let exception = v8::undefined(tc_scope).into(); + let pending_mod_evaluate = { + let mut state = state_rc.borrow_mut(); + state.pending_mod_evaluate.take().unwrap() + }; + pending_mod_evaluate + .sender + .send(exception_to_err_result(tc_scope, exception, false)) + .expect("Failed to send module evaluation error."); + } else if let Some(value) = maybe_value { + assert!( + status == v8::ModuleStatus::Evaluated + || status == v8::ModuleStatus::Errored + ); + let promise = v8::Local::::try_from(value) + .expect("Expected to get promise as module evaluation result"); + let promise_global = v8::Global::new(tc_scope, promise); + let mut state = state_rc.borrow_mut(); + { + let pending_mod_evaluate = state.pending_mod_evaluate.as_ref().unwrap(); + let pending_rejection_was_already_handled = pending_mod_evaluate + .handled_promise_rejections + .contains(&promise_global); + if !pending_rejection_was_already_handled { + global_realm + .0 + .state() + .borrow_mut() + .pending_promise_rejections + .retain(|(key, _)| key != &promise_global); + } + } + let promise_global = v8::Global::new(tc_scope, promise); + state.pending_mod_evaluate.as_mut().unwrap().promise = + Some(promise_global); + tc_scope.perform_microtask_checkpoint(); + } else if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + let pending_mod_evaluate = { + let mut state = state_rc.borrow_mut(); + state.pending_mod_evaluate.take().unwrap() + }; + pending_mod_evaluate.sender.send(Err( + generic_error("Cannot evaluate module, because JavaScript execution has been terminated.") + )).expect("Failed to send module evaluation error."); + } else { + assert!(status == v8::ModuleStatus::Errored); + } + + receiver + } + + /// Clear the module map, meant to be used after initializing extensions. + /// Optionally pass a list of exceptions `(old_name, new_name)` representing + /// specifiers which will be renamed and preserved in the module map. + pub fn clear_module_map( + &self, + exceptions: impl Iterator, + ) { + let mut module_map = self.module_map.borrow_mut(); + let handles = exceptions + .map(|(old_name, new_name)| { + (module_map.get_handle_by_name(old_name).unwrap(), new_name) + }) + .collect::>(); + module_map.clear(); + for (handle, new_name) in handles { + module_map.inject_handle( + ModuleName::from_static(new_name), + ModuleType::JavaScript, + handle, + ) + } + } + + fn dynamic_import_reject( + &mut self, + id: ModuleLoadId, + exception: v8::Global, + ) { + let module_map_rc = self.module_map.clone(); + let scope = &mut self.handle_scope(); + + let resolver_handle = module_map_rc + .borrow_mut() + .dynamic_import_map + .remove(&id) + .expect("Invalid dynamic import id"); + let resolver = resolver_handle.open(scope); + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // rejecting the promise might initiate another `import()` which will + // in turn call `bindings::host_import_module_dynamically_callback` which + // will reach into `ModuleMap` from within the isolate. + let exception = v8::Local::new(scope, exception); + resolver.reject(scope, exception).unwrap(); + scope.perform_microtask_checkpoint(); + } + + fn dynamic_import_resolve(&mut self, id: ModuleLoadId, mod_id: ModuleId) { + let state_rc = self.inner.state.clone(); + let module_map_rc = self.module_map.clone(); + let scope = &mut self.handle_scope(); + + let resolver_handle = module_map_rc + .borrow_mut() + .dynamic_import_map + .remove(&id) + .expect("Invalid dynamic import id"); + let resolver = resolver_handle.open(scope); + + let module = { + module_map_rc + .borrow() + .get_handle(mod_id) + .map(|handle| v8::Local::new(scope, handle)) + .expect("Dyn import module info not found") + }; + // Resolution success + assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated); + + // IMPORTANT: No borrows to `ModuleMap` can be held at this point because + // resolving the promise might initiate another `import()` which will + // in turn call `bindings::host_import_module_dynamically_callback` which + // will reach into `ModuleMap` from within the isolate. + let module_namespace = module.get_module_namespace(); + resolver.resolve(scope, module_namespace).unwrap(); + state_rc.borrow_mut().dyn_module_evaluate_idle_counter = 0; + scope.perform_microtask_checkpoint(); + } + + fn prepare_dyn_imports( + &mut self, + cx: &mut Context, + ) -> Poll> { + if self + .module_map + .borrow() + .preparing_dynamic_imports + .is_empty() + { + return Poll::Ready(Ok(())); + } + + loop { + let poll_result = self + .module_map + .borrow_mut() + .preparing_dynamic_imports + .poll_next_unpin(cx); + + if let Poll::Ready(Some(prepare_poll)) = poll_result { + let dyn_import_id = prepare_poll.0; + let prepare_result = prepare_poll.1; + + match prepare_result { + Ok(load) => { + self + .module_map + .borrow_mut() + .pending_dynamic_imports + .push(load.into_future()); + } + Err(err) => { + let exception = to_v8_type_error(&mut self.handle_scope(), err); + self.dynamic_import_reject(dyn_import_id, exception); + } + } + // Continue polling for more prepared dynamic imports. + continue; + } + + // There are no active dynamic import loads, or none are ready. + return Poll::Ready(Ok(())); + } + } + + fn poll_dyn_imports(&mut self, cx: &mut Context) -> Poll> { + if self.module_map.borrow().pending_dynamic_imports.is_empty() { + return Poll::Ready(Ok(())); + } + + loop { + let poll_result = self + .module_map + .borrow_mut() + .pending_dynamic_imports + .poll_next_unpin(cx); + + if let Poll::Ready(Some(load_stream_poll)) = poll_result { + let maybe_result = load_stream_poll.0; + let mut load = load_stream_poll.1; + let dyn_import_id = load.id; + + if let Some(load_stream_result) = maybe_result { + match load_stream_result { + Ok((request, info)) => { + // A module (not necessarily the one dynamically imported) has been + // fetched. Create and register it, and if successful, poll for the + // next recursive-load event related to this dynamic import. + let register_result = load.register_and_recurse( + &mut self.handle_scope(), + &request, + info, + ); + + match register_result { + Ok(()) => { + // Keep importing until it's fully drained + self + .module_map + .borrow_mut() + .pending_dynamic_imports + .push(load.into_future()); + } + Err(err) => { + let exception = match err { + ModuleError::Exception(e) => e, + ModuleError::Other(e) => { + to_v8_type_error(&mut self.handle_scope(), e) + } + }; + self.dynamic_import_reject(dyn_import_id, exception) + } + } + } + Err(err) => { + // A non-javascript error occurred; this could be due to a an invalid + // module specifier, or a problem with the source map, or a failure + // to fetch the module source code. + let exception = to_v8_type_error(&mut self.handle_scope(), err); + self.dynamic_import_reject(dyn_import_id, exception); + } + } + } else { + // The top-level module from a dynamic import has been instantiated. + // Load is done. + let module_id = + load.root_module_id.expect("Root module should be loaded"); + let result = self.instantiate_module(module_id); + if let Err(exception) = result { + self.dynamic_import_reject(dyn_import_id, exception); + } + self.dynamic_import_module_evaluate(dyn_import_id, module_id)?; + } + + // Continue polling for more ready dynamic imports. + continue; + } + + // There are no active dynamic import loads, or none are ready. + return Poll::Ready(Ok(())); + } + } + + /// "deno_core" runs V8 with Top Level Await enabled. It means that each + /// module evaluation returns a promise from V8. + /// Feature docs: https://v8.dev/features/top-level-await + /// + /// This promise resolves after all dependent modules have also + /// resolved. Each dependent module may perform calls to "import()" and APIs + /// using async ops will add futures to the runtime's event loop. + /// It means that the promise returned from module evaluation will + /// resolve only after all futures in the event loop are done. + /// + /// Thus during turn of event loop we need to check if V8 has + /// resolved or rejected the promise. If the promise is still pending + /// then another turn of event loop must be performed. + fn evaluate_pending_module(&mut self) { + let maybe_module_evaluation = + self.inner.state.borrow_mut().pending_mod_evaluate.take(); + + if maybe_module_evaluation.is_none() { + return; + } + + let mut module_evaluation = maybe_module_evaluation.unwrap(); + let state_rc = self.inner.state.clone(); + let scope = &mut self.handle_scope(); + + let promise_global = module_evaluation.promise.clone().unwrap(); + let promise = promise_global.open(scope); + let promise_state = promise.state(); + + match promise_state { + v8::PromiseState::Pending => { + // NOTE: `poll_event_loop` will decide if + // runtime would be woken soon + state_rc.borrow_mut().pending_mod_evaluate = Some(module_evaluation); + } + v8::PromiseState::Fulfilled => { + scope.perform_microtask_checkpoint(); + // Receiver end might have been already dropped, ignore the result + let _ = module_evaluation.sender.send(Ok(())); + module_evaluation.handled_promise_rejections.clear(); + } + v8::PromiseState::Rejected => { + let exception = promise.result(scope); + scope.perform_microtask_checkpoint(); + + // Receiver end might have been already dropped, ignore the result + if module_evaluation + .handled_promise_rejections + .contains(&promise_global) + { + let _ = module_evaluation.sender.send(Ok(())); + module_evaluation.handled_promise_rejections.clear(); + } else { + let _ = module_evaluation + .sender + .send(exception_to_err_result(scope, exception, false)); + } + } + } + } + + // Returns true if some dynamic import was resolved. + fn evaluate_dyn_imports(&mut self) -> bool { + let pending = std::mem::take( + &mut self.inner.state.borrow_mut().pending_dyn_mod_evaluate, + ); + if pending.is_empty() { + return false; + } + let mut resolved_any = false; + let mut still_pending = vec![]; + for pending_dyn_evaluate in pending { + let maybe_result = { + let scope = &mut self.handle_scope(); + + let module_id = pending_dyn_evaluate.module_id; + let promise = pending_dyn_evaluate.promise.open(scope); + let _module = pending_dyn_evaluate.module.open(scope); + let promise_state = promise.state(); + + match promise_state { + v8::PromiseState::Pending => { + still_pending.push(pending_dyn_evaluate); + None + } + v8::PromiseState::Fulfilled => { + Some(Ok((pending_dyn_evaluate.load_id, module_id))) + } + v8::PromiseState::Rejected => { + let exception = promise.result(scope); + let exception = v8::Global::new(scope, exception); + Some(Err((pending_dyn_evaluate.load_id, exception))) + } + } + }; + + if let Some(result) = maybe_result { + resolved_any = true; + match result { + Ok((dyn_import_id, module_id)) => { + self.dynamic_import_resolve(dyn_import_id, module_id); + } + Err((dyn_import_id, exception)) => { + self.dynamic_import_reject(dyn_import_id, exception); + } + } + } + } + self.inner.state.borrow_mut().pending_dyn_mod_evaluate = still_pending; + resolved_any + } + + /// Asynchronously load specified module and all of its dependencies. + /// + /// The module will be marked as "main", and because of that + /// "import.meta.main" will return true when checked inside that module. + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_main_module( + &mut self, + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + let module_map_rc = self.module_map.clone(); + if let Some(code) = code { + let specifier = specifier.as_str().to_owned().into(); + let scope = &mut self.handle_scope(); + // true for main module + module_map_rc + .borrow_mut() + .new_es_module(scope, true, specifier, code, false) + .map_err(|e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + })?; + } + + let mut load = + ModuleMap::load_main(module_map_rc.clone(), &specifier).await?; + + while let Some(load_result) = load.next().await { + let (request, info) = load_result?; + let scope = &mut self.handle_scope(); + load.register_and_recurse(scope, &request, info).map_err( + |e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + }, + )?; + } + + let root_id = load.root_module_id.expect("Root module should be loaded"); + self.instantiate_module(root_id).map_err(|e| { + let scope = &mut self.handle_scope(); + let exception = v8::Local::new(scope, e); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + })?; + Ok(root_id) + } + + /// Asynchronously load specified ES module and all of its dependencies. + /// + /// This method is meant to be used when loading some utility code that + /// might be later imported by the main module (ie. an entry point module). + /// + /// User must call [`JsRuntime::mod_evaluate`] with returned `ModuleId` + /// manually after load is finished. + pub async fn load_side_module( + &mut self, + specifier: &ModuleSpecifier, + code: Option, + ) -> Result { + let module_map_rc = self.module_map.clone(); + if let Some(code) = code { + let specifier = specifier.as_str().to_owned().into(); + let scope = &mut self.handle_scope(); + // false for side module (not main module) + module_map_rc + .borrow_mut() + .new_es_module(scope, false, specifier, code, false) + .map_err(|e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + })?; + } + + let mut load = + ModuleMap::load_side(module_map_rc.clone(), &specifier).await?; + + while let Some(load_result) = load.next().await { + let (request, info) = load_result?; + let scope = &mut self.handle_scope(); + load.register_and_recurse(scope, &request, info).map_err( + |e| match e { + ModuleError::Exception(exception) => { + let exception = v8::Local::new(scope, exception); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + } + ModuleError::Other(error) => error, + }, + )?; + } + + let root_id = load.root_module_id.expect("Root module should be loaded"); + self.instantiate_module(root_id).map_err(|e| { + let scope = &mut self.handle_scope(); + let exception = v8::Local::new(scope, e); + exception_to_err_result::<()>(scope, exception, false).unwrap_err() + })?; + Ok(root_id) + } + + fn check_promise_rejections(&mut self) -> Result<(), Error> { + let state = self.inner.state.clone(); + let scope = &mut self.handle_scope(); + let state = state.borrow(); + for realm in &state.known_realms { + realm.check_promise_rejections(scope)?; + } + Ok(()) + } + + // Polls pending ops and then runs `Deno.core.eventLoopTick` callback. + fn do_js_event_loop_tick(&mut self, cx: &mut Context) -> Result<(), Error> { + // Handle responses for each realm. + let state = self.inner.state.clone(); + let isolate = &mut self.inner.v8_isolate; + let realm_count = state.borrow().known_realms.len(); + for realm_idx in 0..realm_count { + let realm = state.borrow().known_realms.get(realm_idx).unwrap().clone(); + let context_state = realm.state(); + let mut context_state = context_state.borrow_mut(); + let scope = &mut realm.handle_scope(isolate); + + // We return async responses to JS in unbounded batches (may change), + // each batch is a flat vector of tuples: + // `[promise_id1, op_result1, promise_id2, op_result2, ...]` + // promise_id is a simple integer, op_result is an ops::OpResult + // which contains a value OR an error, encoded as a tuple. + // This batch is received in JS via the special `arguments` variable + // and then each tuple is used to resolve or reject promises + // + // This can handle 15 promises futures in a single batch without heap + // allocations. + let mut args: SmallVec<[v8::Local; 32]> = + SmallVec::with_capacity(32); + + loop { + let item = { + let next = std::pin::pin!(context_state.pending_ops.join_next()); + let Poll::Ready(Some(item)) = next.poll(cx) else { + break; + }; + item + }; + let (promise_id, op_id, mut resp) = item.unwrap().into_inner(); + state + .borrow() + .op_state + .borrow() + .tracker + .track_async_completed(op_id); + context_state.unrefed_ops.remove(&promise_id); + args.push(v8::Integer::new(scope, promise_id).into()); + args.push(match resp.to_v8(scope) { + Ok(v) => v, + Err(e) => OpResult::Err(OpError::new(&|_| "TypeError", e.into())) + .to_v8(scope) + .unwrap(), + }); + } + + let has_tick_scheduled = + v8::Boolean::new(scope, self.inner.state.borrow().has_tick_scheduled); + args.push(has_tick_scheduled.into()); + + let js_event_loop_tick_cb_handle = + context_state.js_event_loop_tick_cb.clone().unwrap(); + let tc_scope = &mut v8::TryCatch::new(scope); + let js_event_loop_tick_cb = js_event_loop_tick_cb_handle.open(tc_scope); + let this = v8::undefined(tc_scope).into(); + drop(context_state); + js_event_loop_tick_cb.call(tc_scope, this, args.as_slice()); + + if let Some(exception) = tc_scope.exception() { + // TODO(@andreubotella): Returning here can cause async ops in other + // realms to never resolve. + return exception_to_err_result(tc_scope, exception, false); + } + + if tc_scope.has_terminated() || tc_scope.is_execution_terminating() { + return Ok(()); + } + } + + Ok(()) + } +} diff --git a/core/runtime/mod.rs b/core/runtime/mod.rs new file mode 100644 index 000000000..2bd3ea9fe --- /dev/null +++ b/core/runtime/mod.rs @@ -0,0 +1,35 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +mod bindings; +mod jsrealm; +mod jsruntime; +#[doc(hidden)] +pub mod ops; +mod snapshot_util; + +#[cfg(test)] +mod tests; + +pub const V8_WRAPPER_TYPE_INDEX: i32 = 0; +pub const V8_WRAPPER_OBJECT_INDEX: i32 = 1; + +pub(crate) use jsrealm::ContextState; +pub use jsrealm::JsRealm; +pub use jsruntime::CompiledWasmModuleStore; +pub use jsruntime::CrossIsolateStore; +pub(crate) use jsruntime::InitMode; +pub use jsruntime::JsRuntime; +pub use jsruntime::JsRuntimeForSnapshot; +pub use jsruntime::JsRuntimeState; +pub use jsruntime::RuntimeOptions; +pub use jsruntime::RuntimeSnapshotOptions; +pub use jsruntime::SharedArrayBufferStore; +pub use jsruntime::Snapshot; +pub use snapshot_util::create_snapshot; +pub use snapshot_util::get_js_files; +pub use snapshot_util::CreateSnapshotOptions; +pub use snapshot_util::CreateSnapshotOutput; +pub use snapshot_util::FilterFn; +pub(crate) use snapshot_util::SnapshottedData; + +pub use bindings::script_origin; +pub use bindings::throw_type_error; diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs new file mode 100644 index 000000000..c9e7fa6c7 --- /dev/null +++ b/core/runtime/ops.rs @@ -0,0 +1,156 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use crate::ops::*; +use crate::OpResult; +use crate::PromiseId; +use anyhow::Error; +use futures::future::Future; +use futures::future::FutureExt; +use futures::future::MaybeDone; +use futures::task::noop_waker; +use std::cell::RefCell; +use std::option::Option; +use std::pin::Pin; +use std::task::Context; +use std::task::Poll; + +#[inline] +pub fn queue_fast_async_op( + ctx: &OpCtx, + promise_id: PromiseId, + op: impl Future> + 'static, +) { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + let fut = op + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(); + // SAFETY: this this is guaranteed to be running on a current-thread executor + ctx.context_state.borrow_mut().pending_ops.spawn(unsafe { + crate::task::MaskFutureAsSend::new(OpCall::pending(ctx, promise_id, fut)) + }); +} + +#[inline] +pub fn map_async_op1( + ctx: &OpCtx, + op: impl Future> + 'static, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + let fut = op + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(); + MaybeDone::Future(fut) +} + +#[inline] +pub fn map_async_op2( + ctx: &OpCtx, + op: impl Future + 'static, +) -> MaybeDone>>> { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + + let fut = op.map(|result| OpResult::Ok(result.into())).boxed_local(); + MaybeDone::Future(fut) +} + +#[inline] +pub fn map_async_op3( + ctx: &OpCtx, + op: Result> + 'static, Error>, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + match op { + Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), + Ok(fut) => MaybeDone::Future( + fut + .map(|result| crate::_ops::to_op_result(get_class, result)) + .boxed_local(), + ), + } +} + +#[inline] +pub fn map_async_op4( + ctx: &OpCtx, + op: Result + 'static, Error>, +) -> MaybeDone>>> { + let get_class = { + let state = RefCell::borrow(&ctx.state); + state.tracker.track_async(ctx.id); + state.get_error_class_fn + }; + + match op { + Err(err) => MaybeDone::Done(OpResult::Err(OpError::new(get_class, err))), + Ok(fut) => MaybeDone::Future( + fut.map(|result| OpResult::Ok(result.into())).boxed_local(), + ), + } +} + +pub fn queue_async_op<'s>( + ctx: &OpCtx, + scope: &'s mut v8::HandleScope, + deferred: bool, + promise_id: PromiseId, + mut op: MaybeDone>>>, +) -> Option> { + // An op's realm (as given by `OpCtx::realm_idx`) must match the realm in + // which it is invoked. Otherwise, we might have cross-realm object exposure. + // deno_core doesn't currently support such exposure, even though embedders + // can cause them, so we panic in debug mode (since the check is expensive). + // TODO(mmastrac): Restore this + // debug_assert_eq!( + // runtime_state.borrow().context(ctx.realm_idx as usize, scope), + // Some(scope.get_current_context()) + // ); + + // All ops are polled immediately + let waker = noop_waker(); + let mut cx = Context::from_waker(&waker); + + // Note that MaybeDone returns () from the future + let op_call = match op.poll_unpin(&mut cx) { + Poll::Pending => { + let MaybeDone::Future(fut) = op else { + unreachable!() + }; + OpCall::pending(ctx, promise_id, fut) + } + Poll::Ready(_) => { + let mut op_result = Pin::new(&mut op).take_output().unwrap(); + // If the op is ready and is not marked as deferred we can immediately return + // the result. + if !deferred { + ctx.state.borrow_mut().tracker.track_async_completed(ctx.id); + return Some(op_result.to_v8(scope).unwrap()); + } + + OpCall::ready(ctx, promise_id, op_result) + } + }; + + // Otherwise we will push it to the `pending_ops` and let it be polled again + // or resolved on the next tick of the event loop. + ctx + .context_state + .borrow_mut() + .pending_ops + // SAFETY: this this is guaranteed to be running on a current-thread executor + .spawn(unsafe { crate::task::MaskFutureAsSend::new(op_call) }); + None +} diff --git a/core/runtime/serialize_deserialize_test.js b/core/runtime/serialize_deserialize_test.js new file mode 100644 index 000000000..70397cdf8 --- /dev/null +++ b/core/runtime/serialize_deserialize_test.js @@ -0,0 +1,74 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +"use strict"; + +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} + +function assertArrayEquals(a1, a2) { + if (a1.length !== a2.length) throw Error("assert"); + + for (const index in a1) { + if (a1[index] !== a2[index]) { + throw Error(`assert: (index ${index}) ${a1[index]} !== ${a2[index]}`); + } + } +} + +function main() { + const emptyString = ""; + const emptyStringSerialized = [255, 15, 34, 0]; + assertArrayEquals( + Deno.core.ops.op_serialize(emptyString), + emptyStringSerialized, + ); + assert( + Deno.core.ops.op_deserialize( + new Uint8Array(emptyStringSerialized), + ) === + emptyString, + ); + + const primitiveValueArray = ["test", "a", null, undefined]; + // deno-fmt-ignore + const primitiveValueArraySerialized = [ + 255, 15, 65, 4, 34, 4, 116, 101, 115, 116, + 34, 1, 97, 48, 95, 36, 0, 4, + ]; + assertArrayEquals( + Deno.core.ops.op_serialize(primitiveValueArray), + primitiveValueArraySerialized, + ); + + assertArrayEquals( + Deno.core.ops.op_deserialize( + new Uint8Array(primitiveValueArraySerialized), + ), + primitiveValueArray, + ); + + const circularObject = { test: null, test2: "dd", test3: "aa" }; + circularObject.test = circularObject; + // deno-fmt-ignore + const circularObjectSerialized = [ + 255, 15, 111, 34, 4, 116, 101, 115, + 116, 94, 0, 34, 5, 116, 101, 115, + 116, 50, 34, 2, 100, 100, 34, 5, + 116, 101, 115, 116, 51, 34, 2, 97, + 97, 123, 3, + ]; + + assertArrayEquals( + Deno.core.ops.op_serialize(circularObject), + circularObjectSerialized, + ); + + const deserializedCircularObject = Deno.core.ops.op_deserialize( + new Uint8Array(circularObjectSerialized), + ); + assert(deserializedCircularObject.test == deserializedCircularObject); +} + +main(); diff --git a/core/runtime/snapshot_util.rs b/core/runtime/snapshot_util.rs new file mode 100644 index 000000000..88c273147 --- /dev/null +++ b/core/runtime/snapshot_util.rs @@ -0,0 +1,252 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. + +use std::path::Path; +use std::path::PathBuf; +use std::time::Instant; + +use crate::runtime::RuntimeSnapshotOptions; +use crate::ExtModuleLoaderCb; +use crate::Extension; +use crate::JsRuntimeForSnapshot; +use crate::RuntimeOptions; +use crate::Snapshot; + +pub type CompressionCb = dyn Fn(&mut Vec, &[u8]); + +pub struct CreateSnapshotOptions { + pub cargo_manifest_dir: &'static str, + pub snapshot_path: PathBuf, + pub startup_snapshot: Option, + pub extensions: Vec, + pub compression_cb: Option>, + pub snapshot_module_load_cb: Option, +} + +pub struct CreateSnapshotOutput { + /// Any files marked as LoadedFromFsDuringSnapshot are collected here and should be + /// printed as 'cargo:rerun-if-changed' lines from your build script. + pub files_loaded_during_snapshot: Vec, +} + +#[must_use = "The files listed by create_snapshot should be printed as 'cargo:rerun-if-changed' lines"] +pub fn create_snapshot( + create_snapshot_options: CreateSnapshotOptions, +) -> CreateSnapshotOutput { + let mut mark = Instant::now(); + + let js_runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + startup_snapshot: create_snapshot_options.startup_snapshot, + extensions: create_snapshot_options.extensions, + ..Default::default() + }, + RuntimeSnapshotOptions { + snapshot_module_load_cb: create_snapshot_options.snapshot_module_load_cb, + }, + ); + println!( + "JsRuntime for snapshot prepared, took {:#?} ({})", + Instant::now().saturating_duration_since(mark), + create_snapshot_options.snapshot_path.display() + ); + mark = Instant::now(); + + let mut files_loaded_during_snapshot = vec![]; + for source in js_runtime + .extensions() + .iter() + .flat_map(|e| vec![e.get_esm_sources(), e.get_js_sources()]) + .flatten() + .flatten() + { + use crate::ExtensionFileSourceCode; + if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = + &source.code + { + files_loaded_during_snapshot.push(path.clone()); + } + } + + let snapshot = js_runtime.snapshot(); + let snapshot_slice: &[u8] = &snapshot; + println!( + "Snapshot size: {}, took {:#?} ({})", + snapshot_slice.len(), + Instant::now().saturating_duration_since(mark), + create_snapshot_options.snapshot_path.display() + ); + mark = Instant::now(); + + let maybe_compressed_snapshot: Box> = + if let Some(compression_cb) = create_snapshot_options.compression_cb { + let mut vec = vec![]; + + vec.extend_from_slice( + &u32::try_from(snapshot.len()) + .expect("snapshot larger than 4gb") + .to_le_bytes(), + ); + + (compression_cb)(&mut vec, snapshot_slice); + + println!( + "Snapshot compressed size: {}, took {:#?} ({})", + vec.len(), + Instant::now().saturating_duration_since(mark), + create_snapshot_options.snapshot_path.display() + ); + mark = std::time::Instant::now(); + + Box::new(vec) + } else { + Box::new(snapshot_slice) + }; + + std::fs::write( + &create_snapshot_options.snapshot_path, + &*maybe_compressed_snapshot, + ) + .unwrap(); + println!( + "Snapshot written, took: {:#?} ({})", + Instant::now().saturating_duration_since(mark), + create_snapshot_options.snapshot_path.display(), + ); + CreateSnapshotOutput { + files_loaded_during_snapshot, + } +} + +pub type FilterFn = Box bool>; + +pub fn get_js_files( + cargo_manifest_dir: &'static str, + directory: &str, + filter: Option, +) -> Vec { + let manifest_dir = Path::new(cargo_manifest_dir); + let mut js_files = std::fs::read_dir(directory) + .unwrap() + .map(|dir_entry| { + let file = dir_entry.unwrap(); + manifest_dir.join(file.path()) + }) + .filter(|path| { + path.extension().unwrap_or_default() == "js" + && filter.as_ref().map(|filter| filter(path)).unwrap_or(true) + }) + .collect::>(); + js_files.sort(); + js_files +} + +fn data_error_to_panic(err: v8::DataError) -> ! { + match err { + v8::DataError::BadType { actual, expected } => { + panic!( + "Invalid type for snapshot data: expected {expected}, got {actual}" + ); + } + v8::DataError::NoData { expected } => { + panic!("No data for snapshot data: expected {expected}"); + } + } +} + +pub(crate) struct SnapshottedData { + pub module_map_data: v8::Global, + pub module_handles: Vec>, +} + +static MODULE_MAP_CONTEXT_DATA_INDEX: usize = 0; + +pub(crate) fn get_snapshotted_data( + scope: &mut v8::HandleScope<()>, + context: v8::Local, +) -> SnapshottedData { + let mut scope = v8::ContextScope::new(scope, context); + + // The 0th element is the module map itself, followed by X number of module + // handles. We need to deserialize the "next_module_id" field from the + // map to see how many module handles we expect. + let result = scope.get_context_data_from_snapshot_once::( + MODULE_MAP_CONTEXT_DATA_INDEX, + ); + + let val = match result { + Ok(v) => v, + Err(err) => data_error_to_panic(err), + }; + + let next_module_id = { + let info_data: v8::Local = + val.get_index(&mut scope, 1).unwrap().try_into().unwrap(); + info_data.length() + }; + + // Over allocate so executing a few scripts doesn't have to resize this vec. + let mut module_handles = Vec::with_capacity(next_module_id as usize + 16); + for i in 1..=next_module_id { + match scope.get_context_data_from_snapshot_once::(i as usize) { + Ok(val) => { + let module_global = v8::Global::new(&mut scope, val); + module_handles.push(module_global); + } + Err(err) => data_error_to_panic(err), + } + } + + SnapshottedData { + module_map_data: v8::Global::new(&mut scope, val), + module_handles, + } +} + +pub(crate) fn set_snapshotted_data( + scope: &mut v8::HandleScope<()>, + context: v8::Global, + snapshotted_data: SnapshottedData, +) { + let local_context = v8::Local::new(scope, context); + let local_data = v8::Local::new(scope, snapshotted_data.module_map_data); + let offset = scope.add_context_data(local_context, local_data); + assert_eq!(offset, MODULE_MAP_CONTEXT_DATA_INDEX); + + for (index, handle) in snapshotted_data.module_handles.into_iter().enumerate() + { + let module_handle = v8::Local::new(scope, handle); + let offset = scope.add_context_data(local_context, module_handle); + assert_eq!(offset, index + 1); + } +} + +/// Returns an isolate set up for snapshotting. +pub(crate) fn create_snapshot_creator( + external_refs: &'static v8::ExternalReferences, + maybe_startup_snapshot: Option, +) -> v8::OwnedIsolate { + if let Some(snapshot) = maybe_startup_snapshot { + match snapshot { + Snapshot::Static(data) => { + v8::Isolate::snapshot_creator_from_existing_snapshot( + data, + Some(external_refs), + ) + } + Snapshot::JustCreated(data) => { + v8::Isolate::snapshot_creator_from_existing_snapshot( + data, + Some(external_refs), + ) + } + Snapshot::Boxed(data) => { + v8::Isolate::snapshot_creator_from_existing_snapshot( + data, + Some(external_refs), + ) + } + } + } else { + v8::Isolate::snapshot_creator(Some(external_refs)) + } +} diff --git a/core/runtime/tests.rs b/core/runtime/tests.rs new file mode 100644 index 000000000..857290b80 --- /dev/null +++ b/core/runtime/tests.rs @@ -0,0 +1,2306 @@ +// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. +use crate::ascii_str; +use crate::error::custom_error; +use crate::error::generic_error; +use crate::error::AnyError; +use crate::error::JsError; +use crate::extensions::OpDecl; +use crate::include_ascii_string; +use crate::module_specifier::ModuleSpecifier; +use crate::modules::AssertedModuleType; +use crate::modules::ModuleCode; +use crate::modules::ModuleInfo; +use crate::modules::ModuleLoadId; +use crate::modules::ModuleLoader; +use crate::modules::ModuleSource; +use crate::modules::ModuleSourceFuture; +use crate::modules::ModuleType; +use crate::modules::ResolutionKind; +use crate::modules::SymbolicModule; +use crate::Extension; +use crate::ZeroCopyBuf; +use crate::*; +use anyhow::Error; +use deno_ops::op; +use futures::future::poll_fn; +use futures::future::Future; +use futures::FutureExt; +use std::cell::RefCell; +use std::pin::Pin; +use std::rc::Rc; +use std::sync::atomic::AtomicUsize; +use std::sync::atomic::Ordering; +use std::sync::Arc; +use std::task::Context; +use std::task::Poll; + +// deno_ops macros generate code assuming deno_core in scope. +mod deno_core { + pub use crate::*; +} + +#[derive(Copy, Clone)] +pub enum Mode { + Async, + AsyncDeferred, + AsyncZeroCopy(bool), +} + +struct TestState { + mode: Mode, + dispatch_count: Arc, +} + +#[op] +async fn op_test( + rc_op_state: Rc>, + control: u8, + buf: Option, +) -> Result { + #![allow(clippy::await_holding_refcell_ref)] // False positive. + let op_state_ = rc_op_state.borrow(); + let test_state = op_state_.borrow::(); + test_state.dispatch_count.fetch_add(1, Ordering::Relaxed); + let mode = test_state.mode; + drop(op_state_); + match mode { + Mode::Async => { + assert_eq!(control, 42); + Ok(43) + } + Mode::AsyncDeferred => { + tokio::task::yield_now().await; + assert_eq!(control, 42); + Ok(43) + } + Mode::AsyncZeroCopy(has_buffer) => { + assert_eq!(buf.is_some(), has_buffer); + if let Some(buf) = buf { + assert_eq!(buf.len(), 1); + } + Ok(43) + } + } +} + +fn setup(mode: Mode) -> (JsRuntime, Arc) { + let dispatch_count = Arc::new(AtomicUsize::new(0)); + deno_core::extension!( + test_ext, + ops = [op_test], + options = { + mode: Mode, + dispatch_count: Arc, + }, + state = |state, options| { + state.put(TestState { + mode: options.mode, + dispatch_count: options.dispatch_count + }) + } + ); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops(mode, dispatch_count.clone())], + get_error_class_fn: Some(&|error| { + crate::error::get_custom_error_class(error).unwrap() + }), + ..Default::default() + }); + + runtime + .execute_script_static( + "setup.js", + r#" + function assert(cond) { + if (!cond) { + throw Error("assert"); + } + } + "#, + ) + .unwrap(); + assert_eq!(dispatch_count.load(Ordering::Relaxed), 0); + (runtime, dispatch_count) +} + +#[tokio::test] +async fn test_ref_unref_ops() { + let (mut runtime, _dispatch_count) = setup(Mode::AsyncDeferred); + runtime + .execute_script_static( + "filename.js", + r#" + + var promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); + var p1 = Deno.core.opAsync("op_test", 42); + var p2 = Deno.core.opAsync("op_test", 42); + "#, + ) + .unwrap(); + { + let realm = runtime.global_realm(); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 0); + } + runtime + .execute_script_static( + "filename.js", + r#" + Deno.core.ops.op_unref_op(p1[promiseIdSymbol]); + Deno.core.ops.op_unref_op(p2[promiseIdSymbol]); + "#, + ) + .unwrap(); + { + let realm = runtime.global_realm(); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 2); + } + runtime + .execute_script_static( + "filename.js", + r#" + Deno.core.ops.op_ref_op(p1[promiseIdSymbol]); + Deno.core.ops.op_ref_op(p2[promiseIdSymbol]); + "#, + ) + .unwrap(); + { + let realm = runtime.global_realm(); + assert_eq!(realm.num_pending_ops(), 2); + assert_eq!(realm.num_unrefed_ops(), 0); + } +} + +#[test] +fn test_dispatch() { + let (mut runtime, dispatch_count) = setup(Mode::Async); + runtime + .execute_script_static( + "filename.js", + r#" + let control = 42; + + Deno.core.opAsync("op_test", control); + async function main() { + Deno.core.opAsync("op_test", control); + } + main(); + "#, + ) + .unwrap(); + assert_eq!(dispatch_count.load(Ordering::Relaxed), 2); +} + +#[test] +fn test_op_async_promise_id() { + let (mut runtime, _dispatch_count) = setup(Mode::Async); + runtime + .execute_script_static( + "filename.js", + r#" + + const p = Deno.core.opAsync("op_test", 42); + if (p[Symbol.for("Deno.core.internalPromiseId")] == undefined) { + throw new Error("missing id on returned promise"); + } + "#, + ) + .unwrap(); +} + +#[test] +fn test_dispatch_no_zero_copy_buf() { + let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(false)); + runtime + .execute_script_static( + "filename.js", + r#" + + Deno.core.opAsync("op_test"); + "#, + ) + .unwrap(); + assert_eq!(dispatch_count.load(Ordering::Relaxed), 1); +} + +#[test] +fn test_dispatch_stack_zero_copy_bufs() { + let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(true)); + runtime + .execute_script_static( + "filename.js", + r#" + const { op_test } = Deno.core.ensureFastOps(); + let zero_copy_a = new Uint8Array([0]); + op_test(null, zero_copy_a); + "#, + ) + .unwrap(); + assert_eq!(dispatch_count.load(Ordering::Relaxed), 1); +} + +#[test] +fn test_execute_script_return_value() { + let mut runtime = JsRuntime::new(Default::default()); + let value_global = + runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); + { + let scope = &mut runtime.handle_scope(); + let value = value_global.open(scope); + assert_eq!(value.integer_value(scope).unwrap(), 3); + } + let value_global = runtime + .execute_script_static("b.js", "b = 'foobar'") + .unwrap(); + { + let scope = &mut runtime.handle_scope(); + let value = value_global.open(scope); + assert!(value.is_string()); + assert_eq!( + value.to_string(scope).unwrap().to_rust_string_lossy(scope), + "foobar" + ); + } +} + +#[tokio::test] +async fn test_poll_value() { + let mut runtime = JsRuntime::new(Default::default()); + poll_fn(move |cx| { + let value_global = runtime + .execute_script_static("a.js", "Promise.resolve(1 + 2)") + .unwrap(); + let v = runtime.poll_value(&value_global, cx); + { + let scope = &mut runtime.handle_scope(); + assert!( + matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 3) + ); + } + + let value_global = runtime + .execute_script_static( + "a.js", + "Promise.resolve(new Promise(resolve => resolve(2 + 2)))", + ) + .unwrap(); + let v = runtime.poll_value(&value_global, cx); + { + let scope = &mut runtime.handle_scope(); + assert!( + matches!(v, Poll::Ready(Ok(v)) if v.open(scope).integer_value(scope).unwrap() == 4) + ); + } + + let value_global = runtime + .execute_script_static("a.js", "Promise.reject(new Error('fail'))") + .unwrap(); + let v = runtime.poll_value(&value_global, cx); + assert!( + matches!(v, Poll::Ready(Err(e)) if e.downcast_ref::().unwrap().exception_message == "Uncaught Error: fail") + ); + + let value_global = runtime + .execute_script_static("a.js", "new Promise(resolve => {})") + .unwrap(); + let v = runtime.poll_value(&value_global, cx); + matches!(v, Poll::Ready(Err(e)) if e.to_string() == "Promise resolution is still pending but the event loop has already resolved."); + Poll::Ready(()) + }).await; +} + +#[tokio::test] +async fn test_resolve_value() { + let mut runtime = JsRuntime::new(Default::default()); + let value_global = runtime + .execute_script_static("a.js", "Promise.resolve(1 + 2)") + .unwrap(); + let result_global = runtime.resolve_value(value_global).await.unwrap(); + { + let scope = &mut runtime.handle_scope(); + let value = result_global.open(scope); + assert_eq!(value.integer_value(scope).unwrap(), 3); + } + + let value_global = runtime + .execute_script_static( + "a.js", + "Promise.resolve(new Promise(resolve => resolve(2 + 2)))", + ) + .unwrap(); + let result_global = runtime.resolve_value(value_global).await.unwrap(); + { + let scope = &mut runtime.handle_scope(); + let value = result_global.open(scope); + assert_eq!(value.integer_value(scope).unwrap(), 4); + } + + let value_global = runtime + .execute_script_static("a.js", "Promise.reject(new Error('fail'))") + .unwrap(); + let err = runtime.resolve_value(value_global).await.unwrap_err(); + assert_eq!( + "Uncaught Error: fail", + err.downcast::().unwrap().exception_message + ); + + let value_global = runtime + .execute_script_static("a.js", "new Promise(resolve => {})") + .unwrap(); + let error_string = runtime + .resolve_value(value_global) + .await + .unwrap_err() + .to_string(); + assert_eq!( + "Promise resolution is still pending but the event loop has already resolved.", + error_string, + ); +} + +#[test] +fn terminate_execution_webassembly() { + let (mut runtime, _dispatch_count) = setup(Mode::Async); + let v8_isolate_handle = runtime.v8_isolate().thread_safe_handle(); + + // Run an infinite loop in Webassemby code, which should be terminated. + let promise = runtime.execute_script_static("infinite_wasm_loop.js", + r#" + (async () => { + const wasmCode = new Uint8Array([ + 0, 97, 115, 109, 1, 0, 0, 0, 1, 4, 1, + 96, 0, 0, 3, 2, 1, 0, 7, 17, 1, 13, + 105, 110, 102, 105, 110, 105, 116, 101, 95, 108, 111, + 111, 112, 0, 0, 10, 9, 1, 7, 0, 3, 64, + 12, 0, 11, 11, + ]); + const wasmModule = await WebAssembly.compile(wasmCode); + globalThis.wasmInstance = new WebAssembly.Instance(wasmModule); + })() + "#).unwrap(); + futures::executor::block_on(runtime.resolve_value(promise)).unwrap(); + let terminator_thread = std::thread::spawn(move || { + std::thread::sleep(std::time::Duration::from_millis(1000)); + + // terminate execution + let ok = v8_isolate_handle.terminate_execution(); + assert!(ok); + }); + let err = runtime + .execute_script_static( + "infinite_wasm_loop2.js", + "globalThis.wasmInstance.exports.infinite_loop();", + ) + .unwrap_err(); + assert_eq!(err.to_string(), "Uncaught Error: execution terminated"); + // Cancel the execution-terminating exception in order to allow script + // execution again. + let ok = runtime.v8_isolate().cancel_terminate_execution(); + assert!(ok); + + // Verify that the isolate usable again. + runtime + .execute_script_static("simple.js", "1 + 1") + .expect("execution should be possible again"); + + terminator_thread.join().unwrap(); +} + +#[test] +fn terminate_execution() { + let (mut isolate, _dispatch_count) = setup(Mode::Async); + let v8_isolate_handle = isolate.v8_isolate().thread_safe_handle(); + + let terminator_thread = std::thread::spawn(move || { + // allow deno to boot and run + std::thread::sleep(std::time::Duration::from_millis(100)); + + // terminate execution + let ok = v8_isolate_handle.terminate_execution(); + assert!(ok); + }); + + // Rn an infinite loop, which should be terminated. + match isolate.execute_script_static("infinite_loop.js", "for(;;) {}") { + Ok(_) => panic!("execution should be terminated"), + Err(e) => { + assert_eq!(e.to_string(), "Uncaught Error: execution terminated") + } + }; + + // Cancel the execution-terminating exception in order to allow script + // execution again. + let ok = isolate.v8_isolate().cancel_terminate_execution(); + assert!(ok); + + // Verify that the isolate usable again. + isolate + .execute_script_static("simple.js", "1 + 1") + .expect("execution should be possible again"); + + terminator_thread.join().unwrap(); +} + +#[test] +fn dangling_shared_isolate() { + let v8_isolate_handle = { + // isolate is dropped at the end of this block + let (mut runtime, _dispatch_count) = setup(Mode::Async); + runtime.v8_isolate().thread_safe_handle() + }; + + // this should not SEGFAULT + v8_isolate_handle.terminate_execution(); +} + +#[test] +fn syntax_error() { + let mut runtime = JsRuntime::new(Default::default()); + let src = "hocuspocus("; + let r = runtime.execute_script_static("i.js", src); + let e = r.unwrap_err(); + let js_error = e.downcast::().unwrap(); + let frame = js_error.frames.first().unwrap(); + assert_eq!(frame.column_number, Some(12)); +} + +#[tokio::test] +async fn test_encode_decode() { + let (mut runtime, _dispatch_count) = setup(Mode::Async); + poll_fn(move |cx| { + runtime + .execute_script( + "encode_decode_test.js", + // Note: We make this to_owned because it contains non-ASCII chars + include_str!("encode_decode_test.js").to_owned().into(), + ) + .unwrap(); + if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { + unreachable!(); + } + Poll::Ready(()) + }) + .await; +} + +#[tokio::test] +async fn test_serialize_deserialize() { + let (mut runtime, _dispatch_count) = setup(Mode::Async); + poll_fn(move |cx| { + runtime + .execute_script( + "serialize_deserialize_test.js", + include_ascii_string!("serialize_deserialize_test.js"), + ) + .unwrap(); + if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { + unreachable!(); + } + Poll::Ready(()) + }) + .await; +} + +#[tokio::test] +async fn test_error_builder() { + #[op] + fn op_err() -> Result<(), Error> { + Err(custom_error("DOMExceptionOperationError", "abc")) + } + + pub fn get_error_class_name(_: &Error) -> &'static str { + "DOMExceptionOperationError" + } + + deno_core::extension!(test_ext, ops = [op_err]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + get_error_class_fn: Some(&get_error_class_name), + ..Default::default() + }); + poll_fn(move |cx| { + runtime + .execute_script_static( + "error_builder_test.js", + include_str!("error_builder_test.js"), + ) + .unwrap(); + if let Poll::Ready(Err(_)) = runtime.poll_event_loop(cx, false) { + unreachable!(); + } + Poll::Ready(()) + }) + .await; +} + +/// Ensure that putting the inspector into OpState doesn't cause crashes. The only valid place we currently allow +/// the inspector to be stashed without cleanup is the OpState, and this should not actually cause crashes. +#[test] +fn inspector() { + let mut runtime = JsRuntime::new(RuntimeOptions { + inspector: true, + ..Default::default() + }); + // This was causing a crash + runtime.op_state().borrow_mut().put(runtime.inspector()); + runtime.execute_script_static("check.js", "null").unwrap(); +} + +#[test] +fn will_snapshot() { + let snapshot = { + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); + runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); + runtime.snapshot() + }; + + let snapshot = Snapshot::JustCreated(snapshot); + let mut runtime2 = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }); + runtime2 + .execute_script_static("check.js", "if (a != 3) throw Error('x')") + .unwrap(); +} + +#[test] +fn will_snapshot2() { + let startup_data = { + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); + runtime + .execute_script_static("a.js", "let a = 1 + 2") + .unwrap(); + runtime.snapshot() + }; + + let snapshot = Snapshot::JustCreated(startup_data); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }, + Default::default(), + ); + + let startup_data = { + runtime + .execute_script_static("check_a.js", "if (a != 3) throw Error('x')") + .unwrap(); + runtime.execute_script_static("b.js", "b = 2 + 3").unwrap(); + runtime.snapshot() + }; + + let snapshot = Snapshot::JustCreated(startup_data); + { + let mut runtime = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }); + runtime + .execute_script_static("check_b.js", "if (b != 5) throw Error('x')") + .unwrap(); + runtime + .execute_script_static("check2.js", "if (!Deno.core) throw Error('x')") + .unwrap(); + } +} + +#[test] +fn test_snapshot_callbacks() { + let snapshot = { + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); + runtime + .execute_script_static( + "a.js", + r#" + Deno.core.setMacrotaskCallback(() => { + return true; + }); + Deno.core.ops.op_set_format_exception_callback(()=> { + return null; + }) + Deno.core.setPromiseRejectCallback(() => { + return false; + }); + a = 1 + 2; + "#, + ) + .unwrap(); + runtime.snapshot() + }; + + let snapshot = Snapshot::JustCreated(snapshot); + let mut runtime2 = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }); + runtime2 + .execute_script_static("check.js", "if (a != 3) throw Error('x')") + .unwrap(); +} + +#[test] +fn test_from_boxed_snapshot() { + let snapshot = { + let mut runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); + runtime.execute_script_static("a.js", "a = 1 + 2").unwrap(); + let snap: &[u8] = &runtime.snapshot(); + Vec::from(snap).into_boxed_slice() + }; + + let snapshot = Snapshot::Boxed(snapshot); + let mut runtime2 = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(snapshot), + ..Default::default() + }); + runtime2 + .execute_script_static("check.js", "if (a != 3) throw Error('x')") + .unwrap(); +} + +#[test] +fn test_get_module_namespace() { + #[derive(Default)] + struct ModsLoader; + + impl ModuleLoader for ModsLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _kind: ResolutionKind, + ) -> Result { + assert_eq!(specifier, "file:///main.js"); + assert_eq!(referrer, "."); + let s = crate::resolve_import(specifier, referrer).unwrap(); + Ok(s) + } + + fn load( + &self, + _module_specifier: &ModuleSpecifier, + _maybe_referrer: Option<&ModuleSpecifier>, + _is_dyn_import: bool, + ) -> Pin> { + async { Err(generic_error("Module loading is not supported")) } + .boxed_local() + } + } + + let loader = std::rc::Rc::new(ModsLoader::default()); + let mut runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader), + ..Default::default() + }); + + let specifier = crate::resolve_url("file:///main.js").unwrap(); + let source_code = ascii_str!( + r#" + export const a = "b"; + export default 1 + 2; + "# + ); + + let module_id = futures::executor::block_on( + runtime.load_main_module(&specifier, Some(source_code)), + ) + .unwrap(); + + #[allow(clippy::let_underscore_future)] + let _ = runtime.mod_evaluate(module_id); + + let module_namespace = runtime.get_module_namespace(module_id).unwrap(); + + let scope = &mut runtime.handle_scope(); + + let module_namespace = v8::Local::::new(scope, module_namespace); + + assert!(module_namespace.is_module_namespace_object()); + + let unknown_export_name = v8::String::new(scope, "none").unwrap(); + let binding = module_namespace.get(scope, unknown_export_name.into()); + + assert!(binding.is_some()); + assert!(binding.unwrap().is_undefined()); + + let empty_export_name = v8::String::new(scope, "").unwrap(); + let binding = module_namespace.get(scope, empty_export_name.into()); + + assert!(binding.is_some()); + assert!(binding.unwrap().is_undefined()); + + let a_export_name = v8::String::new(scope, "a").unwrap(); + let binding = module_namespace.get(scope, a_export_name.into()); + + assert!(binding.unwrap().is_string()); + assert_eq!(binding.unwrap(), v8::String::new(scope, "b").unwrap()); + + let default_export_name = v8::String::new(scope, "default").unwrap(); + let binding = module_namespace.get(scope, default_export_name.into()); + + assert!(binding.unwrap().is_number()); + assert_eq!(binding.unwrap(), v8::Number::new(scope, 3_f64)); +} + +#[test] +fn test_heap_limits() { + let create_params = + v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024); + let mut runtime = JsRuntime::new(RuntimeOptions { + create_params: Some(create_params), + ..Default::default() + }); + let cb_handle = runtime.v8_isolate().thread_safe_handle(); + + let callback_invoke_count = Rc::new(AtomicUsize::new(0)); + let inner_invoke_count = Rc::clone(&callback_invoke_count); + + runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| { + inner_invoke_count.fetch_add(1, Ordering::SeqCst); + cb_handle.terminate_execution(); + current_limit * 2 + }); + let err = runtime + .execute_script_static( + "script name", + r#"let s = ""; while(true) { s += "Hello"; }"#, + ) + .expect_err("script should fail"); + assert_eq!( + "Uncaught Error: execution terminated", + err.downcast::().unwrap().exception_message + ); + assert!(callback_invoke_count.load(Ordering::SeqCst) > 0) +} + +#[test] +fn test_heap_limit_cb_remove() { + let mut runtime = JsRuntime::new(Default::default()); + + runtime.add_near_heap_limit_callback(|current_limit, _initial_limit| { + current_limit * 2 + }); + runtime.remove_near_heap_limit_callback(3 * 1024 * 1024); + assert!(runtime.allocations.near_heap_limit_callback_data.is_none()); +} + +#[test] +fn test_heap_limit_cb_multiple() { + let create_params = + v8::Isolate::create_params().heap_limits(0, 5 * 1024 * 1024); + let mut runtime = JsRuntime::new(RuntimeOptions { + create_params: Some(create_params), + ..Default::default() + }); + let cb_handle = runtime.v8_isolate().thread_safe_handle(); + + let callback_invoke_count_first = Rc::new(AtomicUsize::new(0)); + let inner_invoke_count_first = Rc::clone(&callback_invoke_count_first); + runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| { + inner_invoke_count_first.fetch_add(1, Ordering::SeqCst); + current_limit * 2 + }); + + let callback_invoke_count_second = Rc::new(AtomicUsize::new(0)); + let inner_invoke_count_second = Rc::clone(&callback_invoke_count_second); + runtime.add_near_heap_limit_callback(move |current_limit, _initial_limit| { + inner_invoke_count_second.fetch_add(1, Ordering::SeqCst); + cb_handle.terminate_execution(); + current_limit * 2 + }); + + let err = runtime + .execute_script_static( + "script name", + r#"let s = ""; while(true) { s += "Hello"; }"#, + ) + .expect_err("script should fail"); + assert_eq!( + "Uncaught Error: execution terminated", + err.downcast::().unwrap().exception_message + ); + assert_eq!(0, callback_invoke_count_first.load(Ordering::SeqCst)); + assert!(callback_invoke_count_second.load(Ordering::SeqCst) > 0); +} + +#[test] +fn es_snapshot() { + #[derive(Default)] + struct ModsLoader; + + impl ModuleLoader for ModsLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _kind: ResolutionKind, + ) -> Result { + let s = crate::resolve_import(specifier, referrer).unwrap(); + Ok(s) + } + + fn load( + &self, + _module_specifier: &ModuleSpecifier, + _maybe_referrer: Option<&ModuleSpecifier>, + _is_dyn_import: bool, + ) -> Pin> { + eprintln!("load() should not be called"); + unreachable!() + } + } + + fn create_module( + runtime: &mut JsRuntime, + i: usize, + main: bool, + ) -> ModuleInfo { + let specifier = crate::resolve_url(&format!("file:///{i}.js")).unwrap(); + let prev = i - 1; + let source_code = format!( + r#" + import {{ f{prev} }} from "file:///{prev}.js"; + export function f{i}() {{ return f{prev}() }} + "# + ) + .into(); + + let id = if main { + futures::executor::block_on( + runtime.load_main_module(&specifier, Some(source_code)), + ) + .unwrap() + } else { + futures::executor::block_on( + runtime.load_side_module(&specifier, Some(source_code)), + ) + .unwrap() + }; + assert_eq!(i, id); + + #[allow(clippy::let_underscore_future)] + let _ = runtime.mod_evaluate(id); + futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); + + ModuleInfo { + id, + main, + name: specifier.into(), + requests: vec![crate::modules::ModuleRequest { + specifier: format!("file:///{prev}.js"), + asserted_module_type: AssertedModuleType::JavaScriptOrWasm, + }], + module_type: ModuleType::JavaScript, + } + } + + fn assert_module_map(runtime: &mut JsRuntime, modules: &Vec) { + let module_map = runtime.module_map.borrow(); + assert_eq!(module_map.handles.len(), modules.len()); + assert_eq!(module_map.info.len(), modules.len()); + assert_eq!( + module_map.by_name(AssertedModuleType::Json).len() + + module_map + .by_name(AssertedModuleType::JavaScriptOrWasm) + .len(), + modules.len() + ); + + assert_eq!(module_map.next_load_id, (modules.len() + 1) as ModuleLoadId); + + for info in modules { + assert!(module_map.handles.get(info.id).is_some()); + assert_eq!(module_map.info.get(info.id).unwrap(), info); + assert_eq!( + module_map + .by_name(AssertedModuleType::JavaScriptOrWasm) + .get(&info.name) + .unwrap(), + &SymbolicModule::Mod(info.id) + ); + } + } + + #[op] + fn op_test() -> Result { + Ok(String::from("test")) + } + + let loader = Rc::new(ModsLoader::default()); + let mut runtime = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader.clone()), + extensions: vec![Extension::builder("text_ext") + .ops(vec![op_test::decl()]) + .build()], + ..Default::default() + }, + Default::default(), + ); + + let specifier = crate::resolve_url("file:///0.js").unwrap(); + let source_code = + ascii_str!(r#"export function f0() { return "hello world" }"#); + let id = futures::executor::block_on( + runtime.load_side_module(&specifier, Some(source_code)), + ) + .unwrap(); + + #[allow(clippy::let_underscore_future)] + let _ = runtime.mod_evaluate(id); + futures::executor::block_on(runtime.run_event_loop(false)).unwrap(); + + let mut modules = vec![]; + modules.push(ModuleInfo { + id, + main: false, + name: specifier.into(), + requests: vec![], + module_type: ModuleType::JavaScript, + }); + + modules.extend((1..200).map(|i| create_module(&mut runtime, i, false))); + + assert_module_map(&mut runtime, &modules); + + let snapshot = runtime.snapshot(); + + let mut runtime2 = JsRuntimeForSnapshot::new( + RuntimeOptions { + module_loader: Some(loader.clone()), + startup_snapshot: Some(Snapshot::JustCreated(snapshot)), + extensions: vec![Extension::builder("text_ext") + .ops(vec![op_test::decl()]) + .build()], + ..Default::default() + }, + Default::default(), + ); + + assert_module_map(&mut runtime2, &modules); + + modules.extend((200..400).map(|i| create_module(&mut runtime2, i, false))); + modules.push(create_module(&mut runtime2, 400, true)); + + assert_module_map(&mut runtime2, &modules); + + let snapshot2 = runtime2.snapshot(); + + let mut runtime3 = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader), + startup_snapshot: Some(Snapshot::JustCreated(snapshot2)), + extensions: vec![Extension::builder("text_ext") + .ops(vec![op_test::decl()]) + .build()], + ..Default::default() + }); + + assert_module_map(&mut runtime3, &modules); + + let source_code = r#"(async () => { + const mod = await import("file:///400.js"); + return mod.f400() + " " + Deno.core.ops.op_test(); + })();"#; + let val = runtime3.execute_script_static(".", source_code).unwrap(); + let val = futures::executor::block_on(runtime3.resolve_value(val)).unwrap(); + { + let scope = &mut runtime3.handle_scope(); + let value = v8::Local::new(scope, val); + let str_ = value.to_string(scope).unwrap().to_rust_string_lossy(scope); + assert_eq!(str_, "hello world test"); + } +} + +#[test] +fn test_error_without_stack() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + // SyntaxError + let result = runtime.execute_script_static( + "error_without_stack.js", + r#" +function main() { + console.log("asdf); +} +main(); +"#, + ); + let expected_error = r#"Uncaught SyntaxError: Invalid or unexpected token + at error_without_stack.js:3:15"#; + assert_eq!(result.unwrap_err().to_string(), expected_error); +} + +#[test] +fn test_error_stack() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + let result = runtime.execute_script_static( + "error_stack.js", + r#" +function assert(cond) { + if (!cond) { + throw Error("assert"); + } +} +function main() { + assert(false); +} +main(); + "#, + ); + let expected_error = r#"Error: assert + at assert (error_stack.js:4:11) + at main (error_stack.js:8:3) + at error_stack.js:10:1"#; + assert_eq!(result.unwrap_err().to_string(), expected_error); +} + +#[tokio::test] +async fn test_error_async_stack() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + poll_fn(move |cx| { + runtime + .execute_script_static( + "error_async_stack.js", + r#" + (async () => { + const p = (async () => { + await Promise.resolve().then(() => { + throw new Error("async"); + }); + })(); + try { + await p; + } catch (error) { + console.log(error.stack); + throw error; + } + })();"#, + ) + .unwrap(); + let expected_error = r#"Error: async + at error_async_stack.js:5:13 + at async error_async_stack.js:4:5 + at async error_async_stack.js:9:5"#; + + match runtime.poll_event_loop(cx, false) { + Poll::Ready(Err(e)) => { + assert_eq!(e.to_string(), expected_error); + } + _ => panic!(), + }; + Poll::Ready(()) + }) + .await; +} + +#[tokio::test] +async fn test_error_context() { + use anyhow::anyhow; + + #[op] + fn op_err_sync() -> Result<(), Error> { + Err(anyhow!("original sync error").context("higher-level sync error")) + } + + #[op] + async fn op_err_async() -> Result<(), Error> { + Err(anyhow!("original async error").context("higher-level async error")) + } + + deno_core::extension!(test_ext, ops = [op_err_sync, op_err_async]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + poll_fn(move |cx| { + runtime + .execute_script_static( + "test_error_context_sync.js", + r#" +let errMessage; +try { + Deno.core.ops.op_err_sync(); +} catch (err) { + errMessage = err.message; +} +if (errMessage !== "higher-level sync error: original sync error") { + throw new Error("unexpected error message from op_err_sync: " + errMessage); +} +"#, + ) + .unwrap(); + + let promise = runtime + .execute_script_static( + "test_error_context_async.js", + r#" + +(async () => { +let errMessage; +try { + await Deno.core.opAsync("op_err_async"); +} catch (err) { + errMessage = err.message; +} +if (errMessage !== "higher-level async error: original async error") { + throw new Error("unexpected error message from op_err_async: " + errMessage); +} +})() +"#, + ) + .unwrap(); + + match runtime.poll_value(&promise, cx) { + Poll::Ready(Ok(_)) => {} + Poll::Ready(Err(err)) => panic!("{err:?}"), + _ => panic!(), + } + Poll::Ready(()) + }) + .await; +} + +#[tokio::test] +async fn test_pump_message_loop() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + poll_fn(move |cx| { + runtime + .execute_script_static( + "pump_message_loop.js", + r#" +function assertEquals(a, b) { +if (a === b) return; +throw a + " does not equal " + b; +} +const sab = new SharedArrayBuffer(16); +const i32a = new Int32Array(sab); +globalThis.resolved = false; +(function() { +const result = Atomics.waitAsync(i32a, 0, 0); +result.value.then( + (value) => { assertEquals("ok", value); globalThis.resolved = true; }, + () => { assertUnreachable(); +}); +})(); +const notify_return_value = Atomics.notify(i32a, 0, 1); +assertEquals(1, notify_return_value); +"#, + ) + .unwrap(); + + match runtime.poll_event_loop(cx, false) { + Poll::Ready(Ok(())) => {} + _ => panic!(), + }; + + // noop script, will resolve promise from first script + runtime + .execute_script_static("pump_message_loop2.js", r#"assertEquals(1, 1);"#) + .unwrap(); + + // check that promise from `Atomics.waitAsync` has been resolved + runtime + .execute_script_static( + "pump_message_loop3.js", + r#"assertEquals(globalThis.resolved, true);"#, + ) + .unwrap(); + Poll::Ready(()) + }) + .await; +} + +#[test] +fn test_v8_platform() { + let options = RuntimeOptions { + v8_platform: Some(v8::new_default_platform(0, false).make_shared()), + ..Default::default() + }; + let mut runtime = JsRuntime::new(options); + runtime.execute_script_static("", "").unwrap(); +} + +#[ignore] // TODO(@littledivy): Fast API ops when snapshot is not loaded. +#[test] +fn test_is_proxy() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + let all_true: v8::Global = runtime + .execute_script_static( + "is_proxy.js", + r#" + (function () { + const o = { a: 1, b: 2}; + const p = new Proxy(o, {}); + return Deno.core.ops.op_is_proxy(p) && !Deno.core.ops.op_is_proxy(o) && !Deno.core.ops.op_is_proxy(42); + })() + "#, + ) + .unwrap(); + let mut scope = runtime.handle_scope(); + let all_true = v8::Local::::new(&mut scope, &all_true); + assert!(all_true.is_true()); +} + +#[tokio::test] +async fn test_async_opstate_borrow() { + struct InnerState(u64); + + #[op] + async fn op_async_borrow( + op_state: Rc>, + ) -> Result<(), Error> { + let n = { + let op_state = op_state.borrow(); + let inner_state = op_state.borrow::(); + inner_state.0 + }; + // Future must be Poll::Pending on first call + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + if n != 42 { + unreachable!(); + } + Ok(()) + } + + deno_core::extension!( + test_ext, + ops = [op_async_borrow], + state = |state| state.put(InnerState(42)) + ); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "op_async_borrow.js", + "Deno.core.opAsync(\"op_async_borrow\")", + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap(); +} + +#[tokio::test] +async fn test_sync_op_serialize_object_with_numbers_as_keys() { + #[op] + fn op_sync_serialize_object_with_numbers_as_keys( + value: serde_json::Value, + ) -> Result<(), Error> { + assert_eq!( + value.to_string(), + r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"# + ); + Ok(()) + } + + deno_core::extension!( + test_ext, + ops = [op_sync_serialize_object_with_numbers_as_keys] + ); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "op_sync_serialize_object_with_numbers_as_keys.js", + r#" +Deno.core.ops.op_sync_serialize_object_with_numbers_as_keys({ +lines: { + 100: { + unit: "m" + }, + 200: { + unit: "cm" + } +} +}) +"#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap(); +} + +#[tokio::test] +async fn test_async_op_serialize_object_with_numbers_as_keys() { + #[op] + async fn op_async_serialize_object_with_numbers_as_keys( + value: serde_json::Value, + ) -> Result<(), Error> { + assert_eq!( + value.to_string(), + r#"{"lines":{"100":{"unit":"m"},"200":{"unit":"cm"}}}"# + ); + Ok(()) + } + + deno_core::extension!( + test_ext, + ops = [op_async_serialize_object_with_numbers_as_keys] + ); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "op_async_serialize_object_with_numbers_as_keys.js", + r#" + +Deno.core.opAsync("op_async_serialize_object_with_numbers_as_keys", { +lines: { + 100: { + unit: "m" + }, + 200: { + unit: "cm" + } +} +}) +"#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap(); +} + +#[tokio::test] +async fn test_set_macrotask_callback_set_next_tick_callback() { + #[op] + async fn op_async_sleep() -> Result<(), Error> { + // Future must be Poll::Pending on first call + tokio::time::sleep(std::time::Duration::from_millis(1)).await; + Ok(()) + } + + deno_core::extension!(test_ext, ops = [op_async_sleep]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "macrotasks_and_nextticks.js", + r#" + + (async function () { + const results = []; + Deno.core.setMacrotaskCallback(() => { + results.push("macrotask"); + return true; + }); + Deno.core.setNextTickCallback(() => { + results.push("nextTick"); + Deno.core.ops.op_set_has_tick_scheduled(false); + }); + Deno.core.ops.op_set_has_tick_scheduled(true); + await Deno.core.opAsync('op_async_sleep'); + if (results[0] != "nextTick") { + throw new Error(`expected nextTick, got: ${results[0]}`); + } + if (results[1] != "macrotask") { + throw new Error(`expected macrotask, got: ${results[1]}`); + } + })(); + "#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap(); +} + +#[test] +fn test_has_tick_scheduled() { + use futures::task::ArcWake; + + static MACROTASK: AtomicUsize = AtomicUsize::new(0); + static NEXT_TICK: AtomicUsize = AtomicUsize::new(0); + + #[op] + fn op_macrotask() -> Result<(), AnyError> { + MACROTASK.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + #[op] + fn op_next_tick() -> Result<(), AnyError> { + NEXT_TICK.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + deno_core::extension!(test_ext, ops = [op_macrotask, op_next_tick]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "has_tick_scheduled.js", + r#" + Deno.core.setMacrotaskCallback(() => { + Deno.core.ops.op_macrotask(); + return true; // We're done. + }); + Deno.core.setNextTickCallback(() => Deno.core.ops.op_next_tick()); + Deno.core.ops.op_set_has_tick_scheduled(true); + "#, + ) + .unwrap(); + + struct ArcWakeImpl(Arc); + impl ArcWake for ArcWakeImpl { + fn wake_by_ref(arc_self: &Arc) { + arc_self.0.fetch_add(1, Ordering::Relaxed); + } + } + + let awoken_times = Arc::new(AtomicUsize::new(0)); + let waker = futures::task::waker(Arc::new(ArcWakeImpl(awoken_times.clone()))); + let cx = &mut Context::from_waker(&waker); + + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + assert_eq!(1, MACROTASK.load(Ordering::Relaxed)); + assert_eq!(1, NEXT_TICK.load(Ordering::Relaxed)); + assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + assert_eq!(awoken_times.swap(0, Ordering::Relaxed), 1); + + runtime.inner.state.borrow_mut().has_tick_scheduled = false; + assert!(matches!( + runtime.poll_event_loop(cx, false), + Poll::Ready(Ok(())) + )); + assert_eq!(awoken_times.load(Ordering::Relaxed), 0); + assert!(matches!( + runtime.poll_event_loop(cx, false), + Poll::Ready(Ok(())) + )); + assert_eq!(awoken_times.load(Ordering::Relaxed), 0); +} + +#[test] +fn terminate_during_module_eval() { + #[derive(Default)] + struct ModsLoader; + + impl ModuleLoader for ModsLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _kind: ResolutionKind, + ) -> Result { + assert_eq!(specifier, "file:///main.js"); + assert_eq!(referrer, "."); + let s = crate::resolve_import(specifier, referrer).unwrap(); + Ok(s) + } + + fn load( + &self, + _module_specifier: &ModuleSpecifier, + _maybe_referrer: Option<&ModuleSpecifier>, + _is_dyn_import: bool, + ) -> Pin> { + async move { + Ok(ModuleSource::for_test( + "console.log('hello world');", + "file:///main.js", + )) + } + .boxed_local() + } + } + + let loader = std::rc::Rc::new(ModsLoader::default()); + let mut runtime = JsRuntime::new(RuntimeOptions { + module_loader: Some(loader), + ..Default::default() + }); + + let specifier = crate::resolve_url("file:///main.js").unwrap(); + let source_code = ascii_str!("Deno.core.print('hello\\n')"); + + let module_id = futures::executor::block_on( + runtime.load_main_module(&specifier, Some(source_code)), + ) + .unwrap(); + + runtime.v8_isolate().terminate_execution(); + + let mod_result = + futures::executor::block_on(runtime.mod_evaluate(module_id)).unwrap(); + assert!(mod_result + .unwrap_err() + .to_string() + .contains("JavaScript execution has been terminated")); +} + +#[tokio::test] +async fn test_unhandled_rejection_order() { + let mut runtime = JsRuntime::new(Default::default()); + runtime + .execute_script_static( + "", + r#" + for (let i = 0; i < 100; i++) { + Promise.reject(i); + } + "#, + ) + .unwrap(); + let err = runtime.run_event_loop(false).await.unwrap_err(); + assert_eq!(err.to_string(), "Uncaught (in promise) 0"); +} + +#[tokio::test] +async fn test_set_promise_reject_callback() { + static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0); + + #[op] + fn op_promise_reject() -> Result<(), AnyError> { + PROMISE_REJECT.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + deno_core::extension!(test_ext, ops = [op_promise_reject]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "promise_reject_callback.js", + r#" + // Note: |promise| is not the promise created below, it's a child. + Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => { + if (type !== /* PromiseRejectWithNoHandler */ 0) { + throw Error("unexpected type: " + type); + } + if (reason.message !== "reject") { + throw Error("unexpected reason: " + reason); + } + Deno.core.ops.op_store_pending_promise_rejection(promise); + Deno.core.ops.op_promise_reject(); + }); + new Promise((_, reject) => reject(Error("reject"))); + "#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap_err(); + + assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed)); + + runtime + .execute_script_static( + "promise_reject_callback.js", + r#" + { + const prev = Deno.core.ops.op_set_promise_reject_callback((...args) => { + prev(...args); + }); + } + new Promise((_, reject) => reject(Error("reject"))); + "#, + ) + .unwrap(); + runtime.run_event_loop(false).await.unwrap_err(); + + assert_eq!(2, PROMISE_REJECT.load(Ordering::Relaxed)); +} + +#[tokio::test] +async fn test_set_promise_reject_callback_realms() { + let mut runtime = JsRuntime::new(RuntimeOptions::default()); + let global_realm = runtime.global_realm(); + let realm1 = runtime.create_realm().unwrap(); + let realm2 = runtime.create_realm().unwrap(); + + let realm_expectations = &[ + (&global_realm, "global_realm", 42), + (&realm1, "realm1", 140), + (&realm2, "realm2", 720), + ]; + + // Set up promise reject callbacks. + for (realm, realm_name, number) in realm_expectations { + realm + .execute_script( + runtime.v8_isolate(), + "", + format!( + r#" + + globalThis.rejectValue = undefined; + Deno.core.setPromiseRejectCallback((_type, _promise, reason) => {{ + globalThis.rejectValue = `{realm_name}/${{reason}}`; + }}); + Deno.core.opAsync("op_void_async").then(() => Promise.reject({number})); + "# + ).into() + ) + .unwrap(); + } + + runtime.run_event_loop(false).await.unwrap(); + + for (realm, realm_name, number) in realm_expectations { + let reject_value = realm + .execute_script_static(runtime.v8_isolate(), "", "globalThis.rejectValue") + .unwrap(); + let scope = &mut realm.handle_scope(runtime.v8_isolate()); + let reject_value = v8::Local::new(scope, reject_value); + assert!(reject_value.is_string()); + let reject_value_string = reject_value.to_rust_string_lossy(scope); + assert_eq!(reject_value_string, format!("{realm_name}/{number}")); + } +} + +#[tokio::test] +async fn test_set_promise_reject_callback_top_level_await() { + static PROMISE_REJECT: AtomicUsize = AtomicUsize::new(0); + + #[op] + fn op_promise_reject() -> Result<(), AnyError> { + PROMISE_REJECT.fetch_add(1, Ordering::Relaxed); + Ok(()) + } + + deno_core::extension!(test_ext, ops = [op_promise_reject]); + + #[derive(Default)] + struct ModsLoader; + + impl ModuleLoader for ModsLoader { + fn resolve( + &self, + specifier: &str, + referrer: &str, + _kind: ResolutionKind, + ) -> Result { + assert_eq!(specifier, "file:///main.js"); + assert_eq!(referrer, "."); + let s = crate::resolve_import(specifier, referrer).unwrap(); + Ok(s) + } + + fn load( + &self, + _module_specifier: &ModuleSpecifier, + _maybe_referrer: Option<&ModuleSpecifier>, + _is_dyn_import: bool, + ) -> Pin> { + let code = r#" + Deno.core.ops.op_set_promise_reject_callback((type, promise, reason) => { + Deno.core.ops.op_promise_reject(); + }); + throw new Error('top level throw'); + "#; + + async move { Ok(ModuleSource::for_test(code, "file:///main.js")) } + .boxed_local() + } + } + + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + module_loader: Some(Rc::new(ModsLoader)), + ..Default::default() + }); + + let id = runtime + .load_main_module(&crate::resolve_url("file:///main.js").unwrap(), None) + .await + .unwrap(); + let receiver = runtime.mod_evaluate(id); + runtime.run_event_loop(false).await.unwrap(); + receiver.await.unwrap().unwrap_err(); + + assert_eq!(1, PROMISE_REJECT.load(Ordering::Relaxed)); +} + +#[test] +fn test_op_return_serde_v8_error() { + #[op] + fn op_err() -> Result, anyhow::Error> { + Ok([(1, 2), (3, 4)].into_iter().collect()) // Maps can't have non-string keys in serde_v8 + } + + deno_core::extension!(test_ext, ops = [op_err]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + assert!(runtime + .execute_script_static( + "test_op_return_serde_v8_error.js", + "Deno.core.ops.op_err()" + ) + .is_err()); +} + +#[test] +fn test_op_high_arity() { + #[op] + fn op_add_4( + x1: i64, + x2: i64, + x3: i64, + x4: i64, + ) -> Result { + Ok(x1 + x2 + x3 + x4) + } + + deno_core::extension!(test_ext, ops = [op_add_4]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + let r = runtime + .execute_script_static("test.js", "Deno.core.ops.op_add_4(1, 2, 3, 4)") + .unwrap(); + let scope = &mut runtime.handle_scope(); + assert_eq!(r.open(scope).integer_value(scope), Some(10)); +} + +#[test] +fn test_op_disabled() { + #[op] + fn op_foo() -> Result { + Ok(42) + } + + fn ops() -> Vec { + vec![op_foo::decl().disable()] + } + + deno_core::extension!(test_ext, ops_fn = ops); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + let err = runtime + .execute_script_static("test.js", "Deno.core.ops.op_foo()") + .unwrap_err(); + assert!(err + .to_string() + .contains("TypeError: Deno.core.ops.op_foo is not a function")); +} + +#[test] +fn test_op_detached_buffer() { + use serde_v8::DetachedBuffer; + + #[op] + fn op_sum_take(b: DetachedBuffer) -> Result { + Ok(b.as_ref().iter().clone().map(|x| *x as u64).sum()) + } + + #[op] + fn op_boomerang(b: DetachedBuffer) -> Result { + Ok(b) + } + + deno_core::extension!(test_ext, ops = [op_sum_take, op_boomerang]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + runtime + .execute_script_static( + "test.js", + r#" + const a1 = new Uint8Array([1,2,3]); + const a1b = a1.subarray(0, 3); + const a2 = new Uint8Array([5,10,15]); + const a2b = a2.subarray(0, 3); + if (!(a1.length > 0 && a1b.length > 0)) { + throw new Error("a1 & a1b should have a length"); + } + let sum = Deno.core.ops.op_sum_take(a1b); + if (sum !== 6) { + throw new Error(`Bad sum: ${sum}`); + } + if (a1.length > 0 || a1b.length > 0) { + throw new Error("expecting a1 & a1b to be detached"); + } + const a3 = Deno.core.ops.op_boomerang(a2b); + if (a3.byteLength != 3) { + throw new Error(`Expected a3.byteLength === 3, got ${a3.byteLength}`); + } + if (a3[0] !== 5 || a3[1] !== 10) { + throw new Error(`Invalid a3: ${a3[0]}, ${a3[1]}`); + } + if (a2.byteLength > 0 || a2b.byteLength > 0) { + throw new Error("expecting a2 & a2b to be detached, a3 re-attached"); + } + const wmem = new WebAssembly.Memory({ initial: 1, maximum: 2 }); + const w32 = new Uint32Array(wmem.buffer); + w32[0] = 1; w32[1] = 2; w32[2] = 3; + const assertWasmThrow = (() => { + try { + let sum = Deno.core.ops.op_sum_take(w32.subarray(0, 2)); + return false; + } catch(e) { + return e.message.includes('invalid type; expected: detachable'); + } + }); + if (!assertWasmThrow()) { + throw new Error("expected wasm mem to not be detachable"); + } + "#, + ) + .unwrap(); +} + +#[test] +fn test_op_unstable_disabling() { + #[op] + fn op_foo() -> Result { + Ok(42) + } + + #[op(unstable)] + fn op_bar() -> Result { + Ok(42) + } + + deno_core::extension!( + test_ext, + ops = [op_foo, op_bar], + middleware = |op| if op.is_unstable { op.disable() } else { op } + ); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + runtime + .execute_script_static( + "test.js", + r#" + if (Deno.core.ops.op_foo() !== 42) { + throw new Error("Exptected op_foo() === 42"); + } + if (typeof Deno.core.ops.op_bar !== "undefined") { + throw new Error("Expected op_bar to be disabled") + } + "#, + ) + .unwrap(); +} + +#[test] +fn js_realm_simple() { + let mut runtime = JsRuntime::new(Default::default()); + let main_context = runtime.global_context(); + let main_global = { + let scope = &mut runtime.handle_scope(); + let local_global = main_context.open(scope).global(scope); + v8::Global::new(scope, local_global) + }; + + let realm = runtime.create_realm().unwrap(); + assert_ne!(realm.context(), &main_context); + assert_ne!(realm.global_object(runtime.v8_isolate()), main_global); + + let main_object = runtime.execute_script_static("", "Object").unwrap(); + let realm_object = realm + .execute_script_static(runtime.v8_isolate(), "", "Object") + .unwrap(); + assert_ne!(main_object, realm_object); +} + +#[test] +fn js_realm_init() { + #[op] + fn op_test() -> Result { + Ok(String::from("Test")) + } + + deno_core::extension!(test_ext, ops = [op_test]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + let realm = runtime.create_realm().unwrap(); + let ret = realm + .execute_script_static(runtime.v8_isolate(), "", "Deno.core.ops.op_test()") + .unwrap(); + + let scope = &mut realm.handle_scope(runtime.v8_isolate()); + assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap()); +} + +#[test] +fn js_realm_init_snapshot() { + let snapshot = { + let runtime = + JsRuntimeForSnapshot::new(Default::default(), Default::default()); + let snap: &[u8] = &runtime.snapshot(); + Vec::from(snap).into_boxed_slice() + }; + + #[op] + fn op_test() -> Result { + Ok(String::from("Test")) + } + + deno_core::extension!(test_ext, ops = [op_test]); + let mut runtime = JsRuntime::new(RuntimeOptions { + startup_snapshot: Some(Snapshot::Boxed(snapshot)), + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + let realm = runtime.create_realm().unwrap(); + let ret = realm + .execute_script_static(runtime.v8_isolate(), "", "Deno.core.ops.op_test()") + .unwrap(); + + let scope = &mut realm.handle_scope(runtime.v8_isolate()); + assert_eq!(ret, serde_v8::to_v8(scope, "Test").unwrap()); +} + +#[test] +fn js_realm_sync_ops() { + // Test that returning a ZeroCopyBuf and throwing an exception from a sync + // op result in objects with prototypes from the right realm. Note that we + // don't test the result of returning structs, because they will be + // serialized to objects with null prototype. + + #[op] + fn op_test(fail: bool) -> Result { + if !fail { + Ok(ZeroCopyBuf::empty()) + } else { + Err(crate::error::type_error("Test")) + } + } + + deno_core::extension!(test_ext, ops = [op_test]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + get_error_class_fn: Some(&|error| { + crate::error::get_custom_error_class(error).unwrap() + }), + ..Default::default() + }); + let new_realm = runtime.create_realm().unwrap(); + + // Test in both realms + for realm in [runtime.global_realm(), new_realm].into_iter() { + let ret = realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + const buf = Deno.core.ops.op_test(false); + try { + Deno.core.ops.op_test(true); + } catch(e) { + err = e; + } + buf instanceof Uint8Array && buf.byteLength === 0 && + err instanceof TypeError && err.message === "Test" + "#, + ) + .unwrap(); + assert!(ret.open(runtime.v8_isolate()).is_true()); + } +} + +#[tokio::test] +async fn js_realm_async_ops() { + // Test that returning a ZeroCopyBuf and throwing an exception from a async + // op result in objects with prototypes from the right realm. Note that we + // don't test the result of returning structs, because they will be + // serialized to objects with null prototype. + + #[op] + async fn op_test(fail: bool) -> Result { + if !fail { + Ok(ZeroCopyBuf::empty()) + } else { + Err(crate::error::type_error("Test")) + } + } + + deno_core::extension!(test_ext, ops = [op_test]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + get_error_class_fn: Some(&|error| { + crate::error::get_custom_error_class(error).unwrap() + }), + ..Default::default() + }); + + let global_realm = runtime.global_realm(); + let new_realm = runtime.create_realm().unwrap(); + + let mut rets = vec![]; + + // Test in both realms + for realm in [global_realm, new_realm].into_iter() { + let ret = realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + + (async function () { + const buf = await Deno.core.opAsync("op_test", false); + let err; + try { + await Deno.core.opAsync("op_test", true); + } catch(e) { + err = e; + } + return buf instanceof Uint8Array && buf.byteLength === 0 && + err instanceof TypeError && err.message === "Test" ; + })(); + "#, + ) + .unwrap(); + rets.push((realm, ret)); + } + + runtime.run_event_loop(false).await.unwrap(); + + for ret in rets { + let scope = &mut ret.0.handle_scope(runtime.v8_isolate()); + let value = v8::Local::new(scope, ret.1); + let promise = v8::Local::::try_from(value).unwrap(); + let result = promise.result(scope); + + assert!(result.is_boolean() && result.is_true()); + } +} + +#[ignore] +#[tokio::test] +async fn js_realm_gc() { + static INVOKE_COUNT: AtomicUsize = AtomicUsize::new(0); + struct PendingFuture {} + + impl Future for PendingFuture { + type Output = (); + fn poll(self: Pin<&mut Self>, _cx: &mut Context<'_>) -> Poll<()> { + Poll::Pending + } + } + + impl Drop for PendingFuture { + fn drop(&mut self) { + assert_eq!(INVOKE_COUNT.fetch_sub(1, Ordering::SeqCst), 1); + } + } + + // Never resolves. + #[op] + async fn op_pending() { + assert_eq!(INVOKE_COUNT.fetch_add(1, Ordering::SeqCst), 0); + PendingFuture {}.await + } + + deno_core::extension!(test_ext, ops = [op_pending]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + // Detect a drop in OpState + let opstate_drop_detect = Rc::new(()); + runtime + .op_state() + .borrow_mut() + .put(opstate_drop_detect.clone()); + assert_eq!(Rc::strong_count(&opstate_drop_detect), 2); + + let other_realm = runtime.create_realm().unwrap(); + other_realm + .execute_script( + runtime.v8_isolate(), + "future", + ModuleCode::from_static("Deno.core.opAsync('op_pending')"), + ) + .unwrap(); + while INVOKE_COUNT.load(Ordering::SeqCst) == 0 { + poll_fn(|cx: &mut Context| runtime.poll_event_loop(cx, false)) + .await + .unwrap(); + } + drop(other_realm); + while INVOKE_COUNT.load(Ordering::SeqCst) == 1 { + poll_fn(|cx| runtime.poll_event_loop(cx, false)) + .await + .unwrap(); + } + drop(runtime); + + // Make sure the OpState was dropped properly when the runtime dropped + assert_eq!(Rc::strong_count(&opstate_drop_detect), 1); +} + +#[tokio::test] +async fn js_realm_ref_unref_ops() { + // Never resolves. + #[op] + async fn op_pending() { + futures::future::pending().await + } + + deno_core::extension!(test_ext, ops = [op_pending]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + poll_fn(move |cx| { + let main_realm = runtime.global_realm(); + let other_realm = runtime.create_realm().unwrap(); + + main_realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + + var promise = Deno.core.opAsync("op_pending"); + "#, + ) + .unwrap(); + other_realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + + var promise = Deno.core.opAsync("op_pending"); + "#, + ) + .unwrap(); + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + + main_realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); + Deno.core.unrefOp(promise[promiseIdSymbol]); + "#, + ) + .unwrap(); + assert!(matches!(runtime.poll_event_loop(cx, false), Poll::Pending)); + + other_realm + .execute_script_static( + runtime.v8_isolate(), + "", + r#" + let promiseIdSymbol = Symbol.for("Deno.core.internalPromiseId"); + Deno.core.unrefOp(promise[promiseIdSymbol]); + "#, + ) + .unwrap(); + assert!(matches!( + runtime.poll_event_loop(cx, false), + Poll::Ready(Ok(())) + )); + Poll::Ready(()) + }) + .await; +} + +#[test] +fn test_array_by_copy() { + // Verify that "array by copy" proposal is enabled (https://github.com/tc39/proposal-change-array-by-copy) + let mut runtime = JsRuntime::new(Default::default()); + assert!(runtime + .execute_script_static( + "test_array_by_copy.js", + "const a = [1, 2, 3]; + const b = a.toReversed(); + if (!(a[0] === 1 && a[1] === 2 && a[2] === 3)) { + throw new Error('Expected a to be intact'); + } + if (!(b[0] === 3 && b[1] === 2 && b[2] === 1)) { + throw new Error('Expected b to be reversed'); + }", + ) + .is_ok()); +} + +#[cfg(debug_assertions)] +#[test] +#[should_panic(expected = "Found ops with duplicate names:")] +fn duplicate_op_names() { + mod a { + use super::*; + + #[op] + fn op_test() -> Result { + Ok(String::from("Test")) + } + } + + #[op] + fn op_test() -> Result { + Ok(String::from("Test")) + } + + deno_core::extension!(test_ext, ops = [a::op_test, op_test]); + JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); +} + +#[test] +fn ops_in_js_have_proper_names() { + #[op] + fn op_test_sync() -> Result { + Ok(String::from("Test")) + } + + #[op] + async fn op_test_async() -> Result { + Ok(String::from("Test")) + } + + deno_core::extension!(test_ext, ops = [op_test_sync, op_test_async]); + let mut runtime = JsRuntime::new(RuntimeOptions { + extensions: vec![test_ext::init_ops()], + ..Default::default() + }); + + let src = r#" + if (Deno.core.ops.op_test_sync.name !== "op_test_sync") { + throw new Error(); + } + + if (Deno.core.ops.op_test_async.name !== "op_test_async") { + throw new Error(); + } + + const { op_test_async } = Deno.core.ensureFastOps(); + if (op_test_async.name !== "op_test_async") { + throw new Error(); + } + "#; + runtime.execute_script_static("test", src).unwrap(); +} diff --git a/core/serialize_deserialize_test.js b/core/serialize_deserialize_test.js deleted file mode 100644 index 70397cdf8..000000000 --- a/core/serialize_deserialize_test.js +++ /dev/null @@ -1,74 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. -"use strict"; - -function assert(cond) { - if (!cond) { - throw Error("assert"); - } -} - -function assertArrayEquals(a1, a2) { - if (a1.length !== a2.length) throw Error("assert"); - - for (const index in a1) { - if (a1[index] !== a2[index]) { - throw Error(`assert: (index ${index}) ${a1[index]} !== ${a2[index]}`); - } - } -} - -function main() { - const emptyString = ""; - const emptyStringSerialized = [255, 15, 34, 0]; - assertArrayEquals( - Deno.core.ops.op_serialize(emptyString), - emptyStringSerialized, - ); - assert( - Deno.core.ops.op_deserialize( - new Uint8Array(emptyStringSerialized), - ) === - emptyString, - ); - - const primitiveValueArray = ["test", "a", null, undefined]; - // deno-fmt-ignore - const primitiveValueArraySerialized = [ - 255, 15, 65, 4, 34, 4, 116, 101, 115, 116, - 34, 1, 97, 48, 95, 36, 0, 4, - ]; - assertArrayEquals( - Deno.core.ops.op_serialize(primitiveValueArray), - primitiveValueArraySerialized, - ); - - assertArrayEquals( - Deno.core.ops.op_deserialize( - new Uint8Array(primitiveValueArraySerialized), - ), - primitiveValueArray, - ); - - const circularObject = { test: null, test2: "dd", test3: "aa" }; - circularObject.test = circularObject; - // deno-fmt-ignore - const circularObjectSerialized = [ - 255, 15, 111, 34, 4, 116, 101, 115, - 116, 94, 0, 34, 5, 116, 101, 115, - 116, 50, 34, 2, 100, 100, 34, 5, - 116, 101, 115, 116, 51, 34, 2, 97, - 97, 123, 3, - ]; - - assertArrayEquals( - Deno.core.ops.op_serialize(circularObject), - circularObjectSerialized, - ); - - const deserializedCircularObject = Deno.core.ops.op_deserialize( - new Uint8Array(circularObjectSerialized), - ); - assert(deserializedCircularObject.test == deserializedCircularObject); -} - -main(); diff --git a/core/snapshot_util.rs b/core/snapshot_util.rs deleted file mode 100644 index 88c273147..000000000 --- a/core/snapshot_util.rs +++ /dev/null @@ -1,252 +0,0 @@ -// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license. - -use std::path::Path; -use std::path::PathBuf; -use std::time::Instant; - -use crate::runtime::RuntimeSnapshotOptions; -use crate::ExtModuleLoaderCb; -use crate::Extension; -use crate::JsRuntimeForSnapshot; -use crate::RuntimeOptions; -use crate::Snapshot; - -pub type CompressionCb = dyn Fn(&mut Vec, &[u8]); - -pub struct CreateSnapshotOptions { - pub cargo_manifest_dir: &'static str, - pub snapshot_path: PathBuf, - pub startup_snapshot: Option, - pub extensions: Vec, - pub compression_cb: Option>, - pub snapshot_module_load_cb: Option, -} - -pub struct CreateSnapshotOutput { - /// Any files marked as LoadedFromFsDuringSnapshot are collected here and should be - /// printed as 'cargo:rerun-if-changed' lines from your build script. - pub files_loaded_during_snapshot: Vec, -} - -#[must_use = "The files listed by create_snapshot should be printed as 'cargo:rerun-if-changed' lines"] -pub fn create_snapshot( - create_snapshot_options: CreateSnapshotOptions, -) -> CreateSnapshotOutput { - let mut mark = Instant::now(); - - let js_runtime = JsRuntimeForSnapshot::new( - RuntimeOptions { - startup_snapshot: create_snapshot_options.startup_snapshot, - extensions: create_snapshot_options.extensions, - ..Default::default() - }, - RuntimeSnapshotOptions { - snapshot_module_load_cb: create_snapshot_options.snapshot_module_load_cb, - }, - ); - println!( - "JsRuntime for snapshot prepared, took {:#?} ({})", - Instant::now().saturating_duration_since(mark), - create_snapshot_options.snapshot_path.display() - ); - mark = Instant::now(); - - let mut files_loaded_during_snapshot = vec![]; - for source in js_runtime - .extensions() - .iter() - .flat_map(|e| vec![e.get_esm_sources(), e.get_js_sources()]) - .flatten() - .flatten() - { - use crate::ExtensionFileSourceCode; - if let ExtensionFileSourceCode::LoadedFromFsDuringSnapshot(path) = - &source.code - { - files_loaded_during_snapshot.push(path.clone()); - } - } - - let snapshot = js_runtime.snapshot(); - let snapshot_slice: &[u8] = &snapshot; - println!( - "Snapshot size: {}, took {:#?} ({})", - snapshot_slice.len(), - Instant::now().saturating_duration_since(mark), - create_snapshot_options.snapshot_path.display() - ); - mark = Instant::now(); - - let maybe_compressed_snapshot: Box> = - if let Some(compression_cb) = create_snapshot_options.compression_cb { - let mut vec = vec![]; - - vec.extend_from_slice( - &u32::try_from(snapshot.len()) - .expect("snapshot larger than 4gb") - .to_le_bytes(), - ); - - (compression_cb)(&mut vec, snapshot_slice); - - println!( - "Snapshot compressed size: {}, took {:#?} ({})", - vec.len(), - Instant::now().saturating_duration_since(mark), - create_snapshot_options.snapshot_path.display() - ); - mark = std::time::Instant::now(); - - Box::new(vec) - } else { - Box::new(snapshot_slice) - }; - - std::fs::write( - &create_snapshot_options.snapshot_path, - &*maybe_compressed_snapshot, - ) - .unwrap(); - println!( - "Snapshot written, took: {:#?} ({})", - Instant::now().saturating_duration_since(mark), - create_snapshot_options.snapshot_path.display(), - ); - CreateSnapshotOutput { - files_loaded_during_snapshot, - } -} - -pub type FilterFn = Box bool>; - -pub fn get_js_files( - cargo_manifest_dir: &'static str, - directory: &str, - filter: Option, -) -> Vec { - let manifest_dir = Path::new(cargo_manifest_dir); - let mut js_files = std::fs::read_dir(directory) - .unwrap() - .map(|dir_entry| { - let file = dir_entry.unwrap(); - manifest_dir.join(file.path()) - }) - .filter(|path| { - path.extension().unwrap_or_default() == "js" - && filter.as_ref().map(|filter| filter(path)).unwrap_or(true) - }) - .collect::>(); - js_files.sort(); - js_files -} - -fn data_error_to_panic(err: v8::DataError) -> ! { - match err { - v8::DataError::BadType { actual, expected } => { - panic!( - "Invalid type for snapshot data: expected {expected}, got {actual}" - ); - } - v8::DataError::NoData { expected } => { - panic!("No data for snapshot data: expected {expected}"); - } - } -} - -pub(crate) struct SnapshottedData { - pub module_map_data: v8::Global, - pub module_handles: Vec>, -} - -static MODULE_MAP_CONTEXT_DATA_INDEX: usize = 0; - -pub(crate) fn get_snapshotted_data( - scope: &mut v8::HandleScope<()>, - context: v8::Local, -) -> SnapshottedData { - let mut scope = v8::ContextScope::new(scope, context); - - // The 0th element is the module map itself, followed by X number of module - // handles. We need to deserialize the "next_module_id" field from the - // map to see how many module handles we expect. - let result = scope.get_context_data_from_snapshot_once::( - MODULE_MAP_CONTEXT_DATA_INDEX, - ); - - let val = match result { - Ok(v) => v, - Err(err) => data_error_to_panic(err), - }; - - let next_module_id = { - let info_data: v8::Local = - val.get_index(&mut scope, 1).unwrap().try_into().unwrap(); - info_data.length() - }; - - // Over allocate so executing a few scripts doesn't have to resize this vec. - let mut module_handles = Vec::with_capacity(next_module_id as usize + 16); - for i in 1..=next_module_id { - match scope.get_context_data_from_snapshot_once::(i as usize) { - Ok(val) => { - let module_global = v8::Global::new(&mut scope, val); - module_handles.push(module_global); - } - Err(err) => data_error_to_panic(err), - } - } - - SnapshottedData { - module_map_data: v8::Global::new(&mut scope, val), - module_handles, - } -} - -pub(crate) fn set_snapshotted_data( - scope: &mut v8::HandleScope<()>, - context: v8::Global, - snapshotted_data: SnapshottedData, -) { - let local_context = v8::Local::new(scope, context); - let local_data = v8::Local::new(scope, snapshotted_data.module_map_data); - let offset = scope.add_context_data(local_context, local_data); - assert_eq!(offset, MODULE_MAP_CONTEXT_DATA_INDEX); - - for (index, handle) in snapshotted_data.module_handles.into_iter().enumerate() - { - let module_handle = v8::Local::new(scope, handle); - let offset = scope.add_context_data(local_context, module_handle); - assert_eq!(offset, index + 1); - } -} - -/// Returns an isolate set up for snapshotting. -pub(crate) fn create_snapshot_creator( - external_refs: &'static v8::ExternalReferences, - maybe_startup_snapshot: Option, -) -> v8::OwnedIsolate { - if let Some(snapshot) = maybe_startup_snapshot { - match snapshot { - Snapshot::Static(data) => { - v8::Isolate::snapshot_creator_from_existing_snapshot( - data, - Some(external_refs), - ) - } - Snapshot::JustCreated(data) => { - v8::Isolate::snapshot_creator_from_existing_snapshot( - data, - Some(external_refs), - ) - } - Snapshot::Boxed(data) => { - v8::Isolate::snapshot_creator_from_existing_snapshot( - data, - Some(external_refs), - ) - } - } - } else { - v8::Isolate::snapshot_creator(Some(external_refs)) - } -} -- cgit v1.2.3