Skip to content

Commit b134526

Browse files
joshlfjswrenn
andauthored
[macros] Support shrinking value transmutes (#2581)
* [macros] Support shrinking value transmutes In `transmute!`, support an `#![allow(shrink)]` attribute which is invoked as follows: transmute!(#![allow(shrink)] src); When this attribute is provided, `transmute!` will permit shrinking transmutes, in which the destination value may be smaller than the source value. Co-authored-by: Jack Wrenn <[email protected]> gherrit-pr-id: I46b18b4b1d10507b7e1d2e01b09dc4960cfcdce1 * Update src/macros.rs Co-authored-by: Jack Wrenn <[email protected]> --------- Co-authored-by: Jack Wrenn <[email protected]>
1 parent 053a5eb commit b134526

19 files changed

+230
-164
lines changed

src/macros.rs

Lines changed: 121 additions & 24 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,17 @@
3535
/// original `Src` will be forgotten, and the value of type `Dst` will be
3636
/// returned.
3737
///
38+
/// # `#![allow(shrink)]`
39+
///
40+
/// If `#![allow(shrink)]` is provided, `transmute!` additionally supports
41+
/// transmutations that shrink the size of the value; e.g.:
42+
///
43+
/// ```
44+
/// # use zerocopy::transmute;
45+
/// let u: u32 = transmute!(#![allow(shrink)] 0u64);
46+
/// assert_eq!(u, 0u32);
47+
/// ```
48+
///
3849
/// # Examples
3950
///
4051
/// ```
@@ -51,39 +62,117 @@
5162
/// This macro can be invoked in `const` contexts.
5263
#[macro_export]
5364
macro_rules! transmute {
54-
($e:expr) => {{
55-
// NOTE: This must be a macro (rather than a function with trait bounds)
56-
// because there's no way, in a generic context, to enforce that two
57-
// types have the same size. `core::mem::transmute` uses compiler magic
58-
// to enforce this so long as the types are concrete.
65+
// NOTE: This must be a macro (rather than a function with trait bounds)
66+
// because there's no way, in a generic context, to enforce that two types
67+
// have the same size. `core::mem::transmute` uses compiler magic to enforce
68+
// this so long as the types are concrete.
69+
(#![allow(shrink)] $e:expr) => {{
70+
let mut e = $e;
71+
if false {
72+
// This branch, though never taken, ensures that the type of `e` is
73+
// `IntoBytes` and that the type of the outer macro invocation
74+
// expression is `FromBytes`.
75+
76+
fn transmute<Src, Dst>(src: Src) -> Dst
77+
where
78+
Src: $crate::IntoBytes,
79+
Dst: $crate::FromBytes,
80+
{
81+
let _ = src;
82+
loop {}
83+
}
84+
loop {}
85+
#[allow(unreachable_code)]
86+
transmute(e)
87+
} else {
88+
use $crate::util::macro_util::core_reexport::mem::ManuallyDrop;
89+
90+
// NOTE: `repr(packed)` is important! It ensures that the size of
91+
// `Transmute` won't be rounded up to accomodate `Src`'s or `Dst`'s
92+
// alignment, which would break the size comparison logic below.
93+
//
94+
// As an example of why this is problematic, consider `Src = [u8;
95+
// 5]`, `Dst = u32`. The total size of `Transmute<Src, Dst>` would
96+
// be 8, and so we would reject a `[u8; 5]` to `u32` transmute as
97+
// being size-increasing, which it isn't.
98+
#[repr(C, packed)]
99+
union Transmute<Src, Dst> {
100+
src: ManuallyDrop<Src>,
101+
dst: ManuallyDrop<Dst>,
102+
}
59103

104+
// SAFETY: `Transmute` is a `reper(C)` union whose `src` field has
105+
// type `ManuallyDrop<Src>`. Thus, the `src` field starts at byte
106+
// offset 0 within `Transmute` [1]. `ManuallyDrop<T>` has the same
107+
// layout and bit validity as `T`, so it is sound to transmute `Src`
108+
// to `Transmute`.
109+
//
110+
// [1] https://doc.rust-lang.org/1.85.0/reference/type-layout.html#reprc-unions
111+
//
112+
// [2] Per https://doc.rust-lang.org/1.85.0/std/mem/struct.ManuallyDrop.html:
113+
//
114+
// `ManuallyDrop<T>` is guaranteed to have the same layout and bit
115+
// validity as `T`
116+
let u: Transmute<_, _> = unsafe {
117+
// Clippy: We can't annotate the types; this macro is designed
118+
// to infer the types from the calling context.
119+
#[allow(clippy::missing_transmute_annotations)]
120+
$crate::util::macro_util::core_reexport::mem::transmute(e)
121+
};
122+
123+
if false {
124+
// SAFETY: This code is never executed.
125+
e = ManuallyDrop::into_inner(unsafe { u.src });
126+
// Suppress the `unused_assignments` lint on the previous line.
127+
let _ = e;
128+
loop {}
129+
} else {
130+
// SAFETY: Per the safety comment on `let u` above, the `dst`
131+
// field in `Transmute` starts at byte offset 0, and has the
132+
// same layout and bit validity as `Dst`.
133+
//
134+
// Transmuting `Src` to `Transmute<Src, Dst>` above using
135+
// `core::mem::transmute` ensures that `size_of::<Src>() ==
136+
// size_of::<Transmute<Src, Dst>>()`. A `#[repr(C, packed)]`
137+
// union has the maximum size of all of its fields [1], so this
138+
// is equivalent to `size_of::<Src>() >= size_of::<Dst>()`.
139+
//
140+
// The outer `if`'s `false` branch ensures that `Src: IntoBytes`
141+
// and `Dst: FromBytes`. This, combined with the size bound,
142+
// ensures that this transmute is sound.
143+
//
144+
// [1] Per https://doc.rust-lang.org/1.85.0/reference/type-layout.html#reprc-unions:
145+
//
146+
// The union will have a size of the maximum size of all of
147+
// its fields rounded to its alignment
148+
let dst = unsafe { u.dst };
149+
$crate::util::macro_util::must_use(ManuallyDrop::into_inner(dst))
150+
}
151+
}
152+
}};
153+
($e:expr) => {{
60154
let e = $e;
61155
if false {
62156
// This branch, though never taken, ensures that the type of `e` is
63-
// `IntoBytes` and that the type of this macro invocation expression
64-
// is `FromBytes`.
65-
66-
struct AssertIsIntoBytes<T: $crate::IntoBytes>(T);
67-
let _ = AssertIsIntoBytes(e);
157+
// `IntoBytes` and that the type of the outer macro invocation
158+
// expression is `FromBytes`.
68159

69-
struct AssertIsFromBytes<U: $crate::FromBytes>(U);
70-
#[allow(unused, unreachable_code)]
71-
let u = AssertIsFromBytes(loop {});
72-
u.0
160+
fn transmute<Src, Dst>(src: Src) -> Dst
161+
where
162+
Src: $crate::IntoBytes,
163+
Dst: $crate::FromBytes,
164+
{
165+
let _ = src;
166+
loop {}
167+
}
168+
loop {}
169+
#[allow(unreachable_code)]
170+
transmute(e)
73171
} else {
74172
// SAFETY: `core::mem::transmute` ensures that the type of `e` and
75173
// the type of this macro invocation expression have the same size.
76174
// We know this transmute is safe thanks to the `IntoBytes` and
77175
// `FromBytes` bounds enforced by the `false` branch.
78-
//
79-
// We use this reexport of `core::mem::transmute` because we know it
80-
// will always be available for crates which are using the 2015
81-
// edition of Rust. By contrast, if we were to use
82-
// `std::mem::transmute`, this macro would not work for such crates
83-
// in `no_std` contexts, and if we were to use
84-
// `core::mem::transmute`, this macro would not work in `std`
85-
// contexts in which `core` was not manually imported. This is not a
86-
// problem for 2018 edition crates.
87176
let u = unsafe {
88177
// Clippy: We can't annotate the types; this macro is designed
89178
// to infer the types from the calling context.
@@ -92,7 +181,7 @@ macro_rules! transmute {
92181
};
93182
$crate::util::macro_util::must_use(u)
94183
}
95-
}}
184+
}};
96185
}
97186

98187
/// Safely transmutes a mutable or immutable reference of one type to an
@@ -1046,6 +1135,10 @@ mod tests {
10461135
let x: [u8; 8] = transmute!(array_of_arrays);
10471136
assert_eq!(x, array_of_u8s);
10481137

1138+
// Test that memory is transmuted as expected when shrinking.
1139+
let x: [[u8; 2]; 3] = transmute!(#![allow(shrink)] array_of_u8s);
1140+
assert_eq!(x, [[0u8, 1], [2, 3], [4, 5]]);
1141+
10491142
// Test that the source expression's value is forgotten rather than
10501143
// dropped.
10511144
#[derive(IntoBytes)]
@@ -1058,12 +1151,16 @@ mod tests {
10581151
}
10591152
#[allow(clippy::let_unit_value)]
10601153
let _: () = transmute!(PanicOnDrop(()));
1154+
#[allow(clippy::let_unit_value)]
1155+
let _: () = transmute!(#![allow(shrink)] PanicOnDrop(()));
10611156

10621157
// Test that `transmute!` is legal in a const context.
10631158
const ARRAY_OF_U8S: [u8; 8] = [0u8, 1, 2, 3, 4, 5, 6, 7];
10641159
const ARRAY_OF_ARRAYS: [[u8; 2]; 4] = [[0, 1], [2, 3], [4, 5], [6, 7]];
10651160
const X: [[u8; 2]; 4] = transmute!(ARRAY_OF_U8S);
10661161
assert_eq!(X, ARRAY_OF_ARRAYS);
1162+
const X_SHRINK: [[u8; 2]; 3] = transmute!(#![allow(shrink)] ARRAY_OF_U8S);
1163+
assert_eq!(X_SHRINK, [[0u8, 1], [2, 3], [4, 5]]);
10671164

10681165
// Test that `transmute!` works with `!Immutable` types.
10691166
let x: usize = transmute!(UnsafeCell::new(1usize));

tests/ui-msrv/include_value_not_from_bytes.stderr

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ error[E0277]: the trait bound `NotZerocopy<u32>: zerocopy::FromBytes` is not sat
44
19 | const NOT_FROM_BYTES: NotZerocopy<u32> = include_value!("../../testdata/include_value/data");
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy<u32>`
66
|
7-
note: required by `AssertIsFromBytes`
7+
note: required by a bound in `NOT_FROM_BYTES::transmute`
88
--> tests/ui-msrv/include_value_not_from_bytes.rs:19:42
99
|
1010
19 | const NOT_FROM_BYTES: NotZerocopy<u32> = include_value!("../../testdata/include_value/data");
1111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12+
| |
13+
| required by a bound in this
14+
| required by this bound in `NOT_FROM_BYTES::transmute`
1215
= note: this error originates in the macro `$crate::transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui-msrv/transmute-dst-not-frombytes.stderr

Lines changed: 4 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -4,9 +4,12 @@ error[E0277]: the trait bound `NotZerocopy: zerocopy::FromBytes` is not satisfie
44
19 | const DST_NOT_FROM_BYTES: NotZerocopy = transmute!(AU16(0));
55
| ^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy`
66
|
7-
note: required by `AssertIsFromBytes`
7+
note: required by a bound in `DST_NOT_FROM_BYTES::transmute`
88
--> tests/ui-msrv/transmute-dst-not-frombytes.rs:19:41
99
|
1010
19 | const DST_NOT_FROM_BYTES: NotZerocopy = transmute!(AU16(0));
1111
| ^^^^^^^^^^^^^^^^^^^
12+
| |
13+
| required by a bound in this
14+
| required by this bound in `DST_NOT_FROM_BYTES::transmute`
1215
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui-msrv/transmute-ptr-to-usize.stderr

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,12 @@ error[E0277]: the trait bound `*const usize: IntoBytes` is not satisfied
44
20 | const POINTER_VALUE: usize = transmute!(&0usize as *const usize);
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoBytes` is not implemented for `*const usize`
66
|
7-
note: required by `AssertIsIntoBytes`
7+
note: required by a bound in `POINTER_VALUE::transmute`
88
--> tests/ui-msrv/transmute-ptr-to-usize.rs:20:30
99
|
1010
20 | const POINTER_VALUE: usize = transmute!(&0usize as *const usize);
1111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12-
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
13-
14-
error[E0277]: the trait bound `*const usize: IntoBytes` is not satisfied
15-
--> tests/ui-msrv/transmute-ptr-to-usize.rs:20:30
16-
|
17-
20 | const POINTER_VALUE: usize = transmute!(&0usize as *const usize);
18-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `IntoBytes` is not implemented for `*const usize`
19-
|
20-
note: required by a bound in `AssertIsIntoBytes`
21-
--> tests/ui-msrv/transmute-ptr-to-usize.rs:20:30
22-
|
23-
20 | const POINTER_VALUE: usize = transmute!(&0usize as *const usize);
24-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `AssertIsIntoBytes`
12+
| |
13+
| required by a bound in this
14+
| required by this bound in `POINTER_VALUE::transmute`
2515
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1 @@
1+
../ui-nightly/transmute-size-increase-allow-shrink.rs
Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
error[E0512]: cannot transmute between types of different sizes, or dependently-sized types
2+
--> tests/ui-msrv/transmute-size-increase-allow-shrink.rs:20:29
3+
|
4+
20 | const INCREASE_SIZE: AU16 = transmute!(#![allow(shrink)] 0u8);
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6+
|
7+
= note: source type: `u8` (8 bits)
8+
= note: target type: `Transmute<u8, AU16>` (16 bits)
9+
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui-msrv/transmute-src-not-intobytes.stderr

Lines changed: 4 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -4,22 +4,12 @@ error[E0277]: the trait bound `NotZerocopy<AU16>: zerocopy::IntoBytes` is not sa
44
19 | const SRC_NOT_AS_BYTES: AU16 = transmute!(NotZerocopy(AU16(0)));
55
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::IntoBytes` is not implemented for `NotZerocopy<AU16>`
66
|
7-
note: required by `AssertIsIntoBytes`
7+
note: required by a bound in `SRC_NOT_AS_BYTES::transmute`
88
--> tests/ui-msrv/transmute-src-not-intobytes.rs:19:32
99
|
1010
19 | const SRC_NOT_AS_BYTES: AU16 = transmute!(NotZerocopy(AU16(0)));
1111
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
12-
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)
13-
14-
error[E0277]: the trait bound `NotZerocopy<AU16>: zerocopy::IntoBytes` is not satisfied
15-
--> tests/ui-msrv/transmute-src-not-intobytes.rs:19:32
16-
|
17-
19 | const SRC_NOT_AS_BYTES: AU16 = transmute!(NotZerocopy(AU16(0)));
18-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::IntoBytes` is not implemented for `NotZerocopy<AU16>`
19-
|
20-
note: required by a bound in `AssertIsIntoBytes`
21-
--> tests/ui-msrv/transmute-src-not-intobytes.rs:19:32
22-
|
23-
19 | const SRC_NOT_AS_BYTES: AU16 = transmute!(NotZerocopy(AU16(0)));
24-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `AssertIsIntoBytes`
12+
| |
13+
| required by a bound in this
14+
| required by this bound in `SRC_NOT_AS_BYTES::transmute`
2515
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui-nightly/include_value_not_from_bytes.stderr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ error[E0277]: the trait bound `NotZerocopy<u32>: zerocopy::FromBytes` is not sat
22
--> tests/ui-nightly/include_value_not_from_bytes.rs:19:42
33
|
44
19 | const NOT_FROM_BYTES: NotZerocopy<u32> = include_value!("../../testdata/include_value/data");
5-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
6-
| |
7-
| the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy<u32>`
8-
| required by a bound introduced by this call
5+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy<u32>`
96
|
107
= note: Consider adding `#[derive(FromBytes)]` to `NotZerocopy<u32>`
118
= help: the following other types implement trait `zerocopy::FromBytes`:
@@ -18,9 +15,12 @@ error[E0277]: the trait bound `NotZerocopy<u32>: zerocopy::FromBytes` is not sat
1815
AtomicIsize
1916
AtomicU16
2017
and $N others
21-
note: required by a bound in `AssertIsFromBytes`
18+
note: required by a bound in `NOT_FROM_BYTES::transmute`
2219
--> tests/ui-nightly/include_value_not_from_bytes.rs:19:42
2320
|
2421
19 | const NOT_FROM_BYTES: NotZerocopy<u32> = include_value!("../../testdata/include_value/data");
25-
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^ required by this bound in `AssertIsFromBytes`
22+
| ^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^^
23+
| |
24+
| required by a bound in this function
25+
| required by this bound in `transmute`
2626
= note: this error originates in the macro `$crate::transmute` which comes from the expansion of the macro `include_value` (in Nightly builds, run with -Z macro-backtrace for more info)

tests/ui-nightly/transmute-dst-not-frombytes.stderr

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,7 @@ error[E0277]: the trait bound `NotZerocopy: zerocopy::FromBytes` is not satisfie
22
--> tests/ui-nightly/transmute-dst-not-frombytes.rs:19:41
33
|
44
19 | const DST_NOT_FROM_BYTES: NotZerocopy = transmute!(AU16(0));
5-
| ^^^^^^^^^^^^^^^^^^^
6-
| |
7-
| the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy`
8-
| required by a bound introduced by this call
5+
| ^^^^^^^^^^^^^^^^^^^ the trait `zerocopy::FromBytes` is not implemented for `NotZerocopy`
96
|
107
= note: Consider adding `#[derive(FromBytes)]` to `NotZerocopy`
118
= help: the following other types implement trait `zerocopy::FromBytes`:
@@ -18,9 +15,12 @@ error[E0277]: the trait bound `NotZerocopy: zerocopy::FromBytes` is not satisfie
1815
AtomicIsize
1916
AtomicU16
2017
and $N others
21-
note: required by a bound in `AssertIsFromBytes`
18+
note: required by a bound in `DST_NOT_FROM_BYTES::transmute`
2219
--> tests/ui-nightly/transmute-dst-not-frombytes.rs:19:41
2320
|
2421
19 | const DST_NOT_FROM_BYTES: NotZerocopy = transmute!(AU16(0));
25-
| ^^^^^^^^^^^^^^^^^^^ required by this bound in `AssertIsFromBytes`
22+
| ^^^^^^^^^^^^^^^^^^^
23+
| |
24+
| required by a bound in this function
25+
| required by this bound in `transmute`
2626
= note: this error originates in the macro `transmute` (in Nightly builds, run with -Z macro-backtrace for more info)

0 commit comments

Comments
 (0)