| Crates.io | movable-ref |
| lib.rs | movable-ref |
| version | 0.2.0 |
| created_at | 2025-06-22 20:15:10.668722+00 |
| updated_at | 2025-09-25 09:34:55.75026+00 |
| description | A tool for building movable self-referential types |
| homepage | |
| repository | https://github.com/engali94/movable-ref |
| max_upload_size | |
| id | 1721902 |
| size | 94,112 |
Movable self-referential data for Rust without pinning or runtime bookkeeping.
no_std projects and can be tuned to an 8-bit offset for tightly packed layouts.debug-guards feature adds runtime assertions while you are iterating; release builds stay lean.SelfRefVec, relocate across buffers, or compact in place).Install the crate:
[dependencies]
movable-ref = "0.1.0"
Wrap a field in SelfRefCell to keep the unsafe details contained:
use movable_ref::SelfRefCell;
struct Message {
body: SelfRefCell<String, i16>,
}
impl Message {
fn new(body: String) -> Self {
Self { body: SelfRefCell::new(body).expect("offset fits in i16") }
}
fn body(&self) -> &str {
self.body.get()
}
fn body_mut(&mut self) -> &mut String {
self.body.get_mut()
}
}
let mut msg = Message::new("move me".into());
assert_eq!(msg.body(), "move me");
let mut together = Vec::new();
together.push(msg); // moved to heap inside Vec
assert_eq!(together[0].body(), "move me");
For advanced scenarios you can work with SelfRef directly, but doing so means
reasoning about raw pointers. The recommended path is to use SelfRefCell
inside your types and expose regular safe methods, as shown above.
Rust normally stores raw pointers. Absolute addresses break the moment a struct moves. SelfRef<T, I> stores only the signed offset (I) between the pointer and the value it targets plus the metadata needed to rebuild fat pointers ([T], str, trait objects).
When the owner moves, the relative distance stays the same, so recomputing the pointer after the move just works. Choose I to match the size of your container: i8 covers ±127 bytes, i16 covers ±32 KiB, isize covers most use cases.
SelfRef uses unsafe internally, so it is important to follow the invariants:
SelfRef::set right after constructing the struct. The pointer stays unset otherwise.The crate provides layers to help you respect those rules:
SelfRefCell hides the unsafe parts and gives you safe try_get/try_get_mut accessors.debug-guards feature during development to assert that recorded absolute pointers still match after moves.Failure modes are documented in the crate root (src/lib.rs). Use the safe helpers whenever possible; unchecked calls are intended for tightly controlled internals.
The Criterion benchmarks live in benches/performance.rs.
| Operation | Direct | SelfRef | Pin<Box |
Rc<RefCell |
|---|---|---|---|---|
| Access (ps) | 329 | 331 | 365 | 429 |
| Create (ns) | 19 | 38 | 46 | 40 |
| Move (ns) | 49 | 58 | N/A | 50 (clone) |
Memory usage per pointer:
SelfRef<T, i8> : 1 byte (±127 bytes)
SelfRef<T, i16> : 2 bytes (±32 KiB)
SelfRef<T, i32> : 4 bytes (±2 GiB)
*const T : 8 bytes
Rc<RefCell<T>> : 8 bytes + heap allocation
cargo bench will rebuild these tables for your target.
| Task | Command |
|---|---|
| Lint | cargo clippy --all-targets -- -D warnings |
| Format | cargo fmt |
| Tests | cargo test |
| Miri | cargo +nightly miri test (see full matrix below) |
| AddressSanitizer | RUSTFLAGS="-Zsanitizer=address" ASAN_OPTIONS=detect_leaks=0 cargo +nightly test |
Miri matrix:
cargo +nightly miri setup
cargo +nightly miri test
cargo +nightly miri test --no-default-features
cargo +nightly miri test --features nightly
cargo +nightly miri test --features debug-guards
| Approach | Moves? | Memory | Runtime cost | Notes |
|---|---|---|---|---|
SelfRef |
✅ | 1–8 bytes | None | Works in no_std, flexible integer offsets |
Pin<Box<T>> |
❌ | 8+ bytes | Allocation | Stable but data cannot move |
Rc<RefCell<T>> |
➖ (clone) | 16+ bytes | Borrow checking + refcount | Allows interior mutability |
ouroboros |
✅ | varies | None | Macro DSL, less manual control |
MIT licensed. See LICENSE-MIT for details.