23

I have a script that iterates using ObjectSpace#each_object with no args. Then it prints how many instances exist for each class.

I realized that some classes redefine the #class instance method, so I had to find another way to get the actual class; Let's say it's stored in variable "klass", and klass === object is true.

In Ruby 1.8 I could do this, assuming Object wasn't monkeypatched:

Object.instance_method(:class).bind(object).call

This worked for ActiveSupport::Duration instances:

# Ruby 1.8
# (tries to trick us)
20.seconds.class
=> Fixnum
# don't try to trick us, we can tell
Object.instance_method(:class).bind(20.seconds).call
=> ActiveSupport::Duration

But, in Ruby 1.9 this no longer works:

# Ruby 1.9
# we are not smart...
Object.instance_method(:class).bind(20.seconds).call
TypeError: bind argument must be an instance of Object
  from (irb):53:in `bind'
  from (irb):53
  from /Users/user/.rvm/rubies/ruby-1.9.2-p0/bin/irb:17:in `<main>'

It turns out that ActiveSupport::Duration subclasses ActiveSupport::BasicObject. The latter is made to subclass ::BasicObject in Ruby 1.9, so Object is excluded from the inheritance chain. This doesn't, and can't, happen in Ruby 1.8, so ActiveSupport::BasicObject is a subclass of Object.

I haven't found any way to detect the actual class of a Ruby 1.9 object that isn't an instance of Object. BasicObject in 1.9 is really bare-bones:

BasicObject.instance_methods
=> [:==, :equal?, :!, :!=, :instance_eval, :instance_exec, :__send__]

Ideas?

UPDATE:

Since ruby 1.9 reached end-of-life, I'm changing my accept to @indirect's answer. The mentions of ruby 1.9 above are merely for historical purposes, to show that the change from 1.8 to 1.9 was the original cause of my problem.

