Skip to content

Commit

Permalink
[derive] Support TryFromBytes for structs
Browse files Browse the repository at this point in the history
TODO:
- Should DataExt::fields have a different shape? Extracting field names
  for enums doesn't really make sense
- Add SAFETY comments in emitted `is_bit_valid` impl
- Do we need to update the TryFromBytes doc comment at all?
- Lots and lots of tests

Makes progress on #5
  • Loading branch information
joshlf committed Sep 12, 2023
1 parent d868b3d commit c068ed5
Show file tree
Hide file tree
Showing 7 changed files with 162 additions and 47 deletions.
8 changes: 6 additions & 2 deletions src/byteorder.rs
Original file line number Diff line number Diff line change
Expand Up @@ -176,16 +176,20 @@ example of how it can be used for parsing UDP packets.
[`AsBytes`]: crate::AsBytes
[`Unaligned`]: crate::Unaligned"),
#[derive(Copy, Clone, Eq, PartialEq, Hash)]
#[cfg_attr(any(feature = "derive", test), derive(FromZeroes, FromBytes, AsBytes, Unaligned))]
#[cfg_attr(any(feature = "derive", test), derive(TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned))]
#[repr(transparent)]
pub struct $name<O>([u8; $bytes], PhantomData<O>);
}

impl_known_layout!(O => $name<O>);

