Repository: denoland/deno_core
Pull request #756 (2024-05-24 16:03:04)

chore: update v8
Upgrade V8!

- Remove Symbol.asyncDispose polyfill
- Use new cppgc api!
  - The isolate now owns the cppgc heap, so it doesn't need to be manually dropped.
  - Use `cppgc::Persistent` instead of `v8::Global` wrapper
  - Use cached FunctionTemplate to create cppgc objects
  - Use `GarbageCollected` trait so that cppgc objects can actually contain heap references.
  - A ton of updates to op2 codegen for all of this

Fixes: https://github.com/denoland/deno_core/issues/689
Fixes: https://github.com/denoland/deno_core/issues/671

0f-0b: This PR resolves #671 and #689.

bartlomieju: @devsnek can you open a PR against Deno that uses this branch so we can make sure we can safely cut `deno_core` release?

Patch:
diff --git a/.github/workflows/ci-coverage/action.yml b/.github/workflows/ci-coverage/action.yml
index 2f5f1af..1e4ff87 100644
--- a/.github/workflows/ci-coverage/action.yml
+++ b/.github/workflows/ci-coverage/action.yml
@@ -9,7 +9,8 @@ runs:
       run: |
         cargo llvm-cov --workspace --release --all-features --bins --tests --examples --exclude deno_ops_compile_test_runner --lcov --output-path lcov.info
     - name: Upload coverage to Codecov
-      uses: codecov/codecov-action@v3
+      uses: codecov/codecov-action@v4
       with:
         files: lcov.info
         fail_ci_if_error: false
+        token: ${{ inputs.codecov_token }}
diff --git a/.github/workflows/ci-job.yml b/.github/workflows/ci-job.yml
index c272478..be57cbe 100644
--- a/.github/workflows/ci-job.yml
+++ b/.github/workflows/ci-job.yml
@@ -77,6 +77,8 @@ jobs:
         id: coverage
         if: inputs.job == 'coverage'
         uses: ./.github/workflows/ci-coverage
+        with:
+          token: ${{ secrets.CODECOV_TOKEN }}
 
       - name: Lint
         id: lint
diff --git a/Cargo.lock b/Cargo.lock
index 49ed970..2c58e43 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -182,6 +182,29 @@ dependencies = [
  "serde",
 ]
 
+[[package]]
+name = "bindgen"
+version = "0.69.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "a00dc851838a2120612785d195287475a3ac45514741da670b735818822129a0"
+dependencies = [
+ "bitflags",
+ "cexpr",
+ "clang-sys",
+ "itertools",
+ "lazy_static",
+ "lazycell",
+ "log",
+ "prettyplease",
+ "proc-macro2",
+ "quote",
+ "regex",
+ "rustc-hash",
+ "shlex",
+ "syn",
+ "which 4.4.2",
+]
+
 [[package]]
 name = "bit-set"
 version = "0.5.3"
@@ -248,6 +271,15 @@ version = "1.0.97"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "099a5357d84c4c61eb35fc8eafa9a79a902c2f76911e5747ced4e032edd8d9b4"
 
+[[package]]
+name = "cexpr"
+version = "0.6.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "6fac387a98bb7c37292057cffc56d62ecb629900026402633ae9160df93a8766"
+dependencies = [
+ "nom",
+]
+
 [[package]]
 name = "cfg-if"
 version = "1.0.0"
@@ -281,6 +313,17 @@ dependencies = [
  "half",
 ]
 
+[[package]]
+name = "clang-sys"
+version = "1.8.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0b023947811758c97c59bf9d1c188fd619ad4718dcaa767947df1cadb14f39f4"
+dependencies = [
+ "glob",
+ "libc",
+ "libloading",
+]
+
 [[package]]
 name = "clap"
 version = "4.5.4"
@@ -1124,12 +1167,28 @@ version = "1.4.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e2abad23fbc42b3700f2f279844dc832adb2b2eb069b2df918f455c4e18cc646"
 
+[[package]]
+name = "lazycell"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "830d08ce1d1d941e6b30645f1a0eb5643013d835ce3779a5fc208261dbe10f55"
+
 [[package]]
 name = "libc"
 version = "0.2.154"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "ae743338b92ff9146ce83992f766a31066a91a8c84a45e0e9f21e7cf6de6d346"
 
+[[package]]
+name = "libloading"
+version = "0.8.4"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "e310b3a6b5907f99202fcdb4960ff45b93735d7c7d96b760fcff8db2dc0e103d"
+dependencies = [
+ "cfg-if",
+ "windows-targets 0.52.5",
+]
+
 [[package]]
 name = "linux-raw-sys"
 version = "0.4.13"
@@ -1167,6 +1226,12 @@ dependencies = [
  "autocfg",
 ]
 
+[[package]]
+name = "minimal-lexical"
+version = "0.2.1"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a"
+
 [[package]]
 name = "miniz_oxide"
 version = "0.7.2"
@@ -1193,6 +1258,16 @@ version = "1.0.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "650eef8c711430f1a879fdd01d4745a7deea475becfb90269c06775983bbf086"
 
+[[package]]
+name = "nom"
+version = "7.1.3"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "d273983c5a657a70a3e8f2a01329822f3b8c8172b73826411a55751e404a0a4a"
+dependencies = [
+ "memchr",
+ "minimal-lexical",
+]
+
 [[package]]
 name = "num-bigint"
 version = "0.4.5"
@@ -1283,6 +1358,12 @@ dependencies = [
  "windows-targets 0.52.5",
 ]
 
+[[package]]
+name = "paste"
+version = "1.0.15"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "57c0d7b74b563b49d38dae00a0c37d4d6de9b432382b2892f0574ddcae73fd0a"
+
 [[package]]
 name = "pathdiff"
 version = "0.2.1"
@@ -1799,6 +1880,12 @@ dependencies = [
  "digest",
 ]
 
+[[package]]
+name = "shlex"
+version = "1.3.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0fda2ff0d084019ba4d7c6f371c95d8fd75ce3524c3cb8fb653a3023f6323e64"
+
 [[package]]
 name = "signal-hook-registry"
 version = "1.4.2"
