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

Commit 750e6b4

Browse files
committed
Allow registering Ruby callbacks for V8 objects.
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.
1 parent 5a1632a commit 750e6b4

File tree

9 files changed

+212
-10
lines changed

9 files changed

+212
-10
lines changed

ext/v8/handle.h

Lines changed: 81 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,81 @@
1+
// -*- mode: c++ -*-
2+
#ifndef RR_HANDLE_H
3+
#define RR_HANDLE_H
4+
#include "rr.h"
5+
6+
namespace rr {
7+
class Handle : public Ref<void> {
8+
public:
9+
struct Finalizer;
10+
inline Handle(VALUE value) : Ref<void>(value) {}
11+
inline Handle(v8::Isolate* isolate, v8::Local<void> data)
12+
: Ref<void>(isolate, data) {}
13+
14+
static inline void Init() {
15+
ClassBuilder("Handle").
16+
defineMethod("__DefineFinalizer__", &__DefineFinalizer__).
17+
store(&Class);
18+
}
19+
static VALUE __DefineFinalizer__(VALUE self, VALUE code) {
20+
Handle handle(self);
21+
v8::Isolate* isolate(handle);
22+
new Finalizer(isolate, handle, code);
23+
return Qnil;
24+
}
25+
26+
/**
27+
* Finalizer is responsible for capturing a piece of Ruby code and
28+
* pushing it onto a queue once the V8 object points to is garbage
29+
* collected. It is passed a handle and a Ruby object at which
30+
* point it allocates a new storage cell that it holds
31+
* weakly. Once the object referenced by its storage cell is
32+
* garbage collected, the Finalizer enqueues the Ruby code so that
33+
* it can be run later from Ruby.
34+
*/
35+
struct Finalizer {
36+
Finalizer(Isolate isolate, v8::Local<void> handle, VALUE code) :
37+
cell(new v8::Global<void>(isolate, handle)), callback(code) {
38+
39+
// make sure that this code does not get GC'd by Ruby.
40+
isolate.retainObject(code);
41+
42+
// install the callback
43+
cell->SetWeak<Finalizer>(this, &finalize, v8::WeakCallbackType::kParameter);
44+
}
45+
46+
/**
47+
* When this finalizer container is destroyed, also clear out
48+
* the V8 storage cell and delete it.
49+
*/
50+
inline ~Finalizer() {
51+
cell->Reset();
52+
delete cell;
53+
}
54+
55+
/**
56+
* This implements a V8 GC WeakCallback, which will be invoked
57+
* whenever the given object is garbage collected. It's job is to
58+
* notify the Ruby isolate that the Ruby finalizer is ready to be
59+
* run, as well as to clean up the
60+
*/
61+
static void finalize(const v8::WeakCallbackInfo<Finalizer>& info) {
62+
Isolate isolate(info.GetIsolate());
63+
Finalizer* finalizer(info.GetParameter());
64+
isolate.v8FinalizerReady(finalizer->callback);
65+
delete finalizer;
66+
}
67+
68+
/**
69+
* The storage cell that is held weakly.
70+
*/
71+
v8::Global<void>* cell;
72+
73+
/**
74+
* The Ruby callable representing this finalizer.
75+
*/
76+
VALUE callback;
77+
};
78+
};
79+
}
80+
81+
#endif /* RR_HANDLE_H */

