Skip to content

Packed Structs

A normal Zig struct is designed for ordinary data modeling.

A normal Zig struct is designed for ordinary data modeling.

const Point = struct {
    x: i32,
    y: i32,
};

This is the right choice most of the time. You define fields, create values, pass them to functions, and access fields by name.

A packed struct has a different purpose.

A packed struct gives you tighter control over how fields are stored in memory. It is mainly used when you need to describe data at the bit level.

const Flags = packed struct {
    read: bool,
    write: bool,
    execute: bool,
};

This struct represents three boolean flags. In a normal struct, each bool may take more space than one bit because the compiler chooses a layout that is convenient for the target. In a packed struct, Zig packs the fields more tightly.

Packed structs are useful for things like file formats, network protocols, CPU registers, binary headers, and bit flags.

Why Normal Structs Are Usually Better

A normal struct lets the compiler choose a good memory layout.

const User = struct {
    id: u64,
    active: bool,
    score: u32,
};

The compiler may add padding between fields. Padding is unused space inserted so that fields can be aligned efficiently in memory.

For beginner code, this is good. You get simple field access and efficient ordinary behavior.

A packed struct tells Zig: layout matters here.

That is a stronger promise. Use it only when the exact representation is part of the problem.

A Simple Packed Struct

Here is a small example:

const Flags = packed struct {
    read: bool,
    write: bool,
    execute: bool,
};

You can create a value like this:

const flags = Flags{
    .read = true,
    .write = false,
    .execute = true,
};

You can access fields normally:

if (flags.read) {
    // reading is allowed
}

The syntax looks almost the same as a normal struct. The important difference is the word packed.

packed struct {
    // fields
}

That word changes the representation.

Packed Structs Are About Representation

Consider this value:

const flags = Flags{
    .read = true,
    .write = false,
    .execute = true,
};

Conceptually, this is three yes-or-no values.

read    = true
write   = false
execute = true

As bits, you can think of it like this:

read write execute
  1     0      1

Packed structs let you describe that kind of layout directly in Zig code.

This is useful when the bits are not just an implementation detail. Sometimes the bits are the data format.

Integer Fields in Packed Structs

Packed structs often use small integer fields.

const Header = packed struct {
    version: u4,
    kind: u4,
};

Here, u4 means an unsigned 4-bit integer.

The whole struct can fit into 8 bits:

version: 4 bits
kind:    4 bits

Example:

const header = Header{
    .version = 1,
    .kind = 9,
};

This is useful when a byte is split into several smaller fields.

In ordinary code, you usually use integer types like u8, u16, u32, and usize.

In packed binary layouts, smaller integer widths like u1, u2, u3, or u4 can be useful.

Example: A Byte Split into Fields

Suppose one byte stores two pieces of information:

high 4 bits: version
low 4 bits:  kind

We can model that with a packed struct:

const Header = packed struct {
    kind: u4,
    version: u4,
};

Now a single value has named fields:

const h = Header{
    .kind = 2,
    .version = 1,
};

Instead of writing bit masks everywhere, you can write:

const k = h.kind;
const v = h.version;

This makes the program easier to read.

The exact bit order can matter when communicating with external formats. When you write code for a real binary format, read Zig’s rules and the format specification carefully. Do not guess.

Boolean Fields

A boolean field in a packed struct can be used as a single flag.

const Permissions = packed struct {
    read: bool,
    write: bool,
    execute: bool,
};

This gives names to individual permission bits.

Example:

const p = Permissions{
    .read = true,
    .write = true,
    .execute = false,
};

This is easier to read than using raw numbers:

const p = 0b011;

The raw number is compact, but it does not explain itself. The packed struct gives each bit a name.

Packed Structs and Bit Flags

Packed structs are a good fit when you would otherwise write code like this:

const READ: u8 = 0b001;
const WRITE: u8 = 0b010;
const EXECUTE: u8 = 0b100;

With bit masks, checking a flag looks like this:

const can_read = (flags & READ) != 0;

With a packed struct, the idea is named directly:

const can_read = permissions.read;

Both approaches can be useful. Bit masks are common and portable. Packed structs can make the code clearer when you control the layout and understand the representation.

Packed Structs Are Not Classes

A packed struct is still a struct.

It can have fields. It can have declarations. It can have methods.

const Permissions = packed struct {
    read: bool,
    write: bool,
    execute: bool,

    fn canExecute(self: Permissions) bool {
        return self.execute;
    }
};

Use it like this:

const p = Permissions{
    .read = true,
    .write = false,
    .execute = true,
};

const ok = p.canExecute();

The method rules are the same as normal structs.

The packed part changes the data layout, not the basic idea of struct methods.

When Packed Structs Are Useful

Use packed structs when you are modeling data whose bit layout matters.

Common cases include:

Use caseWhy packed structs help
Hardware registersEach bit or group of bits may control a device feature
Binary file formatsFields may be stored in fixed bit positions
Network packetsHeaders often contain compact fields
Permission flagsSeveral booleans can fit into one small value
Embedded systemsMemory layout and size may be critical

For ordinary application data, use a normal struct.

Be Careful with External Data

Packed structs are tempting when reading binary data.

For example, you may want to map bytes directly to a packed struct. Be careful.

External data can involve:

IssueMeaning
EndiannessByte order may differ between systems
AlignmentSome addresses may not be safe for direct access
Format versionsThe layout may change over time
Invalid valuesThe bytes may not represent a valid Zig value

For serious binary parsing, do not simply cast arbitrary bytes and hope they are valid. Parse carefully, check the data, and handle errors.

Packed structs are a tool. They do not remove the need to validate input.

Normal, Extern, and Packed Structs

Zig gives you several struct layout choices.

FormMain purpose
structOrdinary Zig data
extern structMatch C ABI layout
packed structControl compact bit-level layout

Use struct by default.

Use extern struct when you must match C.

Use packed struct when you must describe compact binary layout.

This distinction matters. The three forms solve different problems.

A Small Practical Example

Suppose we want to describe three feature flags:

const FeatureFlags = packed struct {
    logging: bool,
    metrics: bool,
    tracing: bool,
};

We can use it in a config struct:

const Config = struct {
    flags: FeatureFlags,
};

const config = Config{
    .flags = FeatureFlags{
        .logging = true,
        .metrics = false,
        .tracing = true,
    },
};

Now the code reads clearly:

if (config.flags.logging) {
    // enable logging
}

if (config.flags.tracing) {
    // enable tracing
}

The field names explain the meaning of each bit.

The Main Idea

A packed struct is a struct with a compact, layout-focused representation.

Use it when the exact bits matter:

const Flags = packed struct {
    read: bool,
    write: bool,
    execute: bool,
};

Do not use packed structs just because they look efficient. Start with normal structs. Reach for packed structs when you are working with binary layouts, hardware registers, protocol fields, or compact flags where representation is part of the design.