Skip to content

Commit

Permalink
Implement try_transmute! (google#1018)
Browse files Browse the repository at this point in the history
Closes google#1013.

Makes progress towards #5.
  • Loading branch information
jswrenn committed May 14, 2024
1 parent 6dd5f5f commit 5b095e0
Show file tree
Hide file tree
Showing 30 changed files with 611 additions and 4 deletions.
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@ Zerocopy provides three macros for safe, zero-cost casting between types:
the same size
- `transmute_mut` converts a mutable reference of one type to a mutable
reference of another type of the same size
- `transmute_ref` converts transmutes a mutable or immutable reference
- `transmute_ref` converts a mutable or immutable reference
of one type to an immutable reference of another type of the same size

These macros perform *compile-time* alignment and size checks, but cannot be
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -239,7 +239,7 @@ impl<Src, Dst, A, V> From<SizeError<Src, Dst>> for ConvertError<A, SizeError<Src
#[derive(PartialEq, Eq)]
pub struct ValidityError<Src, Dst: ?Sized + TryFromBytes> {
/// The source value involved in the conversion.
src: Src,
pub(crate) src: Src,
/// The inner destination type inolved in the conversion.
dst: PhantomData<Dst>,
}
Expand Down
110 changes: 109 additions & 1 deletion src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -58,7 +58,7 @@
//! the same size
//! - [`transmute_mut`] converts a mutable reference of one type to a mutable
//! reference of another type of the same size
//! - [`transmute_ref`] converts transmutes a mutable or immutable reference
//! - [`transmute_ref`] converts a mutable or immutable reference
//! of one type to an immutable reference of another type of the same size
//!
//! These macros perform *compile-time* alignment and size checks, but cannot be
Expand Down Expand Up @@ -4409,6 +4409,70 @@ macro_rules! transmute_mut {
}}
}

/// Conditionally transmutes a value of one type to a value of another type of
/// the same size.
///
/// This macro behaves like an invocation of this function:
///
/// ```ignore
/// const fn try_transmute<Src, Dst>(src: Src) -> Result<Dst, ValidityError<Src, Dst>>
/// where
/// Src: IntoBytes,
/// Dst: TryFromBytes,
/// size_of::<Src>() == size_of::<Dst>(),
/// {
/// # /*
/// ...
/// # */
/// }
/// ```
///
/// However, unlike a function, this macro can only be invoked when the types of
/// `Src` and `Dst` are completely concrete. The types `Src` and `Dst` are
/// inferred from the calling context; they cannot be explicitly specified in
/// the macro invocation.
///
/// Note that the `Src` produced by the expression `$e` will *not* be dropped.
/// Semantically, its bits will be copied into a new value of type `Dst`, the
/// original `Src` will be forgotten, and the value of type `Dst` will be
/// returned.
///
/// # Examples
///
/// ```
/// # use zerocopy::try_transmute;
/// assert_eq!(try_transmute!(0u8), Ok(false));
/// assert_eq!(try_transmute!(1u8), Ok(true));
///
/// let maybe_bool: Result<bool, _> = try_transmute!(255u8);
/// assert_eq!(maybe_bool.unwrap_err().into_src(), 255u8);
///
/// ```
#[macro_export]
macro_rules! try_transmute {
($e:expr) => {{
// NOTE: This must be a macro (rather than a function with trait bounds)
// because there's no way, in a generic context, to enforce that two
// types have the same size. `core::mem::transmute` uses compiler magic
// to enforce this so long as the types are concrete.

let e = $e;
if false {
// Check that the sizes of the source and destination types are
// equal.

// SAFETY: This code is never executed.
Ok(unsafe {
// Clippy: It's okay to transmute a type to itself.
#[allow(clippy::useless_transmute, clippy::missing_transmute_annotations)]
$crate::macro_util::core_reexport::mem::transmute(e)
})
} else {
$crate::macro_util::try_transmute::<_, _>(e)
}
}}
}

/// Includes a file and safely transmutes it to a value of an arbitrary type.
///
/// The file will be included as a byte array, `[u8; N]`, which will be
Expand Down Expand Up @@ -5607,6 +5671,50 @@ mod tests {
assert_eq!(*y, 0);
}

