Skip to content

Commit

Permalink
Add transmute_ref! macro (#183)
Browse files Browse the repository at this point in the history
This macro is like the existing `transmute!`, but it transmutes
immutable references rather than values.

Release 0.7.8.

Issue #159
  • Loading branch information
joshlf committed Oct 4, 2023
1 parent edffeac commit 646fe3b
Show file tree
Hide file tree
Showing 102 changed files with 2,556 additions and 13 deletions.
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Expand Up @@ -122,13 +122,17 @@ jobs:
with:
toolchain: ${{ env.ZC_TOOLCHAIN }}
targets: ${{ matrix.target }}
# We require the `rust-src` component to ensure that the compiler
# error output generated during UI tests matches that generated on
# local developer machines; see
# https://github.com/rust-lang/rust/issues/116433.
#
# Only nightly has a working Miri, so we skip installing on all other
# toolchains. This expression is effectively a ternary expression -
# see [1] for details.
#
# [1]
# https://github.com/actions/runner/issues/409#issuecomment-752775072
components: clippy ${{ matrix.toolchain == 'nightly' && ', miri' || '' }}
# [1] https://github.com/actions/runner/issues/409#issuecomment-752775072
components: clippy, rust-src ${{ matrix.toolchain == 'nightly' && ', miri' || '' }}

- name: Rust Cache
uses: Swatinem/rust-cache@a95ba195448af2da9b00fb742d14ffaaf3c21f43 # v2.7.0
Expand Down
8 changes: 4 additions & 4 deletions Cargo.toml
Expand Up @@ -11,7 +11,7 @@
[package]
edition = "2021"
name = "zerocopy"
version = "0.7.7"
version = "0.7.8"
authors = ["Joshua Liebow-Feeser <joshlf@google.com>"]
description = "Utilities for zero-copy parsing and serialization"
license = "BSD-2-Clause"
Expand Down Expand Up @@ -41,7 +41,7 @@ simd-nightly = ["simd"]
__internal_use_only_features_that_work_on_stable = ["alloc", "derive", "simd"]

[dependencies]
zerocopy-derive = { version = "=0.7.7", path = "zerocopy-derive", optional = true }
zerocopy-derive = { version = "=0.7.8", path = "zerocopy-derive", optional = true }

[dependencies.byteorder]
version = "1.3"
Expand All @@ -52,7 +52,7 @@ optional = true
# zerocopy-derive remain equal, even if the 'derive' feature isn't used.
# See: https://github.com/matklad/macro-dep-test
[target.'cfg(any())'.dependencies]
zerocopy-derive = { version = "=0.7.7", path = "zerocopy-derive" }
zerocopy-derive = { version = "=0.7.8", path = "zerocopy-derive" }

[dev-dependencies]
assert_matches = "1.5"
Expand All @@ -67,4 +67,4 @@ testutil = { path = "testutil" }
# CI test failures.
trybuild = { version = "=1.0.85", features = ["diff"] }
# In tests, unlike in production, zerocopy-derive is not optional
zerocopy-derive = { version = "=0.7.7", path = "zerocopy-derive" }
zerocopy-derive = { version = "=0.7.8", path = "zerocopy-derive" }
203 changes: 201 additions & 2 deletions src/lib.rs
Expand Up @@ -1652,7 +1652,7 @@ macro_rules! transmute {
// `AsBytes` and that the type of this macro invocation expression
// is `FromBytes`.
const fn transmute<T: $crate::AsBytes, U: $crate::FromBytes>(_t: T) -> U {
unreachable!()
loop {}
}
transmute(e)
} else {
Expand All @@ -1669,7 +1669,154 @@ macro_rules! transmute {
// `core::mem::transmute`, this macro would not work in `std`
// contexts in which `core` was not manually imported. This is not a
// problem for 2018 edition crates.
unsafe { $crate::macro_util::core_reexport::mem::transmute(e) }
unsafe {
// Clippy: It's okay to transmute a type to itself.
#[allow(clippy::useless_transmute)]
$crate::macro_util::core_reexport::mem::transmute(e)
}
}
}}
}

