// This is a sketch of how to make caching of Ruby objects work with | |
// Ruby's GC under Clownfish. We use a registry that keeps a list of all | |
// Ruby objects created from Clownfish. This list is walked during GC to | |
// mark all objects with a positive Clownfish refcount and prevent them | |
// from being destroyed if all references from Ruby space were dropped. | |
// | |
// Once a Clownfish object has been passed to Ruby, it lives on even with | |
// a zero Clownfish refcount. This means that there are no more references | |
// from Clownfish but possibly from Ruby space. The Clownfish refcount | |
// might even become positive again if an object is passed back to | |
// Clownfish from Ruby. | |
// | |
// The registry uses a free list to add and remove values in amortized | |
// constant time. This makes it necessary to store a registry index in | |
// cfish_Obj instead of a Ruby VALUE, resulting in an additional | |
// indirection. This also means that the registry can't be compacted. | |
// | |
// Free list nodes use the LSB as marker. | |
typedef struct { | |
size_t ruby_reg_index; // Used to lookup cached Ruby object. | |
size_t ref_count; | |
Class *klass; | |
} Obj; | |
typedef union { | |
VALUE ruby_value; | |
size_t free_list_node; | |
} RubyRegEntry; | |
typedef struct { | |
RubyRegEntry *entries; | |
size_t free_list_head; // Index, not a node. | |
size_t size; | |
} RubyReg; | |
static RubyReg *ruby_reg; | |
void* | |
Obj_To_Host(Obj *self) { | |
VALUE v; | |
if (self->ruby_reg_index) { | |
v = ruby_reg->entries[self->ruby_reg_index].ruby_value; | |
} | |
else { | |
VALUE ruby_class = Class_find_ruby_class(self->klass); | |
v = Data_Wrap_Struct(ruby_class, NULL, Obj_ruby_gc_free, self); | |
self->ruby_reg_index = RubyReg_add(ruby_reg, v); | |
} | |
return (void*)v; | |
} | |
void | |
Obj_dec_refcnt(Obj *self) { | |
if (--self->ref_count == 0 && !self->ruby_reg_index) { | |
// Object was never passed to Ruby. | |
Obj_Destroy(self); | |
} | |
} | |
void | |
Obj_ruby_gc_free(void *arg) { | |
Obj *self = (Obj*)arg; | |
RubyReg_remove(ruby_reg, self->ruby_reg_index); | |
Obj_Destroy(self); | |
} | |
void | |
RubyReg_ruby_gc_mark(void *arg) { | |
RubyReg *self = (RubyReg*)arg; | |
for (size_t i = 0; i < self->size; i++) { | |
RubyRegEntry entry = self->entries[i]; | |
if (S_on_free_list(entry)) { continue; } | |
VALUE v = entry.ruby_value; | |
Obj *obj = (Obj*)DATA_PTR(v); | |
if (obj->ref_count > 0) { | |
// Only mark objects with a positive refcount. | |
rb_gc_mark(v); | |
} | |
} | |
} | |
RubyReg* | |
RubyReg_new(size_t size) { | |
RubyReg *self = (RubyReg*)malloc(sizeof(RubyReg)); | |
RubyRegEntry *entries = (RubyRegEntry*)malloc(size * sizeof(RubyRegEntry)); | |
entries[0] = 0; // Unused | |
// Initialize free list. | |
for (size_t i = 1; i < size - 1; i++) { | |
entries[i].free_list_node = S_make_free_list_node(i + 1); | |
} | |
entries[size-1] = S_make_free_list_node(0); | |
self->entries = entries; | |
self->free_list_head = 1; | |
self->size = size; | |
return self; | |
} | |
size_t | |
RubyReg_add(RubyReg *self, VALUE v) { | |
if (!self->free_list_head) { | |
RubyReg_grow(self); | |
} | |
size_t index = self->free_list_head; | |
RubyRegEntry *entry = &self->entries[index]; | |
self->free_list_head = S_free_list_index(entry->free_list_node); | |
entry->ruby_val = v; | |
return index; | |
} | |
RubyReg_remove(RubyReg *self, size_t index) { | |
self->entries[index].free_list_node | |
= S_make_free_list_node(self->free_list_head); | |
self->free_list_head = index; | |
} | |
static inline size_t | |
S_make_free_list_node(size_t index) { | |
return (index << 1) | 1; | |
} | |
static inline bool | |
S_on_free_list(Entry entry) { | |
return entry.free_list_node & 1; | |
} | |
static inline size_t | |
S_free_list_index(size_t node) { | |
return node >> 1; | |
} |
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment