The core concept of Rust is Ownership, a GC-enabled language that allows the runtime to sweep the heap at runtime and release unreferenced garbage objects, such as go. For languages like c/c++, you need to manage the allocation and release of memory yourself.

Rust uses the concept of Ownership and appends various check rules to the compiler to implement memory management. Note that most of Rust’s work is done at compile time, so there is no additional overhead at runtime. Here are three principles.

  • each value, has an owner
  • At the same time, a value can have only one owner
  • When the owner leaves the scope, the corresponding value is automatically droppe

RAII

Let’s first look at leaving the scope and releasing it automatically.

1
2
3
fn main() {
    let _s = String::from("hello");
}

The simplest code, just one line, allocates the string on the heap and disassembles it to observe how memory is managed.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
Dump of assembler code for function hello_cargo::main:
src/main.rs:
1	fn main() {
   0x000055555555d190 <+0>:	sub    $0x18,%rsp

2	    let _s = String::from("hello");
   0x000055555555d194 <+4>:	mov    %rsp,%rdi
   0x000055555555d197 <+7>:	lea    0x2bf86(%rip),%rsi        # 0x555555589124
   0x000055555555d19e <+14>:	mov    $0x5,%edx
   0x000055555555d1a3 <+19>:	callq  0x55555555c480 <<alloc::string::String as core::convert::From<&str>>::from>

3	}
=> 0x000055555555d1a8 <+24>:	mov    %rsp,%rdi
   0x000055555555d1ab <+27>:	callq  0x55555555c700 <core::ptr::drop_in_place<alloc::string::String>>
   0x000055555555d1b0 <+32>:	add    $0x18,%rsp
   0x000055555555d1b4 <+36>:	retq
End of assembler dump.

The assembly on line 2 calls core::convert::From to create the string variable. Then on line 3, at the end of main, core::ptr::drop_in_place is called automatically to free the string.

The automatic destructuring out of scope is much like c++’s RAII (Resource Acquisition Is Initialization), except that rust calls drop trait out of scope.

Ownership

1
2
3
4
5
6
fn main() {
    let s1 = String::from("hello");
    let s2 = s1;

    println!("s1 = {}, s2 = {}", s1, s2);
}

This code will definitely work with go, but not with rust.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
hello_cargo# cargo run
   Compiling hello_cargo v0.1.0 (/root/zerun.dong/code/rusttest/hello_cargo)
error[E0382]: borrow of moved value: `s1`
 --> src/main.rs:5:34
  |
2 |     let s1 = String::from("hello");
  |         -- move occurs because `s1` has type `String`, which does not implement the `Copy` trait
3 |     let s2 = s1;
  |              -- value moved here
4 |
5 |     println!("s1 = {}, s2 = {}", s1, s2);
  |                                  ^^ value borrowed here after move

The execution reports an error because the ownership of the s1 value has been moved to s2, and the original s1 is no longer available. move occurs because s1 has type String , which does not implement the Copy trait.

rust ownership

You can see the result of the string, just like go, the string header has pointer pointing to heap memory. If s1, s2 are shallow copies and the pointer points to data on the heap, then the same piece of memory will be freed twice after leaving scope!

1
2
3
4
5
6
fn main() {
    let s1 = String::from("hello");
    let s2 = s1.clone();

    println!("s1 = {}, s2 = {}", s1, s2);
}

You can call s1.clone() to make a deep copy to solve this problem. But it is inefficient to make a memory copy every time, so the concept of reference references will be introduced later.

Ownership and functions

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
fn main() {
    let s = String::from("hello");  // s comes into scope

    takes_ownership(s);             // s's value moves into the function...
                                    // ... and so is no longer valid here

    let x = 5;                      // x comes into scope

    makes_copy(x);                  // x would move into the function,
                                    // but i32 is Copy, so it's okay to still
                                    // use x afterward

} // Here, x goes out of scope, then s. But because s's value was moved, nothing
  // special happens.

fn takes_ownership(some_string: String) { // some_string comes into scope
    println!("{}", some_string);
} // Here, some_string goes out of scope and `drop` is called. The backing
  // memory is freed.

fn makes_copy(some_integer: i32) { // some_integer comes into scope
    println!("{}", some_integer);
} // Here, some_integer goes out of scope. Nothing special happens.

This is an example from the website. When takes_ownership is executed, the ownership of the value corresponding to s is transferred to the function and released when it leaves the scope, so if the main function tries to use it again, it will report an error.

But at the same time, x is an int value, and the makes_copy function will copy this value, not transfer ownership.

Move,Copy,Clone

In Rust, if the type does not implement Copy, then the type is assigned, passed, and returned with Move semantics, which is a bit awkward and not intuitive.

The previous string is a Move, so let’s look at an integer example.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
zerun.dong$ cat src/main.rs
fn main() {
    let s1 = 1234;
    let s2 = s1;

    println!("s1 = {}, s2 = {}", s1, s2);
}

zerun.dong$ cargo run
    Finished dev [unoptimized + debuginfo] target(s) in 0.00s
     Running `target/debug/hello_cargo`
s1 = 1234, s2 = 1234

Here s1 is the i32 type by default, which implements the Copy semantics, so s1 can be used afterwards.

To see which types implement Copy semantics by default, they are generally scalars, i.e. types allocated on the stack and sized at compile time.

  • All the integer types, such as u32.
  • The Boolean type, bool, with values true and false.
  • All the floating point types, such as f64.
  • The character type, char.
  • Tuples, if they only contain types that also implement Copy. For example, (i32, i32) implements Copy, but (i32, String) does not.

So how is it handled for custom types, such as struct?

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
struct Data {
    a: i64,
    b: i64,
}

fn test(d: Data) {
    let _d = d;
}

fn main() {
    let d = Data{a:1,b:1};
    test(d);
    println!("a is {}, b is {}", d.a, d.b);
}

zerun.dong$ cargo run
   Compiling hello_cargo v0.1.0 (/Users/zerun.dong/code/rusttest/hello_cargo)
error[E0382]: borrow of moved value: `d`
  --> src/main.rs:14:39
   |
12 |     let d = Data{a:1,b:1};
   |         - move occurs because `d` has type `Data`, which does not implement the `Copy` trait
13 |     test(d);
   |          - value moved here
14 |     println!("a is {}, b is {}", d.a, d.b);
   |                                       ^^^ value borrowed here after move

As you can see, the error is reported because d has been moved when test is called, so the variable d can no longer be used in main.

But the problem is that struct Data members are all integers, and they all implement Copy by default.

1
2
3
4
5
#[derive(Copy, Clone)]
struct Data {
    a: i64,
    b: i64,
}

Here you need to mark the struct #[derive(Copy, Clone)] , so that the custom type automatically implements the Copy trait, which requires that all fields have already implemented Copy.

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
enum E1 {
    Text,
    Digit,
}
struct S2 {
    u: usize,
    e: E1,
    s: String,
}
impl Clone for S2 {
    fn clone(&self) -> Self {
        // 生成新的E1实例
        let e = match self.e {
            E1::Text => E1::Text,
            E1::Digit => E1::Digit,
        };
        Self {
            u: self.u,
            e,
            s: self.s.clone(),
        }
    }
}

For struct S2 , since S2 has String type in its field, String type does not implement Copy trait, so S2 type cannot implement Copy trait.

S2 also contains E1 type, which does not implement Clone and Copy trait, but we can implement Clone trait for S2 type ourselves, and generate a new E1 instance in Clone::clone method, which can clone a new S2 instance.