From 7072da4199e8e87a4acbc5593a0aab50cbb63df9 Mon Sep 17 00:00:00 2001 From: Divy Srivastava Date: Mon, 30 Oct 2023 08:10:15 -0700 Subject: [PATCH] Add `cppgc` bindings (#1336) https://v8.dev/blog/high-performance-cpp-gc Oilpan in Deno design doc: https://www.notion.so/denolandinc/Oilpan-cppgc-in-Deno-e194f4268e9f4135ba97610ff7d3a949?pvs=4 Oilpan can be used to implement GC'able resources in Deno Closes https://github.com/denoland/rusty_v8/issues/933 --- examples/cppgc-object.rs | 129 +++++++++++++++++ examples/cppgc.rs | 91 ++++++++++++ src/binding.cc | 73 ++++++++++ src/cppgc.rs | 294 +++++++++++++++++++++++++++++++++++++++ src/isolate.rs | 18 +++ src/lib.rs | 1 + tests/test_cppgc.rs | 137 ++++++++++++++++++ 7 files changed, 743 insertions(+) create mode 100644 examples/cppgc-object.rs create mode 100644 examples/cppgc.rs create mode 100644 src/cppgc.rs create mode 100644 tests/test_cppgc.rs diff --git a/examples/cppgc-object.rs b/examples/cppgc-object.rs new file mode 100644 index 00000000..577a9176 --- /dev/null +++ b/examples/cppgc-object.rs @@ -0,0 +1,129 @@ +// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. +use std::cell::Cell; + +struct Wrappable { + id: String, + trace_count: Cell, +} + +impl v8::cppgc::GarbageCollected for Wrappable { + fn trace(&self, _visitor: &v8::cppgc::Visitor) { + println!("Wrappable::trace() {}", self.id); + self.trace_count.set(self.trace_count.get() + 1); + } +} + +impl Drop for Wrappable { + fn drop(&mut self) { + println!("Wrappable::drop() {}", self.id); + } +} + +// Set a custom embedder ID for the garbage collector. cppgc will use this ID to +// identify the object that it manages. +const DEFAULT_CPP_GC_EMBEDDER_ID: u16 = 0xde90; + +fn main() { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::set_flags_from_string("--no_freeze_flags_after_init --expose-gc"); + v8::V8::initialize_platform(platform.clone()); + v8::V8::initialize(); + + v8::cppgc::initalize_process(platform.clone()); + + { + let isolate = &mut v8::Isolate::new(v8::CreateParams::default()); + + // Create a managed heap. + let heap = v8::cppgc::Heap::create( + platform, + v8::cppgc::HeapCreateParams::new(v8::cppgc::WrapperDescriptor::new( + 0, + 1, + DEFAULT_CPP_GC_EMBEDDER_ID, + )), + ); + + isolate.attach_cpp_heap(&heap); + + let handle_scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(handle_scope); + let scope = &mut v8::ContextScope::new(handle_scope, context); + let global = context.global(scope); + { + let func = v8::Function::new( + scope, + |scope: &mut v8::HandleScope, + args: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue| { + let id = args.get(0).to_rust_string_lossy(scope); + + let templ = v8::ObjectTemplate::new(scope); + templ.set_internal_field_count(2); + + let obj = templ.new_instance(scope).unwrap(); + + let member = v8::cppgc::make_garbage_collected( + scope.get_cpp_heap(), + Box::new(Wrappable { + trace_count: Cell::new(0), + id, + }), + ); + + obj.set_aligned_pointer_in_internal_field( + 0, + &DEFAULT_CPP_GC_EMBEDDER_ID as *const u16 as _, + ); + obj.set_aligned_pointer_in_internal_field(1, member.handle as _); + + rv.set(obj.into()); + }, + ) + .unwrap(); + let name = v8::String::new(scope, "make_wrap").unwrap(); + global.set(scope, name.into(), func.into()).unwrap(); + } + + let source = v8::String::new( + scope, + r#" + make_wrap('gc me pls'); // Inaccessible after scope. + globalThis.wrap = make_wrap('dont gc me'); // Accessible after scope. + "#, + ) + .unwrap(); + execute_script(scope, source); + + scope + .request_garbage_collection_for_testing(v8::GarbageCollectionType::Full); + } + + // Gracefully shutdown the process. + unsafe { + v8::cppgc::shutdown_process(); + v8::V8::dispose(); + } + v8::V8::dispose_platform(); +} + +fn execute_script( + context_scope: &mut v8::ContextScope, + script: v8::Local, +) { + let scope = &mut v8::HandleScope::new(context_scope); + let try_catch = &mut v8::TryCatch::new(scope); + + let script = v8::Script::compile(try_catch, script, None) + .expect("failed to compile script"); + + if script.run(try_catch).is_none() { + let exception_string = try_catch + .stack_trace() + .or_else(|| try_catch.exception()) + .map(|value| value.to_rust_string_lossy(try_catch)) + .unwrap_or_else(|| "no stack trace".into()); + + panic!("{}", exception_string); + } +} diff --git a/examples/cppgc.rs b/examples/cppgc.rs new file mode 100644 index 00000000..abcf5b6e --- /dev/null +++ b/examples/cppgc.rs @@ -0,0 +1,91 @@ +// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. +// +// This sample program shows how to set up a stand-alone cppgc heap. +use std::ops::Deref; + +// Simple string rope to illustrate allocation and garbage collection below. +// The rope keeps the next parts alive via regular managed reference. +struct Rope { + part: String, + next: Option>, +} + +impl std::fmt::Display for Rope { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "{}", self.part)?; + if let Some(next) = &self.next { + write!(f, "{}", next.deref())?; + } + Ok(()) + } +} + +impl Rope { + pub fn new(part: String, next: Option>) -> Box { + Box::new(Self { part, next }) + } +} + +impl v8::cppgc::GarbageCollected for Rope { + fn trace(&self, visitor: &v8::cppgc::Visitor) { + if let Some(member) = &self.next { + visitor.trace(member); + } + } +} + +impl Drop for Rope { + fn drop(&mut self) { + println!("Dropping {}", self.part); + } +} + +const DEFAULT_CPP_GC_EMBEDDER_ID: u16 = 0xde90; + +fn main() { + let platform = v8::new_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform.clone()); + v8::V8::initialize(); + v8::cppgc::initalize_process(platform.clone()); + + { + // Create a managed heap. + let heap = v8::cppgc::Heap::create( + platform, + v8::cppgc::HeapCreateParams::new(v8::cppgc::WrapperDescriptor::new( + 0, + 1, + DEFAULT_CPP_GC_EMBEDDER_ID, + )), + ); + + // Allocate a string rope on the managed heap. + let rope = v8::cppgc::make_garbage_collected( + &heap, + Rope::new( + String::from("Hello "), + Some(v8::cppgc::make_garbage_collected( + &heap, + Rope::new(String::from("World!"), None), + )), + ), + ); + + println!("{}", unsafe { rope.get() }); + // Manually trigger garbage collection. + heap.enable_detached_garbage_collections_for_testing(); + heap.collect_garbage_for_testing( + v8::cppgc::EmbedderStackState::MayContainHeapPointers, + ); + heap.collect_garbage_for_testing( + v8::cppgc::EmbedderStackState::NoHeapPointers, + ); + } + + // Gracefully shutdown the process. + unsafe { + v8::cppgc::shutdown_process(); + v8::V8::dispose(); + } + v8::V8::dispose_platform(); +} diff --git a/src/binding.cc b/src/binding.cc index ed4e778b..dda7bb57 100644 --- a/src/binding.cc +++ b/src/binding.cc @@ -11,6 +11,7 @@ #include "unicode/locid.h" #include "v8-callbacks.h" #include "v8/include/libplatform/libplatform.h" +#include "v8/include/v8-cppgc.h" #include "v8/include/v8-fast-api-calls.h" #include "v8/include/v8-inspector.h" #include "v8/include/v8-internal.h" @@ -29,6 +30,8 @@ #include "v8/src/objects/objects.h" #include "v8/src/objects/smi.h" +#include "cppgc/platform.h" + using namespace support; template @@ -3582,3 +3585,73 @@ void v8__PropertyDescriptor__set_configurable(v8::PropertyDescriptor* self, } } // extern "C" + +// cppgc + +extern "C" { + +using RustTraceFn = void (*)(void* obj, cppgc::Visitor*); +using RustDestroyFn = void (*)(void* obj); + +class RustObj final: public cppgc::GarbageCollected { + public: + explicit RustObj(void* obj, RustTraceFn trace, RustDestroyFn destroy): trace_(trace), destroy_(destroy), obj_(obj) {} + + ~RustObj() { + destroy_(obj_); + } + + void Trace(cppgc::Visitor* visitor) const { + trace_(obj_, visitor); + } + + private: + RustTraceFn trace_; + RustDestroyFn destroy_; + void* obj_; +}; + +void cppgc__initialize_process(v8::Platform* platform) { + cppgc::InitializeProcess(platform->GetPageAllocator()); +} + +void cppgc__shutdown_process() { + cppgc::ShutdownProcess(); +} + +v8::CppHeap* cppgc__heap__create(v8::Platform* platform, int wrappable_type_index, + int wrappable_instance_index, uint16_t embedder_id) { + std::unique_ptr heap = v8::CppHeap::Create(platform, v8::CppHeapCreateParams { + {}, + v8::WrapperDescriptor(wrappable_type_index, wrappable_instance_index, embedder_id), + }); + return heap.release(); +} + +void v8__Isolate__AttachCppHeap(v8::Isolate* isolate, v8::CppHeap* cpp_heap) { + isolate->AttachCppHeap(cpp_heap); +} + +v8::CppHeap* v8__Isolate__GetCppHeap(v8::Isolate* isolate) { + return isolate->GetCppHeap(); +} + +void cppgc__heap__DELETE(v8::CppHeap* self) { delete self; } + +void cppgc__heap__enable_detached_garbage_collections_for_testing(v8::CppHeap* heap) { + heap->EnableDetachedGarbageCollectionsForTesting(); +} + +void cppgc__heap__collect_garbage_for_testing(v8::CppHeap* heap, cppgc::EmbedderStackState stack_state) { + heap->CollectGarbageForTesting(stack_state); +} + +RustObj* cppgc__make_garbage_collectable(v8::CppHeap* heap, void* obj, RustTraceFn trace, RustDestroyFn destroy) { + return cppgc::MakeGarbageCollected(heap->GetAllocationHandle(), obj, trace, destroy); +} + +void cppgc__visitor__trace(cppgc::Visitor* visitor, RustObj* member) { + visitor->Trace(*member); +} + +} // extern "C" \ No newline at end of file diff --git a/src/cppgc.rs b/src/cppgc.rs new file mode 100644 index 00000000..f2fce22b --- /dev/null +++ b/src/cppgc.rs @@ -0,0 +1,294 @@ +// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license + +use crate::platform::Platform; +use crate::support::int; +use crate::support::Opaque; +use crate::support::SharedRef; +use crate::support::UniqueRef; + +extern "C" { + fn cppgc__initialize_process(platform: *mut Platform); + fn cppgc__shutdown_process(); + + fn cppgc__heap__create( + platform: *mut Platform, + wrappable_type_index: int, + wrappable_instance_index: int, + embedder_id_for_garbage_collected: u16, + ) -> *mut Heap; + fn cppgc__heap__DELETE(heap: *mut Heap); + fn cppgc__make_garbage_collectable( + heap: *mut Heap, + obj: *mut (), + trace: TraceFn, + destroy: DestroyFn, + ) -> *mut (); + + fn cppgc__heap__enable_detached_garbage_collections_for_testing( + heap: *mut Heap, + ); + fn cppgc__heap__collect_garbage_for_testing( + heap: *mut Heap, + stack_state: EmbedderStackState, + ); + + fn cppgc__visitor__trace(visitor: *const Visitor, member: *const ()); +} + +/// Process-global initialization of the garbage collector. Must be called before +/// creating a Heap. +/// +/// Can be called multiple times when paired with `ShutdownProcess()`. +pub fn initalize_process(platform: SharedRef) { + unsafe { + cppgc__initialize_process(&*platform as *const Platform as *mut _); + } +} + +/// # Safety +/// +/// Must be called after destroying the last used heap. Some process-global +/// metadata may not be returned and reused upon a subsequent +/// `initalize_process()` call. +pub unsafe fn shutdown_process() { + cppgc__shutdown_process(); +} + +/// Visitor passed to trace methods. All managed pointers must have called the +/// Visitor's trace method on them. +/// +/// ``` +/// use v8::cppgc::{Member, Visitor, GarbageCollected}; +/// +/// struct Foo { foo: Member } +/// +/// impl GarbageCollected for Foo { +/// fn trace(&self, visitor: &Visitor) { +/// visitor.trace(&self.foo); +/// } +/// } +/// ``` +#[repr(C)] +#[derive(Debug)] +pub struct Visitor(Opaque); + +impl Visitor { + pub fn trace(&self, member: &Member) { + unsafe { cppgc__visitor__trace(self, member.handle) } + } +} + +#[repr(C)] +pub enum EmbedderStackState { + /// Stack may contain interesting heap pointers. + MayContainHeapPointers, + /// Stack does not contain any interesting heap pointers. + NoHeapPointers, +} + +/// Specifies supported marking types. +#[repr(u8)] +pub enum MarkingType { + /// Atomic stop-the-world marking. This option does not require any write barriers but is the most intrusive in terms of jank. + Atomic, + /// Incremental marking interleaves marking with the rest of the application workload on the same thread. + Incremental, + /// Incremental and concurrent marking. + IncrementalAndConcurrent, +} + +/// Specifies supported sweeping types. +#[repr(u8)] +pub enum SweepingType { + /// Atomic stop-the-world sweeping. All of sweeping is performed at once. + Atomic, + /// Incremental sweeping interleaves sweeping with the rest of the application workload on the same thread. + Incremental, + /// Incremental and concurrent sweeping. Sweeping is split and interleaved with the rest of the application. + IncrementalAndConcurrent, +} + +pub type InternalFieldIndex = int; + +/// Describes how V8 wrapper objects maintain references to garbage-collected C++ objects. +pub struct WrapperDescriptor { + /// Index of the wrappable type. + pub wrappable_type_index: InternalFieldIndex, + /// Index of the wrappable instance. + pub wrappable_instance_index: InternalFieldIndex, + /// Embedder id identifying instances of garbage-collected objects. It is expected that + /// the first field of the wrappable type is a uint16_t holding the id. Only references + /// to instances of wrappables types with an id of embedder_id_for_garbage_collected will + /// be considered by Heap. + pub embedder_id_for_garbage_collected: u16, +} + +impl WrapperDescriptor { + pub fn new( + wrappable_type_index: InternalFieldIndex, + wrappable_instance_index: InternalFieldIndex, + embedder_id_for_garbage_collected: u16, + ) -> Self { + Self { + wrappable_type_index, + wrappable_instance_index, + embedder_id_for_garbage_collected, + } + } +} + +pub struct HeapCreateParams { + wrapper_descriptor: WrapperDescriptor, + /// Specifies which kind of marking are supported by the heap. + pub marking_support: MarkingType, + /// Specifies which kind of sweeping are supported by the heap. + pub sweeping_support: SweepingType, +} + +impl HeapCreateParams { + pub fn new(wrapper_descriptor: WrapperDescriptor) -> Self { + Self { + wrapper_descriptor, + marking_support: MarkingType::IncrementalAndConcurrent, + sweeping_support: SweepingType::IncrementalAndConcurrent, + } + } +} + +type TraceFn = unsafe extern "C" fn(*mut (), *mut Visitor); +type DestroyFn = unsafe extern "C" fn(*mut ()); + +/// A heap for allocating managed C++ objects. +/// +/// Similar to v8::Isolate, the heap may only be accessed from one thread at a +/// time. +#[repr(C)] +#[derive(Debug)] +pub struct Heap(Opaque); + +impl Drop for Heap { + fn drop(&mut self) { + unsafe { cppgc__heap__DELETE(self as *mut Heap) } + } +} + +impl Heap { + pub fn create( + platform: SharedRef, + params: HeapCreateParams, + ) -> UniqueRef { + let WrapperDescriptor { + wrappable_type_index, + wrappable_instance_index, + embedder_id_for_garbage_collected, + } = params.wrapper_descriptor; + + unsafe { + UniqueRef::from_raw(cppgc__heap__create( + &*platform as *const Platform as *mut _, + wrappable_type_index, + wrappable_instance_index, + embedder_id_for_garbage_collected, + )) + } + } + + pub fn collect_garbage_for_testing(&self, stack_state: EmbedderStackState) { + unsafe { + cppgc__heap__collect_garbage_for_testing( + self as *const Heap as *mut _, + stack_state, + ); + } + } + + pub fn enable_detached_garbage_collections_for_testing(&self) { + unsafe { + cppgc__heap__enable_detached_garbage_collections_for_testing( + self as *const Heap as *mut _, + ); + } + } +} + +/// Base trait for managed objects. +pub trait GarbageCollected { + fn trace(&self, _visitor: &Visitor) {} +} + +/// Members are used to contain strong pointers to other garbage +/// collected objects. All members fields on garbage collected objects +/// must be trace in the `trace` method. +pub struct Member { + pub handle: *mut (), + ptr: *mut T, +} + +impl Member { + /// Returns a raw pointer to the object. + /// + /// # Safety + /// + /// There are no guarantees that the object is alive and not garbage collected. + pub unsafe fn get(&self) -> &T { + unsafe { &*self.ptr } + } +} + +impl std::ops::Deref for Member { + type Target = T; + + fn deref(&self) -> &Self::Target { + unsafe { &*self.ptr } + } +} + +/// Constructs an instance of T, which is a garbage collected type. +/// +/// The object will be allocated on the heap and managed by cppgc. During +/// marking, the object will be traced by calling the `trace` method on it. +/// +/// During sweeping, the destructor will be called and the memory will be +/// freed using `Box::from_raw`. +pub fn make_garbage_collected( + heap: &Heap, + obj: Box, +) -> Member { + unsafe extern "C" fn destroy(obj: *mut ()) { + let _ = Box::from_raw(obj as *mut T); + } + + unsafe { make_garbage_collected_raw(heap, Box::into_raw(obj), destroy::) } +} + +/// # Safety +/// +/// By calling this function, you are giving up ownership of `T` to the +/// garbage collector. +/// +/// `obj` must be a pointer to a valid instance of T allocated on the heap. +/// +/// `drop_fn` must be a function that drops the instance of T. This function +/// will be called when the object is garbage collected. +pub unsafe fn make_garbage_collected_raw( + heap: &Heap, + obj: *mut T, + destroy: DestroyFn, +) -> Member { + unsafe extern "C" fn trace( + obj: *mut (), + visitor: *mut Visitor, + ) { + let obj = unsafe { &*(obj as *const T) }; + obj.trace(unsafe { &*visitor }); + } + + let handle = cppgc__make_garbage_collectable( + heap as *const Heap as *mut _, + obj as _, + trace::, + destroy, + ); + + Member { handle, ptr: obj } +} diff --git a/src/isolate.rs b/src/isolate.rs index ade12644..76e481a5 100644 --- a/src/isolate.rs +++ b/src/isolate.rs @@ -1,4 +1,5 @@ // Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. +use crate::cppgc::Heap; use crate::function::FunctionCallbackInfo; use crate::gc::GCCallbackFlags; use crate::gc::GCType; @@ -420,6 +421,8 @@ extern "C" { isolate: *mut Isolate, change_in_bytes: i64, ) -> i64; + fn v8__Isolate__GetCppHeap(isolate: *mut Isolate) -> *mut Heap; + fn v8__Isolate__AttachCppHeap(isolate: *mut Isolate, heap: *mut Heap); fn v8__Isolate__SetPrepareStackTraceCallback( isolate: *mut Isolate, callback: PrepareStackTraceCallback, @@ -1084,6 +1087,21 @@ impl Isolate { } } + /// Attaches a managed C++ heap as an extension to the JavaScript heap. + /// + /// The embedder maintains ownership of the CppHeap. At most one C++ heap + /// can be attached to V8. + #[inline(always)] + pub fn attach_cpp_heap(&mut self, heap: &Heap) { + unsafe { + v8__Isolate__AttachCppHeap(self, heap as *const Heap as *mut _); + } + } + + pub fn get_cpp_heap(&mut self) -> &Heap { + unsafe { &*v8__Isolate__GetCppHeap(self) } + } + #[inline(always)] pub fn set_oom_error_handler(&mut self, callback: OomErrorCallback) { unsafe { v8__Isolate__SetOOMErrorHandler(self, callback) }; diff --git a/src/lib.rs b/src/lib.rs index a6c2a69c..e17b398b 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -31,6 +31,7 @@ mod array_buffer; mod array_buffer_view; mod bigint; mod context; +pub mod cppgc; mod data; mod date; mod exception; diff --git a/tests/test_cppgc.rs b/tests/test_cppgc.rs new file mode 100644 index 00000000..fc5feb5a --- /dev/null +++ b/tests/test_cppgc.rs @@ -0,0 +1,137 @@ +// Copyright 2019-2021 the Deno authors. All rights reserved. MIT license. +use std::sync::atomic::{AtomicUsize, Ordering}; +use v8::cppgc::{GarbageCollected, Visitor}; + +struct CppGCGuard { + pub platform: v8::SharedRef, +} + +fn initalize_test() -> CppGCGuard { + v8::V8::set_flags_from_string("--no_freeze_flags_after_init --expose-gc"); + let platform = v8::new_unprotected_default_platform(0, false).make_shared(); + v8::V8::initialize_platform(platform.clone()); + v8::V8::initialize(); + v8::cppgc::initalize_process(platform.clone()); + + CppGCGuard { platform } +} + +impl Drop for CppGCGuard { + fn drop(&mut self) { + unsafe { + v8::cppgc::shutdown_process(); + v8::V8::dispose(); + } + v8::V8::dispose_platform(); + } +} + +const DEFAULT_CPP_GC_EMBEDDER_ID: u16 = 0xde90; + +#[test] +fn cppgc_object_wrap() { + let guard = initalize_test(); + + static TRACE_COUNT: AtomicUsize = AtomicUsize::new(0); + static DROP_COUNT: AtomicUsize = AtomicUsize::new(0); + + struct Wrap; + + impl GarbageCollected for Wrap { + fn trace(&self, _: &Visitor) { + TRACE_COUNT.fetch_add(1, Ordering::SeqCst); + } + } + + impl Drop for Wrap { + fn drop(&mut self) { + DROP_COUNT.fetch_add(1, Ordering::SeqCst); + } + } + + fn op_make_wrap( + scope: &mut v8::HandleScope, + _: v8::FunctionCallbackArguments, + mut rv: v8::ReturnValue, + ) { + let templ = v8::ObjectTemplate::new(scope); + templ.set_internal_field_count(2); + + let obj = templ.new_instance(scope).unwrap(); + + let member = + v8::cppgc::make_garbage_collected(scope.get_cpp_heap(), Box::new(Wrap)); + + obj.set_aligned_pointer_in_internal_field( + 0, + &DEFAULT_CPP_GC_EMBEDDER_ID as *const u16 as _, + ); + obj.set_aligned_pointer_in_internal_field(1, member.handle as _); + + rv.set(obj.into()); + } + + { + let isolate = &mut v8::Isolate::new(Default::default()); + // Create a managed heap. + let heap = v8::cppgc::Heap::create( + guard.platform.clone(), + v8::cppgc::HeapCreateParams::new(v8::cppgc::WrapperDescriptor::new( + 0, + 1, + DEFAULT_CPP_GC_EMBEDDER_ID, + )), + ); + + isolate.attach_cpp_heap(&heap); + + let handle_scope = &mut v8::HandleScope::new(isolate); + let context = v8::Context::new(handle_scope); + let scope = &mut v8::ContextScope::new(handle_scope, context); + let global = context.global(scope); + { + let func = v8::Function::new(scope, op_make_wrap).unwrap(); + let name = v8::String::new(scope, "make_wrap").unwrap(); + global.set(scope, name.into(), func.into()).unwrap(); + } + + let source = v8::String::new( + scope, + r#" + make_wrap(); // Inaccessible after scope. + globalThis.wrap = make_wrap(); // Accessible after scope. + "#, + ) + .unwrap(); + execute_script(scope, source); + + assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 0); + + scope + .request_garbage_collection_for_testing(v8::GarbageCollectionType::Full); + + assert!(TRACE_COUNT.load(Ordering::SeqCst) > 0); + assert_eq!(DROP_COUNT.load(Ordering::SeqCst), 1); + } +} + +fn execute_script( + context_scope: &mut v8::ContextScope, + script: v8::Local, +) { + let scope = &mut v8::HandleScope::new(context_scope); + let try_catch = &mut v8::TryCatch::new(scope); + + let script = v8::Script::compile(try_catch, script, None) + .expect("failed to compile script"); + + if script.run(try_catch).is_none() { + let exception_string = try_catch + .stack_trace() + .or_else(|| try_catch.exception()) + .map(|value| value.to_rust_string_lossy(try_catch)) + .unwrap_or_else(|| "no stack trace".into()); + + panic!("{}", exception_string); + } +}