Rust: Structure Memory when Using a Function<!-- --> | <!-- -->Patrick Desjardins Blog
Patrick Desjardins Blog
Patrick Desjardins picture from a conference

Rust: Structure Memory when Using a Function

Posted on: November 4, 2022

I recently learned about the tool called clippy that is available if you use cargo. It provides lint information on your Rust code to ensure you are using good practices but also helps to use Rust idiomatic and consistent code. For example, one recommendation was to not use .clone() on one of my structures. The suggestion made me think. I was passing the structure into a few functions, and I wanted to ensure no function was modifying the content.

Coming from a JavaScript/TypeScript background, I am cloning and passing the clone to have immutability. However, in Rust, passing a structure in a function does not give a reference in memory to that structure. Nor that passing a structure into a function allows the function to mutate. Rust has two explicit keywords: mut and & to allow mutation and to pass by reference.

Here is a short code that illustrates how a structure is preserved. But, first, let's create a structure.

1#[derive(Copy, Clone)]
2struct Test {
3 x: u32
4}

Second, let's create a main function that will create a structure we do not want to modify.

1fn main() {
2 // Create value and print its memory address
3 let t = Test {x:1};
4 println!("Original t: {:p}", &t);
5
6 // Call the mutate function
7 let new_t = mutate(t);
8
9 // Print the values
10 println!("{}",t.x);
11 println!("{}",new_t.x);
12}

The main function calls the mutate and returns a new instance of the struct. The expectation is that t remains with the value 1 and that the returned value will be 100.

1fn mutate(t_param: Test) ->Test {
2 // Create a variable that is `x` but marked that it can be mutated
3 let mut new_test = t_param;
4
5 // Print memory address
6 println!("t_param: {:p}", &t_param);
7 println!("new_test: {:p}", &new_test);
8
9 // Mutate the value from 1 to 100
10 new_test.x = 100;
11 new_test
12}

The mutate function cannot change the value of t_param.x without the mut keyword. Rust will throw a compilation error saying that the variable must be mutable. Adding mut to a new variable creates new variables. The memory prints below the assignment show two addresses, which means that the variable passed by the parameter (t_param) is not using the same memory as new_test.

The program prints:

1Original t: 0x7ffcc6ba0e94
2t_param: 0x7ffcc6ba0dc0
3new_test: 0x7ffcc6ba0dc4
41
5100

Moving Ownership

In the mutate function, we moved ownership to another variable by assigning the parameter to a new variable.

1let mut new_test = t_param;

The ownership is not transferred because of mut

The same occurs when we assign a variable to a function by parameter. For example, moves ownership in the following code:

1let new_test = t_param;

The reason is to avoid having two or more variables with a pointer to a space in memory. It avoids bugs.

The consequence of moving the ownership is that the former variable cannot access anything from the object.

Let's modify the former example and see:

1fn main() {
2 let mut t = Test {x:1};
3 let mut new_test = t;
4 println!("t_param: {}", t.x);
5 println!("new_test: {}", new_test.x);
6
7 t.x = 2;
8 new_test.x = 1000;
9 println!("t_param: {}", t.x);
10 println!("new_test: {}", new_test.x);
11
12 println!("t_param: {:p}", &t);
13 println!("new_test: {:p}", &new_test);
14}

The output is:

1t_param: 1
2new_test: 1
3t_param: 2
4new_test: 1000
5t_param: 0x7ffd03e227f0
6new_test: 0x7ffd03e227f4

We can see that the t_param and new_test can adjust the x without affecting each other.

Function Parameter

What small detail that may have gone unnoticed is the trait above the struct #[derive(Copy, Clone)]. This is required to pass the structure in a function otherwise would create a compilation error with a borrow of moved value message. The copy trait is required because, unlike primitive types, they are copied by default. The clone is required because of the copy trait. You can quickly see what I am saying by clicking in VSCode on the copy, which will lead you to the source code of the trait, and you will see:

1#[rustc_unsafe_specialization_marker]
2#[rustc_diagnostic_item = "Copy"]
3pub trait Copy: Clone {
4 // Empty.
5}

We can step back and have the code (without trait on the struct). Then, we realize the code does not comply with the borrow of moved value.

1struct Test {
2 x: u32
3}
4
5fn main() {
6 let t = Test {x:1};
7 let new_test = t;
8 println!("t_param: {:p}", &t);
9 println!("new_test: {:p}", &new_test);
10}

Adding #[derive(Copy, Clone)] makes the compiler happy. Once again, the reason is that we are moving from t to new_test, which does a copy.

The output is with two different addresses:

1t_param: 0x7ffda950bdb0
2new_test: 0x7ffda950bdb4

Reference a.k.a. borrowing

Sometimes, you may want to pass more than one variable without making copies. For example, you are giving a reference, or in the Rust's lingo, is to borrow, which cause an alias to the source of truth. However, there are some additional rules. For example, borrowing into a mutable variable with &mut is only possible once per scope, a.k.a. lifetime. So, for example, you can &mut in a function once and do it again in another function.

1#[derive(Copy, Clone)]
2struct Test {
3 x: u32
4}
5
6fn main() {
7 let mut t = Test {x:1};
8 println!("t_param: {}", t.x);
9 println!("t_param: {:p}", &t);
10 display(&mut t);
11 println!("t_param: {}", t.x);
12 println!("t_param: {:p}", &t);
13}
14
15fn display(new_test: &mut Test){
16 new_test.x = 1000;
17 println!("Function x value: {}", new_test.x);
18 println!("Function struct ref address: {:p}", new_test);
19}

In this example, the output of add the addresses is the same:

1t_param: 1
2t_param: 0x7ffc5187bd64
3Function x value: 1000
4Function struct ref address: 0x7ffc5187bd64
5t_param: 1000
6t_param: 0x7ffc5187bd64

The function shows that the reference address is the same as the original struct with 0x7ffc5187bd64.

Conclusion

Clippy was right! There was no need to call explicitly .clone() because the structure is getting cloned automatically.