How do I work with mem::transmute in Rust?

Walkthrough

mem::transmute<T, U> reinterprets the bits of a value of type T as type U. It's one of the most powerful and dangerous operations in Rust, completely bypassing the type system. Both types must have the same size.

Key concepts:

  • Bit-level reinterpretation — Raw bits are reinterpreted as another type
  • Same size required — T and U must have identical sizes (compile-time enforced)
  • Extremely unsafe — No type checking, no validation
  • Undefined behavior risk — Easy to create UB with transmute

When transmute is appropriate:

  • Converting between types with identical layouts
  • FFI type punning (when documented as safe)
  • Implementing low-level abstractions
  • Converting function pointer types

Safer alternatives to prefer:

  • as for numeric conversions
  • From/Into for type conversions
  • try_from/try_into for fallible conversions
  • transmute_copy when sizes differ
  • bytemuck crate for safe transmutations
  • zerocopy crate for safe reinterpretation

Code Examples

Basic transmute Usage (Unsafe!)

use std::mem;
 
fn main() {
    // Transmute between same-sized types
    let bytes: [u8; 4] = [0x12, 0x34, 0x56, 0x78];
    
    // Reinterpret bytes as u32 (little-endian systems)
    let number: u32 = unsafe { mem::transmute(bytes) };
    println!("Bytes {:?} as u32: {}", bytes, number);
    
    // Transmute back
    let back_to_bytes: [u8; 4] = unsafe { mem::transmute(number) };
    println!("u32 {} back to bytes: {:?}", number, back_to_bytes);
}

Safe Alternative: as Cast

fn main() {
    // Instead of transmute for numeric types, use as:
    let a: i32 = 42;
    let b: u32 = a as u32;  // Safe, defined behavior
    
    // Or use From/Into for known-safe conversions
    let c: u32 = i32::from_ne_bytes([0x12, 0x34, 0x56, 0x78]);
    let d: [u8; 4] = c.to_ne_bytes();
    
    println!("i32: {}, u32: {}, bytes: {:?}", a, b, d);
}

Transmuting Floats and Integers

use std::mem;
 
fn main() {
    // Transmute f64 to u64 (same size)
    let float_val: f64 = 3.14159;
    let bits: u64 = unsafe { mem::transmute(float_val) };
    println!("f64 {} as bits: {:#016x}", float_val, bits);
    
    // Transmute back
    let back: f64 = unsafe { mem::transmute(bits) };
    println!("Bits back to f64: {}", back);
    
    // Safer alternative: use to_bits/from_bits
    let safe_bits = float_val.to_bits();
    let safe_back = f64::from_bits(safe_bits);
    println!("Safe conversion: {}", safe_back);
}

Transmuting References

use std::mem;
 
fn main() {
    // Transmute &T to &U (same layout required!)
    let slice: &[u8] = &[0x01, 0x02, 0x03, 0x04];
    
    // WARNING: This is only safe if the layouts are identical!
    // &[u8; 4] and &[u8] have DIFFERENT layouts - this would be UB!
    
    // Safe example: &[u8; 4] to &[u32; 1] (both are 4 bytes + metadata)
    let arr: [u8; 4] = [0x01, 0x02, 0x03, 0x04];
    let arr_ref: &[u8; 4] = &arr;
    
    // This transmutes the reference, not the data
    // DANGEROUS: alignment and endianness matter!
    // unsafe {
    //     let u32_ref: &[u32; 1] = mem::transmute(arr_ref);
    // }
    
    // Safer approach: use try_into or explicit conversion
    let u32_val = u32::from_ne_bytes(*arr_ref);
    println!("Converted: {}", u32_val);
}

Transmuting Function Pointers

use std::mem;
 
fn add(a: i32, b: i32) -> i32 {
    a + b
}
 
fn multiply(a: i32, b: i32) -> i32 {
    a * b
}
 
fn main() {
    // Function pointer types
    type BinaryOp = fn(i32, i32) -> i32;
    
    let op: BinaryOp = add;
    println!("add(2, 3) = {}", op(2, 3));
    
    // Transmuting function pointers is dangerous but sometimes needed for FFI
    // The ABI must be compatible!
    
    // Example: Converting to a "generic" function pointer
    type GenericFn = unsafe extern "C" fn();
    
    // WARNING: Calling through this requires knowing the actual signature
    // unsafe {
    //     let generic: GenericFn = mem::transmute(op);
    //     // Must transmute back to correct type to call safely
    // }
}

Transmute for Type Punning (FFI)

use std::mem;
use std::ffi::c_void;
 
// FFI structures with overlapping layouts
#[repr(C)]
struct sockaddr_in {
    sin_family: u16,
    sin_port: u16,
    sin_addr: u32,
    sin_zero: [u8; 8],
}
 
#[repr(C)]
struct sockaddr {
    sa_family: u16,
    sa_data: [u8; 14],
}
 
// FFI often requires casting between related types
// In C: (struct sockaddr *)&addr_in
// In Rust: need to ensure layouts match!
 
