Skip to content

Understanding `@` Builtins

Zig has special built-in functions whose names start with @.

Understanding @ Builtins

Zig has special built-in functions whose names start with @.

You have already seen one of them:

const std = @import("std");

The function @import is a Zig builtin. It is provided by the compiler itself. You do not define it. You do not import it from the standard library. It is always available.

Builtins are used for operations that need direct help from the compiler. Some of them work with types. Some work with memory. Some work with pointers. Some work with compile-time information. Some expose low-level machine behavior.

A normal function is written by you or by a library. A builtin is part of the language.

Why Builtins Exist

Zig is a small language, but it still needs ways to do low-level work.

For example, Zig needs ways to ask questions like:

What is the size of this type?

What is the alignment of this type?

What is the type of this value?

Can this pointer be reinterpreted as another pointer type?

Can this integer be converted safely?

Can this file be embedded into the program at compile time?

These operations cannot always be written as normal library functions. They need compiler knowledge.

For example:

const size = @sizeOf(u64);

This asks the compiler for the size of u64.

The answer is known at compile time. On normal Zig targets, u64 is 8 bytes.

const alignment = @alignOf(u64);

This asks the compiler for the required memory alignment of u64.

These are not runtime guesses. The compiler knows them while building the program.

Builtins Use the @ Prefix

The @ prefix makes builtins easy to recognize.

Compare these two calls:

std.debug.print("hello\n", .{});

and:

@sizeOf(u64);

The first one is a normal function call from the standard library.

The second one is a compiler builtin.

This naming rule is helpful. When you see @, you know the code is asking the compiler to do something special.

Builtins Are Still Checked

Builtins are powerful, but they are not unchecked magic.

The compiler still checks their arguments and result types.

For example:

const x: u8 = @intCast(300);

This is not a safe conversion, because 300 does not fit in u8. A u8 can only store values from 0 to 255.

Zig rejects or traps invalid conversions depending on what is known at compile time and which build mode is used.

A valid conversion looks like this:

const big: u16 = 200;
const small: u8 = @intCast(big);

This says: convert big to u8.

But Zig still treats this as something important. Integer casts can lose information, so the cast must be explicit.

Builtins Often Work at Compile Time

Many builtins are most useful during compilation.

Example:

const T = u32;
const size = @sizeOf(T);

Here, size is known before the program runs.

You can use this information in compile-time checks:

comptime {
    if (@sizeOf(usize) < 8) {
        @compileError("This program requires a 64-bit target");
    }
}

This code runs during compilation.

If usize is smaller than 8 bytes, the compiler stops and prints the custom error message.

That is a common Zig style: check assumptions early, at compile time, instead of waiting for runtime.

Some Builtins Are Low-Level

Some builtins let you work close to raw memory.

Example:

const bytes: [4]u8 = .{ 1, 2, 3, 4 };
const value: u32 = @bitCast(bytes);

@bitCast reinterprets the bits of one value as another type with the same size.

This kind of operation is useful in systems programming, binary formats, networking, emulators, and compilers.

But it must be used carefully. You are telling Zig: keep the same bits, but treat them as a different type.

That is different from a normal numeric conversion.

For example:

const a: u8 = 65;
const b: u32 = a;

This converts the number 65 into a larger integer type.

But:

const bytes: [4]u8 = .{ 65, 0, 0, 0 };
const value: u32 = @bitCast(bytes);

This treats the raw byte pattern as a u32.

The meaning depends on layout and target rules. For beginners, the important point is simple: @bitCast is about bits, not ordinary numeric meaning.

Builtins Can Inspect Types

Zig can inspect types at compile time.

One important builtin is @TypeOf:

const x = 123;
const T = @TypeOf(x);

Here, T becomes the type of x.

Another important builtin is @typeInfo:

const info = @typeInfo(u32);

This gives compile-time information about a type.

You can use this to write generic code. For example, a function can behave differently for integers, structs, arrays, or pointers.

You do not need to understand full reflection yet. For now, remember this: Zig types are values at compile time, and builtins let you inspect and use them.

Builtins Can Stop Compilation

The builtin @compileError stops compilation with a message.

comptime {
    @compileError("not implemented yet");
}

This is useful when writing generic code.

Suppose a function only supports integer types. If someone tries to use it with a struct or a slice, you can report a clear error during compilation.

fn onlyInteger(comptime T: type, value: T) T {
    const info = @typeInfo(T);

    switch (info) {
        .int => return value,
        else => @compileError("onlyInteger only accepts integer types"),
    }
}

This is better than allowing confusing code to compile and fail later.

Builtins Are Not All the Same Kind

Do not think of all builtins as one category of behavior.

They are grouped by purpose.

Some ask about types:

@TypeOf(value)
@typeInfo(T)
@sizeOf(T)
@alignOf(T)

Some convert values:

@intCast(value)
@floatCast(value)
@bitCast(value)

Some work with pointers:

@ptrCast(ptr)
@alignCast(ptr)

Some control compilation:

@compileError("message")
@setEvalBranchQuota(10000)

Some interact with files:

@import("std")
@embedFile("data.txt")

Some interact with the target machine:

@memcpy(dest, source)
@memset(dest, value)

You do not need to memorize all of them now. The goal of this chapter is to learn how to recognize them and understand why they exist.

Builtins vs Standard Library Functions

A builtin belongs to the language compiler.

A standard library function belongs to std.

This is a builtin:

@sizeOf(u32)

This is a standard library function:

std.mem.eql(u8, a, b)

The standard library is written in Zig. You can read its source code.

Builtins are implemented by the compiler. You use them from Zig code, but you do not implement them in Zig.

This difference matters because builtins often operate on things that normal code cannot access directly, such as type metadata, compile-time state, or low-level representation.

A Practical Example

Here is a small example that uses several builtins:

const std = @import("std");

pub fn main() void {
    const T = u64;

    std.debug.print("type: {s}\n", .{@typeName(T)});
    std.debug.print("size: {} bytes\n", .{@sizeOf(T)});
    std.debug.print("alignment: {} bytes\n", .{@alignOf(T)});
}

Possible output:

type: u64
size: 8 bytes
alignment: 8 bytes

This program asks the compiler about the type u64 and prints the answers at runtime.

The values are known during compilation, but the program prints them when it runs.

How to Read Code with Builtins

When you see a builtin, ask one question first:

What is the compiler being asked to do?

For example:

@sizeOf(T)

The compiler is being asked for the size of a type.

@intCast(x)

The compiler is being asked to convert an integer explicitly.

@compileError("bad type")

The compiler is being told to stop compilation.

@embedFile("logo.png")

The compiler is being asked to include a file’s bytes inside the program.

This habit makes builtins much easier to understand. Do not treat @ as strange syntax. Treat it as a sign that the compiler is directly involved.

Key Idea

Zig builtins are compiler-provided functions for operations that normal library code cannot fully express.

They make low-level and compile-time work explicit.

When you see a name starting with @, read it as: this code is asking the Zig compiler for special help.