I have a vector of u8
that I want to interpret as a vector of u32
. It is assumed that the bytes are in the right order. I don't want to allocate new memory and copy bytes after casting. I got the following to work:
use std::mem;
fn reinterpret(mut v: Vec<u8>) -> Option<Vec<u32>> {
let v_len = v.len();
v.shrink_to_fit();
if v_len % 4 != 0 {
None
} else {
let v_cap = v.capacity();
let v_ptr = v.as_mut_ptr();
println!("{:?}|{:?}|{:?}", v_len, v_cap, v_ptr);
let v_reinterpret = unsafe { Vec::from_raw_parts(v_ptr as *mut u32, v_len / 4, v_cap / 4) };
println!("{:?}|{:?}|{:?}",
v_reinterpret.len(),
v_reinterpret.capacity(),
v_reinterpret.as_ptr());
println!("{:?}", v_reinterpret);
println!("{:?}", v); // v is still alive, but is same as rebuilt
mem::forget(v);
Some(v_reinterpret)
}
}
fn main() {
let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
let test = reinterpret(v);
println!("{:?}", test);
}
However, there's an obvious problem here. From the shrink_to_fit
documentation:
It will drop down as close as possible to the length but the allocator may still inform the vector that there is space for a few more elements.
Does this mean that my capacity may still not be a multiple of the size of u32
after calling shrink_to_fit
? If in from_raw_parts
I set capacity to v_len/4
with v.capacity()
not an exact multiple of 4, do I leak those 1-3 bytes, or will they go back into the memory pool because of mem::forget
on v
?
Is there any other problem I am overlooking here?
I think moving v
into reinterpret guarantees that it's not accessible from that point on, so there's only one owner from the mem::forget(v)
call onwards.
A contiguous growable array type, written as Vec<T> , short for 'vector'.
To remove all elements from a vector in Rust, use . retain() method to keep all elements the do not match. let mut v = vec![ "A", "warm", "fall", "warm", "day"]; let elem = "warm"; // element to remove v.
Vec<u8> is like Box<[u8]> , except it additionally stores a "capacity" count, making it three machine words wide. Separately stored capacity allows for efficient resizing of the underlying array. It's the basis for String .
This is an old question, and it looks like it has a working solution in the comments. I've just written up what exactly goes wrong here, and some solutions that one might create/use in today's Rust.
Vec::from_raw_parts
is an unsafe function, and thus you must satisfy its invariants, or you invoke undefined behavior.
Quoting from the documentation for Vec::from_raw_parts
:
ptr
needs to have been previously allocated via String/Vec (at least, it's highly likely to be incorrect if it wasn't).T
needs to have the same size and alignment as what ptr was allocated with. (T having a less strict alignment is not sufficient, the alignment really needs to be equal to satsify the dealloc requirement that memory must be allocated and deallocated with the same layout.)- length needs to be less than or equal to capacity.
- capacity needs to be the capacity that the pointer was allocated with.
So, to answer your question, if capacity
is not equal to the capacity of the original vec, then you've broken this invariant. This gives you undefined behavior.
Note that the requirement isn't on size_of::<T>() * capacity
either, though, which brings us to the next topic.
Is there any other problem I am overlooking here?
Three things.
First, the function as written is disregarding another requirement of from_raw_parts
. Specifically, T
must have the same size as alignment as the original T
. u32
is four times as big as u8
, so this again breaks this requirement. Even if capacity*size
remains the same, size
isn't, and capacity
isn't. This function will never be sound as implemented.
Second, even if all of the above was valid, you've also ignored the alignment. u32
must be aligned to 4-byte boundaries, while a Vec<u8>
is only guaranteed to be aligned to a 1-byte boundary.
A comment on the OP mentions:
I think on x86_64, misalignment will have performance penalty
It's worth noting that while this may be true of machine language, it is not true for Rust. The rust reference explicitly states "A value of alignment n must only be stored at an address that is a multiple of n." This is a hard requirement.
Vec::from_raw_parts
seems like it's pretty strict, and that's for a reason. In Rust, the allocator API operates not only on allocation size, but on a Layout
, which is the combination of size, number of things, and alignment of individual elements. In C with memalloc
, all the allocator can rely upon is that the size is the same, and some minimum alignment. In Rust, though, it's allowed to rely on the entire Layout
, and invoke undefined behavior if not.
So in order to correctly deallocate the memory, Vec
needs to know the exact type that it was allocated with. By converting a Vec<u32>
into Vec<u8>
, it no longer knows this information, and so it can no longer properly deallocate this memory.
Vec::from_raw_parts
's strictness comes from the fact that it needs to deallocate the memory. If we create a borrowing slice, &[u32]
instead, we no longer need to deal with it! There is no capacity when turning a &[u8]
into &[u32]
, so we should be all good, right?
Well, almost. You still have to deal with alignment. Primitives are generally aligned to their size, so a [u8]
is only guaranteed to be aligned to 1-byte boundaries, while [u32]
must be aligned to a 4-byte boundary.
If you want to chance it, though, and create a [u32]
if possible, there's a function for that - <[T]>::align_to
:
pub unsafe fn align_to<U>(&self) -> (&[T], &[U], &[T])
This will trim of any starting and ending misaligned values, and then give you a slice in the middle of your new type. It's unsafe, but the only invariant you need to satisfy is that the elements in the middle slice are valid.
It's sound to reinterpret 4 u8
values as a u32
value, so we're good.
Putting it all together, a sound version of the original function would look like this. This operates on borrowed rather than owned values, but given that reinterpreting an owned Vec
is instant-undefined-behavior in any case, I think it's safe to say this is the closest sound function:
use std::mem;
fn reinterpret(v: &[u8]) -> Option<&[u32]> {
let (trimmed_front, u32s, trimmed_back) = unsafe { v.align_to::<u32>() };
if trimmed_front.is_empty() && trimmed_back.is_empty() {
Some(u32s)
} else {
// either alignment % 4 != 0 or len % 4 != 0, so we can't do this op
None
}
}
fn main() {
let mut v: Vec<u8> = vec![1, 1, 1, 1, 1, 1, 1, 1];
let test = reinterpret(&v);
println!("{:?}", test);
}
As a note, this could also be done with std::slice::from_raw_parts
rather than align_to
. However, that requires manually dealing with the alignment, and all it really gives is more things we need to ensure we're doing right. Well, that and compatibility with older compilers - align_to
was introduced in 2018 in Rust 1.30.0, and wouldn't have existed when this question was asked.
If you do need a Vec<u32>
for long term data storage, I think the best option is to just allocate new memory. The old memory is allocated for u8
s anyways, and wouldn't work.
This can be made fairly simple with some functional programming:
fn reinterpret(v: &[u8]) -> Option<Vec<u32>> {
let v_len = v.len();
if v_len % 4 != 0 {
None
} else {
let result = v
.chunks_exact(4)
.map(|chunk: &[u8]| -> u32 {
let chunk: [u8; 4] = chunk.try_into().unwrap();
let value = u32::from_ne_bytes(chunk);
value
})
.collect();
Some(result)
}
}
First, we use <[T]>::chunks_exact
to iterate over chunks of 4
u8
s. Next, try_into
to convert from &[u8]
to [u8; 4]
. The &[u8]
is guaranteed to be length 4, so this never fails.
We use u32::from_ne_bytes
to convert the bytes into a u32
using native endianness. If interacting with a network protocol, or on-disk serialization, then using from_be_bytes
or from_le_bytes
may be preferable. And finally, we collect
to turn our result back into a Vec<u32>
.
As a last note, a truly general solution might use both of these techniques. If we change the return type to Cow<'_, [u32]>
, we could return aligned, borrowed data if it works, and allocate a new array if it doesn't! Not quite the best of both worlds, but close.
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