fn main() {
    let addr_in = sockaddr_in {
        sin_family: 2,
        sin_port: 8080u16.to_be(),
        sin_addr: 0x7f000001,  // 127.0.0.1
        sin_zero: [0; 8],
    };
    
    // Both structs are 16 bytes
    println!("sockaddr_in size: {}", mem::size_of::<sockaddr_in>());
    println!("sockaddr size: {}", mem::size_of::<sockaddr>());
    
    // For FFI, this transmute is sometimes necessary
    // unsafe {
    //     let addr: sockaddr = mem::transmute(addr_in);
    // }
    
    // Safer: use a union or explicit conversion
}

Transmute Copy for Different Sizes

use std::mem;
 
fn main() {
    // transmute requires same size
    // transmute_copy copies bytes to a new value
    
    let small: [u8; 2] = [0x12, 0x34];
    let large: [u8; 4] = [0x00, 0x00, 0x00, 0x00];
    
    // Can't use transmute - sizes differ!
    // let x: [u8; 4] = unsafe { mem::transmute(small) };  // Won't compile
    
    // transmute_copy reads size_of::<U>() bytes
    // BE CAREFUL: reads beyond source if U is larger!
    let source = [0x12u8, 0x34, 0x56, 0x78];
    let dest: u32 = unsafe { mem::transmute_copy(&source) };
    println!("transmute_copy result: {}", dest);
}

Transmute with Slices

use std::mem;
 
fn main() {
    // Convert slice of one type to slice of another
    // This requires careful size and alignment handling!
    
    let bytes: [u8; 8] = [0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07, 0x08];
    
    // Convert &[u8; 8] to &[u32; 2] (both 8 bytes)
    // But requires proper alignment!
    
    // Using from_raw_parts for slice conversion
    let u32_slice: &[u32] = unsafe {
        std::slice::from_raw_parts(
            bytes.as_ptr() as *const u32,
            bytes.len() / 4,
        )
    };
    
    // Note: This may cause UB if alignment is wrong!
    // Prefer safe alternatives when possible
    
    println!("u32 slice len: {}", u32_slice.len());
}

Safe Transmute with bytemuck Crate

// Add to Cargo.toml: bytemuck = "1.0"
 
fn main() {
    // bytemuck provides safe transmute for Pod (Plain Old Data) types
    use bytemuck::{Pod, Zeroable, cast, try_cast};
    
    // Safe transmute using bytemuck
    let bytes: [u8; 4] = [0x12, 0x34, 0x56, 0x78];
    
    // bytemuck::cast is safe for Pod types
    let number: u32 = cast(bytes);
    println!("bytemuck cast: {}", number);
    
    // Can also try_cast which returns Result
    match try_cast::<[u8; 4], u32>(bytes) {
        Ok(n) => println!("Success: {}", n),
        Err(_) => println!("Cast failed"),
    }
}

Transmute for Trait Objects

use std::mem;
 
trait Animal {
    fn speak(&self);
}
 
struct Dog;
impl Animal for Dog {
    fn speak(&self) {
        println!("Woof!");
    }
}
 
struct Cat;
impl Animal for Cat {
    fn speak(&self) {
        println!("Meow!");
    }
}
 
fn main() {
    // Trait object representation: (data_ptr, vtable_ptr)
    let dog: Box<dyn Animal> = Box::new(Dog);
    
    // The trait object is a fat pointer (2 pointers)
    println!("Size of &dyn Animal: {}", mem::size_of::<&dyn Animal>());
    println!("Size of (usize, usize): {}", mem::size_of::<(usize, usize)>());
    
    // Transmuting trait objects is extremely dangerous!
    // The vtable layout is not stable
    
    // Safe pattern: use Any for downcasting
    use std::any::Any;
    let erased: Box<dyn Any> = Box::new(Dog);
    if let Some(dog) = erased.downcast_ref::<Dog>() {
        dog.speak();
    }
}

Transmute Lifetime (Unsound!)

use std::mem;
 
// WARNING: Transmuting lifetimes is almost always wrong!
// This creates dangling references!
 
// UNSOUND EXAMPLE (do not use):
// fn extend_lifetime<'a, 'b>(s: &'a str) -> &'b str {
//     unsafe { mem::transmute::<&'a str, &'b str>(s) }
// }
 
// This would let you use a reference after its referent is dropped:
// fn bad() {
//     let s = String::from("hello");
//     let extended = extend_lifetime(&s);
//     drop(s);  // s is dropped
//     println!("{}", extended);  // Use after free!
// }
 
fn main() {
    println!("Transmuting lifetimes is unsound!");
    println!("Use covariance or lifetime bounds instead.");
}

Transmute for Copy Types

use std::mem;
 
#[derive(Clone, Copy)]
#[repr(C)]
struct Color {
    r: u8,
    g: u8,
    b: u8,
    a: u8,
}
 
impl Color {
    fn new(r: u8, g: u8, b: u8, a: u8) -> Self {
        Self { r, g, b, a }
    }
    
    fn as_u32(&self) -> u32 {
        // Safe conversion for #[repr(C)] types
        u32::from_ne_bytes([self.r, self.g, self.b, self.a])
    }
    
