The current situations is that StaticMutex
contains the implementation details (e.g. pthread_mutex_t
) and is either used in a static
(through its public methods taking &'static self
) or in Mutex<T>
, as a private Box<StaticMutex>
field.
This is done because StaticMutex
should never be moved in memory.
One might consider appropriate to add immovable types to Rust, as seen here.
But that is largely unfeasible, such types could never be constructed other than by struct literals (or enum variant constructors) and would thus require public fields - not really what you want for Mutex<T>
.
I believe I’ve found a middle ground, a rule that could accommodate most existing code, while removing the indirection in Mutex
and obsoleting StaticMutex
:
Such types can be freely moved as long as they haven’t been borrowed.
Borrowing observes the address, which is what needs to remain constant for each “object” in memory of that type.
If we use a trait with a default impl
and opt-out, it could look like this:
unsafe trait Relocate {}
impl Relocate for .. {}
impl<T> !Relocate for Mutex<T> {}
unsafe impl<'a, T> Relocate for &'a T {}
unsafe impl<'a, T> Relocate for &'a mut T {}
unsafe impl<T> Relocate for *const T {}
// This impl makes Box and Arc implement Relocate implicitly.
unsafe impl<T> Relocate for *mut T {}
struct MyStruct { data: Mutex<Vec<i32>> }
fn make() -> Arc<MyStruct> {
let mutex = Mutex::new(vec![1, 2, 3]);
// Calling mutex.lock() (or even just doing &mutex) at this point
// would borrow the mutex and prevent moving it later.
let my = MyStruct {
data: mutex
};
// Same here for &my, &my.mutex or my.mutex.lock().
Arc::new(my);
}
The real problem in designing this lies in the interaction with generics, e.g:
// In std::ptr:
fn read<T: Relocate>(x: *const T) -> T {...}
ptr::read
is used by mem::swap
which is used by mem::replace
which is what Option::take
does.
If we end up with T: Relocate
bounds just to use Option::<T>::take
, the fallout might be too much. I think we should at least try to devise some kind of bound deduction scheme.
One such scheme could be based on argument and return types: if nothing is known about them, assume they implement Relocate
:
impl<T> Option<T> {
// &T always implements Relocate even if T doesn't
fn as_ref(&self) -> Option<&T> {...}
// &mut T implements Relocate, but the returned T might not
fn take_unwrap(&mut self) -> T {...}
}
// &mut T implements Relocate, so T is not restricted
fn take_and_print<T: Debug>(x: &mut Option<T>) {
// But that means calling take_unwrap will error,
// as it requires Relocate
println!("{:?}", x.take_unwrap());
}
I am not certain how this scheme would be implemented in the compiler, but it has the smallest amount of annotation overhead I could think of.
If we can do this, maybe we should have a similar story for Sized
- where only localized uses of ptr::read
(directly or transitively) that are not reflected in, e.g. the return type, would require a bound.
As an interesting aside, this scheme means that T: ?Sized
would never be required, only T: Sized
(and that presumably only rarely).