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

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

object = Object.new
class << object
  def foo; "object#foo" 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
      old_critical = Thread.critical
      Thread.critical = true
      disabled_gc = !GC.disable

      GC.enable if disabled_gc
      Thread.critical = old_critical


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}

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

class Object
  def prototype
    return self.class if immediate?

    sklass = class << self; self end
      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))
    ret = nil
    Internal.critical do
        old_flags = internal_sklass.flags
        internal_sklass.flags &= ~Internal::FL_SINGLETON
        ret = Class.new(sklass)
        internal_sklass.flags = old_flags

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