12

I need to allocate a buffer for reading from a File, but this buffer must be aligned to the size of the cache line (64 bytes). I am looking for a function somewhat like this for Vec:

pub fn with_capacity_and_aligned(capacity: usize, alignment: u8) -> Vec<T>

which would give me the 64 byte alignment that I need. This obviously doesn't exist, but there might be some equivalences (i.e. "hacks") that I don't know about.

So, when I use this function (which will give me the desired alignment), I could write this code safely:

#[repr(C)]
struct Header {
    magic: u32,
    some_data1: u32,
    some_data2: u64,
}

let cache_line_size = 64; // bytes
let buffer: Vec<u8> = Vec::<u8>::with_capacity_and_alignment(some_size, cache_line_size);
match file.read_to_end(&mut buffer) {
    Ok(_) => {
        let header: Header = {
            // and since the buffer is aligned to 64 bytes, I wont get any SEGFAULT
            unsafe { transmute(buffer[0..(size_of::<Header>())]) }
        };
    }
}

and not get any panics because of alignment issues (like launching an instruction).

trent
  • 25,033
  • 7
  • 51
  • 90
Nulik
  • 6,748
  • 10
  • 60
  • 129
  • If you use `mmap` it will be page-aligned for you ... pass different flags and/or `madvise` depending on your specific need. – o11c Feb 12 '20 at 03:38

1 Answers1

13

You can enforce the alignment of a type to a certain size using #[repr(align(...))]. We also use repr(C) to ensure that this type has the same memory layout as an array of bytes.

You can then create a vector of the aligned type and transform it to a vector of appropriate type:

use std::mem;

#[repr(C, align(64))]
struct AlignToSixtyFour([u8; 64]);

unsafe fn aligned_vec(n_bytes: usize) -> Vec<u8> {
    // Lazy math to ensure we always have enough.
    let n_units = (n_bytes / mem::size_of::<AlignToSixtyFour>()) + 1;

    let mut aligned: Vec<AlignToSixtyFour> = Vec::with_capacity(n_units);

    let ptr = aligned.as_mut_ptr();
    let len_units = aligned.len();
    let cap_units = aligned.capacity();

    mem::forget(aligned);

    Vec::from_raw_parts(
        ptr as *mut u8,
        len_units * mem::size_of::<AlignToSixtyFour>(),
        cap_units * mem::size_of::<AlignToSixtyFour>(),
    )
}

There are no guarantees that the Vec<u8> will remain aligned if you reallocate the data. This means that you cannot reallocate so you will need to know how big to allocate up front.

The function is unsafe for the same reason. When the type is dropped, the memory must be back to its original allocation, but this function cannot control that.

Thanks to BurntSushi5 for corrections and additions.

See also:

Because of the limitations and unsafety above, another potential idea would be to allocate a big-enough buffer (maybe with some wiggle room), and then use align_to to get a properly aligned chunk. You could use the same AlignToSixtyFour type as above, and then convert the &[AlignToSixtyFour] into a &[u8] with similar logic.

This technique could be used to give out (optionally mutable) slices that are aligned. Since they are slices, you don't have to worry about the user reallocating or dropping them. This would allow you to wrap it up in a nicer type.


All that being said, I think that relying on alignment here is inappropriate for your actual goal of reading a struct from a file. Simply read the bytes (u32, u32, u64) and build the struct:

use byteorder::{LittleEndian, ReadBytesExt}; // 1.3.4
use std::{fs::File, io};

#[derive(Debug)]
struct Header {
    magic: u32,
    some_data1: u32,
    some_data2: u64,
}

impl Header {
    fn from_reader(mut reader: impl io::Read) -> Result<Self, Box<dyn std::error::Error>> {
        let magic = reader.read_u32::<LittleEndian>()?;
        let some_data1 = reader.read_u32::<LittleEndian>()?;
        let some_data2 = reader.read_u64::<LittleEndian>()?;

        Ok(Self {
            magic,
            some_data1,
            some_data2,
        })
    }
}

fn main() -> Result<(), Box<dyn std::error::Error>> {
    let mut f = File::open("/etc/hosts")?;

    let header = Header::from_reader(&mut f)?;

    println!("{:?}", header);

    Ok(())
}

See also:

Shepmaster
  • 388,571
  • 95
  • 1,107
  • 1,366
  • 2
    Good answer, but I think there are two issues. First, I believe `AlignToSixtyFour` needs to be `repr(transparent)` (or `repr(C)`), otherwise it's not guaranteed to share the same memory layout as a `[u8; 64]`. Second, I think your `aligned_vec` function should be marked `unsafe`. You hit the nail on the head about reallocs being dangerous, but freeing memory is also dangerous. It's UB to free an allocation with a different alignment than what it was allocated with. e.g., See https://doc.rust-lang.org/std/alloc/trait.GlobalAlloc.html#tymethod.dealloc – BurntSushi5 Feb 12 '20 at 11:47
  • 1
    @BurntSushi5 thank you! We can't use `repr(transparent)` in conjunction with `align`, so it'd have to be `C`. – Shepmaster Feb 12 '20 at 14:48