Question

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.

Was it helpful?

Solution

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.

Licensed under: CC-BY-SA with attribution
Not affiliated with StackOverflow
scroll top