I want to do this in a zero-copy manner -- to have the Vec just 'vanish' (without dropping), and to have the Vec<Cell> to it's memory.
Context: I have functions that return Vec. I don't think it makes sense to change them to return Vec<Cell>, but there are situations where I want the returned result changed into a Vec<Cell>, perferably without copying.
Given taht mem::transmute can take arbitrary types as inputs, how does it know that Vecs have a length field and to call it? Or does it not matter, and something like the following happens:
each Vec has a "fixed size component" (length, capacity, pointer to heap addr that contains actual Vec elements)
transmute never touches the heap elements; it just looks at the "Vec header" and pretends the bits forms a "Vec<Cell> header" -- so it never follows the pointer to the heap, and it never goes the heap, performing any op per element
If you don't mind nightly APIs, you can also get a &mut [f32] from the Vec, then construct a &Cell<[f32]> from it using from_mut, and then subsequently construct a &[Cell<f32>] from that using as_slice_of_cells. It's a bit convoluted, but it may do the job you want of it if you can use a slice of cells for the thing you want to do.
transmute doesn't understand anything about Vec or any other type. It merely cheats the type system (it's a syntax sugar for casting pointers).
However, it should work, because Cell<f32> and f32 have identical size and memory representation, so vec's length/capacity will be the same, and pointer to the data will have the same alignment.
Note that using a transmute here is officially UB right now, because Vec<f32> and Vec<Cell<f32>> are different repr(Rust) types which the compiler is thus free to layout in different ways.
The correct thing to do here is to use Vec::from_raw_parts like so
use std::cell::Cell;
use std::mem::ManuallyDrop;
pub fn cellify<T>(v: Vec<T>) -> Vec<Cell<T>> {
let v = ManuallyDrop::new(v);
unsafe { Vec::from_raw_parts(v.as_ptr() as _, v.len(), v.capacity()) }
}
(The pointer cast is sound because Cell and UnsafeCell are repr(transparent).)
@scottmcm : Thanks for your insightful post. Let us see if I can take it apart step by step.
Cell, UnsafeCell being repr(transparent) means that Cell and UnsafeCell are laid out exactly the same way as T. This is why it's okay for us to do a Vec.as_ptr() and interpret it as a pointer for Cell.
Vec, Vec<Cell> have type repr(Rust). This means Rust is hypothetically allowed to do specialized layout for one or both of them, thus resulting in the undefined behaviour. We get around this potential UB by constructinga new Vec directly via Vec::from_raw_parts
In our actual function, we do:
pub fn cellify<T>(v: Vec<T>) -> Vec<Cell<T>> {
let v2 = ManuallyDrop::new(v);
let v3 = unsafe { Vec::from_raw_parts(v2.as_ptr() as _, v2.len(), v2.capacity()) }
v3
}```
After the "let v2 = ..." line, "v" is no longer valid, as it has been moved. However, when v2 goes out of scope, there is no auto drop since it's a ManuallyDrop, and we do not have a ManuallyDrop::drop(v2) call.
We construct the v3, which if of type Vec<Cell<T>>. When v3 drops, all the objects in the vec will be freed. v3 gets returned at the end of the function.
This process is O(1) time. We construct a new Vec, but we don't do anything to the elements of the Vec.
Is this all correct?
I think for Vecs, it can be done because vectors can be made with raw parts, but what about HashMap or BTreemap where we need a conversion from , say, Hashmap<i32, Cell<T>> to Hashmap<i32, T>
We might need this kind of conversion when we want the Hashmap modified in a single-thread with internal-mutability and when we are done with that, convert it and then access it from multiple threads, which is safe.
but according to @ucottmcm transmuting directly causes UB.
So, this problem can be pretty much generalized to a container of cells situation, and I'm wondering if there is any best practice of it.
The method is marked as unsafe, but does have some safeguards and it should be fine in your case.
Edit: oops, for safety, I have constrained the types to be Copy, but Cell is not. What I really wanted was for there to be no Drop implementation, but I don't think I can do that with the compiler. So I moved the check into the method, and released 0.5.1 with this change.