Kelvin
  • 20,119
  • 3
  • 60
  • 68
  • 1
    I had the same problem once and I gave up. There are a [few approaches](http://groups.google.com/group/comp.lang.ruby/browse_thread/thread/7e35b392a0b78768) but either didn't work for me or were too intrusive. Maybe you can redefine your question and point directly to want you are looking for in the origin and not trying to make one of the possible approaches to work. – fguillen Feb 08 '12 at 17:15
  • @fguillen Thanks for the link. The post about using self.inherited looks promising. – Kelvin Feb 08 '12 at 18:41
  • Accepted Frederick Cheung's answer. I chose it over my solution because it probably performs better. Others may have different requirements or constraints - just upvote whichever one you like. – Kelvin Feb 08 '12 at 21:49
  • Switched acceptance to paon's answer. It doesn't depend on external libs, and the only downside is that it allocates the eigenclass on every BasicObject you call it on. The only change I'd make would be to define the method as `__realclass__` instead of `class`. – Kelvin May 08 '12 at 19:12
  • FYI: see my new answer based on paon's solution. I kept the acceptance on paon's because the core idea was his. – Kelvin May 09 '12 at 18:46

8 Answers8

13

The following solution refers to the superclass of the eigenclass. As a consequence, it has the side effect of allocating the eigenclass (detectable by ObjectSpace.count_objects[:T_CLASS] in MRI). But since BasicObject#class is only invoked on blank slate objects (i.e. objects that are not kind-of Object, i.e. that are not Objects) the side effect also applies just for blank slate objects. For Objects, the standard Kernel#class is invoked.

class BasicObject
  def class
    (class << self; self end).superclass
  end
end

# tests:
puts RUBY_VERSION               # 1.9.2
class B < BasicObject; end
class X;               end
p BasicObject.new.class             # BasicObject
p B          .new.class             # B
p X          .new.class             # X
p               6.class             # Fixnum
p B.instance_method(:class).owner   # BasicObject
p X.instance_method(:class).owner   # Kernel
p          6.method(:class).owner   # Kernel

Edit - Note: Indeed, there is an issue with ActiveSupport::Duration. This class uses interception (method_missing) for redirecting messages to the :value attribute. As a consequence, it provides false introspection for its instances. To preserve this falsity, it is necessary to use another name for the class map, e.g. the proposed __realclass__. Thus, the modified solution might look like this:

class BasicObject
  def __realclass__; (class << self; self end).superclass end
end
class Object; alias __realclass__ class end

Another way of not invoking class << self on Objects is via Module#===, as suggested by Kelvin on this page.

paon
  • 378
  • 3
  • 12
  • This is pure genius... It can even be defined after all libs are loaded. But you should define a different method name like `__realclass__`, otherwise `20.seconds.class` won't return Fixnum -- that can break a lot of code. If you do that, I'll accept your answer. – Kelvin Apr 18 '12 at 22:54
  • Btw, I tried using `singleton_class.superclass` instead, but I got `TypeError: can't define singleton`. Guess BasicObject's are weird like that. – Kelvin Apr 18 '12 at 22:56
  • @Kelvin: `Fixnum`s (as well as other immediate values: `false`, `true`, `nil`, and `Symbol`s) are not blank slate objects so that `Kernel#class` gets invoked for them as before. Using `singleton_class` does not work because it is an instance method of `Kernel`. – paon Apr 19 '12 at 14:00
  • @Kelvin: +1 for the `::Object === self` test. I did not know that `Module#===` is the inverse of `Kernel#kind_of?`. – paon May 09 '12 at 10:41
  • I just realized why this works. It's because the singleton class is a `Class` instance. `Class` is a subclass of `Object`, which is why the `superclass` method is defined on instances. When the singleton gets created, the `BasicObject` subclass is set as its superclass. Weird ruby magic! – Kelvin Jul 26 '12 at 21:26
8

If you can upgrade to Ruby 2.0, you don't need to implement anything at all:

>> Kernel.instance_method(:class).bind(BasicObject.new).call
=> BasicObject
indirect
  • 3,470
  • 2
  • 25
  • 13
  • 1
    +1 Any links to info on why this works? `BasicObject` doesn't include `Kernel`, so why can a `Kernel` method be bound to it? – Kelvin Sep 05 '13 at 16:00
  • A similar trick would be: (class << BasicObject.new; self; end).superclass – rosenfeld Jun 17 '14 at 22:25
  • +1, pure Ruby awesomeness, no need for anything else + this works for anything sub-classed from BasicObject. Agree with @Kelvin though, be nice to see why this works. – MatzFan Aug 24 '16 at 19:54
  • 2
    Answer to @Kelvin's question: It appears that all module methods can be bound to _any_ object in this way. The source of MRI [UnboundMethod#bind](http://ruby-doc.org/core-2.3.1/UnboundMethod.html#method-i-bind) includes a guard clause to throw errors which begins: `if (!RB_TYPE_P(methclass, T_MODULE) &&...` before it goes on to test whether the object is `kind_of?` the unbound method's 'owner' class - nice to know. – MatzFan Aug 25 '16 at 15:33
  • Thanks @MatzFan . So the `bind` enforcement is weaker in ruby 2.x. I wonder what the rationale was behind this change. – Kelvin Nov 13 '17 at 19:39
5

I don't know about doing it in Ruby, but this is straightforward using the C API to Ruby. The RubyInline Gem makes adding bits of C to your Ruby code quite easy:

require 'inline'
class Example
  inline do |builder|  
    builder.c_raw_singleton <<SRC, :arity => 1
      VALUE true_class(VALUE self, VALUE to_test) {
        return rb_obj_class(to_test);
      }
SRC
   end
end

And then:

1.9.2p180 :033 > Example.true_class(20.minutes)
 => ActiveSupport::Duration 
the Tin Man
  • 158,662
  • 42
  • 215
  • 303
Frederick Cheung
  • 83,189
  • 8
  • 152
  • 174
  • This is quite elegant and doesn't seem to interfere with other parts the app. I've upvoted but need to consider a bit more before accepting. – Kelvin Feb 08 '12 at 18:27
  • Btw, I thought it might be possible to something similar in FFI, but I didn't see a way to convert a ruby object into a VALUE pointer and vice-versa. – Kelvin Feb 08 '12 at 18:39
  • There seems to be a minor bug in RubyInline - once the ~/.ruby_inline directory is created, then I can't define any more inline functions in irb. Inline is trying to get the mtime of '(irb)'. Loading it from an external file works fine. – Kelvin Feb 08 '12 at 19:17
5

fguillen's link made me think of this way.

Pros:

  1. It doesn't need external libraries.

Cons:

  1. It must be executed before loading any classes that subclass BasicObject.
  2. It adds a method to every new class

.

class BasicObject
  def self.inherited(klass)
    klass.send(:define_method, :__realclass__) { klass }
  end
  def __realclass__
    BasicObject
  end
end

# ensures that every Object will also have this method
class Object
  def __realclass__
    Object.instance_method(:class).bind(self).call
  end
end

require 'active_support/core_ext'

20.seconds.__realclass__  # => ActiveSupport::Duration

# this doesn't raise errors, so it looks like all objects respond to our method
ObjectSpace.each_object{|e| e.__realclass__ }
Kelvin
  • 20,119
  • 3
  • 60
  • 68
  • As I read the question, I was also thinking of an `inherited` hook... nice code, I like it! – Alex D Feb 08 '12 at 21:40
3

This is my modification of @paon's answer:

Reasoning behind the changes:

  • Method name doesn't clash with existing libs, e.g. the ActiveSupport::Duration instance behavior 2.seconds.class remains Fixnum.
  • Since Object doesn't have its own __realclass__ method, we want to avoid allocating the eigenclass for those instances. @paon's original answer did this inherently by defining the class method name.

class BasicObject
  def __realclass__
    ::Object === self ?
      # Note: to be paranoid about Object instances, we could 
      # use Object.instance_method(:class).bind(s).call.
      self.class :
      (class << self; self end).superclass
  end
end

# test
require 'active_support/core_ext/integer'
require 'active_support/core_ext/numeric'

duration = 2.seconds
string = 'hello world'
p duration.class  # => Fixnum
p string.class    # => String
GC.start
p ObjectSpace.count_objects[:T_CLASS]  # => 566

# creates the eigenclass
p duration.__realclass__  # => ActiveSupport::Duration
p ObjectSpace.count_objects[:T_CLASS]  # => 567

# doesn't create the eigenclass
p string.__realclass__  # => String
p ObjectSpace.count_objects[:T_CLASS]  # => 567
Kelvin
  • 20,119
  • 3
  • 60
  • 68
1
(class << object; self; end).superclass
rosenfeld
  • 1,730
  • 15
  • 19
0

The following code creates a BasicKernel module via duplication of the Kernel module and subsequent removal of all methods except the class method. The BasicKernel is included into the BasicObject class (just like Kernel is included into Object).

In req_methods, you can specify arbitrary subset of Kernel methods to be preserved.

class BasicObject
  include ::BasicKernel = ::Kernel.dup.module_eval {
    v = $VERBOSE
    $VERBOSE = nil               # suppress object_id warning
    req_methods = [:class]       # required methods (to be preserved)
    all_methods = public_instance_methods +
               protected_instance_methods +
                 private_instance_methods
    all_methods.each { |x| remove_method(x) unless req_methods.include?(x) }
    $VERBOSE = v
    self
  }
end

# tests:
puts RUBY_VERSION               # 1.9.2
class B < BasicObject; end
class X;               end
p BasicObject.new.class           # BasicObject
p B          .new.class           # B
p X          .new.class           # X
p B.instance_method(:class).owner # BasicKernel
p X.instance_method(:class).owner # Kernel
p Object.ancestors                # [Object, Kernel, BasicObject, BasicKernel]
p BasicKernel.instance_methods    # [:class]

Edit: See the Note in https://stackoverflow.com/a/10216927/641718

Community
  • 1
  • 1
paon
  • 378
  • 3
  • 12
  • This is a nice concept, but doesn't address the `ActiveSupport::Duration` issue, e.g. `2.seconds.class` *should* return Fixnum. – Kelvin Apr 23 '12 at 16:39
0

For the similar situation where you simply want a class you created that inherits from BasicObject to support the #class method, you can copy the method over from Kernel.

class Foo < BasicObject
  define_method(:class, ::Kernel.instance_method(:class))
end

f = Foo.new
puts f.class
=> Foo
Connor McKay
  • 580
  • 7
  • 13