safety_comment! {
/// SAFETY:
/// `$name<O>` is `repr(transparent)`, and so it has the same layout
/// as its only non-zero field, which is a `u8` array. `u8` arrays
/// are `FromZeroes`, `FromBytes`, `AsBytes`, and `Unaligned`.
/// are `TryFromBytes`, `FromZeroes`, `FromBytes`, `AsBytes`, and
/// `Unaligned`.
impl_or_verify!(O => TryFromBytes for $name<O>);
impl_or_verify!(O => FromZeroes for $name<O>);
impl_or_verify!(O => FromBytes for $name<O>);
impl_or_verify!(O => AsBytes for $name<O>);
Expand Down
12 changes: 12 additions & 0 deletions src/derive_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -66,6 +66,7 @@ macro_rules! union_has_padding {
#[cfg(test)]
mod tests {
use crate::util::testutil::*;
use crate::*;

#[test]
fn test_struct_has_padding() {
Expand Down Expand Up @@ -124,4 +125,15 @@ mod tests {
// anyway.
test!(#[repr(C)] #[repr(packed)] {a: u8, b: u64} => true);
}

#[test]
fn foo() {
#[derive(TryFromBytes)]
struct Foo {
f: u8,
b: bool,
}

impl_known_layout!(Foo);
}
}
29 changes: 23 additions & 6 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -1000,6 +1000,11 @@ pub unsafe trait FromBytes: FromZeroes {
/// [github-repo]: https://github.com/google/zerocopy
/// [`try_from_ref`]: TryFromBytes::try_from_ref
pub unsafe trait TryFromBytes: KnownLayout {
#[doc(hidden)]
fn only_derive_is_allowed_to_implement_this_trait()
where
Self: Sized;

/// Does a given memory range contain a valid instance of `Self`?
///
/// # Safety
Expand Down Expand Up @@ -3621,10 +3626,16 @@ mod tests {
//
// This is used to test the custom derives of our traits. The `[u8]` type
// gets a hand-rolled impl, so it doesn't exercise our custom derives.
#[derive(Debug, Eq, PartialEq, FromZeroes, FromBytes, AsBytes, Unaligned)]
#[derive(Debug, Eq, PartialEq, TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned)]
#[repr(transparent)]
struct Unsized([u8]);

// SAFETY: `Unsized` is a `#[repr(transparent)]` wrapper around `[u8]`.
unsafe_impl_known_layout!(
#[repr([u8])]
Unsized
);

impl Unsized {
fn from_mut_slice(slc: &mut [u8]) -> &mut Unsized {
// SAFETY: This *probably* sound - since the layouts of `[u8]` and
Expand Down Expand Up @@ -4564,14 +4575,16 @@ mod tests {
assert_eq!(too_many_bytes[0], 123);
}

#[derive(Debug, Eq, PartialEq, FromZeroes, FromBytes, AsBytes)]
#[derive(Debug, Eq, PartialEq, TryFromBytes, FromZeroes, FromBytes, AsBytes)]
#[repr(C)]
struct Foo {
a: u32,
b: Wrapping<u32>,
c: Option<NonZeroU32>,
}

impl_known_layout!(Foo);

let expected_bytes: Vec<u8> = if cfg!(target_endian = "little") {
vec![1, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0]
} else {
Expand All @@ -4593,12 +4606,14 @@ mod tests {

#[test]
fn test_array() {
#[derive(FromZeroes, FromBytes, AsBytes)]
#[derive(TryFromBytes, FromZeroes, FromBytes, AsBytes)]
#[repr(C)]
struct Foo {
a: [u16; 33],
}

impl_known_layout!(Foo);

let foo = Foo { a: [0xFFFF; 33] };
let expected = [0xFFu8; 66];
assert_eq!(foo.as_bytes(), &expected[..]);
Expand Down Expand Up @@ -4657,23 +4672,25 @@ mod tests {

#[test]
fn test_transparent_packed_generic_struct() {
#[derive(AsBytes, FromZeroes, FromBytes, Unaligned)]
#[derive(AsBytes, TryFromBytes, FromZeroes, FromBytes, Unaligned)]
#[repr(transparent)]
struct Foo<T> {
_t: T,
_phantom: PhantomData<()>,
}

impl_known_layout!(T => Foo<T>);
assert_impl_all!(Foo<u32>: FromZeroes, FromBytes, AsBytes);
assert_impl_all!(Foo<u8>: Unaligned);

#[derive(AsBytes, FromZeroes, FromBytes, Unaligned)]
#[derive(AsBytes, TryFromBytes, FromZeroes, FromBytes, Unaligned)]
#[repr(packed)]
struct Bar<T, U> {
_t: T,
_u: U,
}

impl_known_layout!(T, U => Bar<T, U>);
assert_impl_all!(Bar<u8, AU64>: FromZeroes, FromBytes, AsBytes, Unaligned);
}

Expand Down Expand Up @@ -4937,7 +4954,7 @@ mod tests {
assert_impls!(Wrapping<bool>: TryFromBytes, FromZeroes, AsBytes, Unaligned, !FromBytes);
assert_impls!(Wrapping<NotZerocopy>: !TryFromBytes, !FromZeroes, !FromBytes, !AsBytes, !Unaligned);

assert_impls!(Unalign<u8>: FromZeroes, FromBytes, AsBytes, Unaligned, !TryFromBytes);
assert_impls!(Unalign<u8>: TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned, TryFromBytes);
assert_impls!(Unalign<NotZerocopy>: Unaligned, !TryFromBytes, !FromZeroes, !FromBytes, !AsBytes);

assert_impls!([u8]: TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned);
Expand Down
22 changes: 16 additions & 6 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,6 +130,9 @@ macro_rules! unsafe_impl {
let $candidate = unsafe { &*(candidate.as_ptr() as *const $repr) };
$is_bit_valid
}

#[allow(clippy::missing_inline_in_public_items)]
fn only_derive_is_allowed_to_implement_this_trait() {}
};
(@method TryFromBytes ; |$candidate:ident: NonNull<$repr:ty>| $is_bit_valid:expr) => {
#[inline]
Expand All @@ -141,8 +144,15 @@ macro_rules! unsafe_impl {
let $candidate = unsafe { NonNull::new_unchecked(candidate.as_ptr() as *mut $repr) };
$is_bit_valid
}

#[allow(clippy::missing_inline_in_public_items)]
fn only_derive_is_allowed_to_implement_this_trait() {}
};
(@method TryFromBytes) => {
#[inline(always)] unsafe fn is_bit_valid(_: NonNull<Self>) -> bool { true }
#[allow(clippy::missing_inline_in_public_items)]
fn only_derive_is_allowed_to_implement_this_trait() {}
};
(@method TryFromBytes) => { #[inline(always)] unsafe fn is_bit_valid(_: NonNull<Self>) -> bool { true } };
(@method $trait:ident) => {
#[allow(clippy::missing_inline_in_public_items)]
fn only_derive_is_allowed_to_implement_this_trait() {}
Expand Down Expand Up @@ -253,17 +263,17 @@ macro_rules! impl_known_layout {
($(const $constvar:ident : $constty:ty, $tyvar:ident $(: ?$optbound:ident)? => $ty:ty),* $(,)?) => {
$(impl_known_layout!(@inner const $constvar: $constty, $tyvar $(: ?$optbound)? => $ty);)*
};
($($tyvar:ident $(: ?$optbound:ident)? => $ty:ty),* $(,)?) => {
$(impl_known_layout!(@inner , $tyvar $(: ?$optbound)? => $ty);)*
($($($tyvar:ident $(: ?$optbound:ident)?),* => $ty:ty),* $(,)?) => {
$(impl_known_layout!(@inner , $($tyvar $(: ?$optbound)?),* => $ty);)*
};
($($ty:ty),*) => { $(impl_known_layout!(@inner , => $ty);)* };
(@inner $(const $constvar:ident : $constty:ty)? , $($tyvar:ident $(: ?$optbound:ident)?)? => $ty:ty) => {
(@inner $(const $constvar:ident : $constty:ty)? , $($tyvar:ident $(: ?$optbound:ident)?),* => $ty:ty) => {
const _: () = {
use core::ptr::NonNull;

impl<$(const $constvar : $constty,)? $($tyvar $(: ?$optbound)?)?> sealed::KnownLayoutSealed for $ty {}
impl<$(const $constvar : $constty,)? $($tyvar $(: ?$optbound)?),*> sealed::KnownLayoutSealed for $ty {}
// SAFETY: Delegates safety to `DstLayout::for_type`.
unsafe impl<$(const $constvar : $constty,)? $($tyvar $(: ?$optbound)?)?> KnownLayout for $ty {
unsafe impl<$(const $constvar : $constty,)? $($tyvar $(: ?$optbound)?),*> KnownLayout for $ty {
const LAYOUT: DstLayout = DstLayout::for_type::<$ty>();

// SAFETY: `.cast` preserves address and provenance.
Expand Down
7 changes: 6 additions & 1 deletion src/wrappers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -54,10 +54,15 @@ use super::*;
// [3] https://github.com/google/zerocopy/issues/209
#[allow(missing_debug_implementations)]
#[derive(Default, Copy)]
#[cfg_attr(any(feature = "derive", test), derive(FromZeroes, FromBytes, AsBytes, Unaligned))]
#[cfg_attr(
any(feature = "derive", test),
derive(TryFromBytes, FromZeroes, FromBytes, AsBytes, Unaligned)
)]
#[repr(C, packed)]
pub struct Unalign<T>(T);

impl_known_layout!(T => Unalign<T>);

safety_comment! {
/// SAFETY:
/// - `Unalign<T>` is `repr(packed)`, so it is unaligned regardless of the
Expand Down
56 changes: 41 additions & 15 deletions zerocopy-derive/src/ext.rs
Original file line number Diff line number Diff line change
Expand Up @@ -2,48 +2,74 @@
// Use of this source code is governed by a BSD-style license that can be
// found in the LICENSE file.

use syn::{Data, DataEnum, DataStruct, DataUnion, Type};
use proc_macro2::TokenStream;
use quote::ToTokens;
use syn::{Data, DataEnum, DataStruct, DataUnion, Field, Index, Type};

pub trait DataExt {
/// Extract the types of all fields. For enums, extract the types of fields
/// from each variant.
fn field_types(&self) -> Vec<&Type>;
/// Extract the names and types of all fields. For enums, extract the names
/// and types of fields from each variant. For tuple structs, the names are
/// the indices used to index into the struct (ie, `0`, `1`, etc).
///
/// TODO: Extracting field names for enums doesn't really make sense. Types
/// makes sense because we don't care about where they live - we just care
/// about transitive ownership. But for field names, we'd only use them when
/// generating is_bit_valid, which cares about where they live.
fn fields(&self) -> Vec<(TokenStream, &Type)>;
}

impl DataExt for Data {
fn field_types(&self) -> Vec<&Type> {
fn fields(&self) -> Vec<(TokenStream, &Type)> {
match self {
Data::Struct(strc) => strc.field_types(),
Data::Enum(enm) => enm.field_types(),
Data::Union(un) => un.field_types(),
Data::Struct(strc) => strc.fields(),
Data::Enum(enm) => enm.fields(),
Data::Union(un) => un.fields(),
}
}
}

impl DataExt for DataStruct {
fn field_types(&self) -> Vec<&Type> {
self.fields.iter().map(|f| &f.ty).collect()
fn fields(&self) -> Vec<(TokenStream, &Type)> {
map_fields(&self.fields)
}
}

impl DataExt for DataEnum {
fn field_types(&self) -> Vec<&Type> {
self.variants.iter().flat_map(|var| &var.fields).map(|f| &f.ty).collect()
fn fields(&self) -> Vec<(TokenStream, &Type)> {
map_fields(self.variants.iter().flat_map(|var| &var.fields))
}
}

impl DataExt for DataUnion {
fn field_types(&self) -> Vec<&Type> {
self.fields.named.iter().map(|f| &f.ty).collect()
fn fields(&self) -> Vec<(TokenStream, &Type)> {
map_fields(&self.fields.named)
}
}

fn map_fields<'a>(
fields: impl 'a + IntoIterator<Item = &'a Field>,
) -> Vec<(TokenStream, &'a Type)> {
fields
.into_iter()
.enumerate()
.map(|(idx, f)| {
(
f.ident
.as_ref()
.map(ToTokens::to_token_stream)
.unwrap_or_else(|| Index::from(idx).to_token_stream()),
&f.ty,
)
})
.collect()
}

pub trait EnumExt {
fn is_c_like(&self) -> bool;
}

impl EnumExt for DataEnum {
fn is_c_like(&self) -> bool {
self.field_types().is_empty()
self.fields().is_empty()
}
}
Loading

0 comments on commit c068ed5

Please sign in to comment.