Eloff
Eloff

Reputation: 21656

Zero-copy convert slice of integers to slice of bytes

How to convert e.g. &[u64] to &[u8]? I contend that it's safe to do with this method (edited to make harder to misuse):

use num_traits::PrimInt;

/// Reinterpret a slice of T as a slice of bytes without copying.
/// Only use with simple copy types like integers, floats, bools, etc. Don't use with structs or enums.
pub fn get_bytes<T: PrimInt>(array: &[T]) -> &[u8] {
    // Add some checks to try and catch unsound use
    debug_assert!(size_of::<T>() <= 16);
    debug_assert!(size_of::<T>().is_power_of_two());
    debug_assert_eq!(size_of::<T>(), align_of::<T>());
    // Safety: &[u64] can be safely converted to &[u8]
    // (so why doesn't rust have a safe method for this?)
    unsafe { std::slice::from_raw_parts(array.as_ptr() as *const u8, array.len() * std::mem::size_of::<T>()) }
}

https://play.rust-lang.org/?version=stable&mode=debug&edition=2021&gist=8f30b03d44aadd6c720057337ac41236

That's how it would be written in C or C++. It's not safe to do the inverse conversion, because the alignment of the types differs. But casting down into a slice of bytes works, and it's why you can cast everything to char* in C.

Does Rust expose a safe method to do this? I'm currently just using the code above, but it'd be nice to get rid of one more unsafe block if I can. If not, why not? Is it unsafe for some reason I haven't considered?

Upvotes: 0

Views: 1045

Answers (2)

Chayim Friedman
Chayim Friedman

Reputation: 70860

The bytemuck crate is the crate for this kind of things. It has the cast_slice() function for that:

pub fn get_bytes<T: bytemuck::NoUninit>(array: &[T]) -> &[u8] {
    bytemuck::cast_slice(array)
}

However, your function is unsound: it allows calling with types with padding bytes, but reinterpreting padding bytes (essentially uninit) as u8 is UB. bytemuck::cast_slice() prohibits this by requiring the type to implement NoUninit. You can #[derive(NoUninit)] for your types, as long as they satisfy all requirements.

Upvotes: 3

cafce25
cafce25

Reputation: 27227

Your function is unsound:

#[derive(Debug, Clone, Copy)]
struct Thingy {
    a: u32,
    b: u16,
}
/// Reinterpret a slice of T as a slice of bytes without copying.
/// Only use with simple copy types like integers, floats, bools, etc. Don't use with structs or enums.
pub fn get_bytes<T: Copy>(array: &[T]) -> &[u8] {
    // Safety: &[u64] can be safely converted to &[u8]
    // (so why doesn't rust have a safe method for this?)
    unsafe { std::slice::from_raw_parts(array.as_ptr() as *const u8, array.len() * std::mem::size_of::<T>()) }
}

fn main() {
    let a = [Thingy {
        a: 0xca_fc_e2_50,
        b: 0x12_34,
    }, Thingy {
        a: 0x98_76_54_32,
        b: 0xca_fc,
    }];
    let b: &[u8] = get_bytes(&a);
    println!("{:?}", b);
    // [80, 226, 252, 202, 52, 18, 0, 0, 50, 84, 118, 152, 252, 202, 0, 0]
}

As you can see we can read the unwritten to 0 bytes between the Thingys

If the caller has to hold up some constraints the function should be unsafe itself.

Upvotes: 0

Related Questions