Memory Safety
On this page 33
Home provides memory safety without garbage collection through a combination of ownership, borrowing, and lifetime analysis. This approach catches memory errors at compile time while maintaining predictable performance.
Overview
Home's memory safety guarantees:
- No null pointer dereferences: Optional types replace null
- No use-after-free: Ownership prevents dangling pointers
- No double-free: Single ownership ensures one deallocation
- No data races: Borrowing rules prevent concurrent mutation
- No buffer overflows: Bounds checking with opt-out
Ownership Model
Single Ownership
Every value has exactly one owner:
fn main() {
let s1 = String.from("hello") // s1 owns the string
let s2 = s1 // Ownership moves to s2
// print(s1) // Error: s1 no longer valid
print(s2) // OK: s2 is the owner
} // s2 is dropped here, memory freed
Move Semantics
struct LargeData {
buffer: [u8; 1_000_000],
}
fn process(data: LargeData) {
// data is moved in, this function owns it
// ...
} // data dropped here
fn main() {
let data = LargeData { buffer: [0; 1_000_000] }
process(data) // Ownership transfers
// process(data) // Error: data was moved
}
Copy Types
Small, simple types implement Copy for implicit duplication:
# [derive(Copy, Clone)]
struct Point {
x: i32,
y: i32,
}
fn main() {
let p1 = Point { x: 1, y: 2 }
let p2 = p1 // p1 is copied, not moved
print("{p1.x}, {p2.x}") // Both valid
}
Clone for Explicit Copies
let s1 = String.from("hello")
let s2 = s1.clone() // Explicit deep copy
print("{s1}, {s2}") // Both valid
Borrowing
Immutable Borrows
fn calculate_length(s: &string) -> usize {
s.len()
} // s goes out of scope, but doesn't drop (it's borrowed)
fn main() {
let s = String.from("hello")
let len = calculate_length(&s) // Borrow s
print("Length of '{s}' is {len}") // s still valid
}
Mutable Borrows
fn append_world(s: &mut string) {
s.push_str(", world!")
}
fn main() {
let mut s = String.from("hello")
append_world(&mut s) // Mutable borrow
print(s) // "hello, world!"
}
Borrowing Rules
fn main() {
let mut data = vec![1, 2, 3]
// Multiple immutable borrows OK
let r1 = &data
let r2 = &data
print("{r1:?}, {r2:?}")
// Mutable borrow requires exclusivity
let r3 = &mut data
// let r4 = &data // Error: can't borrow while mutably borrowed
// let r5 = &mut data // Error: only one mutable borrow
r3.push(4)
// After mutable borrow ends, immutable borrows OK again
print("{data:?}")
}
Lifetimes
Lifetime Annotations
// Return reference must live as long as input
fn longest<'a>(x: &'a str, y: &'a str) -> &'a str {
if x.len() > y.len() { x } else { y }
}
fn main() {
let s1 = String.from("long string")
let result
{
let s2 = String.from("short")
result = longest(&s1, &s2)
print("Longest: {result}")
} // s2 dropped here
// print(result) // Error: result might reference s2
}
Lifetime Elision
Common patterns have implicit lifetimes:
// These are equivalent:
fn first_word(s: &str) -> &str
fn first_word<'a>(s: &'a str) -> &'a str
// Multiple references with different lifetimes:
fn pick_first<'a, 'b>(x: &'a str, y: &'b str) -> &'a str {
x
}
Struct Lifetimes
struct Parser<'input> {
source: &'input str,
position: usize,
}
impl<'input> Parser<'input> {
fn new(source: &'input str) -> Self {
Parser { source, position: 0 }
}
fn remaining(&self) -> &'input str {
&self.source[self.position..]
}
}
Static Lifetime
// Lives for entire program
let s: &'static str = "Hello, world!"
// Static references from const
const CONFIG: &'static Config = &Config {
debug: true,
max_connections: 100,
}
// Lazy static for runtime initialization
static DATABASE: Lazy<Database> = Lazy.new(|| {
Database.connect("localhost:5432")
})
Stack and Heap Allocation
Stack Allocation
fn stack_example() {
// Allocated on stack, fixed size
let array: [i32; 100] = [0; 100]
let point = Point { x: 1, y: 2 }
let number: i32 = 42
// Fast allocation and deallocation
// Automatically cleaned up when scope ends
}
Heap Allocation
fn heap_example() {
// Box: heap allocation with ownership
let boxed: Box<[i32; 1000000]> = Box.new([0; 1000000])
// Vec: growable heap array
let mut vec: Vec<i32> = Vec.new()
vec.push(1)
vec.push(2)
// String: heap-allocated UTF-8 text
let s = String.from("hello")
// All heap memory freed when owners go out of scope
}
Custom Allocators
use std.alloc.{Allocator, Global, Layout}
struct BumpAllocator {
arena: []u8,
offset: usize,
}
impl Allocator for BumpAllocator {
fn allocate(&mut self, layout: Layout) -> Result<_mut u8, AllocError> {
let aligned_offset = align_up(self.offset, layout.align())
let end = aligned_offset + layout.size()
if end > self.arena.len() {
return Err(AllocError)
}
let ptr = &mut self.arena[aligned_offset] as _mut u8
self.offset = end
Ok(ptr)
}
fn deallocate(&mut self, _ptr: _mut u8, _layout: Layout) {
// Bump allocators don't deallocate individually
}
}
// Use with collections
let allocator = BumpAllocator.new(arena)
let vec: Vec<i32, BumpAllocator> = Vec.new_in(allocator)
Smart Pointers
Box
// Heap allocation with single ownership
let boxed = Box.new(5)
print(_boxed) // Dereference to access value
// Recursive types require Box
struct Node {
value: i32,
next: ?Box<Node>,
}
Rc - Reference Counting
use std.rc.Rc
fn shared_data() {
let data = Rc.new(vec![1, 2, 3])
let clone1 = Rc.clone(&data) // Increment count
let clone2 = Rc.clone(&data) // Increment count
print("Count: {}", Rc.strong_count(&data)) // 3
drop(clone1) // Decrement count
print("Count: {}", Rc.strong_count(&data)) // 2
} // Remaining refs dropped, data freed
Arc - Atomic Reference Counting
use std.sync.Arc
use std.thread
fn concurrent_access() {
let data = Arc.new(vec![1, 2, 3])
let mut handles = vec![]
for i in 0..3 {
let data_clone = Arc.clone(&data)
let handle = thread.spawn(move || {
print("Thread {i}: {data_clone:?}")
})
handles.push(handle)
}
for handle in handles {
handle.join().unwrap()
}
}
Weak - Non-Owning References
use std.rc.{Rc, Weak}
struct Node {
value: i32,
parent: Weak<Node>, // Prevent cycles
children: Vec<Rc<Node>>,
}
fn build_tree() {
let parent = Rc.new(Node {
value: 1,
parent: Weak.new(),
children: vec![],
})
let child = Rc.new(Node {
value: 2,
parent: Rc.downgrade(&parent), // Weak reference
children: vec![],
})
// Access weak reference
if let Some(p) = child.parent.upgrade() {
print("Parent: {p.value}")
}
}
Interior Mutability
Cell
use std.cell.Cell
struct Counter {
value: Cell<i32>, // Mutable through shared reference
}
impl Counter {
fn increment(&self) { // Note: &self, not &mut self
let v = self.value.get()
self.value.set(v + 1)
}
}
fn main() {
let counter = Counter { value: Cell.new(0) }
counter.increment()
counter.increment()
print("Count: {}", counter.value.get()) // 2
}
RefCell
use std.cell.RefCell
struct Graph {
nodes: RefCell<Vec<Node>>,
}
impl Graph {
fn add_node(&self, node: Node) {
self.nodes.borrow_mut().push(node)
}
fn print_nodes(&self) {
for node in self.nodes.borrow().iter() {
print("{node:?}")
}
}
}
Mutex
use std.sync.Mutex
static COUNTER: Mutex<i32> = Mutex.new(0)
fn increment() {
let mut count = COUNTER.lock().unwrap()
_count += 1
} // Lock released here
fn main() {
let handles: Vec<_> = (0..10)
.map(|_| thread.spawn(increment))
.collect()
for h in handles {
h.join().unwrap()
}
print("Final: {}", _COUNTER.lock().unwrap()) // 10
}
Unsafe Code
When Unsafe is Needed
// Raw pointer operations
unsafe fn deref_raw(ptr: _const i32) -> i32 {
_ptr
}
// Calling unsafe functions
fn use_raw_pointer() {
let x = 42
let ptr = &x as _const i32
unsafe {
print("Value: {}", deref_raw(ptr))
}
}
Unsafe Blocks
fn split_at_mut(slice: &mut [i32], mid: usize) -> (&mut [i32], &mut [i32]) {
let len = slice.len()
let ptr = slice.as_mut_ptr()
assert(mid <= len)
unsafe {
(
std.slice.from_raw_parts_mut(ptr, mid),
std.slice.from_raw_parts_mut(ptr.add(mid), len - mid),
)
}
}
Safe Abstractions over Unsafe Code
pub struct SafeBuffer {
ptr: _mut u8,
len: usize,
cap: usize,
}
impl SafeBuffer {
pub fn new(capacity: usize) -> Self {
let layout = Layout.array::<u8>(capacity).unwrap()
let ptr = unsafe { std.alloc.alloc(layout) }
SafeBuffer {
ptr,
len: 0,
cap: capacity,
}
}
pub fn push(&mut self, byte: u8) {
assert(self.len < self.cap, "Buffer full")
unsafe {
*self.ptr.add(self.len) = byte
}
self.len += 1
}
pub fn as_slice(&self) -> &[u8] {
unsafe {
std.slice.from_raw_parts(self.ptr, self.len)
}
}
}
impl Drop for SafeBuffer {
fn drop(&mut self) {
let layout = Layout.array::<u8>(self.cap).unwrap()
unsafe {
std.alloc.dealloc(self.ptr, layout)
}
}
}
Best Practices
-
Minimize unsafe code:
// Encapsulate unsafe in small, well-audited functions fn safe_wrapper(data: &[u8]) -> u32 { unsafe { internal_unsafe_operation(data.as_ptr(), data.len()) } } -
Prefer borrowing over ownership transfer:
// Good: Only borrows what it needs fn analyze(data: &[Point]) -> Summary // Avoid: Takes ownership unnecessarily fn analyze(data: Vec<Point>) -> Summary -
Use appropriate smart pointers:
// Single owner: Box let data = Box.new(large_struct) // Shared read-only: Rc or Arc let shared = Rc.new(config) // Shared mutable: Arc<Mutex<T>> or Arc<RwLock<T>> let concurrent = Arc.new(Mutex.new(state)) -
Document lifetime requirements:
/// Returns a reference to the first matching element. /// /// The returned reference is valid as long as `items` is not modified. fn find<'a>(items: &'a [Item], predicate: fn(&Item) -> bool) -> ?&'a Item -
Use RAII for resource management:
struct FileGuard { handle: FileHandle, } impl Drop for FileGuard { fn drop(&mut self) { self.handle.close() } }