Inside Ruby’s Object Model

May 21, 2026

How MRI Really Implements include, prepend, extend, Singleton Classes and Method Lookup

Ruby’s object model looks elegant from the outside:

module Logging
def call
puts "before"
super
end
end
class Service
prepend Logging
def call
puts "service"
end
end

But internally, MRI/CRuby performs a surprising amount of machinery to make this work.

Under the hood:

  • include does not copy methods
  • prepend rewires the method lookup chain
  • extend injects modules into singleton classes
  • class methods are singleton methods
  • inheritance is literally a linked list of internal structures
  • method lookup walks internal C-level tables

This article explores how Ruby’s object model actually works by following the MRI source code itself:

  • object.c
  • class.c
  • eval.c
  • vm_method.c
  • proc.c

The goal is not just to learn Ruby syntax.

The goal is to understand how Ruby thinks.


Tokyo Topographic Map
Built for Ruby on Rails

Build Maps Without
Google APIs

Generate beautiful production-ready maps directly from your Rails backend. Fast rendering, zero external dependencies, full control.

✓ No API fees ✓ Self-hosted ✓ Rails Native ✓ Fast Rendering
Why developers switch
Replace expensive map stacks.

Stop relying on third-party map billing and bloated JS libraries. Render static or dynamic maps directly in Ruby.

Try It Now
Tokyo MapView Demo

1. The Root of Ruby’s Object System

Everything in Ruby begins during interpreter boot.

Inside object.c, MRI initializes the root hierarchy of the language:

rb_cBasicObject = rb_define_class("BasicObject", Qnil);
rb_cObject = rb_define_class("Object", rb_cBasicObject);
rb_cModule = rb_define_class("Module", rb_cObject);
rb_cClass = rb_define_class("Class", rb_cModule);

This creates Ruby’s fundamental inheritance chain:

Class < Module < Object < BasicObject

Which means:

  • Class is itself an object
  • Class inherits from Module
  • modules are objects too
  • everything eventually inherits from BasicObject

This is one of Ruby’s most important design decisions: the object model is self-hosted.

Classes themselves participate in the same object system as ordinary objects.


2. Why Object Already Has Methods

After creating the root classes, MRI defines Kernel:

rb_mKernel = rb_define_module("Kernel");
rb_include_module(rb_cObject, rb_mKernel);

This is why every normal Ruby object automatically gets methods like:

puts
class
respond_to?
is_a?
nil?
object_id

Because Object includes Kernel.

So the real ancestor chain is closer to:

Object
Kernel (iclass proxy)
BasicObject

This is the first hint that Ruby internally inserts hidden nodes into the inheritance chain.

That hidden node is called T_ICLASS.

We’ll come back to it soon.


3. Module Is the Real Core of Ruby

Most developers think Class is the center of Ruby.

Internally, it’s actually Module.

Class inherits almost all behavior from Module.

That includes:

  • include
  • prepend
  • method visibility
  • constants
  • method definition
  • hooks
  • reflection
  • ancestors
  • metaprogramming

MRI defines these methods mostly inside:

  • object.c
  • eval.c
  • vm_method.c
  • proc.c

For example:

include
prepend
alias_method
define_method
public
private
protected
const_get
method_defined?
ancestors

are all methods on Module.

This means classes are essentially:

Modules that can instantiate objects

That’s a powerful mental model.


4. How include Really Works

Most developers assume include copies methods into a class.

Ruby does NOT do that.

Instead, MRI inserts a special internal proxy node into the superclass chain.

Example:

module Logging
def log
puts "hello"
end
end
class Service
include Logging
end

Most people imagine:

Service now owns log

But internally MRI builds something closer to:

Service
Logging(iclass)
Object
Kernel
BasicObject

The important detail:

Logging(iclass)

is NOT the original module.

It is an internal proxy node called:

T_ICLASS

MRI creates this node inside:

rb_include_module()

which eventually calls:

do_include_modules_at()

inside class.c.


5. The Secret of T_ICLASS

T_ICLASS is one of the most important structures in Ruby internals.

It acts like a lightweight proxy inserted into the ancestor chain.

The critical optimization:

The iclass SHARES the module method table.

Ruby does not duplicate methods.

Instead:

Service
Logging(iclass)
shared method table
Logging

That means:

module Logging
def log
"A"
end
end
class Service
include Logging
end
module Logging
def another
"B"
end
end

Service immediately sees another.

Because both point to the same internal method table.

This is extremely elegant.

It keeps:

  • includes cheap
  • memory usage low
  • dynamic modification possible

6. Why Duplicate Includes Don’t Happen

MRI checks whether a module already exists in the ancestor chain.

Internally it compares:

  • method tables
  • ancestor nodes
  • cyclic inclusion

So this:

include A
include A

silently becomes:

include A

No duplicate insertion occurs.

That logic also lives inside:

do_include_modules_at()

7. extend Is Just include on a Singleton Class

This is one of Ruby’s biggest “aha!” moments.

Consider:

obj.extend(MyModule)

Internally MRI roughly performs:

