CS3210 Design Operating Systems

Taesoo Kim





Rust 2: Ownership and Lifetime

Taesoo Kim

Administrivia

Q&A

Q&A

  1. Code accepted by Rustc vs. not? (all code rejected by Rustc is unsafe?)

The book mentioned the lifetime system being too coarse and throwing compiler errors where it shouldn’t, since it hasn’t been updated and fixed yet. Have you personally experienced this often, and wrote correct code that was counted as incorrect? Should we expect this to happen often?

  1. Is str and string literal the same?
  2. Why traits instead of classes? (→ Lec 8)

Agenda

Owned type: &str vs. String

let s = String::from("Hello"); // what's thye type of s?

let x = "Hello"; // what's the type of x?
let _: () = x;

Owned type: &str vs. String

let x = "Hello";

Owned type: &str vs. String

dbg!(std::mem::size_of::<str>());
//=> error: `str` doesn't have a size known at compile-time
dbg!(std::mem::size_of::<&str>());   //=> 16

dbg!(std::mem::size_of::<String>()); //=> 24
dbg!(std::mem::size_of::<&String>());//=>  8

Example: benefit of owned type

let mut x = String::from("Hello");
x.push_str(" World!"); // Isn't the size of String changed?

dbg!(x);

Example: benefit of owned type

impl String {
    pub fn push_str(&mut self, string: &str) { .. }
    //              |
    //              +--> a mutable reference
}

let mut x = String::from("Hello");

{
  //=> String::push_str(&x, " World!")
  x.push_str(" World!");
}

dbg!(x);

Introducing a slice type (e.g., &str)

let x: &str = "Hello";

Example of a slice type

let x = String::from("Hello World");
let y = &x[0..5];

Thinking of memory safety

// in C++
std::string s = "Hello";
const char *c = s.c_str();

s.append(" World!");

std::cout << c;
// in Rust
let mut s = String::from("Hello");
let c = s.as_str();
    
s.push_str(" World!");

dbg!(c);

Memory safety by restricted aliasing

let mut s = String::from("Hello");
let c = s.as_str();
    
s.push_str(" World!");

dbg!(c);

Aliasing for better optimization

// in Rust (rustc -C opt-level=3)
fn compute(input: &u32, output: &mut u32) {
  if *input > 10 { *output  = 1; }
  if *input >  5 { *output *= 2; }
}
// in C (gcc -O3)
void compute(int *input, int *output) {
  if (*input > 10) *output  = 1;
  if (*input >  5) *output *= 2;
}

Ref. Nomicon: 3.2 Aliasing

Aliasing: C version

void compute(int *input, int *output):
  if (*input > 10) *output  = 1;
  if (*input >  5) *output *= 2;
0000000000001190 <compute>:
    1190:  mov    eax,DWORD PTR [rdi]
    1192:  cmp    eax,0xa
 +--1195:  jle    119f <compute+0xf>
 |  1197:  mov    DWORD PTR [rsi],0x1
 |  119d:  mov    eax,DWORD PTR [rdi]
 +->119f:  cmp    eax,0x5
 +--11a2:  jle    11a6 <compute+0x16>
 |  11a4:  shl    DWORD PTR [rsi],1
 +->11a6:  ret

Aliasing: Rust version

fn compute(input: &u32, output: &mut u32):
  if *input > 10 { *output  = 1; }
  if *input >  5 { *output *= 2; }
00000000000044c0 <testing::compute>:
    44c0:  mov    eax,DWORD PTR [rdi]
    44c2:  cmp    eax,0xb
+---44c5:  jb     44d4 <testing::compute+0x14>
|   44c7:  mov    DWORD PTR [rsi],0x1
|   44cd:  mov    eax,0x2
|+- 44d2:  jmp    44dd <testing::compute+0x1d>
+|->44d4:  cmp    eax,0x6
 |+-44d7:  jb     44df <testing::compute+0x1f>
 || 44d9:  mov    eax,DWORD PTR [rsi]
 || 44db:  add    eax,eax
 +|>44dd:  mov    DWORD PTR [rsi],eax
  +>44df:  ret

Aliasing: Rust version

00000000000044c0 <testing::compute>:
    44c0:  mov    eax,DWORD PTR [rdi]
    44c2:  cmp    eax,0xb
