2

Using the Piston image crate, I can write an image by feeding it a Vec<u8>, but my actual data is Vec<Rgb<u8>> (because that is a lot easier to deal with, and I want to grow it dynamically).

How can I convert Vec<Rgb<u8>> to Vec<u8>? Rgb<u8> is really [u8; 3]. Does this have to be an unsafe conversion?

Timmmm
  • 88,195
  • 71
  • 364
  • 509

2 Answers2

6

The answer depends on whether you are fine with copying the data. If copying is not an issue for you, you can do something like this:

let img: Vec<Rgb<u8>> = ...;
let buf: Vec<u8> = img.iter().flat_map(|rgb| rgb.data.iter()).cloned().collect();

If you want to perform the conversion without copying, though, we first need to make sure that your source and destination types actually have the same memory layout. Rust makes very few guarantees about the memory layout of structs. It currently does not even guarantee that a struct with a single member has the same memory layout as the member itself.

In this particular case, the Rust memory layout is not relevant though, since Rgb is defined as

#[repr(C)]
pub struct Rgb<T: Primitive> {
    pub data: [T; 3],
}

The #[repr(C)] attribute specifies that the memory layout of the struct should be the same as an equivalent C struct. The C memory layout is not fully specified in the C standard, but according to the unsafe code guidelines, there are some rules that hold for "most" platforms:

  • Field order is preserved.
  • The first field begins at offset 0.
  • Assuming the struct is not packed, each field's offset is aligned to the ABI-mandated alignment for that field's type, possibly creating unused padding bits.
  • The total size of the struct is rounded up to its overall alignment.

