summaryrefslogtreecommitdiff
path: root/core/runtime.rs
diff options
context:
space:
mode:
authorBartek IwaƄczuk <biwanczuk@gmail.com>2020-09-06 21:44:29 +0200
committerGitHub <noreply@github.com>2020-09-06 21:44:29 +0200
commitf57a2c1e85387afe48b7bdb57176dafb156bb86e (patch)
treec198f7c965a883acacc9c1410c620d6290cd43ef /core/runtime.rs
parent16a9c92aba9ba8376393d6df4fa9677ac6113578 (diff)
refactor(core): rename CoreIsolate to JsRuntime (#7373)
deno_core/ - rename core_isolate.rs to runtime.rs - rename CoreIsolate to JsRuntime - rename JSError to JsError - rename JSStackFrame to JsStackFrame cli/ - update references from deno_core::CoreIsolate to deno_core::JsRuntime - rename deno_core::JSError to deno_core::JsError - rename fmt_errors::JSError to fmt_errors::JsError
Diffstat (limited to 'core/runtime.rs')
-rw-r--r--core/runtime.rs2268
1 files changed, 2268 insertions, 0 deletions
diff --git a/core/runtime.rs b/core/runtime.rs
new file mode 100644
index 000000000..2f574b195
--- /dev/null
+++ b/core/runtime.rs
@@ -0,0 +1,2268 @@
+// Copyright 2018-2020 the Deno authors. All rights reserved. MIT license.
+
+use rusty_v8 as v8;
+
+use crate::bindings;
+use crate::errors::attach_handle_to_error;
+use crate::errors::ErrWithV8Handle;
+use crate::futures::FutureExt;
+use crate::module_specifier::ModuleSpecifier;
+use crate::modules::LoadState;
+use crate::modules::ModuleId;
+use crate::modules::ModuleLoadId;
+use crate::modules::ModuleLoader;
+use crate::modules::ModuleSource;
+use crate::modules::Modules;
+use crate::modules::NoopModuleLoader;
+use crate::modules::PrepareLoadFuture;
+use crate::modules::RecursiveModuleLoad;
+use crate::ops::*;
+use crate::shared_queue::SharedQueue;
+use crate::shared_queue::RECOMMENDED_SIZE;
+use crate::ErrBox;
+use crate::JsError;
+use crate::OpRouter;
+use futures::stream::FuturesUnordered;
+use futures::stream::StreamExt;
+use futures::stream::StreamFuture;
+use futures::task::AtomicWaker;
+use futures::Future;
+use std::any::Any;
+use std::cell::Cell;
+use std::cell::RefCell;
+use std::collections::HashMap;
+use std::convert::From;
+use std::convert::TryFrom;
+use std::ffi::c_void;
+use std::mem::forget;
+use std::ops::Deref;
+use std::ops::DerefMut;
+use std::option::Option;
+use std::pin::Pin;
+use std::rc::Rc;
+use std::sync::Once;
+use std::task::Context;
+use std::task::Poll;
+
+type PendingOpFuture = Pin<Box<dyn Future<Output = (OpId, Box<[u8]>)>>>;
+
+/// Stores a script used to initialize a Isolate
+pub struct Script<'a> {
+ pub source: &'a str,
+ pub filename: &'a str,
+}
+
+// TODO(ry) It's ugly that we have both Script and OwnedScript. Ideally we
+// wouldn't expose such twiddly complexity.
+struct OwnedScript {
+ pub source: String,
+ pub filename: String,
+}
+
+impl From<Script<'_>> for OwnedScript {
+ fn from(s: Script) -> OwnedScript {
+ OwnedScript {
+ source: s.source.to_string(),
+ filename: s.filename.to_string(),
+ }
+ }
+}
+
+pub enum Snapshot {
+ Static(&'static [u8]),
+ JustCreated(v8::StartupData),
+ Boxed(Box<[u8]>),
+}
+
+/// Represents data used to initialize an isolate at startup, either
+/// in the form of a binary snapshot or a JavaScript source file.
+pub enum StartupData<'a> {
+ Script(Script<'a>),
+ Snapshot(Snapshot),
+ None,
+}
+
+impl StartupData<'_> {
+ fn into_options(self) -> (Option<OwnedScript>, Option<Snapshot>) {
+ match self {
+ Self::Script(script) => (Some(script.into()), None),
+ Self::Snapshot(snapshot) => (None, Some(snapshot)),
+ Self::None => (None, None),
+ }
+ }
+}
+
+type JsErrorCreateFn = dyn Fn(JsError) -> ErrBox;
+
+pub type GetErrorClassFn = dyn for<'e> Fn(&'e ErrBox) -> &'static str;
+
+/// Objects that need to live as long as the isolate
+#[derive(Default)]
+struct IsolateAllocations {
+ near_heap_limit_callback_data:
+ Option<(Box<RefCell<dyn Any>>, v8::NearHeapLimitCallback)>,
+}
+
+/// A single execution context of JavaScript. Corresponds roughly to the "Web
+/// Worker" concept in the DOM. A JsRuntime is a Future that can be used with
+/// an event loop (Tokio, async_std).
+////
+/// The JsRuntime future completes when there is an error or when all
+/// pending ops have completed.
+///
+/// Ops are created in JavaScript by calling Deno.core.dispatch(), and in Rust
+/// by implementing dispatcher function that takes control buffer and optional zero copy buffer
+/// as arguments. An async Op corresponds exactly to a Promise in JavaScript.
+pub struct JsRuntime {
+ // This is an Option<OwnedIsolate> instead of just OwnedIsolate to workaround
+ // an safety issue with SnapshotCreator. See JsRuntime::drop.
+ v8_isolate: Option<v8::OwnedIsolate>,
+ snapshot_creator: Option<v8::SnapshotCreator>,
+ has_snapshotted: bool,
+ needs_init: bool,
+ startup_script: Option<OwnedScript>,
+ allocations: IsolateAllocations,
+}
+
+/// Internal state for JsRuntime which is stored in one of v8::Isolate's
+/// embedder slots.
+pub struct JsRuntimeState {
+ pub global_context: Option<v8::Global<v8::Context>>,
+ pub(crate) shared_ab: Option<v8::Global<v8::SharedArrayBuffer>>,
+ pub(crate) js_recv_cb: Option<v8::Global<v8::Function>>,
+ pub(crate) js_macrotask_cb: Option<v8::Global<v8::Function>>,
+ pub(crate) pending_promise_exceptions: HashMap<i32, v8::Global<v8::Value>>,
+ pub(crate) js_error_create_fn: Box<JsErrorCreateFn>,
+ pub(crate) shared: SharedQueue,
+ pub(crate) pending_ops: FuturesUnordered<PendingOpFuture>,
+ pub(crate) pending_unref_ops: FuturesUnordered<PendingOpFuture>,
+ pub(crate) have_unpolled_ops: Cell<bool>,
+ pub(crate) op_router: Rc<dyn OpRouter>,
+ loader: Rc<dyn ModuleLoader>,
+ pub modules: Modules,
+ pub(crate) dyn_import_map:
+ HashMap<ModuleLoadId, v8::Global<v8::PromiseResolver>>,
+ preparing_dyn_imports: FuturesUnordered<Pin<Box<PrepareLoadFuture>>>,
+ pending_dyn_imports: FuturesUnordered<StreamFuture<RecursiveModuleLoad>>,
+ waker: AtomicWaker,
+}
+
+impl Deref for JsRuntime {
+ type Target = v8::Isolate;
+ fn deref(&self) -> &v8::Isolate {
+ self.v8_isolate.as_ref().unwrap()
+ }
+}
+
+impl DerefMut for JsRuntime {
+ fn deref_mut(&mut self) -> &mut v8::Isolate {
+ self.v8_isolate.as_mut().unwrap()
+ }
+}
+
+impl Drop for JsRuntime {
+ fn drop(&mut self) {
+ if let Some(creator) = self.snapshot_creator.take() {
+ // TODO(ry): in rusty_v8, `SnapShotCreator::get_owned_isolate()` returns
+ // a `struct OwnedIsolate` which is not actually owned, hence the need
+ // here to leak the `OwnedIsolate` in order to avoid a double free and
+ // the segfault that it causes.
+ let v8_isolate = self.v8_isolate.take().unwrap();
+ forget(v8_isolate);
+
+ // TODO(ry) V8 has a strange assert which prevents a SnapshotCreator from
+ // being deallocated if it hasn't created a snapshot yet.
+ // https://github.com/v8/v8/blob/73212783fbd534fac76cc4b66aac899c13f71fc8/src/api.cc#L603
+ // If that assert is removed, this if guard could be removed.
+ // WARNING: There may be false positive LSAN errors here.
+ if self.has_snapshotted {
+ drop(creator);
+ }
+ }
+ }
+}
+
+#[allow(clippy::missing_safety_doc)]
+pub unsafe fn v8_init() {
+ let platform = v8::new_default_platform().unwrap();
+ v8::V8::initialize_platform(platform);
+ v8::V8::initialize();
+ // TODO(ry) This makes WASM compile synchronously. Eventually we should
+ // remove this to make it work asynchronously too. But that requires getting
+ // PumpMessageLoop and RunMicrotasks setup correctly.
+ // See https://github.com/denoland/deno/issues/2544
+ let argv = vec![
+ "".to_string(),
+ "--wasm-test-streaming".to_string(),
+ "--no-wasm-async-compilation".to_string(),
+ "--harmony-top-level-await".to_string(),
+ "--experimental-wasm-bigint".to_string(),
+ ];
+ v8::V8::set_flags_from_command_line(argv);
+}
+
+/// Minimum and maximum bytes of heap used in an isolate
+pub struct HeapLimits {
+ /// By default V8 starts with a small heap and dynamically grows it to match
+ /// the set of live objects. This may lead to ineffective garbage collections
+ /// at startup if the live set is large. Setting the initial heap size avoids
+ /// such garbage collections. Note that this does not affect young generation
+ /// garbage collections.
+ pub initial: usize,
+ /// When the heap size approaches `max`, V8 will perform series of
+ /// garbage collections and invoke the
+ /// [NearHeapLimitCallback](TODO).
+ /// If the garbage collections do not help and the callback does not
+ /// increase the limit, then V8 will crash with V8::FatalProcessOutOfMemory.
+ pub max: usize,
+}
+
+pub(crate) struct IsolateOptions {
+ loader: Rc<dyn ModuleLoader>,
+ op_router: Rc<dyn OpRouter>,
+ startup_script: Option<OwnedScript>,
+ startup_snapshot: Option<Snapshot>,
+ will_snapshot: bool,
+ heap_limits: Option<HeapLimits>,
+}
+
+impl JsRuntime {
+ /// startup_data defines the snapshot or script used at startup to initialize
+ /// the isolate.
+ pub fn new(
+ op_router: Rc<dyn OpRouter>,
+ startup_data: StartupData,
+ will_snapshot: bool,
+ ) -> Self {
+ let (startup_script, startup_snapshot) = startup_data.into_options();
+ let options = IsolateOptions {
+ loader: Rc::new(NoopModuleLoader),
+ op_router,
+ startup_script,
+ startup_snapshot,
+ will_snapshot,
+ heap_limits: None,
+ };
+
+ Self::from_options(options)
+ }
+
+ // TODO(bartlomieju): add `new_with_loader_and_heap_limits` function?
+ /// Create new isolate that can load and execute ESModules.
+ pub fn new_with_loader(
+ loader: Rc<dyn ModuleLoader>,
+ op_router: Rc<dyn OpRouter>,
+ startup_data: StartupData,
+ will_snapshot: bool,
+ ) -> Self {
+ let (startup_script, startup_snapshot) = startup_data.into_options();
+ let options = IsolateOptions {
+ loader,
+ op_router,
+ startup_script,
+ startup_snapshot,
+ will_snapshot,
+ heap_limits: None,
+ };
+
+ Self::from_options(options)
+ }
+
+ /// This is useful for controlling memory usage of scripts.
+ ///
+ /// See [`HeapLimits`](struct.HeapLimits.html) for more details.
+ ///
+ /// Make sure to use [`add_near_heap_limit_callback`](#method.add_near_heap_limit_callback)
+ /// to prevent v8 from crashing when reaching the upper limit.
+ pub fn with_heap_limits(
+ op_router: Rc<dyn OpRouter>,
+ startup_data: StartupData,
+ heap_limits: HeapLimits,
+ ) -> Self {
+ let (startup_script, startup_snapshot) = startup_data.into_options();
+ let options = IsolateOptions {
+ loader: Rc::new(NoopModuleLoader),
+ op_router,
+ startup_script,
+ startup_snapshot,
+ will_snapshot: false,
+ heap_limits: Some(heap_limits),
+ };
+
+ Self::from_options(options)
+ }
+
+ fn from_options(options: IsolateOptions) -> Self {
+ static DENO_INIT: Once = Once::new();
+ DENO_INIT.call_once(|| {
+ unsafe { v8_init() };
+ });
+
+ let global_context;
+ let (mut isolate, maybe_snapshot_creator) = if options.will_snapshot {
+ // TODO(ry) Support loading snapshots before snapshotting.
+ assert!(options.startup_snapshot.is_none());
+ let mut creator =
+ v8::SnapshotCreator::new(Some(&bindings::EXTERNAL_REFERENCES));
+ let isolate = unsafe { creator.get_owned_isolate() };
+ let mut isolate = JsRuntime::setup_isolate(isolate);
+ {
+ let scope = &mut v8::HandleScope::new(&mut isolate);
+ let context = bindings::initialize_context(scope);
+ global_context = v8::Global::new(scope, context);
+ creator.set_default_context(context);
+ }
+ (isolate, Some(creator))
+ } else {
+ let mut params = v8::Isolate::create_params()
+ .external_references(&**bindings::EXTERNAL_REFERENCES);
+ let snapshot_loaded = if let Some(snapshot) = options.startup_snapshot {
+ params = match snapshot {
+ Snapshot::Static(data) => params.snapshot_blob(data),
+ Snapshot::JustCreated(data) => params.snapshot_blob(data),
+ Snapshot::Boxed(data) => params.snapshot_blob(data),
+ };
+ true
+ } else {
+ false
+ };
+
+ if let Some(heap_limits) = options.heap_limits {
+ params = params.heap_limits(heap_limits.initial, heap_limits.max)
+ }
+
+ let isolate = v8::Isolate::new(params);
+ let mut isolate = JsRuntime::setup_isolate(isolate);
+ {
+ let scope = &mut v8::HandleScope::new(&mut isolate);
+ let context = if snapshot_loaded {
+ v8::Context::new(scope)
+ } else {
+ // If no snapshot is provided, we initialize the context with empty
+ // main source code and source maps.
+ bindings::initialize_context(scope)
+ };
+ global_context = v8::Global::new(scope, context);
+ }
+ (isolate, None)
+ };
+
+ isolate.set_slot(Rc::new(RefCell::new(JsRuntimeState {
+ global_context: Some(global_context),
+ pending_promise_exceptions: HashMap::new(),
+ shared_ab: None,
+ js_recv_cb: None,
+ js_macrotask_cb: None,
+ js_error_create_fn: Box::new(JsError::create),
+ shared: SharedQueue::new(RECOMMENDED_SIZE),
+ pending_ops: FuturesUnordered::new(),
+ pending_unref_ops: FuturesUnordered::new(),
+ have_unpolled_ops: Cell::new(false),
+ op_router: options.op_router,
+ modules: Modules::new(),
+ loader: options.loader,
+ dyn_import_map: HashMap::new(),
+ preparing_dyn_imports: FuturesUnordered::new(),
+ pending_dyn_imports: FuturesUnordered::new(),
+ waker: AtomicWaker::new(),
+ })));
+
+ Self {
+ v8_isolate: Some(isolate),
+ snapshot_creator: maybe_snapshot_creator,
+ has_snapshotted: false,
+ needs_init: true,
+ startup_script: options.startup_script,
+ allocations: IsolateAllocations::default(),
+ }
+ }
+
+ fn setup_isolate(mut isolate: v8::OwnedIsolate) -> v8::OwnedIsolate {
+ 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
+ }
+
+ pub fn state(isolate: &v8::Isolate) -> Rc<RefCell<JsRuntimeState>> {
+ let s = isolate.get_slot::<Rc<RefCell<JsRuntimeState>>>().unwrap();
+ s.clone()
+ }
+
+ /// Executes a bit of built-in JavaScript to provide Deno.sharedQueue.
+ pub(crate) fn shared_init(&mut self) {
+ if self.needs_init {
+ self.needs_init = false;
+ js_check(self.execute("core.js", include_str!("core.js")));
+ // Maybe execute the startup script.
+ if let Some(s) = self.startup_script.take() {
+ self.execute(&s.filename, &s.source).unwrap()
+ }
+ }
+ }
+
+ /// Executes traditional JavaScript code (traditional = not ES modules)
+ ///
+ /// ErrBox can be downcast to a type that exposes additional information about
+ /// the V8 exception. By default this type is JsError, however it may be a
+ /// different type if JsRuntime::set_js_error_create_fn() has been used.
+ pub fn execute(
+ &mut self,
+ js_filename: &str,
+ js_source: &str,
+ ) -> Result<(), ErrBox> {
+ self.shared_init();
+
+ let state_rc = Self::state(self);
+ let state = state_rc.borrow();
+
+ let scope = &mut v8::HandleScope::with_context(
+ self.v8_isolate.as_mut().unwrap(),
+ state.global_context.as_ref().unwrap(),
+ );
+
+ drop(state);
+
+ let source = v8::String::new(scope, js_source).unwrap();
+ let name = v8::String::new(scope, js_filename).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);
+ }
+ };
+
+ match script.run(tc_scope) {
+ Some(_) => Ok(()),
+ None => {
+ assert!(tc_scope.has_caught());
+ let exception = tc_scope.exception().unwrap();
+ exception_to_err_result(tc_scope, exception)
+ }
+ }
+ }
+
+ /// Takes a snapshot. The isolate should have been created with will_snapshot
+ /// set to true.
+ ///
+ /// ErrBox can be downcast to a type that exposes additional information about
+ /// the V8 exception. By default this type is JsError, however it may be a
+ /// different type if JsRuntime::set_js_error_create_fn() has been used.
+ pub fn snapshot(&mut self) -> v8::StartupData {
+ assert!(self.snapshot_creator.is_some());
+ let state = Self::state(self);
+
+ // Note: create_blob() method must not be called from within a HandleScope.
+ // TODO(piscisaureus): The rusty_v8 type system should enforce this.
+ state.borrow_mut().global_context.take();
+
+ std::mem::take(&mut state.borrow_mut().modules);
+
+ let snapshot_creator = self.snapshot_creator.as_mut().unwrap();
+ let snapshot = snapshot_creator
+ .create_blob(v8::FunctionCodeHandling::Keep)
+ .unwrap();
+ self.has_snapshotted = true;
+
+ snapshot
+ }
+
+ /// 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<C>(&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::<C>));
+ if let Some((_, prev_cb)) = prev {
+ self
+ .v8_isolate
+ .as_mut()
+ .unwrap()
+ .remove_near_heap_limit_callback(prev_cb, 0);
+ }
+
+ self
+ .v8_isolate
+ .as_mut()
+ .unwrap()
+ .add_near_heap_limit_callback(near_heap_limit_callback::<C>, 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
+ .as_mut()
+ .unwrap()
+ .remove_near_heap_limit_callback(cb, heap_limit);
+ }
+ }
+}
+
+extern "C" fn near_heap_limit_callback<F>(
+ data: *mut c_void,
+ current_heap_limit: usize,
+ initial_heap_limit: usize,
+) -> usize
+where
+ F: FnMut(usize, usize) -> usize,
+{
+ let callback = unsafe { &mut *(data as *mut F) };
+ callback(current_heap_limit, initial_heap_limit)
+}
+
+impl Future for JsRuntime {
+ type Output = Result<(), ErrBox>;
+
+ fn poll(self: Pin<&mut Self>, cx: &mut Context) -> Poll<Self::Output> {
+ let runtime = self.get_mut();
+ runtime.shared_init();
+
+ let state_rc = Self::state(runtime);
+ {
+ let state = state_rc.borrow();
+ state.waker.register(cx.waker());
+ }
+
+ let has_preparing = {
+ let state = state_rc.borrow();
+ !state.preparing_dyn_imports.is_empty()
+ };
+ if has_preparing {
+ let poll_imports = runtime.prepare_dyn_imports(cx)?;
+ assert!(poll_imports.is_ready());
+ }
+
+ let has_pending = {
+ let state = state_rc.borrow();
+ !state.pending_dyn_imports.is_empty()
+ };
+ if has_pending {
+ let poll_imports = runtime.poll_dyn_imports(cx)?;
+ assert!(poll_imports.is_ready());
+ }
+
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **runtime,
+ state_rc.borrow().global_context.as_ref().unwrap(),
+ );
+
+ check_promise_exceptions(scope)?;
+
+ let mut overflow_response: Option<(OpId, Box<[u8]>)> = None;
+
+ loop {
+ let mut state = state_rc.borrow_mut();
+ // Now handle actual ops.
+ state.have_unpolled_ops.set(false);
+
+ let pending_r = state.pending_ops.poll_next_unpin(cx);
+ match pending_r {
+ Poll::Ready(None) => break,
+ Poll::Pending => break,
+ Poll::Ready(Some((op_id, buf))) => {
+ let successful_push = state.shared.push(op_id, &buf);
+ if !successful_push {
+ // If we couldn't push the response to the shared queue, because
+ // there wasn't enough size, we will return the buffer via the
+ // legacy route, using the argument of deno_respond.
+ overflow_response = Some((op_id, buf));
+ break;
+ }
+ }
+ };
+ }
+
+ loop {
+ let mut state = state_rc.borrow_mut();
+ let unref_r = state.pending_unref_ops.poll_next_unpin(cx);
+ #[allow(clippy::match_wild_err_arm)]
+ match unref_r {
+ Poll::Ready(None) => break,
+ Poll::Pending => break,
+ Poll::Ready(Some((op_id, buf))) => {
+ let successful_push = state.shared.push(op_id, &buf);
+ if !successful_push {
+ // If we couldn't push the response to the shared queue, because
+ // there wasn't enough size, we will return the buffer via the
+ // legacy route, using the argument of deno_respond.
+ overflow_response = Some((op_id, buf));
+ break;
+ }
+ }
+ };
+ }
+
+ {
+ let state = state_rc.borrow();
+ if state.shared.size() > 0 {
+ drop(state);
+ async_op_response(scope, None)?;
+ // The other side should have shifted off all the messages.
+ let state = state_rc.borrow();
+ assert_eq!(state.shared.size(), 0);
+ }
+ }
+
+ {
+ if let Some((op_id, buf)) = overflow_response.take() {
+ async_op_response(scope, Some((op_id, buf)))?;
+ }
+
+ drain_macrotasks(scope)?;
+
+ check_promise_exceptions(scope)?;
+ }
+
+ let state = state_rc.borrow();
+ // We're idle if pending_ops is empty.
+ if state.pending_ops.is_empty()
+ && state.pending_dyn_imports.is_empty()
+ && state.preparing_dyn_imports.is_empty()
+ {
+ Poll::Ready(Ok(()))
+ } else {
+ if state.have_unpolled_ops.get() {
+ state.waker.wake();
+ }
+ Poll::Pending
+ }
+ }
+}
+
+impl JsRuntimeState {
+ /// Allows a callback to be set whenever a V8 exception is made. This allows
+ /// the caller to wrap the JsError into an error. By default this callback
+ /// is set to JsError::create.
+ pub fn set_js_error_create_fn(
+ &mut self,
+ f: impl Fn(JsError) -> ErrBox + 'static,
+ ) {
+ self.js_error_create_fn = Box::new(f);
+ }
+
+ // Called by V8 during `Isolate::mod_instantiate`.
+ pub fn module_resolve_cb(
+ &mut self,
+ specifier: &str,
+ referrer_id: ModuleId,
+ ) -> ModuleId {
+ let referrer = self.modules.get_name(referrer_id).unwrap();
+ let specifier = self
+ .loader
+ .resolve(specifier, referrer, false)
+ .expect("Module should have been already resolved");
+ self.modules.get_id(specifier.as_str()).unwrap_or(0)
+ }
+
+ // Called by V8 during `Isolate::mod_instantiate`.
+ pub fn dyn_import_cb(
+ &mut self,
+ resolver_handle: v8::Global<v8::PromiseResolver>,
+ specifier: &str,
+ referrer: &str,
+ ) {
+ debug!("dyn_import specifier {} referrer {} ", specifier, referrer);
+
+ let load = RecursiveModuleLoad::dynamic_import(
+ specifier,
+ referrer,
+ self.loader.clone(),
+ );
+ self.dyn_import_map.insert(load.id, resolver_handle);
+ self.waker.wake();
+ let fut = load.prepare().boxed_local();
+ self.preparing_dyn_imports.push(fut);
+ }
+}
+
+fn async_op_response<'s>(
+ scope: &mut v8::HandleScope<'s>,
+ maybe_buf: Option<(OpId, Box<[u8]>)>,
+) -> Result<(), ErrBox> {
+ let context = scope.get_current_context();
+ let global: v8::Local<v8::Value> = context.global(scope).into();
+ let js_recv_cb = JsRuntime::state(scope)
+ .borrow()
+ .js_recv_cb
+ .as_ref()
+ .map(|cb| v8::Local::new(scope, cb))
+ .expect("Deno.core.recv has not been called.");
+
+ let tc_scope = &mut v8::TryCatch::new(scope);
+
+ match maybe_buf {
+ Some((op_id, buf)) => {
+ let op_id: v8::Local<v8::Value> =
+ v8::Integer::new(tc_scope, op_id as i32).into();
+ let ui8: v8::Local<v8::Value> =
+ boxed_slice_to_uint8array(tc_scope, buf).into();
+ js_recv_cb.call(tc_scope, global, &[op_id, ui8])
+ }
+ None => js_recv_cb.call(tc_scope, global, &[]),
+ };
+
+ match tc_scope.exception() {
+ None => Ok(()),
+ Some(exception) => exception_to_err_result(tc_scope, exception),
+ }
+}
+
+fn drain_macrotasks<'s>(scope: &mut v8::HandleScope<'s>) -> Result<(), ErrBox> {
+ let context = scope.get_current_context();
+ let global: v8::Local<v8::Value> = context.global(scope).into();
+
+ let js_macrotask_cb = match JsRuntime::state(scope)
+ .borrow_mut()
+ .js_macrotask_cb
+ .as_ref()
+ {
+ Some(cb) => v8::Local::new(scope, cb),
+ None => return Ok(()),
+ };
+
+ // Repeatedly invoke macrotask callback until it returns true (done),
+ // such that ready microtasks would be automatically run before
+ // next macrotask is processed.
+ let tc_scope = &mut v8::TryCatch::new(scope);
+
+ loop {
+ let is_done = js_macrotask_cb.call(tc_scope, global, &[]);
+
+ if let Some(exception) = tc_scope.exception() {
+ return exception_to_err_result(tc_scope, exception);
+ }
+
+ let is_done = is_done.unwrap();
+ if is_done.is_true() {
+ break;
+ }
+ }
+
+ Ok(())
+}
+
+pub(crate) fn exception_to_err_result<'s, T>(
+ scope: &mut v8::HandleScope<'s>,
+ exception: v8::Local<v8::Value>,
+) -> Result<T, ErrBox> {
+ // TODO(piscisaureus): in rusty_v8, `is_execution_terminating()` should
+ // also be implemented on `struct Isolate`.
+ let is_terminating_exception =
+ scope.thread_safe_handle().is_execution_terminating();
+ let mut exception = exception;
+
+ if is_terminating_exception {
+ // TerminateExecution was called. Cancel exception termination so that the
+ // exception can be created..
+ // TODO(piscisaureus): in rusty_v8, `cancel_terminate_execution()` should
+ // also be implemented on `struct Isolate`.
+ scope.thread_safe_handle().cancel_terminate_execution();
+
+ // Maybe make a new exception object.
+ if exception.is_null_or_undefined() {
+ let message = v8::String::new(scope, "execution terminated").unwrap();
+ exception = v8::Exception::error(scope, message);
+ }
+ }
+
+ let js_error = JsError::from_v8_exception(scope, exception);
+
+ let state_rc = JsRuntime::state(scope);
+ let state = state_rc.borrow();
+ let js_error = (state.js_error_create_fn)(js_error);
+
+ if is_terminating_exception {
+ // Re-enable exception termination.
+ // TODO(piscisaureus): in rusty_v8, `terminate_execution()` should also
+ // be implemented on `struct Isolate`.
+ scope.thread_safe_handle().terminate_execution();
+ }
+
+ Err(js_error)
+}
+
+fn check_promise_exceptions<'s>(
+ scope: &mut v8::HandleScope<'s>,
+) -> Result<(), ErrBox> {
+ let state_rc = JsRuntime::state(scope);
+ let mut state = state_rc.borrow_mut();
+
+ if let Some(&key) = state.pending_promise_exceptions.keys().next() {
+ let handle = state.pending_promise_exceptions.remove(&key).unwrap();
+ drop(state);
+ let exception = v8::Local::new(scope, handle);
+ exception_to_err_result(scope, exception)
+ } else {
+ Ok(())
+ }
+}
+
+pub fn js_check<T>(r: Result<T, ErrBox>) -> T {
+ if let Err(e) = r {
+ panic!(e.to_string());
+ }
+ r.unwrap()
+}
+
+fn boxed_slice_to_uint8array<'sc>(
+ scope: &mut v8::HandleScope<'sc>,
+ buf: Box<[u8]>,
+) -> v8::Local<'sc, v8::Uint8Array> {
+ assert!(!buf.is_empty());
+ let buf_len = buf.len();
+ let backing_store = v8::ArrayBuffer::new_backing_store_from_boxed_slice(buf);
+ let backing_store_shared = backing_store.make_shared();
+ let ab = v8::ArrayBuffer::with_backing_store(scope, &backing_store_shared);
+ v8::Uint8Array::new(scope, ab, 0, buf_len)
+ .expect("Failed to create UintArray8")
+}
+
+// Related to module loading
+impl JsRuntime {
+ /// Low-level module creation.
+ ///
+ /// Called during module loading or dynamic import loading.
+ fn mod_new(
+ &mut self,
+ main: bool,
+ name: &str,
+ source: &str,
+ ) -> Result<ModuleId, ErrBox> {
+ let state_rc = Self::state(self);
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **self,
+ state_rc.borrow().global_context.as_ref().unwrap(),
+ );
+
+ let name_str = v8::String::new(scope, name).unwrap();
+ let source_str = v8::String::new(scope, source).unwrap();
+
+ let origin = bindings::module_origin(scope, name_str);
+ let source = v8::script_compiler::Source::new(source_str, &origin);
+
+ let tc_scope = &mut v8::TryCatch::new(scope);
+
+ let maybe_module = v8::script_compiler::compile_module(tc_scope, source);
+
+ if tc_scope.has_caught() {
+ assert!(maybe_module.is_none());
+ let e = tc_scope.exception().unwrap();
+ return exception_to_err_result(tc_scope, e);
+ }
+
+ let module = maybe_module.unwrap();
+ let id = module.get_identity_hash();
+
+ let mut import_specifiers: Vec<ModuleSpecifier> = vec![];
+ for i in 0..module.get_module_requests_length() {
+ let import_specifier =
+ module.get_module_request(i).to_rust_string_lossy(tc_scope);
+ let state = state_rc.borrow();
+ let module_specifier =
+ state.loader.resolve(&import_specifier, name, false)?;
+ import_specifiers.push(module_specifier);
+ }
+
+ state_rc.borrow_mut().modules.register(
+ id,
+ name,
+ main,
+ v8::Global::<v8::Module>::new(tc_scope, module),
+ import_specifiers,
+ );
+
+ Ok(id)
+ }
+
+ /// Instantiates a ES module
+ ///
+ /// ErrBox can be downcast to a type that exposes additional information about
+ /// the V8 exception. By default this type is JsError, however it may be a
+ /// different type if JsRuntime::set_js_error_create_fn() has been used.
+ fn mod_instantiate(&mut self, id: ModuleId) -> Result<(), ErrBox> {
+ let state_rc = Self::state(self);
+ let state = state_rc.borrow();
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **self,
+ state.global_context.as_ref().unwrap(),
+ );
+ let tc_scope = &mut v8::TryCatch::new(scope);
+
+ let module = match state.modules.get_info(id) {
+ Some(info) => v8::Local::new(tc_scope, &info.handle),
+ None if id == 0 => return Ok(()),
+ _ => panic!("module id {} not found in module table", id),
+ };
+ drop(state);
+
+ if module.get_status() == v8::ModuleStatus::Errored {
+ exception_to_err_result(tc_scope, module.get_exception())?
+ }
+
+ let result =
+ module.instantiate_module(tc_scope, bindings::module_resolve_callback);
+ match result {
+ Some(_) => Ok(()),
+ None => {
+ let exception = tc_scope.exception().unwrap();
+ exception_to_err_result(tc_scope, exception)
+ }
+ }
+ }
+
+ /// Evaluates an already instantiated ES module.
+ ///
+ /// ErrBox can be downcast to a type that exposes additional information about
+ /// the V8 exception. By default this type is JsError, however it may be a
+ /// different type if JsRuntime::set_js_error_create_fn() has been used.
+ pub fn mod_evaluate(&mut self, id: ModuleId) -> Result<(), ErrBox> {
+ self.shared_init();
+
+ let state_rc = Self::state(self);
+
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **self,
+ state_rc.borrow().global_context.as_ref().unwrap(),
+ );
+
+ let module = state_rc
+ .borrow()
+ .modules
+ .get_info(id)
+ .map(|info| v8::Local::new(scope, &info.handle))
+ .expect("ModuleInfo not found");
+ let mut status = module.get_status();
+
+ if status == v8::ModuleStatus::Instantiated {
+ // 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 maybe_value = module.evaluate(scope);
+
+ // Update status after evaluating.
+ status = module.get_status();
+
+ if let Some(value) = maybe_value {
+ assert!(
+ status == v8::ModuleStatus::Evaluated
+ || status == v8::ModuleStatus::Errored
+ );
+ let promise = v8::Local::<v8::Promise>::try_from(value)
+ .expect("Expected to get promise as module evaluation result");
+ let promise_id = promise.get_identity_hash();
+ let mut state = state_rc.borrow_mut();
+ state.pending_promise_exceptions.remove(&promise_id);
+ } else {
+ assert!(status == v8::ModuleStatus::Errored);
+ }
+ }
+
+ match status {
+ v8::ModuleStatus::Evaluated => Ok(()),
+ v8::ModuleStatus::Errored => {
+ let exception = module.get_exception();
+ exception_to_err_result(scope, exception)
+ .map_err(|err| attach_handle_to_error(scope, err, exception))
+ }
+ other => panic!("Unexpected module status {:?}", other),
+ }
+ }
+
+ fn dyn_import_error(
+ &mut self,
+ id: ModuleLoadId,
+ err: ErrBox,
+ ) -> Result<(), ErrBox> {
+ let state_rc = Self::state(self);
+
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **self,
+ state_rc.borrow().global_context.as_ref().unwrap(),
+ );
+
+ let resolver_handle = state_rc
+ .borrow_mut()
+ .dyn_import_map
+ .remove(&id)
+ .expect("Invalid dyn import id");
+ let resolver = resolver_handle.get(scope);
+
+ let exception = err
+ .downcast_ref::<ErrWithV8Handle>()
+ .map(|err| err.get_handle(scope))
+ .unwrap_or_else(|| {
+ let message = err.to_string();
+ let message = v8::String::new(scope, &message).unwrap();
+ v8::Exception::type_error(scope, message)
+ });
+
+ resolver.reject(scope, exception).unwrap();
+ scope.perform_microtask_checkpoint();
+ Ok(())
+ }
+
+ fn dyn_import_done(
+ &mut self,
+ id: ModuleLoadId,
+ mod_id: ModuleId,
+ ) -> Result<(), ErrBox> {
+ let state_rc = Self::state(self);
+
+ debug!("dyn_import_done {} {:?}", id, mod_id);
+ assert!(mod_id != 0);
+ let scope = &mut v8::HandleScope::with_context(
+ &mut **self,
+ state_rc.borrow().global_context.as_ref().unwrap(),
+ );
+
+ let resolver_handle = state_rc
+ .borrow_mut()
+ .dyn_import_map
+ .remove(&id)
+ .expect("Invalid dyn import id");
+ let resolver = resolver_handle.get(scope);
+
+ let module = {
+ let state = state_rc.borrow();
+ state
+ .modules
+ .get_info(mod_id)
+ .map(|info| v8::Local::new(scope, &info.handle))
+ .expect("Dyn import module info not found")
+ };
+ // Resolution success
+ assert_eq!(module.get_status(), v8::ModuleStatus::Evaluated);
+
+ let module_namespace = module.get_module_namespace();
+ resolver.resolve(scope, module_namespace).unwrap();
+ scope.perform_microtask_checkpoint();
+ Ok(())
+ }
+
+ fn prepare_dyn_imports(
+ &mut self,
+ cx: &mut Context,
+ ) -> Poll<Result<(), ErrBox>> {
+ let state_rc = Self::state(self);
+
+ loop {
+ let r = {
+ let mut state = state_rc.borrow_mut();
+ state.preparing_dyn_imports.poll_next_unpin(cx)
+ };
+ match r {
+ Poll::Pending | Poll::Ready(None) => {
+ // There are no active dynamic import loaders, or none are ready.
+ return Poll::Ready(Ok(()));
+ }
+ Poll::Ready(Some(prepare_poll)) => {
+ let dyn_import_id = prepare_poll.0;
+ let prepare_result = prepare_poll.1;
+
+ match prepare_result {
+ Ok(load) => {
+ let state = state_rc.borrow_mut();
+ state.pending_dyn_imports.push(load.into_future());
+ }
+ Err(err) => {
+ self.dyn_import_error(dyn_import_id, err)?;
+ }
+ }
+ }
+ }
+ }
+ }
+
+ fn poll_dyn_imports(&mut self, cx: &mut Context) -> Poll<Result<(), ErrBox>> {
+ let state_rc = Self::state(self);
+ loop {
+ let poll_result = {
+ let mut state = state_rc.borrow_mut();
+ state.pending_dyn_imports.poll_next_unpin(cx)
+ };
+
+ match poll_result {
+ Poll::Pending | Poll::Ready(None) => {
+ // There are no active dynamic import loaders, or none are ready.
+ return Poll::Ready(Ok(()));
+ }
+ Poll::Ready(Some(load_stream_poll)) => {
+ 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(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.
+ match self.register_during_load(info, &mut load) {
+ Ok(()) => {
+ // Keep importing until it's fully drained
+ let state = state_rc.borrow_mut();
+ state.pending_dyn_imports.push(load.into_future());
+ }
+ Err(err) => self.dyn_import_error(dyn_import_id, err)?,
+ }
+ }
+ 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.
+ self.dyn_import_error(dyn_import_id, err)?
+ }
+ }
+ } else {
+ // The top-level module from a dynamic import has been instantiated.
+ // Load is done.
+ let module_id = load.root_module_id.unwrap();
+ self.mod_instantiate(module_id)?;
+ match self.mod_evaluate(module_id) {
+ Ok(()) => self.dyn_import_done(dyn_import_id, module_id)?,
+ Err(err) => self.dyn_import_error(dyn_import_id, err)?,
+ };
+ }
+ }
+ }
+ }
+ }
+
+ fn register_during_load(
+ &mut self,
+ info: ModuleSource,
+ load: &mut RecursiveModuleLoad,
+ ) -> Result<(), ErrBox> {
+ let ModuleSource {
+ code,
+ module_url_specified,
+ module_url_found,
+ } = info;
+
+ let is_main =
+ load.state == LoadState::LoadingRoot && !load.is_dynamic_import();
+ let referrer_specifier =
+ ModuleSpecifier::resolve_url(&module_url_found).unwrap();
+
+ let state_rc = Self::state(self);
+ // #A There are 3 cases to handle at this moment:
+ // 1. Source code resolved result have the same module name as requested
+ // and is not yet registered
+ // -> register
+ // 2. Source code resolved result have a different name as requested:
+ // 2a. The module with resolved module name has been registered
+ // -> alias
+ // 2b. The module with resolved module name has not yet been registered
+ // -> register & alias
+
+ // If necessary, register an alias.
+ if module_url_specified != module_url_found {
+ let mut state = state_rc.borrow_mut();
+ state
+ .modules
+ .alias(&module_url_specified, &module_url_found);
+ }
+
+ let maybe_mod_id = {
+ let state = state_rc.borrow();
+ state.modules.get_id(&module_url_found)
+ };
+
+ let module_id = match maybe_mod_id {
+ Some(id) => {
+ // Module has already been registered.
+ debug!(
+ "Already-registered module fetched again: {}",
+ module_url_found
+ );
+ id
+ }
+ // Module not registered yet, do it now.
+ None => self.mod_new(is_main, &module_url_found, &code)?,
+ };
+
+ // Now we must iterate over all imports of the module and load them.
+ let imports = {
+ let state_rc = Self::state(self);
+ let state = state_rc.borrow();
+ state.modules.get_children(module_id).unwrap().clone()
+ };
+
+ for module_specifier in imports {
+ let is_registered = {
+ let state_rc = Self::state(self);
+ let state = state_rc.borrow();
+ state.modules.is_registered(&module_specifier)
+ };
+ if !is_registered {
+ load
+ .add_import(module_specifier.to_owned(), referrer_specifier.clone());
+ }
+ }
+
+ // If we just finished loading the root module, store the root module id.
+ if load.state == LoadState::LoadingRoot {
+ load.root_module_id = Some(module_id);
+ load.state = LoadState::LoadingImports;
+ }
+
+ if load.pending.is_empty() {
+ load.state = LoadState::Done;
+ }
+
+ Ok(())
+ }
+
+ /// Asynchronously load specified module and all of it's dependencies
+ ///
+ /// User must call `JsRuntime::mod_evaluate` with returned `ModuleId`
+ /// manually after load is finished.
+ pub async fn load_module(
+ &mut self,
+ specifier: &ModuleSpecifier,
+ code: Option<String>,
+ ) -> Result<ModuleId, ErrBox> {
+ self.shared_init();
+ let loader = {
+ let state_rc = Self::state(self);
+ let state = state_rc.borrow();
+ state.loader.clone()
+ };
+
+ let load = RecursiveModuleLoad::main(&specifier.to_string(), code, loader);
+ let (_load_id, prepare_result) = load.prepare().await;
+
+ let mut load = prepare_result?;
+
+ while let Some(info_result) = load.next().await {
+ let info = info_result?;
+ self.register_during_load(info, &mut load)?;
+ }
+
+ let root_id = load.root_module_id.expect("Root module id empty");
+ self.mod_instantiate(root_id).map(|_| root_id)
+ }
+}
+
+#[cfg(test)]
+pub mod tests {
+ use super::*;
+ use crate::modules::ModuleSourceFuture;
+ use crate::ops::*;
+ use crate::BasicState;
+ use crate::BufVec;
+ use futures::future::lazy;
+ use futures::FutureExt;
+ use std::io;
+ use std::ops::FnOnce;
+ use std::rc::Rc;
+ use std::sync::atomic::{AtomicUsize, Ordering};
+ use std::sync::Arc;
+
+ pub fn run_in_task<F>(f: F)
+ where
+ F: FnOnce(&mut Context) + Send + 'static,
+ {
+ futures::executor::block_on(lazy(move |cx| f(cx)));
+ }
+
+ fn poll_until_ready<F>(future: &mut F, max_poll_count: usize) -> F::Output
+ where
+ F: Future + Unpin,
+ {
+ let mut cx = Context::from_waker(futures::task::noop_waker_ref());
+ for _ in 0..max_poll_count {
+ match future.poll_unpin(&mut cx) {
+ Poll::Pending => continue,
+ Poll::Ready(val) => return val,
+ }
+ }
+ panic!(
+ "JsRuntime still not ready after polling {} times.",
+ max_poll_count
+ )
+ }
+
+ enum Mode {
+ Async,
+ AsyncUnref,
+ AsyncZeroCopy(u8),
+ OverflowReqSync,
+ OverflowResSync,
+ OverflowReqAsync,
+ OverflowResAsync,
+ }
+
+ struct TestOpRouter {
+ mode: Mode,
+ dispatch_count: Arc<AtomicUsize>,
+ }
+
+ impl OpRouter for TestOpRouter {
+ fn route_op(self: Rc<Self>, op_id: OpId, bufs: BufVec) -> Op {
+ if op_id != 1 {
+ return Op::NotFound;
+ }
+ self.dispatch_count.fetch_add(1, Ordering::Relaxed);
+ match self.mode {
+ Mode::Async => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 1);
+ assert_eq!(bufs[0][0], 42);
+ let buf = vec![43u8].into_boxed_slice();
+ Op::Async(futures::future::ready(buf).boxed())
+ }
+ Mode::AsyncUnref => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 1);
+ assert_eq!(bufs[0][0], 42);
+ let fut = async {
+ // This future never finish.
+ futures::future::pending::<()>().await;
+ vec![43u8].into_boxed_slice()
+ };
+ Op::AsyncUnref(fut.boxed())
+ }
+ Mode::AsyncZeroCopy(count) => {
+ assert_eq!(bufs.len(), count as usize);
+ bufs.iter().enumerate().for_each(|(idx, buf)| {
+ assert_eq!(buf.len(), 1);
+ assert_eq!(idx, buf[0] as usize);
+ });
+
+ let buf = vec![43u8].into_boxed_slice();
+ Op::Async(futures::future::ready(buf).boxed())
+ }
+ Mode::OverflowReqSync => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 100 * 1024 * 1024);
+ let buf = vec![43u8].into_boxed_slice();
+ Op::Sync(buf)
+ }
+ Mode::OverflowResSync => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 1);
+ assert_eq!(bufs[0][0], 42);
+ let mut vec = Vec::<u8>::new();
+ vec.resize(100 * 1024 * 1024, 0);
+ vec[0] = 99;
+ let buf = vec.into_boxed_slice();
+ Op::Sync(buf)
+ }
+ Mode::OverflowReqAsync => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 100 * 1024 * 1024);
+ let buf = vec![43u8].into_boxed_slice();
+ Op::Async(futures::future::ready(buf).boxed())
+ }
+ Mode::OverflowResAsync => {
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 1);
+ assert_eq!(bufs[0][0], 42);
+ let mut vec = Vec::<u8>::new();
+ vec.resize(100 * 1024 * 1024, 0);
+ vec[0] = 4;
+ let buf = vec.into_boxed_slice();
+ Op::Async(futures::future::ready(buf).boxed())
+ }
+ }
+ }
+ }
+
+ fn setup(mode: Mode) -> (JsRuntime, Arc<AtomicUsize>) {
+ let dispatch_count = Arc::new(AtomicUsize::new(0));
+ let test_state = Rc::new(TestOpRouter {
+ mode,
+ dispatch_count: dispatch_count.clone(),
+ });
+ let mut runtime = JsRuntime::new(test_state, StartupData::None, false);
+
+ js_check(runtime.execute(
+ "setup.js",
+ r#"
+ function assert(cond) {
+ if (!cond) {
+ throw Error("assert");
+ }
+ }
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+ (runtime, dispatch_count)
+ }
+
+ #[test]
+ fn test_dispatch() {
+ let (mut runtime, dispatch_count) = setup(Mode::Async);
+ js_check(runtime.execute(
+ "filename.js",
+ r#"
+ let control = new Uint8Array([42]);
+ Deno.core.send(1, control);
+ async function main() {
+ Deno.core.send(1, control);
+ }
+ main();
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 2);
+ }
+
+ #[test]
+ fn test_dispatch_no_zero_copy_buf() {
+ let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(0));
+ js_check(runtime.execute(
+ "filename.js",
+ r#"
+ Deno.core.send(1);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn test_dispatch_stack_zero_copy_bufs() {
+ let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(2));
+ js_check(runtime.execute(
+ "filename.js",
+ r#"
+ let zero_copy_a = new Uint8Array([0]);
+ let zero_copy_b = new Uint8Array([1]);
+ Deno.core.send(1, zero_copy_a, zero_copy_b);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn test_dispatch_heap_zero_copy_bufs() {
+ let (mut runtime, dispatch_count) = setup(Mode::AsyncZeroCopy(5));
+ js_check(runtime.execute(
+ "filename.js",
+ r#"
+ let zero_copy_a = new Uint8Array([0]);
+ let zero_copy_b = new Uint8Array([1]);
+ let zero_copy_c = new Uint8Array([2]);
+ let zero_copy_d = new Uint8Array([3]);
+ let zero_copy_e = new Uint8Array([4]);
+ Deno.core.send(1, zero_copy_a, zero_copy_b, zero_copy_c, zero_copy_d, zero_copy_e);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn test_poll_async_delayed_ops() {
+ run_in_task(|cx| {
+ let (mut runtime, dispatch_count) = setup(Mode::Async);
+
+ js_check(runtime.execute(
+ "setup2.js",
+ r#"
+ let nrecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => {
+ nrecv++;
+ });
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+ js_check(runtime.execute(
+ "check1.js",
+ r#"
+ assert(nrecv == 0);
+ let control = new Uint8Array([42]);
+ Deno.core.send(1, control);
+ assert(nrecv == 0);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ js_check(runtime.execute(
+ "check2.js",
+ r#"
+ assert(nrecv == 1);
+ Deno.core.send(1, control);
+ assert(nrecv == 1);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 2);
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ js_check(runtime.execute("check3.js", "assert(nrecv == 2)"));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 2);
+ // We are idle, so the next poll should be the last.
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ });
+ }
+
+ #[test]
+ fn test_poll_async_optional_ops() {
+ run_in_task(|cx| {
+ let (mut runtime, dispatch_count) = setup(Mode::AsyncUnref);
+ js_check(runtime.execute(
+ "check1.js",
+ r#"
+ Deno.core.setAsyncHandler(1, (buf) => {
+ // This handler will never be called
+ assert(false);
+ });
+ let control = new Uint8Array([42]);
+ Deno.core.send(1, control);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ // The above op never finish, but runtime can finish
+ // because the op is an unreffed async op.
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ })
+ }
+
+ #[test]
+ fn terminate_execution() {
+ let (mut isolate, _dispatch_count) = setup(Mode::Async);
+ // TODO(piscisaureus): in rusty_v8, the `thread_safe_handle()` method
+ // should not require a mutable reference to `struct rusty_v8::Isolate`.
+ let v8_isolate_handle =
+ isolate.v8_isolate.as_mut().unwrap().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("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.
+ // TODO(piscisaureus): in rusty_v8, `cancel_terminate_execution()` should
+ // also be implemented on `struct Isolate`.
+ let ok = isolate
+ .v8_isolate
+ .as_mut()
+ .unwrap()
+ .thread_safe_handle()
+ .cancel_terminate_execution();
+ assert!(ok);
+
+ // Verify that the isolate usable again.
+ isolate
+ .execute("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);
+ // TODO(piscisaureus): in rusty_v8, the `thread_safe_handle()` method
+ // should not require a mutable reference to `struct rusty_v8::Isolate`.
+ runtime.v8_isolate.as_mut().unwrap().thread_safe_handle()
+ };
+
+ // this should not SEGFAULT
+ v8_isolate_handle.terminate_execution();
+ }
+
+ #[test]
+ fn overflow_req_sync() {
+ let (mut runtime, dispatch_count) = setup(Mode::OverflowReqSync);
+ js_check(runtime.execute(
+ "overflow_req_sync.js",
+ r#"
+ let asyncRecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => { asyncRecv++ });
+ // Large message that will overflow the shared space.
+ let control = new Uint8Array(100 * 1024 * 1024);
+ let response = Deno.core.dispatch(1, control);
+ assert(response instanceof Uint8Array);
+ assert(response.length == 1);
+ assert(response[0] == 43);
+ assert(asyncRecv == 0);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn overflow_res_sync() {
+ // TODO(ry) This test is quite slow due to memcpy-ing 100MB into JS. We
+ // should optimize this.
+ let (mut runtime, dispatch_count) = setup(Mode::OverflowResSync);
+ js_check(runtime.execute(
+ "overflow_res_sync.js",
+ r#"
+ let asyncRecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => { asyncRecv++ });
+ // Large message that will overflow the shared space.
+ let control = new Uint8Array([42]);
+ let response = Deno.core.dispatch(1, control);
+ assert(response instanceof Uint8Array);
+ assert(response.length == 100 * 1024 * 1024);
+ assert(response[0] == 99);
+ assert(asyncRecv == 0);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn overflow_req_async() {
+ run_in_task(|cx| {
+ let (mut runtime, dispatch_count) = setup(Mode::OverflowReqAsync);
+ js_check(runtime.execute(
+ "overflow_req_async.js",
+ r#"
+ let asyncRecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => {
+ assert(buf.byteLength === 1);
+ assert(buf[0] === 43);
+ asyncRecv++;
+ });
+ // Large message that will overflow the shared space.
+ let control = new Uint8Array(100 * 1024 * 1024);
+ let response = Deno.core.dispatch(1, control);
+ // Async messages always have null response.
+ assert(response == null);
+ assert(asyncRecv == 0);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ js_check(runtime.execute("check.js", "assert(asyncRecv == 1);"));
+ });
+ }
+
+ #[test]
+ fn overflow_res_async() {
+ run_in_task(|_cx| {
+ // TODO(ry) This test is quite slow due to memcpy-ing 100MB into JS. We
+ // should optimize this.
+ let (mut runtime, dispatch_count) = setup(Mode::OverflowResAsync);
+ js_check(runtime.execute(
+ "overflow_res_async.js",
+ r#"
+ let asyncRecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => {
+ assert(buf.byteLength === 100 * 1024 * 1024);
+ assert(buf[0] === 4);
+ asyncRecv++;
+ });
+ // Large message that will overflow the shared space.
+ let control = new Uint8Array([42]);
+ let response = Deno.core.dispatch(1, control);
+ assert(response == null);
+ assert(asyncRecv == 0);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ poll_until_ready(&mut runtime, 3).unwrap();
+ js_check(runtime.execute("check.js", "assert(asyncRecv == 1);"));
+ });
+ }
+
+ #[test]
+ fn overflow_res_multiple_dispatch_async() {
+ // TODO(ry) This test is quite slow due to memcpy-ing 100MB into JS. We
+ // should optimize this.
+ run_in_task(|_cx| {
+ let (mut runtime, dispatch_count) = setup(Mode::OverflowResAsync);
+ js_check(runtime.execute(
+ "overflow_res_multiple_dispatch_async.js",
+ r#"
+ let asyncRecv = 0;
+ Deno.core.setAsyncHandler(1, (buf) => {
+ assert(buf.byteLength === 100 * 1024 * 1024);
+ assert(buf[0] === 4);
+ asyncRecv++;
+ });
+ // Large message that will overflow the shared space.
+ let control = new Uint8Array([42]);
+ let response = Deno.core.dispatch(1, control);
+ assert(response == null);
+ assert(asyncRecv == 0);
+ // Dispatch another message to verify that pending ops
+ // are done even if shared space overflows
+ Deno.core.dispatch(1, control);
+ "#,
+ ));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 2);
+ poll_until_ready(&mut runtime, 3).unwrap();
+ js_check(runtime.execute("check.js", "assert(asyncRecv == 2);"));
+ });
+ }
+
+ #[test]
+ fn test_pre_dispatch() {
+ run_in_task(|mut cx| {
+ let (mut runtime, _dispatch_count) = setup(Mode::OverflowResAsync);
+ js_check(runtime.execute(
+ "bad_op_id.js",
+ r#"
+ let thrown;
+ try {
+ Deno.core.dispatch(100);
+ } catch (e) {
+ thrown = e;
+ }
+ assert(String(thrown) === "TypeError: Unknown op id: 100");
+ "#,
+ ));
+ if let Poll::Ready(Err(_)) = runtime.poll_unpin(&mut cx) {
+ unreachable!();
+ }
+ });
+ }
+
+ #[test]
+ fn core_test_js() {
+ run_in_task(|mut cx| {
+ let (mut runtime, _dispatch_count) = setup(Mode::Async);
+ js_check(runtime.execute("core_test.js", include_str!("core_test.js")));
+ if let Poll::Ready(Err(_)) = runtime.poll_unpin(&mut cx) {
+ unreachable!();
+ }
+ });
+ }
+
+ #[test]
+ fn syntax_error() {
+ let mut runtime =
+ JsRuntime::new(BasicState::new(), StartupData::None, false);
+ let src = "hocuspocus(";
+ let r = runtime.execute("i.js", src);
+ let e = r.unwrap_err();
+ let js_error = e.downcast::<JsError>().unwrap();
+ assert_eq!(js_error.end_column, Some(11));
+ }
+
+ #[test]
+ fn test_encode_decode() {
+ run_in_task(|mut cx| {
+ let (mut runtime, _dispatch_count) = setup(Mode::Async);
+ js_check(runtime.execute(
+ "encode_decode_test.js",
+ include_str!("encode_decode_test.js"),
+ ));
+ if let Poll::Ready(Err(_)) = runtime.poll_unpin(&mut cx) {
+ unreachable!();
+ }
+ });
+ }
+
+ #[test]
+ fn will_snapshot() {
+ let snapshot = {
+ let mut runtime =
+ JsRuntime::new(BasicState::new(), StartupData::None, true);
+ js_check(runtime.execute("a.js", "a = 1 + 2"));
+ runtime.snapshot()
+ };
+
+ let startup_data = StartupData::Snapshot(Snapshot::JustCreated(snapshot));
+ let mut runtime2 = JsRuntime::new(BasicState::new(), startup_data, false);
+ js_check(runtime2.execute("check.js", "if (a != 3) throw Error('x')"));
+ }
+
+ #[test]
+ fn test_from_boxed_snapshot() {
+ let snapshot = {
+ let mut runtime =
+ JsRuntime::new(BasicState::new(), StartupData::None, true);
+ js_check(runtime.execute("a.js", "a = 1 + 2"));
+ let snap: &[u8] = &*runtime.snapshot();
+ Vec::from(snap).into_boxed_slice()
+ };
+
+ let startup_data = StartupData::Snapshot(Snapshot::Boxed(snapshot));
+ let mut runtime2 = JsRuntime::new(BasicState::new(), startup_data, false);
+ js_check(runtime2.execute("check.js", "if (a != 3) throw Error('x')"));
+ }
+
+ #[test]
+ fn test_heap_limits() {
+ let heap_limits = HeapLimits {
+ initial: 0,
+ max: 20 * 1024, // 20 kB
+ };
+ let mut runtime = JsRuntime::with_heap_limits(
+ BasicState::new(),
+ StartupData::None,
+ heap_limits,
+ );
+ let cb_handle = runtime.thread_safe_handle();
+
+ let callback_invoke_count = Rc::new(AtomicUsize::default());
+ 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 name",
+ r#"let s = ""; while(true) { s += "Hello"; }"#,
+ )
+ .expect_err("script should fail");
+ assert_eq!(
+ "Uncaught Error: execution terminated",
+ err.downcast::<JsError>().unwrap().message
+ );
+ assert!(callback_invoke_count.load(Ordering::SeqCst) > 0)
+ }
+
+ #[test]
+ fn test_heap_limit_cb_remove() {
+ let mut runtime =
+ JsRuntime::new(BasicState::new(), StartupData::None, false);
+
+ runtime.add_near_heap_limit_callback(|current_limit, _initial_limit| {
+ current_limit * 2
+ });
+ runtime.remove_near_heap_limit_callback(20 * 1024);
+ assert!(runtime.allocations.near_heap_limit_callback_data.is_none());
+ }
+
+ #[test]
+ fn test_heap_limit_cb_multiple() {
+ let heap_limits = HeapLimits {
+ initial: 0,
+ max: 20 * 1024, // 20 kB
+ };
+ let mut runtime = JsRuntime::with_heap_limits(
+ BasicState::new(),
+ StartupData::None,
+ heap_limits,
+ );
+ let cb_handle = runtime.thread_safe_handle();
+
+ let callback_invoke_count_first = Rc::new(AtomicUsize::default());
+ 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::default());
+ 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 name",
+ r#"let s = ""; while(true) { s += "Hello"; }"#,
+ )
+ .expect_err("script should fail");
+ assert_eq!(
+ "Uncaught Error: execution terminated",
+ err.downcast::<JsError>().unwrap().message
+ );
+ assert_eq!(0, callback_invoke_count_first.load(Ordering::SeqCst));
+ assert!(callback_invoke_count_second.load(Ordering::SeqCst) > 0);
+ }
+
+ #[test]
+ fn test_mods() {
+ #[derive(Default)]
+ struct ModsLoader {
+ pub count: Arc<AtomicUsize>,
+ }
+
+ impl ModuleLoader for ModsLoader {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ _is_main: bool,
+ ) -> Result<ModuleSpecifier, ErrBox> {
+ self.count.fetch_add(1, Ordering::Relaxed);
+ assert_eq!(specifier, "./b.js");
+ assert_eq!(referrer, "file:///a.js");
+ let s = ModuleSpecifier::resolve_import(specifier, referrer).unwrap();
+ Ok(s)
+ }
+
+ fn load(
+ &self,
+ _module_specifier: &ModuleSpecifier,
+ _maybe_referrer: Option<ModuleSpecifier>,
+ _is_dyn_import: bool,
+ ) -> Pin<Box<ModuleSourceFuture>> {
+ unreachable!()
+ }
+ }
+
+ let loader = Rc::new(ModsLoader::default());
+ let state = BasicState::new();
+
+ let resolve_count = loader.count.clone();
+ let dispatch_count = Arc::new(AtomicUsize::new(0));
+ let dispatch_count_ = dispatch_count.clone();
+
+ let dispatcher = move |_state: Rc<BasicState>, bufs: BufVec| -> Op {
+ dispatch_count_.fetch_add(1, Ordering::Relaxed);
+ assert_eq!(bufs.len(), 1);
+ assert_eq!(bufs[0].len(), 1);
+ assert_eq!(bufs[0][0], 42);
+ let buf = [43u8, 0, 0, 0][..].into();
+ Op::Async(futures::future::ready(buf).boxed())
+ };
+ state.register_op("test", dispatcher);
+
+ let mut runtime =
+ JsRuntime::new_with_loader(loader, state, StartupData::None, false);
+
+ js_check(runtime.execute(
+ "setup.js",
+ r#"
+ function assert(cond) {
+ if (!cond) {
+ throw Error("assert");
+ }
+ }
+ "#,
+ ));
+
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+
+ let specifier_a = "file:///a.js".to_string();
+ let mod_a = runtime
+ .mod_new(
+ true,
+ &specifier_a,
+ r#"
+ import { b } from './b.js'
+ if (b() != 'b') throw Error();
+ let control = new Uint8Array([42]);
+ Deno.core.send(1, control);
+ "#,
+ )
+ .unwrap();
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+
+ let state_rc = JsRuntime::state(&runtime);
+ {
+ let state = state_rc.borrow();
+ let imports = state.modules.get_children(mod_a);
+ assert_eq!(
+ imports,
+ Some(&vec![ModuleSpecifier::resolve_url("file:///b.js").unwrap()])
+ );
+ }
+ let mod_b = runtime
+ .mod_new(false, "file:///b.js", "export function b() { return 'b' }")
+ .unwrap();
+ {
+ let state = state_rc.borrow();
+ let imports = state.modules.get_children(mod_b).unwrap();
+ assert_eq!(imports.len(), 0);
+ }
+
+ js_check(runtime.mod_instantiate(mod_b));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+ assert_eq!(resolve_count.load(Ordering::SeqCst), 1);
+
+ js_check(runtime.mod_instantiate(mod_a));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 0);
+
+ js_check(runtime.mod_evaluate(mod_a));
+ assert_eq!(dispatch_count.load(Ordering::Relaxed), 1);
+ }
+
+ #[test]
+ fn dyn_import_err() {
+ #[derive(Clone, Default)]
+ struct DynImportErrLoader {
+ pub count: Arc<AtomicUsize>,
+ }
+
+ impl ModuleLoader for DynImportErrLoader {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ _is_main: bool,
+ ) -> Result<ModuleSpecifier, ErrBox> {
+ self.count.fetch_add(1, Ordering::Relaxed);
+ assert_eq!(specifier, "/foo.js");
+ assert_eq!(referrer, "file:///dyn_import2.js");
+ let s = ModuleSpecifier::resolve_import(specifier, referrer).unwrap();
+ Ok(s)
+ }
+
+ fn load(
+ &self,
+ _module_specifier: &ModuleSpecifier,
+ _maybe_referrer: Option<ModuleSpecifier>,
+ _is_dyn_import: bool,
+ ) -> Pin<Box<ModuleSourceFuture>> {
+ async { Err(io::Error::from(io::ErrorKind::NotFound).into()) }.boxed()
+ }
+ }
+
+ // Test an erroneous dynamic import where the specified module isn't found.
+ run_in_task(|cx| {
+ let loader = Rc::new(DynImportErrLoader::default());
+ let count = loader.count.clone();
+ let mut runtime = JsRuntime::new_with_loader(
+ loader,
+ BasicState::new(),
+ StartupData::None,
+ false,
+ );
+
+ js_check(runtime.execute(
+ "file:///dyn_import2.js",
+ r#"
+ (async () => {
+ await import("/foo.js");
+ })();
+ "#,
+ ));
+
+ assert_eq!(count.load(Ordering::Relaxed), 0);
+ // We should get an error here.
+ let result = runtime.poll_unpin(cx);
+ if let Poll::Ready(Ok(_)) = result {
+ unreachable!();
+ }
+ assert_eq!(count.load(Ordering::Relaxed), 2);
+ })
+ }
+
+ #[derive(Clone, Default)]
+ struct DynImportOkLoader {
+ pub prepare_load_count: Arc<AtomicUsize>,
+ pub resolve_count: Arc<AtomicUsize>,
+ pub load_count: Arc<AtomicUsize>,
+ }
+
+ impl ModuleLoader for DynImportOkLoader {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ _is_main: bool,
+ ) -> Result<ModuleSpecifier, ErrBox> {
+ let c = self.resolve_count.fetch_add(1, Ordering::Relaxed);
+ assert!(c < 4);
+ assert_eq!(specifier, "./b.js");
+ assert_eq!(referrer, "file:///dyn_import3.js");
+ let s = ModuleSpecifier::resolve_import(specifier, referrer).unwrap();
+ Ok(s)
+ }
+
+ fn load(
+ &self,
+ specifier: &ModuleSpecifier,
+ _maybe_referrer: Option<ModuleSpecifier>,
+ _is_dyn_import: bool,
+ ) -> Pin<Box<ModuleSourceFuture>> {
+ self.load_count.fetch_add(1, Ordering::Relaxed);
+ let info = ModuleSource {
+ module_url_specified: specifier.to_string(),
+ module_url_found: specifier.to_string(),
+ code: "export function b() { return 'b' }".to_owned(),
+ };
+ async move { Ok(info) }.boxed()
+ }
+
+ fn prepare_load(
+ &self,
+ _load_id: ModuleLoadId,
+ _module_specifier: &ModuleSpecifier,
+ _maybe_referrer: Option<String>,
+ _is_dyn_import: bool,
+ ) -> Pin<Box<dyn Future<Output = Result<(), ErrBox>>>> {
+ self.prepare_load_count.fetch_add(1, Ordering::Relaxed);
+ async { Ok(()) }.boxed_local()
+ }
+ }
+
+ #[test]
+ fn dyn_import_ok() {
+ run_in_task(|cx| {
+ let loader = Rc::new(DynImportOkLoader::default());
+ let prepare_load_count = loader.prepare_load_count.clone();
+ let resolve_count = loader.resolve_count.clone();
+ let load_count = loader.load_count.clone();
+ let mut runtime = JsRuntime::new_with_loader(
+ loader,
+ BasicState::new(),
+ StartupData::None,
+ false,
+ );
+
+ // Dynamically import mod_b
+ js_check(runtime.execute(
+ "file:///dyn_import3.js",
+ r#"
+ (async () => {
+ let mod = await import("./b.js");
+ if (mod.b() !== 'b') {
+ throw Error("bad1");
+ }
+ // And again!
+ mod = await import("./b.js");
+ if (mod.b() !== 'b') {
+ throw Error("bad2");
+ }
+ })();
+ "#,
+ ));
+
+ // First poll runs `prepare_load` hook.
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Pending));
+ assert_eq!(prepare_load_count.load(Ordering::Relaxed), 1);
+
+ // Second poll actually loads modules into the isolate.
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ assert_eq!(resolve_count.load(Ordering::Relaxed), 4);
+ assert_eq!(load_count.load(Ordering::Relaxed), 2);
+ assert!(matches!(runtime.poll_unpin(cx), Poll::Ready(Ok(_))));
+ assert_eq!(resolve_count.load(Ordering::Relaxed), 4);
+ assert_eq!(load_count.load(Ordering::Relaxed), 2);
+ })
+ }
+
+ #[test]
+ fn dyn_import_borrow_mut_error() {
+ // https://github.com/denoland/deno/issues/6054
+ run_in_task(|cx| {
+ let loader = Rc::new(DynImportOkLoader::default());
+ let prepare_load_count = loader.prepare_load_count.clone();
+ let mut runtime = JsRuntime::new_with_loader(
+ loader,
+ BasicState::new(),
+ StartupData::None,
+ false,
+ );
+ js_check(runtime.execute(
+ "file:///dyn_import3.js",
+ r#"
+ (async () => {
+ let mod = await import("./b.js");
+ if (mod.b() !== 'b') {
+ throw Error("bad");
+ }
+ // Now do any op
+ Deno.core.ops();
+ })();
+ "#,
+ ));
+ // First poll runs `prepare_load` hook.
+ let _ = runtime.poll_unpin(cx);
+ assert_eq!(prepare_load_count.load(Ordering::Relaxed), 1);
+ // Second poll triggers error
+ let _ = runtime.poll_unpin(cx);
+ })
+ }
+
+ #[test]
+ fn es_snapshot() {
+ #[derive(Default)]
+ struct ModsLoader;
+
+ impl ModuleLoader for ModsLoader {
+ fn resolve(
+ &self,
+ specifier: &str,
+ referrer: &str,
+ _is_main: bool,
+ ) -> Result<ModuleSpecifier, ErrBox> {
+ assert_eq!(specifier, "file:///main.js");
+ assert_eq!(referrer, ".");
+ let s = ModuleSpecifier::resolve_import(specifier, referrer).unwrap();
+ Ok(s)
+ }
+
+ fn load(
+ &self,
+ _module_specifier: &ModuleSpecifier,
+ _maybe_referrer: Option<ModuleSpecifier>,
+ _is_dyn_import: bool,
+ ) -> Pin<Box<ModuleSourceFuture>> {
+ unreachable!()
+ }
+ }
+
+ let loader = std::rc::Rc::new(ModsLoader::default());
+ let mut runtime = JsRuntime::new_with_loader(
+ loader,
+ BasicState::new(),
+ StartupData::None,
+ true,
+ );
+
+ let specifier = ModuleSpecifier::resolve_url("file:///main.js").unwrap();
+ let source_code = "Deno.core.print('hello\\n')".to_string();
+
+ let module_id = futures::executor::block_on(
+ runtime.load_module(&specifier, Some(source_code)),
+ )
+ .unwrap();
+
+ js_check(runtime.mod_evaluate(module_id));
+
+ let _snapshot = runtime.snapshot();
+ }
+}