Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/binding.cc
Original file line number Diff line number Diff line change
Expand Up @@ -169,6 +169,18 @@ void v8__Isolate__Enter(v8::Isolate* isolate) { isolate->Enter(); }

void v8__Isolate__Exit(v8::Isolate* isolate) { isolate->Exit(); }

void v8__Locker__CONSTRUCT(uninit_t<v8::Locker>* buf, v8::Isolate* isolate) {
construct_in_place<v8::Locker>(buf, isolate);
}

void v8__Locker__DESTRUCT(v8::Locker* self) { self->~Locker(); }

bool v8__Locker__IsLocked(v8::Isolate* isolate) {
return v8::Locker::IsLocked(isolate);
}

size_t v8__Locker__SIZE() { return sizeof(v8::Locker); }

v8::Isolate* v8__Isolate__GetCurrent() { return v8::Isolate::GetCurrent(); }

const v8::Data* v8__Isolate__GetCurrentHostDefinedOptions(
Expand Down
173 changes: 173 additions & 0 deletions src/isolate.rs
Original file line number Diff line number Diff line change
Expand Up @@ -931,6 +931,14 @@ impl Isolate {
OwnedIsolate::new(Self::new_impl(params))
}

/// Creates an isolate for use with `v8::Locker` in multi-threaded scenarios.
///
/// Unlike `Isolate::new()`, this does not automatically enter the isolate.
#[allow(clippy::new_ret_no_self)]
pub fn new_unentered(params: CreateParams) -> UnenteredIsolate {
UnenteredIsolate::new(Self::new_impl(params))
}

#[allow(clippy::new_ret_no_self)]
pub fn snapshot_creator(
external_references: Option<Cow<'static, [ExternalReference]>>,
Expand Down Expand Up @@ -2155,6 +2163,91 @@ impl AsMut<Isolate> for Isolate {
}
}

/// An isolate that must be accessed via [`Locker`].
///
/// Unlike [`OwnedIsolate`], this isolate does not automatically enter itself
/// upon creation. Instead, you must use a [`Locker`] to access it:
///
/// ```ignore
/// let mut isolate = v8::Isolate::new_unentered(Default::default());
///
/// // Access the isolate through a Locker
/// {
/// let mut locker = v8::Locker::new(&mut isolate);
/// let scope = &mut v8::HandleScope::new(&mut *locker);
/// // ... use scope ...
/// }
///
/// // The locker is dropped, isolate can be used from another thread
/// ```
///
/// # Thread Safety
///
/// `UnenteredIsolate` implements `Send`, meaning it can be transferred between
/// threads. However, V8 isolates are not thread-safe by themselves. You must:
///
/// 1. Only access the isolate through a [`Locker`]
/// 2. Never have multiple `Locker`s for the same isolate simultaneously
/// (V8 will block if you try)
///
/// # Dropping
///
/// When dropped, the isolate will be properly disposed. The drop will panic
/// if a [`Locker`] is currently held for this isolate.
#[derive(Debug)]
pub struct UnenteredIsolate {
cxx_isolate: NonNull<RealIsolate>,
}

impl UnenteredIsolate {
pub(crate) fn new(cxx_isolate: *mut RealIsolate) -> Self {
Self {
cxx_isolate: NonNull::new(cxx_isolate).unwrap(),
}
}

/// Returns the raw pointer to the underlying V8 isolate.
///
/// # Safety
///
/// The returned pointer is only valid while this `UnenteredIsolate` exists
/// and should only be used while a [`Locker`] is held.
#[inline]
pub fn as_raw(&self) -> *mut RealIsolate {
self.cxx_isolate.as_ptr()
}
}

impl Drop for UnenteredIsolate {
fn drop(&mut self) {
// Safety check: ensure no Locker is held
debug_assert!(
!crate::scope::raw::Locker::is_locked(self.cxx_isolate),
"Cannot drop UnenteredIsolate while a Locker is held. \
Drop the Locker first."
);

unsafe {
let isolate = Isolate::from_raw_ref_mut(&mut self.cxx_isolate);
let snapshot_creator =
isolate.get_annex_mut().maybe_snapshot_creator.take();
assert!(
snapshot_creator.is_none(),
"v8::UnenteredIsolate::create_blob must be called before dropping"
);
isolate.dispose_annex();
Platform::notify_isolate_shutdown(&get_current_platform(), isolate);
isolate.dispose();
}
}
}

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

/// Collection of V8 heap information.
///
/// Instances of this class can be passed to v8::Isolate::GetHeapStatistics to
Expand Down Expand Up @@ -2474,3 +2567,83 @@ impl AsRef<Isolate> for Isolate {
self
}
}

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

impl<'a> Locker<'a> {
/// Creates a new `Locker` for the given isolate.
///
/// This will:
/// 1. Acquire the V8 lock (via `v8::Locker`)
/// 2. Enter the isolate (via `v8::Isolate::Enter()`)
///
/// When the `Locker` is dropped, the isolate is exited and the lock is released.
///
/// The ordering is critical: we must hold the lock before calling Enter(),
/// because Enter() modifies V8's entry_stack_ which is not thread-safe.
pub fn new(isolate: &'a mut UnenteredIsolate) -> Self {
let isolate_ptr = isolate.cxx_isolate;

// Acquire the lock first (must hold lock before touching entry_stack_)
let mut raw = unsafe { crate::scope::raw::Locker::uninit() };
unsafe { raw.init(isolate_ptr) };

// Now enter the isolate (safe because we hold the lock)
unsafe {
v8__Isolate__Enter(isolate_ptr.as_ptr());
}

Self {
raw: std::mem::ManuallyDrop::new(raw),
isolate,
}
}

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

impl Drop for Locker<'_> {
fn drop(&mut self) {
unsafe {
// Exit first (while we still hold the lock), then release the lock.
// Reverse order of new(): Lock -> Enter, so drop: Exit -> Unlock.
v8__Isolate__Exit(self.isolate.cxx_isolate.as_ptr());
std::mem::ManuallyDrop::drop(&mut self.raw);
}
}
}

impl Deref for Locker<'_> {
type Target = Isolate;
fn deref(&self) -> &Self::Target {
unsafe { Isolate::from_raw_ref(&self.isolate.cxx_isolate) }
}
}

