Repository: denoland/deno
Pull request: fix(core): store Global<Context> ptr for libuv-compat callbacks

## Summary

- Store a `v8::Global<Context>` via `Global::into_raw()` once during `register_uv_loop` instead of storing a `v8::Local<Context>` pointer at the start of each event loop tick
- V8 keeps the persistent-handle slot updated across GC cycles, so the pointer remains valid for the lifetime of the runtime
- The raw Global is reconstructed and properly dropped in `JsRealmInner::destroy`
- Remove the per-tick `transmute` + `const` size assert that was previously needed

Ported from https://github.com/denoland/deno_core/pull/1315

Original author: @littledivy

## Test plan

- [x] `cargo check -p deno_core` passes

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Changed files:
filename: libs/core/runtime/jsrealm.rs
patch: @@ -115,9 +115,9 @@ pub struct ContextState {
   /// `UvLoopInner` and `ContextState` are `!Send` -- all access is on the
   /// event loop thread.
   pub(crate) uv_loop_inner: Cell<Option<*const UvLoopInner>>,
-  /// Raw pointer to the `uv_loop_t` handle, used to set `loop_.data`
-  /// to the current `v8::Context` at the start of each event loop tick
-  /// so that libuv-style C callbacks can retrieve the context.
+  /// Raw pointer to the `uv_loop_t` handle. At registration time,
+  /// `loop_.data` is set to a `Global::into_raw()` pointer so that
+  /// libuv-style C callbacks can retrieve the context.
   ///
   /// # Safety
   /// Same lifetime requirements as `uv_loop_inner` above.
@@ -244,6 +244,25 @@ impl JsRealmInner {
     let raw_ptr = self.state().isolate.unwrap();
     // SAFETY: We know the isolate outlives the realm
     let mut isolate = unsafe { v8::Isolate::from_raw_isolate_ptr(raw_ptr) };
+
+    if let Some(loop_ptr) = state.uv_loop_ptr.get() {
+      // SAFETY: `loop_ptr` is valid for the lifetime of the runtime
+      // (guaranteed by `register_uv_loop`). `data` was set to a
+      // `Global::into_raw()` pointer during registration. We
+      // reconstruct the Global via `from_raw` so it is properly
+      // dropped. `NonNull::new_unchecked` is safe because we checked
+      // `!is_null()`. `isolate` is valid because we just obtained it
+      // from the raw isolate pointer above.
+      unsafe {
+        let data = (*loop_ptr).data;
+        if !data.is_null() {
+          let raw = std::ptr::NonNull::new_unchecked(data as *mut v8::Context);
+          let _global = v8::Global::from_raw(&mut isolate, raw);
+          (*loop_ptr).data = std::ptr::null_mut();
+        }
+      }
+    }
+
     v8::scope!(let scope, &mut isolate);
     // These globals will prevent snapshots from completing, take them
     state.exception_state.prepare_to_destroy();

filename: libs/core/runtime/jsruntime.rs
patch: @@ -1546,8 +1546,9 @@ impl JsRuntime {
   /// (timers, I/O, idle, prepare, check, close) are driven by
   /// `poll_event_loop`.
   ///
-  /// The v8::Context pointer is stored in `loop_.data` at the start of each
-  /// event loop tick so that libuv-style callbacks can retrieve it.
+  /// A `v8::Global<v8::Context>` is created via `Global::into_raw()` and
+  /// stored in `loop_.data` so that libuv-style callbacks can retrieve
+  /// the context. The raw Global is dropped during realm cleanup.
   ///
   /// # Safety
   /// `loop_ptr` must be a valid, initialized `uv_loop_t` pointer that
@@ -1556,12 +1557,23 @@ impl JsRuntime {
     &mut self,
     loop_ptr: *mut crate::uv_compat::uv_loop_t,
   ) {
-    let context_state = &self.inner.main_realm.0.context_state;
+    let realm = &self.inner.main_realm;
+    let context_state = &realm.0.context_state;
     let inner_ptr =
       unsafe { crate::uv_compat::uv_loop_get_inner_ptr(loop_ptr) };
     let uv_inner = inner_ptr as *const crate::uv_compat::UvLoopInner;
     context_state.uv_loop_inner.set(Some(uv_inner));
     context_state.uv_loop_ptr.set(Some(loop_ptr));
+
+    let global_ctx = realm.0.context().clone();
+    let raw = global_ctx.into_raw();
+    // SAFETY: `loop_ptr` is a valid, initialized `uv_loop_t` guaranteed
+    // by the caller. `raw` is a persistent-handle slot pointer from
+    // `Global::into_raw()` — V8 keeps it updated across GC cycles.
+    // The raw Global is reconstructed and dropped in `JsRealmInner::destroy`.
+    unsafe {
+      (*loop_ptr).data = raw.as_ptr() as *mut std::ffi::c_void;
+    }
   }
 
   /// Returns the runtime's op names, ordered by OpId.
@@ -2011,24 +2023,6 @@ impl JsRuntime {
     let modules = &realm.0.module_map;
     let context_state = &realm.0.context_state;
 
-    // Set the v8::Context pointer in the uv_loop so libuv-style callbacks
-    // can retrieve it via context_from_loop().
-    if let Some(loop_ptr) = context_state.uv_loop_ptr.get() {
-      let context = scope.get_current_context();
-      // SAFETY: `v8::Local<v8::Context>` is a thin pointer (one pointer
-      // wide). We store it as `*mut c_void` in `loop_.data` for the
-      // duration of this event loop tick. Callbacks reconstruct it via
-      // `std::mem::transmute` in `context_from_loop()`. The context is
-      // alive for the entire tick because `scope` holds it.
-      const _: () = assert!(
-        std::mem::size_of::<v8::Local<v8::Context>>()
-          == std::mem::size_of::<*mut std::ffi::c_void>()
-      );
-      unsafe {
-        let ctx_ptr: *mut std::ffi::c_void = std::mem::transmute(context);
-        (*loop_ptr).data = ctx_ptr;
-      }
-    }
     let exception_state = &context_state.exception_state;
 
     // Tight I/O loop: when run_io does work, re-run I/O phases immediately