How to implement `AsRef<[u8]>` for `[T]` where `T` is `#[repr(u8)]`?

I have a type that is uses #[repr(u8)] and I need to pass an array of it as [u8], but the conversion is not implemented:

#[repr(u8)]
enum A{
    Zero = 0,
    One = 1
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a :&[A] = &[A::One, A::Zero, A::One];
    println!("{}", takes_u8(a));
}

(Playground)

Errors:

   Compiling playground v0.0.1 (/playground)
error[E0277]: the trait bound `[A]: AsRef<[u8]>` is not satisfied
  --> src/main.rs:13:29
   |
13 |     println!("{}", takes_u8(a));
   |                    -------- ^ the trait `AsRef<[u8]>` is not implemented for `[A]`
   |                    |
   |                    required by a bound introduced by this call
   |
   = help: the following other types implement trait `AsRef<T>`:
             [T; N]
             [T]
   = note: required for `&[A]` to implement `AsRef<[u8]>`
note: required by a bound in `takes_u8`
  --> src/main.rs:7:16
   |
7  | fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
   |                ^^^^^^^^^^^ required by this bound in `takes_u8`

I was hoping that repr would implement AsRef variants automatically. I tried manually with no luck:

impl AsRef<u8> for A {
    fn as_ref(&self) -> &u8 {
        let a = *self as u8;
        &a
    }
}

// Is this possible?
impl AsRef<[u8]> for [A] { ...

Is there a way to do this in safe Rust? And would it be a bad idea for the compiler to implement these automatically, since they are essentially the same type?

There is no built-in way to do this in safe Rust. #[repr(u8)] does not cause a type to implement any traits, so there's nothing to base a generic implementation on.

There is the bytemuck crate for these sorts of conversions, however you absolutely must not use bytemuck in this case.

The problem is that while it might be valid to turn an A into a u8, bytemuck requires being able to go in the opposite direction, and that isn't valid.

If you want to do this, I believe you'll have to use unsafe Rust.

Edit: see kpreid's reply below.

That's likely what the "Safe transmute project" is for, but they don't seem to have had any activity in over 3 years.

3 Likes

Ahh, bummer! Thanks for the context and explanation though.

Here is how I ended up implementing this using unsafe Rust:

#[repr(u8)]
enum A {
    Zero = 0,
    One = 1
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a :&[A] = &[A::One, A::Zero, A::One];
    let data: &[u8];
    unsafe {
        data = std::mem::transmute(a);
    }
    let expect: &[u8] = &[1, 0, 1];
    assert_eq!(data, expect);
    println!("{}", takes_u8(data));
}

(Playground)

I would strongly recommend putting this in a function. Any time you have unsafe code, you want to push it into the tiniest possible corner to limit its potential for damage. I'd also recommend lots of tests, including on "obvious" things.

Here's some adjusted code on the playpen.

use std::mem;

#[repr(u8)]
enum A{
    Zero = 0,
    One = 1
}

#[test]
fn test_size() {
    assert_eq!(mem::size_of::<A>(), mem::size_of::<u8>());
}

impl A {
    fn slice_as_u8(slice: &[A]) -> &[u8] {
        // SAFETY: A and u8 must be same size (see `test_size`).
        unsafe {
            std::mem::transmute::<&[A], &[u8]>(slice)
        }
    }
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a: &[A] = &[A::One, A::Zero, A::One];
    let data = A::slice_as_u8(a);
    let expect: &[u8] = &[1, 0, 1];
    assert_eq!(data, expect);
    println!("{}", takes_u8(data));
}

I also fully spelled out the transmute call. Can never be too careful about making sure transmute is doing exactly what you expect it to be doing.

1 Like

Although do note that the transmute is wrong. The layout of pointers, and especially wide pointers, is not guaranteed. This transmute can break and cause UB any time. You should use slice::from_raw_parts() instead.

4 Likes

Another place you can look is the zerocopy crate, which has some ties to rustlang's safe transmute project.

In particular, you can derive AsBytes for your type A. Then, because of this blanket implementation on slices, you can just call as_bytes on your slice. Something like this:

use zerocopy::AsBytes;

#[repr(u8)]
#[derive(AsBytes)]
enum A {
    Zero = 0,
    One = 1
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a :&[A] = &[A::One, A::Zero, A::One];
    println!("{}", takes_u8(a.as_bytes()));
}

Tested this and verified it does what you want.

2 Likes

With @H2CO3's comment about slice::from_raw_parts and some hacky compile time assertions (does Rust not have something better?) my current version looks like this:

#[repr(u8)]
enum A{
    Zero = 0,
    One = 1,
    // Two(usize), // try uncommenting
}

const fn check(_: usize) {}

impl A {
    const TEST1: usize = std::mem::size_of::<A>() - std::mem::size_of::<u8>();
    const TEST2: usize = std::mem::size_of::<u8>() - std::mem::size_of::<A>();

    const fn slice_as_u8(a: &[A]) -> &[u8] {
        check(Self::TEST1);
        check(Self::TEST2);
        unsafe { std::slice::from_raw_parts(a.as_ptr() as *const u8, a.len()) }
    }
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a: &[A] = &[A::One, A::Zero, A::One];
    let data = A::slice_as_u8(a);
    assert_eq!(data, &[1, 0, 1]);
    println!("{}", takes_u8(data));
}

(Playground)

Output:

2

Errors:

   Compiling playground v0.0.1 (/playground)
    Finished dev [unoptimized + debuginfo] target(s) in 0.38s
     Running `target/debug/playground`

No, bytemuck includes the NoUninit trait and derive which is precisely what is needed here. This code compiles, and is sound (as it should be since there is no unsafe in it).

#[derive(Clone, Copy, bytemuck::NoUninit)]
#[repr(u8)]
enum A {
    Zero = 0,
    One = 1
}

fn takes_u8<T: AsRef<[u8]>>(a: T) -> usize {
    a.as_ref().iter().map(|x| *x as usize).sum()
}

fn main() {
    let a :&[A] = &[A::One, A::Zero, A::One];
    println!("{}", takes_u8(bytemuck::cast_slice(a)));
}
6 Likes

Thank you very much for the correction. I saw the following in bytemuck's documentation and interpreted it as there being only one functional trait to worry about:

All the functions here are guarded by the Pod trait, which is a sub-trait of the Zeroable trait.

Since A wasn't safe to implement Pod for, I assumed that was that. Just goes to show: never trust documentation :stuck_out_tongue:

This topic was automatically closed 90 days after the last reply. We invite you to open a new topic if you have further questions or comments.