707: `std::mem::transmute` — Reinterpreting Bytes
Difficulty: 4 Level: Expert Reinterpret a value's bits as a different type — the nuclear option of unsafe Rust.The Problem This Solves
Sometimes you genuinely need to look at the same bits through a different lens: inspect the IEEE-754 bit pattern of a float, reinterpret a `&str` fat pointer as `&[u8]`, or work with a C library that passes type-erased void pointers. Safe Rust has no way to do this directly — the type system prevents you from pretending a `f32` is a `u32`. `mem::transmute<T, U>` tells the compiler: "Take these bits, call them type `U` instead of type `T`, and don't generate any conversion code." It is the most dangerous function in the standard library. Misuse causes undefined behaviour immediately — not a panic, not a crash you can catch, but silent corruption or security vulnerabilities. Almost every use case has a safer, named alternative. Reach for `transmute` only after confirming no safer alternative exists. unsafe is a tool, not a crutch — use only when safe Rust genuinely can't express the pattern.The Intuition
Transmute is bitwise reinterpretation. The bits in memory stay exactly the same; only the type label changes. The compiler enforces one compile-time rule: `size_of::<T>() == size_of::<U>()`. Everything else — alignment, validity invariants, aliasing rules, lifetime correctness — is your responsibility. The canonical safer alternatives:- Float bits → integer: `f32::to_bits()` / `f32::from_bits()`
- `&str` → `&[u8]`: `.as_bytes()`
- Lifetime extension: use arena allocators or `Rc<T>` instead
- Type erasure: `Box<dyn Any>` or `NonNull<c_void>`
How It Works in Rust
use std::mem;
// ── Safe alternative is always preferred ────────────────────────────────
fn f32_to_u32_safe(f: f32) -> u32 { f.to_bits() } // use this
// ── Transmute: same result, higher risk ─────────────────────────────────
fn f32_to_u32_transmute(f: f32) -> u32 {
unsafe {
// SAFETY: f32 and u32 both have size 4 and align 4 on all targets;
// every bit pattern of u32 is a valid u32.
mem::transmute::<f32, u32>(f)
}
}
// ── Size mismatch is a compile-time error ────────────────────────────────
// let _: u32 = unsafe { mem::transmute::<u8, u32>(0u8) }; // ERROR
// ── Lifetime extension — extremely dangerous ────────────────────────────
/// # Safety
/// Caller must guarantee the referent is valid for the entire lifetime 'b.
unsafe fn extend_lifetime<'a, 'b, T>(r: &'a T) -> &'b T {
// SAFETY: Caller contract — almost always a design smell.
// Prefer arena allocators (bumpalo) or Rc/Arc instead.
mem::transmute::<&'a T, &'b T>(r)
}
The size check is a compile-time guard: `const _: () = assert!(mem::size_of::<A>() == mem::size_of::<B>());`. Add this assertion next to any transmute to document the invariant explicitly.
What This Unlocks
- Low-level bit manipulation — SIMD type punning, IEEE-754 sign bit extraction, and hardware register interpretation.
- FFI type erasure — convert between `*mut c_void` and a typed pointer when the C API demands it and no safe cast exists.
- Zero-copy deserialization — reinterpret a validated byte buffer as a `#[repr(C)]` struct (with careful alignment and validity proofs).
Key Differences
| Concept | OCaml | Rust |
|---|---|---|
| Bit reinterpretation | `Obj.magic` (always unsafe, no size check) | `mem::transmute` (compile error if sizes differ) |
| Float bits | No built-in; need `Int32.bits_of_float` (Obj.magic) | `f32::to_bits()` (safe) or `mem::transmute` (unsafe) |
| &str as bytes | `Bytes.unsafe_of_string` | `.as_bytes()` (safe) or transmute (unsafe) |
| Lifetime extension | Not a typed concept | Lifetime parameter transmute — extremely dangerous |
| Safety default | No safeguards | Compiler blocks mismatched sizes; auditor blocks the rest |
//! 707 — std::mem::transmute: Reinterpreting Bytes
//! Safe-alternative-first approach; transmute only when necessary.
use std::mem;
// ── 1. Float ↔ integer bits (prefer safe API) ────────────────────────────
fn f32_to_u32_transmute(f: f32) -> u32 {
unsafe {
// SAFETY: f32 and u32 both have size 4 and align 4 on all targets;
// every bit pattern of u32 is a valid u32 (no validity invariants).
mem::transmute::<f32, u32>(f)
}
}
fn f32_to_u32_safe(f: f32) -> u32 { f.to_bits() } // prefer this
// ── 2. &str → &[u8] (prefer .as_bytes()) ─────────────────────────────────
fn str_as_bytes(s: &str) -> &[u8] {
unsafe {
// SAFETY: &str is layout-compatible with &[u8] (fat pointer: ptr + len).
// The bytes of valid UTF-8 are valid u8 values. Prefer s.as_bytes().
mem::transmute::<&str, &[u8]>(s)
}
}
// ── 3. Extending a lifetime (VERY DANGEROUS — shown for pedagogy) ─────────
/// Extend the lifetime of a reference.
/// Almost always a design smell — prefer arena allocators or Rc.
///
/// # Safety
/// Caller must guarantee the referent is valid for the entire lifetime 'b.
#[allow(dead_code)]
unsafe fn extend_lifetime<'a, 'b, T>(r: &'a T) -> &'b T {
// SAFETY: Caller contract documented above.
mem::transmute::<&'a T, &'b T>(r)
}
// ── Compile-time size assertion ───────────────────────────────────────────
const _: () = assert!(mem::size_of::<f32>() == mem::size_of::<u32>());
fn main() {
let pi: f32 = std::f32::consts::PI;
let t = f32_to_u32_transmute(pi);
let s = f32_to_u32_safe(pi);
println!("transmute: {:#010x}", t);
println!("to_bits: {:#010x}", s);
println!("Equal: {}", t == s);
let text = "hello, Rust!";
let via_transmute = str_as_bytes(text);
let via_safe = text.as_bytes();
println!("Bytes match: {}", via_transmute == via_safe);
// Size mismatch would be a COMPILE ERROR:
// let _: u32 = unsafe { mem::transmute::<u8, u32>(0u8) }; // error
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_float_transmute_matches_to_bits() {
for &f in &[0.0f32, 1.0, -1.0, f32::INFINITY, 1.234] {
assert_eq!(f32_to_u32_transmute(f), f32_to_u32_safe(f));
}
}
#[test]
fn test_str_bytes() {
let s = "abcde";
assert_eq!(str_as_bytes(s), b"abcde");
}
#[test]
fn test_nan_transmute() {
let nan = f32::NAN;
assert_eq!(f32_to_u32_transmute(nan), f32_to_u32_safe(nan));
}
}
(* OCaml: Obj.magic is the transmute equivalent — equally dangerous.
Always prefer typed conversions. *)
(** Float to bits — idiomatic safe way. *)
let float_to_bits (f : float) : int64 = Int64.bits_of_float f
let bits_to_float (b : int64) : float = Int64.float_of_bits b
(** int32 byte view via Bytes — safe byte manipulation. *)
let int32_to_bytes_le (n : int32) : bytes =
let b = Bytes.create 4 in
Bytes.set_int32_le b 0 n;
b
let () =
let pi = Float.pi in
Printf.printf "pi bits: 0x%Lx\n" (float_to_bits pi);
Printf.printf "round-trip: %f (equal: %b)\n"
(bits_to_float (float_to_bits pi))
(pi = bits_to_float (float_to_bits pi));
let bytes = int32_to_bytes_le 0x12345678l in
print_string "int32 LE bytes: ";
Bytes.iter (fun c -> Printf.printf "%02x " (Char.code c)) bytes;
print_newline ()