Movable self-referential data for Rust without pinning or runtime bookkeeping.
- Store offsets instead of absolute pointers so your data can move freely across stack, heap, arenas, or embedded buffers.
- Works in
no_stdprojects and can be tuned to an 8-bit offset for tightly packed layouts. - Core API is explicit—helper macros are available but completely optional.
- Optional
debug-guardsfeature adds runtime assertions while you are iterating; release builds stay lean.
- You need a self-referential struct that must move (e.g., push onto a
Vec, relocate across buffers, or compact in place). - You are targeting embedded or real-time systems where every byte matters and heap allocation is either expensive or unavailable.
- You want predictable behaviour and explicit control instead of macro-generated code or hidden reference counting.
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:
- Initialise immediately: call
SelfRef::setright after constructing the struct. The pointer stays unset otherwise. - Keep layout stable: do not reorder or remove the referenced field after initialisation.
- Move the whole struct together: individual fields must not be detached from the container.
The crate provides layers to help you respect those rules:
SelfRefCellhides the unsafe parts and gives you safetry_get/try_get_mutaccessors.- Enable the
debug-guardsfeature 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.