diff --git a/ext/v8/handle.h b/ext/v8/handle.h new file mode 100644 index 00000000..a79841ff --- /dev/null +++ b/ext/v8/handle.h @@ -0,0 +1,124 @@ +// -*- mode: c++ -*- +#ifndef RR_HANDLE_H +#define RR_HANDLE_H +#include "rr.h" + +namespace rr { + class Handle : public Ref { + public: + struct Finalizer; + inline Handle(VALUE value) : Ref(value) {} + + static inline void Init() { + ClassBuilder("Handle"). + defineSingletonMethod("New", &New). + defineMethod("IsEmpty", &IsEmpty). + defineMethod("SetWeak", &SetWeak). + defineMethod("ClearWeak", &ClearWeak). + store(&Class); + } + + /** + * Creates a New Handle to this object of the same class. If you + * have a V8::C::Object, then V8::C::Handle::New(object) will also + * be a V8::C::Object and represent a completely new handle to the + * object that will also prevent the object from being garbage + * collected by v8 (provided it has not be Reset()) + */ + static VALUE New(VALUE self, VALUE other) { + if (!rb_funcall(other, rb_intern("kind_of?"), 1, Handle::Class)) { + rb_raise(rb_eArgError, "not a V8::C::Handle"); + return Qnil; + } else { + VALUE cls = rb_class_of(other); + Ref ref(other); + v8::Isolate* isolate(ref); + v8::Local handle(ref); + return Data_Wrap_Struct(cls, 0, &destroy, new Holder(isolate, handle)); + } + } + + /** + * Calls v8::Handle::SetWeak, but the API is slightly different + * than the C++. The only parameter is a callable object that will + * be enqueued when value referenced by this handle is garbage + * collected. This code will not be called immediately. Instead, + * each callable must be iterated through from within Ruby code + * using the Isolate#__EachV8Finalizer__ method. Which will + * dequeue all finalizers that have not been yet run and yield + * them to the passed block. The sequence is roughly this: + * + * 1. value becomes finalizable + * 2. the v8 native finalizer runs. + * 3. Ruby callable is enqueued and will be seen by the next + * invocation of __EachV8Finalizer__ + */ + static VALUE SetWeak(VALUE self, VALUE callback) { + Handle handle(self); + Isolate isolate((v8::Isolate*)handle); + + // make sure this callback is not garbage collected + isolate.retainObject(callback); + + Holder* holder(handle.unwrapHolder()); + Finalizer* finalizer = new Finalizer(holder->cell, callback); + + // mark weak and install the callback + holder->cell->SetWeak(finalizer, &finalize, v8::WeakCallbackType::kParameter); + return Qnil; + } + + static VALUE ClearWeak(VALUE self) { + Handle handle(self); + Locker lock(handle); + + Holder* holder(handle.unwrapHolder()); + + Finalizer* finalizer = holder->cell->ClearWeak(); + delete finalizer; + return Qnil; + } + + static VALUE IsEmpty(VALUE self) { + Handle handle(self); + Locker lock(handle); + + Holder* holder(handle.unwrapHolder()); + return Bool(holder->cell->IsEmpty()); + } + + static void finalize(const v8::WeakCallbackInfo& info) { + Isolate isolate(info.GetIsolate()); + Finalizer* finalizer = info.GetParameter(); + + // clear the storage cell. This is required by the V8 API. + finalizer->cell->Reset(); + + // notify that this finalizer is ready to run. + isolate.v8FinalizerReady(finalizer->callback); + delete finalizer; + } + + /** + * A simple data structure to hold the objects necessary for + * finalization. + */ + struct Finalizer { + Finalizer(v8::Persistent* cell_, VALUE code) : + cell(cell_), callback(code) { + } + + /** + * The storage cell that is held weakly. + */ + v8::Persistent* cell; + + /** + * The Ruby callable representing this finalizer. + */ + VALUE callback; + }; + }; +} + +#endif /* RR_HANDLE_H */ diff --git a/ext/v8/init.cc b/ext/v8/init.cc index 0a0a9a29..28f68243 100644 --- a/ext/v8/init.cc +++ b/ext/v8/init.cc @@ -16,6 +16,7 @@ extern "C" { V8::Init(); DefineEnums(); Isolate::Init(); + Handle::Init(); Handles::Init(); Context::Init(); Maybe::Init(); diff --git a/ext/v8/isolate.cc b/ext/v8/isolate.cc index b14e532e..802a9453 100644 --- a/ext/v8/isolate.cc +++ b/ext/v8/isolate.cc @@ -15,7 +15,8 @@ namespace rr { defineMethod("ThrowException", &ThrowException). defineMethod("SetCaptureStackTraceForUncaughtExceptions", &SetCaptureStackTraceForUncaughtExceptions). defineMethod("IdleNotificationDeadline", &IdleNotificationDeadline). - + defineMethod("RequestGarbageCollectionForTesting", &RequestGarbageCollectionForTesting). + defineMethod("__EachV8Finalizer__", &__EachV8Finalizer__). store(&Class); } @@ -30,7 +31,7 @@ namespace rr { v8::Isolate* isolate = v8::Isolate::New(create_params); isolate->SetData(0, data); - isolate->AddGCPrologueCallback(&clearReferences); + isolate->AddGCPrologueCallback(&clearReferences, v8::kGCTypeAll); data->isolate = isolate; return Isolate(isolate); @@ -66,10 +67,31 @@ namespace rr { return Qnil; } - VALUE Isolate::IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds) { Isolate isolate(self); Locker lock(isolate); return Bool(isolate->IdleNotificationDeadline(NUM2DBL(deadline_in_seconds))); } + + VALUE Isolate::RequestGarbageCollectionForTesting(VALUE self) { + Isolate isolate(self); + Locker lock(isolate); + isolate->RequestGarbageCollectionForTesting(v8::Isolate::kFullGarbageCollection); + return Qnil; + } + VALUE Isolate::__EachV8Finalizer__(VALUE self) { + if (!rb_block_given_p()) { + rb_raise(rb_eArgError, "Expected block"); + return Qnil; + } + int state(0); + { + Isolate isolate(self); + isolate.eachV8Finalizer(&state); + } + if (state != 0) { + rb_jump_tag(state); + } + return Qnil; + } } diff --git a/ext/v8/isolate.h b/ext/v8/isolate.h index c60c571e..156ebba6 100644 --- a/ext/v8/isolate.h +++ b/ext/v8/isolate.h @@ -34,7 +34,10 @@ namespace rr { static VALUE IsExecutionTerminating(VALUE self); static VALUE CancelTerminateExecution(VALUE self); static VALUE ThrowException(VALUE self, VALUE error); + static VALUE IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds); + static VALUE RequestGarbageCollectionForTesting(VALUE self); static VALUE SetCaptureStackTraceForUncaughtExceptions(VALUE self, VALUE capture, VALUE stack_limit, VALUE options); + static VALUE __EachV8Finalizer__(VALUE self); inline Isolate(IsolateData* data_) : data(data_) {} inline Isolate(v8::Isolate* isolate) : @@ -155,6 +158,45 @@ namespace rr { rb_funcall(data->retained_objects, rb_intern("remove"), 1, object); } + /** + * Indicate that a finalizer that had been associated with a given + * V8 object is now ready to run because that V8 object has now + * been garbage collected. + * + * This can be called from anywhere and does not need to hold + * either Ruby or V8 locks. It is designed though to be called + * from the V8 GC callback that determines that the object is no + * more. + */ + inline void v8FinalizerReady(VALUE code) { + data->v8_finalizer_queue.enqueue(code); + } + + /** + * Iterates through all of the V8 finalizers that have been marked + * as ready and yields them. They wil be dequeued after this + * point, and so will never be seen again. + */ + inline void eachV8Finalizer(int* state) { + VALUE finalizer; + while (data->v8_finalizer_queue.try_dequeue(finalizer)) { + rb_protect(&yieldOneV8Finalizer, finalizer, state); + // we no longer need to retain this object from garbage + // collection. + releaseObject(finalizer); + if (*state != 0) { + break; + } + } + } + /** + * Yield a single value. This is wrapped in a function, so that + * any exceptions that happen don't blow out the stack. + */ + static VALUE yieldOneV8Finalizer(VALUE finalizer) { + return rb_yield(finalizer); + } + /** * The `gc_mark()` callback for this Isolate's * Data_Wrap_Struct. It releases all pending Ruby objects. @@ -190,9 +232,6 @@ namespace rr { } } - - static VALUE IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds); - /** * Recent versions of V8 will segfault unless you pass in an * ArrayBufferAllocator into the create params of an isolate. This @@ -248,6 +287,19 @@ namespace rr { */ ConcurrentQueue rb_release_queue; + /** + * Sometimes it is useful to get a callback into Ruby whenever a + * JavaScript object is garbage collected by V8. This is done by + * calling v8_object._DefineFinalizer(some_proc). However, we + * cannot actually run this Ruby code inside the V8 garbage + * collector. It's not safe! It might end up allocating V8 + * objects, or doing all kinds of who knows what! Instead, the + * ruby finalizer gets pushed onto this queue where it can be + * invoked later from ruby code with a call to + * isolate.__RunV8Finalizers__! + */ + ConcurrentQueue v8_finalizer_queue; + /** * Contains a number of tokens representing all of the live Ruby * references that are currently active in this Isolate. Every diff --git a/ext/v8/rr.h b/ext/v8/rr.h index 37a5e4d0..0e01d375 100644 --- a/ext/v8/rr.h +++ b/ext/v8/rr.h @@ -30,9 +30,9 @@ inline VALUE not_implemented(const char* message) { #include "isolate.h" #include "ref.h" - #include "v8.h" #include "locks.h" +#include "handle.h" #include "handles.h" #include "context.h" diff --git a/ext/v8/v8.cc b/ext/v8/v8.cc index 4bdc60d5..16dce5e2 100644 --- a/ext/v8/v8.cc +++ b/ext/v8/v8.cc @@ -13,7 +13,7 @@ namespace rr { ClassBuilder("V8"). // defineSingletonMethod("IdleNotification", &IdleNotification). - // defineSingletonMethod("SetFlagsFromString", &SetFlagsFromString). + defineSingletonMethod("SetFlagsFromString", &SetFlagsFromString). // defineSingletonMethod("SetFlagsFromCommandLine", &SetFlagsFromCommandLine). // defineSingletonMethod("PauseProfiler", &PauseProfiler). // defineSingletonMethod("ResumeProfiler", &ResumeProfiler). @@ -30,6 +30,11 @@ namespace rr { defineSingletonMethod("GetVersion", &GetVersion); } + VALUE V8::SetFlagsFromString(VALUE self, VALUE string) { + v8::V8::SetFlagsFromString(RSTRING_PTR(string), RSTRING_LEN(string)); + return Qnil; + } + VALUE V8::Dispose(VALUE self) { v8::V8::Dispose(); v8::V8::ShutdownPlatform(); diff --git a/ext/v8/v8.h b/ext/v8/v8.h index a8f5ca55..fa4b7104 100644 --- a/ext/v8/v8.h +++ b/ext/v8/v8.h @@ -5,7 +5,7 @@ namespace rr { static void Init(); // static VALUE IdleNotification(int argc, VALUE argv[], VALUE self); - // static VALUE SetFlagsFromString(VALUE self, VALUE string); + static VALUE SetFlagsFromString(VALUE self, VALUE string); // static VALUE SetFlagsFromCommandLine(VALUE self, VALUE args, VALUE remove_flags); // static VALUE AdjustAmountOfExternalAllocatedMemory(VALUE self, VALUE change_in_bytes); // static VALUE PauseProfiler(VALUE self); diff --git a/ext/v8/value.cc b/ext/v8/value.cc index ed59380e..9decc1bc 100644 --- a/ext/v8/value.cc +++ b/ext/v8/value.cc @@ -3,7 +3,7 @@ namespace rr { void Value::Init() { - ClassBuilder("Value"). + ClassBuilder("Value", Handle::Class). defineMethod("IsUndefined", &IsUndefined). defineMethod("IsNull", &IsNull). defineMethod("IsTrue", &IsTrue). diff --git a/spec/c/handle_spec.rb b/spec/c/handle_spec.rb new file mode 100644 index 00000000..38e2d3cd --- /dev/null +++ b/spec/c/handle_spec.rb @@ -0,0 +1,83 @@ +require 'c_spec_helper' + +describe V8::C::Handle do + before { V8::C::V8::SetFlagsFromString("--expose_gc") } + after { V8::C::V8::SetFlagsFromString("") } + + describe "Registering a v8 GC callback" do + before do + @isolate = V8::C::Isolate::New() + V8::C::HandleScope(@isolate) do + @context = V8::C::Context::New(@isolate) + @context.Enter() + @object = V8::C::Object::New(@isolate) + @results = {} + @object.SetWeak(proc { @did_finalize = true}) + @context.Exit() + end + @isolate.RequestGarbageCollectionForTesting() + @isolate.__EachV8Finalizer__(&:call) + end + after do + V8::C::V8::SetFlagsFromString("") + end + + it "runs registered V8 finalizer procs when a v8 object is garbage collected" do + expect(@did_finalize).to be_truthy + end + end + + describe "Capturing a new handle to an existing object" do + before do + @isolate = V8::C::Isolate::New() + V8::C::HandleScope(@isolate) do + @context = V8::C::Context::New(@isolate) + @context.Enter() + @object = V8::C::Object::New(@isolate) + @other = V8::C::Handle::New(@object) + @context.Exit() + end + end + it "can call methods on it" do + V8::C::HandleScope(@isolate) do + expect(@other.GetIdentityHash()).to eq @object.GetIdentityHash() + end + end + + describe ". Then making the original handle weak" do + before do + V8::C::HandleScope(@isolate) do + @results = {} + @object.SetWeak(proc { @did_finalize = true}) + end + @isolate.RequestGarbageCollectionForTesting() + @isolate.__EachV8Finalizer__(&:call) + end + it "does not gc the v8 object" do + V8::C::HandleScope(@isolate) do + expect(@object.IsEmpty()).to_not be_truthy + expect(@did_finalize).to_not be_truthy + end + end + + describe ". Then making the second handle weak" do + before do + V8::C::HandleScope(@isolate) do + @other.SetWeak(proc { @did_finalize = true }) + end + @isolate.RequestGarbageCollectionForTesting() + @isolate.__EachV8Finalizer__(&:call) + end + it "garbage collects the v8 object" do + expect(@did_finalize).to be_truthy + end + it "indicates that both object handles are now empty" do + V8::C::HandleScope(@isolate) do + expect(@object.IsEmpty()).to be_truthy + expect(@other.IsEmpty()).to be_truthy + end + end + end + end + end +end