Skip to content

Commit

Permalink
[derive] Implement a IntoBytes-based Hash derive
Browse files Browse the repository at this point in the history
The standard library's derive for `Hash` generates a recursive descent
into the fields of the type it is applied to. This commit adds a
`ByteHash` derive that generates an optimized, byte-oriented `Hash`
implementation for types that implement `IntoBytes`. Instead of a
recursive descent, the generated implementation makes a single call
to `Hasher::write()` in both `Hash::hash()` and `Hash::hash_slice()`,
feeding the hasher the bytes of the type or slice all at once.

Resolves #2075
  • Loading branch information
max-heller committed Dec 23, 2024
1 parent 2c8ef74 commit 1358fd7
Show file tree
Hide file tree
Showing 5 changed files with 136 additions and 1 deletion.
16 changes: 16 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -5432,6 +5432,22 @@ pub unsafe trait Unaligned {
Self: Sized;
}

/// Derives an optimized implementation of [`Hash`] for types that implement
/// [`IntoBytes`] and [`Immutable`].
///
/// The standard library's derive for `Hash` generates a recursive descent
/// into the fields of the type it is applied to. Instead, the implementation
/// derived by this macro makes a single call to [`Hasher::write()`] for both
/// [`Hash::hash()`] and [`Hash::hash_slice()`], feeding the hasher the bytes
/// of the type or slice all at once.
///
/// [`Hash`]: core::hash::Hash
/// [`Hash::hash()`]: core::hash::Hash::hash()
/// [`Hash::hash_slice()`]: core::hash::Hash::hash_slice()
#[cfg(any(feature = "derive", test))]
#[cfg_attr(doc_cfg, doc(cfg(feature = "derive")))]
pub use zerocopy_derive::ByteHash;

#[cfg(feature = "alloc")]
#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))]
#[cfg(zerocopy_panic_in_const_and_vec_try_reserve_1_57_0)]
Expand Down
42 changes: 42 additions & 0 deletions zerocopy-derive/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -107,6 +107,7 @@ derive!(FromZeros => derive_from_zeros => derive_from_zeros_inner);
derive!(FromBytes => derive_from_bytes => derive_from_bytes_inner);
derive!(IntoBytes => derive_into_bytes => derive_into_bytes_inner);
derive!(Unaligned => derive_unaligned => derive_unaligned_inner);
derive!(ByteHash => derive_hash => derive_hash_inner);

/// Deprecated: prefer [`FromZeros`] instead.
#[deprecated(since = "0.8.0", note = "`FromZeroes` was renamed to `FromZeros`")]
Expand Down Expand Up @@ -528,6 +529,45 @@ fn derive_unaligned_inner(ast: &DeriveInput, _top_level: Trait) -> Result<TokenS
}
}

fn derive_hash_inner(ast: &DeriveInput, _top_level: Trait) -> Result<TokenStream, Error> {
let type_ident = &ast.ident;
let (impl_generics, ty_generics, where_clause) = ast.generics.split_for_impl();
let where_predicates = where_clause.map(|clause| &clause.predicates);
Ok(quote! {
// TODO(#553): Add a test that generates a warning when
// `#[allow(deprecated)]` isn't present.
#[allow(deprecated)]
// While there are not currently any warnings that this suppresses (that
// we're aware of), it's good future-proofing hygiene.
#[automatically_derived]
impl #impl_generics ::zerocopy::util::macro_util::core_reexport::hash::Hash for #type_ident #ty_generics
where
Self: ::zerocopy::IntoBytes + ::zerocopy::Immutable,
#where_predicates
{
fn hash<H>(&self, state: &mut H)
where
H: ::zerocopy::util::macro_util::core_reexport::hash::Hasher,
{
::zerocopy::util::macro_util::core_reexport::hash::Hasher::write(
state,
::zerocopy::IntoBytes::as_bytes(self)
)
}

fn hash_slice<H>(data: &[Self], state: &mut H)
where
H: ::zerocopy::util::macro_util::core_reexport::hash::Hasher,
{
::zerocopy::util::macro_util::core_reexport::hash::Hasher::write(
state,
::zerocopy::IntoBytes::as_bytes(data)
)
}
}
})
}