#[test]
fn test_try_transmute() {
// Test that memory is transmuted with `try_transmute` as expected.
let array_of_bools = [false, true, false, true, false, true, false, true];
let array_of_arrays = [[0, 1], [0, 1], [0, 1], [0, 1]];
let x: Result<[[u8; 2]; 4], _> = try_transmute!(array_of_bools);
assert_eq!(x, Ok(array_of_arrays));
let x: Result<[bool; 8], _> = try_transmute!(array_of_arrays);
assert_eq!(x, Ok(array_of_bools));

// Test that `try_transmute!` works with `!Immutable` types.
let x: Result<usize, _> = try_transmute!(UnsafeCell::new(1usize));
assert_eq!(x.unwrap(), 1);
let x: Result<UnsafeCell<usize>, _> = try_transmute!(1usize);
assert_eq!(x.unwrap().into_inner(), 1);
let x: Result<UnsafeCell<isize>, _> = try_transmute!(UnsafeCell::new(1usize));
assert_eq!(x.unwrap().into_inner(), 1);

#[derive(FromBytes, IntoBytes, Debug, PartialEq)]
#[repr(transparent)]
struct PanicOnDrop<T>(T);

impl<T> Drop for PanicOnDrop<T> {
fn drop(&mut self) {
panic!("PanicOnDrop dropped");
}
}

// Since `try_transmute!` semantically moves its argument on failure,
// the `PanicOnDrop` is not dropped, and thus this shouldn't panic.
let x: Result<usize, _> = try_transmute!(PanicOnDrop(1usize));
assert_eq!(x, Ok(1));

// Since `try_transmute!` semantically returns ownership of its argument
// on failure, the `PanicOnDrop` is returned rather than dropped, and
// thus this shouldn't panic.
let y: Result<bool, _> = try_transmute!(PanicOnDrop(2u8));
// We have to use `map_err` instead of comparing against
// `Err(PanicOnDrop(2u8))` because the latter would create and then drop
// its `PanicOnDrop` temporary, which would cause a panic.
assert_eq!(y.as_ref().map_err(|p| &p.src.0), Err::<&bool, _>(&2u8));
mem::forget(y);
}

#[test]
fn test_transmute_mut() {
// Test that memory is transmuted as expected.
Expand Down
88 changes: 88 additions & 0 deletions src/macro_util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -24,6 +24,14 @@ use core::{marker::PhantomData, mem::ManuallyDrop};
#[cfg(__INTERNAL_USE_ONLY_NIGHTLY_FEATURES_IN_TESTS)]
use core::ptr::{self, NonNull};

use crate::{
pointer::{
invariant::{self, AtLeast, Invariants},
AliasingSafe, AliasingSafeReason, BecauseExclusive,
},
IntoBytes, Ptr, TryFromBytes, ValidityError,
};

/// A compile-time check that should be one particular value.
pub trait ShouldBe<const VALUE: bool> {}

Expand Down Expand Up @@ -409,6 +417,86 @@ pub unsafe fn transmute_mut<'dst, 'src: 'dst, Src: 'src, Dst: 'dst>(
unsafe { &mut *dst }
}

