Skip to content
This repository has been archived by the owner on Dec 4, 2023. It is now read-only.

Commit

Permalink
Allow registering Ruby callbacks for V8 objects.
Browse files Browse the repository at this point in the history
This allows Ruby code to listen directly for when V8 object is garbage
collected. This is done with the `__DefineFinalizer__` method which can
be invoked on any handle. E.g.

  v8_object.__DefineFinalizer__(method_that_generates_callable)

Just like in Ruby, care should be taken to ensure that the finalizer
does not reference the V8 object at all, otherwise, it might prevent it
from being garbage collected. The later, once v8_object has been gc'd,
the finalizer will be enqueued into an internal data structure that can
be accessed via the isolate's `__EachV8Finalizer`

  isolate.__EachV8Finalizer__ do |finalizer|
    finalizer.call()
  end

There was a question of whether to follow the strict V8 API for this,
and expose the `SetWeak` method, but this would mean actually making a
handle weak, which is fine, but then we would have to add a mechanism
to capture a strong reference from any reference which we don't have.

We may want to revisit this at some later date.
  • Loading branch information
cowboyd committed Aug 28, 2015
1 parent 5a1632a commit 5cebcd4
Show file tree
Hide file tree
Showing 9 changed files with 212 additions and 10 deletions.
81 changes: 81 additions & 0 deletions ext/v8/handle.h
Original file line number Diff line number Diff line change
@@ -0,0 +1,81 @@
// -*- mode: c++ -*-
#ifndef RR_DATA_H
#define RR_DATA_H
#include "rr.h"

namespace rr {
class Handle : public Ref<void> {
public:
struct Finalizer;
inline Handle(VALUE value) : Ref<void>(value) {}
inline Handle(v8::Isolate* isolate, v8::Local<void> data)
: Ref<void>(isolate, data) {}

static inline void Init() {
ClassBuilder("Handle").
defineMethod("__DefineFinalizer__", &__DefineFinalizer__).
store(&Class);
}
static VALUE __DefineFinalizer__(VALUE self, VALUE code) {
Handle handle(self);
v8::Isolate* isolate(handle);
new Finalizer(isolate, handle, code);
return Qnil;
}

/**
* Finalizer is responsible for capturing a piece of Ruby code and
* pushing it onto a queue once the V8 object points to is garbage
* collected. It is passed a handle and a Ruby object at which
* point it allocates a new storage cell that it holds
* weakly. Once the object referenced by its storage cell is
* garbage collected, the Finalizer enqueues the Ruby code so that
* it can be run later from Ruby.
*/
struct Finalizer {
Finalizer(Isolate isolate, v8::Local<void> handle, VALUE code) :
cell(new v8::Global<void>(isolate, handle)), callback(code) {

// make sure that this code does not get GC'd by Ruby.
isolate.retainObject(code);

// install the callback
cell->SetWeak<Finalizer>(this, &finalize, v8::WeakCallbackType::kParameter);
}

/**
* When this finalizer container is destroyed, also clear out
* the V8 storage cell and delete it.
*/
inline ~Finalizer() {
cell->Reset();
delete cell;
}

/**
* This implements a V8 GC WeakCallback, which will be invoked
* whenever the given object is garbage collected. It's job is to
* notify the Ruby isolate that the Ruby finalizer is ready to be
* run, as well as to clean up the
*/
static void finalize(const v8::WeakCallbackInfo<Finalizer>& info) {
Isolate isolate(info.GetIsolate());
Finalizer* finalizer(info.GetParameter());
isolate.v8FinalizerReady(finalizer->callback);
delete finalizer;
}

/**
* The storage cell that is held weakly.
*/
v8::Global<void>* cell;

/**
* The Ruby callable representing this finalizer.
*/
VALUE callback;
};
};
}