ext/v8/init.cc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@ extern "C" {
1616
V8::Init();
1717
DefineEnums();
1818
Isolate::Init();
19+
Handle::Init();
1920
Handles::Init();
2021
Context::Init();
2122
Maybe::Init();

ext/v8/isolate.cc

Lines changed: 25 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -12,7 +12,8 @@ namespace rr {
1212
defineMethod("ThrowException", &ThrowException).
1313
defineMethod("SetCaptureStackTraceForUncaughtExceptions", &SetCaptureStackTraceForUncaughtExceptions).
1414
defineMethod("IdleNotificationDeadline", &IdleNotificationDeadline).
15-
15+
defineMethod("RequestGarbageCollectionForTesting", &RequestGarbageCollectionForTesting).
16+
defineMethod("__EachV8Finalizer__", &__EachV8Finalizer__).
1617
store(&Class);
1718
}
1819

@@ -27,7 +28,7 @@ namespace rr {
2728
v8::Isolate* isolate = v8::Isolate::New(create_params);
2829

2930
isolate->SetData(0, data);
30-
isolate->AddGCPrologueCallback(&clearReferences);
31+
isolate->AddGCPrologueCallback(&clearReferences, v8::kGCTypeAll);
3132

3233
data->isolate = isolate;
3334
return Isolate(isolate);
@@ -50,10 +51,31 @@ namespace rr {
5051
return Qnil;
5152
}
5253

53-
5454
VALUE Isolate::IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds) {
5555
Isolate isolate(self);
5656
Locker lock(isolate);
5757
return Bool(isolate->IdleNotificationDeadline(NUM2DBL(deadline_in_seconds)));
5858
}
59+
60+
VALUE Isolate::RequestGarbageCollectionForTesting(VALUE self) {
61+
Isolate isolate(self);
62+
Locker lock(isolate);
63+
isolate->RequestGarbageCollectionForTesting(v8::Isolate::kFullGarbageCollection);
64+
return Qnil;
65+
}
66+
VALUE Isolate::__EachV8Finalizer__(VALUE self) {
67+
if (!rb_block_given_p()) {
68+
rb_raise(rb_eArgError, "Expected block");
69+
return Qnil;
70+
}
71+
int state(0);
72+
{
73+
Isolate isolate(self);
74+
isolate.eachV8Finalizer(&state);
75+
}
76+
if (state != 0) {
77+
rb_jump_tag(state);
78+
}
79+
return Qnil;
80+
}
5981
}

ext/v8/isolate.h

Lines changed: 55 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,9 @@ namespace rr {
3232
static VALUE New(VALUE self);
3333
static VALUE SetCaptureStackTraceForUncaughtExceptions(VALUE self, VALUE capture, VALUE stack_limit, VALUE options);
3434
static VALUE ThrowException(VALUE self, VALUE error);
35+
static VALUE IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds);
36+
static VALUE RequestGarbageCollectionForTesting(VALUE self);
37+
static VALUE __EachV8Finalizer__(VALUE self);
3538

3639
inline Isolate(IsolateData* data_) : data(data_) {}
3740
inline Isolate(v8::Isolate* isolate) :
@@ -152,6 +155,45 @@ namespace rr {
152155
rb_funcall(data->retained_objects, rb_intern("remove"), 1, object);
153156
}
154157

158+
/**
159+
* Indicate that a finalizer that had been associated with a given
160+
* V8 object is now ready to run because that V8 object has now
161+
* been garbage collected.
162+
*
163+
* This can be called from anywhere and does not need to hold
164+
* either Ruby or V8 locks. It is designed though to be called
165+
* from the V8 GC callback that determines that the object is no
166+
* more.
167+
*/
168+
inline void v8FinalizerReady(VALUE code) {
169+
data->v8_finalizer_queue.enqueue(code);
170+
}
171+
172+
/**
173+
* Iterates through all of the V8 finalizers that have been marked
174+
* as ready and yields them. They wil be dequeued after this
175+
* point, and so will never be seen again.
176+
*/
177+
inline void eachV8Finalizer(int* state) {
178+
VALUE finalizer;
179+
while (data->v8_finalizer_queue.try_dequeue(finalizer)) {
180+
rb_protect(&yieldOneV8Finalizer, finalizer, state);
181+
// we no longer need to retain this object from garbage
182+
// collection.
183+
releaseObject(finalizer);
184+
if (*state != 0) {
185+
break;
186+
}
187+
}
188+
}
189+
/**
190+
* Yield a single value. This is wrapped in a function, so that
191+
* any exceptions that happen don't blow out the stack.
192+
*/
193+
static VALUE yieldOneV8Finalizer(VALUE finalizer) {
194+
return rb_yield(finalizer);
195+
}
196+
155197
/**
156198
* The `gc_mark()` callback for this Isolate's
157199
* Data_Wrap_Struct. It releases all pending Ruby objects.
@@ -187,9 +229,6 @@ namespace rr {
187229
}
188230
}
189231

190-
191-
static VALUE IdleNotificationDeadline(VALUE self, VALUE deadline_in_seconds);
192-
193232
/**
194233
* Recent versions of V8 will segfault unless you pass in an
195234
* ArrayBufferAllocator into the create params of an isolate. This
@@ -245,6 +284,19 @@ namespace rr {
245284
*/
246285
ConcurrentQueue<VALUE> rb_release_queue;
247286

