4e433158cd
---ci--- project: atelier phase: 3 milestone: v0.4 status: complete phase_role: execution phase_tag: v0.3.3 requirements: covered: [ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105] partial: [] ---/ci---
7.7 KiB
7.7 KiB
Rust Ownership — Derived Application
Applies Atelier's domain principles to Rust's ownership model specifically. Rust's distinctive strength (Send/Sync, lifetimes, borrowing) earns a dedicated ownership doc rather than an
rs-types.md. Derives fromdomains/docs; introduces no new P-rules (D-063). Seelanguages/rust.mdfor the language first-principles stub.
Ownership and Move Semantics (Concurrency P1 Immutability by Default, C1 Correctness)
- Ownership is unique: at any time, exactly one owner holds a value. Assignment passes ownership (
let y = x;—xis moved, not copied). The compiler rejects use-after-move. Copytypes (integers,bool,&T) duplicate on assignment; everything else moves. AstructisCopyonly if all fields are; opt in via#[derive(Copy, Clone)]only for small, cheap-to-copy types.- Pass by
&Tfor read-only,&mut Tfor mutation: a borrow does not transfer ownership; the caller retains the value after the callee returns. - Applies
concurrency/P1(immutability by default):&Tis shared and immutable;&mut Tis exclusive and mutable. The compiler enforces "one or many, never both" — aliasing XOR mutation, statically.
let s = String::from("hello");
let t = s; // s moved into t
// println!("{}", s); // error: use of moved value
let n = 5;
let m = n; // i32 is Copy: n still usable
println!("{} {}", n, m);
Borrowing and Lifetimes (C1 Correctness, Data P7 Type Fidelity, Concurrency P3 Boundaries are Locks)
&'a Tties a borrow to a lifetime'a: the borrow cannot outlive the owner. Lifetimes are static — the compiler rejects dangling references.- Lifetime elision when unambiguous:
fn first<'a>(s: &'a str) -> &'a stris elided tofn first(s: &str) -> &str(one input → output lifetime). When ambiguous, name the lifetime. 'staticis the longest lifetime (the whole program): not "until I drop it." Use'staticonly for values that genuinely live forever (string literals,consts); leaking to'staticto satisfy the checker is a bug.Ref<'a, T>andRefMut<'a, T>fromRefCellare runtime-checked borrows: the borrow rules still apply, checked at runtime instead of compile time. A secondRefMutpanics.- Applies
concurrency/P3(boundaries are locks):&mut Tis the compile-time lock — exclusive access is the boundary; no runtime mutex needed for single-threaded aliasing discipline.
fn longest<'a>(a: &'a str, b: &'a str) -> &'a str {
if a.len() > b.len() { a } else { b } // borrow tied to both inputs
}
fn dangling() -> &str { // compile error: missing lifetime
let s = String::from("local");
&s // error: s drops at end of fn
}
Send and Sync (Concurrency P1 Immutability by Default, Concurrency P3 Boundaries are Locks, C1 Correctness)
Send: a typeT: Sendmay be moved across thread boundaries. Most types areSend;Rc<T>is not (shared non-atomically refcounted).Sync: a typeT: Syncmay be shared (&T) across threads.RefCell<T>is!Sync(interior mutability without atomics);Mutex<T>isSync(it synchronizes).- The compiler enforces
Send/Syncat the thread-spawn boundary:std::thread::spawn(move || { ... })requires the closure's captures to beSend. - Applies
concurrency/P1andconcurrency/P3:Sendis the move-across-boundary contract;Syncis the share-across-boundary contract. Data races are a compile error, not a runtime detector. This is Rust's distinctive strength over Go's race detector.
use std::rc::Rc;
use std::sync::Arc;
let rc = Rc::new(5);
// std::thread::spawn(move || { println!("{}", rc) }); // error: Rc is !Send
let arc = Arc::new(5);
std::thread::spawn(move || { println!("{}", arc) }); // ok: Arc<T> is Send+Sync
Shared Mutation: Arc, Mutex, RwLock (Concurrency P3 Boundaries are Locks, Concurrency P5 Lock Minimization)
Arc<T>for shared ownership across threads: atomic refcounted. Clone increases the count; the last drop freesT.Mutex<T>for exclusive mutation across threads:lock()blocks until exclusive; the guardMutexGuard<T>derefs to&mut Tand releases on drop.RwLock<T>for read-heavy,Mutex<T>for write-heavy: RwLock allows multiple readers or one writer. For most cases,Mutexis simpler and faster; prefer it unless reads dominate by 10x+.- Hold the lock for the smallest scope:
let g = m.lock().unwrap();then dropgbefore I/O. RAII releases on scope exit; explicitdrop(g)clarifies intent. - Applies
concurrency/P5(lock minimization): prefer message passing (mpscchannels) over locks. When a lock is needed, scope it minimally.
use std::sync::{Arc, Mutex};
use std::thread;
let counter = Arc::new(Mutex::new(0));
let mut handles = vec![];
for _ in 0..10 {
let c = Arc::clone(&counter);
handles.push(thread::spawn(move || {
let mut g = c.lock().unwrap();
*g += 1;
// g drops here, lock released
}));
}
for h in handles { h.join().unwrap(); }
println!("{}", *counter.lock().unwrap());
Interior Mutability (Concurrency P1 Immutability by Default, C1 Correctness)
Cell<T>forCopytypes,RefCell<T>for non-Copy: interior mutability moves the borrow check from compile time to runtime.RefCell::borrow_mut()panics on a second mutable borrow.Mutex<T>/RwLock<T>for thread-safe interior mutability: the runtime check is the lock, not a panic. Use these across threads;RefCellonly single-threaded.UnsafeCell<T>is the primitive; never use directly:Cell,RefCell,Mutexare safe wrappers. DirectUnsafeCellisunsafeand opts out of the aliasing guarantee.- Applies
concurrency/P1: interior mutability is the exception, not the default. Reach for it when an API must present&selfwhile mutating internally (e.g., a cache); document why.
use std::cell::RefCell;
struct Cache {
inner: RefCell<HashMap<String, User>>,
}
impl Cache {
fn get(&self, id: &str) -> Option<User> {
// &self (immutable) but mutates internally
self.inner.borrow_mut().entry(id.to_string()).or_insert_with(|| fetch()).clone()
}
}
Drop and RAII (C1 Correctness, Concurrency P3 Boundaries are Locks)
Dropruns when the owner goes out of scope: nodefer, nofinally. AMutexGuardreleases, aFilecloses, aJoinHandle... does not join (a droppedJoinHandledetaches).Dropis deterministic: it runs at scope exit, not GC time. This is whyArc's refcount is precise andMutexrelease is timely.ManuallyDrop<T>to opt out: for FFI types whose destructor you must call manually. Rare in application code; common inunsafebindings.Droporder: fields in declaration order, then the struct itself. A field that another field'sDropdepends on must be declared last.
struct Resource { name: String }
impl Drop for Resource {
fn drop(&mut self) {
println!("dropping {}", self.name); // runs at scope end
}
}
fn main() {
let _r = Resource { name: "x".into() };
// _r drops here, prints "dropping x"
}
Cross-References
domains/concurrency/first-principles.md— Concurrency P1 Immutability, P3 Boundaries are Locks, P5 Lock Minimization.domains/data/first-principles.md— Data P7 Type Fidelity (lifetimes are the type-level fidelity for references).domains/concurrency/patterns.md— message-passing vs lock patterns applied viaArc/Mutex/mpsc.domains/errors/patterns.md—?propagation relies on ownership transfer of the error.languages/rs-async.md— async borrows (Pin/&mut) build on the lifetime model here.languages/rs-testing.md—Send/Synctests and ownership-based property tests.