ExamplesBy LevelBy TopicLearning Paths
707 Fundamental

transmute basics

Functional Programming

Tutorial

The Problem

This example covers a specific aspect of Rust's unsafe programming model: raw memory manipulation, FFI interop, allocator customization, or soundness principles. These topics are essential for systems programming β€” writing OS components, device drivers, game engines, and any code that must interact with C libraries or control memory layout precisely. Rust's unsafe system is designed to confine unsafety to small, auditable regions while maintaining safety in the surrounding code.

🎯 Learning Outcomes

  • β€’ The specific unsafe feature demonstrated: transmute basics
  • β€’ When this feature is necessary vs when safe alternatives exist
  • β€’ How to use it correctly with appropriate SAFETY documentation
  • β€’ The invariants that must be maintained for the operation to be sound
  • β€’ Real-world contexts: embedded systems, OS kernels, C FFI, performance-critical code
  • Code Example

    // IEEE-754 bits of f32 β€” named safe API, no unsafe needed
    fn f32_bits(f: f32) -> u32 { f.to_bits() }
    fn f32_from_bits(bits: u32) -> f32 { f32::from_bits(bits) }
    
    // &str byte view β€” named safe API
    fn str_bytes(s: &str) -> &[u8] { s.as_bytes() }
    
    // Explicit byte serialisation β€” portable and endian-aware
    fn u32_to_le(n: u32) -> [u8; 4] { n.to_le_bytes() }

    Key Differences

  • Safety model: Rust requires explicit unsafe for these operations; OCaml achieves safety through the GC and type system without explicit unsafe regions.
  • FFI approach: Rust uses raw C types directly with extern "C"; OCaml uses ctypes which wraps C types in OCaml values.
  • Memory control: Rust allows complete control over memory layout (#[repr(C)], custom allocators); OCaml's GC manages memory layout automatically.
  • Auditability: Rust unsafe regions are syntactically visible and toolable; OCaml unsafe operations (Obj.magic, direct C calls) are also explicit but less common.
  • OCaml Approach

    OCaml's GC and type system eliminate most of the need for these unsafe operations. The equivalent functionality typically uses:

  • β€’ C FFI via the ctypes library for external function calls
  • β€’ Bigarray for controlled raw memory access
  • β€’ The GC for memory management (no manual allocators needed)
  • β€’ Bytes.t for mutable byte sequences
  • OCaml programs rarely need operations equivalent to these Rust unsafe patterns.

    Full Source

    #![allow(clippy::all)]
    //! 707 β€” std::mem::transmute: Reinterpreting Bytes
    //!
    //! Safe-alternative-first approach: the safe APIs are always listed first.
    //! Transmute is used only where no stable safe alternative exists.
    //!
    //! Clippy correctly warns when a safe API covers the same operation (e.g.
    //! `f32::to_bits` instead of `transmute::<f32, u32>`). The examples here
    //! therefore focus on cases where transmute is genuinely necessary:
    //!   - Array-of-T β†’ array-of-U (no safe std API for whole-array reinterpretation)
    //!   - `#[repr(C)]` struct ↔ byte array (FFI / packed-pixel patterns)
    
    use std::mem;
    
    // ── Safe primitives β€” the recommended ways ───────────────────────────────
    
    /// IEEE-754 bit pattern of an f32 β€” prefer this over transmute.
    pub fn f32_bits(f: f32) -> u32 {
        f.to_bits()
    }
    pub fn f32_from_bits(bits: u32) -> f32 {
        f32::from_bits(bits)
    }
    
    /// UTF-8 byte view of a &str β€” prefer this over transmute.
    pub fn str_bytes(s: &str) -> &[u8] {
        s.as_bytes()
    }
    
    // ── Case 1: [f32; 4] β†’ [u32; 4] ──────────────────────────────────────────
    //
    // No std API reinterprets a whole array in one call.
    // The safe iterator version is shown alongside for comparison.
    
    /// Safe version β€” element-by-element, no unsafe needed.
    pub fn f32x4_to_bits_safe(arr: [f32; 4]) -> [u32; 4] {
        arr.map(f32::to_bits)
    }
    
    /// Transmute version β€” one instruction when optimised, same result.
    ///
    /// # Safety
    /// `[f32; 4]` and `[u32; 4]` have identical size (16 bytes) and alignment (4).
    /// Every u32 bit pattern is valid, so no validity invariant can be broken.
    pub fn f32x4_to_bits_transmute(arr: [f32; 4]) -> [u32; 4] {
        // SAFETY: [f32; 4] and [u32; 4] have the same size (4 Γ— 4 = 16 bytes)
        // and the same alignment (4). Every bit-pattern of [u32; 4] is valid.
        unsafe { mem::transmute::<[f32; 4], [u32; 4]>(arr) }
    }
    
    /// Round-trip: [u32; 4] β†’ [f32; 4].
    ///
    /// # Safety
    /// Every bit pattern of `[u32; 4]` corresponds to some f32 value (incl. NaN/Inf).
    pub fn u32x4_to_f32x4(arr: [u32; 4]) -> [f32; 4] {
        // SAFETY: [u32; 4] and [f32; 4] have the same size and alignment.
        // Every u32 bit pattern maps to a valid (if non-finite) f32.
        unsafe { mem::transmute::<[u32; 4], [f32; 4]>(arr) }
    }
    
    // ── Case 2: #[repr(C)] struct ↔ [u8; N] ──────────────────────────────────
    //
    // Common in FFI / packed pixel formats. `#[repr(C)]` guarantees layout.
    
    /// A 32-bit RGBA colour β€” layout-stable for FFI / pixel-buffer operations.
    #[repr(C)]
    #[derive(Debug, Clone, Copy, PartialEq, Eq)]
    pub struct Rgba {
        pub r: u8,
        pub g: u8,
        pub b: u8,
        pub a: u8,
    }
    
    /// Safe version using field access β€” verbose but zero-unsafe.
    pub fn rgba_to_bytes_safe(c: Rgba) -> [u8; 4] {
        [c.r, c.g, c.b, c.a]
    }
    
    /// Transmute version β€” `#[repr(C)]` makes this sound.
    ///
    /// # Safety
    /// `Rgba` is `#[repr(C)]` with four `u8` fields β†’ size 4, align 1.
    /// `[u8; 4]` has size 4, align 1, and every bit pattern is a valid `u8`.
    pub fn rgba_to_bytes_transmute(c: Rgba) -> [u8; 4] {
        // SAFETY: Rgba is #[repr(C)] β€” layout is [r, g, b, a] with no padding.
        // [u8; 4] has no validity invariants, so every bit pattern is sound.
        unsafe { mem::transmute::<Rgba, [u8; 4]>(c) }
    }
    
    /// Reconstruct an Rgba from four bytes.
    ///
    /// # Safety
    /// `Rgba` is `#[repr(C)]` with four `u8` fields. Every `u8` is a valid
    /// field value, so every `[u8; 4]` produces a valid `Rgba`.
    pub fn bytes_to_rgba(b: [u8; 4]) -> Rgba {
        // SAFETY: Rgba is #[repr(C)] with size 4 and align 1.
        // Every byte sequence maps to a valid Rgba β€” all u8 values are legal.
        unsafe { mem::transmute::<[u8; 4], Rgba>(b) }
    }
    
    // ── Case 3: generic bytes-of view (raw pointer, not transmute) ────────────
    //
    // Transmute cannot express "borrow the bytes of an arbitrary T" because the
    // output lifetime is not encoded in the types.  The idiomatic approach uses
    // `std::slice::from_raw_parts` instead.
    
    /// Return a byte view of any `Copy + Sized` value, tied to its lifetime.
    ///
    /// # Safety
    /// The returned slice borrows from `val` and must not outlive it.
    /// `T` must have no padding bytes if the caller cares about the byte values
    /// (padding is uninitialised and reading it is undefined behaviour).
    pub fn bytes_of<T: Copy>(val: &T) -> &[u8] {
        // SAFETY: val is a valid reference, so the pointer is non-null and aligned.
        // size_of::<T>() bytes starting at that pointer are part of the value.
        // The lifetime of the returned slice is tied to val's lifetime.
        unsafe { std::slice::from_raw_parts(val as *const T as *const u8, mem::size_of::<T>()) }
    }
    
    // ─────────────────────────────────────────────────────────────────────────
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Safe primitive round-trips (baseline) ─────────────────────────────
    
        #[test]
        fn test_f32_bits_round_trip() {
            for f in [0.0_f32, 1.0, -1.0, f32::MAX, std::f32::consts::PI] {
                assert_eq!(f32_from_bits(f32_bits(f)).to_bits(), f.to_bits());
            }
        }
    
        #[test]
        fn test_str_bytes_is_as_bytes() {
            let s = "hello πŸ¦€";
            assert_eq!(str_bytes(s), s.as_bytes());
        }
    
        // ── [f32; 4] ↔ [u32; 4] ──────────────────────────────────────────────
    
        #[test]
        fn test_f32x4_transmute_matches_safe() {
            let arr = [0.0_f32, 1.0, -1.0, std::f32::consts::PI];
            assert_eq!(
                f32x4_to_bits_transmute(arr),
                f32x4_to_bits_safe(arr),
                "transmute and map must produce identical bits"
            );
        }
    
        #[test]
        fn test_f32x4_round_trip() {
            let original = [1.0_f32, 2.0, 3.0, 4.0];
            let bits = f32x4_to_bits_transmute(original);
            let recovered = u32x4_to_f32x4(bits);
            // Compare bit patterns to handle NaN correctly.
            for (a, b) in original.iter().zip(recovered.iter()) {
                assert_eq!(a.to_bits(), b.to_bits());
            }
        }
    
        #[test]
        fn test_f32x4_known_bit_patterns() {
            // 1.0f32 = 0x3F800000, 0.0f32 = 0x00000000
            let arr = [1.0_f32, 0.0, 1.0, 0.0];
            let bits = f32x4_to_bits_transmute(arr);
            assert_eq!(bits[0], 0x3F80_0000);
            assert_eq!(bits[1], 0x0000_0000);
        }
    
        // ── Rgba ↔ [u8; 4] ───────────────────────────────────────────────────
    
        #[test]
        fn test_rgba_transmute_matches_safe() {
            let c = Rgba {
                r: 0xDE,
                g: 0xAD,
                b: 0xBE,
                a: 0xEF,
            };
            assert_eq!(rgba_to_bytes_transmute(c), rgba_to_bytes_safe(c));
        }
    
        #[test]
        fn test_rgba_round_trip() {
            let original = Rgba {
                r: 255,
                g: 128,
                b: 0,
                a: 64,
            };
            let bytes = rgba_to_bytes_transmute(original);
            let recovered = bytes_to_rgba(bytes);
            assert_eq!(original, recovered);
        }
    
        #[test]
        fn test_rgba_byte_order() {
            let c = Rgba {
                r: 0x12,
                g: 0x34,
                b: 0x56,
                a: 0x78,
            };
            let bytes = rgba_to_bytes_transmute(c);
            // #[repr(C)] guarantees field order: r, g, b, a
            assert_eq!(bytes, [0x12, 0x34, 0x56, 0x78]);
        }
    
        #[test]
        fn test_rgba_zero() {
            let c = Rgba {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            };
            assert_eq!(rgba_to_bytes_transmute(c), [0, 0, 0, 0]);
            assert_eq!(bytes_to_rgba([0; 4]), c);
        }
    
        // ── bytes_of generic view ─────────────────────────────────────────────
    
        #[test]
        fn test_bytes_of_u32_length() {
            let n: u32 = 42;
            assert_eq!(bytes_of(&n).len(), 4);
        }
    
        #[test]
        fn test_bytes_of_rgba_matches_transmute() {
            let c = Rgba {
                r: 1,
                g: 2,
                b: 3,
                a: 4,
            };
            assert_eq!(bytes_of(&c), rgba_to_bytes_transmute(c));
        }
    }
    ✓ Tests Rust test suite
    #[cfg(test)]
    mod tests {
        use super::*;
    
        // ── Safe primitive round-trips (baseline) ─────────────────────────────
    
        #[test]
        fn test_f32_bits_round_trip() {
            for f in [0.0_f32, 1.0, -1.0, f32::MAX, std::f32::consts::PI] {
                assert_eq!(f32_from_bits(f32_bits(f)).to_bits(), f.to_bits());
            }
        }
    
        #[test]
        fn test_str_bytes_is_as_bytes() {
            let s = "hello πŸ¦€";
            assert_eq!(str_bytes(s), s.as_bytes());
        }
    
        // ── [f32; 4] ↔ [u32; 4] ──────────────────────────────────────────────
    
        #[test]
        fn test_f32x4_transmute_matches_safe() {
            let arr = [0.0_f32, 1.0, -1.0, std::f32::consts::PI];
            assert_eq!(
                f32x4_to_bits_transmute(arr),
                f32x4_to_bits_safe(arr),
                "transmute and map must produce identical bits"
            );
        }
    
        #[test]
        fn test_f32x4_round_trip() {
            let original = [1.0_f32, 2.0, 3.0, 4.0];
            let bits = f32x4_to_bits_transmute(original);
            let recovered = u32x4_to_f32x4(bits);
            // Compare bit patterns to handle NaN correctly.
            for (a, b) in original.iter().zip(recovered.iter()) {
                assert_eq!(a.to_bits(), b.to_bits());
            }
        }
    
        #[test]
        fn test_f32x4_known_bit_patterns() {
            // 1.0f32 = 0x3F800000, 0.0f32 = 0x00000000
            let arr = [1.0_f32, 0.0, 1.0, 0.0];
            let bits = f32x4_to_bits_transmute(arr);
            assert_eq!(bits[0], 0x3F80_0000);
            assert_eq!(bits[1], 0x0000_0000);
        }
    
        // ── Rgba ↔ [u8; 4] ───────────────────────────────────────────────────
    
        #[test]
        fn test_rgba_transmute_matches_safe() {
            let c = Rgba {
                r: 0xDE,
                g: 0xAD,
                b: 0xBE,
                a: 0xEF,
            };
            assert_eq!(rgba_to_bytes_transmute(c), rgba_to_bytes_safe(c));
        }
    
        #[test]
        fn test_rgba_round_trip() {
            let original = Rgba {
                r: 255,
                g: 128,
                b: 0,
                a: 64,
            };
            let bytes = rgba_to_bytes_transmute(original);
            let recovered = bytes_to_rgba(bytes);
            assert_eq!(original, recovered);
        }
    
        #[test]
        fn test_rgba_byte_order() {
            let c = Rgba {
                r: 0x12,
                g: 0x34,
                b: 0x56,
                a: 0x78,
            };
            let bytes = rgba_to_bytes_transmute(c);
            // #[repr(C)] guarantees field order: r, g, b, a
            assert_eq!(bytes, [0x12, 0x34, 0x56, 0x78]);
        }
    
        #[test]
        fn test_rgba_zero() {
            let c = Rgba {
                r: 0,
                g: 0,
                b: 0,
                a: 0,
            };
            assert_eq!(rgba_to_bytes_transmute(c), [0, 0, 0, 0]);
            assert_eq!(bytes_to_rgba([0; 4]), c);
        }
    
        // ── bytes_of generic view ─────────────────────────────────────────────
    
        #[test]
        fn test_bytes_of_u32_length() {
            let n: u32 = 42;
            assert_eq!(bytes_of(&n).len(), 4);
        }
    
        #[test]
        fn test_bytes_of_rgba_matches_transmute() {
            let c = Rgba {
                r: 1,
                g: 2,
                b: 3,
                a: 4,
            };
            assert_eq!(bytes_of(&c), rgba_to_bytes_transmute(c));
        }
    }

    Deep Comparison

    OCaml vs Rust: std::mem::transmute β€” Reinterpreting Bytes

    Side-by-Side Code

    OCaml

    (* 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);
      let bytes = int32_to_bytes_le 0x12345678l in
      Bytes.iter (fun c -> Printf.printf "%02x " (Char.code c)) bytes
    

    Rust (safe β€” always prefer these)

    // IEEE-754 bits of f32 β€” named safe API, no unsafe needed
    fn f32_bits(f: f32) -> u32 { f.to_bits() }
    fn f32_from_bits(bits: u32) -> f32 { f32::from_bits(bits) }
    
    // &str byte view β€” named safe API
    fn str_bytes(s: &str) -> &[u8] { s.as_bytes() }
    
    // Explicit byte serialisation β€” portable and endian-aware
    fn u32_to_le(n: u32) -> [u8; 4] { n.to_le_bytes() }
    

    Rust (transmute β€” only where no safe API exists)

    use std::mem;
    
    // [f32; 4] β†’ [u32; 4]: no std API for whole-array reinterpretation
    fn f32x4_to_bits_transmute(arr: [f32; 4]) -> [u32; 4] {
        // SAFETY: [f32; 4] and [u32; 4] have identical size (16 bytes) and
        // alignment (4). Every [u32; 4] bit pattern is valid.
        unsafe { mem::transmute::<[f32; 4], [u32; 4]>(arr) }
    }
    
    // #[repr(C)] struct ↔ [u8; 4]: sound because layout is fully specified
    fn rgba_to_bytes_transmute(c: Rgba) -> [u8; 4] {
        // SAFETY: Rgba is #[repr(C)] with four u8 fields β€” size 4, align 1.
        // [u8; 4] has no validity invariants.
        unsafe { mem::transmute::<Rgba, [u8; 4]>(c) }
    }
    

    Type Signatures

    ConceptOCamlRust
    Float β†’ bits (safe)Int64.bits_of_float : float -> int64f32::to_bits : f32 -> u32
    Bits β†’ float (safe)Int64.float_of_bits : int64 -> floatf32::from_bits : u32 -> f32
    Byte reinterpretation (unsafe)Obj.magic : 'a -> 'bmem::transmute<T, U>(val: T) -> U
    Compile-time size checkruntime mismatch β†’ silent corruptioncompile error if size_of::<T>() != size_of::<U>()
    Array element reinterpretationArray.map Int64.bits_of_floatarr.map(f32::to_bits) (safe) or transmute (whole array)
    Struct β†’ bytesBytes.create + field writesrgba_to_bytes_safe (fields) or transmute with #[repr(C)]

    Key Insights

  • Same nuclear option, different safety models. Both Obj.magic (OCaml) and mem::transmute (Rust) bypass the type system entirely. OCaml surfaces this as a runtime risk with no compile-time guardrails; Rust gates it behind unsafe {}, forcing an explicit proof at the call site.
  • Rust enforces size equality at compile time. transmute::<f32, u64> is a compile error because the sizes differ (4 vs 8 bytes). OCaml's Obj.magic has no such check β€” silent heap corruption is possible.
  • Clippy correctly warns when a safe API already exists. transmute::<f32, u32> triggers unnecessary_transmutes because f32::to_bits() does the same thing safely. The examples here therefore use transmute only where no named safe alternative exists: whole-array type changes ([f32; 4] β†’ [u32; 4]) and #[repr(C)] struct↔bytes conversions.
  • **// SAFETY: comments are mandatory.** Every unsafe { mem::transmute(...) } must be accompanied by a comment proving that alignment, size, validity invariants, and lifetimes are upheld. This is the programmer's written proof obligation β€” it makes code review and audits tractable.
  • **#[repr(C)] is what makes struct transmutes sound.** Without it, the compiler may reorder or pad fields freely; the transmute could read uninitialised padding bytes. OCaml records have runtime metadata making Obj.magic even more hazardous β€” the GC can misinterpret a transmuted value's tag and corrupt the heap.
  • When to Use Each Style

    Use safe Rust APIs when: inspecting float bit patterns (to_bits/from_bits), converting slices (as_bytes), serialising integers (to_le_bytes/to_be_bytes) β€” these cover 99 % of real use cases and are zero-cost at compile time.

    **Use transmute when:** reinterpreting a whole fixed-size array of a primitive type (e.g. for SIMD preparation), working with a #[repr(C)] struct in an FFI context, or implementing a low-level runtime primitive where no safe abstraction yet exists β€” always document with a // SAFETY: proof and benchmark whether the safe alternative is actually slower.

    Exercises

  • Minimize unsafe: Find the smallest possible unsafe region in the source and verify that all safe code is outside the unsafe block.
  • Safe alternative: Identify if a safe alternative exists for the demonstrated technique (e.g., bytemuck for transmute, CString for FFI strings) and implement it.
  • SAFETY documentation: Write a complete SAFETY comment for each unsafe block listing preconditions, invariants, and what would break if violated.
  • Open Source Repos