Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support operations on byte arrays #248

Open
3 tasks
joshlf opened this issue Aug 10, 2023 · 0 comments
Open
3 tasks

Support operations on byte arrays #248

joshlf opened this issue Aug 10, 2023 · 0 comments
Labels
blocked-on-generic_const_exprs https://github.com/rust-lang/rust/issues/76560 blocked-on-rust Blocked on a Rust feature landing or stabilizing compatibility-nonbreaking Changes that are (likely to be) non-breaking

Comments

@joshlf
Copy link
Member

joshlf commented Aug 10, 2023

Progress

  • Add ByteArray as private type
  • Use ByteArray internally to experiment
  • Add public uses of ByteArray

Motivation

We want to be able to support operations on byte arrays, especially [u8; size_of::<T>()] for some zerocopy-compatible type T. E.g.:

trait FromBytes {
    fn from_bytes(bytes: [u8; size_of::<Self>()]) -> Self
        where Self: Sized;
    fn ref_from_bytes(bytes: &[u8; size_of::<Self>()]) -> Self
        where Self: Sized + Unaligned + Immutable;
}

trait AsBytes {
    fn into_bytes(self) -> [u8; size_of::<Self>()]
        where Self: Sized;
    fn as_bytes(&self) -> &[u8; size_of::<Self>()]
        where Self: Sized + Immutable;
}

let t: T = transmute!([0u8; size_of::<T>()]);

A lot of code both inside zerocopy and in user code currently has no way to reason about size equality on byte slices, and so ends up re-doing bounds checks. Consider this code from Fuchsia's packet crate:

fn take_obj_front<T>(&mut self) -> Option<Ref<B, T>>
where
    T: Unaligned,
{
    let bytes = self.take_front(mem::size_of::<T>())?;
    // new_unaligned only returns None if there aren't enough bytes
    Some(Ref::new_unaligned(bytes).unwrap())
}

In this code, self.take_front returns Option<&[u8]>, and so the type system can't reason about the returned byte slice satisfying bytes.len() == size_of::<T>().

As part of #1315, we'd like to support the general pattern of reading or writing objects to byte slices or fancier buffer types. Given support for byte arrays, we could write something like:

trait Buffer {
    fn take_bytes_front<const N: usize>(&mut self) -> Option<&[u8; N]>;

    fn take_obj_front<T: Unaligned>(&mut self) -> Option<&T> {
        let bytes = self.take_bytes_front::<{size_of::<T>()}>()?;
        Some(transmute_ref!(bytes))
    }
}

Stabilize size_of::<T>()

One approach we could take to accomplish this would be to stabilize size_of::<T>() for use in a type in a generic context (a special case of generic_const_exprs.

Polyfill

Another approach - that we can implement on our own without being blocked on Rust - is to add a polyfill type like the following

/// An array of `size_of::<T>()` bytes.
///
/// Since the `generic_const_exprs` feature is unstable, it is not possible
/// to use the type `[u8; size_of::<T>()]` in a context in which `T` is
/// generic. `ByteArray<T>` fills this gap.
///
/// # Layout
///
/// `ByteArray<T>` has the same layout and bit validity as `[u8; size_of::<T>()]`.
#[derive(FromBytes, Unaligned)]
#[repr(transparent)]
pub struct ByteArray<T>(
    // INVARIANT: All of the bytes of this field are initialized.
    Unalign<MaybeUninit<T>>,
);

impl<T> ByteArray<T> {
    // Not necessarily public. This is where we write the unsafe code that understands
    // that `size_of::<T>() == size_of::<ByteArray<T>>()` since the type system itself
    // isn't smart enough to understand that (at least when `T` is generic).
    fn as_t(&self) -> Ptr<'_, T, (invariant::Shared, invariant::Any, invariant::Initialized)> {
        let ptr = Ptr::from_ref(self);
        // SAFETY: TODO
        let ptr = unsafe { ptr.cast_unsized(|b| b as *mut T) };
        // SAFETY: By safety invariant on `ByteArray`, `ByteArray<T>` has the same bit validity
        // as `[u8; _]`, which requires its bytes all be initialized.
        unsafe { ptr.assume_initialized() }
    }
}

Using this polyfill, we could write the Buffer trait from the motivation section as:

trait Buffer {
    fn take_bytes_front<T>(&mut self) -> Option<&ByteArray<T>>;

    fn take_obj_front<T: Unaligned>(&mut self) -> Option<&T> {
        let bytes = self.take_bytes_front::<T>()?;
        Some(bytes.as_t())
    }
}

If we use a type which supports unsized types (Unalign doesn't), we could even make this more powerful than [u8; size_of::<T>()]. For T: Sized, ByteArray<T> would have the same layout as T, but for T: !Sized, it would have a layout closer to [u8]. It's unclear how an unsized version of this could be constructed, though, since the fat pointer would need to know the number of trailing slice elements in T, not the number of bytes.

Interior mutability

TODO: Explain why Stacked Borrows would require T: Immutable, but why we may not need that bound in practice (ie, we can "disable" interior mutability).
d a &ByteArray<T> to the same memory if T contained an UnsafeCell.*

This was originally prototyped (though never merged) here.

TODO: Is it possible to support T: ?Sized? MaybeUninit<T> requires T: Sized, and in general, unions don't support unsized types, so it's not possible to just manually implement a standin MaybeUninit that does support T: ?Sized.

@joshlf joshlf added the compatibility-nonbreaking Changes that are (likely to be) non-breaking label Aug 12, 2023
@joshlf joshlf changed the title Add byte array (polyfill for [u8; size_of::<T>()]) Support operations on byte arrays Sep 18, 2023
@joshlf joshlf added blocked-on-rust Blocked on a Rust feature landing or stabilizing blocked-on-generic_const_exprs https://github.com/rust-lang/rust/issues/76560 labels Sep 18, 2023
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
blocked-on-generic_const_exprs https://github.com/rust-lang/rust/issues/76560 blocked-on-rust Blocked on a Rust feature landing or stabilizing compatibility-nonbreaking Changes that are (likely to be) non-breaking
Projects
None yet
Development

No branches or pull requests

1 participant