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 keep allocating until memory unexpectedly ran out and the program crashed — 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);
}
error[E0382]: borrow of moved value: `first_owner`
--> src/main.rs:4:20
|
2 | let first_owner = String::from("Book");
| ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 | let second_owner = first_owner;
| ----------- value moved here
4 | println!("{}", first_owner);
| ^^^^^^^^^^^ value borrowed here after move
|
help: consider cloning the value if the performance cost is acceptable
|
3 | let second_owner = first_owner.clone();
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
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;
}
// random buffer output after free
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);
}
takes_ownership: Book
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);
}
error[E0382]: borrow of moved value: `first_owner`
--> src/main.rs:4:33
|
2 | let first_owner = String::from("Book");
| ----------- move occurs because `first_owner` has type `String`, which does not implement the `Copy` trait
3 | takes_ownership(first_owner);
| ----------- value moved here
4 | println!("first owner: {}", first_owner);
| ^^^^^^^^^^^ value borrowed here after move
|
note: consider changing this parameter type in function `takes_ownership` to borrow instead if owning the value isn't necessary
--> src/main.rs:7:25
|
7 | fn takes_ownership(str: String) {
| --------------- ^^^^^^ this parameter takes ownership of the value
| |
| in this function
help: consider cloning the value if the performance cost is acceptable
|
3 | takes_ownership(first_owner.clone());
| ++++++++
For more information about this error, try `rustc --explain E0382`.
error: could not compile `playground` (bin "playground") due to 1 previous error
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);
}
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]>());
}
i32: false
f64: false
bool: false
char: false
String: true
Vec: true
File: true
Box: true
(i32, i32): false
(i32, String): true
[i32; 3]: false
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!

Top comments (0)