Skip to content

Commit 8ea0452

Browse files
RainyPixelmax-lt
authored andcommitted
fix(locker): add panic safety and improve documentation
- Add IsolateExitGuard to ensure v8::Isolate::Exit is called if panic occurs during Locker::new() after Enter has been called - Add comprehensive documentation for Locker and UnenteredIsolate including thread safety guarantees and usage examples - Add debug_assert to verify no Locker is held when dropping UnenteredIsolate - Add UnenteredIsolate::as_raw() method for low-level access - Expand test coverage: - locker_state_preserved_across_locks - locker_drop_releases_lock - unentered_isolate_as_raw - locker_send_isolate_between_threads (verifies Send trait works) This improves safety guarantees for multi-threaded isolate usage.
1 parent 2169240 commit 8ea0452

File tree

2 files changed

+240
-2
lines changed

2 files changed

+240
-2
lines changed

src/isolate.rs

Lines changed: 101 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2126,7 +2126,37 @@ impl AsMut<Isolate> for Isolate {
21262126
}
21272127
}
21282128

2129-
/// An isolate that must be accessed via `Locker`. Does not auto-enter.
2129+
/// An isolate that must be accessed via [`Locker`].
2130+
///
2131+
/// Unlike [`OwnedIsolate`], this isolate does not automatically enter itself
2132+
/// upon creation. Instead, you must use a [`Locker`] to access it:
2133+
///
2134+
/// ```ignore
2135+
/// let mut isolate = v8::Isolate::new_unentered(Default::default());
2136+
///
2137+
/// // Access the isolate through a Locker
2138+
/// {
2139+
/// let mut locker = v8::Locker::new(&mut isolate);
2140+
/// let scope = &mut v8::HandleScope::new(&mut *locker);
2141+
/// // ... use scope ...
2142+
/// }
2143+
///
2144+
/// // The locker is dropped, isolate can be used from another thread
2145+
/// ```
2146+
///
2147+
/// # Thread Safety
2148+
///
2149+
/// `UnenteredIsolate` implements `Send`, meaning it can be transferred between
2150+
/// threads. However, V8 isolates are not thread-safe by themselves. You must:
2151+
///
2152+
/// 1. Only access the isolate through a [`Locker`]
2153+
/// 2. Never have multiple `Locker`s for the same isolate simultaneously
2154+
/// (V8 will block if you try)
2155+
///
2156+
/// # Dropping
2157+
///
2158+
/// When dropped, the isolate will be properly disposed. The drop will panic
2159+
/// if a [`Locker`] is currently held for this isolate.
21302160
#[derive(Debug)]
21312161
pub struct UnenteredIsolate {
21322162
cxx_isolate: NonNull<RealIsolate>,
@@ -2138,10 +2168,28 @@ impl UnenteredIsolate {
21382168
cxx_isolate: NonNull::new(cxx_isolate).unwrap(),
21392169
}
21402170
}
2171+
2172+
/// Returns the raw pointer to the underlying V8 isolate.
2173+
///
2174+
/// # Safety
2175+
///
2176+
/// The returned pointer is only valid while this `UnenteredIsolate` exists
2177+
/// and should only be used while a [`Locker`] is held.
2178+
#[inline]
2179+
pub fn as_raw(&self) -> *mut RealIsolate {
2180+
self.cxx_isolate.as_ptr()
2181+
}
21412182
}
21422183

