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

Add transmute_ref! macro #183

Merged
merged 1 commit into from
Oct 4, 2023
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
10 changes: 7 additions & 3 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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
Original file line number Diff line number Diff line change
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.
Copy link

@gootorov gootorov May 28, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

There's one "trick" with traits and associated constants you can do:

use std::mem;
use std::marker::PhantomData;

struct AlignChecker<T, U>(PhantomData<T>, PhantomData<U>);

trait AlignCheck {
    const ALIGN_OF_T: usize;
    const ALIGN_OF_U: usize;
    const ALIGN_EQ_PROOF: () =
        assert!(Self::ALIGN_OF_T == Self::ALIGN_OF_U, "T and U have different alignment");
    
    fn noop();
}

impl<T, U> AlignCheck for AlignChecker<T, U> {
    const ALIGN_OF_T: usize = mem::align_of::<T>();
    const ALIGN_OF_U: usize = mem::align_of::<U>();
    
    #[inline(always)]
    fn noop() {
        // NOTE: This does not compile to anything.
        // However, this `let` must be present to force the compiler to const evaluate our checks.
        let _consteval_proof = Self::ALIGN_EQ_PROOF;
    }
}

Then, noop as-is is zero-cost*

// does not compile to anything*
AlignChecker::<u8, u8>::noop();

// but this produces a compilation error
AlignChecker::<u8, u16>::noop();

So, you can modify noop to contain your generic code.

  • About zero-cost. So, noop is compiled to just a single ret. Calling noop produces a call instruction, unless it is inlined. https://godbolt.org/z/YnaMvxYqh

Sorry if this isn't useful/already known. Also, not sure if this approach has some other limitations. Sharing just in case, because I do not see this technique used often :)

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This is a fantastic idea, thanks! We'll definitely consider it. Just want to confirm first that the check is guaranteed to catch any unsoundness: rust-lang/unsafe-code-guidelines#409

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another thread about it: rust-lang/rust#112090

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Here's another thread about it: rust-lang/rust#112090

Okay, based on the discussion there, it seems that there is such a guarantee, but we'd need to be careful about how we structure things to make sure that we actually write code that benefits from that guarantee.

Copy link

@gootorov gootorov May 31, 2023

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These threads are quite interesting and insightful, thanks!

I got a little more curious, and decided to try to implement something that matches std::mem::transmute as close as possible. It seems it works, but I'm not sure if I haven't missed anything.

use std::mem;
use std::mem::ManuallyDrop;

use zerocopy::AsBytes;
use zerocopy::FromBytes;

// === Part of private API ===

trait SafeTransmute<T: AsBytes, U: FromBytes> {
    const SAFE_TO_TRANSMUTE: () = assert!(mem::size_of::<T>() == mem::size_of::<U>());

    fn transmute(t: T) -> U {
        let _ = Self::SAFE_TO_TRANSMUTE;

        unsafe { transmute_anything(t) }
    }
}

trait SafeTransmuteRef<T: Sized + AsBytes, U: Sized + FromBytes> {
    const SAFE_TO_TRANSMUTE_REF: () = assert!(
        mem::size_of::<T>() == mem::size_of::<U>() && mem::align_of::<T>() >= mem::align_of::<U>(),
    );

    fn transmute_ref<'b, 'a: 'b>(t: &'a T) -> &'b U {
        let _ = Self::SAFE_TO_TRANSMUTE_REF;

        unsafe { transmute_anything(t) }
    }
}

const unsafe fn transmute_anything<T, U>(t: T) -> U {
    union UnsafeTransmuter<T, U> {
        t: ManuallyDrop<T>,
        u: ManuallyDrop<U>,
    }

    ManuallyDrop::into_inner(unsafe {
        UnsafeTransmuter {
            t: ManuallyDrop::new(t),
        }
        .u
    })
}

struct Transmuter;

impl<T: AsBytes, U: FromBytes> SafeTransmute<T, U> for Transmuter {}
impl<T: Sized + AsBytes, U: Sized + FromBytes> SafeTransmuteRef<T, U> for Transmuter {}

// === Part of public API ===

pub fn transmute<T: AsBytes, U: FromBytes>(t: T) -> U {
    Transmuter::transmute(t)
}

pub fn transmute_ref<'b, 'a: 'b, T: Sized + AsBytes, U: Sized + FromBytes>(t: &'a T) -> &'b U {
    Transmuter::transmute_ref(t)
}

fn main() {
    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);

    let u = 0u64;
    let array = [0, 0, 0, 0, 0, 0, 0, 0];
    let x: &[u8; 8] = transmute_ref(&u);
    assert_eq!(*x, array);

    // does not compile (as expected)
    //
    // let increase_alignment: &u16 = transmute_ref::<[u8; 2], u16>(&[0; 2]);
    // let decrease_size: &u8 = transmute_ref::<[u8; 2], u8>(&[0; 2]);
    // let increase_size: &u16 = transmute_ref::<u8, u16>(&0);
    // let int_to_bool: bool = transmute::<u8, bool>(42);
    // let int_ref_to_bool_ref: &bool = transmute_ref::<u8, bool>(&42);
}

I wish we had const fn in traits, then transmute/transmute_ref could've been const as well :)

edit: The original code was a bit too strict w.r.t. transmute, updated.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yeah something like this would be awesome! I'm not gonna get a chance to work on this much for the next few weeks, but feel free to leave more comments here, and I'll get to them when I'm working on this again!

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Note that rust-lang/rust#112301 will likely make this technique a bit annoying for users (although the error messages are way better than the existing technique, and the implementation is way simpler, so IMO it's still worth it on balance).

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I've made a branch experimenting with this approach: #190

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Exciting news! With inline_const being stabilized, the following check will soon be possible on stable compiler:

fn check_align<T, U>() {
    const {
        assert!(
            mem::size_of::<T>() == mem::size_of::<U>() 
                && mem::align_of::<T>() >= mem::align_of::<U>()
        );
    }
}

Link to nightly playground:
https://play.rust-lang.org/?version=nightly&mode=debug&edition=2021&gist=24d5ca56b2834fdfde8ec7681359e729

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Unfortunately our MSRV is currently 1.56, so we won't be able to use that for a while, but I'm sure we'll make use of it once our MSRV is high enough.


// 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() {
joshlf marked this conversation as resolved.
Show resolved Hide resolved
// 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
Original file line number Diff line number Diff line change
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