Skip to content

Commit

Permalink
Add transmute_container! macro
Browse files Browse the repository at this point in the history
  • Loading branch information
joshlf committed Sep 27, 2024
1 parent 6b7a012 commit d54acba
Show file tree
Hide file tree
Showing 4 changed files with 336 additions and 0 deletions.
26 changes: 26 additions & 0 deletions .github/workflows/ci.yml
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down Expand Up @@ -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"
Expand All @@ -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"
Expand Down Expand Up @@ -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
Expand Down
3 changes: 3 additions & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -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"

Expand Down
139 changes: 139 additions & 0 deletions src/macros.rs
Original file line number Diff line number Diff line change
Expand Up @@ -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: $crate::util::macro_util::Container>(_c: C) -> C::Elem {
loop {}
}

fn from_elem<C: $crate::util::macro_util::Container>(_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: ::core::marker::Sized>(T);
struct AssertSrcIsFromBytes<T: ?::core::marker::Sized + $crate::FromBytes>(T);
struct AssertSrcIsIntoBytes<T: ?::core::marker::Sized + $crate::IntoBytes>(T);
struct AssertSrcIsImmutable<T: ?::core::marker::Sized + $crate::Immutable>(T);
struct AssertDstIsSized<T: ::core::marker::Sized>(T);
struct AssertDstIsFromBytes<T: ?::core::marker::Sized + $crate::FromBytes>(T);
struct AssertDstIsIntoBytes<T: ?::core::marker::Sized + $crate::IntoBytes>(T);
struct AssertDstIsImmutable<T: ?::core::marker::Sized + $crate::Immutable>(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::<T>() ==
// size_of::<U>()` and that that `align_of::<T>() >=
// align_of::<U>()`.

// `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::<Src>() == size_of::<Dst>()` thanks to
// the use of `assert_size_eq!` above.
// - We know that `align_of::<Src>() == align_of::<Dst>()` 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
Expand Down Expand Up @@ -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;
Expand Down Expand Up @@ -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]
Expand Down
Loading

0 comments on commit d54acba

Please sign in to comment.