/// Safely transmutes a mutable or immutable reference of one type to an
/// immutable reference of another type of the same size.
///
/// The expression `$e` must have a concrete type, `&T` or `&mut T`, where `T:
/// Sized + AsBytes`. The `transmute_ref!` expression must also have a concrete
/// type, `&U` (`U` is inferred from the calling context), where `U: Sized +
/// FromBytes`. It must be the case that `align_of::<T>() >= align_of::<U>()`.
///
/// The lifetime of the input type, `&T` or `&mut T`, must be the same as or
/// outlive the lifetime of the output type, `&U`.
///
/// # Alignment increase error message
///
/// Because of limitations on macros, the error message generated when
/// `transmute_ref!` is used to transmute from a type of lower alignment to a
/// type of higher alignment is somewhat confusing. For example, the following
/// code:
///
/// ```compile_fail
/// const INCREASE_ALIGNMENT: &u16 = zerocopy::transmute_ref!(&[0u8; 2]);
/// ```
///
/// ...generates the following error:
///
/// ```text
/// error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
/// --> src/lib.rs:1524:34
/// |
/// 5 | const INCREASE_ALIGNMENT: &u16 = zerocopy::transmute_ref!(&[0u8; 2]);
/// | ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
/// |
/// = note: source type: `AlignOf<[u8; 2]>` (8 bits)
/// = note: target type: `MaxAlignsOf<[u8; 2], u16>` (16 bits)
/// = note: this error originates in the macro `zerocopy::transmute_ref` (in Nightly builds, run with -Z macro-backtrace for more info)
/// ```
///
/// This is saying that `max(align_of::<T>(), align_of::<U>()) !=
/// align_of::<T>()`, which is equivalent to `align_of::<T>() <
/// align_of::<U>()`.
#[macro_export]
macro_rules! transmute_ref {
($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 or alignment.

// Reborrow so that mutable references are supported too.
//
// In the rest of the comments, we refer only to `&T` since this
// reborrow ensures that `e` is an immutable reference.
let e = &*$e;

#[allow(unused, clippy::diverging_sub_expression)]
if false {
// This branch, though never taken, ensures that the type of `e` is
// `&T` where `T: 't + Sized + AsBytes`, that the type of this macro
// expression is `&U` where `U: 'u + Sized + FromBytes`, and that
// `'t` outlives `'u`.
const fn transmute<'u, 't: 'u, T: 't + Sized + $crate::AsBytes, U: 'u + Sized + $crate::FromBytes>(_t: &'t T) -> &'u U {
loop {}
}
transmute(e)
} else if false {
// This branch, though never taken, ensures that `size_of::<T>() ==
// size_of::<U>()`.

// `t` is inferred to have type `T` because it's assigned to `e` (of
// type `&T`) as `&t`.
let mut t = unreachable!();
e = &t;

// `u` is inferred to have type `U` because it's used as `&u` as the
// value returned from this branch.
//
// SAFETY: This code is never run.
let u = unsafe {
// Clippy: It's okay to transmute a type to itself.
#[allow(clippy::useless_transmute)]
$crate::macro_util::core_reexport::mem::transmute(t)
};
&u
} else if false {
// This branch, though never taken, ensures that the alignment of
// `T` is greater than or equal to to the alignment of `U`.

// `t` is inferred to have type `T` because it's assigned to `e` (of
// type `&T`) as `&t`.
let mut t = unreachable!();
e = &t;

// `u` is inferred to have type `U` because it's used as `&u` as the
// value returned from this branch.
let mut u = unreachable!();

// The type wildcard in this bound is inferred to be `T` because
// `align_of.into_t()` is assigned to `t` (which has type `T`).
let align_of: $crate::macro_util::AlignOf<_> = unreachable!();
t = align_of.into_t();
// `max_aligns` is inferred to have type `MaxAlignsOf<T, U>` because
// of the inferred types of `t` and `u`.
let mut max_aligns = $crate::macro_util::MaxAlignsOf::new(t, u);

// This transmute will only compile successfully if
// `align_of::<T>() == max(align_of::<T>(), align_of::<U>())` - in
// other words, if `align_of::<T>() >= align_of::<U>()`.
//
// SAFETY: This code is never run.
max_aligns = unsafe { $crate::macro_util::core_reexport::mem::transmute(align_of) };

&u
} else {
// SAFETY:
// - We know that the input and output types are both `Sized` (ie,
// thin) references thanks to the trait bounds on `transmute`
// above, and thanks to the fact that transmute takes and returns
// references.
// - We know that it is sound to view the target type of the input
// reference (`T`) as the target type of the output reference
// (`U`) because `T: AsBytes` and `U: FromBytes` (guaranteed by
// trait bounds on `transmute`) and because `size_of::<T>() ==
// size_of::<U>()` (guaranteed by the first `core::mem::transmute`
// above).
// - We know that alignment is not increased thanks to the second
// `core::mem::transmute` above (the one which transmutes
// `MaxAlignsOf` into `AlignOf`).
//
// We use this reexport of `core::mem::transmute` because we know it
// will always be available for crates which are using the 2015
// edition of Rust. By contrast, if we were to use
// `std::mem::transmute`, this macro would not work for such crates
// in `no_std` contexts, and if we were to use
// `core::mem::transmute`, this macro would not work in `std`
// contexts in which `core` was not manually imported. This is not a
// problem for 2018 edition crates.
unsafe {
// Clippy: It's okay to transmute a type to itself.
#[allow(clippy::useless_transmute)]
$crate::macro_util::core_reexport::mem::transmute(e)
}
}
}}
}
Expand Down Expand Up @@ -3810,6 +3957,58 @@ mod tests {
assert_eq!(X, ARRAY_OF_ARRAYS);
}

