Skip to content

Endianness

Endianness means the order used to store the bytes of a multi-byte value.

Endianness means the order used to store the bytes of a multi-byte value.

A single byte has no byte order problem. It is just one byte.

7F

But a 32-bit integer uses 4 bytes. Those 4 bytes must be placed in some order.

Take this number:

0x12345678

It has four bytes:

12 34 56 78

There are two common ways to store them.

Big-Endian

Big-endian stores the most significant byte first.

12 34 56 78

This matches the way we usually write hexadecimal numbers.

Little-Endian

Little-endian stores the least significant byte first.

78 56 34 12

Many common CPUs use little-endian, including x86-64.

Why This Matters

If one program writes an integer as little-endian and another program reads it as big-endian, the value will be wrong.

For example, these bytes:

78 56 34 12

mean this in little-endian:

0x12345678

But they mean this in big-endian:

0x78563412

Same bytes. Different meaning.

That is why binary file formats and network protocols must define byte order.

Endianness in File Formats

A good binary format says exactly how integers are stored.

Example:

All integers are stored as little-endian.

Then the writer and reader both follow the same rule.

In Zig, do not depend on the machine’s native byte order when reading or writing file formats. Be explicit.

std.mem.writeInt(u32, &buffer, value, .little);
const value = std.mem.readInt(u32, bytes[0..4], .little);

The .little argument is the important part. It says how the bytes should be interpreted.

Writing a Little-Endian Integer

const std = @import("std");

pub fn main() !void {
    var buffer: [4]u8 = undefined;

    std.mem.writeInt(u32, &buffer, 0x12345678, .little);

    for (buffer) |b| {
        std.debug.print("{X:0>2} ", .{b});
    }

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

Expected output:

78 56 34 12

The number is written least significant byte first.

Writing a Big-Endian Integer

const std = @import("std");

pub fn main() !void {
    var buffer: [4]u8 = undefined;

    std.mem.writeInt(u32, &buffer, 0x12345678, .big);

    for (buffer) |b| {
        std.debug.print("{X:0>2} ", .{b});
    }

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

Expected output:

12 34 56 78

The number is written most significant byte first.

Reading Bytes Back

const std = @import("std");

pub fn main() !void {
    const bytes = [_]u8{ 0x78, 0x56, 0x34, 0x12 };

    const little = std.mem.readInt(u32, bytes[0..4], .little);
    const big = std.mem.readInt(u32, bytes[0..4], .big);

    std.debug.print("little = 0x{X}\n", .{little});
    std.debug.print("big    = 0x{X}\n", .{big});
}

Output:

little = 0x12345678
big    = 0x78563412

This example shows the whole issue. The bytes did not change. Only the interpretation changed.

Network Byte Order

Many network protocols use big-endian order.

This is often called network byte order.

For example, if a protocol says a 16-bit port number is stored in network byte order, it means big-endian.

So the number 8080 is:

0x1F90

Stored as big-endian bytes:

1F 90

If you parse protocol data, follow the protocol specification exactly. Do not guess.

Native Endianness

The machine running your program also has a native endianness.

On many desktop and server machines, native endianness is little-endian. But portable code should avoid assuming that.

This is especially important for:

binary files

network packets

disk formats

cross-platform caches

serialized data

data exchanged between different machines

For temporary in-memory calculations, native endianness is usually irrelevant. For stored or transmitted bytes, it matters.

Avoid Pointer Casting for File Data

This is a common mistake:

const value_ptr: *const u32 = @ptrCast(bytes.ptr);
const value = value_ptr.*;

This is unsafe as a parsing pattern.

The bytes may have the wrong alignment.

The bytes may use a different endianness.

The file layout may not match Zig’s in-memory layout.

The input may be too short.

Prefer explicit byte reading:

if (bytes.len < 4) {
    return error.Truncated;
}

const value = std.mem.readInt(u32, bytes[0..4], .little);

This is clearer and safer.

Endianness and Structs

A struct in memory is not the same thing as a file format.

const Header = struct {
    magic: [4]u8,
    count: u32,
};

You might be tempted to write the whole struct to disk. Avoid that for portable formats.

The struct may contain padding. The integer field uses native endianness in memory. Alignment rules can vary. Future field changes can break compatibility.

Instead, define the file layout byte by byte:

bytes 0..4    magic
bytes 4..8    count, u32 little-endian

Then write and read it explicitly:

try file.writeAll("DATA");

var buffer: [4]u8 = undefined;
std.mem.writeInt(u32, &buffer, count, .little);
try file.writeAll(&buffer);

Endianness and Hex Dumps

A hex dump shows bytes in storage order.

If you see:

78 56 34 12

that does not directly say “the number is 0x78563412.”

It says the bytes are stored in this order. To know the number, you need the format’s endianness rule.

If the field is little-endian, the number is:

0x12345678

If the field is big-endian, the number is:

0x78563412

When debugging binary files, always separate these two ideas:

The byte sequence.

The integer interpretation.

Signed Integers

Endianness applies to the byte order of the stored value. Signedness is a separate issue.

For example, a signed 32-bit integer and an unsigned 32-bit integer both use 4 bytes. Endianness decides the byte order. The integer type decides how the resulting bits are interpreted.

const signed_value = std.mem.readInt(i32, bytes[0..4], .little);
const unsigned_value = std.mem.readInt(u32, bytes[0..4], .little);

Same bytes. Same byte order. Different numeric interpretation.

Floating Point Values

Floating point values also occupy multiple bytes, so byte order matters for them too.

For portable binary formats, a common approach is:

read the bytes as an integer with explicit endianness

bit-cast the integer into a float

Example for a 32-bit float:

const bits = std.mem.readInt(u32, bytes[0..4], .little);
const value: f32 = @bitCast(bits);

Writing is the reverse:

const bits: u32 = @bitCast(value);

var buffer: [4]u8 = undefined;
std.mem.writeInt(u32, &buffer, bits, .little);

This keeps byte order explicit.

Choosing an Endianness

For your own binary formats, choose one byte order and document it.

Little-endian is common for local file formats and databases.

Big-endian is common in many network protocols.

The most important rule is consistency. Every reader and writer must agree.

A format should say:

All u16, u32, u64, i16, i32, and i64 fields are little-endian unless otherwise stated.

That one sentence prevents many bugs.

A Small Parser Example

Suppose a file begins with this header:

bytes 0..4    magic: "SIZE"
bytes 4..8    width, u32 little-endian
bytes 8..12   height, u32 little-endian

Parser:

const std = @import("std");

const Header = struct {
    width: u32,
    height: u32,
};

const ParseError = error{
    BadMagic,
    Truncated,
};

fn parseHeader(bytes: []const u8) ParseError!Header {
    if (bytes.len < 12) {
        return error.Truncated;
    }

    if (!std.mem.eql(u8, bytes[0..4], "SIZE")) {
        return error.BadMagic;
    }

    const width = std.mem.readInt(u32, bytes[4..8], .little);
    const height = std.mem.readInt(u32, bytes[8..12], .little);

    return Header{
        .width = width,
        .height = height,
    };
}

Notice what the parser does not do.

It does not cast the bytes to a struct.

It does not use native endianness.

It does not assume the input is long enough.

It reads the format exactly as specified.

Mental Model

Endianness is byte order.

When a value needs more than one byte, the program must know which byte comes first.

For normal arithmetic, you usually do not think about this. For files, network packets, and binary protocols, you must think about it every time you read or write a multi-byte value.

In Zig, the safe habit is simple: use explicit byte-order functions. Read and write integers as bytes with .little or .big, and avoid treating raw file bytes as native structs.