Please, have a look at this Bluesky post from Rust, since it seems that a phishing campaign is targeting crates.io these days. It’s just a coincidence, but today we’ll talk about fixing ownership issues: yet another chapter on memory safety. It will be the chance to refresh previous concepts.
We already saw many aspects of ownership in Rust. Today, I’m going to share more about fixing issues, before compiling: we’ll learn how to respond to borrow checker’s warnings, writing safe code. Rust developers consider this as a core skill, so I’m going to share other two articles about it.
Fixing Ownership Errors
OK, we already saw lots of situations which can potentially cause errors in Rust. They say Rust will always reject an unsafe program
but, most importantly, sometimes it will also reject a safe program
, and that’s bad, so we need to implement code that won’t be rejected.
Returning a Reference to the Stack
Let’s say you only want to print a string. The String
structure uses boxes, so has value ownership and stores it in a heap buffer: it’s maybe the easiest example we can have a look at. Below, just saying, a code that won’t compile, because it’s not safe. Why it isn’t?
fn return_a_string() -> &String {
let s = String::from("Hello world");
&s
}
This is a typical lifetime issue. Long story short, you’re trying to return (that’s why &s
misses the ending semicolon, do you remember?) a reference that has already been deallocated from memory. Solving the error is pretty easy, since you may want to extend the variable lifetime, but we have several ways of doing it.
fn return_a_string() -> String {
let s = String::from("Hello world");
s
}
This is by far my preferred solution. The return_a_string()
function does exactly what it literally means: so, why referencing a string that just need to be returned? The function name doesn’t say return a reference, but a string, which will perhaps be printed by another function. The arrow ->
here means return.
fn return_a_string() -> &'static str {
"Hello world"
}
Above, we extended the lifetime of the string to forever: that’s what 'static
means. We’re not using variables here, but "Hello world"
is a string literal which will never change, as it was an immutable variable. To be honest, even though I prefer the previous solution, this is shorter.
use std::rc::Rc;
fn return_a_string() -> Rc<String> {
let s = Rc::new(String::from("Hello world"));
Rc::clone(&s)
}
Well, on the other hand, the last solution is so long, but it’s a chance to introduce Rc
or Reference Counted: I can’t see a reason to write a code like this to only return a string, honestly. The function above will move the borrow check from compiling to runtime, I still don’t know why, but I trust them.
fn return_a_string(output: &mut String) {
output.replace_range(.., "Hello world");
}
This is even weirder. Here, we’re putting a string into a mutable reference that replaces an empty range: something I wouldn’t imagined, but still a better way to organize memory allocation. I think I missed too many computer science classes to figure out why at the time.
Not Enough Permissions
Apart from lifetime issues, last time we talked about permissions—and we introduced some that only Rust has. The following code doesn’t compile, because we’ll try to mutate a read-only element. So, we won’t have a write permission in the required lifetime phase.
fn stringify_name_with_title(name: &Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
Since name
is an immutable reference of a vector, it lacks the write permission, but .push()
requires it to be executed. The faster way of fixing it is turning the reference to a mutable one. Spoiler: it won’t be that simple, although it’s a legit solution for doing it.
fn stringify_name_with_title(name: &mut Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
The code above will compile successfully, but let’s concentrate on the function meaning: it’s intended to “stringify” a name with its title (mine would be Dr. in Italy, not to be confused with Ph.D. around the world). Then, maybe who calls this function doesn’t want to have a mutable reference to the name.
fn stringify_name_with_title(mut name: Vec<String>) -> String {
name.push(String::from("Esq."));
let full = name.join(" ");
full
}
Again, this will compile, but it doesn’t make sense to get writing capabilities on the name. Your purpose here is adding a title to an existing full name, so why you should be able to change it? Let’s say you have Mario Rossi as an input: you want to get Mario Rossi Esq. as a result, not Luigi Verdi Esq..
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut name_clone = name.clone();
name_clone.push(String::from("Esq."));
let full = name_clone.join(" ");
full
}
Finally, cloning the reference, we get writing capabilities on the clone, and the code will compile without making the name itself mutable. This solution is both legit and logically right, but there’s yet another way of having the same result with fewer lines of code.
fn stringify_name_with_title(name: &Vec<String>) -> String {
let mut full = name.join(" ");
full.push_str(" Esq.");
full
}
If it was JavaScript, full
would have been an array of strings, originally separated by a single space, and we would have added the string Esq.
with a leading space in the end. Then, logically speaking, in Rust you will have a mutable string made of at least three elements, starting with an immutable reference to a name
.
Aliasing and Mutating a Data Structure
What about introducing aliases in the process? Below, dst
will lose writing permission despite being called from an alias, causing an undefined behavior. We’ll talk about those new methods for strings later: don’t worry if you don’t understand them already, so do I.
fn add_big_strings(dst: &mut Vec<String>, src: &[String]) {
let largest: &String = dst.iter().max_by_key(|s| s.len()).unwrap();
for s in src {
if s.len() > largest.len() {
dst.push(s.clone());
}
}
}
Above, we have the same problem we had defining a reference directly, so the code won’t compile: largest
is an alias for dst
, which being immutable (it lacks the write permission) can’t be used by .push()
at line 5. Then, this latest breaks its largest
reference as well.
fn add_big_strings(dst: &mut Vec<String>, src: &[String]) {
let largest: String = dst.iter().max_by_key(|s| s.len()).unwrap().clone();
for s in src {
if s.len() > largest.len() {
dst.push(s.clone());
}
}
}
This will compile, because instead of aliasing a reference, we aliased a clone of the original mutable vector. As you may guess, although it’s legit, it’s not the best way of handling memory allocation. So we should use a completely different approach to get the same result.
fn add_big_strings(dst: &mut Vec<String>, src: &[String]) {
let largest: &String = dst.iter().max_by_key(|s| s.len()).unwrap();
let to_add: Vec<String> = src.iter().filter(|s| s.len() > largest.len()).cloned().collect();
dst.extend(to_add);
}
Back using an alias for the reference, here we introduced an intermediate variable, so that we’ll perform a comparison before moving the ownership of dst
by using the .extend()
method we’ve never seen before. This keeps the required permissions, but allocates memory in to_add
, getting another performance hit.
fn add_big_strings(dst: &mut Vec<String>, src: &[String]) {
let largest_len: usize = dst.iter().max_by_key(|s| s.len()).unwrap().len();
for s in src {
if s.len() > largest_len {
dst.push(s.clone());
}
}
}
Look at the latest solution. We basically stored the largest string length, instead of the largest string as a whole, in a variable called larget_len
. This solved the initial issue, without causing unwanted performance hits, like we stored an array.length()
in a constant via JavaScript.
Copying vs. Moving Out of a Collection
«Types, types everywhere.» Let’s say that you want to copy an element from a vector collection to a variable as a reference: then, you try to dereference it in a second variable. This is 100% legit and will compile if you paste it in your program. But, trust me, it’s just the beginning.
let v: Vec<i32> = vec![0, 1, 2];
let n_ref: &i32 = &v[0];
let n: i32 = *n_ref;
This will work until the vector collection uses numbers—i32
identifies positive numbers that fit a maximum of 32-bit memory allocation. What if we use a different type of data? I think you already guessed that it won’t work with strings, indeed. So, next lines won’t compile at all.
let v: Vec<String> = vec![String::from("Hello world")];
let s_ref: &String = &v[0];
let s: String = *s_ref;
It’s difficult to understand why a collection made of numbers compile, while a a collection made of strings doesn’t. That’s because we didn’t talk about Copy
already: numbers own heap data, strings don’t, then the first code compiles, and the second doesn’t. We always have at least three solutions.
let v: Vec<String> = vec![String::from("Hello world")];
let s_ref: &String = &v[0];
println!("{s_ref}!");
We just printed the reference, without trying to dereference it, so we didn’t move anything here. Of course, if your intent is to move something, this solution is definitely not for you: what about cloning? Again, you wont move ownership, but this could be a better way of getting the same result.
let v: Vec<String> = vec![String::from("Hello world")];
let mut s: String = v[0].clone();
s.push('!');
println!("{s}");
This time we removed references, but cloning the vector collection element we didn’t produce an undefined behavior, even if we pushed an exclamation mark at the end of the string. We did it with the cloned one, not with the original, so the code will compile without errors.
let mut v: Vec<String> = vec![String::from("Hello world")];
let mut s: String = v.remove(0);
s.push('!');
println!("{s}");
assert!(v.len() == 0);
Above, yet another method. We’re basically bringing the string element at index 0
out from the collection, assigning it to the mutable variable s
: we assert that v
has no elements in it right after. Uh, the initial numbers collection had three items, while the strings’ had only one.
Mutating Different Tuple Fields
We don’t have tuples in JavaScript. We do in Python—but here I consider myself just a JavaScript expert, pretending I don’t know different languages. Rust defines tuples the same way, using parentheses: what will happen to ownership if we alter an element of the tuple?
let mut name = (
String::from("Ferris"),
String::from("Rustacean")
);
let first = &name.0;
name.1.push_str(", Esq.");
println!("{first} {}", name.1);
This will compile and print Ferris Rustacean, Esq.
to the terminal emulator, since first
is a reference of name.0
, that itself equals to Ferris
, and name.1
became Rustacean, Esq.
after a .push_str()
execution. Here we’re going to meet a correct code that anyway won’t compile in Rust.
fn get_first(name: &(String, String)) -> &String {
&name.0
}
fn main() {
let mut name = (
String::from("Ferris"),
String::from("Rustacean")
);
let first = get_first(&name);
name.1.push_str(", Esq.");
println!("{first} {}", name.1);
}
At the logical level, there’s no changes to name.1
above, so the code would compile, but Rust doesn’t consider what the function get_first()
does: it just returns a reference to name.0
as before, leaving name.1
unchanged, but it uses the whole tuple name
as a parameter, then Rust blocks its execution a priori.
Mutating Different Array Elements
I’m mainly a JavaScript full-stack developer, then I can pretend I don’t know the concept of tuple. Below, we’re going to see quite the same issue with arrays—that are existing JavaScript data structures instead. And we’ll get lots of compiling errors as above.
let mut a = [0, 1, 2, 3];
let x = &mut a[1];
*x += 1;
println!("{a:?}");
Simplifying, an array in Rust is a more generic kind of a tuple. This means that, while elements in tuples are taken one by one, elements in arrays are taken altogether, so it doesn’t matter if you specify only a[0]
or a[1]
, because the borrow checker will treat them all as a[_]
or a[*]
if you prefer this syntax.
let mut a = [0, 1, 2, 3];
let x = &mut a[1];
let y = &a[2];
*x += *y;
Above, even though we’re assigning a mutable reference to a[1]
only, it’s like we’re doing the same for all indexes of the array, so we can’t assign another reference to y
at line 3, because each index has lost its read permission at line 2. Then, this code is logically safe, but it won’t compile in Rust.
let mut a = [0, 1, 2, 3];
let (a_l, a_r) = a.split_at_mut(2);
let x = &mut a_l[1];
let y = &a_r[0];
*x += *y;
Since the previous code is safe, but it gets anyway blocked, Rust provides a way to bypass the borrow checker in such situations. So, .split_at_mut()
here takes the a
array and puts it in two tuples called a_l
and a_r
respectively. This allow us to use these variables in place of the original array.
let mut a = [0, 1, 2, 3];
let x = &mut a[1] as *mut i32;
let y = &a[2] as *const i32;
unsafe { *x += *y; }
Last, but not least, the unsafe
reserved word allow to execute unsafe blocks. It skips the borrow checker, so the code above does compile: I think we should use it very carefully. The as
here helps us splitting array indexes just like using tuples, but I think we’ll better understand it later.
If you noticed, operations on strings in Rust are closer to Python than JavaScript: it’s not the first time I underline it. Same for tuples. Even though I prefer to provide comparisons with JavaScript, those with Python make more sense: in my humble opinion, a full-stack developer must know both nowadays.
Top comments (0)