Do you ever thought about what happens to your RAM when you run a program? And how the ways you write code can interfere with many other things in your system?
That article will help you to understand more about management memory and HOW RUST WORKS THIS SUBJECT.
Table of Contents
1. Stack and Heap
Before you learn what rust do, you have to learn some concepts. In general we have two types of memory, that are called: Stack and Heap. Now let's introduce them.
1.1 Memory: Stack
Stack, as the name says, works like a stack, following the principle of "Last In, First Out" (LIFO). Maybe it will be easier explain it in steps:
- Imagine a stack of dishes;
- The first dish you put in is the last to be removed;
- When a function is called, a block of memory is 'stacked' on top of the stack;
- When the function ends, this block is "unstacked", freeing this memory.
Normally, the values that will be stored on the stack is known by the compiler (in the compiling time), because it knows how much memory is needed to store. This process occurs automatically, all values are deleted from memory.
Below is an example of this:
fn main() {
let number = 12; // at this moment the variable has created
println!("{}", number); // 12
} // When the owner (main function) goes out of scope, the value will be dropped
In Rust we can create some scopes just by using {}
, and this adds layer into our stack with a limited lifetime. After your goes out from that specific scope, the memory is cleared and you lost the information related. A good and simple example of it is:
fn main() {
{
let number = 12;
println!("{}", number); // 12
}
println!("{}", number); // Cannot find value `number` in this scope
}
1.2 Memory: Heap
In a few words: the heap memory is a free memory space to allocate data that may change.
Imagine that you need to store some variable after starting your program, this variable doesn't have a fixed size known at compile time since it can have size variations or is a direct allocation in memory.
If one of the possibilities above matches, we know that we have Heap memory instead Stack. Heap has a more flexible memory and a large space. Take a look:
let number = Box::new(12); // alocate a integer in heap
let name = String::from("Canhassi"); // alocate a String in heap
In Rust we have two types
of string, &str
and String
.
- &str: has a fixed size based on the text written;
- String: has a size that can be increased, decreased, and deleted.
... and that's why String
is stored on the Heap, and &str
on the stack.
One way to free heap memory is: when a variable that stores something that is on the heap leaves the scope of the function (reaches the end of the function), it will be freed in the same way as the stack.
Okay, now we have a clear image on both memory types, but what the difference to manage memory between Stack and Heap? Let's see these diferences!
2. Borrow checker
Borrow Checker is the part of the Rust compiler that checks and ensures that ownership, borrowing, and lifetime rules are respected.
I'm gonna be honest with you: I had a some issues to understanding how this works at beginning, and I see that is common among new Rustaceans . But don't worry, friend of mine. I'll teach you on the best way possible. But first, let's take a look the code below:
fn main() {
let name = String::from("Canhassi"); // Creating a string variable
print_name(name); // calling the print_name function passing the variable
println!("{}", name); // Error: name borrowed to print_name()
}
fn print_name(name: String) {
println!("{}", name); // print "Canhassi"
}
If you run the compiler with that code you gonna see the following error:
borrow of moved value: name
When we call the print_name
function, the name
variable will be moved to another scope and that scope will be the new owner of it. And the rule of owner goes out scope will applied again. Borrow Checker ensures that once ownership is transferred, the original variable can no longer be used to access that value. this happened in the code above.
Another way that you can use the original variable is using references like that.
fn main() {
let name = String::from("Canhassi"); // Creating a string variable
print_name(&name); // calling the print_name function passing the variable
println!("{}", name); // print "Canhassi"
}
fn print_name(name: &String) {
println!("{}", name); // print "Canhassi"
}
PS: These rules of borrow checker work just with objects allocated in heap.
3. Errors prevention
The Borrow Checker is fundamental to ensuring Rust's memory safety without the need for a garbage collector.
In other languages like C and C++, we (as developers) have to deallocate memory manually with some functions. C++ is known for allowing memory management errors, including memory leaks. C++ itself does not cause memory leaks. Instead, C++ provides a great deal of flexibility and control to the programmer, and this freedom can lead to errors if not used correctly.
A famous error is Undefined Behavior, for example, imagine a function that return e reference of a variable like that
fn main() {
let number = foo(); // calling foo function
}
fn foo() -> &i32 {
let number = 12; // creating a var number
&number // try return a reference of var number
}
This code doesn't work because the Rust compiler has restrict rules with memory management. We can't return a reference of var number
because the scope of this function will go out, and the var number
will disappear, so Rust doesn't let this happen. In C++ we can do that, and it allows the notorious memory leaks.
I think that is kinda cool to know that the Rust compiler avoids this type of error... If this subject is new for you, I can tell that memory leaks can cost a lot of money and is truly hard to fix it, since is never only one memory leak.
Other example is, Double free, that occur when you try free twice the same object for example in C code.
char* ptr = malloc(sizeof(char));
*ptr = 'a';
free(ptr);
free(ptr);
This subject is really extensive and there's many possibilities to generate errors like that when you're working in other languages. But, I'll let you do your own research and make sure to tell me more about on this post comments!
Undefined Behavior
Double Free
4. Conclusion
I wrote this article with the aim of presenting in a more general way how memory management works with Rust. But I recommend for a more complete knowledge the chapter 4 of The Rust Programming Language book. There you will have more examples and more explanations of this content.
I really hope this article was helpful!! 🦀🦀
Top comments (26)
I was not even aware about this terminology before. Stack and Heap seems to be a really common thing for low level languages, but since I'm new to Rust this article helped me a lot.
Thanks for the contribution!
OMG Canhas? Big fan here! Great content man!
Rust borrow checker does not help with managing object hierarchies in heap. It works only around stack and helps only with synchronous function calls. It doesn't work with asynchronous and multithreaded code. Also Rust does not prevent memory leaks.
While Rust's memory management may pose initial challenges for beginners, it proves remarkably powerful once mastered. And this article helped me to make this.
Awesome!!
Good articles.
In multi thread, pay attention to race conditions with references like
print_name(&name);
Technically, &str is simply a reference (an address) which is a fixed size that may point to dynamically created data or static. The only thing on the stack is the address itself, not the actual data, because the slice can also refer to gigabytes of data. I just wanted to make sure there was no confusion and people assumed the slice was a value type. Nice article, btw!
Thank you for the explanation <3
Nicee Canhas
Such an inspiration
I have never used Rust and don't really intend to, but this article was still fascinating, especially since I suspect those concept are relevant in many other languages.
Thanks !