DEV Community

Cover image for The Rust Journey of a JavaScript Developer • Day 4 (2/5)
Federico Moretti
Federico Moretti

Posted on

The Rust Journey of a JavaScript Developer • Day 4 (2/5)

I left you talking about ownership or memory safety in Rust. There’s still much to say, even though we’ll always go round in circles on the same issue. We have several ways of transferring ownership between variables, and it’s important to know at least something about each of them.


Last time we saw the .clone() function that lets you keep the ownership to the original variable, without moving its value: it’s not the only way to achieve this when using box-powered pointers. Today, we will see other aspects of ownership in Rust, consolidating our knowledge.

References and Borrowing

Ownership can be useful in different situations, but most of the time you may want to keep the variables’ values. If, for example, you want to use templating with strings, the same variable should be available more than once. What will happen if you try to print it twice?

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(m1, m2);
    let s = format!("{} {}", m1, m2); // Error: m1 and m2 are moved
}

fn greet(g1: String, g2: String) {
    println!("{} {}!", g1, g2);
}
Enter fullscreen mode Exit fullscreen mode

Above, m1 and m2 were moved by greet(), so this code won’t compile. Looking at the syntax, it should be more familiar for those who are coming from Python: both the format! and the println! macros are similar. Let’s see how to keep the values in this situation.

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    let (m1_again, m2_again) = greet(m1, m2);
    let s = format!("{} {}", m1_again, m2_again);
}

fn greet(g1: String, g2: String) -> (String, String) {
    println!("{} {}!", g1, g2);
    (g1, g2)
}
Enter fullscreen mode Exit fullscreen mode

This way, m1 and m2 become undefined, but m1_again and m1_again don’t. It’s yet another workaround, since we can’t avoid moving the first variables: we just returned g1 and g2 after printing the sentence, assigning their values to m1_again and m2_again respectively.

References Are Non-Owning Pointers

The solution provided above is too verbose. But we do have an alternative: we can use references, which are pointers without ownership. This means that variables will keep their values, even though they are passed to a function as parameters, and we can use it twice or more.

fn main() {
    let m1 = String::from("Hello");
    let m2 = String::from("world");
    greet(&m1, &m2);                 // note the ampersands
    let s = format!("{} {}", m1, m2);
}

fn greet(g1: &String, g2: &String) { // note the ampersands
    println!("{} {}!", g1, g2);
}
Enter fullscreen mode Exit fullscreen mode

Ampersands define references, so &m1 and &m2 reference m1 and m2 without moving their ownership. Notice that also the type of g1 and g2 must be defined the same way: &String represent a reference of String. This mechanism, instead of “moving”, is called “borrowing”.

Dereferencing a Pointer Accesses Its Data

Dereferencing is the action performed by Rust to handle the stack under the hood. Although it’s often an implicit operation, we can make it explicit using asterisks: so, we can see even more clearly how things work, and how to deal with ownership changes and moves.

let mut x: Box<i32> = Box::new(1);
let a: i32 = *x;         // *x reads the heap value, so a = 1
*x += 1;                 // *x on the left-side modifies the heap value,
                         // so x points to the value 2

let r1: &Box<i32> = &x;  // r1 points to x on the stack
let b: i32 = **r1;       // two dereferences get us to the heap value

let r2: &i32 = &*x;      // r2 points to the heap value directly
let c: i32 = *r2;        // so only one dereference is needed to read it
Enter fullscreen mode Exit fullscreen mode

Here the goal is getting the heap value of a given variable. This is possible by adding an asterisk * or more before its declaration: references to the stack need two **, references to the heap value itself just one, since a reference already points to it by design.

let x: Box<i32> = Box::new(-1);
let x_abs1 = i32::abs(*x);  // explicit dereference
let x_abs2 = x.abs();       // implicit dereference
assert_eq!(x_abs1, x_abs2);

let r: &Box<i32> = &x;
let r_abs1 = i32::abs(**r); // explicit dereference (twice)
let r_abs2 = r.abs();       // implicit dereference (twice)
assert_eq!(r_abs1, r_abs2);

let s = String::from("Hello");
let s_len1 = str::len(&s);  // explicit reference
let s_len2 = s.len();       // implicit reference
assert_eq!(s_len1, s_len2);
Enter fullscreen mode Exit fullscreen mode

Above, an example of how dereferencing works with functions and methods: method-calls implicitly dereference the variable, so x become 1 to fit the i32 constraint without an explicit dereference. Otherwise, you can use the function with an explicit dereference to obtain the same value.

Rust Avoids Simultaneous Aliasing and Mutation

OK, but what does it mean in practice? How can I benefit from knowing these concepts? Well, I think that yet another example will explain it better. Let’s say that you’re working with vectors: altering them could break your program, if you don’t pay attention to details.

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &i32 = &v[2];
v.push(4);
println!("Third element is {}", *num);
Enter fullscreen mode Exit fullscreen mode

