summaryrefslogtreecommitdiff
path: root/core/runtime/jsrealm.rs
diff options
context:
space:
mode:
Diffstat (limited to 'core/runtime/jsrealm.rs')
-rw-r--r--core/runtime/jsrealm.rs367
1 files changed, 367 insertions, 0 deletions
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<Rc<v8::Global<v8::Function>>>,
+ pub(crate) js_build_custom_error_cb: Option<Rc<v8::Global<v8::Function>>>,
+ pub(crate) js_promise_reject_cb: Option<Rc<v8::Global<v8::Function>>>,
+ pub(crate) js_format_exception_cb: Option<Rc<v8::Global<v8::Function>>>,
+ pub(crate) js_wasm_streaming_cb: Option<Rc<v8::Global<v8::Function>>>,
+ pub(crate) pending_promise_rejections:
+ VecDeque<(v8::Global<v8::Promise>, v8::Global<v8::Value>)>,
+ pub(crate) unrefed_ops: HashSet<i32, BuildHasherDefault<IdentityHasher>>,
+ pub(crate) pending_ops:
+ JoinSet<MaskResultAsSend<(PromiseId, OpId, OpResult)>>,
+ // 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(), "<anon>", 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<RefCell<ContextState>>,
+ context: Rc<v8::Global<v8::Context>>,
+ runtime_state: Rc<RefCell<JsRuntimeState>>,
+ is_global: bool,
+}
+
+impl JsRealmInner {
+ pub(crate) fn new(
+ context_state: Rc<RefCell<ContextState>>,
+ context: v8::Global<v8::Context>,
+ runtime_state: Rc<RefCell<JsRuntimeState>>,
+ 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<v8::Context> {
+ &self.context
+ }
+
+ #[inline(always)]
+ pub(crate) fn state(&self) -> Rc<RefCell<ContextState>> {
+ 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<v8::Global<v8::Context>>) -> 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<RefCell<ContextState>> {
+ let context = scope.get_current_context();
+ context
+ .get_slot::<Rc<RefCell<ContextState>>>(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<v8::Context> {
+ 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<Local<'a, v8::String>> {
+ 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"
+ /// - "<anon>"
+ /// - "[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<v8::Global<v8::Value>, 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"
+ /// - "<anon>"
+ /// - "[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<v8::Global<v8::Value>, 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);
+ }
+ }
+}