+---44c5:  jb     44d4 <testing::compute+0x14>
|   44c7:  mov    DWORD PTR [rsi],0x1  ; Q. isn't ok to mov 0x2/ret?
|   44cd:  mov    eax,0x2
|+- 44d2:  jmp    44dd <testing::compute+0x1d>
+|->44d4:  cmp    eax,0x6
 |+-44d7:  jb     44df <testing::compute+0x1f>
 || 44d9:  mov    eax,DWORD PTR [rsi]
 || 44db:  add    eax,eax
 +|>44dd:  mov    DWORD PTR [rsi],eax
  +>44df:  ret

Lifetime

let y = {
  let x = String::from("Hello");
  &x
};

Lifetime

fn main() {
  let mut s = String::from("Hello");
  let r1 = &mut s;
  let r2 = &s;

  dbg!(r2);
}

Notating lifetime

let x = 0;
let y = &x;
//  |   |
//  |   +--> a reference (a borrower) should live shorter than the owner
//  +--> y can contain a refernece that lives longer than itself
'a: {                      // this scope's lifetime is 'a
  let x: i32 = 0;
  'b: {                    // this scope's lifetime is 'b
    let y: &i32 = &x;
  }
}

Notating lifetime

'a: {
  let x['a]: i32 = 0;
  'b: {
    // y's lifetime is 'b
    let y['b]: &'a i32 = &x['a];
    //                   &x's lifetime is 'a (or shorter)

    // -> y can only contain a reference that lives longer than itself
    }
  }
}

Notating lifetime

'a: {
  let x['a]: i32 = 0;
  'b: {
    let y['b]: &['a -> 'b] i32 = &x['a];
    // -> y can only contain a reference that lives longer than itself
    //    reduce its lifetime from 'a to 'b
    }
  }
}

Example: lifetime notation

let x = 0;
let y = &x;
let z = &y;
'a: {                      // this scope's lifetime is 'a
  let x: i32 = 0;
  'b: {                    // this scope's lifetime is 'b
    let y: &i32 = &x;
    'c: {                  // this scope's lifetime is 'c
      let z: &&i32 = &y;
    }
  }
}

Example: lifetime notation

let x = 0;
let y = &x;
let z = &y;
'a: {
  let x: i32 = 0;
  'b: {
    let y: &'b i32 = &['a -> 'b] x;
    'c: {
      let z: &'c &'b i32 = &['b -> 'c] y;
    }
  }
}

Ref. Nomicon, Ch3.3 Lifetime

Example: lifetime notation

let x = 0;
let z;
let y = &x;
z = y;

Example: lifetime notation

let x = 0;
let z;
let y = &x;
z = y;
'a: {
  let x['a]: i32 = 0;
  'b: {
    let z['b]: &'b i32;
    'c: {
      let y['c]: &'c i32 = &['a -> 'c] x;
      
      // WARNING: &'b i32 <- &'c i32
      //  z contains a reference that lives shorter than itself!
      z = y;
    }
  }
}

Example: lifetime notation

let x = 0;
let z;
let y = &x;
z = y;
'a: {
  let x['a]: i32 = 0;
  'b: {
    let z['b]: &'b i32;
    'c: {
      // in fact, &x can be 'b or even 'a!
      let y['c]: &'b i32 = &'b x;

      // Okay. &'b i32 <- &'b i32
      z = y;
    }
  }
}

Wait, why trying to minimize lifetime?

fn main() {
  let mut s = String::from("Hello");
  let r1 = &mut s;
  let r2 = &s;

  dbg!(r2);
}

Let’s analyze a previous example

let y = {
  let x = String::from("Hello");
  &x
};
'a: {
  let y['a] = 'b: {
    let x['b] = String::from("Hello");
    &'b x
  };
  // WARNING. y['a] contains a reference that lives shorter (&'b)!
}

Example: function call/return

fn to_string(data: &u32) -> &String {
  let s = format!("{}", data);
  &s
}

Example: function call/return

fn to_string(data: &u32) -> &String {
  let s = format!("{}", data);
  &s
}
fn to_string<'a>(data: &'a u32) -> &'a String {
  'b: {
    let s['b] = format!("{}", data);
    &'b s
    // WARNING: &'a <- &'b
    //  the return value contains a reference that lives shorter
  }
}

Example: how to fix?

fn to_string(data: &u32) -> &String {
  let s = format!("{}", data);
  &s
}