#[test]
fn test_transmute_ref() {
// Test that memory is transmuted as expected.
let array_of_u8s = [0u8, 1, 2, 3, 4, 5, 6, 7];
let array_of_arrays = [[0, 1], [2, 3], [4, 5], [6, 7]];
let x: &[[u8; 2]; 4] = transmute_ref!(&array_of_u8s);
assert_eq!(*x, array_of_arrays);
let x: &[u8; 8] = transmute_ref!(&array_of_arrays);
assert_eq!(*x, array_of_u8s);

// Test that `transmute_ref!` is legal in a const context.
const ARRAY_OF_U8S: [u8; 8] = [0u8, 1, 2, 3, 4, 5, 6, 7];
const ARRAY_OF_ARRAYS: [[u8; 2]; 4] = [[0, 1], [2, 3], [4, 5], [6, 7]];
#[allow(clippy::redundant_static_lifetimes)]
const X: &'static [[u8; 2]; 4] = transmute_ref!(&ARRAY_OF_U8S);
assert_eq!(*X, ARRAY_OF_ARRAYS);

// Test that it's legal to transmute a reference while shrinking the
// lifetime (note that `X` has the lifetime `'static`).
let x: &[u8; 8] = transmute_ref!(X);
assert_eq!(*x, ARRAY_OF_U8S);

// Test that `transmute_ref!` supports decreasing alignment.
let u = AU64(0);
let array = [0, 0, 0, 0, 0, 0, 0, 0];
let x: &[u8; 8] = transmute_ref!(&u);
assert_eq!(*x, array);

// Test that a mutable reference can be turned into an immutable one.
let mut x = 0u8;
#[allow(clippy::useless_transmute)]
let y: &u8 = transmute_ref!(&mut x);
assert_eq!(*y, 0);
}

#[test]
fn test_macros_evaluate_args_once() {
let mut ctr = 0;
let _: usize = transmute!({
ctr += 1;
0usize
});
assert_eq!(ctr, 1);

let mut ctr = 0;
let _: &usize = transmute_ref!({
ctr += 1;
&0usize
});
assert_eq!(ctr, 1);
}

