@ptrCast
@ptrCast converts one pointer type into another pointer type.
It does not move memory. It does not copy memory. It does not change the bytes stored in memory.
It only changes how Zig views the pointer.
That makes it powerful, but also dangerous if used carelessly.
The Basic Idea
Suppose you have a pointer to one type:
const p: *u32 = undefined;Sometimes low-level code needs to view the same address through a different pointer type.
For example:
const q: *u8 = @ptrCast(p);Now q is a pointer to u8.
The memory address is the same. The pointer type changed.
You can read this as:
Treat this pointer as a different pointer type.Why Pointer Casts Exist
Pointer casts are common in systems programming.
You may need them when working with:
binary formats
network packets
C libraries
operating system APIs
memory-mapped files
hardware registers
custom allocators
serialization codeThese areas often deal with raw memory. Raw memory does not always arrive with the exact Zig type you want.
For example, a C API may give you a *anyopaque, which means “pointer to something, but Zig does not know what.”
You may know that the pointer actually points to a specific struct. In that case, you may use @ptrCast to recover the expected pointer type.
@ptrCast Changes the Pointer Type
Example:
const std = @import("std");
const Header = struct {
magic: u32,
version: u16,
};
pub fn main() void {
var header = Header{
.magic = 0x12345678,
.version = 1,
};
const p: *Header = &header;
const bytes: [*]u8 = @ptrCast(p);
std.debug.print("first byte: {}\n", .{bytes[0]});
}Here, p points to a Header.
Then bytes points to the same memory, but treats it as a sequence of bytes.
This kind of code is low-level. It depends on memory layout. It can be useful, but it should be written with care.
@ptrCast Does Not Convert Values
This is important.
A pointer cast does not convert the value stored at the address.
If memory contains a u32, this does not turn the u32 into four independent safe values in a high-level sense. It only lets you inspect the same memory as bytes.
Compare these two ideas:
const x: u32 = 100;
const y: u64 = x;This is a numeric conversion. The value 100 becomes a u64.
But:
const p: *u32 = &x;
const q: *u8 = @ptrCast(p);This is a pointer reinterpretation. The address is retyped.
The number stored in memory did not get converted.
Alignment Still Matters
Pointer casts do not erase alignment rules.
A *u32 pointer must point to memory aligned for u32.
A *u8 pointer only needs byte alignment.
Casting from a stricter pointer to a looser pointer is usually easier:
const p: *u32 = undefined;
const q: *u8 = @ptrCast(p);A u32 pointer points to memory aligned for u32. That address is also valid for u8.
The reverse direction is more serious:
const p: *u8 = undefined;
const q: *u32 = @ptrCast(p);A random *u8 pointer may not be aligned for u32.
If Zig cannot prove the alignment is correct, you may need @alignCast too.
Conceptually:
const q: *u32 = @ptrCast(@alignCast(p));This says two things:
The pointer has the right alignment.
Treat it as a pointer to u32.Use this only when you know the memory really is aligned correctly.
Pointer Casts and anyopaque
anyopaque is Zig’s type for opaque memory.
You often see it in callback APIs and C-style interfaces.
Example:
const Context = struct {
count: usize,
};
fn callback(ctx: *anyopaque) void {
const context: *Context = @ptrCast(@alignCast(ctx));
context.count += 1;
}Here, the callback receives a generic pointer.
Inside the function, we say:
This generic pointer actually points to Context.This is common when an API wants to pass user data through a generic pointer.
But the cast is a promise. If ctx does not really point to a Context, the code is wrong.
Constness Matters
Pointer casts should respect constness.
If you have a pointer to const data:
const p: *const u32 = undefined;You should not use @ptrCast to turn it into a mutable pointer and modify the data.
That would violate the meaning of const.
Correct:
const q: *const u8 = @ptrCast(p);This keeps the result const.
The pointer type changes, but the pointed-to memory remains read-only through that pointer.
Slices Need Extra Care
A slice is not just a pointer.
A slice contains:
pointer
lengthSo this is not the same kind of thing as casting a single pointer:
[]u8When working with slices, you usually cast the pointer part and handle the length separately.
Example:
const std = @import("std");
pub fn main() void {
var numbers = [_]u32{ 1, 2, 3, 4 };
const ptr: [*]u8 = @ptrCast(&numbers);
const byte_len = numbers.len * @sizeOf(u32);
const bytes = ptr[0..byte_len];
std.debug.print("byte length: {}\n", .{bytes.len});
}The array has 4 u32 values.
Each u32 is 4 bytes.
So the byte slice has length 16.
The pointer cast alone does not compute that length for you.
Prefer Standard Library Helpers When Available
For common memory operations, the standard library may provide safer helpers.
For example, when you want the bytes of a value, check the relevant APIs in std.mem before writing pointer casts manually.
Manual casts should be rare in beginner code.
A good rule:
Use @ptrCast only when you are deliberately working with representation-level memory.
Do not use it just to make a type error disappear.
A Dangerous Example
Suppose you write:
var x: u32 = 123;
const p: *f32 = @ptrCast(&x);This tells Zig to treat the memory of an integer as a floating-point number.
The bits are the same, but the meaning changes completely.
This may be useful in very specific low-level code, but most of the time it is a bug.
For value reinterpretation, @bitCast is often clearer than pointer casting, because it works on values rather than addresses.
@ptrCast vs @bitCast
Use @ptrCast when you need to reinterpret a pointer.
Use @bitCast when you need to reinterpret a value’s bits.
Pointer cast:
const p: *u32 = &x;
const q: *u8 = @ptrCast(p);Bit cast:
const bits: u32 = @bitCast(some_f32);The first changes the pointer type.
The second creates a new value with the same bits.
How to Read @ptrCast
When you see this:
const q: *SomeType = @ptrCast(p);read it as:
Use the same address as p, but treat it as a pointer to SomeType.Then ask these questions:
Is the address aligned for SomeType?
Does the memory really contain a SomeType?
Is constness preserved?
Is the lifetime still valid?
Is the length handled correctly if slices are involved?These questions matter because the compiler cannot always protect you after you reinterpret raw memory.
When Beginners Should Use It
At the beginner level, you should mostly avoid @ptrCast.
You may need it when:
calling C APIs
working with opaque callback data
inspecting raw bytes
writing allocators
parsing binary data
interfacing with operating system APIsYou usually do not need it for normal application logic.
If ordinary Zig types can express the operation, prefer ordinary Zig types.
Key Idea
@ptrCast changes one pointer type into another pointer type.
It keeps the same memory address.
It does not convert the stored value.
It does not fix alignment.
It does not prove the memory really has the new type.
Use it when you are intentionally working with raw memory representation, and keep the cast as small and local as possible.