I am having trouble figuring out how to override a rb_ function (like rb_ivar_get) in c. I have the following code:
#include "ruby.h"
void Init_metaobject();
VALUE meta_cObject = Qnil;
VALUE meta_ivar_get(VALUE obj, VALUE mId, VALUE mWarn);
VALUE meta_ivar_set(VALUE obj, VALUE mId, VALUE val);
void Init_metaobject() {
meta_cObject = rb_define_class("MetaObject", rb_cObject);
rb_define_method(meta_cObject, "meta_ivar_get", meta_ivar_get, 2);
rb_define_method(meta_cObject, "meta_ivar_set", meta_ivar_set, 2);
}
VALUE
rb_ivar_get(obj, id)
VALUE obj;
ID id;
{
return meta_ivar_get(obj, ID2SYM(id), Qtrue);
}
VALUE
rb_attr_get(obj, id)
VALUE obj;
ID id;
{
return meta_ivar_get(obj, ID2SYM(id), Qfalse);
}
VALUE
rb_ivar_set(obj, id, val)
VALUE obj;
ID id;
VALUE val;
{
return meta_ivar_set(obj, ID2SYM(id), val);
}
VALUE
meta_ivar_get(obj, mId, mWarn)
VALUE obj;
VALUE mId;
VALUE mWarn;
{
VALUE val;
ID id = rb_to_id(id);
int warn = RTEST(warn);
switch (TYPE(obj)) {
case T_OBJECT:
case T_CLASS:
case T_MODULE:
if (ROBJECT(obj)->iv_tbl && st_lookup(ROBJECT(obj)->iv_tbl, id, &val))
return val;
break;
default:
if (FL_TEST(obj, FL_EXIVAR) || rb_special_const_p(obj))
return generic_ivar_get(obj, id, warn);
break;
}
if (warn) {
rb_warning("instance variable %s not initialized", rb_id2name(id));
}
return Qnil;
}
VALUE
meta_ivar_set(obj, mId, val)
VALUE obj;
VALUE mId;
VALUE val;
{
ID id = rb_to_id(mId);
if (!OBJ_TAINTED(obj) && rb_safe_level() >= 4)
rb_raise(rb_eSecurityError, "Insecure: can't modify instance variable");
if (OBJ_FROZEN(obj)) rb_error_frozen("object");
switch (TYPE(obj)) {
case T_OBJECT:
case T_CLASS:
case T_MODULE:
if (!ROBJECT(obj)->iv_tbl) ROBJECT(obj)->iv_tbl = st_init_numtable();
st_insert(ROBJECT(obj)->iv_tbl, id, val);
break;
default:
generic_ivar_set(obj, id, val);
break;
}
return val;
}
And the following test:
require 'metaobject'
class Tracker < MetaObject
attr_accessor :ivar
def initialize
@ivar = nil
end
def meta_ivar_get(symbol, warn)
puts "Instance variable, #{symbol}, retrieved"
super(symbol, warn)
end
def meta_ivar_set(symbol, obj)
puts "Instance variable, #{symbol}, changed to #{obj.inspect}"
super(symbol, obj)
end
end
obj = Tracker.new
obj.ivar = "Modified"
puts obj.ivar
The output of which is only:
Modified
My thoughts are that the ruby linker is veiling my definition of rb_ivar_get, rb_attr_get, and rb_ivar_set with its definition found in variables.c. Am I right? If so, how can I change it some that that my methods veil ruby's and not the other way around.
You can't do it with an extra .so file. The only way to edit an internal Ruby function is changing it directly. Go to variable.c
, edit it and recompile the whole interpreter. You can, instead, overwrite the attr_accessor
.
EDIT
Another solution with set_trace_func
. This is very slow and I don't thick this is the right way to do it. Anyway, here is it:
$instance_variables_table = {}
$instance_variable_created_proc = proc do |var, value|
puts "Instance variable #{var} created with #{value.inspect}."
end
$instance_variable_changed_proc = proc do |var, new, old|
puts "Instance variable #{var} changed from #{old.inspect} to #{new.inspect}."
end
set_trace_func(proc {|type, file, line, func, binding, mod|
unless type == "call"
eval("instance_variables", binding).each do |iv|
value = eval("instance_variable_get(:#{iv})", binding)
if $instance_variables_table.has_key? iv
if $instance_variables_table[iv] != value
new = value
old = $instance_variables_table[iv]
$instance_variable_changed_proc[iv, new, old]
end
else
$instance_variable_created_proc[iv, value]
end
end
end
$instance_variables_table = {}
eval("instance_variables", binding).each do |iv|
$instance_variables_table[iv] = eval("instance_variable_get(:#{iv})", binding)
end
})
Test code:
class A
def initialize
@test = 1
@test = 2
end
def a
@test = 3
end
end
A.new.a
Output:
Instance variable @test created with 1.
Instance variable @test changed from 1 to 2.
Instance variable @test changed from 2 to 3.
I'm not sure if it works in all cases or if it can be simplified. If you want to do it in a real application, edit variable.c
.