/// A struct is `TryFromBytes` if:
/// - all fields are `TryFromBytes`
fn derive_try_from_bytes_struct(
Expand Down Expand Up @@ -1294,6 +1334,7 @@ enum Trait {
IntoBytes,
Unaligned,
Sized,
ByteHash,
}

impl ToTokens for Trait {
Expand All @@ -1316,6 +1357,7 @@ impl ToTokens for Trait {
Trait::IntoBytes => "IntoBytes",
Trait::Unaligned => "Unaligned",
Trait::Sized => "Sized",
Trait::ByteHash => "ByteHash",
};
let ident = Ident::new(s, Span::call_site());
tokens.extend(core::iter::once(TokenTree::Ident(ident)));
Expand Down
38 changes: 38 additions & 0 deletions zerocopy-derive/src/output_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ use_as_trait_name!(
FromBytes => derive_from_bytes_inner,
IntoBytes => derive_into_bytes_inner,
Unaligned => derive_unaligned_inner,
ByteHash => derive_hash_inner,
);

/// Test that the given derive input expands to the expected output.
Expand Down Expand Up @@ -1981,3 +1982,40 @@ fn test_try_from_bytes_trivial_is_bit_valid_enum() {
} no_build
}
}

#[test]
fn test_hash() {
test! {
ByteHash {
struct Foo<T: Clone>(T) where Self: Sized;
} expands to {
#[allow(deprecated)]
#[automatically_derived]
impl<T: Clone> ::zerocopy::util::macro_util::core_reexport::hash::Hash for Foo<T>
where
Self: ::zerocopy::IntoBytes + ::zerocopy::Immutable,
Self: Sized,
{
fn hash<H>(&self, state: &mut H)
where
H: ::zerocopy::util::macro_util::core_reexport::hash::Hasher,
{
::zerocopy::util::macro_util::core_reexport::hash::Hasher::write(
state,
::zerocopy::IntoBytes::as_bytes(self)
)
}

fn hash_slice<H>(data: &[Self], state: &mut H)
where
H: ::zerocopy::util::macro_util::core_reexport::hash::Hasher,
{
::zerocopy::util::macro_util::core_reexport::hash::Hasher::write(
state,
::zerocopy::IntoBytes::as_bytes(data)
)
}
}
} no_build
}
}
38 changes: 38 additions & 0 deletions zerocopy-derive/tests/hash.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,38 @@
// Copyright 2024 The Fuchsia Authors
//
// Licensed under a BSD-style license <LICENSE-BSD>, Apache License, Version 2.0
// <LICENSE-APACHE or https://www.apache.org/licenses/LICENSE-2.0>, or the MIT
// license <LICENSE-MIT or https://opensource.org/licenses/MIT>, at your option.
// This file may not be copied, modified, or distributed except according to
// those terms.

// See comment in `include.rs` for why we disable the prelude.
#![no_implicit_prelude]
#![allow(warnings)]

include!("include.rs");

#[derive(imp::IntoBytes, imp::Immutable, imp::ByteHash)]
#[repr(C)]
struct Struct {
a: u64,
b: u32,
c: u32,
}

util_assert_impl_all!(Struct: imp::IntoBytes, imp::hash::Hash);

#[test]
fn test_hash() {
use imp::{
hash::{Hash, Hasher},
DefaultHasher,
};
fn hash(val: impl Hash) -> u64 {
let mut hasher = DefaultHasher::new();
val.hash(&mut hasher);
hasher.finish()
}
hash(Struct { a: 10, b: 15, c: 20 });
hash(&[Struct { a: 10, b: 15, c: 20 }, Struct { a: 5, b: 4, c: 3 }]);
}
3 changes: 2 additions & 1 deletion zerocopy-derive/tests/include.rs
Original file line number Diff line number Diff line change
Expand Up @@ -29,13 +29,14 @@ mod imp {
assert_eq,
cell::UnsafeCell,
convert::TryFrom,
hash,
marker::PhantomData,
mem::{ManuallyDrop, MaybeUninit},
option::IntoIter,
prelude::v1::*,
primitive::*,
},
::std::prelude::v1::*,
::std::{collections::hash_map::DefaultHasher, prelude::v1::*},
::zerocopy::*,
};
}
Expand Down

0 comments on commit 1358fd7

Please sign in to comment.