impl DerefMut for Locker<'_> {
fn deref_mut(&mut self) -> &mut Self::Target {
unsafe { Isolate::from_raw_ref_mut(&mut self.isolate.cxx_isolate) }
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -115,6 +115,7 @@ pub use isolate::HostImportModuleWithPhaseDynamicallyCallback;
pub use isolate::HostInitializeImportMetaObjectCallback;
pub use isolate::Isolate;
pub use isolate::IsolateHandle;
pub use isolate::Locker;
pub use isolate::MemoryPressureLevel;
pub use isolate::MessageCallback;
pub use isolate::MessageErrorLevel;
Expand All @@ -129,6 +130,7 @@ pub use isolate::PromiseHookType;
pub use isolate::PromiseRejectCallback;
pub use isolate::RealIsolate;
pub use isolate::TimeZoneDetection;
pub use isolate::UnenteredIsolate;
pub use isolate::UseCounterCallback;
pub use isolate::UseCounterFeature;
pub use isolate::WasmAsyncSuccess;
Expand Down
30 changes: 26 additions & 4 deletions src/scope.rs
Original file line number Diff line number Diff line change
Expand Up @@ -127,9 +127,9 @@
//! So in `ContextScope<'b, 's>`, `'b` is the lifetime of the borrow of the inner scope, and `'s` is the lifetime of the inner scope (and therefore the handles).
use crate::{
Context, Data, DataError, Function, FunctionCallbackInfo, Isolate, Local,
Message, Object, OwnedIsolate, PromiseRejectMessage, PropertyCallbackInfo,
SealedLocal, Value, fast_api::FastApiCallbackOptions, isolate::RealIsolate,
support::assert_layout_subset,
Locker, Message, Object, OwnedIsolate, PromiseRejectMessage,
PropertyCallbackInfo, SealedLocal, Value, fast_api::FastApiCallbackOptions,
isolate::RealIsolate, support::assert_layout_subset,
};
use std::{
any::type_name,
Expand Down Expand Up @@ -279,7 +279,7 @@ mod get_isolate {
pub(crate) use get_isolate::GetIsolate;

mod get_isolate_impls {
use crate::{Promise, PromiseRejectMessage};
use crate::{Locker, Promise, PromiseRejectMessage};

use super::*;
impl GetIsolate for Isolate {
Expand All @@ -294,6 +294,14 @@ mod get_isolate_impls {
}
}

impl GetIsolate for Locker<'_> {
fn get_isolate_ptr(&self) -> *mut RealIsolate {
// Locker derefs to Isolate, which has as_real_ptr()
use std::ops::Deref;
self.deref().as_real_ptr()
}
}

impl GetIsolate for FunctionCallbackInfo {
fn get_isolate_ptr(&self) -> *mut RealIsolate {
self.get_isolate_ptr()
Expand Down Expand Up @@ -442,6 +450,20 @@ impl<'s> NewHandleScope<'s> for OwnedIsolate {
}
}

impl<'s, 'a: 's> NewHandleScope<'s> for Locker<'a> {
type NewScope = HandleScope<'s, ()>;

fn make_new_scope(me: &'s mut Self) -> Self::NewScope {
HandleScope {
raw_handle_scope: unsafe { raw::HandleScope::uninit() },
isolate: unsafe { NonNull::new_unchecked(me.get_isolate_ptr()) },
context: Cell::new(None),
_phantom: PhantomData,
_pinned: PhantomPinned,
}
}
}

impl<'s, 'p: 's, 'i, C> NewHandleScope<'s>
for PinnedRef<'p, CallbackScope<'i, C>>
{
Expand Down
98 changes: 98 additions & 0 deletions src/scope/raw.rs
Original file line number Diff line number Diff line change
Expand Up @@ -219,6 +219,94 @@ impl Drop for AllowJavascriptExecutionScope {
}
}

/// Raw V8 Locker binding.
///
/// This is a low-level wrapper around `v8::Locker`. It must be used with
/// proper two-phase initialization: first call `uninit()`, then `init()`.
///
/// # Memory Layout
///
/// This struct is `#[repr(C)]` and sized to match `v8::Locker` exactly
/// (verified by the `locker_size_matches_v8` test). The size is 2 * sizeof(usize)
/// which equals 16 bytes on 64-bit platforms.
///
/// # Safety Invariants
///
/// 1. **Initialization**: After calling `uninit()`, you MUST call `init()` before
/// the `Locker` is dropped. Dropping an uninitialized `Locker` is undefined
/// behavior because `Drop` will call the C++ destructor on garbage data.
///
/// 2. **Isolate Pointer**: The isolate pointer passed to `init()` must be valid.
///
/// 3. **Single Initialization**: `init()` must be called exactly once. Calling it
/// multiple times is undefined behavior.
///
/// 4. **Thread Affinity**: Once initialized, the `Locker` must be used and dropped
/// on the same thread where it was created.
#[repr(C)]
#[derive(Debug)]
pub(crate) struct Locker([MaybeUninit<usize>; 2]);

#[test]
fn locker_size_matches_v8() {
assert_eq!(
std::mem::size_of::<Locker>(),
unsafe { v8__Locker__SIZE() },
"Locker size mismatch"
);
}

impl Locker {
/// Creates an uninitialized `Locker`.
///
/// # Safety
///
/// The returned `Locker` is in an invalid state. You MUST call `init()` before:
/// - Using the `Locker` in any way
/// - Dropping the `Locker` (including via panic unwinding)
///
/// Failure to initialize before drop will cause undefined behavior because
/// `Drop::drop` will call the C++ destructor on uninitialized memory.
#[inline]
pub unsafe fn uninit() -> Self {
Self(unsafe { MaybeUninit::uninit().assume_init() })
}

/// Initializes the `Locker` for the given isolate.
///
/// # Safety
///
/// - This must be called exactly once after `uninit()`
/// - The isolate pointer must be valid
/// - The isolate must not be locked by another `Locker`
/// - After this call, the `Locker` owns the V8 lock until dropped
#[inline]
pub unsafe fn init(&mut self, isolate: NonNull<RealIsolate>) {
let buf = NonNull::from(self).cast();
unsafe { v8__Locker__CONSTRUCT(buf.as_ptr(), isolate.as_ptr()) };
}

/// Returns `true` if the given isolate is currently locked by any `Locker`.
///
/// This is safe to call from any thread.
pub fn is_locked(isolate: NonNull<RealIsolate>) -> bool {
unsafe { v8__Locker__IsLocked(isolate.as_ptr()) }
}
}

impl Drop for Locker {
/// Releases the V8 lock.
///
/// # Safety (internal)
///
/// This assumes the `Locker` was properly initialized via `init()`.
/// Dropping an uninitialized `Locker` is undefined behavior.
#[inline(always)]
fn drop(&mut self) {
unsafe { v8__Locker__DESTRUCT(self) };
}
}

unsafe extern "C" {
pub(super) fn v8__Isolate__GetCurrent() -> *mut RealIsolate;
pub(super) fn v8__Isolate__GetCurrentContext(
Expand Down Expand Up @@ -311,4 +399,14 @@ unsafe extern "C" {
pub(super) fn v8__AllowJavascriptExecutionScope__DESTRUCT(
this: *mut AllowJavascriptExecutionScope,
);

pub(super) fn v8__Locker__CONSTRUCT(
buf: *mut MaybeUninit<Locker>,
isolate: *mut RealIsolate,
);
pub(super) fn v8__Locker__DESTRUCT(this: *mut Locker);
pub(super) fn v8__Locker__IsLocked(isolate: *mut RealIsolate) -> bool;

#[cfg(test)]
fn v8__Locker__SIZE() -> usize;
}
Loading
Loading