@@ -2648,17 +2735,19 @@ dependencies = [
 
 [[package]]
 name = "v8"
-version = "0.94.0"
+version = "0.95.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "f2295b751f333d95987dded7893a150a557a2df3877f311373400d3a8c580aec"
+checksum = "c4395d3c43b81368d91335ffc78d71cb6e3288d311ab6a55094634cb2afdc5c5"
 dependencies = [
+ "bindgen",
  "bitflags",
  "fslock",
  "gzip-header",
  "home",
  "miniz_oxide",
  "once_cell",
- "which",
+ "paste",
+ "which 6.0.1",
 ]
 
 [[package]]
@@ -2756,6 +2845,18 @@ dependencies = [
  "wasm-bindgen",
 ]
 
+[[package]]
+name = "which"
+version = "4.4.2"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "87ba24419a2078cd2b0f2ede2691b6c66d8e47836da3b6db8265ebad47afbfc7"
+dependencies = [
+ "either",
+ "home",
+ "once_cell",
+ "rustix",
+]
+
 [[package]]
 name = "which"
 version = "6.0.1"
diff --git a/Cargo.toml b/Cargo.toml
index f5c97e1..6e9b00b 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -24,7 +24,7 @@ deno_ops = { version = "0.167.0", path = "./ops" }
 serde_v8 = { version = "0.200.0", path = "./serde_v8" }
 deno_core_testing = { path = "./testing" }
 
-v8 = { version = "0.94.0", default-features = false }
+v8 = { version = "0.95.0", default-features = false }
 deno_ast = { version = "=0.35.3", features = ["transpiling"] }
 deno_unsync = "0.3.2"
 deno_core_icudata = "0.0.73"
diff --git a/core/cppgc.rs b/core/cppgc.rs
index 8b5c002..40dd147 100644
--- a/core/cppgc.rs
+++ b/core/cppgc.rs
@@ -1,74 +1,90 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 
+use crate::JsRuntime;
 use std::any::TypeId;
+pub use v8::cppgc::GarbageCollected;
 
-pub trait GcResource {}
+const CPPGC_TAG: u16 = 1;
 
-struct CppGcObject<T: GcResource> {
+#[repr(C)]
+struct CppGcObject<T: GarbageCollected> {
   tag: TypeId,
-  member: T,
+  member: v8::cppgc::Member<T>,
 }
 
-impl<T: GcResource> v8::cppgc::GarbageCollected for CppGcObject<T> {
-  fn trace(&self, _: &v8::cppgc::Visitor) {}
+impl<T: GarbageCollected> v8::cppgc::GarbageCollected for CppGcObject<T> {
+  fn trace(&self, visitor: &v8::cppgc::Visitor) {
+    visitor.trace(&self.member);
+  }
 }
 
-pub(crate) const DEFAULT_CPP_GC_EMBEDDER_ID: u16 = 0xde90;
+pub(crate) fn cppgc_template_constructor(
+  _scope: &mut v8::HandleScope,
+  _args: v8::FunctionCallbackArguments,
+  _rv: v8::ReturnValue,
+) {
+}
 
-const EMBEDDER_ID_OFFSET: i32 = 0;
-const SLOT_OFFSET: i32 = 1;
-const FIELD_COUNT: usize = 2;
+pub(crate) fn make_cppgc_template<'s>(
+  scope: &mut v8::HandleScope<'s, ()>,
+) -> v8::Local<'s, v8::FunctionTemplate> {
+  v8::FunctionTemplate::new(scope, cppgc_template_constructor)
+}
 
-pub fn make_cppgc_object<'a, T: GcResource + 'static>(
+pub fn make_cppgc_object<'a, T: GarbageCollected + 'static>(
   scope: &mut v8::HandleScope<'a>,
   t: T,
 ) -> v8::Local<'a, v8::Object> {
-  let templ = v8::ObjectTemplate::new(scope);
-  templ.set_internal_field_count(FIELD_COUNT);
-
-  let obj = templ.new_instance(scope).unwrap();
-  let member = v8::cppgc::make_garbage_collected(
-    scope.get_cpp_heap().unwrap(),
-    Box::new(CppGcObject {
-      tag: TypeId::of::<T>(),
-      member: t,
-    }),
-  );
-
-  obj.set_aligned_pointer_in_internal_field(
-    EMBEDDER_ID_OFFSET,
-    &DEFAULT_CPP_GC_EMBEDDER_ID as *const u16 as _,
-  );
-  obj.set_aligned_pointer_in_internal_field(SLOT_OFFSET, member.handle as _);
+  let state = JsRuntime::state_from(scope);
+  let templ =
+    v8::Local::new(scope, state.cppgc_template.borrow().as_ref().unwrap());
+  let func = templ.get_function(scope).unwrap();
+  let obj = func.new_instance(scope, &[]).unwrap();
+
+  let heap = scope.get_cpp_heap().unwrap();
+
+  let member = unsafe {
+    v8::cppgc::make_garbage_collected(
+      heap,
+      CppGcObject {
+        tag: TypeId::of::<T>(),
+        member: v8::cppgc::make_garbage_collected(heap, t),
+      },
+    )
+  };
+
+  unsafe {
+    v8::Object::wrap::<CPPGC_TAG, CppGcObject<T>>(scope, obj, &member);
+  }
+
   obj
 }
 
+#[doc(hidden)]
 #[allow(clippy::needless_lifetimes)]
-pub fn try_unwrap_cppgc_object<'sc, T: GcResource + 'static>(
+pub fn try_unwrap_cppgc_object<'sc, T: GarbageCollected + 'static>(
+  scope: &mut v8::HandleScope<'sc>,
   val: v8::Local<'sc, v8::Value>,
-) -> Option<&'sc T> {
+) -> v8::cppgc::Member<T> {
   let Ok(obj): Result<v8::Local<v8::Object>, _> = val.try_into() else {
-    return None;
+    return v8::cppgc::Member::empty();
   };
 
-  if obj.internal_field_count() != FIELD_COUNT {
-    return None;
+  if !obj.is_api_wrapper() {
+    return v8::cppgc::Member::empty();
   }
 
-  // SAFETY: This code works under the assumption that
-  // there is no other way to obtain an object with
-  // internal field count of 2.
-  //
-  // The object can only be created by `make_cppgc_object`.
-  let member = unsafe {
-    let ptr = obj.get_aligned_pointer_from_internal_field(SLOT_OFFSET);
-    let obj = &*(ptr as *const v8::cppgc::InnerMember);
-    obj.get::<CppGcObject<T>>()
+  let ptr =
+    unsafe { v8::Object::unwrap::<CPPGC_TAG, CppGcObject<T>>(scope, obj) };
+  let Some(obj) = ptr.borrow() else {
+    return v8::cppgc::Member::empty();
   };
 
-  if member.tag != TypeId::of::<T>() {
-    return None;
+  if obj.tag != TypeId::of::<T>() {
+    return v8::cppgc::Member::empty();
   }
 
-  Some(&member.member)
+  let mut h = v8::cppgc::Member::empty();
+  h.set(&obj.member);
+  h
 }
diff --git a/core/lib.rs b/core/lib.rs
index 465aa71..31d6c89 100644
--- a/core/lib.rs
+++ b/core/lib.rs
@@ -69,7 +69,7 @@ pub use crate::async_cell::RcLike;
 pub use crate::async_cell::RcRef;
 pub use crate::convert::FromV8;
 pub use crate::convert::ToV8;
-pub use crate::cppgc::GcResource;
+pub use crate::cppgc::GarbageCollected;
 pub use crate::error::GetErrorClassFn;
 pub use crate::error::JsErrorCreateFn;
 pub use crate::extensions::Extension;
@@ -183,7 +183,6 @@ pub mod _ops {
   pub use super::extensions::OpDecl;
   #[cfg(debug_assertions)]
   pub use super::ops::reentrancy_check;
-  pub use super::ops::CppGcObjectGuard;
   pub use super::ops::OpCtx;
   pub use super::ops_metrics::dispatch_metrics_async;
   pub use super::ops_metrics::dispatch_metrics_fast;
diff --git a/core/ops.rs b/core/ops.rs
index 41ed3e6..f071e01 100644
--- a/core/ops.rs
+++ b/core/ops.rs
@@ -8,7 +8,6 @@ use crate::ops_metrics::OpMetricsFn;
 use crate::runtime::JsRuntimeState;
 use crate::runtime::OpDriverImpl;
 use crate::FeatureChecker;
-use crate::GcResource;
 use crate::OpDecl;
 use futures::task::AtomicWaker;
 use std::cell::RefCell;
@@ -58,37 +57,6 @@ pub fn reentrancy_check(decl: &'static OpDecl) -> Option<ReentrancyGuard> {
   Some(ReentrancyGuard {})
 }
 
-/// A guard for a [`cppgc::Gc`] object that ensures the object is rooted while the guard is alive.
-pub struct CppGcObjectGuard<T: GcResource + 'static> {
-  _global: v8::Global<v8::Value>,
-  t: &'static T,
-}
-
-impl<T: GcResource + 'static> CppGcObjectGuard<T> {
-  pub fn try_new_from_cppgc_object(
-    isolate: &mut Isolate,
-    val: v8::Local<v8::Value>,
-  ) -> Option<Self> {
-    crate::cppgc::try_unwrap_cppgc_object(val).map(|t| Self {
-      _global: v8::Global::new(isolate, val),
-      // SAFETY: `global` will keep T rooted. We never expose T to the user with
-      // a 'static lifetime. Only ever a reference to T tied to the lifetime of
-      // the `CppGcObjectGuard`.
-      t: unsafe { &*(t as *const T) },
-    })
-  }
-
-  /// Returns a reference to the inner T value.
-  ///
-  /// # Safety
-  ///
-  /// This is only safe if you have ensured that the `Isolate` that this object
-  /// is stored in, is still alive.
-  pub unsafe fn as_ref(&self) -> &T {
-    self.t
-  }
-}
-
 #[derive(Clone, Copy)]
 pub struct OpMetadata {
   /// A description of the op for use in sanitizer output.
diff --git a/core/runtime/bindings.rs b/core/runtime/bindings.rs
index 69c8ad3..0b63fd6 100644
--- a/core/runtime/bindings.rs
+++ b/core/runtime/bindings.rs
@@ -9,6 +9,7 @@ use v8::MapFnTo;
 use super::jsruntime::BUILTIN_SOURCES;
 use super::jsruntime::CONTEXT_SETUP_SOURCES;
 use super::v8_static_strings::*;
+use crate::cppgc::cppgc_template_constructor;
 use crate::error::has_call_site;
 use crate::error::is_instance_of_error;
 use crate::error::throw_type_error;
@@ -62,6 +63,10 @@ pub(crate) fn create_external_references(
     pointer: syn_module_eval_fn as *mut c_void,
   });
 
+  references.push(v8::ExternalReference {
+    function: cppgc_template_constructor.map_fn_to(),
+  });
+
   // Using v8::OneByteConst and passing external references to it
   // allows V8 to take an optimized path when deserializing the snapshot.
   for source_file in &CONTEXT_SETUP_SOURCES {
diff --git a/core/runtime/jsruntime.rs b/core/runtime/jsruntime.rs
index 8dc599c..ce658ac 100644
--- a/core/runtime/jsruntime.rs
+++ b/core/runtime/jsruntime.rs
@@ -150,7 +150,6 @@ pub(crate) struct InnerIsolateState {
   main_realm: ManuallyDrop<JsRealm>,
   pub(crate) state: ManuallyDropRc<JsRuntimeState>,
   v8_isolate: ManuallyDrop<v8::OwnedIsolate>,
-  cpp_heap: ManuallyDrop<v8::UniqueRef<v8::cppgc::Heap>>,
 }
 
 impl InnerIsolateState {
@@ -190,10 +189,9 @@ impl InnerIsolateState {
   pub fn prepare_for_snapshot(mut self) -> v8::OwnedIsolate {
     self.cleanup();
     // SAFETY: We're copying out of self and then immediately forgetting self
-    let (state, _cpp_heap, isolate) = unsafe {
+    let (state, isolate) = unsafe {
       (
         ManuallyDrop::take(&mut self.state.0),
-        ManuallyDrop::take(&mut self.cpp_heap),
         ManuallyDrop::take(&mut self.v8_isolate),
       )
     };
@@ -216,7 +214,6 @@ impl Drop for InnerIsolateState {
           eprintln!("WARNING: v8::OwnedIsolate for snapshot was leaked");
         }
       } else {
-        ManuallyDrop::drop(&mut self.cpp_heap);
         ManuallyDrop::drop(&mut self.v8_isolate);
       }
     }
@@ -421,6 +418,7 @@ pub struct JsRuntimeState {
   pub(crate) eval_context_get_code_cache_cb: Option<EvalContextGetCodeCacheCb>,
   pub(crate) eval_context_code_cache_ready_cb:
     Option<EvalContextCodeCacheReadyCb>,
+  pub(crate) cppgc_template: RefCell<Option<v8::Global<v8::FunctionTemplate>>>,
   waker: Arc<AtomicWaker>,
   /// Accessed through [`JsRuntimeState::with_inspector`].
   inspector: RefCell<Option<Rc<RefCell<JsRuntimeInspector>>>>,
@@ -709,6 +707,7 @@ impl JsRuntime {
       // Some fields are initialized later after isolate is created
       inspector: None.into(),
       has_inspector: false.into(),
+      cppgc_template: None.into(),
     });
 
     // ...now we're moving on to ops; set them up, create `OpCtx` for each op
@@ -788,8 +787,6 @@ impl JsRuntime {
       external_refs_static,
     );
 
-    let cpp_heap = setup::init_cppgc(&mut isolate);
-
     // ...isolate is fully set up, we can forward its pointer to the ops to finish
     // their' setup...
     for op_ctx in op_ctxs.iter_mut() {
@@ -833,6 +830,12 @@ impl JsRuntime {
     let main_context = {
       let scope = &mut v8::HandleScope::new(&mut isolate);
 
+      let cppgc_template = crate::cppgc::make_cppgc_template(scope);
+      state_rc
+        .cppgc_template
+        .borrow_mut()
+        .replace(v8::Global::new(scope, cppgc_template));
+
       let context = create_context(
         scope,
         &global_template_middleware,
@@ -955,7 +958,6 @@ impl JsRuntime {
         main_realm: ManuallyDrop::new(main_realm),
         state: ManuallyDropRc(ManuallyDrop::new(state_rc)),
         v8_isolate: ManuallyDrop::new(isolate),
-        cpp_heap: ManuallyDrop::new(cpp_heap),
       },
       allocations: isolate_allocations,
       files_loaded_from_fs_during_snapshot: vec![],
@@ -1904,16 +1906,9 @@ fn create_context<'a>(
 
   let scope = &mut v8::ContextScope::new(scope, context);
 
-  // Get the global wrapper object from the context, get the real inner
-  // global object from it, and and configure it using the middlewares.
-  let global_wrapper = context.global(scope);
-  let real_global = global_wrapper
-    .get_prototype(scope)
-    .unwrap()
-    .to_object(scope)
-    .unwrap();
+  let global = context.global(scope);
   for middleware in global_object_middlewares {
-    middleware(scope, real_global);
+    middleware(scope, global);
   }
   context
 }
diff --git a/core/runtime/ops.rs b/core/runtime/ops.rs
index 1d22ed7..fc5d1da 100644
--- a/core/runtime/ops.rs
+++ b/core/runtime/ops.rs
@@ -525,7 +525,7 @@ mod tests {
   use crate::op2;
   use crate::runtime::JsRuntimeState;
   use crate::FromV8;
-  use crate::GcResource;
+  use crate::GarbageCollected;
   use crate::JsRuntime;
   use crate::OpState;
   use crate::RuntimeOptions;
@@ -1898,7 +1898,7 @@ mod tests {
     pub value: u32,
   }
 
-  impl GcResource for TestResource {}
+  impl GarbageCollected for TestResource {}
 
   #[op2]
   #[cppgc]
diff --git a/core/runtime/ops_rust_to_v8.rs b/core/runtime/ops_rust_to_v8.rs
index 607f3c5..4489bb1 100644
--- a/core/runtime/ops_rust_to_v8.rs
+++ b/core/runtime/ops_rust_to_v8.rs
@@ -413,7 +413,7 @@ impl<'a, T: serde::Serialize> RustToV8Fallible<'a>
 // CppGc
 //
 
-impl<'a, T: crate::cppgc::GcResource + 'static> RustToV8<'a>
+impl<'a, T: crate::cppgc::GarbageCollected + 'static> RustToV8<'a>
   for RustToV8Marker<CppGcMarker, T>
 {
   #[inline(always)]
diff --git a/core/runtime/setup.rs b/core/runtime/setup.rs
index de3a68a..7e935ae 100644
--- a/core/runtime/setup.rs
+++ b/core/runtime/setup.rs
@@ -82,18 +82,11 @@ pub fn init_v8(
   DENO_INIT.call_once(move || v8_init(v8_platform, snapshot, expose_natives));
 }
 
-pub fn init_cppgc(isolate: &mut v8::Isolate) -> v8::UniqueRef<v8::cppgc::Heap> {
-  let heap = v8::cppgc::Heap::create(
+fn create_cpp_heap() -> v8::UniqueRef<v8::cppgc::Heap> {
+  v8::cppgc::Heap::create(
     v8::V8::get_current_platform(),
-    v8::cppgc::HeapCreateParams::new(v8::cppgc::WrapperDescriptor::new(
-      0,
-      1,
-      crate::cppgc::DEFAULT_CPP_GC_EMBEDDER_ID,
-    )),
-  );
-
-  isolate.attach_cpp_heap(&heap);
-  heap
+    v8::cppgc::HeapCreateParams::default(),
+  )
 }
 
 pub fn create_isolate_ptr() -> *mut v8::OwnedIsolate {
@@ -116,16 +109,21 @@ pub fn create_isolate(
   maybe_startup_snapshot: Option<V8Snapshot>,
   external_refs: &'static v8::ExternalReferences,
 ) -> v8::OwnedIsolate {
+  let mut params = maybe_create_params
+    .unwrap_or_default()
+    .embedder_wrapper_type_info_offsets(
+      V8_WRAPPER_TYPE_INDEX,
+      V8_WRAPPER_OBJECT_INDEX,
+    )
+    .cpp_heap(create_cpp_heap());
   let mut isolate = if will_snapshot {
-    snapshot::create_snapshot_creator(external_refs, maybe_startup_snapshot)
+    snapshot::create_snapshot_creator(
+      external_refs,
+      maybe_startup_snapshot,
+      params,
+    )
   } else {
-    let mut params = maybe_create_params
-      .unwrap_or_default()
-      .embedder_wrapper_type_info_offsets(
-        V8_WRAPPER_TYPE_INDEX,
-        V8_WRAPPER_OBJECT_INDEX,
-      )
-      .external_references(&**external_refs);
+    params = params.external_references(&**external_refs);
     let has_snapshot = maybe_startup_snapshot.is_some();
     if let Some(snapshot) = maybe_startup_snapshot {
       params = params.snapshot_blob(snapshot.0);
diff --git a/core/runtime/snapshot.rs b/core/runtime/snapshot.rs
index 17bfdb2..8718061 100644
--- a/core/runtime/snapshot.rs
+++ b/core/runtime/snapshot.rs
@@ -324,13 +324,15 @@ pub(crate) fn store_snapshotted_data_for_snapshot<'snapshot>(
 pub(crate) fn create_snapshot_creator(
   external_refs: &'static v8::ExternalReferences,
   maybe_startup_snapshot: Option<V8Snapshot>,
+  params: v8::CreateParams,
 ) -> v8::OwnedIsolate {
   if let Some(snapshot) = maybe_startup_snapshot {
     v8::Isolate::snapshot_creator_from_existing_snapshot(
       snapshot.0,
       Some(external_refs),
+      Some(params),
     )
   } else {
-    v8::Isolate::snapshot_creator(Some(external_refs))
+    v8::Isolate::snapshot_creator(Some(external_refs), Some(params))
   }
 }
diff --git a/ops/op2/dispatch_async.rs b/ops/op2/dispatch_async.rs
index a5ec579..f9c2212 100644
--- a/ops/op2/dispatch_async.rs
+++ b/ops/op2/dispatch_async.rs
@@ -114,12 +114,6 @@ pub(crate) fn generate_dispatch_async(
   }
   output.extend(quote!(return 2;));
 
-  let with_scope = if generator_state.needs_scope {
-    with_scope(generator_state)
-  } else {
-    quote!()
-  };
-
   let with_opstate = if generator_state.needs_opstate {
     with_opstate(generator_state)
   } else {
@@ -144,6 +138,12 @@ pub(crate) fn generate_dispatch_async(
     quote!()
   };
 
+  let with_scope = if generator_state.needs_scope {
+    with_scope(generator_state)
+  } else {
+    quote!()
+  };
+
   Ok(
     gs_quote!(generator_state(info, slow_function, slow_function_metrics, opctx) => {
       #[inline(always)]
diff --git a/ops/op2/dispatch_fast.rs b/ops/op2/dispatch_fast.rs
index ea0325c..e7f77b8 100644
--- a/ops/op2/dispatch_fast.rs
+++ b/ops/op2/dispatch_fast.rs
@@ -452,9 +452,11 @@ pub(crate) fn generate_dispatch_fast(
   };
 
   let with_self = if generator_state.needs_self {
+    generator_state.needs_fast_scope = true;
     generator_state.needs_fast_api_callback_options = true;
-    gs_quote!(generator_state(self_ty, fast_api_callback_options) => {
-      let Some(self_) = deno_core::_ops::try_unwrap_cppgc_object::<#self_ty>(this.into()) else {
+    gs_quote!(generator_state(self_ty, scope, fast_api_callback_options) => {
+      let self_handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#self_ty>(&mut #scope, this.into());
+      let Some(self_) = self_handle_.borrow() else {
         #fast_api_callback_options.fallback = true;
         // SAFETY: All fast return types have zero as a valid value
         return unsafe { std::mem::zeroed() };
@@ -471,6 +473,22 @@ pub(crate) fn generate_dispatch_fast(
     quote!(Self:: #name)
   };
 
+  let with_scope = if generator_state.needs_fast_scope {
+    generator_state.needs_fast_api_callback_options = true;
+    gs_quote!(generator_state(scope, fast_api_callback_options) => (
+      let mut #scope = unsafe {
+        if !this.is_object() {
+          #fast_api_callback_options.fallback = true;
+          // SAFETY: All fast return types have zero as a valid value
+          return unsafe { std::mem::zeroed() };
+        };
+        deno_core::v8::CallbackScope::new(this)
+      };
+    ))
+  } else {
+    quote!()
+  };
+
   let mut fastsig_metrics = fastsig.clone();
   fastsig_metrics.ensure_fast_api_callback_options();
 
@@ -503,6 +521,7 @@ pub(crate) fn generate_dispatch_fast(
       .unzip();
   let (fastcall_names, fastcall_types): (Vec<_>, Vec<_>) =
     fastsig.input_args(generator_state).into_iter().unzip();
+
   let fast_fn = gs_quote!(generator_state(result, fast_api_callback_options, fast_function, fast_function_metrics) => {
     #[allow(clippy::too_many_arguments)]
     extern "C" fn #fast_function_metrics<'s>(
@@ -531,6 +550,7 @@ pub(crate) fn generate_dispatch_fast(
       let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(&<Self as deno_core::_ops::Op>::DECL);
 
       #with_fast_api_callback_options
+      #with_scope
       #with_opctx
       #with_js_runtime_state
       #with_self
@@ -554,8 +574,10 @@ fn map_v8_fastcall_arg_to_arg(
 ) -> Result<TokenStream, V8MappingError> {
   let GeneratorState {
     opctx,
+    scope,
     js_runtime_state,
     fast_api_callback_options,
+    needs_fast_scope,
     needs_fast_opctx: needs_opctx,
     needs_fast_api_callback_options,
     needs_fast_js_runtime_state: needs_js_runtime_state,
@@ -739,12 +761,14 @@ fn map_v8_fastcall_arg_to_arg(
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
 
+      *needs_fast_scope = true;
       *needs_fast_api_callback_options = true;
       quote! {
-        let Some(#arg_ident) = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(#arg_ident) else {
-            #fast_api_callback_options.fallback = true;
-            // SAFETY: All fast return types have zero as a valid value
-            return unsafe { std::mem::zeroed() };
+        let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident);
+        let Some(#arg_ident) = handle_.borrow() else {
+          #fast_api_callback_options.fallback = true;
+          // SAFETY: All fast return types have zero as a valid value
+          return unsafe { std::mem::zeroed() };
         };
       }
     }
@@ -752,16 +776,22 @@ fn map_v8_fastcall_arg_to_arg(
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
 
+      *needs_fast_scope = true;
       *needs_fast_api_callback_options = true;
       quote! {
+        let mut handle_ = deno_core::v8::cppgc::Member::empty();
         let #arg_ident = if #arg_ident.is_null_or_undefined() {
           None
-        } else if let Some(#arg_ident) = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(#arg_ident) {
-          Some(#arg_ident)
         } else {
-          #fast_api_callback_options.fallback = true;
-          // SAFETY: All fast return types have zero as a valid value
-          return unsafe { std::mem::zeroed() };
+          handle_.set(&deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident));
+          match handle_.borrow() {
+            Some(r) => Some(r),
+            None => {
+              #fast_api_callback_options.fallback = true;
+              // SAFETY: All fast return types have zero as a valid value
+              return unsafe { std::mem::zeroed() };
+            }
+          }
         };
       }
     }
diff --git a/ops/op2/dispatch_slow.rs b/ops/op2/dispatch_slow.rs
index 96c52c0..19cd508 100644
--- a/ops/op2/dispatch_slow.rs
+++ b/ops/op2/dispatch_slow.rs
@@ -91,13 +91,6 @@ pub(crate) fn generate_dispatch_slow(
     |s| V8SignatureMappingError::NoRetValMapping(s, signature.ret_val.clone()),
   )?);
 
-  let with_self = if generator_state.needs_self {
-    with_self(generator_state, &signature.ret_val)
-      .map_err(V8SignatureMappingError::NoSelfMapping)?
-  } else {
-    quote!()
-  };
-
   // 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 {
@@ -106,12 +99,6 @@ pub(crate) fn generate_dispatch_slow(
       quote!()
     };
 
-  let with_scope = if generator_state.needs_scope {
-    with_scope(generator_state)
-  } else {
-    quote!()
-  };
-
   let with_opstate = if generator_state.needs_opstate {
     with_opstate(generator_state)
   } else {
@@ -142,6 +129,19 @@ pub(crate) fn generate_dispatch_slow(
     quote!()
   };
 
+  let with_self = if generator_state.needs_self {
+    with_self(generator_state, &signature.ret_val)
+      .map_err(V8SignatureMappingError::NoSelfMapping)?
+  } else {
+    quote!()
+  };
+
+  let with_scope = if generator_state.needs_scope {
+    with_scope(generator_state)
+  } else {
+    quote!()
+  };
+
   Ok(
     gs_quote!(generator_state(opctx, info, slow_function, slow_function_metrics) => {
       #[inline(always)]
@@ -249,6 +249,7 @@ pub(crate) fn with_self(
   ret_val: &RetVal,
 ) -> Result<TokenStream, V8MappingError> {
   generator_state.needs_opctx = true;
+  generator_state.needs_scope = true;
   let throw_exception = throw_type_error(
     generator_state,
     format!("expected {}", &generator_state.self_ty),
@@ -256,18 +257,24 @@ pub(crate) fn with_self(
   let tokens = if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_))
   {
     let tokens = gs_quote!(generator_state(self_ty, fn_args, scope) => {
-      let Some(self_) = deno_core::_ops::CppGcObjectGuard::<#self_ty>::try_new_from_cppgc_object(&mut *#scope, #fn_args.this().into()) else {
+      let self_handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#self_ty>(&mut #scope, #fn_args.this().into());
+      if self_handle_.borrow().is_none() {
         #throw_exception;
-      };
+      }
+      let mut self_persistent_ = deno_core::v8::cppgc::Persistent::empty();
+      self_persistent_.set(&self_handle_);
+      drop(self_handle_);
     });
-    generator_state.needs_scope = true;
-    generator_state
-      .idents_that_need_to_be_captured_by_future_and_as_refd
-      .push((format_ident!("self_"), false));
+
+    generator_state.moves.push(quote! {
+      let self_ = self_persistent_.borrow().unwrap();
+    });
+
     tokens
   } else {
-    gs_quote!(generator_state(self_ty, fn_args) => {
-      let Some(self_) = deno_core::_ops::try_unwrap_cppgc_object::<#self_ty>(#fn_args.this().into()) else {
+    gs_quote!(generator_state(self_ty, fn_args, scope) => {
+      let self_handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#self_ty>(&mut #scope, #fn_args.this().into());
+      let Some(self_) = self_handle_.borrow() else {
         #throw_exception;
       };
     })
@@ -567,59 +574,75 @@ pub fn from_arg(
       }
     }
     Arg::CppGcResource(ty) => {
+      *needs_scope = true;
+      let scope = scope.clone();
       let throw_exception =
         throw_type_error(generator_state, format!("expected {}", &ty))?;
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
       if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_)) {
-        let scope = &generator_state.scope;
         let tokens = quote! {
-          let Some(#arg_ident) = deno_core::_ops::CppGcObjectGuard::<#ty>::try_new_from_cppgc_object(&mut *#scope, #arg_ident) else {
+          let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident);
+          if handle_.borrow().is_none() {
             #throw_exception;
-          };
+          }
+          let mut #arg_ident = deno_core::v8::cppgc::Persistent::empty();
+          #arg_ident.set(&handle_);
+          drop(handle_);
         };
-        generator_state.needs_scope = true;
-        generator_state
-          .idents_that_need_to_be_captured_by_future_and_as_refd
-          .push((arg_ident.clone(), false));
+        generator_state.moves.push(quote! {
+          let #arg_ident = #arg_ident.borrow().unwrap();
+        });
         tokens
       } else {
         quote! {
-          let Some(#arg_ident) = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(#arg_ident) else {
+          let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident);
+          let Some(#arg_ident) = handle_.borrow() else {
             #throw_exception;
           };
         }
       }
     }
     Arg::OptionCppGcResource(ty) => {
+      *needs_scope = true;
       let throw_exception =
         throw_type_error(generator_state, format!("expected {}", &ty))?;
       let ty =
         syn::parse_str::<syn::Path>(ty).expect("Failed to reparse state type");
+      let scope = &generator_state.scope;
       if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_)) {
-        let scope = &generator_state.scope;
         let tokens = quote! {
           let #arg_ident = if #arg_ident.is_null_or_undefined() {
-            None
-          } else if let Some(#arg_ident) = deno_core::_ops::CppGcObjectGuard::<#ty>::try_new_from_cppgc_object(&mut *#scope, #arg_ident) else {
-            Some(#arg_ident)
+            deno_core::v8::cppgc::Persistent::empty()
           } else {
-            #throw_exception;
+            let handle = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident);
+            if handle.borrow().is_none() {
+              #throw_exception;
+            }
+            let mut persistent = deno_core::v8::cppgc::Persistent::empty();
+            persistent.set(&handle);
+            persistent
           };
         };
-        generator_state.needs_scope = true;
-        generator_state
-          .idents_that_need_to_be_captured_by_future_and_as_refd
-          .push((arg_ident.clone(), true));
+
+        generator_state.moves.push(quote! {
+          let #arg_ident = #arg_ident.borrow();
+        });
+
         tokens
       } else {
         quote! {
+          let mut handle_ = deno_core::v8::cppgc::Member::empty();
           let #arg_ident = if #arg_ident.is_null_or_undefined() {
             None
-          } else if let Some(#arg_ident) = deno_core::_ops::try_unwrap_cppgc_object::<#ty>(#arg_ident) {
-            Some(#arg_ident)
           } else {
-            #throw_exception;
+            handle_.set(&deno_core::_ops::try_unwrap_cppgc_object::<#ty>(&mut #scope, #arg_ident));
+            match handle_.borrow() {
+              Some(r) => Some(r),
+              None => {
+                #throw_exception;
+              }
+            }
           };
         }
       }
@@ -804,22 +827,14 @@ pub fn call(
   let call = quote!(#call_ ( #tokens ));
 
   if matches!(ret_val, RetVal::Future(_) | RetVal::FutureResult(_))
-    && !generator_state
-      .idents_that_need_to_be_captured_by_future_and_as_refd
-      .is_empty()
+    && !generator_state.moves.is_empty()
   {
-    let mut tokens = TokenStream::new();
-    for (ident, is_option) in
-      &generator_state.idents_that_need_to_be_captured_by_future_and_as_refd
-    {
-      tokens.extend(if *is_option {
-        quote!( let #ident = unsafe { #ident.map(|i| i.as_ref()) }; )
-      } else {
-        quote!( let #ident = unsafe { #ident.as_ref() }; )
-      });
+    let mut moves = TokenStream::new();
+    for m in &generator_state.moves {
+      moves.extend(quote!(#m));
     }
     quote!(async move {
-      #tokens
+      #moves
       #call.await
     })
   } else {
diff --git a/ops/op2/generator_state.rs b/ops/op2/generator_state.rs
index 8816bcc..6769504 100644
--- a/ops/op2/generator_state.rs
+++ b/ops/op2/generator_state.rs
@@ -1,5 +1,5 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-use proc_macro2::Ident;
+use proc_macro2::{Ident, TokenStream};
 
 pub struct GeneratorState {
   pub name: Ident,
@@ -37,14 +37,12 @@ pub struct GeneratorState {
   /// Type of the self argument
   pub self_ty: Ident,
 
-  /// Idents that need to be moved into the future and have a reference taken
-  /// before being passed to the underlying call. The bool indicates whether
-  /// the data is wrapped in a `Option`
-  pub idents_that_need_to_be_captured_by_future_and_as_refd: Vec<(Ident, bool)>,
+  pub moves: Vec<TokenStream>,
 
   pub needs_args: bool,
   pub needs_retval: bool,
   pub needs_scope: bool,
+  pub needs_fast_scope: bool,
   pub needs_isolate: bool,
   pub needs_opstate: bool,
   pub needs_opctx: bool,
diff --git a/ops/op2/mod.rs b/ops/op2/mod.rs
index a803614..5638c2d 100644
--- a/ops/op2/mod.rs
+++ b/ops/op2/mod.rs
@@ -175,13 +175,14 @@ fn generate_op2(
     fast_function_metrics,
     promise_id,
     self_ty,
-    idents_that_need_to_be_captured_by_future_and_as_refd: vec![],
+    moves: vec![],
     needs_retval: false,
     needs_scope: false,
     needs_isolate: false,
     needs_opctx: false,
     needs_opstate: false,
     needs_js_runtime_state: false,
+    needs_fast_scope: false,
     needs_fast_opctx: false,
     needs_fast_api_callback_options: false,
     needs_fast_js_runtime_state: false,
diff --git a/ops/op2/signature.rs b/ops/op2/signature.rs
index 4187e56..aa01f4b 100644
--- a/ops/op2/signature.rs
+++ b/ops/op2/signature.rs
@@ -1235,7 +1235,7 @@ fn parse_attributes(
 pub fn is_attribute_special(attr: &Attribute) -> bool {
   parse_attribute(attr).unwrap_or_default().is_some()
     // this is kind of ugly, but #[meta(..)] is the only
-    // attribute that we want to omit from the generated code 
+    // attribute that we want to omit from the generated code
     // that doesn't have a semantic meaning
     || attr.path().is_ident("meta")
 }
@@ -1856,25 +1856,25 @@ mod tests {
     ($name:ident, $error:expr, $f:item) => {
       #[test]
       pub fn $name() {
-        expect_fail(stringify!($f), stringify!($error));
+        #[allow(unused)]
+        use super::ArgError::*;
+        #[allow(unused)]
+        use super::AttributeError::*;
+        #[allow(unused)]
+        use super::SignatureError::*;
+
+        let op = stringify!($f);
+        // Parse the provided macro input as an ItemFn
+        let item_fn = parse_str::<ItemFn>(op)
+          .unwrap_or_else(|_| panic!("Failed to parse {op} as a ItemFn"));
+        let attrs = item_fn.attrs;
+        let error = parse_signature(attrs, item_fn.sig)
+          .expect_err("Expected function to fail to parse");
+        assert_eq!(format!("{error:?}"), format!("{:?}", $error));
       }
     };
   }
 
-  fn expect_fail(op: &str, error: &str) {
-    // Parse the provided macro input as an ItemFn
-    let item_fn = parse_str::<ItemFn>(op)
-      .unwrap_or_else(|_| panic!("Failed to parse {op} as a ItemFn"));
-    let attrs = item_fn.attrs;
-    let err = parse_signature(attrs, item_fn.sig)
-      .expect_err("Expected function to fail to parse");
-    // TODO(mmastrac): this might fail if debug output spacing changes
-    assert_eq!(
-      format!("{err:?}").replace("\n     ", " "),
-      error.to_owned().replace("\n    ", " ")
-    );
-  }
-
   test!(
     fn op_state_and_number(opstate: &mut OpState, a: u32) -> ();
     (Ref(Mut, OpState), Numeric(u32, None)) -> Infallible(Void)
@@ -2040,22 +2040,34 @@ mod tests {
   );
   expect_fail!(
     op_cppgc_resource_owned,
-    ArgError("resource", ExpectedCppGcReference("std::fs::File")),
+    ArgError(
+      "resource".into(),
+      ExpectedCppGcReference("std::fs::File".into())
+    ),
     fn f(#[cppgc] resource: std::fs::File) {}
   );
   expect_fail!(
     op_cppgc_resource_option_owned,
-    ArgError("resource", ExpectedCppGcReference("std::fs::File")),
+    ArgError(
+      "resource".into(),
+      ExpectedCppGcReference("std::fs::File".into())
+    ),
     fn f(#[cppgc] resource: Option<std::fs::File>) {}
   );
   expect_fail!(
     op_cppgc_resource_invalid_type,
-    ArgError("resource", InvalidCppGcType("[std :: fs :: File]")),
+    ArgError(
+      "resource".into(),
+      InvalidCppGcType("[std :: fs :: File]".into())
+    ),
     fn f(#[cppgc] resource: &[std::fs::File]) {}
   );
   expect_fail!(
     op_cppgc_resource_option_invalid_type,
-    ArgError("resource", InvalidCppGcType("[std :: fs :: File]")),
+    ArgError(
+      "resource".into(),
+      InvalidCppGcType("[std :: fs :: File]".into())
+    ),
     fn f(#[cppgc] resource: Option<&[std::fs::File]>) {}
   );
 
@@ -2063,48 +2075,59 @@ mod tests {
 
   expect_fail!(
     op_with_bad_string1,
-    ArgError("s", MissingAttribute("string", "str")),
+    ArgError("s".into(), MissingAttribute("string", "str".into())),
     fn f(s: &str) {}
   );
   expect_fail!(
     op_with_bad_string2,
-    ArgError("s", MissingAttribute("string", "String")),
+    ArgError("s".into(), MissingAttribute("string", "String".into())),
     fn f(s: String) {}
   );
   expect_fail!(
     op_with_bad_string3,
-    ArgError("s", MissingAttribute("string", "Cow<str>")),
+    ArgError("s".into(), MissingAttribute("string", "Cow<str>".into())),
     fn f(s: Cow<str>) {}
   );
   expect_fail!(
     op_with_invalid_string,
-    ArgError("x", InvalidAttributeType("string", "u32")),
+    ArgError("x".into(), InvalidAttributeType("string", "u32".into())),
     fn f(#[string] x: u32) {}
   );
   expect_fail!(
     op_with_invalid_buffer,
-    ArgError("x", InvalidAttributeType("buffer", "u32")),
+    ArgError("x".into(), InvalidAttributeType("buffer", "u32".into())),
     fn f(#[buffer] x: u32) {}
   );
   expect_fail!(
     op_with_bad_attr,
-    RetError(AttributeError(InvalidAttribute("#[badattr]"))),
+    RetError(super::RetError::AttributeError(InvalidAttribute(
+      "#[badattr]".into()
+    ))),
     #[badattr]
     fn f() {}
   );
   expect_fail!(
     op_with_bad_attr2,
-    ArgError("a", AttributeError(InvalidAttribute("#[badattr]"))),
+    ArgError(
+      "a".into(),
+      AttributeError(InvalidAttribute("#[badattr]".into()))
+    ),
     fn f(#[badattr] a: u32) {}
   );
   expect_fail!(
     op_with_missing_global,
-    ArgError("g", MissingAttribute("global", "v8::Global<v8::String>")),
+    ArgError(
+      "g".into(),
+      MissingAttribute("global", "v8::Global<v8::String>".into())
+    ),
     fn f(g: v8::Global<v8::String>) {}
   );
   expect_fail!(
     op_with_invalid_global,
-    ArgError("l", InvalidAttributeType("global", "v8::Local<v8::String>")),
+    ArgError(
+      "l".into(),
+      InvalidAttributeType("global", "v8::Local<v8::String>".into())
+    ),
     fn f(#[global] l: v8::Local<v8::String>) {}
   );
   expect_fail!(
@@ -2115,16 +2138,24 @@ mod tests {
   expect_fail!(
     op_extra_deno_core_v8,
     ArgError(
-      "a",
-      InvalidDenoCorePrefix("deno_core::v8::Function", "v8", "v8::Function")
+      "a".into(),
+      InvalidDenoCorePrefix(
+        "deno_core::v8::Function".into(),
+        "v8".into(),
+        "v8::Function".into()
+      )
     ),
     fn f(a: &deno_core::v8::Function) {}
   );
   expect_fail!(
     op_extra_deno_core_opstate,
     ArgError(
-      "a",
-      InvalidDenoCorePrefix("deno_core::OpState", "OpState", "OpState")
+      "a".into(),
+      InvalidDenoCorePrefix(
+        "deno_core::OpState".into(),
+        "OpState".into(),
+        "OpState".into()
+      )
     ),
     fn f(a: &deno_core::OpState) {}
   );
@@ -2134,17 +2165,17 @@ mod tests {
   expect_fail!(op_with_two_lifetimes, TooManyLifetimes, fn f<'a, 'b>() {});
   expect_fail!(
     op_with_lifetime_bounds,
-    LifetimesMayNotHaveBounds("'a"),
+    LifetimesMayNotHaveBounds("'a".into()),
     fn f<'a: 'b, 'b>() {}
   );
   expect_fail!(
     op_with_missing_bounds,
-    GenericBoundCardinality("B"),
+    GenericBoundCardinality("B".into()),
     fn f<'a, B>() {}
   );
   expect_fail!(
     op_with_duplicate_bounds,
-    GenericBoundCardinality("B"),
+    GenericBoundCardinality("B".into()),
     fn f<'a, B: Trait>()
     where
       B: Trait,
@@ -2153,7 +2184,7 @@ mod tests {
   );
   expect_fail!(
     op_with_extra_bounds,
-    WherePredicateMustAppearInGenerics("C"),
+    WherePredicateMustAppearInGenerics("C".into()),
     fn f<'a, B>()
     where
       B: Trait,
@@ -2164,30 +2195,35 @@ mod tests {
 
   expect_fail!(
     op_with_bad_serde_string,
-    ArgError("s", InvalidAttributeType("serde", "String")),
+    ArgError("s".into(), InvalidAttributeType("serde", "String".into())),
     fn f(#[serde] s: String) {}
   );
   expect_fail!(
     op_with_bad_serde_str,
-    ArgError("s", InvalidAttributeType("serde", "&str")),
+    ArgError("s".into(), InvalidAttributeType("serde", "&str".into())),
     fn f(#[serde] s: &str) {}
   );
 
   expect_fail!(
     op_with_bad_from_v8_string,
-    ArgError("s", InvalidAttributeType("from_v8", "String")),
+    ArgError("s".into(), InvalidAttributeType("from_v8", "String".into())),
     fn f(#[from_v8] s: String) {}
   );
 
   expect_fail!(
     op_with_to_v8_arg,
-    ArgError("s", InvalidAttributePosition("to_v8", "return value")),
+    ArgError(
+      "s".into(),
+      InvalidAttributePosition("to_v8", "return value")
+    ),
     fn f(#[to_v8] s: Foo) {}
   );
 
   expect_fail!(
     op_with_from_v8_ret,
-    RetError(InvalidType(InvalidAttributePosition("from_v8", "argument"))),
+    RetError(super::RetError::InvalidType(InvalidAttributePosition(
+      "from_v8", "argument"
+    ))),
     #[from_v8]
     fn f() -> Foo {}
   );


Test patch:
diff --git a/core/runtime/tests/misc.rs b/core/runtime/tests/misc.rs
index 1ba8ff8..6fd9e52 100644
--- a/core/runtime/tests/misc.rs
+++ b/core/runtime/tests/misc.rs
@@ -1244,7 +1244,9 @@ async fn global_template_middleware() {
     _args: v8::PropertyCallbackArguments<'s>,
     _rv: v8::ReturnValue,
   ) -> v8::Intercepted {
-    unsafe { CALLS.push("descriptor".to_string()) };
+    unsafe {
+      CALLS.push("descriptor".to_string());
+    }
     v8::Intercepted::No
   }
 
@@ -1257,7 +1259,7 @@ async fn global_template_middleware() {
   ) -> v8::Intercepted {
     unsafe {
       CALLS.push("setter".to_string());
-    };
+    }
     v8::Intercepted::No
   }
 
@@ -1270,7 +1272,7 @@ async fn global_template_middleware() {
   ) -> v8::Intercepted {
     unsafe {
       CALLS.push("definer".to_string());
-    };
+    }
     v8::Intercepted::No
   }
 
diff --git a/ops/op2/test_cases/async/async_cppgc.out b/ops/op2/test_cases/async/async_cppgc.out
index f5d5d33..1a48043 100644
--- a/ops/op2/test_cases/async/async_cppgc.out
+++ b/ops/op2/test_cases/async/async_cppgc.out
@@ -162,10 +162,10 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
             };
             let result = {
                 let arg0 = args.get(1usize as i32);
-                let Some(arg0) = deno_core::_ops::CppGcObjectGuard::<
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
                     Wrap,
-                >::try_new_from_cppgc_object(&mut *scope, arg0) else {
-                    let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
+                >(&mut scope, arg0);
+                if handle_.borrow().is_none() {
                     let msg = deno_core::v8::String::new_from_one_byte(
                             &mut scope,
                             "expected Wrap".as_bytes(),
@@ -175,9 +175,12 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
                     let exc = deno_core::v8::Exception::type_error(&mut scope, msg);
                     scope.throw_exception(exc);
                     return 1;
-                };
+                }
+                let mut arg0 = deno_core::v8::cppgc::Persistent::empty();
+                arg0.set(&handle_);
+                drop(handle_);
                 async move {
-                    let arg0 = unsafe { arg0.as_ref() };
+                    let arg0 = arg0.borrow().unwrap();
                     Self::call(arg0).await
                 }
             };
@@ -235,3 +238,129 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
     }
     <op_use_cppgc_object as ::deno_core::_ops::Op>::DECL
 }
+
+#[allow(non_camel_case_types)]
+const fn op_use_optional_cppgc_object() -> ::deno_core::_ops::OpDecl {
+    #[allow(non_camel_case_types)]
+    struct op_use_optional_cppgc_object {
+        _unconstructable: ::std::marker::PhantomData<()>,
+    }
+    impl ::deno_core::_ops::Op for op_use_optional_cppgc_object {
+        const NAME: &'static str = stringify!(op_use_optional_cppgc_object);
+        const DECL: ::deno_core::_ops::OpDecl = ::deno_core::_ops::OpDecl::new_internal_op2(
+            ::deno_core::__op_name_fast!(op_use_optional_cppgc_object),
+            true,
+            false,
+            2usize as u8,
+            Self::v8_fn_ptr as _,
+            Self::v8_fn_ptr_metrics as _,
+            None,
+            None,
+            ::deno_core::OpMetadata {
+                ..::deno_core::OpMetadata::default()
+            },
+        );
+    }
+    impl op_use_optional_cppgc_object {
+        pub const fn name() -> &'static str {
+            stringify!(op_use_optional_cppgc_object)
+        }
+        #[inline(always)]
+        fn slow_function_impl<'s>(
+            info: &'s deno_core::v8::FunctionCallbackInfo,
+        ) -> usize {
+            #[cfg(debug_assertions)]
+            let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(
+                &<Self as deno_core::_ops::Op>::DECL,
+            );
+            let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
+            let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info);
+            let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
+                info,
+            );
+            let opctx: &'s _ = unsafe {
+                &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data())
+                    .value() as *const deno_core::_ops::OpCtx)
+            };
+            let result = {
+                let arg0 = args.get(1usize as i32);
+                let arg0 = if arg0.is_null_or_undefined() {
+                    deno_core::v8::cppgc::Persistent::empty()
+                } else {
+                    let handle = deno_core::_ops::try_unwrap_cppgc_object::<
+                        Wrap,
+                    >(&mut scope, arg0);
+                    if handle.borrow().is_none() {
+                        let msg = deno_core::v8::String::new_from_one_byte(
+                                &mut scope,
+                                "expected Wrap".as_bytes(),
+                                deno_core::v8::NewStringType::Normal,
+                            )
+                            .unwrap();
+                        let exc = deno_core::v8::Exception::type_error(&mut scope, msg);
+                        scope.throw_exception(exc);
+                        return 1;
+                    }
+                    let mut persistent = deno_core::v8::cppgc::Persistent::empty();
+                    persistent.set(&handle);
+                    persistent
+                };
+                async move {
+                    let arg0 = arg0.borrow();
+                    Self::call(arg0).await
+                }
+            };
+            let promise_id = deno_core::_ops::to_i32_option(&args.get(0))
+                .unwrap_or_default();
+            if let Some(result) = deno_core::_ops::map_async_op_infallible(
+                opctx,
+                false,
+                false,
+                promise_id,
+                result,
+                |scope, result| { Ok(deno_core::_ops::RustToV8::to_v8(result, scope)) },
+            ) {
+                deno_core::_ops::RustToV8RetVal::to_v8_rv(result, &mut rv);
+                return 0;
+            }
+            return 2;
+        }
+        extern "C" fn v8_fn_ptr<'s>(info: *const deno_core::v8::FunctionCallbackInfo) {
+            let info: &'s _ = unsafe { &*info };
+            Self::slow_function_impl(info);
+        }
+        extern "C" fn v8_fn_ptr_metrics<'s>(
+            info: *const deno_core::v8::FunctionCallbackInfo,
+        ) {
+            let info: &'s _ = unsafe { &*info };
+            let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
+                info,
+            );
+            let opctx: &'s _ = unsafe {
+                &*(deno_core::v8::Local::<deno_core::v8::External>::cast(args.data())
+                    .value() as *const deno_core::_ops::OpCtx)
+            };
+            deno_core::_ops::dispatch_metrics_async(
+                opctx,
+                deno_core::_ops::OpMetricsEvent::Dispatched,
+            );
+            let res = Self::slow_function_impl(info);
+            if res == 0 {
+                deno_core::_ops::dispatch_metrics_async(
+                    opctx,
+                    deno_core::_ops::OpMetricsEvent::Completed,
+                );
+            } else if res == 1 {
+                deno_core::_ops::dispatch_metrics_async(
+                    opctx,
+                    deno_core::_ops::OpMetricsEvent::Error,
+                );
+            }
+        }
+    }
+    impl op_use_optional_cppgc_object {
+        #[inline(always)]
+        async fn call(_wrap: Option<&Wrap>) {}
+    }
+    <op_use_optional_cppgc_object as ::deno_core::_ops::Op>::DECL
+}
diff --git a/ops/op2/test_cases/async/async_cppgc.rs b/ops/op2/test_cases/async/async_cppgc.rs
index 460193f..c729669 100644
--- a/ops/op2/test_cases/async/async_cppgc.rs
+++ b/ops/op2/test_cases/async/async_cppgc.rs
@@ -1,11 +1,11 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 #![deny(warnings)]
 deno_ops_compile_test_runner::prelude!();
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 
 struct Wrap;
 
-impl GcResource for Wrap {}
+impl GarbageCollected for Wrap {}
 
 #[op2(async)]
 #[cppgc]
@@ -15,3 +15,6 @@ async fn op_make_cppgc_object() -> Wrap {
 
 #[op2(async)]
 async fn op_use_cppgc_object(#[cppgc] _wrap: &Wrap) {}
+
+#[op2(async)]
+async fn op_use_optional_cppgc_object(#[cppgc] _wrap: Option<&Wrap>) {}
diff --git a/ops/op2/test_cases/sync/cppgc_resource.out b/ops/op2/test_cases/sync/cppgc_resource.out
index 196abce..679411c 100644
--- a/ops/op2/test_cases/sync/cppgc_resource.out
+++ b/ops/op2/test_cases/sync/cppgc_resource.out
@@ -83,9 +83,18 @@ const fn op_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let fast_api_callback_options: &'s mut _ = unsafe {
                 &mut *fast_api_callback_options
             };
+            let mut scope = unsafe {
+                if !this.is_object() {
+                    fast_api_callback_options.fallback = true;
+                    return unsafe { std::mem::zeroed() };
+                }
+                deno_core::v8::CallbackScope::new(this)
+            };
             let result = {
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     fast_api_callback_options.fallback = true;
                     return unsafe { std::mem::zeroed() };
                 };
@@ -101,15 +110,17 @@ const fn op_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(
                 &<Self as deno_core::_ops::Op>::DECL,
             );
+            let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
             let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info);
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
                 info,
             );
             let result = {
                 let arg0 = args.get(0usize as i32);
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
-                    let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     let msg = deno_core::v8::String::new_from_one_byte(
                             &mut scope,
                             "expected Wrap".as_bytes(),
@@ -250,16 +261,31 @@ const fn op_option_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let fast_api_callback_options: &'s mut _ = unsafe {
                 &mut *fast_api_callback_options
             };
+            let mut scope = unsafe {
+                if !this.is_object() {
+                    fast_api_callback_options.fallback = true;
+                    return unsafe { std::mem::zeroed() };
+                }
+                deno_core::v8::CallbackScope::new(this)
+            };
             let result = {
+                let mut handle_ = deno_core::v8::cppgc::Member::empty();
                 let arg0 = if arg0.is_null_or_undefined() {
                     None
-                } else if let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<
-                    Wrap,
-                >(arg0) {
-                    Some(arg0)
                 } else {
-                    fast_api_callback_options.fallback = true;
-                    return unsafe { std::mem::zeroed() };
+                    handle_
+                        .set(
+                            &deno_core::_ops::try_unwrap_cppgc_object::<
+                                Wrap,
+                            >(&mut scope, arg0),
+                        );
+                    match handle_.borrow() {
+                        Some(r) => Some(r),
+                        None => {
+                            fast_api_callback_options.fallback = true;
+                            return unsafe { std::mem::zeroed() };
+                        }
+                    }
                 };
                 Self::call(arg0)
             };
@@ -273,29 +299,40 @@ const fn op_option_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(
                 &<Self as deno_core::_ops::Op>::DECL,
             );
+            let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
             let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info);
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
                 info,
             );
             let result = {
                 let arg0 = args.get(0usize as i32);
+                let mut handle_ = deno_core::v8::cppgc::Member::empty();
                 let arg0 = if arg0.is_null_or_undefined() {
                     None
-                } else if let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<
-                    Wrap,
-                >(arg0) {
-                    Some(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 Wrap".as_bytes(),
-                            deno_core::v8::NewStringType::Normal,
-                        )
-                        .unwrap();
-                    let exc = deno_core::v8::Exception::type_error(&mut scope, msg);
-                    scope.throw_exception(exc);
-                    return 1;
+                    handle_
+                        .set(
+                            &deno_core::_ops::try_unwrap_cppgc_object::<
+                                Wrap,
+                            >(&mut scope, arg0),
+                        );
+                    match handle_.borrow() {
+                        Some(r) => Some(r),
+                        None => {
+                            let msg = deno_core::v8::String::new_from_one_byte(
+                                    &mut scope,
+                                    "expected Wrap".as_bytes(),
+                                    deno_core::v8::NewStringType::Normal,
+                                )
+                                .unwrap();
+                            let exc = deno_core::v8::Exception::type_error(
+                                &mut scope,
+                                msg,
+                            );
+                            scope.throw_exception(exc);
+                            return 1;
+                        }
+                    }
                 };
                 Self::call(arg0)
             };
@@ -517,9 +554,18 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let fast_api_callback_options: &'s mut _ = unsafe {
                 &mut *fast_api_callback_options
             };
+            let mut scope = unsafe {
+                if !this.is_object() {
+                    fast_api_callback_options.fallback = true;
+                    return unsafe { std::mem::zeroed() };
+                }
+                deno_core::v8::CallbackScope::new(this)
+            };
             let result = {
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     fast_api_callback_options.fallback = true;
                     return unsafe { std::mem::zeroed() };
                 };
@@ -535,15 +581,17 @@ const fn op_use_cppgc_object() -> ::deno_core::_ops::OpDecl {
             let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(
                 &<Self as deno_core::_ops::Op>::DECL,
             );
+            let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
             let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info);
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
                 info,
             );
             let result = {
                 let arg0 = args.get(0usize as i32);
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
-                    let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     let msg = deno_core::v8::String::new_from_one_byte(
                             &mut scope,
                             "expected Wrap".as_bytes(),
@@ -777,9 +825,18 @@ const fn op_use_cppgc_object_option() -> ::deno_core::_ops::OpDecl {
             let fast_api_callback_options: &'s mut _ = unsafe {
                 &mut *fast_api_callback_options
             };
+            let mut scope = unsafe {
+                if !this.is_object() {
+                    fast_api_callback_options.fallback = true;
+                    return unsafe { std::mem::zeroed() };
+                }
+                deno_core::v8::CallbackScope::new(this)
+            };
             let result = {
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     fast_api_callback_options.fallback = true;
                     return unsafe { std::mem::zeroed() };
                 };
@@ -795,15 +852,17 @@ const fn op_use_cppgc_object_option() -> ::deno_core::_ops::OpDecl {
             let _reentrancy_check_guard = deno_core::_ops::reentrancy_check(
                 &<Self as deno_core::_ops::Op>::DECL,
             );
+            let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
             let mut rv = deno_core::v8::ReturnValue::from_function_callback_info(info);
             let args = deno_core::v8::FunctionCallbackArguments::from_function_callback_info(
                 info,
             );
             let result = {
                 let arg0 = args.get(0usize as i32);
-                let Some(arg0) = deno_core::_ops::try_unwrap_cppgc_object::<Wrap>(arg0)
-                else {
-                    let mut scope = unsafe { deno_core::v8::CallbackScope::new(info) };
+                let handle_ = deno_core::_ops::try_unwrap_cppgc_object::<
+                    Wrap,
+                >(&mut scope, arg0);
+                let Some(arg0) = handle_.borrow() else {
                     let msg = deno_core::v8::String::new_from_one_byte(
                             &mut scope,
                             "expected Wrap".as_bytes(),
diff --git a/ops/op2/test_cases/sync/cppgc_resource.rs b/ops/op2/test_cases/sync/cppgc_resource.rs
index d2fa98c..1fad292 100644
--- a/ops/op2/test_cases/sync/cppgc_resource.rs
+++ b/ops/op2/test_cases/sync/cppgc_resource.rs
@@ -1,11 +1,11 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 #![deny(warnings)]
 deno_ops_compile_test_runner::prelude!();
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 
 struct Wrap;
 
-impl GcResource for Wrap {}
+impl GarbageCollected for Wrap {}
 
 #[op2(fast)]
 fn op_cppgc_object(#[cppgc] _resource: &Wrap) {}
diff --git a/ops/op2/test_cases_fail/lifetimes.rs b/ops/op2/test_cases_fail/lifetimes.rs
index bafad16..adb4863 100644
--- a/ops/op2/test_cases_fail/lifetimes.rs
+++ b/ops/op2/test_cases_fail/lifetimes.rs
@@ -2,12 +2,12 @@
 #![deny(warnings)]
 deno_ops_compile_test_runner::prelude!();
 use deno_core::error::AnyError;
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 use std::future::Future;
 
 struct Wrap;
 
-impl GcResource for Wrap {}
+impl GarbageCollected for Wrap {}
 
 #[op2(fast)]
 fn op_use_cppgc_object(#[cppgc] _wrap: &'static Wrap) {}
diff --git a/ops/op2/test_cases_fail/lifetimes.stderr b/ops/op2/test_cases_fail/lifetimes.stderr
index 89fd320..f20ff7f 100644
--- a/ops/op2/test_cases_fail/lifetimes.stderr
+++ b/ops/op2/test_cases_fail/lifetimes.stderr
@@ -17,29 +17,15 @@ error: unused import: `std::future::Future`
 6 | use std::future::Future;
   |     ^^^^^^^^^^^^^^^^^^^
 
-error[E0521]: borrowed data escapes outside of associated function
+error[E0597]: `handle_` does not live long enough
   --> ../op2/test_cases_fail/lifetimes.rs:12:1
    |
 12 | #[op2(fast)]
-   | ^^^^^^^^^^^^
-   | |
-   | `arg0` is a reference that is only valid in the associated function body
-   | `arg0` escapes the associated function body here
-   | has type `Local<'1, deno_core::v8::Value>`
-   | argument requires that `'1` must outlive `'static`
-   |
-   = note: this error originates in the attribute macro `op2` (in Nightly builds, run with -Z macro-backtrace for more info)
-
-error[E0521]: borrowed data escapes outside of associated function
-  --> ../op2/test_cases_fail/lifetimes.rs:12:1
-   |
-12 | #[op2(fast)]
-   | ^^^^^^^^^^^^
-   | |
-   | `info` is a reference that is only valid in the associated function body
-   | `info` escapes the associated function body here
-   | lifetime `'s` defined here
-   | argument requires that `'s` must outlive `'static`
+   | ^^^^^^^^^^^-
+   | |          |
+   | |          `handle_` dropped here while still borrowed
+   | borrowed value does not live long enough
+   | argument requires that `handle_` is borrowed for `'static`
    |
    = note: this error originates in the attribute macro `op2` (in Nightly builds, run with -Z macro-backtrace for more info)
 
diff --git a/testing/checkin/runner/extensions.rs b/testing/checkin/runner/extensions.rs
index c8a162c..bdabaa5 100644
--- a/testing/checkin/runner/extensions.rs
+++ b/testing/checkin/runner/extensions.rs
@@ -51,7 +51,6 @@ deno_core::extension!(
   esm_entry_point = "ext:checkin_runtime/__init.js",
   esm = [
     dir "checkin/runtime",
-    "__bootstrap.js",
     "__init.js",
     "checkin:async" = "async.ts",
     "checkin:console" = "console.ts",
diff --git a/testing/checkin/runner/ops.rs b/testing/checkin/runner/ops.rs
index 631ad9a..24eb3fe 100644
--- a/testing/checkin/runner/ops.rs
+++ b/testing/checkin/runner/ops.rs
@@ -8,7 +8,7 @@ use deno_core::stats::RuntimeActivitySnapshot;
 use deno_core::stats::RuntimeActivityStats;
 use deno_core::stats::RuntimeActivityStatsFactory;
 use deno_core::stats::RuntimeActivityStatsFilter;
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 use deno_core::OpDecl;
 use deno_core::OpState;
 
@@ -73,7 +73,7 @@ pub struct Stateful {
   name: String,
 }
 
-impl GcResource for Stateful {}
+impl GarbageCollected for Stateful {}
 
 impl Stateful {
   #[op2(method(Stateful))]
diff --git a/testing/checkin/runner/ops_async.rs b/testing/checkin/runner/ops_async.rs
index 98dd8a0..672ed74 100644
--- a/testing/checkin/runner/ops_async.rs
+++ b/testing/checkin/runner/ops_async.rs
@@ -1,7 +1,7 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
 use deno_core::op2;
 use deno_core::v8;
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 use deno_core::OpState;
 use deno_core::V8TaskSpawner;
 use futures::future::poll_fn;
@@ -66,7 +66,7 @@ pub struct TestResource {
   value: u32,
 }
 
-impl GcResource for TestResource {}
+impl GarbageCollected for TestResource {}
 
 #[op2(async)]
 #[cppgc]
diff --git a/testing/checkin/runner/ops_worker.rs b/testing/checkin/runner/ops_worker.rs
index 9b6b655..3aa0715 100644
--- a/testing/checkin/runner/ops_worker.rs
+++ b/testing/checkin/runner/ops_worker.rs
@@ -4,9 +4,8 @@ use anyhow::bail;
 use anyhow::Error;
 use deno_core::op2;
 use deno_core::url::Url;
-use deno_core::v8;
 use deno_core::v8::IsolateHandle;
-use deno_core::GcResource;
+use deno_core::GarbageCollected;
 use deno_core::JsRuntime;
 use deno_core::OpState;
 use deno_core::PollEventLoopOptions;
@@ -34,7 +33,7 @@ pub struct WorkerControl {
   shutdown_flag: Option<UnboundedSender<()>>,
 }
 
-impl GcResource for WorkerControl {}
+impl GarbageCollected for WorkerControl {}
 
 pub struct WorkerChannel {
   tx: UnboundedSender<String>,
@@ -90,13 +89,13 @@ pub fn worker_create(
 }
 
 #[op2]
-pub fn op_worker_spawn<'s>(
-  scope: &mut v8::HandleScope<'s>,
+#[cppgc]
+pub fn op_worker_spawn(
   #[state] this_worker: &Worker,
   #[state] output: &Output,
   #[string] base_url: String,
   #[string] main_script: String,
-) -> Result<v8::Local<'s, v8::Object>, Error> {
+) -> Result<WorkerControl, Error> {
   let output = output.clone();
   let close_watcher = this_worker.close_watcher.clone();
   let (init_send, init_recv) = channel();
@@ -119,7 +118,7 @@ pub fn op_worker_spawn<'s>(
 
   // This is technically a blocking call
   let worker = init_recv.recv()?;
-  Ok(deno_core::cppgc::make_cppgc_object(scope, worker))
+  Ok(worker)
 }
 
 async fn run_worker_task(
@@ -171,10 +170,10 @@ pub async fn op_worker_recv(#[cppgc] worker: &WorkerControl) -> Option<String> {
 }
 
 #[op2]
-pub fn op_worker_parent<'s>(
-  scope: &mut v8::HandleScope<'s>,
+#[cppgc]
+pub fn op_worker_parent(
   state: Rc<RefCell<OpState>>,
-) -> Result<v8::Local<'s, v8::Object>, Error> {
+) -> Result<WorkerControl, Error> {
   let state = state.borrow_mut();
   let worker: &Worker = state.borrow();
   let (Some(worker_channel), Some(close_watcher)) = (
@@ -183,15 +182,12 @@ pub fn op_worker_parent<'s>(
   ) else {
     bail!("No parent worker is available")
   };
-  Ok(deno_core::cppgc::make_cppgc_object(
-    scope,
-    WorkerControl {
-      worker_channel,
-      close_watcher,
-      handle: None,
-      shutdown_flag: None,
-    },
-  ))
+  Ok(WorkerControl {
+    worker_channel,
+    close_watcher,
+    handle: None,
+    shutdown_flag: None,
+  })
 }
 
 #[op2(async)]
diff --git a/testing/checkin/runtime/__bootstrap.js b/testing/checkin/runtime/__bootstrap.js
deleted file mode 100644
index 0fb1253..0000000
--- a/testing/checkin/runtime/__bootstrap.js
+++ /dev/null
@@ -1,4 +0,0 @@
-// Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-
-// Allow 'using' to work in TypeScript
-Symbol.asyncDispose = Symbol("Symbol.asyncDispose");
diff --git a/testing/checkin/runtime/__init.js b/testing/checkin/runtime/__init.js
index f2b5b39..9b4ac1e 100644
--- a/testing/checkin/runtime/__init.js
+++ b/testing/checkin/runtime/__init.js
@@ -1,5 +1,4 @@
 // Copyright 2018-2024 the Deno authors. All rights reserved. MIT license.
-import "./__bootstrap.js";
 import * as async from "checkin:async";
 import * as console from "checkin:console";
 import * as error from "checkin:error";