eigenclass logo
MAIN  Index  Search  Changes  PageRank  Login

Tricking that old, picky interpreter: prototype-based OOP

Ruby's object model has been becoming stricter as of late, preventing some imaginative (albeit ultimately useless?) tricks like the following prototypish OOP:

a = Object.new
def a.foo; "a#foo" end
a.foo                                              # => "a#foo"
ProtoA = Class.new(class << a; self.dup end)
b = ProtoA.new
b.foo                                              # => "a#foo"
RUBY_VERSION                                       # => "1.8.2"

Nowadays, that snippet would die on the Class#dup of the singleton class:

a = Object.new
def a.foo; "a#foo" end
a.foo                                              # => "a#foo"
ProtoA = Class.new(class << a; self.dup end)
b = ProtoA.new
b.foo                                              # => 
# ~> -:4:in `initialize_copy': can't copy singleton class (TypeError)
# ~> 	from -:4

This doesn't mean we cannot do it, though, but it requires some black magic:

a = "foo"
def a.bar; "A#bar" end
proto = a.prototype
proto                                              # => #<Class:0xb7d43f50>
proto.superclass                                   # => #<Class:#<String:0xb7d45544>>
orig = String.instance_methods
proto.instance_methods(true) - orig                # => ["bar"]

ueber_string = proto.new
ueber_string                                       # => ""
ueber_string.bar                                   # => "A#bar"

proto.class_eval do
  def initialize(x); super(x.to_s.upcase) end
end

proto.new("hello, world")                          # => "HELLO, WORLD"

object = Object.new
class << object
  def foo; "object#foo" end
end

obj = object.prototype.new
obj.foo                                            # => "object#foo"

def object.bar; "object#bar" end
obj.bar                                            # => "object#bar"

That's more powerful than a mere Class.new(singleton_class.dup) because changes in the singleton class affect descendents, as happens with normal inheritance (for both classes and modules).

Making it happen


As you might have guessed, the magic Object#prototype method used in the above snippet relies on the same conjuration as evil.rb, which I already used to unfreeze objects and to mess with class hierarchies.

Being built on top of Ruby/DL, as usual we need to declare the types of the internal structures we'll modify on to begin with:

require 'dl/struct'

module Internal
  extend DL::Importable

  typealias "VALUE", nil, nil, nil, "unsigned long"
  typealias "ID", nil, nil, nil, "unsigned long"

  Basic = ["long flags", "VALUE klass"]

  RBasic = struct Basic

  RObject = struct(Basic + ["st_table *iv_tbl"])
  
  RClass = struct(Basic + [
    "st_table *iv_tbl",
    "st_table *m_tbl",
    "VALUE super"
  ])

  
  def self.critical
    begin
      old_critical = Thread.critical
      Thread.critical = true
      disabled_gc = !GC.disable

      yield
    ensure
      GC.enable if disabled_gc
      Thread.critical = old_critical
    end
  end

end

Internal.critical is used to prevent unwanted changes due to GC or other threads while we're changing low-level stuff: were the rest of ruby to see an intermediate state of the structures we're changing, we'd crash in no time.

Immediate objects

Immediate objects have no singleton class so it doesn't make sense to try to use it as a prototype: in that case, we can just return self.class instead of giving up right away*1. This is what we'll do with Fixnums, Symbols, true, false and nil:

class Object
  def immediate?
    [Fixnum, Symbol, NilClass, TrueClass, FalseClass].any?{|klass| klass === self}
  end
end

Tricking ruby

The easiest way to subclass a singleton class is turning it into a normal one and letting ruby create a new one inheriting from it normally, before restoring the flag indicating singleton-ness.

module Internal
  T_ICLASS = 0x04
  T_MODULE = 0x05
  T_MASK   = 0x3f

  FL_FREEZE    = 1 << 10
  FL_SINGLETON = 1 << 11
end

class Object
  def prototype
    return self.class if immediate?

    sklass = class << self; self end
    begin
      internal_sklass = Internal::RClass.new(DL::PtrData.new(sklass.object_id * 2))
    rescue RangeError
      internal_sklass = Internal::RClass.new(DL::PtrData.new(2 ** 32 + sklass.object_id * 2))
    end
    ret = nil
    Internal.critical do
      begin
        old_flags = internal_sklass.flags
        internal_sklass.flags &= ~Internal::FL_SINGLETON
        ret = Class.new(sklass)
      ensure
        internal_sklass.flags = old_flags
      end
    end
    
    ret
  end
end


Did I ever tell you, you're the wind beneath my wings? - Danno (2006-02-15 (Wed) 11:37:15)

Thank you! It's nice to know this dark evilness is back.

Last modified:2006/02/15 09:48:25
Keyword(s):[blog] [ruby] [prototype] [oop] [dl] [singleton] [evil.rb]
References:[evil.rb wants love]

*1 this doesn't mean we can do much with the returned value, though. For instance, there is no Fixnum.new, so it wouldn't be possible to instantiate 42.prototype