From c51c96a755c77de8135f96342476686103fb9ddd Mon Sep 17 00:00:00 2001 From: Joshua Liebow-Feeser Date: Mon, 23 Sep 2024 09:27:31 -0700 Subject: [PATCH] Add transmute_container! macro --- .github/workflows/ci.yml | 26 ++++++ Cargo.toml | 3 + src/macros.rs | 139 ++++++++++++++++++++++++++++++ src/util/macro_util.rs | 178 +++++++++++++++++++++++++++++++++++++++ 4 files changed, 346 insertions(+) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index da8022e0ae..d608a5b83f 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -56,6 +56,7 @@ jobs: # which a particular feature is supported. "zerocopy-core-error", "zerocopy-diagnostic-on-unimplemented", + "zerocopy-gat", "zerocopy-generic-bounds-in-const-fn", "zerocopy-target-has-atomics", "zerocopy-aarch64-simd", @@ -93,6 +94,8 @@ jobs: features: "--all-features" - toolchain: "zerocopy-diagnostic-on-unimplemented" features: "--all-features" + - toolchain: "zerocopy-gat" + features: "--all-features" - toolchain: "zerocopy-generic-bounds-in-const-fn" features: "--all-features" - toolchain: "zerocopy-target-has-atomics" @@ -117,6 +120,8 @@ jobs: toolchain: "zerocopy-core-error" - crate: "zerocopy-derive" toolchain: "zerocopy-diagnostic-on-unimplemented" + - crate: "zerocopy-derive" + toolchain: "zerocopy-gat" - crate: "zerocopy-derive" toolchain: "zerocopy-generic-bounds-in-const-fn" - crate: "zerocopy-derive" @@ -190,6 +195,27 @@ jobs: target: "thumbv6m-none-eabi" - toolchain: "zerocopy-diagnostic-on-unimplemented" target: "wasm32-wasi" + # Exclude most targets targets from the `zerocopy-gat` toolchain since + # the `zerocopy-gat` feature is unrelated to compilation target. This + # only leaves i686 and x86_64 targets. + - toolchain: "zerocopy-gat" + target: "arm-unknown-linux-gnueabi" + - toolchain: "zerocopy-gat" + target: "aarch64-unknown-linux-gnu" + - toolchain: "zerocopy-gat" + target: "powerpc-unknown-linux-gnu" + - toolchain: "zerocopy-gat" + target: "powerpc64-unknown-linux-gnu" + - toolchain: "zerocopy-gat" + target: "riscv64gc-unknown-linux-gnu" + - toolchain: "zerocopy-gat" + target: "s390x-unknown-linux-gnu" + - toolchain: "zerocopy-gat" + target: "x86_64-pc-windows-msvc" + - toolchain: "zerocopy-gat" + target: "thumbv6m-none-eabi" + - toolchain: "zerocopy-gat" + target: "wasm32-wasi" # Exclude most targets targets from the # `zerocopy-generic-bounds-in-const-fn` toolchain since the # `zerocopy-generic-bounds-in-const-fn` feature is unrelated to diff --git a/Cargo.toml b/Cargo.toml index fa3a3ed695..df691df1e0 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -38,6 +38,9 @@ zerocopy-core-error = "1.81.0" # From 1.78.0, Rust supports the `#[diagnostic::on_unimplemented]` attribute. zerocopy-diagnostic-on-unimplemented = "1.78.0" +# From 1.65.0, Rust supports generic associated types (GATs). +zerocopy-gat = "1.65.0" + # From 1.61.0, Rust supports generic types with trait bounds in `const fn`. zerocopy-generic-bounds-in-const-fn = "1.61.0" diff --git a/src/macros.rs b/src/macros.rs index d1d32c81b4..370a5b8a4f 100644 --- a/src/macros.rs +++ b/src/macros.rs @@ -678,6 +678,110 @@ macro_rules! try_transmute_mut { }} } +/// TODO: +/// - Prevent lifetime extension +/// - Justify that this doesn't violate Src or Dst's safety invariants +/// (basically just that they opted into being FromBytes + IntoBytes + +/// Immutable) +#[macro_export] +#[cfg(all(zerocopy_gat, feature = "alloc"))] +#[cfg_attr(doc_cfg, doc(cfg(feature = "alloc")))] +macro_rules! transmute_container { + ($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. + + let e = $e; + + fn into_elem(_c: C) -> C::Elem { + loop {} + } + + fn from_elem(_e: C::Elem) -> C { + loop {} + } + + #[allow(unused, clippy::diverging_sub_expression)] + if false { + // This branch, though never taken, ensures that element type of `e` + // is `T` where `T: Sized + FromBytes + IntoBytes + Immutable` and + // that the element type of this macro expression is `U` where `U: + // Sized + FromBytes + IntoBytes + Immutable`. + + struct AssertSrcIsSized(T); + struct AssertSrcIsFromBytes(T); + struct AssertSrcIsIntoBytes(T); + struct AssertSrcIsImmutable(T); + struct AssertDstIsSized(T); + struct AssertDstIsFromBytes(T); + struct AssertDstIsIntoBytes(T); + struct AssertDstIsImmutable(T); + + let src_elem = into_elem(e); + + if true { + let _ = AssertSrcIsSized(src_elem); + } else if true { + let _ = AssertSrcIsFromBytes(src_elem); + } else if true { + let _ = AssertSrcIsIntoBytes(src_elem); + } else { + let _ = AssertSrcIsImmutable(src_elem); + } + + if true { + #[allow(unused, unreachable_code)] + let u = AssertDstIsSized(loop {}); + from_elem(u.0) + } else if true { + #[allow(unused, unreachable_code)] + let u = AssertDstIsFromBytes(loop {}); + from_elem(u.0) + } else if true { + #[allow(unused, unreachable_code)] + let u = AssertDstIsIntoBytes(loop {}); + from_elem(u.0) + } else { + #[allow(unused, unreachable_code)] + let u = AssertDstIsImmutable(loop {}); + from_elem(u.0) + } + } else if false { + // This branch, though never taken, ensures that `size_of::() == + // size_of::()` and that that `align_of::() >= + // align_of::()`. + + // `t` is inferred to have type `T` because it's assigned to `e` (of + // type `&mut T`) as `&mut t`. + let mut t = loop {}; + e = from_elem(t); + + // `u` is inferred to have type `U` because it's used as `&mut u` as + // the value returned from this branch. + let u; + + $crate::assert_size_eq!(t, u); + $crate::assert_align_eq!(t, u); + + from_elem(u) + } else { + // SAFETY: For source type `Src` and destination type `Dst`: + // - We know that `size_of::() == size_of::()` thanks to + // the use of `assert_size_eq!` above. + // - We know that `align_of::() == align_of::()` thanks to + // the use of `assert_align_eq!` above. + // - We know that `Src` and `Dst` have equivalent bit validity + // thanks to `Src: FromBytes + IntoBytes` and `Dst: FromBytes + + // IntoBytes`, which are guaranteed above. + // - We know that `Src` and `Dst` have `UnsafeCell`s at the same + // offsets thanks to `Src: Immutable` and `Dst: Immutable` above. + let u = unsafe { $crate::util::macro_util::Container::transmute_from(e) }; + $crate::util::macro_util::must_use(u) + } + }} +} + /// 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 @@ -949,6 +1053,27 @@ mod tests { assert_eq!(*y, 0); } + #[test] + #[cfg(all(zerocopy_gat, feature = "alloc"))] + fn test_transmute_container() { + use alloc::{rc::Rc, sync::Arc}; + + macro_rules! test { + ($cont:ident) => { + 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: $cont<[[u8; 2]; 4]> = transmute_container!($cont::new(array_of_u8s)); + assert_eq!(*x, array_of_arrays); + let x: $cont<[u8; 8]> = transmute_container!($cont::new(array_of_arrays)); + assert_eq!(*x, array_of_u8s); + }; + } + + test!(Box); + test!(Rc); + test!(Arc); + } + #[test] fn test_macros_evaluate_args_once() { let mut ctr = 0; @@ -981,6 +1106,20 @@ mod tests { }) .unwrap(); assert_eq!(ctr, 1); + + let mut ctr = 0; + let _: Result<&usize, _> = try_transmute_ref!({ + ctr += 1; + &0usize + }); + assert_eq!(ctr, 1); + + let mut ctr: usize = 0; + let _: Result<&mut usize, _> = try_transmute_mut!({ + ctr += 1; + &mut ctr + }); + assert_eq!(ctr, 1); } #[test] diff --git a/src/util/macro_util.rs b/src/util/macro_util.rs index f7e66056d2..108b8c6e3e 100644 --- a/src/util/macro_util.rs +++ b/src/util/macro_util.rs @@ -418,6 +418,36 @@ macro_rules! assert_align_gt_eq { }}; } +#[doc(hidden)] // `#[macro_export]` bypasses this module's `#[doc(hidden)]`. +#[macro_export] +macro_rules! assert_align_eq { + ($t:ident, $u: ident) => {{ + // The comments here should be read in the context of this macro's + // invocations in `transmute_ref!` and `transmute_mut!`. + if false { + let align_t: $crate::util::macro_util::AlignOf<_> = unreachable!(); + $t = align_t.into_t(); + + let mut align_u: $crate::util::macro_util::AlignOf<_> = unreachable!(); + $u = align_u.into_t(); + + // This transmute will only compile successfully if + // `align_of::() == max(align_of::(), align_of::())` - in + // other words, if `align_of::() >= align_of::()`. + // + // SAFETY: This code is never run. + align_u = unsafe { + // Clippy: We can't annotate the types; this macro is designed + // to infer the types from the calling context. + #[allow(clippy::missing_transmute_annotations)] + $crate::util::macro_util::core_reexport::mem::transmute(align_t) + }; + } else { + loop {} + } + }}; +} + /// Do `t` and `u` have the same size? If not, this macro produces a compile /// error. It must be invoked in a dead codepath. This is used in /// `transmute_ref!` and `transmute_mut!`. @@ -683,6 +713,154 @@ pub const fn must_use(t: T) -> T { t } +#[cfg(all(zerocopy_gat, feature = "alloc"))] +mod container { + use alloc::{boxed::Box, rc::Rc, sync::Arc}; + + pub trait Container { + type Applied: Container; + type Elem; + type Raw; + + fn into_raw(slf: Self) -> Self::Raw; + + /// Constructs a new container from a pointer obtained via `into_raw`. + /// + /// # Safety + /// + /// - `raw` must have been obtained via `into_raw` on either `Self` or + /// `Self::Applied` + /// - `raw` may have been passed through `transmute_raw`, but must not + /// have been otherwise modified + /// - `Self::Elem` must have the same size, alignment, and bit validity + /// as the element type of the container that produced this `raw` via + /// `into_raw` + /// - `Self::Elem` must have `UnsafeCell`s at the same byte ranges as + /// the element type of the container that produced this `raw` via + /// `into_raw` + unsafe fn from_raw(raw: Self::Raw) -> Self; + + fn transmute_raw(raw: as Container>::Raw) -> Self::Raw; + + /// Transmutes this container from one element type to another. + /// + /// # Safety + /// + /// `T` and `Self::Elem` must have the same size, alignment, and bit + /// validity. They must have `UnsafeCell`s at the same byte ranges. + #[inline(always)] + unsafe fn transmute_from(c: Self::Applied) -> Self + where + Self: Sized, + { + let raw = as Container>::into_raw(c); + let raw = Self::transmute_raw::(raw); + + // SAFETY: + // - `raw` was obtained via `into_raw` + // - `raw` was passed through `transmute_raw`, but was not otherwise + // modified + // - The caller promises that `T` and `Self::Elem` have the same + // size, alignment, and bit validity + // - The caller promises that `T` and `Self::Elem` have + // `UnsafeCell`s at the same byte ranges + unsafe { Self::from_raw(raw) } + } + } + + impl Container for Box { + type Applied = Box; + type Elem = T; + type Raw = *mut T; + + #[inline(always)] + fn into_raw(b: Box) -> *mut T { + Box::into_raw(b) + } + + #[inline(always)] + unsafe fn from_raw(ptr: *mut T) -> Box { + unsafe { Box::from_raw(ptr) } + } + + #[inline(always)] + fn transmute_raw(raw: *mut U) -> *mut T { + raw.cast() + } + } + + impl Container for Rc { + type Applied = Rc; + type Elem = T; + type Raw = *const T; + + #[inline(always)] + fn into_raw(r: Rc) -> *const T { + Rc::into_raw(r) + } + + #[inline(always)] + unsafe fn from_raw(ptr: *const T) -> Rc { + // SAFETY: + // - The caller promises that `ptr` was obtained via `into_raw` on + // `Self` or on `Self::Applied` (which is also `Rc`). Since all + // such types implement `into_raw` as `Rc::into_raw`, `ptr` was + // obtained via `Rc::into_raw`. The caller further promises that + // `ptr` was not modified other than being passed through + // `transmute_raw` which, by the same argument, is always + // implemented as a pointer type cast which preserves referent + // bytes and provenance. + // - The caller promises that the original element type and + // `Self::Elem` have the same size, alignment, and bit validity. + // - The caller promises that the original element type and + // `Self::Elem` have `UnsafeCell`s at the same byte ranges. + unsafe { Rc::from_raw(ptr) } + } + + #[inline(always)] + fn transmute_raw(raw: *const U) -> *const T { + raw.cast() + } + } + + impl Container for Arc { + type Applied = Arc; + type Elem = T; + type Raw = *const T; + + #[inline(always)] + fn into_raw(a: Arc) -> *const T { + Arc::into_raw(a) + } + + #[inline(always)] + unsafe fn from_raw(ptr: *const T) -> Arc { + // SAFETY: + // - The caller promises that `ptr` was obtained via `into_raw` on + // `Self` or on `Self::Applied` (which is also `Arc`). Since all + // such types implement `into_raw` as `Arc::into_raw`, `ptr` was + // obtained via `Arc::into_raw`. The caller further promises that + // `ptr` was not modified other than being passed through + // `transmute_raw` which, by the same argument, is always + // implemented as a pointer type cast which preserves referent + // bytes and provenance. + // - The caller promises that the original element type and + // `Self::Elem` have the same size, alignment, and bit validity. + // - The caller promises that the original element type and + // `Self::Elem` have `UnsafeCell`s at the same byte ranges. + unsafe { Arc::from_raw(ptr) } + } + + #[inline(always)] + fn transmute_raw(raw: *const U) -> *const T { + raw.cast() + } + } +} + +#[cfg(all(zerocopy_gat, feature = "alloc"))] +pub use container::*; + // NOTE: We can't change this to a `pub use core as core_reexport` until [1] is // fixed or we update to a semver-breaking version (as of this writing, 0.8.0) // on the `main` branch.