0
0
Fork 0
mirror of https://github.com/denoland/rusty_v8.git synced 2024-12-25 16:49:29 -05:00

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
This commit is contained in:
Divy Srivastava 2023-10-30 08:10:15 -07:00 committed by GitHub
parent fe574ecc4d
commit 7072da4199
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
7 changed files with 743 additions and 0 deletions

129
examples/cppgc-object.rs Normal file
View file

@ -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<u16>,
}
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<v8::HandleScope>,
script: v8::Local<v8::String>,
) {
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);
}
}

91
examples/cppgc.rs Normal file
View file

@ -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<v8::cppgc::Member<Rope>>,
}
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<v8::cppgc::Member<Rope>>) -> Box<Rope> {
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();
}

View file

@ -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 <typename T>
@ -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<RustObj> {
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<v8::CppHeap> 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<RustObj>(heap->GetAllocationHandle(), obj, trace, destroy);
}
void cppgc__visitor__trace(cppgc::Visitor* visitor, RustObj* member) {
visitor->Trace(*member);
}
} // extern "C"

294
src/cppgc.rs Normal file
View file

@ -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<Platform>) {
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<Foo> }
///
/// 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<T: GarbageCollected>(&self, member: &Member<T>) {
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<Platform>,
params: HeapCreateParams,
) -> UniqueRef<Heap> {
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<T: GarbageCollected> {
pub handle: *mut (),
ptr: *mut T,
}
impl<T: GarbageCollected> Member<T> {
/// 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<T: GarbageCollected> std::ops::Deref for Member<T> {
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<T: GarbageCollected>(
heap: &Heap,
obj: Box<T>,
) -> Member<T> {
unsafe extern "C" fn destroy<T>(obj: *mut ()) {
let _ = Box::from_raw(obj as *mut T);
}
unsafe { make_garbage_collected_raw(heap, Box::into_raw(obj), destroy::<T>) }
}
/// # 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<T: GarbageCollected>(
heap: &Heap,
obj: *mut T,
destroy: DestroyFn,
) -> Member<T> {
unsafe extern "C" fn trace<T: GarbageCollected>(
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::<T>,
destroy,
);
Member { handle, ptr: obj }
}

View file

@ -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) };

View file

@ -31,6 +31,7 @@ mod array_buffer;
mod array_buffer_view;
mod bigint;
mod context;
pub mod cppgc;
mod data;
mod date;
mod exception;

137
tests/test_cppgc.rs Normal file
View file

@ -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<v8::Platform>,
}
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<v8::HandleScope>,
script: v8::Local<v8::String>,
) {
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);
}
}