obj.singleton_class.include(MyModule)

That’s basically the entire mechanism.

In C:

rb_include_module(rb_singleton_class(obj), module);

So:

class User
end
module AdminFeatures
def admin?
true
end
end
u = User.new
u.extend(AdminFeatures)

creates something like:

u
#<Class:u>
AdminFeatures(iclass)
User
Object

The module becomes part of the object’s singleton class.

That’s why:

  • only this object gets the methods
  • other instances remain unaffected

8. Singleton Classes (Eigenclasses)

Every Ruby object can have a singleton class.

This class:

  • is anonymous
  • is created lazily
  • lives between the object and its real class

Example:

obj = "hello"
def obj.wave
"👋"
end

MRI creates:

obj
#<Class:obj>
String
Object

The method wave is stored in:

#<Class:obj>

NOT in String.

That’s why only this instance responds to it.


9. Why Class Methods Work

This also explains class methods.

When you write:

class User
def self.find
end
end

Ruby stores find inside:

User.singleton_class

Because classes are objects too.

So internally:

User
#<Class:User>

The method lives on:

#<Class:User>

This is why class methods are just singleton methods on class objects.


10. The Hidden Parallel Hierarchy

Ruby does something even more fascinating.

Singleton classes inherit from singleton classes.

Example:

class Animal
end
class Dog < Animal
end

Normal hierarchy:

Dog → Animal → Object

Singleton hierarchy:

#<Class:Dog>
#<Class:Animal>
#<Class:Object>

This is exactly why class methods are inherited.

Without this parallel hierarchy, Ruby class methods would not propagate through inheritance.


11. prepend Changes Everything

prepend is fundamentally different from include.

include inserts AFTER the class.

prepend inserts BEFORE the class.

Example:

module Logging
def call
puts "before"
super
end
end
class Service
prepend Logging
def call
puts "service"
end
end

Ancestors become:

[Logging, Service, Object, Kernel, BasicObject]

NOT:

[Service, Logging, Object]

That’s why Logging#call executes first.


12. The Real Magic Behind prepend

Internally prepend is much more complicated than include.

MRI creates a hidden structure called:

origin node

Before prepend:

Service(method table)
Object

After prepend:

Service(empty method table)
Logging(iclass)
Service_origin(original methods)
Object

MRI moves the original methods into:

Service_origin

and leaves the class itself almost empty.

This allows lookup order to become:

Logging first
original Service methods later

WITHOUT losing the original implementation.

That is why:

super

works correctly inside prepended modules.

This is one of the smartest pieces of engineering in MRI.


13. Method Lookup in MRI

Eventually Ruby must resolve methods.

MRI performs method lookup inside vm_method.c.

Conceptually it works like this:

while (klass) {
if (method_found)
return method;
klass = RCLASS_SUPER(klass);
}

Ruby literally walks the ancestor chain node by node.

The search order is:

object
singleton class
prepended modules
class
included modules
superclass
its modules
Object
Kernel
BasicObject
method_missing

This chain is Ruby’s true runtime execution model.


14. Complete MRO Example

Consider:

module M1; end
module M2; end
module M3; end
class Base
include M1
end
class Child < Base
prepend M2
include M3
end

MRI resolves ancestors as:

[M2, Child, M3, Base, M1, Object, Kernel, BasicObject]

Important rules:

  1. prepended modules come first
  2. then the class itself
  3. included modules come afterward
  4. superclass chain comes later

Understanding this explains most “Ruby magic”.


15. Refinements: Scoped Monkey Patching

Refinements are Ruby’s attempt to make monkey patching safer.

Example:

module StringRefinement
refine String do
def shout
upcase
end
end
end
using StringRefinement

Refinements:

  • are lexical
  • are scope-local
  • are NOT global monkey patches

MRI marks refinements internally using flags like:

RMODULE_IS_REFINEMENT

and prevents them from being:

  • included
  • prepended

because they are special scoped modules.


16. Ruby’s Object Model Is a Linked Graph

At a high level, MRI’s object system is really:

  • linked structures
  • shared method tables
  • proxy nodes
  • singleton classes
  • dynamic lookup chains

Not a static inheritance tree.

Ruby’s flexibility comes from:

  • inserting nodes dynamically
  • rewriting lookup order
  • sharing internal method tables
  • treating classes as ordinary objects

That’s why Ruby can support:

  • monkey patching
  • mixins
  • metaprogramming
  • refinements
  • singleton methods
  • dynamic dispatch

without requiring a rigid static type system.


17. Final Thoughts

Ruby’s elegance comes from the fact that the language is deeply consistent internally.

The same primitives power almost everything:

  • classes are objects
  • class methods are singleton methods
  • extend is singleton-class inclusion
  • prepend is ancestor-chain rewriting
  • include is T_ICLASS insertion
  • inheritance is linked-list traversal

Once you understand:

  • singleton classes
  • T_ICLASS
  • origin nodes
  • method lookup order

Ruby stops feeling magical.

And starts feeling beautifully engineered.

Article content

Leave a comment