Repository: denoland/deno_core
Pull request #59 (2023-07-20 00:29:38)

refactor: Add methods for module handling to JsRealm
Extracted from https://github.com/denoland/deno_core/pull/41.


Patch:
diff --git a/core/benches/ops/sync.rs b/core/benches/ops/sync.rs
index 82a16fd..24708a7 100644
--- a/core/benches/ops/sync.rs
+++ b/core/benches/ops/sync.rs
@@ -11,6 +11,11 @@ deno_core::extension!(
     op_string,
     op_string_old,
     op_string_option_u32,
+    op_local,
+    op_local_scope,
+    op_local_nofast,
+    op_global,
+    op_global_scope,
   ],
   state = |state| {
     state.put(1234u32);
@@ -46,6 +51,26 @@ pub fn op_string_option_u32(#[string] s: &str) -> Option<u32> {
   Some(s.len() as _)
 }
 
+#[op2(fast)]
+pub fn op_local(_s: v8::Local<v8::String>) {}
+
+#[op2]
+pub fn op_local_scope(_scope: &mut v8::HandleScope, _s: v8::Local<v8::String>) {
+}
+
+#[op2(nofast)]
+pub fn op_local_nofast(_s: v8::Local<v8::String>) {}
+
+#[op2]
+pub fn op_global(#[global] _s: v8::Global<v8::String>) {}
+
+#[op2]
+pub fn op_global_scope(
+  _scope: &mut v8::HandleScope,
+  #[global] _s: v8::Global<v8::String>,
+) {
+}
+
 fn bench_op(
   b: &mut Bencher,
   count: usize,
@@ -253,6 +278,31 @@ fn bench_op_string_option_u32(b: &mut Bencher) {
   bench_op(b, BENCH_COUNT, "op_string_option_u32", 1, "accum += op_string_option_u32('this is a reasonably long string that we would like to get the length of!');");
 }
 
+/// A fast function that takes a v8::Local<String>
+fn bench_op_v8_local(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_local", 1, "op_local('this is a reasonably long string that we would like to get the length of!');");
+}
+
+/// A function that takes a v8::Local<String>
+fn bench_op_v8_local_scope(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_local_scope", 1, "op_local_scope('this is a reasonably long string that we would like to get the length of!');");
+}
+
+/// A function that takes a v8::Local<String>
+fn bench_op_v8_local_nofast(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_local_nofast", 1, "op_local_nofast('this is a reasonably long string that we would like to get the length of!');");
+}
+
+/// A function that takes a v8::Global<String>
+fn bench_op_v8_global(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_global", 1, "op_global('this is a reasonably long string that we would like to get the length of!');");
+}
+
+/// A function that takes a v8::Global<String>
+fn bench_op_v8_global_scope(b: &mut Bencher) {
+  bench_op(b, BENCH_COUNT, "op_global_scope", 1, "op_global_scope('this is a reasonably long string that we would like to get the length of!');");
+}
+
 benchmark_group!(
   benches,
   baseline,
@@ -270,6 +320,11 @@ benchmark_group!(
   bench_op_string_old_large_utf8_1000,
   bench_op_string_old_large_utf8_1000000,
   bench_op_string_option_u32,
+  bench_op_v8_local,
+  bench_op_v8_local_scope,
+  bench_op_v8_local_nofast,
+  bench_op_v8_global,
+  bench_op_v8_global_scope,
 );
 
 benchmark_main!(benches);
diff --git a/core/ops.rs b/core/ops.rs
index 80e1c14..1c5ab13 100644
--- a/core/ops.rs
+++ b/core/ops.rs
@@ -24,6 +24,7 @@ use std::sync::Arc;
 use v8::fast_api::CFunctionInfo;
 use v8::fast_api::CTypeInfo;
 use v8::fast_api::Int64Representation;
+use v8::Isolate;
 
 pub type PromiseId = i32;
 pub type OpId = u16;
@@ -125,6 +126,8 @@ pub fn to_op_result<R: Serialize + 'static>(
 // stored in a contiguous array.
 pub struct OpCtx {
   pub id: OpId,
+  /// A stashed Isolate that ops can use in callbacks
+  pub isolate: *mut Isolate,
   pub state: Rc<RefCell<OpState>>,
   pub decl: Rc<OpDecl>,
   pub fast_fn_c_info: Option<NonNull<v8::fast_api::CFunctionInfo>>,
@@ -137,6 +140,7 @@ pub struct OpCtx {
 impl OpCtx {
   pub(crate) fn new(
     id: OpId,
+    isolate: *mut Isolate,
     context_state: Rc<RefCell<ContextState>>,
     decl: Rc<OpDecl>,
     state: Rc<RefCell<OpState>>,
@@ -171,6 +175,7 @@ impl OpCtx {
       context_state,
       fast_fn_c_info,
       last_fast_error: UnsafeCell::new(None),
+      isolate,
     }
   }
 
diff --git a/core/ops_builtin_v8.rs b/core/ops_builtin_v8.rs
index 64daae1..d2303c1 100644
--- a/core/ops_builtin_v8.rs
+++ b/core/ops_builtin_v8.rs
@@ -38,9 +38,8 @@ pub fn op_unref_op(scope: &mut v8::HandleScope, promise_id: i32) {
 #[op2(core)]
 pub fn op_set_promise_reject_callback<'a>(
   scope: &mut v8::HandleScope<'a>,
-  cb: v8::Local<'a, v8::Function>,
+  #[global] cb: v8::Global<v8::Function>,
 ) -> Option<v8::Local<'a, v8::Function>> {
-  let cb = v8::Global::new(scope, cb);
   let context_state_rc = JsRealm::state_from_scope(scope);
   let old = context_state_rc
     .borrow_mut()
@@ -686,9 +685,8 @@ pub fn op_memory_usage(scope: &mut v8::HandleScope) -> MemoryUsage {
 #[op2(core)]
 pub fn op_set_wasm_streaming_callback(
   scope: &mut v8::HandleScope,
-  cb: v8::Local<v8::Function>,
+  #[global] cb: v8::Global<v8::Function>,
 ) -> Result<(), Error> {
-  let cb = v8::Global::new(scope, cb);
   let context_state_rc = JsRealm::state_from_scope(scope);
   let mut context_state = context_state_rc.borrow_mut();
   // The callback to pass to the v8 API has to be a unit type, so it can't
@@ -850,9 +848,8 @@ pub fn op_apply_source_map(
 #[op2(core)]
 pub fn op_set_format_exception_callback<'a>(
   scope: &mut v8::HandleScope<'a>,
-  cb: v8::Local<'a, v8::Function>,
+  #[global] cb: v8::Global<v8::Function>,
 ) -> Option<v8::Local<'a, v8::Value>> {
-  let cb = v8::Global::new(scope, cb);
   let context_state_rc = JsRealm::state_from_scope(scope);
   let old = context_state_rc
     .borrow_mut()
@@ -870,43 +867,39 @@ pub fn op_event_loop_has_more_work(scope: &mut v8::HandleScope) -> bool {
 #[op2(core)]
 pub fn op_store_pending_promise_rejection(
   scope: &mut v8::HandleScope,
-  promise: v8::Local<v8::Promise>,
-  reason: v8::Local<v8::Value>,
+  #[global] promise: v8::Global<v8::Promise>,
+  #[global] reason: v8::Global<v8::Value>,
 ) {
   let context_state_rc = JsRealm::state_from_scope(scope);
   let mut context_state = context_state_rc.borrow_mut();
-  let promise_global = v8::Global::new(scope, promise);
-  let error_global = v8::Global::new(scope, reason);
   context_state
     .pending_promise_rejections
-    .push_back((promise_global, error_global));
+    .push_back((promise, reason));
 }
 
 #[op2(core)]
 pub fn op_remove_pending_promise_rejection(
   scope: &mut v8::HandleScope,
-  promise: v8::Local<v8::Promise>,
+  #[global] promise: v8::Global<v8::Promise>,
 ) {
   let context_state_rc = JsRealm::state_from_scope(scope);
   let mut context_state = context_state_rc.borrow_mut();
-  let promise_global = v8::Global::new(scope, promise);
   context_state
     .pending_promise_rejections
-    .retain(|(key, _)| key != &promise_global);
+    .retain(|(key, _)| key != &promise);
 }
 
 #[op2(core)]
 pub fn op_has_pending_promise_rejection(
   scope: &mut v8::HandleScope,
-  promise: v8::Local<v8::Promise>,
+  #[global] promise: v8::Global<v8::Promise>,
 ) -> bool {
   let context_state_rc = JsRealm::state_from_scope(scope);
   let context_state = context_state_rc.borrow();
-  let promise_global = v8::Global::new(scope, promise);
   context_state
     .pending_promise_rejections
     .iter()
-    .any(|(key, _)| key == &promise_global)
+    .any(|(key, _)| key == &promise)
 }
 
 #[op2(core)]
diff --git a/core/runtime/jsrealm.rs b/core/runtime/jsrealm.rs
index b01a4fb..1862f39 100644
--- a/core/runtime/jsrealm.rs
+++ b/core/runtime/jsrealm.rs
@@ -1,8 +1,14 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use super::bindings;
 use crate::error::exception_to_err_result;
+use crate::error::generic_error;
+use crate::error::to_v8_type_error;
 use crate::joinset::JoinSet;
+use crate::module_specifier::ModuleSpecifier;
 use crate::modules::ModuleCode;
+use crate::modules::ModuleError;
+use crate::modules::ModuleId;
+use crate::modules::ModuleLoadId;
 use crate::modules::ModuleMap;
 use crate::ops::OpCtx;
 use crate::runtime::JsRuntimeState;
@@ -11,6 +17,8 @@ use crate::OpId;
 use crate::OpResult;
 use crate::PromiseId;
 use anyhow::Error;
+use futures::channel::oneshot;
+use futures::stream::StreamExt;
 use std::cell::RefCell;
 use std::collections::HashSet;
 use std::collections::VecDeque;
@@ -18,6 +26,8 @@ use std::hash::BuildHasherDefault;
 use std::hash::Hasher;
 use std::option::Option;
 use std::rc::Rc;
+use std::task::Context;
+use std::task::Poll;
 use v8::HandleScope;
 use v8::Local;
 
@@ -40,6 +50,20 @@ impl Hasher for IdentityHasher {
   }
 }
 
+pub(crate) struct DynImportModEvaluate {
+  pub(crate) load_id: ModuleLoadId,
+  pub(crate) module_id: ModuleId,
+  pub(crate) promise: v8::Global<v8::Promise>,
+  pub(crate) module: v8::Global<v8::Module>,
+}
+
+pub(crate) struct ModEvaluate {
+  pub(crate) promise: Option<v8::Global<v8::Promise>>,
+  pub(crate) has_evaluated: bool,
+  pub(crate) handled_promise_rejections: Vec<v8::Global<v8::Promise>>,
+  pub(crate) sender: oneshot::Sender<Result<(), Error>>,
+}
+
 #[derive(Default)]
 pub(crate) struct ContextState {
   pub(crate) js_event_loop_tick_cb: Option<Rc<v8::Global<v8::Function>>>,
@@ -49,6 +73,10 @@ pub(crate) struct ContextState {
   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>)>,
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub(crate) pending_dyn_mod_evaluate: Vec<DynImportModEvaluate>,
+  pub(crate) pending_mod_evaluate: Option<ModEvaluate>,
   pub(crate) unrefed_ops: HashSet<i32, BuildHasherDefault<IdentityHasher>>,
   pub(crate) pending_ops: JoinSet<(PromiseId, OpId, OpResult)>,
   // We don't explicitly re-read this prop but need the slice to live alongside
@@ -140,6 +168,28 @@ impl JsRealmInner {
     self.context_state.borrow().unrefed_ops.len()
   }
 
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub fn has_pending_dyn_imports(&self) -> bool {
+    self.module_map.borrow().has_pending_dynamic_imports()
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub fn has_pending_dyn_module_evaluation(&self) -> bool {
+    !self
+      .context_state
+      .borrow()
+      .pending_dyn_mod_evaluate
+      .is_empty()
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub fn has_pending_module_evaluation(&self) -> bool {
+    self.context_state.borrow().pending_mod_evaluate.is_some()
+  }
+
   #[inline(always)]
   pub fn context(&self) -> &v8::Global<v8::Context> {
     &self.context
@@ -361,7 +411,711 @@ impl JsRealm {
     }
   }
 
-  // TODO(andreubotella): `mod_evaluate`, `load_main_module`, `load_side_module`
+  /// 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(
+    &self,
+    isolate: &mut v8::Isolate,
+    module_id: ModuleId,
+  ) -> Result<v8::Global<v8::Object>, Error> {
+    self
+      .0
+      .module_map()
+      .borrow()
+      .get_module_namespace(&mut self.handle_scope(isolate), module_id)
+  }
+
+  pub(crate) fn instantiate_module(
+    &self,
+    isolate: &mut v8::Isolate,
+    id: ModuleId,
+  ) -> Result<(), v8::Global<v8::Value>> {
+    self
+      .0
+      .module_map()
+      .borrow_mut()
+      .instantiate_module(&mut self.handle_scope(isolate), id)
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  fn dynamic_import_module_evaluate(
+    &self,
+    isolate: &mut v8::Isolate,
+    load_id: ModuleLoadId,
+    id: ModuleId,
+  ) -> Result<(), Error> {
+    let module_handle = self
+      .0
+      .module_map()
+      .borrow()
+      .get_handle(id)
+      .expect("ModuleInfo not found");
+
+    let status = {
+      let scope = &mut self.handle_scope(isolate);
+      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 scope = &mut self.handle_scope(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::<v8::Promise>::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
+        .0
+        .context_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(
+    &self,
+    isolate: &mut v8::Isolate,
+    id: ModuleId,
+  ) -> oneshot::Receiver<Result<(), Error>> {
+    let state_rc = self.0.state();
+    let module_map_rc = self.0.module_map();
+    let scope = &mut self.handle_scope(isolate);
+    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,
+      "{} {} ({})",
+      if status == v8::ModuleStatus::Evaluated {
+        "Module already evaluated. Perhaps you've re-provided a module or extension that was already included in the snapshot?"
+      } else {
+        "Module not instantiated"
+      },
+      module_map_rc
+        .borrow()
+        .get_info_by_id(id)
+        .unwrap()
+        .name
+        .as_str(),
+      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 = self
+      .0
+      .runtime_state
+      .borrow_mut()
+      .dispatched_exception
+      .is_some();
+    if has_dispatched_exception {
+      // This will be overridden 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::<v8::Promise>::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 {
+          state
+            .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
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  fn dynamic_import_reject(
+    &self,
+    isolate: &mut v8::Isolate,
+    id: ModuleLoadId,
+    exception: v8::Global<v8::Value>,
+  ) {
+    let module_map_rc = self.0.module_map();
+    let scope = &mut self.handle_scope(isolate);
+
+    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();
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  fn dynamic_import_resolve(
+    &self,
+    isolate: &mut v8::Isolate,
+    id: ModuleLoadId,
+    mod_id: ModuleId,
+  ) {
+    let state_rc = self.0.runtime_state.clone();
+    let module_map_rc = self.0.module_map();
+    let scope = &mut self.handle_scope(isolate);
+
+    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();
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub(in crate::runtime) fn prepare_dyn_imports(
+    &self,
+    isolate: &mut v8::Isolate,
+    cx: &mut Context,
+  ) -> Poll<Result<(), Error>> {
+    if self
+      .0
+      .module_map()
+      .borrow()
+      .preparing_dynamic_imports
+      .is_empty()
+    {
+      return Poll::Ready(Ok(()));
+    }
+
+    loop {
+      let poll_result = self
+        .0
+        .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
+              .0
+              .module_map()
+              .borrow_mut()
+              .pending_dynamic_imports
+              .push(load.into_future());
+          }
+          Err(err) => {
+            let exception =
+              to_v8_type_error(&mut self.handle_scope(isolate), err);
+            self.dynamic_import_reject(isolate, 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(()));
+    }
+  }
+
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub(in crate::runtime) fn poll_dyn_imports(
+    &self,
+    isolate: &mut v8::Isolate,
+    cx: &mut Context,
+  ) -> Poll<Result<(), Error>> {
+    if self
+      .0
+      .module_map()
+      .borrow()
+      .pending_dynamic_imports
+      .is_empty()
+    {
+      return Poll::Ready(Ok(()));
+    }
+
+    loop {
+      let poll_result = self
+        .0
+        .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(isolate),
+                &request,
+                info,
+              );
+
+              match register_result {
+                Ok(()) => {
+                  // Keep importing until it's fully drained
+                  self
+                    .0
+                    .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(isolate), e)
+                    }
+                  };
+                  self.dynamic_import_reject(isolate, 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(isolate), err);
+              self.dynamic_import_reject(isolate, 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(isolate, module_id);
+          if let Err(exception) = result {
+            self.dynamic_import_reject(isolate, dyn_import_id, exception);
+          }
+          self.dynamic_import_module_evaluate(
+            isolate,
+            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.
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub(in crate::runtime) fn evaluate_pending_module(
+    &self,
+    isolate: &mut v8::Isolate,
+  ) {
+    let maybe_module_evaluation = self
+      .0
+      .context_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.0.state();
+    let scope = &mut self.handle_scope(isolate);
+
+    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.
+  // TODO(bartlomieju): remove, once migration to realm module handling is complete
+  #[allow(unused)]
+  pub(in crate::runtime) fn evaluate_dyn_imports(
+    &self,
+    isolate: &mut v8::Isolate,
+  ) -> bool {
+    let pending = std::mem::take(
+      &mut self.0.context_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(isolate);
+
+        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(isolate, dyn_import_id, module_id);
+          }
+          Err((dyn_import_id, exception)) => {
+            self.dynamic_import_reject(isolate, dyn_import_id, exception);
+          }
+        }
+      }
+    }
+    self.0.context_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 [`JsRealm::mod_evaluate`] with returned `ModuleId`
+  /// manually after load is finished.
+  pub async fn load_main_module(
+    &self,
+    isolate: &mut v8::Isolate,
+    specifier: &ModuleSpecifier,
+    code: Option<ModuleCode>,
+  ) -> Result<ModuleId, Error> {
+    let module_map_rc = self.0.module_map();
+    if let Some(code) = code {
+      let specifier = specifier.as_str().to_owned().into();
+      let scope = &mut self.handle_scope(isolate);
+      // 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(isolate);
+      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(isolate, root_id).map_err(|e| {
+      let scope = &mut self.handle_scope(isolate);
+      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 [`JsRealm::mod_evaluate`] with returned `ModuleId`
+  /// manually after load is finished.
+  pub async fn load_side_module(
+    &self,
+    isolate: &mut v8::Isolate,
+    specifier: &ModuleSpecifier,
+    code: Option<ModuleCode>,
+  ) -> Result<ModuleId, Error> {
+    let module_map_rc = self.0.module_map();
+    if let Some(code) = code {
+      let specifier = specifier.as_str().to_owned().into();
+      let scope = &mut self.handle_scope(isolate);
+      // 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(isolate);
+      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(isolate, root_id).map_err(|e| {
+      let scope = &mut self.handle_scope(isolate);
+      let exception = v8::Local::new(scope, e);
+      exception_to_err_result::<()>(scope, exception, false).unwrap_err()
+    })?;
+    Ok(root_id)
+  }
 }
 
 impl Drop for JsRealm {
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 737861b..6b62fe2 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -25,6 +25,8 @@ use crate::modules::ModuleLoadId;
 use crate::modules::ModuleLoader;
 use crate::modules::ModuleMap;
 use crate::ops::*;
+use crate::runtime::jsrealm::DynImportModEvaluate;
+use crate::runtime::jsrealm::ModEvaluate;
 use crate::runtime::ContextState;
 use crate::runtime::JsRealm;
 use crate::source_map::SourceMapCache;
@@ -60,6 +62,7 @@ use std::sync::Mutex;
 use std::sync::Once;
 use std::task::Context;
 use std::task::Poll;
+use v8::Isolate;
 
 const STATE_DATA_OFFSET: u32 = 0;
 
@@ -241,20 +244,6 @@ impl DerefMut for JsRuntimeForSnapshot {
   }
 }
 
-pub(crate) struct DynImportModEvaluate {
-  load_id: ModuleLoadId,
-  module_id: ModuleId,
-  promise: v8::Global<v8::Promise>,
-  module: v8::Global<v8::Module>,
-}
-
-pub(crate) struct ModEvaluate {
-  pub(crate) promise: Option<v8::Global<v8::Promise>>,
-  pub(crate) has_evaluated: bool,
-  pub(crate) handled_promise_rejections: Vec<v8::Global<v8::Promise>>,
-  sender: oneshot::Sender<Result<(), Error>>,
-}
-
 pub struct CrossIsolateStore<T>(Arc<Mutex<CrossIsolateStoreInner<T>>>);
 
 struct CrossIsolateStoreInner<T> {
@@ -307,7 +296,7 @@ pub struct JsRuntimeState {
   pub(crate) pending_mod_evaluate: Option<ModEvaluate>,
   /// 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) dyn_module_evaluate_idle_counter: u32,
   pub(crate) source_map_getter: Option<Rc<Box<dyn SourceMapGetter>>>,
   pub(crate) source_map_cache: Rc<RefCell<SourceMapCache>>,
   pub(crate) op_state: Rc<RefCell<OpState>>,
@@ -605,12 +594,13 @@ impl JsRuntime {
 
     let weak = Rc::downgrade(&state_rc);
     let context_state = Rc::new(RefCell::new(ContextState::default()));
-    let op_ctxs = ops
+    let mut op_ctxs = ops
       .into_iter()
       .enumerate()
       .map(|(id, decl)| {
         OpCtx::new(
           id as u16,
+          std::ptr::null_mut(),
           context_state.clone(),
           Rc::new(decl),
           op_state.clone(),
@@ -619,13 +609,9 @@ impl JsRuntime {
       })
       .collect::<Vec<_>>()
       .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,
-      &additional_references,
-    );
+    let refs = bindings::external_references(&op_ctxs, &additional_references);
     // V8 takes ownership of external_references.
     let refs: &'static v8::ExternalReferences = Box::leak(Box::new(refs));
 
@@ -653,6 +639,12 @@ impl JsRuntime {
       }
       v8::Isolate::new(params)
     };
+
+    for op_ctx in op_ctxs.iter_mut() {
+      op_ctx.isolate = isolate.as_mut() as *mut Isolate;
+    }
+    context_state.borrow_mut().op_ctxs = op_ctxs;
+
     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(
@@ -836,6 +828,7 @@ impl JsRuntime {
         .map(|op_ctx| {
           OpCtx::new(
             op_ctx.id,
+            op_ctx.isolate,
             context_state.clone(),
             op_ctx.decl.clone(),
             op_ctx.state.clone(),
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index e18512b..c47bd8c 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -393,6 +393,17 @@ pub fn to_string_ptr(string: &v8::fast_api::FastApiOneByteString) -> String {
   }
 }
 
+/// Converts a [`v8::Value`] to an owned string.
+#[inline(always)]
+pub fn to_string(scope: &mut v8::Isolate, string: &v8::Value) -> String {
+  if !string.is_string() {
+    return String::new();
+  }
+
+  let string: &v8::String = unsafe { std::mem::transmute(string) };
+  string.to_rust_string_lossy(scope)
+}
+
 /// Converts a [`v8::String`] to either an owned string, or a borrowed string, depending on whether it fits into the
 /// provided buffer.
 #[inline(always)]
@@ -534,6 +545,7 @@ mod tests {
       op_test_v8_type_handle_scope,
       op_test_v8_type_handle_scope_obj,
       op_test_v8_type_handle_scope_result,
+      op_test_v8_global,
       op_test_serde_v8,
       op_state_rc,
       op_state_ref,
@@ -1139,6 +1151,25 @@ mod tests {
     Ok(())
   }
 
+  #[op2(core)]
+  pub fn op_test_v8_global(
+    scope: &mut v8::HandleScope,
+    #[global] s: v8::Global<v8::String>,
+  ) -> u32 {
+    let s = s.open(scope);
+    s.length() as _
+  }
+
+  #[tokio::test]
+  pub async fn test_op_v8_global() -> Result<(), Box<dyn std::error::Error>> {
+    run_test2(
+      1,
+      "op_test_v8_global",
+      "assert(op_test_v8_global('hello world') == 11)",
+    )?;
+    Ok(())
+  }
+
   #[derive(Serialize, Deserialize)]
   pub struct Serde {
     pub s: String,
diff --git a/ops/op2/README.md b/ops/op2/README.md
index e45513a..ef752c7 100644
--- a/ops/op2/README.md
+++ b/ops/op2/README.md
@@ -382,6 +382,76 @@ v8::Local<v8::...>
 <tr>
 <td>
 
+```rust
+#[global] v8::Global<v8::Value>
+```
+
+</td><td>
+
+</td><td>
+any
+</td><td>
+⚠️ Slower than `v8::Local`.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[global] v8::Global<v8::String>
+```
+
+</td><td>
+
+</td><td>
+String
+</td><td>
+⚠️ Slower than `v8::Local`.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[global] v8::Global<v8::Object>
+```
+
+</td><td>
+
+</td><td>
+Object
+</td><td>
+⚠️ Slower than `v8::Local`.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[global] v8::Global<v8::Function>
+```
+
+</td><td>
+
+</td><td>
+Function
+</td><td>
+⚠️ Slower than `v8::Local`.
+</td></tr>
+<tr>
+<td>
+
+```rust
+#[global] v8::Global<v8::...>
+```
+
+</td><td>
+
+</td><td>
+...
+</td><td>
+⚠️ Slower than `v8::Local`.
+</td></tr>
+<tr>
+<td>
+
 ```rust
 #[serde] SerdeType
 ```
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index e2a502e..f3e86d6 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -417,7 +417,9 @@ fn map_arg_to_v8_fastcall_type(
     | Arg::State(..)
     | Arg::OptionState(..) => V8FastCallType::Virtual,
     // Other types + ref types are not handled
-    Arg::OptionNumeric(_) | Arg::SerdeV8(_) | Arg::Ref(..) => return Ok(None),
+    Arg::OptionNumeric(_) | Arg::Option(_) | Arg::SerdeV8(_) | Arg::Ref(..) => {
+      return Ok(None)
+    }
     // We don't support v8 global arguments
     Arg::V8Global(_) => return Ok(None),
     // We don't support v8 type arguments
diff --git a/ops/op2/dispatch_shared.rs b/ops/op2/dispatch_shared.rs
index 562dab2..0177a2f 100644
--- a/ops/op2/dispatch_shared.rs
+++ b/ops/op2/dispatch_shared.rs
@@ -21,7 +21,24 @@ pub fn v8_intermediate_to_arg(i: &Ident, arg: &Arg) -> TokenStream {
       quote!(match &#i { None => None, Some(v) => Some(::std::ops::DerefMut::deref_mut(v)) })
     }
     Arg::OptionV8Local(_) => quote!(#i),
-    _ => unreachable!("Not a v8 arg: {arg:?}"),
+    _ => unreachable!("Not a v8 local/ref arg: {arg:?}"),
+  };
+  quote!(let #i = #arg;)
+}
+
+/// Given an [`Arg`] containing a V8 value, converts this value to itself final form.
+pub fn v8_intermediate_to_global_arg(
+  deno_core: &TokenStream,
+  isolate: &Ident,
+  i: &Ident,
+  arg: &Arg,
+) -> TokenStream {
+  let arg = match arg {
+    Arg::V8Global(_) => quote!(#deno_core::v8::Global::new(&mut #isolate, #i)),
+    Arg::OptionV8Global(_) => {
+      quote!(#deno_core::v8::Global::new(&mut #isolate, #i))
+    }
+    _ => unreachable!("Not a v8 global arg: {arg:?}"),
   };
   quote!(let #i = #arg;)
 }
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index d5aaa03..65b67ab 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -1,5 +1,6 @@
 // Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
 use super::dispatch_shared::v8_intermediate_to_arg;
+use super::dispatch_shared::v8_intermediate_to_global_arg;
 use super::dispatch_shared::v8_to_arg;
 use super::generator_state::GeneratorState;
 use super::signature::Arg;
@@ -63,6 +64,14 @@ pub(crate) fn generate_dispatch_slow(
   output.extend(call(generator_state)?);
   output.extend(return_value(generator_state, &signature.ret_val)?);
 
+  // We only generate the isolate if we need it but don't need a scope. We call it `scope`.
+  let with_isolate =
+    if generator_state.needs_isolate && !generator_state.needs_scope {
+      with_isolate(generator_state)
+    } else {
+      quote!()
+    };
+
   let with_scope = if generator_state.needs_scope {
     with_scope(generator_state)
   } else {
@@ -100,6 +109,7 @@ pub(crate) fn generate_dispatch_slow(
         #with_retval
         #with_args
         #with_opctx
+        #with_isolate
         #with_opstate
 
         #output
@@ -108,6 +118,15 @@ pub(crate) fn generate_dispatch_slow(
   )
 }
 
+pub(crate) fn with_isolate(
+  generator_state: &mut GeneratorState,
+) -> TokenStream {
+  generator_state.needs_opctx = true;
+  gs_quote!(generator_state(opctx, scope) =>
+    (let mut #scope = unsafe { &mut *#opctx.isolate };)
+  )
+}
+
 pub(crate) fn with_scope(generator_state: &mut GeneratorState) -> TokenStream {
   gs_quote!(generator_state(deno_core, info, scope) =>
     (let mut #scope = unsafe { #deno_core::v8::CallbackScope::new(&*#info) };)
@@ -171,6 +190,7 @@ pub fn from_arg(
     scope,
     opstate,
     needs_scope,
+    needs_isolate,
     needs_opstate,
     ..
   } = &mut generator_state;
@@ -230,19 +250,26 @@ pub fn from_arg(
       }
     }
     Arg::Option(Special::String) => {
-      *needs_scope = true;
+      // Only requires isolate, not a full scope
+      *needs_isolate = true;
       quote! {
-        let #arg_ident = #arg_ident.to_rust_string_lossy(&mut #scope);
+        let #arg_ident = if #arg_ident.is_null_or_undefined() {
+          None
+        } else {
+          Some(#deno_core::_ops::to_string(&mut #scope, &#arg_ident))
+        };
       }
     }
     Arg::Special(Special::String) => {
-      *needs_scope = true;
+      // Only requires isolate, not a full scope
+      *needs_isolate = true;
       quote! {
-        let #arg_ident = #arg_ident.to_rust_string_lossy(&mut #scope);
+        let #arg_ident = #deno_core::_ops::to_string(&mut #scope, &#arg_ident);
       }
     }
     Arg::Special(Special::RefStr) => {
-      *needs_scope = true;
+      // Only requires isolate, not a full scope
+      *needs_isolate = true;
       quote! {
         // Trade stack space for potentially non-allocating strings
         let mut #arg_temp: [::std::mem::MaybeUninit<u8>; #deno_core::_ops::STRING_STACK_BUFFER_SIZE] = [::std::mem::MaybeUninit::uninit(); #deno_core::_ops::STRING_STACK_BUFFER_SIZE];
@@ -250,7 +277,8 @@ pub fn from_arg(
       }
     }
     Arg::Special(Special::CowStr) => {
-      *needs_scope = true;
+      // Only requires isolate, not a full scope
+      *needs_isolate = true;
       quote! {
         // Trade stack space for potentially non-allocating strings
         let mut #arg_temp: [::std::mem::MaybeUninit<u8>; #deno_core::_ops::STRING_STACK_BUFFER_SIZE] = [::std::mem::MaybeUninit::uninit(); #deno_core::_ops::STRING_STACK_BUFFER_SIZE];
@@ -329,6 +357,24 @@ pub fn from_arg(
         extract_intermediate,
       )?
     }
+    Arg::V8Global(v8) | Arg::OptionV8Global(v8) => {
+      // Only requires isolate, not a full scope
+      *needs_isolate = true;
+      let deno_core = deno_core.clone();
+      let scope = scope.clone();
+      let throw_type_error =
+        || throw_type_error(generator_state, format!("expected {v8:?}"));
+      let extract_intermediate =
+        v8_intermediate_to_global_arg(&deno_core, &scope, &arg_ident, arg);
+      v8_to_arg(
+        v8,
+        &arg_ident,
+        arg,
+        &deno_core,
+        throw_type_error,
+        extract_intermediate,
+      )?
+    }
     Arg::SerdeV8(_class) => {
       *needs_scope = true;
       let deno_core = deno_core.clone();
diff --git a/ops/op2/generator_state.rs b/ops/op2/generator_state.rs
index 00af91b..796c633 100644
--- a/ops/op2/generator_state.rs
+++ b/ops/op2/generator_state.rs
@@ -37,6 +37,7 @@ pub struct GeneratorState {
   pub needs_args: bool,
   pub needs_retval: bool,
   pub needs_scope: bool,
+  pub needs_isolate: bool,
   pub needs_opstate: bool,
   pub needs_opctx: bool,
   pub needs_fast_opctx: bool,
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index 23f856a..5bbc2df 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -47,10 +47,12 @@ pub enum Op2Error {
   V8MappingError(#[from] V8MappingError),
   #[error("Failed to parse signature")]
   SignatureError(#[from] SignatureError),
+  #[error("This op cannot use both ({0}) and ({1})")]
+  InvalidAttributeCombination(&'static str, &'static str),
   #[error("This op is fast-compatible and should be marked as (fast)")]
   ShouldBeFast,
-  #[error("This op is not fast-compatible and should not be marked as (fast)")]
-  ShouldNotBeFast,
+  #[error("This op is not fast-compatible and should not be marked as ({0})")]
+  ShouldNotBeFast(&'static str),
   #[error("This op is async and should be marked as (async)")]
   ShouldBeAsync,
   #[error("This op is not async and should not be marked as (async)")]
@@ -67,8 +69,13 @@ pub enum V8MappingError {
 
 #[derive(Default)]
 pub(crate) struct MacroConfig {
+  /// Use `(core)` for ops that live in `deno_core`.
   pub core: bool,
+  /// Generate a fastcall method (must be fastcall compatible).
   pub fast: bool,
+  /// Do not generate a fastcall method (must be fastcall compatible).
+  pub nofast: bool,
+  /// Marks an async function (either `async fn` or `fn -> impl Future`)
   pub r#async: bool,
 }
 
@@ -95,6 +102,8 @@ impl MacroConfig {
         config.core = true;
       } else if flag == "fast" {
         config.fast = true;
+      } else if flag == "nofast" {
+        config.nofast = true;
       } else if flag == "async" {
         config.r#async = true;
       } else {
@@ -102,6 +111,17 @@ impl MacroConfig {
       }
     }
 
+    // Test for invalid attribute combinations
+    if config.fast && config.nofast {
+      return Err(Op2Error::InvalidAttributeCombination("fast", "nofast"));
+    }
+    if config.fast && config.r#async {
+      return Err(Op2Error::InvalidAttributeCombination("fast", "async"));
+    }
+    if config.nofast && config.r#async {
+      return Err(Op2Error::InvalidAttributeCombination("nofast", "async"));
+    }
+
     Ok(config)
   }
 
@@ -216,6 +236,7 @@ fn generate_op2(
     promise_id,
     needs_retval: false,
     needs_scope: false,
+    needs_isolate: false,
     needs_opctx: false,
     needs_opstate: false,
     needs_fast_opctx: false,
@@ -240,14 +261,22 @@ fn generate_op2(
   let (fast_definition, fast_fn) =
     match generate_dispatch_fast(&mut generator_state, &signature)? {
       Some((fast_definition, fast_fn)) => {
-        if !config.fast {
+        if !config.fast && !config.nofast {
           return Err(Op2Error::ShouldBeFast);
         }
-        (quote!(Some({#fast_definition})), fast_fn)
+        // nofast requires the function to be valid for fast
+        if config.nofast {
+          (quote!(None), quote!())
+        } else {
+          (quote!(Some({#fast_definition})), fast_fn)
+        }
       }
       None => {
         if config.fast {
-          return Err(Op2Error::ShouldNotBeFast);
+          return Err(Op2Error::ShouldNotBeFast("fast"));
+        }
+        if config.nofast {
+          return Err(Op2Error::ShouldNotBeFast("nofast"));
         }
         (quote!(None), quote!())
       }
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index 476edb5..45f28cf 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -214,6 +214,7 @@ pub enum Arg {
   Option(Special),
   OptionNumeric(NumericArg),
   OptionV8Local(V8Arg),
+  OptionV8Global(V8Arg),
   V8Local(V8Arg),
   V8Global(V8Arg),
   OptionV8Ref(RefType, V8Arg),
@@ -258,6 +259,7 @@ impl Arg {
       Arg::V8Ref(RefType::Ref, v8) => quote!(&#deno_core::v8::#v8),
       Arg::V8Ref(RefType::Mut, v8) => quote!(&mut #deno_core::v8::#v8),
       Arg::V8Local(v8) => quote!(#deno_core::v8::Local<#deno_core::v8::#v8>),
+      Arg::V8Global(v8) => quote!(#deno_core::v8::Global<#deno_core::v8::#v8>),
       Arg::OptionV8Ref(RefType::Ref, v8) => {
         quote!(::std::option::Option<&#deno_core::v8::#v8>)
       }
@@ -267,6 +269,9 @@ impl Arg {
       Arg::OptionV8Local(v8) => {
         quote!(::std::option::Option<#deno_core::v8::Local<#deno_core::v8::#v8>>)
       }
+      Arg::OptionV8Global(v8) => {
+        quote!(::std::option::Option<#deno_core::v8::Global<#deno_core::v8::#v8>>)
+      }
       _ => todo!(),
     }
   }
@@ -277,6 +282,7 @@ impl Arg {
       self,
       Arg::OptionV8Ref(..)
         | Arg::OptionV8Local(..)
+        | Arg::OptionV8Global(..)
         | Arg::OptionNumeric(..)
         | Arg::Option(..)
         | Arg::OptionState(..)
@@ -298,6 +304,7 @@ pub enum ParsedTypeContainer {
   COption(ParsedType),
   CRcRefCell(ParsedType),
   COptionV8Local(ParsedType),
+  COptionV8Global(ParsedType),
   CV8Local(ParsedType),
   CV8Global(ParsedType),
 }
@@ -372,6 +379,8 @@ pub enum AttributeModifier {
   State,
   /// #[buffer], for buffers.
   Buffer(BufferMode),
+  /// #[global], for [`v8::Global`]s
+  Global,
 }
 
 #[derive(Error, Debug)]
@@ -432,6 +441,8 @@ pub enum ArgError {
   InvalidBufferMode(String, String),
   #[error("Cannot use #[serde] for type: {0}")]
   InvalidSerdeAttributeType(String),
+  #[error("Invalid #[global] type: {0}")]
+  InvalidGlobalType(String),
   #[error("Invalid v8 type: {0}")]
   InvalidV8Type(String),
   #[error("Internal error: {0}")]
@@ -440,6 +451,8 @@ pub enum ArgError {
   MissingStringAttribute,
   #[error("Missing a #[buffer] attribute")]
   MissingBufferAttribute,
+  #[error("Missing a #[global] attribute")]
+  MissingGlobalAttribute,
   #[error("Invalid #[state] type '{0}'")]
   InvalidStateType(String),
   #[error("Argument attribute error")]
@@ -673,6 +686,7 @@ fn parse_attribute(
       (#[buffer(unsafe)]) => Some(AttributeModifier::Buffer(BufferMode::Unsafe)),
       (#[buffer(copy)]) => Some(AttributeModifier::Buffer(BufferMode::Copy)),
       (#[buffer(detach)]) => Some(AttributeModifier::Buffer(BufferMode::Detach)),
+      (#[global]) => Some(AttributeModifier::Global),
       (#[allow ($_rule:path)]) => None,
       (#[doc = $_attr:literal]) => None,
     })
@@ -752,6 +766,7 @@ fn parse_type_path(
           Arg::V8Ref(RefType::Ref, v8) => Ok(COption(TV8(v8))),
           Arg::V8Ref(RefType::Mut, v8) => Ok(COption(TV8Mut(v8))),
           Arg::V8Local(v8) => Ok(COptionV8Local(TV8(v8))),
+          Arg::V8Global(v8) => Ok(COptionV8Global(TV8(v8))),
           _ => Err(ArgError::InvalidType(stringify_token(ty)))
         }
       }
@@ -778,6 +793,11 @@ fn parse_type_path(
   }
 
   match res {
+    CV8Global(_) => {
+      if attrs.primary != Some(AttributeModifier::Global) {
+        return Err(ArgError::MissingGlobalAttribute);
+      }
+    }
     CBare(TSpecial(Special::RefStr | Special::CowStr | Special::String)) => {
       if attrs.primary != Some(AttributeModifier::String) {
         return Err(ArgError::MissingStringAttribute);
@@ -802,9 +822,15 @@ fn parse_type_path(
       if let Some(AttributeModifier::Buffer(_)) = attrs.primary {
         return Err(ArgError::InvalidBufferType(stringify_token(tp)));
       }
+      if attrs.primary == Some(AttributeModifier::Global) {
+        return Err(ArgError::InvalidGlobalType(stringify_token(tp)));
+      }
     }
     _ => {
       // Ignore for other containers
+      if attrs.primary == Some(AttributeModifier::Global) {
+        return Err(ArgError::InvalidGlobalType(stringify_token(tp)));
+      }
     }
   }
 
@@ -902,10 +928,9 @@ pub(crate) fn parse_type(
       AttributeModifier::State => {
         return parse_type_state(ty);
       }
-      AttributeModifier::String => {
-        // We handle this as part of the normal parsing process
-      }
-      AttributeModifier::Buffer(_) => {
+      AttributeModifier::String
+      | AttributeModifier::Buffer(_)
+      | AttributeModifier::Global => {
         // We handle this as part of the normal parsing process
       }
       AttributeModifier::Smi => {
@@ -1005,6 +1030,7 @@ pub(crate) fn parse_type(
       COption(TSpecial(special)) => Ok(Arg::Option(special)),
       CRcRefCell(TSpecial(special)) => Ok(Arg::RcRefCell(special)),
       COptionV8Local(TV8(v8)) => Ok(Arg::OptionV8Local(v8)),
+      COptionV8Global(TV8(v8)) => Ok(Arg::OptionV8Global(v8)),
       COption(TV8(v8)) => Ok(Arg::OptionV8Ref(RefType::Ref, v8)),
       COption(TV8Mut(v8)) => Ok(Arg::OptionV8Ref(RefType::Mut, v8)),
       CV8Local(TV8(v8)) => Ok(Arg::V8Local(v8)),
@@ -1175,7 +1201,7 @@ mod tests {
     <'s, AB: some::Trait, BC: OtherTrait> (Special(RefStr)) -> Infallible(Void)
   );
   test!(
-    fn op_v8_types(s: &mut v8::String, sopt: Option<&mut v8::String>, s2: v8::Local<v8::String>, s3: v8::Global<v8::String>);
+    fn op_v8_types(s: &mut v8::String, sopt: Option<&mut v8::String>, s2: v8::Local<v8::String>, #[global] s3: v8::Global<v8::String>);
     (V8Ref(Mut, String), OptionV8Ref(Mut, String), V8Local(String), V8Global(String)) -> Infallible(Void)
   );
   test!(
@@ -1252,6 +1278,16 @@ mod tests {
     ArgError("a", AttributeError(InvalidAttribute("#[badattr]"))),
     fn f(#[badattr] a: u32) {}
   );
+  expect_fail!(
+    op_with_missing_global,
+    ArgError("g", MissingGlobalAttribute),
+    fn f(g: v8::Global<v8::String>) {}
+  );
+  expect_fail!(
+    op_with_invalid_global,
+    ArgError("l", InvalidGlobalType("v8::Local<v8::String>")),
+    fn f(#[global] l: v8::Local<v8::String>) {}
+  );
 
   // Generics
 
diff --git a/ops/op2/valid_args.md b/ops/op2/valid_args.md
index 662edd5..fd1aaf7 100644
--- a/ops/op2/valid_args.md
+++ b/ops/op2/valid_args.md
@@ -1,39 +1,41 @@
-| Supported | Rust                           | Fastcall | V8                                                  | Notes                                                                                                                                                                                                          |
-| --------- | ------------------------------ | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
-| X         | bool                           | X        | Bool                                                |                                                                                                                                                                                                                |
-| X         | i8                             | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | u8                             | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | i16                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | u16                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | i32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | u32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | isize                          | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | usize                          | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | f32                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | f64                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
-| X         | #[string] String               | X        | String                                              | Fastcall available only if string is Latin-1. Will always create an allocated, UTF-8 copy of the String data.                                                                                                  |
-| X         | #[string] &str                 | X        | String                                              | Fastcall available only if string is Latin-1. Will create an owned `String` copy of the String data if it doesn't fit on the stack. Will never allocate in a fastcall, but will copy Latin-1 -> UTF-8.         |
-| X         | #[string] Cow<str>             | X        | String                                              | Fastcall available only if string is Latin-1. Will create a `Cow::Owned` copy of the String data if it doesn't fit on the stack. Will always be `Cow::Borrowed` in a fastcall, but will copy Latin-1 -> UTF-8. |
-| X         | &v8::Value                     | X        | any                                                 |                                                                                                                                                                                                                |
-| X         | &v8::**V8**                    | X        | **V8**                                              |                                                                                                                                                                                                                |
-| X         | v8::Local<v8::Value>           | X        | any                                                 |                                                                                                                                                                                                                |
-| X         | v8::Local<v8::**V8**>          | X        | **V8**                                              |                                                                                                                                                                                                                |
-| X         | #[serde] SerdeType             |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
-| X         | #[serde] (Tuple, Tuple)        |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
-| X         | #[buffer] &mut [u8]            | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
-| X         | #[buffer] &[u8]                | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
-| X         | #[buffer(copy)] Vec<u8>        | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
-| X         | #[buffer(copy)] Box<[u8]>      | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
-| X         | #[buffer(copy)] bytes::Bytes   | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
-|           | #[buffer] V8Slice              | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
-|           | #[buffer(detach)] V8Slice      | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
-|           | #[buffer] V8ResizableSlice     | X        | ArrayBuffer, ArrayBufferView (resizable=true)       | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
-|           | #[buffer] JSBuffer             | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
-|           | #[buffer(detach)] JSBuffer     | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
-|           | #[buffer(unsafe)] bytes::Bytes | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of the buffer.                                                                                                                                                                    |
-|           | #[buffer(detach)] bytes::Bytes | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
-| X         | &OpState                       | X        |                                                     |                                                                                                                                                                                                                |
-| X         | &mut OpState                   | X        |                                                     |                                                                                                                                                                                                                |
-| X         | Rc<RefCell<OpState>>           | X        |                                                     |                                                                                                                                                                                                                |
-| X         | #[state] &StateObject          | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |
-| X         | #[state] &mut StateObject      | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |
+| Supported | Rust                             | Fastcall | V8                                                  | Notes                                                                                                                                                                                                          |
+| --------- | -------------------------------- | -------- | --------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
+| X         | bool                             | X        | Bool                                                |                                                                                                                                                                                                                |
+| X         | i8                               | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u8                               | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | i16                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u16                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | i32                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | u32                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | isize                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | usize                            | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | f32                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | f64                              | X        | Uint32, Int32, Number, BigInt                       |                                                                                                                                                                                                                |
+| X         | #[string] String                 | X        | String                                              | Fastcall available only if string is Latin-1. Will always create an allocated, UTF-8 copy of the String data.                                                                                                  |
+| X         | #[string] &str                   | X        | String                                              | Fastcall available only if string is Latin-1. Will create an owned `String` copy of the String data if it doesn't fit on the stack. Will never allocate in a fastcall, but will copy Latin-1 -> UTF-8.         |
+| X         | #[string] Cow<str>               | X        | String                                              | Fastcall available only if string is Latin-1. Will create a `Cow::Owned` copy of the String data if it doesn't fit on the stack. Will always be `Cow::Borrowed` in a fastcall, but will copy Latin-1 -> UTF-8. |
+| X         | &v8::Value                       | X        | any                                                 |                                                                                                                                                                                                                |
+| X         | &v8::**V8**                      | X        | **V8**                                              |                                                                                                                                                                                                                |
+| X         | v8::Local<v8::Value>             | X        | any                                                 |                                                                                                                                                                                                                |
+| X         | v8::Local<v8::**V8**>            | X        | **V8**                                              |                                                                                                                                                                                                                |
+| X         | #[global] v8::Global<v8::Value>  |          | any                                                 | ⚠️ Slower than `v8::Local`.                                                                                                                                                                                     |
+| X         | #[global] v8::Global<v8::**V8**> |          | **V8**                                              | ⚠️ Slower than `v8::Local`.                                                                                                                                                                                     |
+| X         | #[serde] SerdeType               |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
+| X         | #[serde] (Tuple, Tuple)          |          | any                                                 | ⚠️ May be slow.                                                                                                                                                                                                 |
+| X         | #[buffer] &mut [u8]              | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
+| X         | #[buffer] &[u8]                  | X        | UInt8Array (resizable=true,false)                   | ⚠️ JS may modify the contents of the slice if V8 is called re-entrantly.                                                                                                                                        |
+| X         | #[buffer(copy)] Vec<u8>          | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
+| X         | #[buffer(copy)] Box<[u8]>        | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
+| X         | #[buffer(copy)] bytes::Bytes     | X        | UInt8Array (resizable=true,false)                   | Safe, but forces a copy.                                                                                                                                                                                       |
+|           | #[buffer] V8Slice                | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer(detach)] V8Slice        | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+|           | #[buffer] V8ResizableSlice       | X        | ArrayBuffer, ArrayBufferView (resizable=true)       | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer] JSBuffer               | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of slices obtained from buffer.                                                                                                                                                   |
+|           | #[buffer(detach)] JSBuffer       | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+|           | #[buffer(unsafe)] bytes::Bytes   | X        | ArrayBuffer, ArrayBufferView (resizable=false)      | ⚠️ JS may modify the contents of the buffer.                                                                                                                                                                    |
+|           | #[buffer(detach)] bytes::Bytes   | X        | ArrayBuffer, ArrayBufferView (resizable=true,false) | Safe.                                                                                                                                                                                                          |
+| X         | &OpState                         | X        |                                                     |                                                                                                                                                                                                                |
+| X         | &mut OpState                     | X        |                                                     |                                                                                                                                                                                                                |
+| X         | Rc<RefCell<OpState>>             | X        |                                                     |                                                                                                                                                                                                                |
+| X         | #[state] &StateObject            | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |
+| X         | #[state] &mut StateObject        | X        |                                                     | Extracts an object from `OpState`.                                                                                                                                                                             |


Test patch:
diff --git a/ops/op2/test_cases/compiler_pass/sync.rs b/ops/op2/test_cases/compiler_pass/sync.rs
index 0a4ead3..6050be7 100644
--- a/ops/op2/test_cases/compiler_pass/sync.rs
+++ b/ops/op2/test_cases/compiler_pass/sync.rs
@@ -2,6 +2,8 @@
 #![deny(warnings)]
 deno_ops_compile_test_runner::prelude!();
 
+use deno_core::v8;
+
 // Collect a few examples that we'll smoke test when not running on the CI.
 
 #[op2(fast)]
@@ -17,3 +19,12 @@ struct Something {}
 
 #[op2(fast)]
 fn op_state_rc(#[state] _arg: &Something, #[state] _arg_opt: Option<&Something>) {}
+
+#[op2(fast)]
+fn op_v8_1(_s: v8::Local<v8::String>) {}
+
+#[op2(fast)]
+fn op_v8_2(_s: &v8::String) {}
+
+#[op2]
+fn op_v8_3(#[global] _s: v8::Global<v8::String>) {}
diff --git a/ops/op2/test_cases/sync/nofast.out b/ops/op2/test_cases/sync/nofast.out
new file mode 100644
index 0000000..2fe391e
--- /dev/null
+++ b/ops/op2/test_cases/sync/nofast.out
@@ -0,0 +1,43 @@
+#[allow(non_camel_case_types)]
+struct op_nofast {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_nofast {
+    const NAME: &'static str = stringify!(op_nofast);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_nofast),
+        false,
+        false,
+        false,
+        2usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_nofast {
+    pub const fn name() -> &'static str {
+        stringify!(op_nofast)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let arg0 = args.get(0usize as i32);
+        let arg0 = deno_core::_ops::to_u32(&arg0) as _;
+        let arg1 = args.get(1usize as i32);
+        let arg1 = deno_core::_ops::to_u32(&arg1) as _;
+        let result = Self::call(arg0, arg1);
+        rv.set_uint32(result as u32);
+    }
+    #[inline(always)]
+    fn call(a: u32, b: u32) -> u32 {
+        a + b
+    }
+}
diff --git a/ops/op2/test_cases/sync/nofast.rs b/ops/op2/test_cases/sync/nofast.rs
new file mode 100644
index 0000000..824bd6d
--- /dev/null
+++ b/ops/op2/test_cases/sync/nofast.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+#[op2(nofast)]
+fn op_nofast(a: u32, b: u32) -> u32 {
+  a + b
+}
diff --git a/ops/op2/test_cases/sync/string_cow.out b/ops/op2/test_cases/sync/string_cow.out
index 58e4c9a..332ce6f 100644
--- a/ops/op2/test_cases/sync/string_cow.out
+++ b/ops/op2/test_cases/sync/string_cow.out
@@ -42,13 +42,17 @@ impl op_string_cow {
         result
     }
     extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
-        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
         let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
             &*info
         });
         let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
             &*info
         });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let mut scope = unsafe { &mut *opctx.isolate };
         let arg0 = args.get(0usize as i32);
         let mut arg0_temp: [::std::mem::MaybeUninit<
             u8,
diff --git a/ops/op2/test_cases/sync/string_option_return.out b/ops/op2/test_cases/sync/string_option_return.out
index e36a800..5be2519 100644
--- a/ops/op2/test_cases/sync/string_option_return.out
+++ b/ops/op2/test_cases/sync/string_option_return.out
@@ -9,7 +9,7 @@ impl deno_core::_ops::Op for op_string_return {
         false,
         false,
         false,
-        0usize as u8,
+        1usize as u8,
         Self::v8_fn_ptr as _,
         None,
     );
@@ -27,7 +27,16 @@ impl op_string_return {
         let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
             &*info
         });
-        let result = Self::call();
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let arg0 = args.get(0usize as i32);
+        let arg0 = if arg0.is_null_or_undefined() {
+            None
+        } else {
+            Some(deno_core::_ops::to_string(&mut scope, &arg0))
+        };
+        let result = Self::call(arg0);
         if let Some(result) = result {
             if result.is_empty() {
                 rv.set_empty_string();
@@ -40,7 +49,7 @@ impl op_string_return {
         }
     }
     #[inline(always)]
-    pub fn call() -> Option<String> {
-        Some("".to_owned())
+    pub fn call(s: Option<String>) -> Option<String> {
+        s
     }
 }
diff --git a/ops/op2/test_cases/sync/string_option_return.rs b/ops/op2/test_cases/sync/string_option_return.rs
index a141cb8..76d63d5 100644
--- a/ops/op2/test_cases/sync/string_option_return.rs
+++ b/ops/op2/test_cases/sync/string_option_return.rs
@@ -4,6 +4,6 @@ deno_ops_compile_test_runner::prelude!();
 
 #[op2]
 #[string]
-pub fn op_string_return() -> Option<String> {
-    Some("".to_owned())
+pub fn op_string_return(#[string] s: Option<String>) -> Option<String> {
+    s
 }
diff --git a/ops/op2/test_cases/sync/string_owned.out b/ops/op2/test_cases/sync/string_owned.out
index b209033..063d7d6 100644
--- a/ops/op2/test_cases/sync/string_owned.out
+++ b/ops/op2/test_cases/sync/string_owned.out
@@ -39,15 +39,19 @@ impl op_string_owned {
         result
     }
     extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
-        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
         let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
             &*info
         });
         let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
             &*info
         });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let mut scope = unsafe { &mut *opctx.isolate };
         let arg0 = args.get(0usize as i32);
-        let arg0 = arg0.to_rust_string_lossy(&mut scope);
+        let arg0 = deno_core::_ops::to_string(&mut scope, &arg0);
         let result = Self::call(arg0);
         rv.set_uint32(result as u32);
     }
diff --git a/ops/op2/test_cases/sync/string_ref.out b/ops/op2/test_cases/sync/string_ref.out
index 35dfb79..99c1a0a 100644
--- a/ops/op2/test_cases/sync/string_ref.out
+++ b/ops/op2/test_cases/sync/string_ref.out
@@ -42,13 +42,17 @@ impl op_string_owned {
         result
     }
     extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
-        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
         let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
             &*info
         });
         let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
             &*info
         });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let mut scope = unsafe { &mut *opctx.isolate };
         let arg0 = args.get(0usize as i32);
         let mut arg0_temp: [::std::mem::MaybeUninit<
             u8,
diff --git a/ops/op2/test_cases/sync/v8_global.out b/ops/op2/test_cases/sync/v8_global.out
new file mode 100644
index 0000000..7f24a2e
--- /dev/null
+++ b/ops/op2/test_cases/sync/v8_global.out
@@ -0,0 +1,58 @@
+#[allow(non_camel_case_types)]
+pub struct op_global {
+    _unconstructable: ::std::marker::PhantomData<()>,
+}
+impl deno_core::_ops::Op for op_global {
+    const NAME: &'static str = stringify!(op_global);
+    const DECL: deno_core::_ops::OpDecl = deno_core::_ops::OpDecl::new_internal(
+        stringify!(op_global),
+        false,
+        false,
+        false,
+        1usize as u8,
+        Self::v8_fn_ptr as _,
+        None,
+    );
+}
+impl op_global {
+    pub const fn name() -> &'static str {
+        stringify!(op_global)
+    }
+    #[deprecated(note = "Use the const op::DECL instead")]
+    pub const fn decl() -> deno_core::_ops::OpDecl {
+        <Self as deno_core::_ops::Op>::DECL
+    }
+    extern "C" fn v8_fn_ptr(info: *const deno_core::v8::FunctionCallbackInfo) {
+        let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(unsafe {
+            &*info
+        });
+        let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(unsafe {
+            &*info
+        });
+        let opctx = unsafe {
+            &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data()).value()
+                as *const deno_core::_ops::OpCtx)
+        };
+        let mut scope = unsafe { &mut *opctx.isolate };
+        let arg0 = args.get(0usize as i32);
+        let Ok(mut arg0) = deno_core::_ops::v8_try_convert::<
+            deno_core::v8::String,
+        >(arg0) else {
+        let mut scope = unsafe { deno_core::v8::CallbackScope::new(&*info) };
+        let msg = deno_core::v8::String::new_from_one_byte(
+                &mut scope,
+                "expected String".as_bytes(),
+                deno_core::v8::NewStringType::Normal,
+            )
+            .unwrap();
+        let exc = deno_core::v8::Exception::error(&mut scope, msg);
+        scope.throw_exception(exc);
+        return;
+    };
+        let arg0 = deno_core::v8::Global::new(&mut scope, arg0);
+        let result = Self::call(arg0);
+        rv.set_null();
+    }
+    #[inline(always)]
+    pub fn call(_g: v8::Global<v8::String>) {}
+}
diff --git a/ops/op2/test_cases/sync/v8_global.rs b/ops/op2/test_cases/sync/v8_global.rs
new file mode 100644
index 0000000..89cf2df
--- /dev/null
+++ b/ops/op2/test_cases/sync/v8_global.rs
@@ -0,0 +1,8 @@
+// Copyright 2018-2023 the Deno authors. All rights reserved. MIT license.
+#![deny(warnings)]
+deno_ops_compile_test_runner::prelude!();
+
+use deno_core::v8;
+
+#[op2]
+pub fn op_global(#[global] _g: v8::Global<v8::String>) {}