#endif /* RR_DATA_H */
1 change: 1 addition & 0 deletions ext/v8/init.cc
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ extern "C" {
V8::Init();
DefineEnums();
Isolate::Init();
Handle::Init();
Handles::Init();
Context::Init();
Maybe::Init();
Expand Down
28 changes: 25 additions & 3 deletions ext/v8/isolate.cc
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,8 @@ namespace rr {
defineMethod("ThrowException", &ThrowException).
defineMethod("SetCaptureStackTraceForUncaughtExceptions", &SetCaptureStackTraceForUncaughtExceptions).
defineMethod("IdleNotificationDeadline", &IdleNotificationDeadline).

defineMethod("RequestGarbageCollectionForTesting", &RequestGarbageCollectionForTesting).
defineMethod("__EachV8Finalizer__", &__EachV8Finalizer__).
store(&Class);
}

Expand All @@ -27,7 +28,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);
Expand All @@ -50,10 +51,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;
}
}
58 changes: 55 additions & 3 deletions ext/v8/isolate.h
Original file line number Diff line number Diff line change
Expand Up @@ -32,6 +32,9 @@ namespace rr {
static VALUE New(VALUE self);
static VALUE SetCaptureStackTraceForUncaughtExceptions(VALUE self, VALUE capture, VALUE stack_limit, VALUE options);
static VALUE ThrowException(VALUE self, VALUE error);
static VALUE IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds);
static VALUE RequestGarbageCollectionForTesting(VALUE self);
static VALUE __EachV8Finalizer__(VALUE self);

inline Isolate(IsolateData* data_) : data(data_) {}
inline Isolate(v8::Isolate* isolate) :
Expand Down Expand Up @@ -152,6 +155,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.
Expand Down Expand Up @@ -187,9 +229,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
Expand Down Expand Up @@ -245,6 +284,19 @@ namespace rr {
*/
ConcurrentQueue<VALUE> 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<VALUE> v8_finalizer_queue;

/**
* Contains a number of tokens representing all of the live Ruby
* references that are currently active in this Isolate. Every
Expand Down
2 changes: 1 addition & 1 deletion ext/v8/rr.h
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
7 changes: 6 additions & 1 deletion ext/v8/v8.cc
Original file line number Diff line number Diff line change
Expand Up @@ -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).
Expand All @@ -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();
Expand Down
2 changes: 1 addition & 1 deletion ext/v8/v8.h
Original file line number Diff line number Diff line change
Expand Up @@ -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);
Expand Down
2 changes: 1 addition & 1 deletion ext/v8/value.cc
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@
namespace rr {

void Value::Init() {
ClassBuilder("Value").
ClassBuilder("Value", Handle::Class).
defineMethod("IsUndefined", &IsUndefined).
defineMethod("IsNull", &IsNull).
defineMethod("IsTrue", &IsTrue).
Expand Down
41 changes: 41 additions & 0 deletions spec/c/handle_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,41 @@
require 'c_spec_helper'

describe V8::C::Handle do
before do
V8::C::V8::SetFlagsFromString("--expose_gc")
@isolate = V8::C::Isolate::New()
V8::C::HandleScope(@isolate) do
@context = V8::C::Context::New(@isolate)
@context.Enter()
GC.stress = true
2.times { v8_c_handle_spec_define_finalized_object(@isolate, self)}
@context.Exit()
end
@isolate.RequestGarbageCollectionForTesting()
@isolate.__EachV8Finalizer__ do |finalizer|
finalizer.call
end
end
after do
GC.stress = false
V8::C::V8::SetFlagsFromString("")
end

it "runs registered V8 finalizer procs when a v8 object is garbage collected" do
expect(@did_finalize).to be >= 1
end
end

def v8_c_handle_spec_did_finalize(spec)
proc {
spec.instance_eval do
@did_finalize ||= 0
@did_finalize += 1
end
}
end

def v8_c_handle_spec_define_finalized_object(isolate, spec)
object = V8::C::Object::New(isolate)
object.__DefineFinalizer__(v8_c_handle_spec_did_finalize(spec))
end

0 comments on commit 5cebcd4

Please sign in to comment.