Why?

Ownership is the concept in Rust that determines how memory is managed.

In languages with a garbage collector, allocating objects is straightforward — the GC scans for unreachable objects and frees their memory.

…but Rust has no garbage collector. Without any rules, we could free memory and still use it, or free it twice — and that’s exactly where Ownership comes in.

fn main() {
    let first_owner = String::from("Book");
    let second_owner = first_owner;
    println!("{}", first_owner);
    println!("{}", second_owner);
}
Ctrl+Enter to run

As you can see — we barely wrote anything and Rust is already complaining. But that’s intentional!

Remove the first println!() (you can try it right here :)) and the code compiles just fine.

Why?

Because every value has exactly one owner, and by assigning it to another variable we invalidated the first one.

Bug or feature?

To illustrate the problem more clearly, let’s write something similar in C:

  #include <stdio.h>
  #include <stdlib.h>
  #include <string.h>

  int main() {
      char *s = malloc(strlen("Rust") + 1);
      strcpy(s, "Rust");
      free(s);
      printf("%s\n", s);
      return 0;
  }
Ctrl+Enter to run

How does this relate to Rust?

In the C example above, we were able to free memory and then use it anyway — creating a potential exploit or undefined behavior.

In C there is no ownership, so who owns s?

Nobody — it’s just a pointer.

The programmer bears full responsibility for ensuring memory is freed at the right moment, without creating an opportunity for remote code execution.

This class of bugs has its own CVE category with high CVSS scores, and we’ve seen hundreds of them in the wild. Rust eliminates this statically — the program won’t compile instead of silently misbehaving.

Transferring ownership through functions

Let’s try passing ownership through a function:

fn main() {
    let first_owner = String::from("Book");
    takes_ownership(first_owner);
}

fn takes_ownership(str: String)  {
    println!("takes_ownership: {}", str); 
}
Ctrl+Enter to run

The program runs fine and prints the value. Now let’s add a println! to print first_owner after the call:

fn main() {
    let first_owner = String::from("Book");
    takes_ownership(first_owner);
    println!("first owner: {}", first_owner);
}

fn takes_ownership(str: String)  {
    println!("takes_ownership: {}", str); 
}
Ctrl+Enter to run

Rust won’t compile again — ownership was transferred into takes_ownership.

The invisible Drop

We know Rust enforces a single owner. Let’s compile our program and inspect the resulting assembly using objdump:

objdump -d -M intel target/debug/ownership | grep -A 20 "takes_ownership"
  ===============================================================
  2318b: mov rsi, [rsp+0x8]       second argument for _print (len)
  ========= println implementation ============
  2319e: jmp 231a0                jump to drop

  231a0: mov rdi, [rsp+0x18]      pointer to String (argument for drop)
  231a5: call drop_in_place        drop at }
  231aa: add rsp, 0x58            restore stack
  231ae: ret                      return from function
  

In garbage-collected languages, the GC checks whether objects are out of scope and frees their memory. In Rust, the compiler injects the resource cleanup at the end of the value’s lifetime — in this case at }.

fn main() {
    let first_owner = String::from("Book"); // move ownership into takes_ownership
    takes_ownership(first_owner);
    println!("first owner: {}", first_owner);
} 

fn takes_ownership(str: String)  {
    println!("takes_ownership: {}", str); 
} // drop injected here

This is analogous to a destructor (~MyClass()) in C++ — with the difference that Rust guarantees it runs exactly once through the ownership system, whereas in C++ it depends on the programmer’s discipline.

Rust also allows implementing the Drop trait, which is called whenever a value goes out of scope.

Let’s use the file-opening mechanism from std:

  use std::fs::File;

  fn main() {
      {
          let f = File::open("/etc/hostname").unwrap();
          println!("file opened");
      }  // close(fd) here

      println!("file already closed");
  }
  impl Drop for OwnedFd {
      fn drop(&mut self) {
          unsafe {
              let _ = libc::close(self.fd.as_inner());  // ← syscall close(fd)
          }
      }
  }

libc::close is called automatically — it all happens under the hood!

To give ownership back, simply return the value:

fn takes_ownership(str: String)  {
    println!("takes_ownership: {}", str);
    str // return ownership back
}

Copy: when ownership transfer becomes a copy

fn main() {
    let first_owner:i32 = 10;
    takes_ownership(first_owner);
    println!("first_owner: {}", first_owner);
}

fn takes_ownership(num: i32)  {
    println!("takes_ownership: {}", num); 
}
Ctrl+Enter to run

Compared to our first example, Rust should be complaining here — but instead it compiles just fine.

  use std::fs::File;

  fn main() {
      println!("i32:    {}", std::mem::needs_drop::<i32>());
      println!("f64:    {}", std::mem::needs_drop::<f64>());
      println!("bool:   {}", std::mem::needs_drop::<bool>());
      println!("char:   {}", std::mem::needs_drop::<char>());

      println!("String: {}", std::mem::needs_drop::<String>());
      println!("Vec:    {}", std::mem::needs_drop::<Vec<i32>>());
      println!("File:   {}", std::mem::needs_drop::<File>());
      println!("Box:    {}", std::mem::needs_drop::<Box<i32>>());

      println!("(i32, i32):    {}", std::mem::needs_drop::<(i32, i32)>());
      println!("(i32, String): {}", std::mem::needs_drop::<(i32, String)>());
      println!("[i32; 3]:      {}", std::mem::needs_drop::<[i32; 3]>());
  }
Ctrl+Enter to run

For primitive types, Rust creates a copy instead of moving ownership. These types live entirely on the stack — a copy is literally a few bytes, with no side effects, no allocations, no pointers.

Two copies are completely independent.

Rust implements a trait that tells the compiler a type can be copied bitwise:

#[derive(Copy, Clone)]
struct Point {
    x: f64,
    y: f64,
}

This only makes sense when a type lives entirely on the stack and doesn’t manage any resources (e.g. heap memory, file descriptors).

That’s why String cannot implement Copy — it owns an internal buffer on the heap.

But code sometimes needs not just to transfer ownership, but to access the same value from multiple places.

That’s what Borrowing is for — which will be the topic of the next post. Stay tuned!