Ever tried to send a Rust struct directly to a GPU buffer? Or serialize something to raw bytes without serde? You hit a wall fast. Rust's type system protects you from most things, but when you need raw bytes, you're usually stuck writing unsafe code with std::mem::transmute.
That's where bytemuck comes in.
The Problem
Let's say you have a vertex for GPU rendering:
#[repr(C)]
struct Vertex {
position: [f32; 3],
color: [f32; 4],
uv: [f32; 2],
}
To send this to the GPU, you need &[u8]. The naive approach:
// DANGER: transmute is unsound if you get the size wrong
let bytes: &[u8] = std::mem::transmute(&vertex);
This works, but one typo and you're reading garbage. The compiler can't help you.
What Bytemuck Gives You
Bytemuck provides two traits that make this safe:
Pod(Plain Old Data): Types that are safe to cast to/from bytesZeroable: Types that can be safely initialized to zero
Add the derive macros:
use bytemuck::{Pod, Zeroable};
#[repr(C)]
#[derive(Pod, Zeroable)]
struct Vertex {
position: [f32; 3],
color: [f32; 4],
uv: [f32; 2],
}
Now the casting functions are safe:
// Safe casting - the derive macro enforces correctness at compile time
let bytes: &[u8] = bytemuck::bytes_of(&vertex);
let bytes_slice: &[u8] = bytemuck::cast_slice(&vertices);
// Back to structs
let recovered: &Vertex = bytemuck::from_bytes(bytes).unwrap();
The derive macro checks all the requirements: no references, no padding issues (with #[repr(C)]), no types that can't be safely zeroed. If your struct can't implement Pod, you get a compile error—not a runtime crash.
Why This Matters
Three real-world use cases where bytemuck shines:
-
GPU buffers — wgpu, gfx, rendy all use it. Your vertex data goes straight to the GPU without unsafe boilerplate.
-
Binary file formats — writing WAV headers, BMP files, custom binary formats. You get
&[u8]directly. -
FFI — sending Rust structs to C libraries.
Podtypes are guaranteed to have the same memory layout as C structs.
The Safety Story
Here's what makes bytemuck clever: it's not avoiding unsafe. It's moving the unsafe from every call site to the derive macro implementation.
When you write #[derive(Pod)], the macro generates code that contains unsafe—but it only runs once, at compile time. The macro verifies your type is actually safe to cast. Every call site after that is safe by construction.
You trust the macro. The macro trusts (verifies) your struct. You get safe APIs.
What's the Catch?
Not every type can be Pod. The requirements:
- No references (
&Tor&mut T) - No
UnsafeCell - No padding (use
#[repr(C)]or#[repr(packed)]) - All fields must also be
Pod
If you have a Vec<String> in your struct, you're stuck. That's intentional—bytemuck is for flat, POD types.
The Ecosystem
Bytemuck pairs well with:
- wgpu — GPU rendering
- glam — math types (vec3, mat4, etc.) all implement
Pod - zerocopy — for deserializing from bytes (the other direction)
It's small (no dependencies), stable, and used in production by games and graphics apps.
Bytemuck won't be in every Rust project. But when you need it—GPU work, binary formats, FFI—it's the tool that makes the impossible safe. The compile-time guarantees mean you catch mistakes before they crash.
That's the Rust way: move the cost from runtime to compile time, from debugging to coding.