What is Ownership?
Ownership is the defining feature of Oxide. It's the system that allows Oxide to make memory safety guarantees without needing a garbage collector. Understanding ownership is crucial to becoming proficient in Oxide. In this chapter, we'll explore ownership and related features that help manage memory automatically.
Oxide inherits Rust's brilliant ownership system with one simple observation: Rust was right. Rather than reinvent the wheel, Oxide provides Oxide's familiar syntax while keeping ownership semantics identical to Rust. This means you get the same safety guarantees, the same zero-cost abstractions, and the same performance—just with a syntax that feels more natural if you're coming from Swift, Kotlin, or TypeScript.
The Ownership Rules
Oxide has three fundamental ownership rules:
- Each value in Oxide has a variable that is its owner
- There can only be one owner at a time
- When the owner goes out of scope, the value is dropped (freed)
These rules prevent memory leaks and use-after-free bugs at compile time. Let's explore each with examples.
Variable Scope
A scope is the range within a program where an item is valid. Consider this example:
fn main() {
{ // s is not yet declared
let s = "hello" // s is valid from this point forward
println!("\(s)") // s is still valid
} // this scope is now over, and s is no longer valid
// println!("\(s)") // Error: s is out of scope
}
When s comes into scope, it is valid. It remains valid until it goes out of scope. At that point, the string's memory is automatically freed.
The String Type and the Move Semantic
Let's look at how ownership works with dynamically allocated data. We'll use String as our example because it's more interesting than the &str literals we've seen so far:
fn main() {
let s1 = String.from("hello")
let s2 = s1 // s1's data is MOVED to s2
// println!("\(s1)") // Error: s1 no longer owns the data
println!("\(s2)") // OK: s2 owns the data
}
When we assign s1 to s2, the ownership transfers. This is different from copying the data—it's moving ownership. s1 is no longer valid, and s2 owns the string data.
Why move instead of copy? Moving is Oxide's way of ensuring that only one owner exists at a time. This prevents multiple parts of your code from trying to manage the same memory, which would be a recipe for bugs.
Here's what happens in memory:
s1is created and points to allocated memory containing "hello"s2 = s1moves the pointer, length, and capacity froms1tos2s1is invalidated (its ownership is lost)- When
s2goes out of scope, the memory is freed
This is sometimes called a "shallow copy" in other languages, but Oxide goes further by making the original binding invalid. There's no risk of both variables trying to free the same memory.
Ownership and Functions
The same ownership rules apply when passing values to functions:
fn main() {
let s = String.from("hello")
takesOwnership(s)
// println!("\(s)") // Error: s has been moved into the function
}
fn takesOwnership(someString: String) {
println!("\(someString)")
} // someString goes out of scope and the string is dropped
When s is passed to takesOwnership, ownership of the string is transferred to the function's parameter. Once the function returns, the string is dropped.
If you want to use s after calling the function, you need to get the ownership back:
fn main() {
let s = String.from("hello")
let s = takesAndReturnsOwnership(s)
println!("\(s)") // OK: we got ownership back
}
fn takesAndReturnsOwnership(someString: String): String {
someString // ownership is returned
}
This pattern—taking ownership and returning it—is cumbersome. This is why Oxide has references and borrowing, which we'll explore in the next section. For now, understand that ownership follows these simple rules everywhere in your code.
Ownership and Copying
Some types in Oxide are simple enough that their values can be copied bit-by-bit without issues. These types implement the Copy trait. If a type implements Copy, ownership is not moved—the value is copied instead:
fn main() {
let x = 5
let y = x // x is COPIED, not moved
println!("x = \(x), y = \(y)") // Both are valid!
}
Integer types, floating-point numbers, booleans, and characters all implement Copy because they're small and live on the stack. More complex types like String do not implement Copy because their data lives on the heap.
As a rule of thumb:
- Stack types (integers, floats, bools, chars) implement
Copy - Heap types (strings, vectors, collections) do NOT implement
Copy
Implicit Returns and Ownership
In Oxide, the last expression in a function is the return value. This interacts with ownership in an important way:
fn createString(): String {
let s = String.from("hello")
s // ownership is transferred to the caller
}
fn main() {
let result = createString()
println!("\(result)") // OK: createString returned ownership
}
The value s goes out of scope at the end of createString, but because it's being returned, ownership is transferred to the caller. The memory is not dropped.
Why Ownership Matters
Oxide's ownership system provides several critical benefits:
- Memory Safety: Ownership prevents use-after-free and double-free bugs
- No Garbage Collector: Memory is freed automatically without runtime overhead
- No Runtime Errors: Memory errors are caught at compile time, not in production
- Zero-Cost Abstractions: The safety checks have no runtime cost
The elegance of Rust's ownership system is that it aligns with how we actually think about resources. When a function takes ownership, it's clear that the function is responsible for that resource. When it returns something, it transfers responsibility to the caller. This natural model prevents accidental resource leaks.
Rust's Ownership: The Gold Standard
Rust's ownership system is considered one of the greatest achievements in programming language design. It solved a problem that plagued systems programming for decades: how to provide memory safety without garbage collection. Oxide embraces this system completely.
If you've used languages with garbage collectors, this might feel unfamiliar at first. But once you understand the rules, you'll find that ownership makes code clearer and safer. You're not fighting the language—the language is helping you express your intent precisely.
Summary
- Each value has one owner at a time
- Ownership transfers when assigning to a new variable or passing to a function
- When the owner goes out of scope, the value is dropped (memory is freed)
- Types that implement
Copyare copied instead of moved - The ownership rules are the same as Rust—we kept what worked perfectly
The ownership system might seem strict, but it's what makes Oxide safe by default. In the next section, we'll learn about references and borrowing, which lets you use data without taking ownership of it.