From 1ed35fd22ebcde876fbaff4e2ad286b30b4a9e2a Mon Sep 17 00:00:00 2001 From: Andreu Botella Date: Mon, 9 May 2022 12:20:55 +0200 Subject: [PATCH] feat: Weak handles and finalizers (#895) This change adds support for weak handles that don't prevent GC of the referenced objects, through the `v8::Weak` API. A weak handle can be empty (if it was created empty or its object was GC'd) or non-empty, and if non-empty it allows getting its object as a global or local. When creating a `v8::Weak` you can also set a finalizer that will be called at some point after the object is GC'd, as long as the weak handle is still alive at that point. This finalization corresponds to the second-pass callback in `kParameter` mode in the C++ API, so it will only be called after the object is GC'd. The finalizer function is a `FnOnce` that may close over data, and which takes a `&mut Isolate` as an argument. The C++ finalization API doesn't guarantee _when_ or even _if_ the finalizer will ever be called, but in order to prevent memory leaks, the rusty_v8 wrapper ensures that it will be called at some point, even if it's just before the isolate gets dropped. `v8::Weak` implements `Clone`, but a finalizer is tied to a single weak handle, so its clones won't be able to keep the finalizer alive. And in fact, cloning will create a new weak handle that isn't tied to a finalizer at all. `v8::Weak::clone_with_finalizer` can be used to make a clone of a weak handle which has a finalizer tied to it. Note that `v8::Weak` doesn't implement `Hash`, because the hash would have to change once the handle's object is GC'd, which is a big gotcha and would break some of the algorithms that rely on hashes, such as the Rust std's `HashMap`. --- src/binding.cc | 24 +++ src/handle.rs | 436 ++++++++++++++++++++++++++++++++++++++++++++++ src/isolate.rs | 20 +++ src/lib.rs | 1 + tests/test_api.rs | 368 ++++++++++++++++++++++++++++++++++++++ 5 files changed, 849 insertions(+) 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); +}