Interior mutable sliceable boxed slice

I would like to create an interior mutable boxed slice that can be sliced as &[T]. But I'm struggling to find a way to do it:

  • UnsafeCell<Box<[T]>> would require making multiple mutable references to the box to dereference it.
  • Box<UnsafeCell<[T]>> would work but I can't figure out how to actually create it.
  • Box<[UnsafeCell<T>]> is not slicable as &[T] (currently UnsafeCell is #[repr(transparent)] but that's not guaranteed).

Is this even possible?

Coercions.

let b: Box<UnsafeCell<[_]>> = Box::new(UnsafeCell::new([1, 2, 3, 4, 5]));

Would it be possible to do this for a dynamic number of elements?

No, UnsafeCell::new() requires the argument to be Sized.

By the way, what do you mean by this? It doesn't seem correct:

There's no need for mutable aliasing at all (playground):

let b: UnsafeCell<Box<[_]>> = UnsafeCell::new(vec![1, 2, 3, 4, 5].into_boxed_slice());

{
    let slice = unsafe { &(*b.get())[..] };
    println!("Immutable: {:?}", slice);
}

{
    let slice = unsafe { &mut (*b.get())[..] };
    slice[2] = 999;
    println!("Mutable: {:?}", slice);
}

That feels like the sort of breaking change that shouldn't happen in std, the same as removing #[repr(C)] would be.

1 Like

I need to access different elements in the slice mutably from multiple threads (it's checked using atomics separately). To avoid creating multiple mutable references to the box I would have to deference it immutably, but then I can't access the elements mutably without UB.

I suppose that it's unlikely to happen. But I would also like my code to work with Loom (whose UnsafeCell<T> is different in layout from T), and the std does explicitly say not to rely on it.

1 Like

Could you post some actual code? I don't see why this is special to UnsafeCell<Box<[T]>>, it seem to be exactly as easy or hard to do with a primitive array.

use std::cell::UnsafeCell;
use std::mem::MaybeUninit;
use std::sync::atomic::{self, AtomicUsize};

struct Storage<T> {
    slice: UnsafeCell<Box<[MaybeUninit<T>]>>,
    len: AtomicUsize,
}
impl<T> Storage<T> {
    fn push_2(&self, a: T, b: T) -> Option<&mut [T]> {
        let capacity = unsafe { &*self.slice.get() }.len();
        let old_len = self
            .len
            .fetch_update(
                atomic::Ordering::Acquire,
                atomic::Ordering::Relaxed,
                |len| {
                    if len + 2 > capacity {
                        None
                    } else {
                        Some(len + 2)
                    }
                },
            )
            .ok()?;

        let reserved = unsafe {
            // UB: Multiple mut references to the box are created!
            &mut (&mut *self.slice.get())[old_len..old_len + 2]
        };
        reserved[0] = MaybeUninit::new(a);
        reserved[1] = MaybeUninit::new(b);

        Some(unsafe { &mut *(reserved as *mut [MaybeUninit<T>] as *mut [T]) })
    }
}

I feel like this comment in the standard library does not refer to the #repr[transparent] being unstable at all. Instead it talks about the fact that the standard library is allowed to turn an immutable reference into a mutable pointer here that can then legally be turned into a mutable reference. Ordinarily turning an immutable reference into a mutable one is always UB. On the other hand, the fact that UnsafeCell is transparent is used multiple time in Cell, for example in from_mut or as_slice_of_cells, without any comment that this would be only possible with the special status of he standard library.

I do feel like the comment you linked there is misleading though in that it can be interpreted as the #repr[transparent] being unstable; someone should open an issue about it perhaps. Also I have no idea which casts relying on UnsafeCellʼs transparency are legal and which arenʼt. For example going from &T to &UnsafeCell<T> is probably UB. I don't know about &[UnsafeCell<T>] to &UnsafeCell<[T]>.

All of this is just my personal interpretation. It would be nice to get someone more knowledgeable to comment on these questions.

2 Likes

Well, Cell has an as_slice_of_cells() method, which turns a Cell<&[T]> into a &[Cell<T>]. If this is possible, then the inverse ought to be possible as well, and for UnsafeCell too – it would be weird if it wasn't guaranteed. The parallel with MaybeUninit can also be drawn, which IIRC can be transmuted exactly in the way that you described. These conversions should probably be allowed explicitly – if not in user code, at least (or, I would say, preferably) by means of implementing appropriate as_*() functions on these types in the standard library.

1 Like

So I opened an issue on the Rust repo about this, hopefully my questions can be answered and the documentation can be improved.

1 Like