In Part 1, I looked at why learning C can be valuable for a Ruby developer-not to replace Ruby, but to understand what happens underneath it.
This time, we go closer to the machine.
The concepts are simple:
memory, addresses, pointers, stack, heap.
But they completely change the way you think about Ruby objects.
Everything ultimately becomes memory
Consider this Ruby code:
name = "Ruby"
At the Ruby level, we think:
name → "Ruby"
At the machine level, however, something must exist in memory.
There is storage for the string’s data, metadata describing the object, and some mechanism for Ruby to refer to that object.
The exact representation is an implementation detail, but the important idea is:
Ruby objects ultimately have a physical representation in memory.
C lets us see memory directly.
Memory has addresses
Consider:
int number = 42;
The variable has a value:
42
but it also occupies some location in memory.
We can ask C for that location:
printf("%p", (void *)&number);
The & operator means:
Give me the address of
number.
You might see something like:
0x7ffee1234abc
The actual address is not important.
The concept is.
Memory0x7ffee1234abc ↓ [42]
Now we have crossed an important boundary.
We are no longer thinking only about values.
We are thinking about where those values live.
A pointer stores an address
C lets us store that address:
int number = 42;int *ptr = &number;
Now:
number ↓[42]ptr ↓[address of number]
And:
printf("%d", *ptr);
The * dereferences the pointer.
It means:
Go to the address stored in
ptrand access the value there.
So:
*ptr = 100;
changes the original variable:
number = 100
This is one of C’s defining characteristics.
You can explicitly work with addresses and the data behind them.
Ruby references are not C pointers
This is an important distinction.
Ruby variables behave somewhat like references from a conceptual perspective, but Ruby does not expose raw memory addresses and pointer arithmetic in normal Ruby code.
For example:
name = "Ruby"other = name
You can think:
name │ └────→ String objectother │ └────→ same String object
But Ruby does not let you simply say:
"Take this address and add 8 bytes."
C does.
That difference is fundamental.
Ruby gives you an object model.
C gives you memory-level primitives from which many such abstractions can be built.
Stack and heap
Now we reach another important concept.
A running program uses memory in different ways. Two areas you’ll encounter immediately are the stack and the heap.
Consider:
void calculate() { int number = 42;}
The local variable has automatic storage associated with the function’s execution.
Conceptually:
Stackcalculate()┌───────────────┐│ number = 42 │└───────────────┘
When the function returns, that stack storage is no longer needed.
Dynamic allocation is different:
int *number = malloc(sizeof(int));*number = 42;
Now memory is allocated dynamically.
Conceptually:
Stack┌───────────────┐│ number │──────┐└───────────────┘ │ ↓ Heap ┌────────┐ │ 42 │ └────────┘
And C expects you to eventually release it:
free(number);
This explicit ownership model is one of the biggest differences between C and Ruby.
Ruby’s heap becomes a much more interesting subject
In Ruby, you normally write:
user = User.new
and never ask:
Who called malloc?Where exactly is this object?Who will release its memory?
Ruby’s runtime manages those details.
The object is allocated under Ruby’s memory-management system, and the garbage collector tracks object reachability and determines when memory can be reclaimed.
So rather than:
Application → malloc → free
you generally experience:
Ruby code ↓Ruby runtime ↓allocation ↓Ruby heap ↓GC
Learning C makes that second model much easier to reason about.
The fascinating part: VALUE
Now we arrive at one of the concepts that makes CRuby internals especially interesting.
In CRuby, Ruby values are represented internally using a type called:
VALUE
You will encounter VALUE everywhere when reading the Ruby C implementation and C extension APIs.
Conceptually, you can think of it as:
the low-level representation Ruby uses to pass around Ruby values inside the runtime.
For example, a Ruby C API function may look conceptually like:
VALUE rb_str_new_cstr(const char *ptr);
and C extension methods often receive and return VALUEs.
That means your Ruby object:
"hello"
does not remain some abstract concept all the way down.
CRuby represents it using its internal object/value machinery.
Not every Ruby value is simply a pointer
This is where Ruby becomes particularly interesting.
A common beginner assumption is:
Ruby object = pointer to heap object
That’s useful as a rough mental model, but it isn’t the whole story.
CRuby uses a representation that can encode certain immediate values directly rather than allocating a separate heap object for every value.
Integers are a classic example.
So when you write:
number = 42
you shouldn’t automatically imagine:
number ↓heap object containing 42
The runtime has specialized representations for some Ruby values.
This is one reason looking at CRuby internals is so educational.
A high-level statement such as:
“Ruby variables point to objects”
is useful, but the implementation is much more nuanced.
Why this matters for a Ruby developer
Let’s take:
a = 10b = 10
At the Ruby language level, you care that both variables represent the integer 10.
After learning some C and Ruby internals, you start asking different questions:
Are these separate objects?Is 10 heap allocated?How does CRuby represent integers?How does Ruby distinguish integers from ordinary heap objects?What exactly is stored in VALUE?
Those are much deeper questions.
And they lead directly into:
- immediate values
- object flags
- object headers
- pointer tagging
- garbage collection
- object allocation
- Ruby’s internal data structures
Pointers explain something else: object identity
Ruby lets us ask:
a = Object.newb = aa.equal?(b)# => true
Why?
Because both variables refer to the same object.
Conceptually:
a ─────┐ ↓ [Object] ↑ │b ─────┘
C gives you the vocabulary to understand this relationship:
referenceaddresspointermemory location
Again, Ruby intentionally hides the actual pointer from application code.
But the underlying concept of “multiple references to the same object” remains.
The danger of C is also the lesson
Ruby protects you from many classes of memory errors.
In C, you can easily write:
int *ptr = malloc(sizeof(int));*ptr = 42;free(ptr);*ptr = 100;
Now you’re accessing memory after it has been released.
That’s a use-after-free.
You can also leak memory:
int *ptr = malloc(sizeof(int));/* forgot free(ptr) */
Or write outside an allocated buffer:
int numbers[10];numbers[100] = 42;
These bugs are difficult precisely because C gives you so much control.
And that is the paradox:
The freedom that makes C powerful is the same freedom that makes it dangerous.
Ruby takes many of these responsibilities away from you.
The real payoff
After learning these concepts, this Ruby code:
users = 10_000.times.map { User.new }
starts looking different.
Instead of only seeing:
Ruby objects
you can begin thinking:
Ruby objects ↓object representation ↓memory allocation ↓references ↓Ruby heap ↓garbage collector
And when a Rails application starts consuming hundreds of megabytes of memory, that mental model becomes much more useful.
You can ask better questions.
Not just:
“Why is Rails using so much memory?”
but:
“What objects are being allocated, how long do they remain reachable, and how does Ruby’s allocator and GC interact with that workload?”
That’s a much more senior-level way of investigating the problem.
Where we go next
We have now established the foundation:
C ↓Memory ↓Addresses ↓Pointers ↓Stack / Heap ↓Ruby references ↓VALUE ↓CRuby object representation
The next step gets even more interesting:
What does a Ruby object actually look like inside CRuby?
We’ll look at concepts such as object headers, RBasic, type information, flags, heap allocation, and how the garbage collector sees Ruby objects.
That’s where the gap between:
User.new
and:
VALUE obj;
starts to disappear.
Happy Learning! 🚀