In unsafe code, is it correct to have several mutable references (not pointers) to the same array, as long as they are not used to write to the same indices?
I would like to yield several (distinct) mutable views of an underlying array, that I can modify from different threads.
If the disjoint parts are contiguous, this is trivial, by just calling split_at_mut
on the slice:
let mut v = [1, 2, 3, 4];
{
let (left, right) = v.split_at_mut(2);
left[0] = 5;
right[0] = 6;
}
assert!(v == [5, 2, 6, 4]);
But I also want to expose non-contiguous disjoint parts. For simplicity, let's say we want to retrieve a mutable "view" for even indices, and another mutable "view" for odd indices.
Contrary to split_at_mut()
, we could not retrieve two mutable references (we want a safe abstraction!), so we use two structure instances instead, exposing only mutable access to even (resp. odd) indices:
let data = &mut [0i32; 11];
let (mut even, mut odd) = split_fields(data);
// …
With some unsafe code, it is easy to get such a safe abstraction. Here is a possible implementation:
use std::marker::PhantomData;
struct SliceField<'a> {
ptr: *mut i32,
len: usize,
field: usize,
marker: PhantomData<&'a mut i32>,
}
impl SliceField<'_> {
fn inc(&mut self) {
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) += 1;
}
}
}
fn dec(&mut self) {
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) -= 1;
}
}
}
}
unsafe impl Send for SliceField<'_> {}
fn split_fields(array: &mut [i32]) -> (SliceField<'_>, SliceField<'_>) {
(
SliceField {
ptr: array.as_mut_ptr(),
len: array.len(),
field: 0,
marker: PhantomData,
},
SliceField {
ptr: array.as_mut_ptr(),
len: array.len(),
field: 1,
marker: PhantomData,
},
)
}
fn main() {
let data = &mut [0i32; 11];
{
let (mut even, mut odd) = split_fields(data);
rayon::join(|| even.inc(), || odd.dec());
}
// this prints [1, -1, 1, -1, 1, -1, 1, -1, 1, -1, 1]
println!("{:?}", data);
}
So far, so good.
However, accessing a raw pointer is far for convenient: contrary to slices, we cannot use operator []
or iterators.
unsafe {
for i in (self.field..self.len).step_by(2) {
*self.ptr.add(i) += 1;
}
}
The obvious idea is to convert locally the raw pointer to a slice in the unsafe implementation:
let slice = unsafe { slice::from_raw_parts_mut(self.ptr, self.len) };
Then we could, for example, rewrite our implementation in functional style:
slice.iter_mut().skip(self.field).step_by(2).for_each(|x| *x += 1);
For this sample, it may not worth it, but for more complex code, it could be far more convenient to use slices instead of raw pointers.
Is this correct?
This obviously violates the borrowing rules: two threads may simultaneously hold a mutable reference to the very same memory location. However, they may never write to the same indices.
Mutable reference aliasing is not listed as an unsafe superpower, but the list is not presented as exhaustive.
Is aliasing of mutable references correct
No, it is never correct to alias mutable references (mutable pointers are a more nuanced concept). That's breaking one of the principal rules of references.
None of the qualifications you've provided matter — you cannot have mutable aliasing of references. The code being inside of an unsafe
block makes no difference. Doing so is automatic and instant undefined behavior.
fn main() {
let mut x = [42, 84];
let x_raw = &mut x as *mut _;
let x_even: &mut [i32; 2] = unsafe { &mut *x_raw };
let x_odd: &mut [i32; 2] = unsafe { &mut *x_raw };
println!("{}, {}", x_even[0], x_odd[1]);
}
Miri states:
error[E0080]: constant evaluation error: Borrow being dereferenced (Uniq(1772)) does not exist on the stack
--> src/main.rs:8:24
|
8 | println!("{}, {}", x_even[0], x_odd[1]);
| ^^^^^^^^^ Borrow being dereferenced (Uniq(1772)) does not exist on the stack
|
= note: inside call to `main` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:34
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:53
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:297:40
= note: inside call to `std::panicking::try::do_call::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:293:5
= note: inside call to `std::panicking::try::<i32, [closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:388:9
= note: inside call to `std::panic::catch_unwind::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:25
= note: inside call to `std::rt::lang_start_internal` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:5
= note: inside call to `std::rt::lang_start::<()>`
While an UnsafeCell
can help you build safe abstractions, you still have to adhere to the rules of references. Replacing a type with UnsafeCell
will not make things magically work:
use std::cell::UnsafeCell;
fn main() {
let x = UnsafeCell::new([42, 84]);
let x_even: &mut [i32; 2] = unsafe { &mut *x.get() };
let x_odd: &mut [i32; 2] = unsafe { &mut *x.get() };
println!("{}, {}", x_even[0], x_odd[1]);
}
error[E0080]: constant evaluation error: Borrow being dereferenced (Uniq(1776)) does not exist on the stack
--> src/main.rs:9:24
|
9 | println!("{}, {}", x_even[0], x_odd[1]);
| ^^^^^^^^^ Borrow being dereferenced (Uniq(1776)) does not exist on the stack
|
= note: inside call to `main` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:34
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:53
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:297:40
= note: inside call to `std::panicking::try::do_call::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:293:5
= note: inside call to `std::panicking::try::<i32, [closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:388:9
= note: inside call to `std::panic::catch_unwind::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:25
= note: inside call to `std::rt::lang_start_internal` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:5
= note: inside call to `std::rt::lang_start::<()>`
UnsafeCell
's docs explicitly call this out:
A
&mut T
reference may be released to safe code provided neither other&mut T
nor&T
co-exist with it. A&mut T
must always be unique.
In fact, even if your slices don't start at the same point but they overlap somehow, that's also aliasing and undefined behavior:
fn main() {
let mut x = [0, 1, 2];
let x_raw = &mut x as *mut [i32];
let x_0: &mut [i32] = unsafe { &mut (*x_raw)[0..2] };
let x_1: &mut [i32] = unsafe { &mut (*x_raw)[1..3] };
if x_0 == x_1 {
println!("They are equal");
}
}
error[E0080]: constant evaluation error: Borrow being dereferenced (Uniq(1807)) does not exist on the stack
--> /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libcore/cmp.rs:1041:65
|
1041 | fn eq(&self, other: &&'b mut B) -> bool { PartialEq::eq(*self, *other) }
| ^^^^^ Borrow being dereferenced (Uniq(1807)) does not exist on the stack
|
note: inside call to `std::cmp::impls::<impl std::cmp::PartialEq<&'b mut B> for &'a mut A><[i32], [i32]>::eq` at src/main.rs:8:8
--> src/main.rs:8:8
|
8 | if x_0 == x_1 {
| ^^^^^^^^^^
= note: inside call to `main` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:34
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:53
= note: inside call to closure at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:297:40
= note: inside call to `std::panicking::try::do_call::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panicking.rs:293:5
= note: inside call to `std::panicking::try::<i32, [closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe]>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/panic.rs:388:9
= note: inside call to `std::panic::catch_unwind::<[closure@DefId(1/1:1900 ~ std[82ff]::rt[0]::lang_start_internal[0]::{{closure}}[0]) 0:&dyn std::ops::Fn() -> i32 + std::marker::Sync + std::panic::RefUnwindSafe], i32>` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:52:25
= note: inside call to `std::rt::lang_start_internal` at /root/.rustup/toolchains/nightly-x86_64-unknown-linux-gnu/lib/rustlib/src/rust/src/libstd/rt.rs:64:5
= note: inside call to `std::rt::lang_start::<()>`
The documentation for UnsafeCell
states:
The
UnsafeCell<T>
type is the only legal way to obtain aliasable data that is considered mutable.
[...]
The compiler makes optimizations based on the knowledge that&T
is not mutably aliased or mutated, and that&mut T
is unique.
So no, what you are trying to is not valid, unless you use an UnsafeCell
.
If you love us? You can donate to us via Paypal or buy me a coffee so we can maintain and grow! Thank you!
Donate Us With