// or even simpler?
fn to_string() -> &String;

Example: aliasing a mutable reference (again)

let mut s = String::from("Hello");
let c = s.as_str();
    
s.push_str(" World!");

dbg!(c);

Example: aliasing a mutable reference (again)

let mut s = String::from("Hello");
let c = s.as_str();
    
s.push_str(" World!");

dbg!(c);

Example: aliasing a mutable reference (again)

let mut s = String::from("Hello");
// => fn as_str(&'a self) -> &'a str 
let c = s.as_str();

// => fn push_str(&'a mut self, string: &'a str)
s.push_str(" World!");

dbg!(c);

Example: aliasing a mutable reference (again)

let mut s = String::from("Hello");
// => fn as_str(&'a self) -> &'a str 
let _r1 = &s;
let c = String::as_str(_r1);

// => fn push_str(&'a mut self, string: &'a str)
let _r2 = &mut s;
String::push_str(_r2, " World!");

dbg!(c);

Example: aliasing a mutable reference (again)

let mut s = String::from("Hello");
let _r1: &String = &s;
let c: &str = String::as_str(_r1);

let _r2: &mut String = &mut s;
String::push_str(_r2, " World!");

dbg!(c);

Example: aliasing a mutable reference (again)

'a: {
  let mut s = String::from("Hello");
  'b: {
    let _r1: &String = &s;
    let c: &str = String::as_str(_r1);

    'c: {
      let _r2: &mut String = &mut s;
      String::push_str(_r2, " World!");
     }

     dbg!(c);
  }
}

Example: aliasing a mutable reference (again)

'a: {
  let mut s = String::from("Hello");
  'b: {
    let _r1: &'b String = &['a -> 'b]s;
    let c: &'b str = String::as_str(_r1);

    'c: {
      let _r2: &'c mut String = &['a -> 'c] mut s;
      String::push_str(_r2, " World!");
     }

     dbg!(c);
  }
}

Example: aliasing a mutable reference (again)

'a: {
  let mut s = String::from("Hello");
  'b: {
    let _r1: &'b String = &['a -> 'b]s;
    let c: &'b str = String::as_str(_r1);

    'c: {
      let _r2: &'c mut String = &['a -> 'c] mut s;
      String::push_str(_r2, " World!");
     }

     dbg!(c);
  }
}

Example: lifetime vs. scope (again)

let mut s = String::from("Hello");
let c = s.as_str();
dbg!(c);
    
s.push_str(" World!");

Example: lifetime vs. scope (again)

'a: {
  let mut s = String::from("Hello");
  'b: {
    let c = String::as_str(&'b s);
    dbg!(c);
   }
  'c: {
    String::push_str(&'c mut s, &'c " World!");
  }
}

Lifetime elision

&'a T
&'a mut T
T<'a>

fn print(s: &str);        // elided
fn print<'a>(s: &'a str); // expanded

fn get_str() -> &str;     // Q. how to interpret?
fn get_longer(s1: &str, s2: &str) -> &str; // Q. how to interpret?

Ref. Nomicon, 3.5 Lifetime Elision

Example: using lifetime annotation

fn lhs<'a>(s1: &'a str, s2: &'a str) -> &'a str {}
fn lhs<'a, 'b>(s1: &'a str, s2: &'b str) -> &'a str {}

fn rhs<'a, 'b>(s1: &'a str, s2: &'b str) -> &'b str {}

Example: using lifetime annotation

// an instance of S should live as long as s ('a)
//   or consider it as a constructor function that accepts
//   &'a str and produces S that should live just long as 'a
struct S<'a> {
  s: &'a str,
}

// promises to produce &str that lives just long as both params
fn get_longer<'a>(s1: &'a str, s2: &'a str) -> &'a str {
  if s1.len() < s2.len() {
    s2
  } else {
    s1
  }
}

Example: complicated lifetime

fn get(s: &str) -> (&str, &str) {}

fn get<'a>(s: &'a str) -> (&'a str, &'a str) {}
fn get<'a, 'b>(s: &'a str) -> (&'a str, &'b str) {}

Unbounded lifetime (’static)

let _: () = "Hello";

Example: using ’static

// String::from(s: &str) -> String
let s = String::from("Hello");

// => fn push_str(&mut self, string: &str)
s.push_str(" World!");

Next lecture

References