Ownership in Rust

Ownership in Rust

As I mentioned earlier here about "borrowing" in Rust, this concept is relative new for me and is an "unique" feature from Rust. By using this concept, Rust guarantee me memory-safety without needing a garbage collector.

Ownership is a central feature of Rust. Before we dive deeper, let's talk why we should care about this concept.

Every program have to manage the way they use a computer's memory while running. Some languages (like JavaScript) have garbage collection that constantly looks for no longer used memory as the program runs; in other languages, the programmer must explicitly allocate and free the memory (like in C).

Rust don't use either garage collection nor memory allocation, they inventing the new concept: Borrowing, the way how memory is managed through a system of ownership with a set of rules that the compiler checks at compile time.

For giving you more context, let's we create some basic example. For make it easier to understand, let's first we write the code in JavaScript.

function saySomething (string) {
  console.log("%s", string)
}

function main () {
  const hello = 'hello world'
  saySomething(hello) // hello world
}

main()

JavaScript code above is understandable, everyone can expect how the program runs even without actually run it on their own machine. Let's create something familiar in Rust.

fn say_something(string: String) {
  println!("{}", string);
}

fn main() {
  let hello = "hello world";
  say_something(hello);
}

Let's compile it

$ rustc main.rs -o main

error[E0308]: mismatched types
 --> main.rs:7:19
  |
7 |     say_something(hello);
  |                   ^^^^^
  |                   |
  |                   expected struct `std::string::String`, found &str
  |                   help: try using a conversion method: `hello.to_string()`
  |
  = note: expected type `std::string::String`
             found type `&str`

error: aborting due to previous error

For more information about this error, try `rustc --explain E0308`.

Oops, let's follow the instruction by executing rustc --explain E0308.

This error occurs when the compiler was unable to infer the concrete type of a
variable. It can occur for several cases, the most common of which is a
mismatch in the expected type that the compiler inferred for a variable's
initializing expression, and the actual type explicitly assigned to the
variable.

For example:

```
let x: i32 = "I am not a number!";
//     ~~~   ~~~~~~~~~~~~~~~~~~~~
//      |             |
//      |    initializing expression;
//      |    compiler infers type `&str`
//      |
//    type `i32` assigned to variable `x`

Explanation above is cannot be understood. Since below TypeScript code is compiled to our original JavaScript code above.

function saySomething (string: String) {
  console.log("%s", string)
}

function main () {
  const hello = 'hello world'
  saySomething(hello) // hello world
}

main()

And even successfully runs on my machine.

So, what's wrong here?

That borrowing stuff. Let's try change Rust code above to this instead:

fn main() {
  let hello = "hello world";
  println!("{}", hello);
}

It works as expected!

I use this example to makes us easier understanding "borrowing concept" in Rust.

Ownership Rules

Remember what is ownership in Rust? the way how memory is managed through a system of ownership with a set of rules that the compiler checks at compile time.

Yes, that rules. What rules?

  • Each value in Rust has a variable that’s called its owner.
  • There can only be one owner at a time.
  • When the owner goes out of scope, the value will be dropped.

Let me illustrate this.

fn say_something(string: String) {
  // 4. `string` value here is not come from hello
  // 	since it was out of scope. Compiler assuming `string` to reference
  //	but the parameter is accept value, not reference.
  println!("{}", string);
}

fn main() {
  // 1. hello is not valid here since it don't declared yet
  let hello = "hello world";

  // 2. now hello is valid since it was declared
  println!("{}", hello);
  
  // 3. call `say_something` fn with "value" from `hello`
  // 	and pass the value to parameter
  say_something(hello);
}

That's why the error was:

  = note: expected type `std::string::String`
             found type `&str`

How to deal with this?

We should know why Rust did this.

Let me explain this in short, but I will skip the topic about "lifetimes" for now (I will talk about it in next post). OK, This code:

let hello = "hello world"

// is equivalent with

let hello: &'static str = "hello world"

They are 'static because they're stored directly in the final binary, and so will be valid for the 'static duration. And yes, hello variable above is "statically allocated" since the type is &'static str.

Since hello variable is static, and static is only lives in one instance, its understandable why:

  • say_something is cannot be run, since the parameter they expect was a value; not a reference.
  • string in say_something is doesn't know how to get the value since they doesn't know where the pointer is (it only lives in main function)

To solve this, we can convert to std::string::String instead of 'static str for the type. So the type of hello is a String which is dynamically allocated (heap) than compiler will ignore the static memory allocation which is done in compile time.

To practically solve this, we have 2 option:

  • While calling say_something function, call to_string function in parameter (e.g: say_something.to_string())
  • Convert hello variable "directly" to String. We can use String::from or use to_string like above which is has similar behavior

Let's try this

fn say_something(string: String) {
  println!("{}", string);
}

fn main() {
  let hello = "hello world";
  say_something(hello.to_string());
}

Let's compile & execute this.

$ rustc main.rs -o main

./main

hello world

It works as expected.

Ok to make this easier to understand, let's see where is the difference. I create this code just to make sure how the memory is allocated and how Rust use the memory.

fn say_something(string: String) {
  println!("{:?}", string.as_ptr());
  println!("{}", string);
}

fn main() {
  let hello = "hello world";
  println!("from main");
  println!("{:?}", hello.as_ptr());
  println!("{}", hello);
  println!();
    
  println!("from say_something(hello)");
  say_something(String::from(hello));
  println!();
  
  println!("from say_something(String::from(\"hello world\"))");
  say_something(String::from("hello world"));
  println!();
  
}

Result from code above was like this:

Basically our program is only to output the same "hello world" text to stdout. But see the differences:

  • in main, the memory was allocated in 0x10d116daa
  • in another, the memory was allocated in same pointer: 0x7f91aef00000

Why this happens? Because in main (which is referring to "hello world" with 'static str type) the memory is "statically allocated" in compile-time, and another is "dynamically allocated" in runtime even with different way (first is taken from string, and the second is directly via String::from).

Maybe you think this problem is only in "type" not the "borrowing concept" itself. I use this case to making us easier in understanding borrowing concept in Rust by using the "static" and "dynamic" memory allocation.

This is why Rust is memory-safe even without garbage collection and "alloc-free memory" concept, because Rust doing the memory-things in compile time too.

From this post I hope we can understand the basic of borrowing concept in Rust, we'll talk about "lifetimes" in next post. Stay tune!

Discussion here.

Repo here.