#[test]
fn test_address() {
// Test that the `Deref` and `DerefMut` implementations return a
Expand Down
89 changes: 88 additions & 1 deletion src/macro_util.rs
Expand Up @@ -13,7 +13,7 @@

#![allow(missing_debug_implementations)]

use core::marker::PhantomData;
use core::{marker::PhantomData, mem::ManuallyDrop};

/// A compile-time check that should be one particular value.
pub trait ShouldBe<const VALUE: bool> {}
Expand All @@ -23,6 +23,40 @@ pub struct HasPadding<T: ?Sized, const VALUE: bool>(PhantomData<T>);

impl<T: ?Sized, const VALUE: bool> ShouldBe<VALUE> for HasPadding<T, VALUE> {}

/// A type whose size is equal to `align_of::<T>()`.
#[repr(C)]
pub struct AlignOf<T> {
// This field ensures that:
// - The size is always at least 1 (the minimum possible alignment).
// - If the alignment is greater than 1, Rust has to round up to the next
// multiple of it in order to make sure that `Align`'s size is a multiple
// of that alignment. Without this field, its size could be 0, which is a
// valid multiple of any alignment.
_u: u8,
_a: [T; 0],
}

impl<T> AlignOf<T> {
#[inline(never)] // Make `missing_inline_in_public_items` happy.
pub fn into_t(self) -> T {
unreachable!()
}
}

/// A type whose size is equal to `max(align_of::<T>(), align_of::<U>())`.
#[repr(C)]
pub union MaxAlignsOf<T, U> {
_t: ManuallyDrop<AlignOf<T>>,
_u: ManuallyDrop<AlignOf<U>>,
}

impl<T, U> MaxAlignsOf<T, U> {
#[inline(never)] // Make `missing_inline_in_public_items` happy.
pub fn new(_t: T, _u: U) -> MaxAlignsOf<T, U> {
unreachable!()
}
}

/// Does the struct type `$t` have padding?
///
/// `$ts` is the list of the type of every field in `$t`. `$t` must be a
Expand Down Expand Up @@ -71,8 +105,61 @@ pub mod core_reexport {

#[cfg(test)]
mod tests {
use core::mem;

use super::*;
use crate::util::testutil::*;

#[test]
fn test_align_of() {
macro_rules! test {
($ty:ty) => {
assert_eq!(mem::size_of::<AlignOf<$ty>>(), mem::align_of::<$ty>());
};
}

test!(());
test!(u8);
test!(AU64);
test!([AU64; 2]);
}

#[test]
fn test_max_aligns_of() {
macro_rules! test {
($t:ty, $u:ty) => {
assert_eq!(
mem::size_of::<MaxAlignsOf<$t, $u>>(),
core::cmp::max(mem::align_of::<$t>(), mem::align_of::<$u>())
);
};
}

test!(u8, u8);
test!(u8, AU64);
test!(AU64, u8);
}

#[test]
fn test_typed_align_check() {
// Test that the type-based alignment check used in `transmute_ref!`
// behaves as expected.

macro_rules! assert_t_align_gteq_u_align {
($t:ty, $u:ty, $gteq:expr) => {
assert_eq!(
mem::size_of::<MaxAlignsOf<$t, $u>>() == mem::size_of::<AlignOf<$t>>(),
$gteq
);
};
}

assert_t_align_gteq_u_align!(u8, u8, true);
assert_t_align_gteq_u_align!(AU64, AU64, true);
assert_t_align_gteq_u_align!(AU64, u8, true);
assert_t_align_gteq_u_align!(u8, AU64, false);
}

#[test]
fn test_struct_has_padding() {
// Test that, for each provided repr, `struct_has_padding!` reports the
Expand Down
1 change: 1 addition & 0 deletions tests/ui-msrv/transmute-ref-alignment-increase.rs

0 comments on commit 646fe3b

Please sign in to comment.