diff --git a/src/binding.cc b/src/binding.cc index 88764e04..959cf20a 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -354,11 +354,35 @@ const v8::Data* v8__Global__New(v8::Isolate* isolate, const v8::Data& other) { return make_pod(std::move(global)); } +const v8::Data* v8__Global__NewWeak( + v8::Isolate* isolate, const v8::Data& other, void* parameter, + v8::WeakCallbackInfo::Callback callback) { + auto global = v8::Global(isolate, ptr_to_local(&other)); + global.SetWeak(parameter, callback, v8::WeakCallbackType::kParameter); + return make_pod(std::move(global)); +} + void v8__Global__Reset(const v8::Data* data) { auto global = ptr_to_global(data); global.Reset(); } +v8::Isolate* v8__WeakCallbackInfo__GetIsolate( + const v8::WeakCallbackInfo* self) { + return self->GetIsolate(); +} + +void* v8__WeakCallbackInfo__GetParameter( + const v8::WeakCallbackInfo* self) { + return self->GetParameter(); +} + +void v8__WeakCallbackInfo__SetSecondPassCallback( + const v8::WeakCallbackInfo* self, + v8::WeakCallbackInfo::Callback callback) { + self->SetSecondPassCallback(callback); +} + void v8__ScriptCompiler__Source__CONSTRUCT( uninit_t* buf, const v8::String& source_string, const v8::ScriptOrigin* origin, diff --git a/src/handle.rs b/src/handle.rs index 8bc5f135..30917b84 100644 --- a/src/handle.rs +++ b/src/handle.rs @@ -1,4 +1,5 @@ use std::borrow::Borrow; +use std::cell::Cell; use std::hash::Hash; use std::hash::Hasher; use std::marker::PhantomData; @@ -7,6 +8,9 @@ use std::mem::transmute; use std::ops::Deref; use std::ptr::NonNull; +use libc::c_void; + +use crate::support::Opaque; use crate::Data; use crate::HandleScope; use crate::Isolate; @@ -15,7 +19,23 @@ use crate::IsolateHandle; extern "C" { fn v8__Local__New(isolate: *mut Isolate, other: *const Data) -> *const Data; fn v8__Global__New(isolate: *mut Isolate, data: *const Data) -> *const Data; + fn v8__Global__NewWeak( + isolate: *mut Isolate, + data: *const Data, + parameter: *const c_void, + callback: extern "C" fn(*const WeakCallbackInfo), + ) -> *const Data; fn v8__Global__Reset(data: *const Data); + fn v8__WeakCallbackInfo__GetIsolate( + this: *const WeakCallbackInfo, + ) -> *mut Isolate; + fn v8__WeakCallbackInfo__GetParameter( + this: *const WeakCallbackInfo, + ) -> *mut c_void; + fn v8__WeakCallbackInfo__SetSecondPassCallback( + this: *const WeakCallbackInfo, + callback: extern "C" fn(*const WeakCallbackInfo), + ); } /// An object reference managed by the v8 garbage collector. @@ -437,3 +457,419 @@ impl HandleHost { unsafe { self.get_isolate().as_ref() }.thread_safe_handle() } } + +/// An object reference that does not prevent garbage collection for the object, +/// and which allows installing finalization callbacks which will be called +/// after the object has been GC'd. +/// +/// Note that finalization callbacks are tied to the lifetime of a `Weak`, +/// and will not be called after the `Weak` is dropped. +/// +/// # `Clone` +/// +/// Since finalization callbacks are specific to a `Weak` instance, cloning +/// will create a new object reference without a finalizer, as if created by +/// [`Self::new`]. You can use [`Self::clone_with_finalizer`] to attach a +/// finalization callback to the clone. +#[derive(Debug)] +pub struct Weak { + data: Option>>, + isolate_handle: IsolateHandle, +} + +impl Weak { + pub fn new(isolate: &mut Isolate, handle: impl Handle) -> Self { + let HandleInfo { data, host } = handle.get_handle_info(); + host.assert_match_isolate(isolate); + Self::new_raw(isolate, data, None) + } + + /// Create a weak handle with a finalization callback installed. + /// + /// There is no guarantee as to *when* the finalization callback will be + /// invoked. However, unlike the C++ API, this API guarantees that when an + /// isolate is destroyed, any finalizers that haven't been called yet will be + /// run, unless a [`Global`] reference is keeping the object alive. Other than + /// that, there is still no guarantee as to when the finalizers will be + /// called. + /// + /// The callback does not have access to the inner value, because it has + /// already been collected by the time it runs. + pub fn with_finalizer( + isolate: &mut Isolate, + handle: impl Handle, + finalizer: Box, + ) -> Self { + let HandleInfo { data, host } = handle.get_handle_info(); + host.assert_match_isolate(isolate); + let finalizer_id = isolate.get_finalizer_map_mut().add(finalizer); + Self::new_raw(isolate, data, Some(finalizer_id)) + } + + fn new_raw( + isolate: *mut Isolate, + data: NonNull, + finalizer_id: Option, + ) -> Self { + let weak_data = Box::new(WeakData { + pointer: Default::default(), + finalizer_id, + weak_dropped: Cell::new(false), + }); + let data = data.cast().as_ptr(); + let data = unsafe { + v8__Global__NewWeak( + isolate, + data, + weak_data.deref() as *const _ as *const c_void, + Self::first_pass_callback, + ) + }; + weak_data + .pointer + .set(Some(unsafe { NonNull::new_unchecked(data as *mut _) })); + Self { + data: Some(weak_data), + isolate_handle: unsafe { (*isolate).thread_safe_handle() }, + } + } + + /// Creates a new empty handle, identical to one for an object that has + /// already been GC'd. + pub fn empty(isolate: &mut Isolate) -> Self { + Weak { + data: None, + isolate_handle: isolate.thread_safe_handle(), + } + } + + /// Clones this handle and installs a finalizer callback on the clone, as if + /// by calling [`Self::with_finalizer`]. + /// + /// Note that if this handle is empty (its value has already been GC'd), the + /// finalization callback will never run. + pub fn clone_with_finalizer( + &self, + finalizer: Box, + ) -> Self { + self.clone_raw(Some(finalizer)) + } + + fn clone_raw( + &self, + finalizer: Option>, + ) -> Self { + if let Some(data) = self.get_pointer() { + // SAFETY: We're in the isolate's thread, because Weak isn't Send or + // Sync. + let isolate_ptr = unsafe { self.isolate_handle.get_isolate_ptr() }; + if isolate_ptr.is_null() { + unreachable!("Isolate was dropped but weak handle wasn't reset."); + } + + let finalizer_id = if let Some(finalizer) = finalizer { + let isolate = unsafe { &mut *isolate_ptr }; + Some(isolate.get_finalizer_map_mut().add(finalizer)) + } else { + None + }; + Self::new_raw(isolate_ptr, data, finalizer_id) + } else { + Weak { + data: None, + isolate_handle: self.isolate_handle.clone(), + } + } + } + + /// Converts an optional raw pointer created with [`Weak::into_raw()`] back to + /// its original `Weak`. + /// + /// This method is called with `Some`, the pointer is invalidated and it + /// cannot be used with this method again. Additionally, it is unsound to call + /// this method with an isolate other than that in which the original `Weak` + /// was created. + pub unsafe fn from_raw( + isolate: &mut Isolate, + data: Option>>, + ) -> Self { + Weak { + data: data.map(|raw| Box::from_raw(raw.cast().as_ptr())), + isolate_handle: isolate.thread_safe_handle(), + } + } + + /// Consume this `Weak` handle and return the underlying raw pointer, or + /// `None` if the value has been GC'd. + /// + /// The return value can be converted back into a `Weak` by using + /// [`Weak::from_raw`]. Note that `Weak` allocates some memory, and if this + /// method returns `Some`, the pointer must be converted back into a `Weak` + /// for it to be freed. + /// + /// Note that this method might return `Some` even after the V8 value has been + /// GC'd. + pub fn into_raw(mut self) -> Option>> { + if let Some(data) = self.data.take() { + let has_finalizer = if let Some(finalizer_id) = data.finalizer_id { + // SAFETY: We're in the isolate's thread because Weak isn't Send or Sync + let isolate_ptr = unsafe { self.isolate_handle.get_isolate_ptr() }; + if isolate_ptr.is_null() { + // Disposed isolates have no finalizers. + false + } else { + let isolate = unsafe { &mut *isolate_ptr }; + isolate.get_finalizer_map().map.contains_key(&finalizer_id) + } + } else { + false + }; + + if data.pointer.get().is_none() && !has_finalizer { + // If the pointer is None and we're not waiting for the second pass, + // drop the box and return None. + None + } else { + assert!(!data.weak_dropped.get()); + Some(unsafe { NonNull::new_unchecked(Box::into_raw(data)) }) + } + } else { + None + } + } + + fn get_pointer(&self) -> Option> { + if let Some(data) = &self.data { + // It seems like when the isolate is dropped, even the first pass callback + // might not be called. + if unsafe { self.isolate_handle.get_isolate_ptr() }.is_null() { + None + } else { + data.pointer.get() + } + } else { + None + } + } + + pub fn is_empty(&self) -> bool { + self.get_pointer().is_none() + } + + pub fn to_global(&self, isolate: &mut Isolate) -> Option> { + if let Some(data) = self.get_pointer() { + let handle_host: HandleHost = (&self.isolate_handle).into(); + handle_host.assert_match_isolate(isolate); + Some(unsafe { Global::new_raw(isolate, data) }) + } else { + None + } + } + + pub fn to_local<'s>( + &self, + scope: &mut HandleScope<'s, ()>, + ) -> Option> { + if let Some(data) = self.get_pointer() { + let handle_host: HandleHost = (&self.isolate_handle).into(); + handle_host.assert_match_isolate(scope); + let local = unsafe { + scope.cast_local(|sd| { + v8__Local__New(sd.get_isolate_ptr(), data.cast().as_ptr()) as *const T + }) + }; + Some(local.unwrap()) + } else { + None + } + } + + // Finalization callbacks. + + extern "C" fn first_pass_callback(wci: *const WeakCallbackInfo) { + // SAFETY: If this callback is called, then the weak handle hasn't been + // reset, which means the `Weak` instance which owns the pinned box that the + // parameter points to hasn't been dropped. + let weak_data = unsafe { + let ptr = v8__WeakCallbackInfo__GetParameter(wci); + &*(ptr as *mut WeakData) + }; + + let data = weak_data.pointer.take().unwrap(); + unsafe { + v8__Global__Reset(data.cast().as_ptr()); + } + + // Only set the second pass callback if there could be a finalizer. + if weak_data.finalizer_id.is_some() { + unsafe { + v8__WeakCallbackInfo__SetSecondPassCallback( + wci, + Self::second_pass_callback, + ) + }; + } + } + + extern "C" fn second_pass_callback(wci: *const WeakCallbackInfo) { + // SAFETY: This callback is guaranteed by V8 to be called in the isolate's + // thread before the isolate is disposed. + let isolate = unsafe { &mut *v8__WeakCallbackInfo__GetIsolate(wci) }; + + // SAFETY: This callback might be called well after the first pass callback, + // which means the corresponding Weak might have been dropped. In Weak's + // Drop impl we make sure that if the second pass callback hasn't yet run, the + // Box> is leaked, so it will still be alive by the time this + // callback is called. + let weak_data = unsafe { + let ptr = v8__WeakCallbackInfo__GetParameter(wci); + &*(ptr as *mut WeakData) + }; + let finalizer: Option> = { + let finalizer_id = weak_data.finalizer_id.unwrap(); + isolate.get_finalizer_map_mut().map.remove(&finalizer_id) + }; + + if weak_data.weak_dropped.get() { + // SAFETY: If weak_dropped is true, the corresponding Weak has been dropped, + // so it's safe to take ownership of the Box> and drop it. + let _ = unsafe { + Box::from_raw(weak_data as *const WeakData as *mut WeakData) + }; + } + + if let Some(finalizer) = finalizer { + finalizer(isolate); + } + } +} + +impl Clone for Weak { + fn clone(&self) -> Self { + self.clone_raw(None) + } +} + +impl Drop for Weak { + fn drop(&mut self) { + // Returns whether the finalizer existed. + let remove_finalizer = |finalizer_id: Option| -> bool { + if let Some(finalizer_id) = finalizer_id { + // SAFETY: We're in the isolate's thread because `Weak` isn't Send or Sync. + let isolate_ptr = unsafe { self.isolate_handle.get_isolate_ptr() }; + if !isolate_ptr.is_null() { + let isolate = unsafe { &mut *isolate_ptr }; + let finalizer = + isolate.get_finalizer_map_mut().map.remove(&finalizer_id); + return finalizer.is_some(); + } + } + false + }; + + if let Some(data) = self.get_pointer() { + // If the pointer is not None, the first pass callback hasn't been + // called yet, and resetting will prevent it from being called. + unsafe { v8__Global__Reset(data.cast().as_ptr()) }; + remove_finalizer(self.data.as_ref().unwrap().finalizer_id); + } else if let Some(weak_data) = self.data.take() { + // The second pass callback removes the finalizer, so if there is one, + // the second pass hasn't yet run, and WeakData will have to be alive. + // In that case we leak the WeakData but remove the finalizer. + if remove_finalizer(weak_data.finalizer_id) { + weak_data.weak_dropped.set(true); + Box::leak(weak_data); + } + } + } +} + +impl Eq for Weak where T: Eq {} + +impl PartialEq for Weak +where + T: PartialEq, +{ + fn eq(&self, other: &Rhs) -> bool { + let HandleInfo { + data: other_data, + host: other_host, + } = other.get_handle_info(); + let self_host: HandleHost = (&self.isolate_handle).into(); + if !self_host.match_host(other_host, None) { + false + } else if let Some(self_data) = self.get_pointer() { + unsafe { self_data.as_ref() == other_data.as_ref() } + } else { + false + } + } +} + +impl PartialEq> for Weak +where + T: PartialEq, +{ + fn eq(&self, other: &Weak) -> bool { + let self_host: HandleHost = (&self.isolate_handle).into(); + let other_host: HandleHost = (&other.isolate_handle).into(); + if !self_host.match_host(other_host, None) { + return false; + } + match (self.get_pointer(), other.get_pointer()) { + (Some(self_data), Some(other_data)) => unsafe { + self_data.as_ref() == other_data.as_ref() + }, + (None, None) => true, + _ => false, + } + } +} + +/// The inner mechanism behind [`Weak`] and finalizations. +/// +/// This struct is heap-allocated and will not move until it's dropped, so it +/// can be accessed by the finalization callbacks by creating a shared reference +/// from a pointer. The fields are wrapped in [`Cell`] so they are modifiable by +/// both the [`Weak`] and the finalization callbacks. +pub struct WeakData { + pointer: Cell>>, + finalizer_id: Option, + weak_dropped: Cell, +} + +impl std::fmt::Debug for WeakData { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + f.debug_struct("WeakData") + .field("pointer", &self.pointer) + .finish_non_exhaustive() + } +} + +#[repr(C)] +struct WeakCallbackInfo(Opaque); + +type FinalizerId = usize; + +#[derive(Default)] +pub(crate) struct FinalizerMap { + map: std::collections::HashMap>, + next_id: FinalizerId, +} + +impl FinalizerMap { + pub(crate) fn add( + &mut self, + finalizer: Box, + ) -> FinalizerId { + let id = self.next_id; + // TODO: Overflow. + self.next_id += 1; + self.map.insert(id, finalizer); + id + } + + pub(crate) fn is_empty(&self) -> bool { + self.map.is_empty() + } +} diff --git a/src/isolate.rs b/src/isolate.rs index 85c17c3a..cfe061e3 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -1,5 +1,6 @@ // Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. use crate::function::FunctionCallbackInfo; +use crate::handle::FinalizerMap; use crate::isolate_create_params::raw; use crate::isolate_create_params::CreateParams; use crate::promise::PromiseRejectMessage; @@ -374,6 +375,14 @@ impl Isolate { } } + pub(crate) fn get_finalizer_map(&self) -> &FinalizerMap { + &self.get_annex().finalizer_map + } + + pub(crate) fn get_finalizer_map_mut(&mut self) -> &mut FinalizerMap { + &mut self.get_annex_mut().finalizer_map + } + fn get_annex_arc(&self) -> Arc { let annex_ptr = self.get_annex(); let annex_arc = unsafe { Arc::from_raw(annex_ptr) }; @@ -709,6 +718,15 @@ impl Isolate { // Drop the scope stack. ScopeData::drop_root(self); + // If there are finalizers left to call, we trigger GC to try and call as + // many of them as possible. + if !self.get_annex().finalizer_map.is_empty() { + // A low memory notification triggers a synchronous GC, which means + // finalizers will be called during the course of the call, rather than at + // some later point. + self.low_memory_notification(); + } + // Set the `isolate` pointer inside the annex struct to null, so any // IsolateHandle that outlives the isolate will know that it can't call // methods on the isolate. @@ -763,6 +781,7 @@ impl Isolate { pub(crate) struct IsolateAnnex { create_param_allocations: Box, slots: HashMap, + finalizer_map: FinalizerMap, // The `isolate` and `isolate_mutex` fields are there so an `IsolateHandle` // (which may outlive the isolate itself) can determine whether the isolate // is still alive, and if so, get a reference to it. Safety rules: @@ -782,6 +801,7 @@ impl IsolateAnnex { Self { create_param_allocations, slots: HashMap::default(), + finalizer_map: FinalizerMap::default(), isolate, isolate_mutex: Mutex::new(()), } diff --git a/src/lib.rs b/src/lib.rs index f4594bba..3bb389d1 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -90,6 +90,7 @@ pub use function::*; pub use handle::Global; pub use handle::Handle; pub use handle::Local; +pub use handle::Weak; pub use isolate::HeapStatistics; pub use isolate::HostImportModuleDynamicallyCallback; pub use isolate::HostInitializeImportMetaObjectCallback; diff --git a/tests/test_api.rs b/tests/test_api.rs index aeffc86b..db8fc9b1 100644 --- a/tests/test_api.rs +++ b/tests/test_api.rs @@ -135,6 +135,39 @@ fn global_handles() { } } +#[test] +fn global_from_into_raw() { + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let (raw, weak) = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + let global = v8::Global::new(scope, local); + + let weak = v8::Weak::new(scope, &global); + let raw = global.into_raw(); + (raw, weak) + }; + + eval(scope, "gc()").unwrap(); + assert!(!weak.is_empty()); + + { + let reconstructed = unsafe { v8::Global::from_raw(scope, raw) }; + + let global_from_weak = weak.to_global(scope).unwrap(); + assert_eq!(global_from_weak, reconstructed); + } + + eval(scope, "gc()").unwrap(); + assert!(weak.is_empty()); +} + #[test] fn local_handle_deref() { let _setup_guard = setup(); @@ -6272,3 +6305,338 @@ fn instance_of() { assert!(array.instance_of(&mut scope, array_constructor).unwrap()); } + +#[test] +fn weak_handle() { + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let weak = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + + let weak = v8::Weak::new(scope, &local); + assert!(!weak.is_empty()); + assert_eq!(weak, local); + assert_eq!(weak.to_local(scope), Some(local)); + + weak + }; + + let scope = &mut v8::HandleScope::new(scope); + + eval(scope, "gc()").unwrap(); + + assert!(weak.is_empty()); + assert_eq!(weak.to_local(scope), None); +} + +#[test] +fn finalizers() { + use std::cell::Cell; + use std::ops::Deref; + use std::rc::Rc; + + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + // The finalizer for a dropped Weak is never called. + { + { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + let _ = + v8::Weak::with_finalizer(scope, &local, Box::new(|_| unreachable!())); + } + + let scope = &mut v8::HandleScope::new(scope); + eval(scope, "gc()").unwrap(); + } + + let finalizer_called = Rc::new(Cell::new(false)); + let weak = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + + // We use a channel to send data into the finalizer without having to worry + // about lifetimes. + let (tx, rx) = std::sync::mpsc::sync_channel::<( + Rc>, + Rc>, + )>(1); + + let weak = Rc::new(v8::Weak::with_finalizer( + scope, + &local, + Box::new(move |_| { + let (weak, finalizer_called) = rx.try_recv().unwrap(); + finalizer_called.set(true); + assert!(weak.is_empty()); + }), + )); + + tx.send((weak.clone(), finalizer_called.clone())).unwrap(); + + assert!(!weak.is_empty()); + assert_eq!(weak.deref(), &local); + assert_eq!(weak.to_local(scope), Some(local)); + + weak + }; + + let scope = &mut v8::HandleScope::new(scope); + eval(scope, "gc()").unwrap(); + assert!(weak.is_empty()); + assert!(finalizer_called.get()); +} + +#[test] +fn weak_from_global() { + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let global = { + let scope = &mut v8::HandleScope::new(scope); + let object = v8::Object::new(scope); + v8::Global::new(scope, object) + }; + + let weak = v8::Weak::new(scope, &global); + assert!(!weak.is_empty()); + assert_eq!(weak.to_global(scope).unwrap(), global); + + drop(global); + eval(scope, "gc()").unwrap(); + assert!(weak.is_empty()); +} + +#[test] +fn weak_from_into_raw() { + use std::cell::Cell; + use std::rc::Rc; + + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let finalizer_called = Rc::new(Cell::new(false)); + + assert_eq!(v8::Weak::::empty(scope).into_raw(), None); + assert!(unsafe { v8::Weak::::from_raw(scope, None) }.is_empty()); + + // regular back and forth + { + finalizer_called.take(); + let (weak1, weak2) = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + let weak = v8::Weak::new(scope, &local); + let weak_with_finalizer = v8::Weak::with_finalizer( + scope, + &local, + Box::new({ + let finalizer_called = finalizer_called.clone(); + move |_| { + finalizer_called.set(true); + } + }), + ); + let raw1 = weak.into_raw(); + let raw2 = weak_with_finalizer.into_raw(); + assert!(raw1.is_some()); + assert!(raw2.is_some()); + let weak1 = unsafe { v8::Weak::from_raw(scope, raw1) }; + let weak2 = unsafe { v8::Weak::from_raw(scope, raw2) }; + assert_eq!(weak1.to_local(scope), Some(local)); + assert_eq!(weak2.to_local(scope), Some(local)); + assert!(!finalizer_called.get()); + (weak1, weak2) + }; + eval(scope, "gc()").unwrap(); + assert!(weak1.is_empty()); + assert!(weak2.is_empty()); + assert!(finalizer_called.get()); + } + + // into_raw from a GC'd pointer + { + let weak = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + v8::Weak::new(scope, &local) + }; + assert!(!weak.is_empty()); + eval(scope, "gc()").unwrap(); + assert!(weak.is_empty()); + assert_eq!(weak.into_raw(), None); + } + + // It's fine if there's a GC while the Weak is leaked. + { + finalizer_called.take(); + let (weak, weak_with_finalizer) = { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + let weak = v8::Weak::new(scope, &local); + let weak_with_finalizer = v8::Weak::with_finalizer( + scope, + &local, + Box::new({ + let finalizer_called = finalizer_called.clone(); + move |_| { + finalizer_called.set(true); + } + }), + ); + (weak, weak_with_finalizer) + }; + assert!(!weak.is_empty()); + assert!(!weak_with_finalizer.is_empty()); + assert!(!finalizer_called.get()); + let raw1 = weak.into_raw(); + let raw2 = weak_with_finalizer.into_raw(); + assert!(raw1.is_some()); + assert!(raw2.is_some()); + eval(scope, "gc()").unwrap(); + assert!(finalizer_called.get()); + let weak1 = unsafe { v8::Weak::from_raw(scope, raw1) }; + let weak2 = unsafe { v8::Weak::from_raw(scope, raw2) }; + assert!(weak1.is_empty()); + assert!(weak2.is_empty()); + } + + // Leaking a Weak will not crash the isolate. + { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + v8::Weak::new(scope, local).into_raw(); + v8::Weak::with_finalizer(scope, local, Box::new(|_| {})).into_raw(); + eval(scope, "gc()").unwrap(); + } + eval(scope, "gc()").unwrap(); +} + +#[test] +fn drop_weak_from_raw_in_finalizer() { + use std::cell::Cell; + use std::rc::Rc; + + let _setup_guard = setup(); + + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let weak_ptr = Rc::new(Cell::new(None)); + let finalized = Rc::new(Cell::new(false)); + + { + let scope = &mut v8::HandleScope::new(scope); + let local = v8::Object::new(scope); + let weak = v8::Weak::with_finalizer( + scope, + &local, + Box::new({ + let weak_ptr = weak_ptr.clone(); + let finalized = finalized.clone(); + move |isolate| { + let weak_ptr = weak_ptr.get().unwrap(); + let weak: v8::Weak = + unsafe { v8::Weak::from_raw(isolate, Some(weak_ptr)) }; + drop(weak); + finalized.set(true); + } + }), + ); + weak_ptr.set(weak.into_raw()); + } + + assert!(!finalized.get()); + eval(scope, "gc()").unwrap(); + assert!(finalized.get()); +} + +#[test] +fn finalizer_on_global_object() { + use std::cell::Cell; + use std::rc::Rc; + + let _setup_guard = setup(); + + let weak; + let finalized = Rc::new(Cell::new(false)); + + { + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let global_object = context.global(scope); + weak = v8::Weak::with_finalizer( + scope, + global_object, + Box::new({ + let finalized = finalized.clone(); + move |_| finalized.set(true) + }), + ); + } + + assert!(finalized.get()); + drop(weak); +} + +#[test] +fn finalizer_on_kept_global() { + // If a global is kept alive after an isolate is dropped, any finalizers won't + // be called. + + use std::cell::Cell; + use std::rc::Rc; + + let _setup_guard = setup(); + + let global; + let weak; + let finalized = Rc::new(Cell::new(false)); + + { + let isolate = &mut v8::Isolate::new(Default::default()); + let scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(scope); + let scope = &mut v8::ContextScope::new(scope, context); + + let object = v8::Object::new(scope); + global = v8::Global::new(scope, &object); + weak = v8::Weak::with_finalizer( + scope, + &object, + Box::new({ + let finalized = finalized.clone(); + move |_| finalized.set(true) + }), + ) + } + + assert!(weak.is_empty()); + assert!(!finalized.get()); + drop(weak); + drop(global); +}