
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 endendclass Service prepend Logging def call puts "service" endend
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.
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:
putsclassrespond_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:
includeprependalias_methoddefine_methodpublicprivateprotectedconst_getmethod_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" endendclass Service include Loggingend
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" endendclass Service include Loggingendmodule Logging def another "B" endend
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 Ainclude 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 Userendmodule AdminFeatures def admin? true endendu = User.newu.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 endend
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 Animalendclass Dog < Animalend
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 endendclass Service prepend Logging def call puts "service" endend
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; endmodule M2; endmodule M3; endclass Base include M1endclass Child < Base prepend M2 include M3end
MRI resolves ancestors as:
[M2, Child, M3, Base, M1, Object, Kernel, BasicObject]
Important rules:
- prepended modules come first
- then the class itself
- included modules come afterward
- 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 endendusing 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.
