Skip to content

Commit b9e6849

Browse files
committed
Add next_gen_transmute RFC
1 parent b145b2e commit b9e6849

File tree

1 file changed

+261
-0
lines changed

1 file changed

+261
-0
lines changed

text/3844-next-gen-transmute.md

Lines changed: 261 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,261 @@
1+
- Feature Name: `next_gen_transmute`
2+
- Start Date: (fill me in with today's date, YYYY-MM-DD)
3+
- RFC PR: [rust-lang/rfcs#0000](https://github.com/rust-lang/rfcs/pull/0000)
4+
- Rust Issue: [rust-lang/rust#0000](https://github.com/rust-lang/rust/issues/0000)
5+
6+
# Summary
7+
[summary]: #summary
8+
9+
Change `mem::transmute` from having a magic size check to having an ordinary
10+
`const { … }`-enforced check plus some normal lints.
11+
12+
Add a `mem::union_transmute` for an even-less-restricted transmute where size
13+
mismatches are both allowed and not (necessarily) UB.
14+
15+
16+
# Motivation
17+
[motivation]: #motivation
18+
19+
From [#106281](https://github.com/rust-lang/rust/pull/106281#issuecomment-1496648190):
20+
21+
> Many valid (but not provably so) transmute calls are currently rejected by the compiler's checks, pushing folks to less ergonomic options like transmute_copy or pointer casts.
22+
23+
Today, the one-liner when the compiler doesn't let you `transmute` is to instead do
24+
25+
```rust
26+
mem::transmute_copy(&mem::ManuallyDrop(other))
27+
```
28+
29+
But that's not great. It doesn't communicate that the programmer *expected* the size
30+
to match, and thus there's no opportunity for the compiler to help catch a mistaken
31+
expectation. Plus it obfuscates other locations that really do want `transmute_copy`,
32+
perhaps because they're intentionally reading a prefix out of something.
33+
34+
It would be nice to move `mem::transmute` to being a normal function -- not the one
35+
intrinsic we let people call directly -- in a way that it can be more flexible for
36+
users as well as easier to update in the compiler without semver worries.
37+
38+
39+
# Guide-level explanation
40+
[guide-level-explanation]: #guide-level-explanation
41+
42+
## `union_transmute`
43+
44+
The `union_transmute` function is a general way to reinterpret the byte representation
45+
of one type as a different type. This is equivalent to writing and reading through
46+
a `union`, like
47+
48+
```rust
49+
const unsafe fn union_transmute<T, U>(t: T) -> U {
50+
#[repr(C)]
51+
union Transmute<A, B> {
52+
a: ManuallyDrop<A>,
53+
b: ManuallyDrop<B>,
54+
}
55+
let u = unsafe {
56+
Transmute { a: ManuallyDrop::new(t) }.b
57+
};
58+
ManuallyDrop::into_inner(u)
59+
}
60+
```
61+
62+
or to copying over the common prefix length like
63+
64+
```rust
65+
const unsafe fn union_transmute<T, U>(t: T) -> U {
66+
let mut u = MaybeUninit::<U>::uninit();
67+
unsafe {
68+
let bytes = Ord::min(size_of::<T>(), size_of::<U>());
69+
ptr::copy_nonoverlapping::<u8>((&raw const t).cast(), u.as_mut_ptr().cast(), bytes);
70+
u.assume_init()
71+
}
72+
}
73+
```
74+
75+
You might have heard this referred to as "type punning" or similar as well.
76+
It's also a very similar operation to what you can get by casting pointers,
77+
though because it's by *value* you don't need to worry about the alignments of
78+
`T` and `U` the way you would when doing something like
79+
`(&raw const x).cast::<U>().read()`.
80+
81+
This is incredibly `unsafe` because nearly all combinations of types are going
82+
to be immediately UB. For example, `union_transmute::<u32, u64>(x)` is always
83+
UB because half of the value being read is uninitialized.
84+
85+
However, it's still useful to have this available as the fully-general operation
86+
for those cases where it's useful. For example, it's sound to use it for
87+
`union_transmute::<[T; BIG], [T; SMALL]>(…)` to read a prefix of an array.
88+
Or a SIMD type like `#[repr(C, align(16))] struct AlignedF32x3(Simd<f32, 3>);`
89+
can (at least based on the current plans for `Simd`) soundly then `union_transmute`
90+
back and forth between `AlignedF32x3` and `[f32; 3]`, despite their different sizes.
91+
92+
And of course some things are trivially sound, like `union_transmute::<T, ()>`
93+
as that would read zero bytes, which of course works. (There's no need to use
94+
`union_transmute` for that, though, since it's better spelled `mem::forget`.)
95+
96+
## `transmute`
97+
98+
The `transmute` function does the same thing as `union_transmute` when it compiles,
99+
but adds the restriction that the input and output types must have the same size.
100+
101+
It's essentially this:
102+
```rust
103+
const unsafe fn transmute<T, U>(t: T) -> U {
104+
const { assert!(size_of::<T>() == size_of::<U>()) };
105+
union_transmute<T, U>(t)
106+
}
107+
```
108+
109+
This has its own name because it's particularly common that when transmuting
110+
you're *expecting* the two types to be the same size, and it's helpful both to
111+
communicate that to the reader and let the compiler help double-check it.
112+
113+
For example, `transmute::<[u32; N], [u64; M]>` is only going to be sound when
114+
the sizes match (aka when M = 2×N), so might as well have that checked at
115+
compile-time instead of potentially letting something unsound sneak in.
116+
117+
Using a const-assert this way does mean that some calls to `transmute` that can
118+
never actually work will not be rejected at declaration time, only sometime later
119+
when the function in question is actually used by something else.
120+
121+
To mitigate that, there's a number of lints:
122+
123+
- `deny`-by-default lints for things where the compiler knows the sizes are
124+
different, such as `transmute::<u32, u64>`.
125+
- `warn`-by-default lints for things where it's *possible* that the size will
126+
match, but it's still suspicious, such as `transmute::<[u32; N], u32>` or
127+
`transmute::<[u64; N], [u32; N]>` where only one monomorphization can work.
128+
129+
The full complement of such lints is not listed here as they're regularly updated
130+
to catch more definitely-wrong cases and be smart enough to prove more things
131+
as *not* being suspicious.
132+
133+
> 📜 Historical Note 📜
134+
>
135+
> In previous versions of rust, `transmute` was actually a hard error when the
136+
> compiler couldn't *prove* that the types were the same size. This was limiting
137+
> in practice, as humans are smarter than the rules we're willing to run during
138+
> type checking -- this is why `unsafe` code exists at all, really -- and meant
139+
> that people needed workarounds.
140+
>
141+
> All those cases that were previously caught produce lints instead, now, with
142+
> the possible exception of things that were errors before only from the compiler
143+
> being insufficiently smart. For example, `transmute::<[[u32; N]; 2], [u64; N]>`
144+
> was previously rejected despite those two types always having the same size
145+
> for any monomorphization, so it might not lint now.
146+
147+
148+
# Reference-level explanation
149+
[reference-level-explanation]: #reference-level-explanation
150+
151+
Because lint details are non-normative, in some sense the implementation is trivial.
152+
Just add `union_transmute` and change `transmute` as above in `core`.
153+
154+
## Possible implementation approach
155+
156+
Today we already have a `transmute_unchecked` intrinsic in rustc which doesn't
157+
have a compile-time size check, but *is* still documented as UB if the sizes
158+
don't match. That intrinsic can be changed to be defined as the union version,
159+
perhaps with fallback MIR using that exact definition, and used to implement
160+
both of the library functions. Those library functions would probably also
161+
add some UbChecks (which aren't possible today as `mem::transmute` is a
162+
re-exported intrinsic, not an actual function).
163+
164+
The change to make `mem::transmute` an ordinary function would need to update
165+
the existing `check_transmutes` in typeck to instead be a lint that looks for
166+
calls to `#[rustc_diagnostic_item = "transmute"]` instead. (That diagnostic
167+
item already exists.) For starters the same logic could be used as a
168+
deny-be-default lint, as the most similar diagnostics, with any splitting done
169+
separately over time at the discretion of the diagnostics experts.
170+
171+
This should be straight-forward in CTFE and codegen as well. Once lowered such
172+
that we have locals for the source and destination, this can be implemented by
173+
just copying over the number of bytes in the shorter value (plus uninitializing
174+
the upper bytes in the target, if it's bigger).
175+
176+
For example, in cg_clif the general-case copy currently uses the destination size
177+
<https://github.com/rust-lang/rust/blob/6d091b2baa33698682453c7bb72809554204e434/compiler/rustc_codegen_cranelift/src/value_and_place.rs#L641>
178+
but it could use the min of the source and destination size.
179+
180+
In cg_ssa the general case for SSA values actually already supports this
181+
<https://github.com/rust-lang/rust/blob/6d091b2baa33698682453c7bb72809554204e434/compiler/rustc_codegen_ssa/src/mir/rvalue.rs#L309-L316>
182+
and thus would just need the earlier "just emit `unreachable` if the sizes don't
183+
match" check removed to reach it.
184+
185+
There are some other cases that will need more care, like transmuting a larger
186+
`BackendRepr::Scalar` to a smaller `BackendRepr::Memory` where the current code
187+
would do a OOB write if unchanged, but nothing particularly troublesome is expected.
188+
189+
The internal changes (to codegen and similar) would probably happen first so they
190+
could be implemented and tested before doing the publicly-visible switchover.
191+
192+
193+
# Drawbacks
194+
[drawbacks]: #drawbacks
195+
196+
- The more transmute-related functions we add the more people might feel encouraged
197+
to use them, even if we'd rather not.
198+
- Lots of people don't like post-mono errors, and would rather Rust never have them.
199+
- This is still massively-unsound, so doesn't solve the biggest problems.
200+
- Weird crimes using transmute to check sizes without ever actually running the
201+
transmute might not get caught by the linting or post-mono check.
202+
203+
204+
# Rationale and alternatives
205+
[rationale-and-alternatives]: #rationale-and-alternatives
206+
207+
## Aren't the hard errors better than post-mono ones, on transmute?
208+
209+
Well, there's two big reasons to prefer post-mono here:
210+
211+
1. By being post-mono, it's 100% accurate. Sure, if we could easily be perfectly
212+
accurate earlier in the pipeline that would be nice, but since it's layout-based
213+
that's extremely difficult at best because of layering. (For anything related
214+
to coroutines that's particularly bad.)
215+
2. By being *hard* errors, rather than lints, there's a bunch more breaking change
216+
concerns. Any added smarts that allow something to compile need to be around
217+
*forever* (as removing them would be breaking), and similarly the exact details
218+
of those checks need to be updated in the specification. Those changes also
219+
impact the MSRV of libraries that depend on them. Whereas putting those smarts
220+
into *lints* instead mean that cap-lints applies and we can detect new issues
221+
without worrying about back-compat. And the details of lints don't need to be
222+
written down in the specification either.
223+
224+
## Do we really need the `union_transmute`?
225+
226+
Not strictly, no. We could continue to say that the only "official" kind of
227+
transmute is one where the sizes are definitely equal at runtime.
228+
229+
That said, the SIMD case was most persuasive to the RFC author. If we chose to
230+
offer an `AlignedSimd<T, N>` that rounds up to some implementation-chosen multiple
231+
in order to offer more alignment (than a `PackedSimd<T, N>` would), it would be
232+
quite convenient to have a name and primitive operation for the kind of transmute
233+
that would work for both directions of `[T; N]``AlignedSimd<T, N>`.
234+
235+
Plus using `union`s for type punning like this is something that people already
236+
do, so having a name for it helps make what's happening more obvious, plus gives
237+
a place for us to provider better documentation and linting when they do use it.
238+
239+
240+
# Prior art
241+
[prior-art]: #prior-art
242+
243+
Unknown.
244+
245+
246+
# Unresolved questions
247+
[unresolved-questions]: #unresolved-questions
248+
249+
During implementation:
250+
- Should MIR's `CastKind::Transmute` retain its equal-size precondition?
251+
252+
For nightly and continuing after stabilization:
253+
- What exactly are the correct lints to have about these functions?
254+
255+
256+
# Future possibilities
257+
[future-possibilities]: #future-possibilities
258+
259+
Nothing new foreseen. Hopefully the safe-transmute project will continue to
260+
make progress and help people use `mem::(union_)transmute` less going forward.
261+

0 commit comments

Comments
 (0)