287+
/**
288+
* Sometimes it is useful to get a callback into Ruby whenever a
289+
* JavaScript object is garbage collected by V8. This is done by
290+
* calling v8_object._DefineFinalizer(some_proc). However, we
291+
* cannot actually run this Ruby code inside the V8 garbage
292+
* collector. It's not safe! It might end up allocating V8
293+
* objects, or doing all kinds of who knows what! Instead, the
294+
* ruby finalizer gets pushed onto this queue where it can be
295+
* invoked later from ruby code with a call to
296+
* isolate.__RunV8Finalizers__!
297+
*/
298+
ConcurrentQueue<VALUE> v8_finalizer_queue;
299+
248300
/**
249301
* Contains a number of tokens representing all of the live Ruby
250302
* references that are currently active in this Isolate. Every

ext/v8/rr.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -30,9 +30,9 @@ inline VALUE not_implemented(const char* message) {
3030
#include "isolate.h"
3131

3232
#include "ref.h"
33-
3433
#include "v8.h"
3534
#include "locks.h"
35+
#include "handle.h"
3636
#include "handles.h"
3737
#include "context.h"
3838

ext/v8/v8.cc

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ namespace rr {
1313

1414
ClassBuilder("V8").
1515
// defineSingletonMethod("IdleNotification", &IdleNotification).
16-
// defineSingletonMethod("SetFlagsFromString", &SetFlagsFromString).
16+
defineSingletonMethod("SetFlagsFromString", &SetFlagsFromString).
1717
// defineSingletonMethod("SetFlagsFromCommandLine", &SetFlagsFromCommandLine).
1818
// defineSingletonMethod("PauseProfiler", &PauseProfiler).
1919
// defineSingletonMethod("ResumeProfiler", &ResumeProfiler).
@@ -30,6 +30,11 @@ namespace rr {
3030
defineSingletonMethod("GetVersion", &GetVersion);
3131
}
3232

33+
VALUE V8::SetFlagsFromString(VALUE self, VALUE string) {
34+
v8::V8::SetFlagsFromString(RSTRING_PTR(string), RSTRING_LEN(string));
35+
return Qnil;
36+
}
37+
3338
VALUE V8::Dispose(VALUE self) {
3439
v8::V8::Dispose();
3540
v8::V8::ShutdownPlatform();

ext/v8/v8.h

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ namespace rr {
55

66
static void Init();
77
// static VALUE IdleNotification(int argc, VALUE argv[], VALUE self);
8-
// static VALUE SetFlagsFromString(VALUE self, VALUE string);
8+
static VALUE SetFlagsFromString(VALUE self, VALUE string);
99
// static VALUE SetFlagsFromCommandLine(VALUE self, VALUE args, VALUE remove_flags);
1010
// static VALUE AdjustAmountOfExternalAllocatedMemory(VALUE self, VALUE change_in_bytes);
1111
// static VALUE PauseProfiler(VALUE self);

ext/v8/value.cc

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,7 +3,7 @@
33
namespace rr {
44

55
void Value::Init() {
6-
ClassBuilder("Value").
6+
ClassBuilder("Value", Handle::Class).
77
defineMethod("IsUndefined", &IsUndefined).
88
defineMethod("IsNull", &IsNull).
99
defineMethod("IsTrue", &IsTrue).

spec/c/handle_spec.rb

Lines changed: 41 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,41 @@
1+
require 'c_spec_helper'
2+
3+
describe V8::C::Handle do
4+
before do
5+
V8::C::V8::SetFlagsFromString("--expose_gc")
6+
@isolate = V8::C::Isolate::New()
7+
V8::C::HandleScope(@isolate) do
8+
@context = V8::C::Context::New(@isolate)
9+
@context.Enter()
10+
GC.stress = true
11+
2.times { v8_c_handle_spec_define_finalized_object(@isolate, self)}
12+
@context.Exit()
13+
end
14+
@isolate.RequestGarbageCollectionForTesting()
15+
@isolate.__EachV8Finalizer__ do |finalizer|
16+
finalizer.call
17+
end
18+
end
19+
after do
20+
GC.stress = false
21+
V8::C::V8::SetFlagsFromString("")
22+
end
23+
24+
it "runs registered V8 finalizer procs when a v8 object is garbage collected" do
25+
expect(@did_finalize).to be >= 1
26+
end
27+
end
28+
29+
def v8_c_handle_spec_did_finalize(spec)
30+
proc {
31+
spec.instance_eval do
32+
@did_finalize ||= 0
33+
@did_finalize += 1
34+
end
35+
}
36+
end
37+
38+
def v8_c_handle_spec_define_finalized_object(isolate, spec)
39+
object = V8::C::Object::New(isolate)
40+
object.__DefineFinalizer__(v8_c_handle_spec_did_finalize(spec))
41+
end

0 commit comments

Comments
 (0)