Smart Pointers in Rust (Box, Rc, Arc, RefCell, Mutex, RwLock)
As you know Rust is null-safe language and no garbage collection. It has some pointers which have some rules for preventing shooting our foot. We can’t use pointers directly as we wish in Rust, we must use them behind of some types, these types have special name: Smart Pointer. Actually there is no “smart”, they have only some strict rules, that’s all. Let us deep dive into the some basic topics like pointer, reference, immutability etc…
What is pointer?
Pointer is basically a variable which have memory location, not value itself. Value can live in stack or heap but pointers must live in stack. Pointers have specific length and because of that compiler puts pointers to stack itself. But as I said before, pointers can point to stack or heap. We need to remember that, pointers have two behaviours: reach to value, create pointer from value. In Rust the first one name is dereference, second one name is reference.
What is smart pointer?
As I said before, there isn’t anything which has an intelligence or there isn’t anything which we can call ‘smart’, only there are some pointer management struct which have some rules about managing access ways to a variable. We can’t use pointers as we wish in Rust and because of that rust must provide some mechanisms for securely reaching to a variable. With these mechanisms we can securely use threads and we can trust to our app for it doesn’t have any memory leak or memory related bug.
How smart pointers handle these rules?
In Rust we can use unsafe code. This sounds like not safe but actually unsafe code means that some strict rules not applying in this area, that’s all. For example we can use multiple mutable reference, dereferencing a raw pointer etc. In unsafe area we can make safe things with additional checks which we can’t do them in safe area. Smart pointers actually makes this.
And one last thing, you need to open your mind to new names. Mostly Rust developers have strong background knowledge of another languages, which have garbage collection or unsecured pointer usage (Java, C#, PHP, Javascript, C, C++ etc). Rust has some different rules which not exist all of these languages, because of that there are different things with different names which you never heard before.
Rc<T> – Reference Counted
Counting referencing is only helper thing to another smart pointers like RefCell, Mutex etc. We will discuss all of these but you must only keep that thing in your mind, Rc saves any variable inside of it and counts the references. And we can’t mutate the variable. Rc only gives us references, that’s all. Because of that we pass another smart pointer to inside of Rc.
Some rules about Rc:
- Rc implements the Deref trait but you can’t use * character for moving out the inner data. You can only access immutable reference to inner value.
let mut rc_string = Rc::new("test".to_string());
let inner_val: String = *rc_string; // cannot move out of an Rc
let inner_val: &String = &*rc_string;
- You can use * for accessing to the wrapped value but you can’t modify it. Because of that you have to combine Rc with another smart pointers which described below.
RefCell<T> – Reference Cell
Basically RefCell‘s purpose is only applying borrow check rules at runtime. We can get multiple immutable references and single mutable reference to inside value and all of these checks made by RefCell, not Rust’s compiler. Why this thing exist in Rust? Sometimes we need to share same data between different threads/async functions and we have to be sure that this value is protecting with borrow checker rules at runtime. Don’t forget that, we don’t use RefCell (or another smart pointers) alone, we use it with Rc or Arc smart pointers for securely sharing data between threads/async/sync functions. Let’s go to code:
let rcell_val_1 = RefCell::new(10);
let mut rcell_mut_val_1 = rcell_val_1.borrow_mut();
// This is valid for compiler but it generates error at runtime.
// let mut rcell_mut_val_2 = rcell_val_1.borrow_mut();
*rcell_mut_val_1 = 20;
println!("Val: {}", rcell_val_1.borrow()); // 20
Now let’s combine Rc with RefCell and get/set the value:
let rc2 = Rc::new(RefCell::new(10));
println!("{:?}", rc2.borrow()); // 10
// `*rc2.borrow_mut()` section is exactly points to our value (10)
// and we can assign new value to there.
// Also we don't need to drop mutable reference with this notation.
*rc2.borrow_mut() = 20;
println!("{:?}", rc2.borrow()); // 20
// Also the replace() function makes same thing
rc2.replace(30);
println!("{:?}", rc2.borrow()); // 30
// ⏯️ play
As you can see we can read/write value easily with RefCell.
RwLock<T> – Read-Write Lock
RwLock is abbreviation of Read-Write Lock. This smart pointer provides two important function to us. One is write() another is read(). With these functions we can reach to the inner value for write or read purposes. Let’s code something:
let rwlock_1 = RwLock::new(0);
let mut writer = rwlock_1.write().unwrap();
*writer += 1;
drop(writer); // In current example we have to drop the writer.
let reader = rwlock_1.read().unwrap();
println!("Data: {}", *reader);
In the same block we have to manually drop the writer ownership, otherwise the thread will be blocked. Normally we don’t use RwLock in same thread but we can use it in different threads and async/sync functions. As I said before, we must combine these smart pointers with Rc and Arc.
Mutex<T> – Mutual Exclusion
Mutex is abbreviation of “Mutual Exclusion”. I can hear that you’re saying “what the hell is that?”. Don’t worry, I will explain. You can think this smart pointer as a room, inner value is staying in this room and only one person can enter to this room at a time. When someone enters to this room it locks the door from inside, makes something with inner value and quits from room. After that another person can enter to room and locks the door from inside. He makes anything with inner value and exits from door and so it goes like that.
This smart pointer gives us only one function: lock(). With this function we can enter to the room and lock the door from inside. Let us write some code:
// Our main hero
let age_mutex = Mutex::new(20);
// Enter to the room and lock the door from inside and
// access to data as mutable.
let mut age_lock = age_mutex.lock().unwrap();
*age_lock += 1;
// Dropping the lock means that quit from the room.
drop(age_lock);
// Lock the door from inside, read the data and
// quit from the door immediately.
println!("Age: {}", age_mutex.lock().unwrap());
// I want to enter the room again and lock the door from inside.
let mut age_lock = age_mutex.lock().unwrap();
*age_lock += 1;
// Destroy the lock. Otherwise next line will wait until
// you exit from the door. (In this example it waits forever.)
drop(age_lock);
println!("Age: {}", age_mutex.lock().unwrap());
As you guess the result will be like that:
Age: 21
Age: 22
So, good, what we gonna do with this? This example not seems too much sense. Nah, this is only showing the basic rules. You must combine this with Rc and Arc. Don’t forget that, none of the smart pointers must be use alone. Let’s raise the bar a little higher.
// To be continued.
Last Word
As you can see we don’t use smart pointers alone. We combine them with Rc, Arc and also Option, Result etc… With this way we can easily write thread/async/sync programs without any memory bug or violation. Smart pointers provide us everything we need while adhering to the Rust rules and they are awesome.
Stay rusty all the time.
0 yorum