29ffb42898
---ci--- project: atelier phase: 0 milestone: v0.4 status: complete requirements: covered: [ATELIER-92, ATELIER-93, ATELIER-94, ATELIER-95, ATELIER-96, ATELIER-97, ATELIER-98, ATELIER-99, ATELIER-100, ATELIER-101, ATELIER-102, ATELIER-103, ATELIER-104, ATELIER-105, ATELIER-106, ATELIER-107, ATELIER-108, ATELIER-109, ATELIER-110, ATELIER-111, ATELIER-112, ATELIER-113, ATELIER-114, ATELIER-115, ATELIER-116, ATELIER-117] partial: [] ---/ci---
136 lines
7.7 KiB
Markdown
136 lines
7.7 KiB
Markdown
# 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 from `domains/` docs; introduces no new P-rules (D-063).
|
|
> See `languages/rust.md` for 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;` — `x` is moved, not copied). The compiler rejects use-after-move.
|
|
- **`Copy` types (integers, `bool`, `&T`) duplicate on assignment; everything else moves.** A `struct` is `Copy` only if all fields are; opt in via `#[derive(Copy, Clone)]` only for small, cheap-to-copy types.
|
|
- **Pass by `&T` for read-only, `&mut T` for mutation:** a borrow does not transfer ownership; the caller retains the value after the callee returns.
|
|
- **Applies `concurrency/P1` (immutability by default):** `&T` is shared and immutable; `&mut T` is exclusive and mutable. The compiler enforces "one or many, never both" — aliasing XOR mutation, statically.
|
|
|
|
```rust
|
|
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 T` ties 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 str` is elided to `fn first(s: &str) -> &str` (one input → output lifetime). When ambiguous, name the lifetime.
|
|
- **`'static` is the longest lifetime (the whole program):** not "until I drop it." Use `'static` only for values that genuinely live forever (string literals, `const`s); leaking to `'static` to satisfy the checker is a bug.
|
|
- **`Ref<'a, T>` and `RefMut<'a, T>` from `RefCell` are runtime-checked borrows:** the borrow rules still apply, checked at runtime instead of compile time. A second `RefMut` panics.
|
|
- **Applies `concurrency/P3` (boundaries are locks):** `&mut T` is the compile-time lock — exclusive access is the boundary; no runtime mutex needed for single-threaded aliasing discipline.
|
|
|
|
```rust
|
|
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 type `T: Send` may be moved across thread boundaries. Most types are `Send`; `Rc<T>` is not (shared non-atomically refcounted).
|
|
- **`Sync`:** a type `T: Sync` may be shared (`&T`) across threads. `RefCell<T>` is `!Sync` (interior mutability without atomics); `Mutex<T>` is `Sync` (it synchronizes).
|
|
- **The compiler enforces `Send`/`Sync` at the thread-spawn boundary:** `std::thread::spawn(move || { ... })` requires the closure's captures to be `Send`.
|
|
- **Applies `concurrency/P1` and `concurrency/P3`:** `Send` is the move-across-boundary contract; `Sync` is 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.
|
|
|
|
```rust
|
|
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 frees `T`.
|
|
- **`Mutex<T>` for exclusive mutation across threads:** `lock()` blocks until exclusive; the guard `MutexGuard<T>` derefs to `&mut T` and releases on drop.
|
|
- **`RwLock<T>` for read-heavy, `Mutex<T>` for write-heavy:** RwLock allows multiple readers or one writer. For most cases, `Mutex` is simpler and faster; prefer it unless reads dominate by 10x+.
|
|
- **Hold the lock for the smallest scope:** `let g = m.lock().unwrap();` then drop `g` before I/O. RAII releases on scope exit; explicit `drop(g)` clarifies intent.
|
|
- **Applies `concurrency/P5` (lock minimization):** prefer message passing (`mpsc` channels) over locks. When a lock is needed, scope it minimally.
|
|
|
|
```rust
|
|
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>` for `Copy` types, `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; `RefCell` only single-threaded.
|
|
- **`UnsafeCell<T>` is the primitive; never use directly:** `Cell`, `RefCell`, `Mutex` are safe wrappers. Direct `UnsafeCell` is `unsafe` and 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 `&self` while mutating internally (e.g., a cache); document why.
|
|
|
|
```rust
|
|
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)
|
|
|
|
- **`Drop` runs when the owner goes out of scope:** no `defer`, no `finally`. A `MutexGuard` releases, a `File` closes, a `JoinHandle`... does not join (a dropped `JoinHandle` detaches).
|
|
- **`Drop` is deterministic:** it runs at scope exit, not GC time. This is why `Arc`'s refcount is precise and `Mutex` release is timely.
|
|
- **`ManuallyDrop<T>` to opt out:** for FFI types whose destructor you must call manually. Rare in application code; common in `unsafe` bindings.
|
|
- **`Drop` order: fields in declaration order, then the struct itself.** A field that another field's `Drop` depends on must be declared last.
|
|
|
|
```rust
|
|
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 via `Arc`/`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`/`Sync` tests and ownership-based property tests. |