Attribution: This article was based on content by @birdculture on hackernews.
Original: https://blog.polybdenum.com/2024/06/07/the-inconceivable-types-of-rust-how-to-make-self-borrows-safe.html
Introduction
Self-borrows — when an object returns references to its own fields or borrows from self across calls — are one of Rust’s most subtle design points. You want the ergonomics of referencing internal buffers or creating self-referential structures, but you must preserve Rust’s guarantees about ownership, aliasing, and moves. In this tutorial you will learn safe, practical patterns to express self-borrows in Rust, when to require Pin, and when you must fall back to tested unsafe abstractions or existing crates.
Background: Rust enforces memory safety with ownership, borrowing, and lifetimes; the borrow checker ensures references don’t outlive their data.
Credit: This tutorial builds on practical discussions such as birdculture (2024) and Rust core work on lifetimes and pinning (Klabnik & Nichols, 2019; Matsakis, 2018).
Key Takeaways
- Prefer API designs that avoid returning long-lived borrows of
self; return owned or short-lived values instead. - Use
RefCell/Rcfor single-threaded interior mutability andPinto prevent moves when you need stable addresses. - For robust self-referential data, prefer well-tested crates (e.g.,
ouroboros/self_cell) or encapsulated unsafe code with strong invariants.
Estimated total time: ~60–90 minutes.
Prerequisites
You will need:
- Rust toolchain (rustc and cargo) installed and up-to-date (Rust 1.60+ recommended).
- Familiarity with Rust ownership, borrowing, and lifetimes.
- Basic knowledge of
Box,Rc,Arc,RefCell, andMutex. - Optional: experience with async/await and
Pin.
Estimated time: 5 minutes.
Setup/Installation
- Create a new cargo project:
cargo new self_borrow_tutorial
- Add dependencies for examples if needed:
- In
Cargo.tomlyou may addouroborosorself_cellfor advanced patterns.
- In
Estimated time: 5–10 minutes.
Expected result: A new Rust project scaffolded and ready for code examples.
Step-by-step Instructions
We’ll go through numbered, incremental approaches from safest to more advanced.
1) Prefer owned or short-lived borrows (API design)
Goal: Avoid leaking borrows of self.
Code example 1 — return owned value instead of &self:
|
|
Comments: cloning moves ownership to the caller and avoids lifetime issues.
Estimated time: 5–10 minutes. Expected output: No borrow-checker errors; caller owns the value.
2) Interior mutability for single-threaded cases (RefCell + Rc)
Goal: Temporarily borrow internal fields without requiring &mut self on the API.
Code example 2 — using Rc<RefCell<T>>:
|
|
Comments: RefCell enforces borrow rules at runtime; avoid returning RefMut across await points.
Estimated time: 10–15 minutes. Expected output: Code compiles and runtime panics if borrow rules are violated.
Background:
RefCellprovides interior mutability in single-threaded contexts via runtime checks.
3) Pinning to prevent moves for stable addresses
Goal: Ensure a value’s memory address won’t change while you hold a reference into it.
Code example 3 — simple Pin<Box<T>> usage and projection:
|
|
Comments: requiring Pin<&mut Self> signals to callers that the object must not be moved.
Estimated time: 15–20 minutes.
Expected output: Compiles if called from a pinned value (e.g., Pin::new(&mut boxed)).
Citation: Pin semantics and guidance are discussed in Turon (2019).
4) Use well-tested crates for self-referential structs
Goal: Avoid writing unsafe self-referential logic; use a crate that encapsulates invariants.
Code example 4 — using ouroboros-style pattern (conceptual):
|
|
Comments: crates like ouroboros and self_cell create safe constructors that return types with internal references that remain valid.
Estimated time: 20–30 minutes to read docs and integrate. Expected output: A self-referential struct that you can use without manual unsafe code.
Citation: Practical solutions for self-referential data are available in community crates (ouroboros, self_cell).
5) Localized unsafe with UnsafeCell when necessary
Goal: Use UnsafeCell only when you must and wrap it in a safe API with documented invariants.
Code example 5 — controlled unsafe pattern:
|
|
Comments: You must prove that borrow_mut cannot be used concurrently in a way that violates aliasing; prefer using RefCell/locks or pinning.
Estimated time: 20–45 minutes to implement correct invariants and tests. Expected output: Compiles, but requires careful review and tests; misuse can produce undefined behavior.
Citation: Unsafe interior mutability is the basis of many safe abstractions in Rust (Klabnik & Nichols, 2019).
6) Async/await and cross-boundary borrows
Goal: Avoid borrowing &self across await points unless the borrow is 'static or pinned appropriately.
Practical rule: Do not return &self from an async fn across an .await. Instead, clone or move the necessary data into the future using Arc or owned values.
Code example 6 — move data into the async block:
|
|
Estimated time: 10–15 minutes. Expected output: No borrow errors crossing await points.
Citation: NLL and async interactions are part of ongoing borrow-checker evolution (Matsakis, 2018).
Common Issues & Troubleshooting
-
Borrow-checker rejects returning
&selfthat outlives a method:- Fix: return owned values, use
Pin, or redesign the API.
- Fix: return owned values, use
-
Panics with
RefCellat runtime (already borrowed):- Fix: restructure code to avoid nested borrows or use
Ref/RefMutproperly.
- Fix: restructure code to avoid nested borrows or use
-
Use-after-move when storing self-references:
- Fix: require
Pinto prevent moves or avoid self-referential storage.
- Fix: require
-
Unsafe code compiles but causes UB:
- Fix: add tests, assertions, and code comments documenting invariants; prefer safe wrapper crates.
Estimated time for debugging common issues: 10–30 minutes per issue depending on complexity.
Next Steps / Further Learning
- Read The Rust Programming Language (Klabnik & Nichols, 2019) for ownership fundamentals.
- Explore Pin and interior mutability posts by Rust core contributors (Turon, 2019; Matsakis, 2018).
- Evaluate crates:
ouroboros,self_cell, andpin-projectfor safe abstractions. - When writing unsafe wrappers, add fuzz/ASAN testing and clear documentation of invariants.
Estimated time: ongoing; 1–3 hours to read and experiment.
Additional Resources
- Klabnik & Nichols (2019). The Rust Programming Language.
- Matsakis (2018). Work on non-lexical lifetimes and borrow checker improvements.
- birdculture (2024). “The inconceivable types of Rust: How to make self-borrows safe” — practical perspectives and examples.
ouroboros,self_cell,pin-projectcrates for real-world patterns.
Citations:
- Klabnik & Nichols (2019)
- Matsakis (2018)
- Turon (2019)
- birdculture (2024)
Estimated time to follow resources: variable.
Common Pitfalls (summary)
- Returning references to fields that can be moved.
- Borrowing across
.awaitboundaries. - Using
UnsafeCellwithout a documented invariant. - Forgetting
Pinwhen you need a stable address.
Expected overall outcome: After completing these steps, you will be able to choose an appropriate pattern for self-borrows: prefer redesigns and owned returns for ergonomics, use RefCell/Rc or Arc+locks for interior mutability, require Pin when addresses must be stable, and rely on established crates or well-tested unsafe wrappers for complex self-referential structures.
Further learning will deepen your ability to balance safety, ergonomics, and performance in Rust APIs that need to expose internal references.
References
-
The inconceivable types of Rust: How to make self-borrows safe (2024) — @birdculture on hackernews