As pointed out in the comments, the C standard theoretically allows additional padding at the end of the struct. However, the Piston image library itself makes the assumption that a slice of channel data has the same memory layout as the Rgb struct, so if you are on a platform where this assumption does not hold, all bets are off anyway (and I couldnt' find any evidence that such a platform exists).

Rust does guarantee that arrays, slices and vectors are densely packed, and that structs and arrays have an alignment equal to the maximum alignment of their elements. Together with the assumption that the layout of Rgb is as specified by the rules I quotes above, this guarantees that Rgb<u8> is indeed laid out as three consecutive bytes in memory, and that Vec<Rgb<u8>> is indeed a consecutive, densely packed buffer of RGB values, so our conversion is safe. We still need to use unsafe code to write it:

let p = img.as_mut_ptr();
let len = img.len() * mem::size_of::<Rgb<u8>>();
let cap = img.capacity() * mem::size_of::<Rgb<u8>>();
mem::forget(img);
let buf: Vec<u8> = unsafe { Vec::from_raw_parts(p as *mut u8, len, cap) };

If you want to protect against the case that there is additional padding at the end of Rgb, you can check whether size_of::<Rgb<u8>>() is indeed 3. If it is, you can use the unsafe non-copying version, otherwise you have to use the first version above.

Sven Marnach
  • 574,206
  • 118
  • 941
  • 841
  • 3
    The entire paragraph explaining why the programmer believes this to be safe should be a comment directly next to the `unsafe` block. – Shepmaster Nov 08 '18 at 20:13
  • 1
    C doesn't guarantee anything about potential padding bytes at the end of such structure. A valid implementation can align `data` to 4 bytes for exemple. https://port70.net/~nsz/c/c11/n1570.html#6.7.2.1p14, https://port70.net/~nsz/c/c11/n1570.html#6.7.2.1p17, (C17 didn't change that). So `#[repr(C)]` doesn't change the problem. That very rare and unexpected but that exist on some embedded system. – Stargateur Nov 08 '18 at 23:37
  • @Stargateur When I wrote the answer, I looked up the rules of `repr(C)` in the [unsafe code guidelines](https://github.com/rust-rfcs/unsafe-code-guidelines/blob/master/reference/src/representation/structs-and-tuples.md#c-compatible-layout-repr-c) – specifically "The total size of the struct is rounded up to its overall alignment." However, I missed that this is only valid for "most" platforms. – Sven Marnach Nov 09 '18 at 09:10
  • @Stargateur While it's clear that the standard allows the additional padding, I'm not convinced that this can actually happen in practice. I tried to find evidence that there are targets that would actually add additional padding in this particular case, but couldn't find any. Do you have a link by any chance? – Sven Marnach Nov 09 '18 at 09:31
  • [Exemple](https://rextester.com/UOC10348) (Of course, this use an extension for simplicity) but the idea is that the memory layout of `struct data` is valid according to C ISO, Rust **and C** doesn't give a lot of guarantee about memory layout. C have few guarantee however but not the one you need to say "this is safe in any implementation". ABI C is implemented defined, there is **NOT** one standard ABI in C. ABI is not even quoted in the ISO. People can assume anything but that doesn't concern C. – Stargateur Nov 09 '18 at 10:24
  • As the Rust book say ["The order, size, and alignment of fields is exactly what you would **expect** from C or C++"](https://doc.rust-lang.org/nomicon/other-reprs.html#reprc) and as your link say "**Assuming** the struct is not packed, each field's offset is aligned[^aligned] to the ABI-mandated alignment for that field's type, possibly creating unused padding bits.". – Stargateur Nov 09 '18 at 10:24
  • `repr(C)` is by definition not safe because implementation are free to do a lot of thing for the same reason Rust didn't give guarantee to allow optimisation. `repr(C)` is a quick way to assume a lot of thing. I would advise to use `#[repr(align(N))]` or `#[repr(packed(N))]` instead, and to add static assert to unsure data layout is what we need. According to ISO C, and I don't really need an exemple, this is not safe. But yes, this will probably work in most case if not all. – Stargateur Nov 09 '18 at 10:24
  • @Stargateur Of course, if you explicily require a different alignment, you will get padding. And I never doubted that the standard theoretically allows this. What I doubt is that there is any target supported by LLVM that would actually add the padding without explicit annotation in practice. There is simply no reason to have a different alignment for a single-member struct than for its member, and the single member is guaranteed to have an alignment of 1. – Sven Marnach Nov 09 '18 at 10:36
  • "Assuming the struct is not packed" – this should read "assuming the struct isn't `repr(packed)`. It's not an assumption about ABI implementation details. I already addressed your other points in the updated answer. – Sven Marnach Nov 09 '18 at 10:36
  • "no reason to have a different alignment for a single-member struct than for its member, and the single member is guaranteed to have an alignment of 1.", I agree. In general, Rust or C don't have a reason to do something else. My point is `repr(C)` on this case doesn't change anything, Rust and C have almost the same guarantee on data layout. The only big difference is the field order and its imply that the first field begins at offset 0 but in this case there is only one field. Just saying be aware that C doesn't guarantee anymore than Rust in this case. – Stargateur Nov 09 '18 at 10:55
  • @Stargateur Fair enough, agreed that the guarantees of `repr(C)` aren't necessarily _stronger_, just _different_. I will reword the answer accordingly. – Sven Marnach Nov 09 '18 at 12:43
2

You choose the Vec<Rgb<u8>> storage format because it's easier to deal with and you want it to grow dynamically. But as you noticed, there's no guarantee of compatibility of its storage with a Vec<u8>, and no safe conversion.

Why not take the problem the other way and build a convenient facade for a Vec<u8> ?

type Rgb = [u8; 3];

#[derive(Debug)]
struct Img(Vec<u8>);

impl Img {
    fn new() -> Img {
        Img(Vec::new())
    }

    fn push(&mut self, rgb: &Rgb) {
        self.0.push(rgb[0]);
        self.0.push(rgb[1]);
        self.0.push(rgb[2]);
    }

    // other convenient methods
}

fn main() {
    let mut img = Img::new();
    let rgb : Rgb = [1, 2, 3];
    img.push(&rgb);
    img.push(&rgb);
    println!("{:?}", img);
}
Denys Séguret
  • 372,613
  • 87
  • 782
  • 758
  • For what it's worth, the image library basically provides such a façade in the form of the [`ImageBuffer` struct](https://docs.rs/image/0.20.1/image/struct.ImageBuffer.html), so the right answer is probably to simply use that. – Sven Marnach Nov 09 '18 at 10:20
  • I didn't use `ImageBuffer` because it has a fixed width & height and as I noted in the question I want to grow by `Vec<>` dynamically. – Timmmm Nov 09 '18 at 11:13