/// Is a given source a valid instance of `Self`?
///
/// # Safety
///
/// Unsafe code may assume that, if `is_mut_src_valid(src)` returns true, `*src`
/// is a bit-valid instance of `Dst`, and that the size of `Src` is greater than
/// or equal to the size of `Dst`.
///
/// # Panics
///
/// `is_src_valid` may either produce a post-monomorphization error or a panic
/// if `Dst` is bigger than `Src`. Otherwise, `is_src_valid` panics under the
/// same circumstances as [`is_bit_valid`].
///
/// [`is_bit_valid`]: TryFromBytes::is_bit_valid
#[doc(hidden)]
#[inline]
fn is_src_valid<Src, Dst, I, R>(src: Ptr<'_, Src, I>) -> bool
where
Src: IntoBytes,
Dst: TryFromBytes + AliasingSafe<Src, I::Aliasing, R>,
I: Invariants<Validity = invariant::Valid>,
I::Aliasing: AtLeast<invariant::Shared>,
R: AliasingSafeReason,
{
crate::util::assert_dst_not_bigger_than_src::<Src, Dst>();

// SAFETY: This is a pointer cast, satisfying the following properties:
// - `p as *mut Dst` addresses a subset of the `bytes` addressed by `src`,
// because we assert above that the size of `Dst` is less than or equal to
// the size of `Src`.
// - `p as *mut Dst` is a provenance-preserving cast
// - Because `Dst: AliasingSafe<Src, I::Aliasing, _>`, either:
// - `I::Aliasing` is `Exclusive`
// - `Src` and `Dst` are both `Immutable`, in which case they
// trivially contain `UnsafeCell`s at identical locations
#[allow(clippy::as_conversions)]
let c_ptr = unsafe { src.cast_unsized(|p| p as *mut Dst) };

// SAFETY: `c_ptr` is derived from `src` which is `IntoBytes`. By
// invariant on `IntoByte`s, `c_ptr`'s referent consists entirely of
// initialized bytes.
let c_ptr = unsafe { c_ptr.assume_initialized() };

Dst::is_bit_valid(c_ptr)
}

/// Attempts to transmute `Src` into `Dst`.
///
/// A helper for `try_transmute!`.
///
/// # Panics
///
/// `try_transmute` may either produce a post-monomorphization error or a panic
/// if `Dst` is bigger than `Src`. Otherwise, `try_transmute` panics under the
/// same circumstances as [`is_bit_valid`].
///
/// [`is_bit_valid`]: TryFromBytes::is_bit_valid
#[inline(always)]
pub fn try_transmute<Src, Dst>(mut src: Src) -> Result<Dst, ValidityError<Src, Dst>>
where
Src: IntoBytes,
Dst: TryFromBytes,
{
if !is_src_valid::<Src, Dst, _, BecauseExclusive>(Ptr::from_mut(&mut src)) {
return Err(ValidityError::new(src));
}

let src = ManuallyDrop::new(src);

// SAFETY: By contract on `is_src_valid`, we have confirmed both that `Dst`
// is no larger than `Src`, and that `src` is a bit-valid instance of `Dst`.
// These conditions are preserved through the `ManuallyDrop<T>` wrapper,
// which is documented to have identical and layout bit validity to its
// inner value [1].
//
// [1]: https://doc.rust-lang.org/std/mem/struct.ManuallyDrop.html
Ok(unsafe { core::mem::transmute_copy(&*src) })
}

/// A function which emits a warning if its return value is not used.
#[must_use]
#[inline(always)]
Expand Down
2 changes: 1 addition & 1 deletion src/pointer/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@
mod aliasing_safety;
mod ptr;

pub use aliasing_safety::{AliasingSafe, BecauseExclusive, BecauseImmutable};
pub use aliasing_safety::{AliasingSafe, AliasingSafeReason, BecauseExclusive, BecauseImmutable};
pub use ptr::{invariant, Ptr};

use crate::Unaligned;
Expand Down
17 changes: 17 additions & 0 deletions src/util.rs
Original file line number Diff line number Diff line change
Expand Up @@ -607,6 +607,23 @@ where
const_assert!(<T as ConstAssert>::DST_IS_NOT_ZST);
}

/// Assert at compile time that the size of `Dst` <= the size of `Src`.
pub(crate) const fn assert_dst_not_bigger_than_src<Src, Dst>() {
trait ConstAssert {
const DST_NOT_BIGGER_THAN_SRC: bool;
}

impl<Src, Dst> ConstAssert for (Src, Dst) {
const DST_NOT_BIGGER_THAN_SRC: bool = {
let dst_bigger_than_src = mem::size_of::<Dst>() > mem::size_of::<Src>();
const_assert!(!dst_bigger_than_src);
!dst_bigger_than_src
};
}

const_assert!(<(Src, Dst) as ConstAssert>::DST_NOT_BIGGER_THAN_SRC);
}

