Repository: denoland/deno_core
Pull request #58 (2023-07-19 22:07:52)

feat(ops): op2 support for v8::Global<...> 
This adds support for v8 globals, along with a benchmark. `v8::Global`s are constructed from a `*mut Isolate` that is stashed in `OpCtx`, making this faster than when using a scope.

```
test bench_op_v8_global                     ... bench:      37,070 ns/iter (+/- 7,077)
test bench_op_v8_global_scope               ... bench:      44,636 ns/iter (+/- 1,668)
test bench_op_v8_local                      ... bench:       3,289 ns/iter (+/- 75)
test bench_op_v8_local_nofast               ... bench:       6,209 ns/iter (+/- 196)
test bench_op_v8_local_scope                ... bench:      12,640 ns/iter (+/- 440)
```

In addition, string methods that can use the isolate to skip scope creation have been updated.


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/modules/mod.rs b/core/modules/mod.rs
index 83f10bc..dd7323d 100644
--- a/core/modules/mod.rs
+++ b/core/modules/mod.rs
@@ -242,7 +242,7 @@ pub(crate) type PrepareLoadFuture =
 pub type ModuleSourceFuture = dyn Future<Output = Result<ModuleSource, Error>>;
 
 type ModuleLoadFuture =
-  dyn Future<Output = Result<(ModuleRequest, ModuleSource), Error>>;
+  dyn Future<Output = Result<Option<(ModuleRequest, ModuleSource)>, Error>>;
 
 #[derive(Debug, PartialEq, Eq)]
 pub enum ResolutionKind {
@@ -290,6 +290,7 @@ pub(crate) struct RecursiveModuleLoad {
   module_map_rc: Rc<RefCell<ModuleMap>>,
   pending: FuturesUnordered<Pin<Box<ModuleLoadFuture>>>,
   visited: HashSet<ModuleRequest>,
+  visited_as_alias: Rc<RefCell<HashSet<String>>>,
   // The loader is copied from `module_map_rc`, but its reference is cloned
   // ahead of time to avoid already-borrowed errors.
   loader: Rc<dyn ModuleLoader>,
@@ -350,6 +351,7 @@ impl RecursiveModuleLoad {
       loader,
       pending: FuturesUnordered::new(),
       visited: HashSet::new(),
+      visited_as_alias: Default::default(),
     };
     // FIXME(bartlomieju): this seems fishy
     // Ignore the error here, let it be hit in `Stream::poll_next()`.
@@ -513,7 +515,12 @@ impl RecursiveModuleLoad {
         .unwrap()
         .clone();
       for module_request in imports {
-        if !self.visited.contains(&module_request) {
+        if !self.visited.contains(&module_request)
+          && !self
+            .visited_as_alias
+            .borrow()
+            .contains(&module_request.specifier)
+        {
           if let Some(module_id) = self.module_map_rc.borrow().get_id(
             module_request.specifier.as_str(),
             module_request.asserted_module_type,
@@ -523,14 +530,29 @@ impl RecursiveModuleLoad {
             let request = module_request.clone();
             let specifier =
               ModuleSpecifier::parse(&module_request.specifier).unwrap();
+            let visited_as_alias = self.visited_as_alias.clone();
             let referrer = referrer.clone();
             let loader = self.loader.clone();
             let is_dynamic_import = self.is_dynamic_import();
             let fut = async move {
+              // `visited_as_alias` unlike `visited` is checked as late as
+              // possible because it can only be populated after completed
+              // loads, meaning a duplicate load future may have already been
+              // dispatched before we know it's a duplicate.
+              if visited_as_alias.borrow().contains(specifier.as_str()) {
+                return Ok(None);
+              }
               let load_result = loader
                 .load(&specifier, Some(&referrer), is_dynamic_import)
                 .await;
-              load_result.map(|s| (request, s))
+              if let Ok(source) = &load_result {
+                if let Some(found_specifier) = &source.module_url_found {
+                  visited_as_alias
+                    .borrow_mut()
+                    .insert(found_specifier.as_str().to_string());
+                }
+              }
+              load_result.map(|s| Some((request, s)))
             };
             self.pending.push(fut.boxed_local());
           }
@@ -590,7 +612,7 @@ impl Stream for RecursiveModuleLoad {
             Default::default(),
             &module_specifier,
           );
-          futures::future::ok((module_request, module_source)).boxed()
+          futures::future::ok(Some((module_request, module_source))).boxed()
         } else {
           let maybe_referrer = match inner.init {
             LoadInit::DynamicImport(_, ref referrer, _) => {
@@ -616,7 +638,7 @@ impl Stream for RecursiveModuleLoad {
                 is_dynamic_import,
               )
               .await;
-            result.map(|s| (module_request, s))
+            result.map(|s| Some((module_request, s)))
           }
           .boxed_local()
         };
@@ -627,7 +649,15 @@ impl Stream for RecursiveModuleLoad {
       LoadState::LoadingRoot | LoadState::LoadingImports => {
         match inner.pending.try_poll_next_unpin(cx)? {
           Poll::Ready(None) => unreachable!(),
-          Poll::Ready(Some(info)) => Poll::Ready(Some(Ok(info))),
+          Poll::Ready(Some(None)) => {
+            if inner.pending.is_empty() {
+              inner.state = LoadState::Done;
+              Poll::Ready(None)
+            } else {
+              Poll::Pending
+            }
+          }
+          Poll::Ready(Some(Some(info))) => Poll::Ready(Some(Ok(info))),
           Poll::Pending => Poll::Pending,
         }
       }
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/jsruntime.rs b/core/runtime/jsruntime.rs
index 737861b..1b01d08 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -60,6 +60,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;
 
@@ -605,12 +606,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 +621,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 +651,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 +840,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/core/modules/tests.rs b/core/modules/tests.rs
index 0eb7ce5..d9aad41 100644
--- a/core/modules/tests.rs
+++ b/core/modules/tests.rs
@@ -1250,3 +1250,109 @@ fn import_meta_snapshot() {
     )
     .unwrap();
 }
+
+#[tokio::test]
+async fn no_duplicate_loads() {
+  // Both of these imports will cause redirects - ie. their "found" specifier
+  // will be different than requested specifier.
+  const MAIN_SRC: &str = r#"
+  import "https://example.com/foo.js";
+  import "https://example.com/bar.js";
+  "#;
+
+  // foo.js is importing bar.js that is already redirected
+  const FOO_SRC: &str = r#"
+  import "https://example.com/v1/bar.js";
+  "#;
+
+  struct ConsumingLoader {
+    pub files: Rc<RefCell<HashMap<String, Option<String>>>>,
+  }
+
+  impl Default for ConsumingLoader {
+    fn default() -> Self {
+      let files = HashMap::from_iter([
+        (
+          "https://example.com/v1/foo.js".to_string(),
+          Some(FOO_SRC.to_string()),
+        ),
+        (
+          "https://example.com/v1/bar.js".to_string(),
+          Some("".to_string()),
+        ),
+        ("file:///main.js".to_string(), Some(MAIN_SRC.to_string())),
+      ]);
+      Self {
+        files: Rc::new(RefCell::new(files)),
+      }
+    }
+  }
+
+  impl ModuleLoader for ConsumingLoader {
+    fn resolve(
+      &self,
+      specifier: &str,
+      referrer: &str,
+      _kind: ResolutionKind,
+    ) -> Result<ModuleSpecifier, Error> {
+      let referrer = if referrer == "." {
+        "file:///"
+      } else {
+        referrer
+      };
+
+      Ok(resolve_import(specifier, referrer)?)
+    }
+
+    fn load(
+      &self,
+      module_specifier: &ModuleSpecifier,
+      _maybe_referrer: Option<&ModuleSpecifier>,
+      _is_dyn_import: bool,
+    ) -> Pin<Box<ModuleSourceFuture>> {
+      let found_specifier =
+        if module_specifier.as_str() == "https://example.com/foo.js" {
+          Some("https://example.com/v1/foo.js".to_string())
+        } else if module_specifier.as_str() == "https://example.com/bar.js" {
+          Some("https://example.com/v1/bar.js".to_string())
+        } else {
+          None
+        };
+
+      let mut files = self.files.borrow_mut();
+      eprintln!(
+        "getting specifier {} {:?}",
+        module_specifier.as_str(),
+        found_specifier
+      );
+      let source_code = if let Some(found) = &found_specifier {
+        files.get_mut(found).unwrap().take().unwrap()
+      } else {
+        files
+          .get_mut(module_specifier.as_str())
+          .unwrap()
+          .take()
+          .unwrap()
+      };
+      let module_source = ModuleSource {
+        code: ModuleCode::from(source_code),
+        module_type: ModuleType::JavaScript,
+        module_url_specified: module_specifier.clone().into(),
+        module_url_found: found_specifier.map(|s| s.into()),
+      };
+      async move { Ok(module_source) }.boxed_local()
+    }
+  }
+
+  let loader = Rc::new(ConsumingLoader::default());
+  let mut runtime = JsRuntime::new(RuntimeOptions {
+    module_loader: Some(loader),
+    ..Default::default()
+  });
+
+  let spec = resolve_url("file:///main.js").unwrap();
+  let a_id = runtime.load_main_module(&spec, None).await.unwrap();
+  #[allow(clippy::let_underscore_future)]
+  let _ = runtime.mod_evaluate(a_id);
+  runtime.run_event_loop(false).await.unwrap();
+}
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>) {}