2 unstable releases
0.2.0 | Dec 2, 2023 |
---|---|
0.1.0 | Dec 1, 2023 |
#53 in #owned
Used in owned-pin
6KB
96 lines
Owned Pin
This crate deals with data that is owned by some entity but (maybe) immovable in memory. It is inspired by R-value references in C++.
See the documentation for more information.
Examples
With Pin<P>
only, we cannot guarantee the move semantics of the value.
use core::pin::{Pin, pin};
use core::marker::PhantomPinned;
fn try_to_take_the_ownership_of<T>(pinned: Pin<&mut T>) {}
let mut value = pin!(PhantomPinned);
// The caller can reborrow the value...
try_to_take_the_ownership_of(value.as_mut());
// ... so the same pinned data can be used twice,
// thus unable to guarantee the move semantics.
try_to_take_the_ownership_of(value.as_mut());
In practice, this is because there is no such smart pointer that owns data by holding a unique reference to some other location on the stack.
Thus, we introduce the OnStack<T>
smart pointer and an alias of OPin<T>
= Pin<OnStack<T>>
, which both "own" and "pin" the data on the stack, enabling the example above to work as desired:
use owned_pin::{OPin, opin};
use core::marker::PhantomPinned;
fn take_the_ownership_of<T>(owned_and_pinned: OPin<T>) {}
let value = opin!(PhantomPinned);
// The `as_mut` method of `OPin` actually
// returns a `Pin<&mut T>`...
take_the_ownership_of(value);
// ... so the value itself cannot be used again.
// The line below causes rustc to emit `E0382`.
// take_the_ownership_of(value);
With data that implements Unpin
, we can even move it out from the wrapper safe and sound:
use owned_pin::{opin, unpin};
// Pins the value onto the stack.
let pinned = opin!(String::from("Hello!"));
// Retrieves back the data because `String` is `Unpin`.
let string: String = unpin(pinned);
Motivation: To Use R-value References of C++ in Rust
This crate is inspired by R-value references in C++, and OPin<T>
behaves more similarly to R-value references than the original move semantics in Rust and the Pin<&mut T>
wrapper.
Rust (original) v.s. C++
In the original move semantics of Rust, when a variable is moved to another scope, its storage place on the stack becomes immediately invalid.
fn take_the_ownership_of(s: String) {
let _ = s;
}
let a = String::from("Hello!");
take_the_ownership_of(a);
In the example above, when a
is moved into the function, the original storage place of a
on the original call stack, a.k.a. the storage place of pointer to the heap, length and capacity in the inner representation of String
, becomes invalid and inaccessible immediately.
However, the move semantics in C++, delay this invalidation until the moved reference (R-value reference) of the original value is assigned to a new variable:
void take_the_ownership_of(std::string&&);
std::string a("Hello!");
// The original storage place of `a` keeps
// containing the valid value...
take_the_ownership_of(std::move(a));
void take_the_ownership_of(std::string&& s) {
// ... until this actual assignment.
std::string moved(std::forward<std::string>(s));
// The usage of `s` before the assignment
// doesn't invalidate the original storage
// place.
}
In a conclusive comparison, the original move semantics of Rust copy the value on the stack eagerly on each move operation if not optimized, while the C++ version splits the operation of "Moving the ownership semantically" and "Copying data on the stack", which theoretically reduces the frequency of the latter operation.
owned-pin
v.s. C++
This crate's implementation aims to imitate the move semantics of C++:
use owned_pin::{OPin, opin, unpin};
let a = String::from("Hello!");
// The original storage place of `a` keeps
// containing the valid value...
take_the_ownership_of(opin!(a));
fn take_the_ownership_of(s: OPin<String>) {
// ... until this actual `unpin`ning.
let moved = unpin(s);
// The usage of `s` before the unpinning
// doesn't invalidate the original storage
// place.
}
This is achieved by the OnStack<T>
smart pointer, which is a wrapper around &mut ManuallyDrop<T>
. When an OnStack<T>
goes out of scope silently, it calls the unsafe ManuallyDrop::drop
function to release the referenced T
, while ManuallyDrop::take
is called when the inner value is intended to be moved out from the reference.
A short comparison between OPin<T>
and C++'s R-value reference:
OPin<T> in Rust |
T&& in C++ |
---|---|
let pinned = opin!(value); |
T&& rvalue = std::move(value); |
let another_pinned = pinned; |
T&& another_rvalue = std::forward<T>(rvalue); |
let unpinned = unpin(another_pinned); |
T assigned(std::forward<T>(another_rvalue)); |
The last row assumes T
is Unpin
in Rust, and the move constructor of T
is not deleted in C++.
In conclusion, this crate enables users to adopt the move semantics of C++ in Rust, which reduces the frequency of stack copying, while preserving the the full use of the compile-time borrow checker and drop checker of Rust.
License
MIT OR Apache-2.0
Dependencies
~245–700KB
~17K SLoC