/// Since we support multiple versions of Rust, there are often features which
/// have been stabilized in the most recent stable release which do not yet
/// exist (stably) on our MSRV. This module provides polyfills for those
Expand Down
1 change: 1 addition & 0 deletions tests/ui-msrv/try_transmute-dst-not-tryfrombytes.rs
37 changes: 37 additions & 0 deletions tests/ui-msrv/try_transmute-dst-not-tryfrombytes.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
error[E0277]: the trait bound `NotZerocopy: TryFromBytes` is not satisfied
--> tests/ui-msrv/try_transmute-dst-not-tryfrombytes.rs:17:58
|
17 | let dst_not_try_from_bytes: Result<NotZerocopy, _> = try_transmute!(AU16(0));
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `TryFromBytes` is not implemented for `NotZerocopy`
|
note: required by a bound in `try_transmute`
--> src/macro_util.rs
|
| Dst: TryFromBytes,
| ^^^^^^^^^^^^ required by this bound in `try_transmute`
= note: this error originates in the macro `try_transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

error[E0277]: the trait bound `NotZerocopy: TryFromBytes` is not satisfied
--> tests/ui-msrv/try_transmute-dst-not-tryfrombytes.rs:17:33
|
17 | let dst_not_try_from_bytes: Result<NotZerocopy, _> = try_transmute!(AU16(0));
| ^^^^^^^^^^^^^^^^^^^^^^ the trait `TryFromBytes` is not implemented for `NotZerocopy`
|
note: required by a bound in `ValidityError`
--> src/error.rs
|
| pub struct ValidityError<Src, Dst: ?Sized + TryFromBytes> {
| ^^^^^^^^^^^^ required by this bound in `ValidityError`

error[E0277]: the trait bound `NotZerocopy: TryFromBytes` is not satisfied
--> tests/ui-msrv/try_transmute-dst-not-tryfrombytes.rs:17:58
|
17 | let dst_not_try_from_bytes: Result<NotZerocopy, _> = try_transmute!(AU16(0));
| ^^^^^^^^^^^^^^^^^^^^^^^ the trait `TryFromBytes` is not implemented for `NotZerocopy`
|
note: required by a bound in `ValidityError`
--> src/error.rs
|
| pub struct ValidityError<Src, Dst: ?Sized + TryFromBytes> {
| ^^^^^^^^^^^^ required by this bound in `ValidityError`
= note: this error originates in the macro `try_transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
1 change: 1 addition & 0 deletions tests/ui-msrv/try_transmute-size-decrease.rs
17 changes: 17 additions & 0 deletions tests/ui-msrv/try_transmute-size-decrease.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
warning: unused variable: `decrease_size`
--> tests/ui-msrv/try_transmute-size-decrease.rs:19:9
|
19 | let decrease_size: Result<u8, _> = try_transmute!(AU16(0));
| ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_decrease_size`
|
= note: `#[warn(unused_variables)]` on by default

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
--> tests/ui-msrv/try_transmute-size-decrease.rs:19:40
|
19 | let decrease_size: Result<u8, _> = try_transmute!(AU16(0));
| ^^^^^^^^^^^^^^^^^^^^^^^
|
= note: source type: `AU16` (16 bits)
= note: target type: `u8` (8 bits)
= note: this error originates in the macro `try_transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
1 change: 1 addition & 0 deletions tests/ui-msrv/try_transmute-size-increase.rs
17 changes: 17 additions & 0 deletions tests/ui-msrv/try_transmute-size-increase.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,17 @@
warning: unused variable: `increase_size`
--> tests/ui-msrv/try_transmute-size-increase.rs:19:9
|
19 | let increase_size: Result<AU16, _> = try_transmute!(0u8);
| ^^^^^^^^^^^^^ help: if this is intentional, prefix it with an underscore: `_increase_size`
|
= note: `#[warn(unused_variables)]` on by default

error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
--> tests/ui-msrv/try_transmute-size-increase.rs:19:42
|
19 | let increase_size: Result<AU16, _> = try_transmute!(0u8);
| ^^^^^^^^^^^^^^^^^^^
|
= note: source type: `u8` (8 bits)
= note: target type: `AU16` (16 bits)
= note: this error originates in the macro `try_transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
1 change: 1 addition & 0 deletions tests/ui-msrv/try_transmute-src-not-intobytes.rs
12 changes: 12 additions & 0 deletions tests/ui-msrv/try_transmute-src-not-intobytes.stderr
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
error[E0277]: the trait bound `NotZerocopy<AU16>: zerocopy::IntoBytes` is not satisfied
--> tests/ui-msrv/try_transmute-src-not-intobytes.rs:18:47
|
18 | let src_not_into_bytes: Result<AU16, _> = try_transmute!(NotZerocopy(AU16(0)));
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::IntoBytes` is not implemented for `NotZerocopy<AU16>`
|
note: required by a bound in `try_transmute`
--> src/macro_util.rs
|
| Src: IntoBytes,
| ^^^^^^^^^ required by this bound in `try_transmute`
= note: this error originates in the macro `try_transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
Loading

0 comments on commit 5b095e0

Please sign in to comment.