As a Ruby developer, I have spent years enjoying one of Ruby’s biggest strengths: abstraction.
I can write:
users = User.where(active: true)
and focus on the business problem rather than memory allocation, pointers, system calls, or CPU instructions.
That is exactly why Ruby is productive.
But recently, I started asking a different question:
What is actually happening underneath my Ruby code?
What happens when Ruby creates an object?
Where does that object live?
Who allocates the memory?
Who releases it?
What does an array really look like internally?
What happens when Ruby calls a method?
And that leads to an interesting realization:
Learning C is not necessarily about moving away from Ruby. It can be a way of understanding Ruby at a much deeper level.
This is the first part of that journey.
Ruby hides the machine – intentionally
Consider this:
user = User.new
At the Ruby level, this is trivial.
But conceptually, a lot more is happening.
Ruby needs to:
- Represent the object.
- Allocate memory for it.
- Initialize its internal state.
- Keep track of the object for garbage collection.
- Maintain references between objects.
- Eventually reclaim its memory.
Ruby handles these details for us.
That abstraction is one of the reasons we love Ruby.
But it also means that most Ruby developers don’t need to think about the actual machine.
C removes much of that abstraction.
C forces you to think about memory
In C, you quickly encounter things like:
int number = 42;
and:
int *ptr = &number;
The second line introduces a concept that Ruby normally keeps away from you: the memory address of a value.
You can explicitly allocate memory:
int *numbers = malloc(100 * sizeof(int));
and explicitly release it:
free(numbers);
That changes your mental model.
Instead of thinking only in terms of:
objects
methods
classes
you begin thinking about:
memory
addresses
bytes
layouts
allocation
lifetime
references
And this is extremely useful when trying to understand Ruby internally.
Ruby objects are still data in memory
Take a simple Ruby value:
name = "Abhilash"
As a Ruby developer, you normally think:
name → String
A lower-level mindset makes you ask:
name
↓
Ruby value/reference
↓
Object representation
↓
Memory
↓
Bytes
Ruby doesn’t magically escape the laws of computing.
At some point, that string has to exist in memory.
The same is true for:
ArrayHashIntegerStringUser
They all ultimately have machine-level representations.
Learning C helps you become curious about those representations.
Stack vs Heap
One of the first concepts worth learning in C is the difference between stack and heap memory.
For example:
void example() {
int number = 10;
}
The local variable has automatic storage duration associated with the function’s execution.
Dynamic allocation looks different:
int *number = malloc(sizeof(int));
*number = 10;
free(number);
Now the program explicitly controls the allocation and lifetime.
This distinction is extremely important when later studying Ruby’s memory management.
Ruby objects are managed by the runtime rather than by application code using malloc and free directly.
That leads naturally to the next question:
Who manages Ruby’s heap?
The answer takes us into the Ruby garbage collector.
Garbage collection becomes much easier to understand
A Ruby developer typically learns:
“Ruby has a garbage collector, so I don’t need to manually free objects.”
That’s correct, but incomplete.
Once you understand manual memory management in C, garbage collection becomes much more interesting.
You can start thinking about:
Object allocation
↓
Heap
↓
References
↓
Object becomes unreachable
↓
Garbage collector
↓
Memory can be reclaimed
Instead of viewing GC as some magical Ruby feature, you begin seeing it as a runtime memory-management strategy.
That distinction is important.
Ruby didn’t eliminate memory management.
It automated memory management.
C also teaches you that data layout matters
Consider:
struct User {
int id;
char name[50];
};
You are explicitly describing a data structure’s layout.
You begin thinking about questions such as:
- How many bytes does this structure occupy?
- How are fields aligned?
- Are objects contiguous?
- How efficiently will the CPU access them?
- What happens to cache locality?
Ruby normally shields you from these concerns.
But when performance suddenly matters, these concepts become valuable.
For example, processing millions of objects isn’t only about algorithmic complexity.
Memory access patterns can matter too.
This is one reason understanding low-level systems concepts can make you a better high-level developer.
Then there is the most interesting part: Ruby itself uses C
This is where the journey becomes particularly relevant to Ruby developers.
The standard Ruby implementation, CRuby, is largely implemented in C.
That means the language we write:
array.map(&:name)
eventually reaches a runtime implemented at a much lower level.
Conceptually:
Ruby code
↓
Ruby parser / VM
↓
CRuby runtime
↓
Operating system
↓
CPU / memory
Once you start reading Ruby’s C source code, concepts that initially look mysterious start becoming understandable:
VALUE
Ruby objects
references
object allocation
method dispatch
garbage collection
VM execution
And suddenly C stops being just another programming language.
It becomes a lens through which you can inspect Ruby itself.
Why should a senior Rails developer care?
You don’t need to write your next Rails application in C.
That isn’t the point.
The goal is to develop a deeper mental model.
When you write:
100_000.times do User.newend
you should eventually be able to think beyond the Ruby syntax.
You start wondering:
How many allocations?
Where are those objects stored?
How does GC discover them?
What references exist?
How much memory is being consumed?
What happens when these objects become unreachable?
What is the runtime doing while my Ruby code executes?
Those questions are far more valuable than memorizing another Rails API.
The goal of this journey
My objective isn’t:
“Become a C programmer.”
It is:
Become a Ruby developer who understands what Ruby is doing underneath.
And the roadmap becomes surprisingly clear:
C fundamentals
↓
Pointers & memory
↓
Stack & heap
↓
Processes & system calls
↓
C programming at system level
↓
CRuby internals
↓
Ruby VM
↓
Garbage collection
↓
Ruby C extensions
The interesting part is that the deeper you go into C, the less mysterious Ruby becomes.
Ruby’s abstractions don’t disappear.
You simply start seeing what is behind them.
And for me, that is the real power of learning C as a Ruby developer.
Part 2 will start with the most important foundation: memory, pointers, stack, heap, and how these concepts map to the Ruby object model.
For Part 2, I’d make memory + pointers + stack/heap → Ruby objects and VALUE the central theme. That is where this series can become genuinely fascinating for an experienced Ruby developer.
Happy Learning! 🚀