Search code examples
arraysrustcastingtraits

Rust How do you define a default method for similar types?


A very common pattern I have to deal with is, I am given some raw byte data. This data can represent an array of floats, 2D vectors, Matrices...

I know the data is compact and properly aligned. In C usually you would just do:

vec3 * ptr = (vec3*)data;

And start reading from it.

I am trying to create a view to this kind of data in rust to be able to read and write to the buffer as follows:


pub trait AccessView<T>
{
    fn access_view<'a>(
        offset : usize,
        length : usize,
        buffer : &'a Vec<u8>) -> &'a mut [T]
    {
        let bytes = &buffer[offset..(offset + length)];
        let ptr = bytes.as_ptr() as *mut T;
        return unsafe { std::slice::from_raw_parts_mut(ptr, length / size_of::<T>()) };
    }
}

And then calling it:

   let data: &[f32] =
        AccessView::<f32>::access_view(0, 32, &buffers[0]);

The idea is, I should be able to replace f32 with vec3 or mat4 and get a slice view into the underlying data.

This is crashing with:

   --> src/main.rs:341:9
    |
341 |         AccessView::<f32>::access_view(&accessors[0], &buffer_views, &buffers);
    |         ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ cannot infer type
    |
    = note: cannot satisfy `_: AccessView<f32>`

How could I use rust to achieve my goal? i.e. have a generic "template" for turning a set of raw bytes into a range checked slice view casted to some type.


Solution

  • There are two important problems I can identify:

    • You are using a trait incorrectly. You have to connect a trait to an actual type. If you want to call it the way you do, it needs to be a struct instead.
    • Soundness. You are creating a mutable reference from an immutable one through unsafe code. This is unsound and dangerous. By using unsafe, you tell the compiler that you manually verified that your code is sound, and the borrow checker should blindly believe you. Your code, however, is not sound.

    To part 1, @BlackBeans gave you a good answer already. I would still do it a little differently, though. I would directly imlement the trait for &[u8], so you can write data.access_view::<T>().

    To part 2, you at least need to make the input data &mut. Further, make sure they have the same lifetime, otherwise the compiler might not realize that they are actually connected.

    Also, don't use &Vec<u8> as an argument; in general, use slices (&[u8]) instead.

    Be aware that with all that said, there still is the problem of ENDIANESS. The behavior you will get will not be consistent between platforms. Use other means of conversion instead if that is something you require. Do not put this code in a generic library, at max use it for your own personal project.

    That all said, here is what I came up with:

    pub trait AccessView {
        fn access_view<'a, T>(&'a mut self, offset: usize, length: usize) -> &'a mut [T];
    }
    
    impl AccessView for [u8] {
        fn access_view<T>(&mut self, offset: usize, length: usize) -> &mut [T] {
            let bytes = &mut self[offset..(offset + length)];
            let ptr = bytes.as_ptr() as *mut T;
            return unsafe { std::slice::from_raw_parts_mut(ptr, length / ::std::mem::size_of::<T>()) };
        }
    }
    
    impl AccessView for Vec<u8> {
        fn access_view<T>(&mut self, offset: usize, length: usize) -> &mut [T] {
            self.as_mut_slice().access_view(offset, length)
        }
    }
    
    fn main() {
        let mut data: Vec<u8> = vec![1, 2, 3, 4, 5, 6, 7, 8];
        println!("{:?}", data);
    
        let float_view: &mut [f32] = data.access_view(2, 4);
        float_view[0] = 42.0;
        println!("{:?}", float_view);
    
        println!("{:?}", data);
    
        // println!("{:?}", float_view);   // Adding this would cause a compiler error, which shows that we implemented lifetimes correctly
    }
    
    [1, 2, 3, 4, 5, 6, 7, 8]
    [42.0]
    [1, 2, 0, 0, 40, 66, 7, 8]