    fn from_u32(value: u32) -> Self {
        let [r, g, b, a] = value.to_ne_bytes();
        Self { r, g, b, a }
    }
}
 
fn main() {
    let color = Color::new(255, 128, 64, 255);
    println!("Color as u32: {}", color.as_u32());
    
    let back = Color::from_u32(color.as_u32());
    println!("Reconstructed: r={}, g={}, b={}, a={}", back.r, back.g, back.b, back.a);
}

Zero-Cost Type Conversion

use std::mem;
 
#[repr(transparent)]
struct Wrapper<T>(T);
 
impl<T> Wrapper<T> {
    fn new(inner: T) -> Self {
        Wrapper(inner)
    }
    
    fn into_inner(self) -> T {
        self.0
    }
    
    // For #[repr(transparent)] types, transmute is actually safe!
    // But prefer using the inner value directly or as_ref/as_mut
}
 
fn main() {
    let wrapped = Wrapper::new(42i32);
    
    // repr(transparent) guarantees same layout
    println!("Wrapper<i32> size: {}", mem::size_of::<Wrapper<i32>>());
    println!("i32 size: {}", mem::size_of::<i32>());
    
    // Safe: repr(transparent) guarantees identical layout
    let inner: i32 = unsafe { mem::transmute(wrapped) };
    println!("Transmuted: {}", inner);
}

Common Transmute Mistakes

use std::mem;
 
fn main() {
    // MISTAKE 1: Different sizes
    // let x: u32 = unsafe { mem::transmute(0u8) };  // Won't compile!
    
    // MISTAKE 2: Ignoring alignment
    // let bytes: [u8; 4] = [1, 2, 3, 4];
    // let ptr = bytes.as_ptr() as *const u32;
    // let val = unsafe { *ptr };  // UB if not aligned!
    
    // MISTAKE 3: Transmuting &T to &mut T (UB!)
    // let x = 42;
    // let ref_x = &x;
    // let mut_x: &mut i32 = unsafe { mem::transmute(ref_x) };  // UB!
    
    // MISTAKE 4: Transmuting away Send/Sync
    // struct NotSync(std::cell::Cell<i32>);
    // let sync: std::sync::Mutex<NotSync> = /* ... */;
    // let unsync = unsafe { mem::transmute(sync) };  // UB when used!
    
    println!("Avoid these common transmute mistakes!");
    
    // CORRECT APPROACH: Use safe abstractions
    let bytes = [1u8, 2, 3, 4];
    let val = u32::from_ne_bytes(bytes);  // Safe!
    println!("Safe conversion: {}", val);
}

Debugging Transmute Issues

use std::mem;
 
fn main() {
    // Check sizes at compile time
    assert_eq!(mem::size_of::<u32>(), mem::size_of::<[u8; 4]>());
    
    // Check alignment
    println!("u32 alignment: {}", mem::align_of::<u32>());
    println!("[u8; 4] alignment: {}", mem::align_of::<[u8; 4]>());
    
    // Use transmute_copy carefully if sizes differ
    let source: u16 = 0x1234;
    let dest: u32 = unsafe { mem::transmute_copy(&source) };
    // Warning: reads 4 bytes from 2-byte source! UB!
    
    // Better: explicit zero-extension
    let safe_dest: u32 = source as u32;
    println!("Safe result: {}", safe_dest);
}

Summary

transmute Requirements:

Requirement Enforcement
Same size Compile-time (must be equal)
Same alignment Not enforced (can cause UB)
Valid bit pattern Not enforced (can cause UB)
Valid lifetime Not enforced (UB to violate)
Maintains safety invariants Not enforced (UB to violate)

transmute vs Alternatives:

Method Safety Use Case
as Safe Numeric casts, pointer casts
From/Into Safe Known-safe type conversions
try_from/try_into Safe Fallible type conversions
to_ne_bytes/from_ne_bytes Safe Numeric to bytes
transmute Unsafe Bit reinterpretation
transmute_copy Unsafe Copy bits to different size
bytemuck::cast Safe Pod type transmutation

When transmute is Safe:

Scenario Safety
#[repr(transparent)] types ✅ Generally safe
#[repr(C)] same-layout structs ⚠️ Check alignment
Numeric types to bytes ✅ Use to_ne_bytes instead
Bytes to numeric ⚠️ Use from_ne_bytes instead
&T to &mut T ❌ Always UB
Lifetime extension ❌ Always UB
Different sizes ❌ Won't compile

Key Points:

  • transmute reinterprets bits as another type of the same size
  • It's extremely dangerous—prefer safe alternatives
  • Both types must have identical sizes (compile-time check)
  • Alignment, validity, and safety invariants are NOT checked
  • Use bytemuck or zerocopy crates for safe transmutation
  • transmute_copy allows different sizes but reads arbitrary memory
  • Never transmute &T to &mut T (undefined behavior)
  • Never transmute to extend lifetimes (dangling references)
  • Use repr(C) or repr(transparent) when transmute is necessary
  • Consider Pod (Plain Old Data) types for safe bit manipulation