Skip to content

Commit 51e35dd

Browse files
littledivyclaude
authored andcommitted
fix(core): store Global<Context> ptr for libuv-compat callbacks
Instead of storing a `v8::Local<Context>` pointer in `loop_.data` at the start of each event loop tick (which is only valid for that tick), store a `v8::Global<Context>` via `Global::into_raw()` once during `register_uv_loop`. 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 dropped in `JsRealmInner::destroy`. Ported from denoland/deno_core#1315 Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
1 parent ad52a9f commit 51e35dd

2 files changed

Lines changed: 37 additions & 24 deletions

File tree

libs/core/runtime/jsrealm.rs

Lines changed: 22 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -115,9 +115,9 @@ pub struct ContextState {
115115
/// `UvLoopInner` and `ContextState` are `!Send` -- all access is on the
116116
/// event loop thread.
117117
pub(crate) uv_loop_inner: Cell<Option<*const UvLoopInner>>,
118-
/// Raw pointer to the `uv_loop_t` handle, used to set `loop_.data`
119-
/// to the current `v8::Context` at the start of each event loop tick
120-
/// so that libuv-style C callbacks can retrieve the context.
118+
/// Raw pointer to the `uv_loop_t` handle. At registration time,
119+
/// `loop_.data` is set to a `Global::into_raw()` pointer so that
120+
/// libuv-style C callbacks can retrieve the context.
121121
///
122122
/// # Safety
123123
/// Same lifetime requirements as `uv_loop_inner` above.
@@ -244,6 +244,25 @@ impl JsRealmInner {
244244
let raw_ptr = self.state().isolate.unwrap();
245245
// SAFETY: We know the isolate outlives the realm
246246
let mut isolate = unsafe { v8::Isolate::from_raw_isolate_ptr(raw_ptr) };
247+
248+
if let Some(loop_ptr) = state.uv_loop_ptr.get() {
249+
// SAFETY: `loop_ptr` is valid for the lifetime of the runtime
250+
// (guaranteed by `register_uv_loop`). `data` was set to a
251+
// `Global::into_raw()` pointer during registration. We
252+
// reconstruct the Global via `from_raw` so it is properly
253+
// dropped. `NonNull::new_unchecked` is safe because we checked
254+
// `!is_null()`. `isolate` is valid because we just obtained it
255+
// from the raw isolate pointer above.
256+
unsafe {
257+
let data = (*loop_ptr).data;
258+
if !data.is_null() {
259+
let raw = std::ptr::NonNull::new_unchecked(data as *mut v8::Context);
260+
let _global = v8::Global::from_raw(&mut isolate, raw);
261+
(*loop_ptr).data = std::ptr::null_mut();
262+
}
263+
}
264+
}
265+
247266
v8::scope!(let scope, &mut isolate);
248267
// These globals will prevent snapshots from completing, take them
249268
state.exception_state.prepare_to_destroy();

libs/core/runtime/jsruntime.rs

Lines changed: 15 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -1546,8 +1546,9 @@ impl JsRuntime {
15461546
/// (timers, I/O, idle, prepare, check, close) are driven by
15471547
/// `poll_event_loop`.
15481548
///
1549-
/// The v8::Context pointer is stored in `loop_.data` at the start of each
1550-
/// event loop tick so that libuv-style callbacks can retrieve it.
1549+
/// A `v8::Global<v8::Context>` is created via `Global::into_raw()` and
1550+
/// stored in `loop_.data` so that libuv-style callbacks can retrieve
1551+
/// the context. The raw Global is dropped during realm cleanup.
15511552
///
15521553
/// # Safety
15531554
/// `loop_ptr` must be a valid, initialized `uv_loop_t` pointer that
@@ -1556,12 +1557,23 @@ impl JsRuntime {
15561557
&mut self,
15571558
loop_ptr: *mut crate::uv_compat::uv_loop_t,
15581559
) {
1559-
let context_state = &self.inner.main_realm.0.context_state;
1560+
let realm = &self.inner.main_realm;
1561+
let context_state = &realm.0.context_state;
15601562
let inner_ptr =
15611563
unsafe { crate::uv_compat::uv_loop_get_inner_ptr(loop_ptr) };
15621564
let uv_inner = inner_ptr as *const crate::uv_compat::UvLoopInner;
15631565
context_state.uv_loop_inner.set(Some(uv_inner));
15641566
context_state.uv_loop_ptr.set(Some(loop_ptr));
1567+
1568+
let global_ctx = realm.0.context().clone();
1569+
let raw = global_ctx.into_raw();
1570+
// SAFETY: `loop_ptr` is a valid, initialized `uv_loop_t` guaranteed
1571+
// by the caller. `raw` is a persistent-handle slot pointer from
1572+
// `Global::into_raw()` — V8 keeps it updated across GC cycles.
1573+
// The raw Global is reconstructed and dropped in `JsRealmInner::destroy`.
1574+
unsafe {
1575+
(*loop_ptr).data = raw.as_ptr() as *mut std::ffi::c_void;
1576+
}
15651577
}
15661578

15671579
/// Returns the runtime's op names, ordered by OpId.
@@ -2011,24 +2023,6 @@ impl JsRuntime {
20112023
let modules = &realm.0.module_map;
20122024
let context_state = &realm.0.context_state;
20132025

2014-
// Set the v8::Context pointer in the uv_loop so libuv-style callbacks
2015-
// can retrieve it via context_from_loop().
2016-
if let Some(loop_ptr) = context_state.uv_loop_ptr.get() {
2017-
let context = scope.get_current_context();
2018-
// SAFETY: `v8::Local<v8::Context>` is a thin pointer (one pointer
2019-
// wide). We store it as `*mut c_void` in `loop_.data` for the
2020-
// duration of this event loop tick. Callbacks reconstruct it via
2021-
// `std::mem::transmute` in `context_from_loop()`. The context is
2022-
// alive for the entire tick because `scope` holds it.
2023-
const _: () = assert!(
2024-
std::mem::size_of::<v8::Local<v8::Context>>()
2025-
== std::mem::size_of::<*mut std::ffi::c_void>()
2026-
);
2027-
unsafe {
2028-
let ctx_ptr: *mut std::ffi::c_void = std::mem::transmute(context);
2029-
(*loop_ptr).data = ctx_ptr;
2030-
}
2031-
}
20322026
let exception_state = &context_state.exception_state;
20332027

20342028
// Tight I/O loop: when run_io does work, re-run I/O phases immediately

0 commit comments

Comments
 (0)