21432184
impl Drop for UnenteredIsolate {
21442185
fn drop(&mut self) {
2186+
// Safety check: ensure no Locker is held
2187+
debug_assert!(
2188+
!crate::scope::raw::Locker::is_locked(self.cxx_isolate),
2189+
"Cannot drop UnenteredIsolate while a Locker is held. \
2190+
Drop the Locker first."
2191+
);
2192+
21452193
unsafe {
21462194
let isolate = Isolate::from_raw_ref_mut(&mut self.cxx_isolate);
21472195
let snapshot_creator =
@@ -2157,7 +2205,10 @@ impl Drop for UnenteredIsolate {
21572205
}
21582206
}
21592207

2160-
// Thread safety ensured by Locker.
2208+
// SAFETY: UnenteredIsolate can be sent between threads because:
2209+
// 1. The underlying V8 isolate is not accessed directly - all access goes through Locker
2210+
// 2. Locker ensures proper synchronization when accessing the isolate
2211+
// 3. V8's Locker internally uses a mutex to prevent concurrent access
21612212
unsafe impl Send for UnenteredIsolate {}
21622213

21632214
/// Collection of V8 heap information.
@@ -2461,25 +2512,73 @@ impl AsRef<Isolate> for Isolate {
24612512
}
24622513

24632514
/// Locks an isolate and enters it for the current thread.
2515+
///
2516+
/// This is a RAII wrapper around V8's `v8::Locker`. It ensures that the isolate
2517+
/// is properly locked before any V8 operations and unlocked when dropped.
2518+
///
2519+
/// # Thread Safety
2520+
///
2521+
/// `Locker` does not implement `Send` or `Sync`. Once created, it must be used
2522+
/// only on the thread where it was created. The underlying `UnenteredIsolate`
2523+
/// implements `Send`, allowing it to be transferred between threads, but a new
2524+
/// `Locker` must be created on each thread that needs to access the isolate.
2525+
///
2526+
/// # Panic Safety
2527+
///
2528+
/// `Locker::new()` is panic-safe. If a panic occurs during construction,
2529+
/// the isolate will be properly exited via a drop guard.
24642530
pub struct Locker<'a> {
24652531
raw: std::mem::ManuallyDrop<crate::scope::raw::Locker>,
24662532
isolate: &'a mut UnenteredIsolate,
24672533
}
24682534

2535+
/// Guard to ensure `v8__Isolate__Exit` is called if panic occurs after Enter.
2536+
struct IsolateExitGuard(*mut RealIsolate);
2537+
2538+
impl Drop for IsolateExitGuard {
2539+
fn drop(&mut self) {
2540+
unsafe { v8__Isolate__Exit(self.0) };
2541+
}
2542+
}
2543+
24692544
impl<'a> Locker<'a> {
2545+
/// Creates a new `Locker` for the given isolate.
2546+
///
2547+
/// This will:
2548+
/// 1. Enter the isolate (via `v8::Isolate::Enter()`)
2549+
/// 2. Acquire the V8 lock (via `v8::Locker`)
2550+
///
2551+
/// When the `Locker` is dropped, the lock is released and the isolate is exited.
2552+
///
2553+
/// # Panics
2554+
///
2555+
/// This function is panic-safe. If initialization fails, the isolate will be
2556+
/// properly exited.
24702557
pub fn new(isolate: &'a mut UnenteredIsolate) -> Self {
24712558
let isolate_ptr = isolate.cxx_isolate;
2559+
2560+
// Enter the isolate first
24722561
unsafe {
24732562
v8__Isolate__Enter(isolate_ptr.as_ptr());
24742563
}
2564+
2565+
// Create exit guard - will call Exit if we panic before completing
2566+
let exit_guard = IsolateExitGuard(isolate_ptr.as_ptr());
2567+
2568+
// Initialize the raw Locker
24752569
let mut raw = unsafe { crate::scope::raw::Locker::uninit() };
24762570
unsafe { raw.init(isolate_ptr) };
2571+
2572+
// Success - forget the guard so it doesn't call Exit
2573+
std::mem::forget(exit_guard);
2574+
24772575
Self {
24782576
raw: std::mem::ManuallyDrop::new(raw),
24792577
isolate,
24802578
}
24812579
}
24822580

2581+
/// Returns `true` if the given isolate is currently locked by any `Locker`.
24832582
pub fn is_locked(isolate: &UnenteredIsolate) -> bool {
24842583
crate::scope::raw::Locker::is_locked(isolate.cxx_isolate)
24852584
}

tests/test_locker.rs

Lines changed: 139 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,4 +1,6 @@
11
use std::pin::pin;
2+
use std::sync::mpsc;
3+
use std::thread;
24

35
#[test]
46
fn locker_basic() {
@@ -81,10 +83,147 @@ fn locker_is_locked() {
8183
assert!(!v8::Locker::is_locked(&isolate));
8284
{
8385
let _locker = v8::Locker::new(&mut isolate);
86+
// Locker is now held - but we can't call is_locked because we have &mut isolate
87+
// The lock check happens internally
8488
}
8589
assert!(!v8::Locker::is_locked(&isolate));
8690
}
8791

92+
#[test]
93+
fn locker_state_preserved_across_locks() {
94+
let _setup_guard = setup();
95+
let mut isolate = v8::Isolate::new_unentered(Default::default());
96+
97+
// First lock: create a global and set a value
98+
let global_template = {
99+
let mut locker = v8::Locker::new(&mut isolate);
100+
let scope = pin!(v8::HandleScope::new(&mut *locker));
101+
let scope = &mut scope.init();
102+
let context = v8::Context::new(scope, Default::default());
103+
let scope = &mut v8::ContextScope::new(scope, context);
104+
105+
// Set a global variable
106+
let code = v8::String::new(scope, "globalThis.testValue = 123").unwrap();
107+
let script = v8::Script::compile(scope, code, None).unwrap();
108+
script.run(scope).unwrap();
109+
110+
// Get the global object template for later verification
111+
context.global(scope)
112+
};
113+
drop(global_template);
114+
115+
// Second lock: verify state is preserved (new context won't have the value)
116+
{
117+
let mut locker = v8::Locker::new(&mut isolate);
118+
let scope = pin!(v8::HandleScope::new(&mut *locker));
119+
let scope = &mut scope.init();
120+
let context = v8::Context::new(scope, Default::default());
121+
let scope = &mut v8::ContextScope::new(scope, context);
122+
123+
// New context - value should not exist
124+
let code = v8::String::new(scope, "typeof globalThis.testValue").unwrap();
125+
let script = v8::Script::compile(scope, code, None).unwrap();
126+
let result = script.run(scope).unwrap();
127+
let result_str = result.to_rust_string_lossy(scope);
128+
assert_eq!(result_str, "undefined");
129+
}
130+
}
131+
132+
#[test]
133+
fn locker_drop_releases_lock() {
134+
let _setup_guard = setup();
135+
let mut isolate = v8::Isolate::new_unentered(Default::default());
136+
137+
// Create and immediately drop a locker
138+
{
139+
let locker = v8::Locker::new(&mut isolate);
140+
drop(locker);
141+
}
142+
143+
// Should be able to create another locker without blocking
144+
{
145+
let _locker = v8::Locker::new(&mut isolate);
146+
}
147+
148+
// Isolate should be unlocked now
149+
assert!(!v8::Locker::is_locked(&isolate));
150+
}
151+
152+
#[test]
153+
fn unentered_isolate_as_raw() {
154+
let _setup_guard = setup();
155+
let isolate = v8::Isolate::new_unentered(Default::default());
156+
157+
// as_raw should return a valid pointer
158+
let ptr = isolate.as_raw();
159+
assert!(!ptr.is_null());
160+
}
161+
162+
#[test]
163+
fn locker_send_isolate_between_threads() {
164+
let _setup_guard = setup();
165+
166+
// Create isolate on main thread
167+
let mut isolate = v8::Isolate::new_unentered(Default::default());
168+
169+
// Use on main thread first
170+
{
171+
let mut locker = v8::Locker::new(&mut isolate);
172+
let scope = pin!(v8::HandleScope::new(&mut *locker));
173+
let scope = &mut scope.init();
174+
let context = v8::Context::new(scope, Default::default());
175+
let scope = &mut v8::ContextScope::new(scope, context);
176+
177+
let code = v8::String::new(scope, "1 + 1").unwrap();
178+
let script = v8::Script::compile(scope, code, None).unwrap();
179+
let result = script.run(scope).unwrap();
180+
assert_eq!(result.to_integer(scope).unwrap().value(), 2);
181+
}
182+
183+
// Send to another thread
184+
let (tx, rx) = mpsc::channel();
185+
186+
let handle = thread::spawn(move || {
187+
// Use isolate on worker thread
188+
let mut locker = v8::Locker::new(&mut isolate);
189+
let scope = pin!(v8::HandleScope::new(&mut *locker));
190+
let scope = &mut scope.init();
191+
let context = v8::Context::new(scope, Default::default());
192+
let scope = &mut v8::ContextScope::new(scope, context);
193+
194+
let code = v8::String::new(scope, "2 + 2").unwrap();
195+
let script = v8::Script::compile(scope, code, None).unwrap();
196+
let result = script.run(scope).unwrap();
197+
let value = result.to_integer(scope).unwrap().value();
198+
199+
// Send result back
200+
tx.send(value).unwrap();
201+
202+
// Return isolate ownership
203+
drop(locker);
204+
isolate
205+
});
206+
207+
// Wait for result
208+
let result = rx.recv().unwrap();
209+
assert_eq!(result, 4);
210+
211+
// Get isolate back and use again on main thread
212+
let mut isolate = handle.join().unwrap();
213+
{
214+
let mut locker = v8::Locker::new(&mut isolate);
215+
let scope = pin!(v8::HandleScope::new(&mut *locker));
216+
let scope = &mut scope.init();
217+
let context = v8::Context::new(scope, Default::default());
218+
let scope = &mut v8::ContextScope::new(scope, context);
219+
220+
let code = v8::String::new(scope, "3 + 3").unwrap();
221+
let script = v8::Script::compile(scope, code, None).unwrap();
222+
let result = script.run(scope).unwrap();
223+
assert_eq!(result.to_integer(scope).unwrap().value(), 6);
224+
}
225+
}
226+
88227
fn setup() -> impl Drop {
89228
use std::sync::Once;
90229
static INIT: Once = Once::new();

0 commit comments

Comments
 (0)