This code won’t compile, because the .push() method alters the v length, deallocating the initial array and allocating a new one with an extra element. Then, num lost the ownership of 3 and became undefined: even though 3 still have an index of 2, num can’t be dereferenced.

References Change Permissions on Places

Another interesting aspect of ownership is about permissions. In fact, Rust developers see it as a third kind of permission, after read and write: a variable has both read and own permissions by default, and write is added only specifying mut. Permissions change help to understand the whole process.

let mut v: Vec<i32> = vec![1, 2, 3];   // v: +r, +w, +o
let num: &i32 = &v[2];                 // v: r -w -o, num: +r, -w, +o, *num: +r, -w, -o
println!("Third element is {}", *num); // v: r, +w, +o, num: -r, -w, -o, *num: -r, -w, -o
v.push(4);                             // v: -r, -w, -o, num: -r, -w, -o, *num: -r, -w, -o 
Enter fullscreen mode Exit fullscreen mode

Line by line, you can see how permissions have changed. When using .push() on the vector, v loses them all, but right after it will get them back on the new allocated array, while both num and *num are now undefined. We will see this even better, talking about lifetime.

The Borrow Checker Finds Permission Violations

What’s the problem with v, num, and *num? We have two different problems here: first, num has been set as an immutable reference to v, so this latter it’s borrowed as immutable until num ends its lifetime; then, we have a call to the .push() method, which could invalidate num.

I know it can be difficult to understand: it’s for me, at least. But it’s also a chance to reflect about memory allocation, scopes, permissions, etc., so I think it’s worth it. Every time you do something, Rust performs checks to ensure that your code is valid: the borrow checker blocks potential issues in advance.

I said potential, because .push() appends a value to the array, so the index of v[2] assigned to num will again return the number 3. What I tend not to consider is that, talking about memory allocation, they are two different numbers: existing 3 has been deallocated and a “new” one took its place.

Mutable References Provide Unique and Non-Owning Access to Data

Of course, there’s a solution, and it’s using mutable references instead. This approach makes it possible to reuse the same variable, even if its heap value changed: while immutable references cause an undefined behavior, immutable references don’t, since they add writing permissions.

let mut v: Vec<i32> = vec![1, 2, 3];
let num: &mut i32 = &mut v[2];
*num += 1;
println!("Third element is {}", *num);
println!("Vector is now {:?}", v);
Enter fullscreen mode Exit fullscreen mode

Again, we said that Rust prevent compiling if an action will potentially move a variable: here it doesn’t, because mutable or unique references make it legit. Then, although v[2] has changed with *num += 1, this code compiles and prints 4 and [1, 2, 4] as expected.

Permissions Are Returned At The End of a Reference’s Lifetime

I mentioned references lifetime. Long story short, when a reference’s lifetime ends, the referenced variable recovers its full permissions—reading, writing, and owning: this could happen via dereferencing or executing function with a reference used as either an input or an output.

let mut x = 1;
let y = &x;
let z = *y;
x += z;
Enter fullscreen mode Exit fullscreen mode

Above, x became read-only on line two and got its write and own permissions back on line three, where y was dereferenced. This means that, for example, if we swap lines four and three, code won’t compile: because x hasn’t got its owning permission back before *y.

Data Must Outlive All Of Its References

Rust enforces data safety in two ways. We already know the first one, and the following code won’t compile because of it: s lost its owning permission on line two, then it can’t be dropped in line three, since drop() require ownership, and s_ref hasn’t ended its lifetime yet.

let s = String::from("Hello world");
let s_ref = &s;
drop(s);
println!("{}", s_ref);
Enter fullscreen mode Exit fullscreen mode

How can we solve this issue? Let’s introduce yet another kind of permission: the flow, which is more or less a lifetime control. Flow permissions are assigned whenever a reference is either an input or an output of an executed function. They don’t change in the body of a function, but they do in conditionals.

fn first_or<'a, 'b, 'c>(strings: &'a Vec<String>, default: &'b String) -> &'c String {
    if strings.len() > 0 {
        &strings[0]
    } else {
        default
    }
}
Enter fullscreen mode Exit fullscreen mode

While strings.len() has the permission to flow, whatever defined in brackets don’t, so the function doesn’t compile: Rust doesn’t know if the output reference will point to either strings or default in advance, since there’s a condition to be met, and refuses to compile the sources.


So many topics, yet so little practical information. I know that it’s challenging: I myself am learning a lot as I write. Many university lectures about memory allocation now make sense—but it’s very tiring to keep up with these chapters. In my opinion, Rust is as difficult to learn as C.

Top comments (0)