Low-Level Systems Programming

Endianness

Endianness defines the sequential byte order in which multi-byte integers (u16, u32, u64, u128) are stored in memory or transmitted over a communication link:

  • Big-Endian (BE / Network Byte Order): Stores the most significant byte (MSB) at the lowest memory address (0x12345678 -> [0x12, 0x34, 0x56, 0x78]). Used by TCP/IP headers, DNS, TLS, and PostgreSQL wire protocols.
  • Little-Endian (LE): Stores the least significant byte (LSB) at the lowest memory address (0x12345678 -> [0x78, 0x56, 0x34, 0x12]). Used natively by x86_64, aarch64 (standard OS targets), riscv64, and wasm32, as well as file formats like WASM, ELF (on LE targets), Parquet, and ZIP.
32-bit Integer Value: `0x1234_5678`

Memory Offset:       [ +0 ]   [ +1 ]   [ +2 ]   [ +3 ]
                     ------   ------   ------   ------
Big-Endian (BE):      0x12     0x34     0x56     0x78   (MSB first)
Little-Endian (LE):   0x78     0x56     0x34     0x12   (LSB first)

Explicit Endianness Conversion Methods on All Rust Integers

Never cast a *const u8 byte buffer directly to *const u32 when reading file or network data, as the result will silently invert bytes if the file's endianness differs from the host CPU's endianness (and risks alignment UB). Instead, every Rust integer type provides zero-cost, alignment-safe byte array conversion methods:

Method Signature (on u32) Description
to_be_bytes(self) fn to_be_bytes(self) -> [u8; 4] Converts integer to a big-endian byte array.
from_be_bytes(bytes) fn from_be_bytes(bytes: [u8; 4]) -> u32 Constructs integer from a big-endian byte array.
to_le_bytes(self) fn to_le_bytes(self) -> [u8; 4] Converts integer to a little-endian byte array.
from_le_bytes(bytes) fn from_le_bytes(bytes: [u8; 4]) -> u32 Constructs integer from a little-endian byte array.
to_ne_bytes(self) / from_ne_bytes(b) fn to_ne_bytes(self) -> [u8; 4] Converts to/from the host CPU's native endianness.
to_be(self) / from_be(x) fn to_be(self) -> u32 Swaps bytes (bswap / rev) on little-endian hosts; no-op on big-endian hosts.

On a little-endian CPU (x86_64 or aarch64), u32::from_le_bytes compiles to a single unaligned 32-bit load instruction, and u32::from_be_bytes compiles to a load + bswap/rev (or movbe).

fn main() {
    let value: u32 = 0x1234_5678;

    let be = value.to_be_bytes();
    let le = value.to_le_bytes();

    assert_eq!(be, [0x12, 0x34, 0x56, 0x78]);
    assert_eq!(le, [0x78, 0x56, 0x34, 0x12]);

    assert_eq!(u32::from_be_bytes(be), value);
    assert_eq!(u32::from_le_bytes(le), value);

    let host_endian = if cfg!(target_endian = "little") {
        "little-endian"
    } else {
        "big-endian"
    };

    println!("0x12345678 in BE: {be:02x?}");
    println!("0x12345678 in LE: {le:02x?}");
    println!("Host target_endian: {host_endian}");
}
0x12345678 in BE: [12, 34, 56, 78]
0x12345678 in LE: [78, 56, 34, 12]
Host target_endian: little-endian

Type-Safe Endianness Wrappers for Binary Structs

When defining a #[repr(C)] struct that maps onto a network header or binary disk record, storing raw u32 fields allows callers to accidentally read a big-endian wire value as if it were a native-endian integer. Wrapping wire integers in a #[repr(transparent)] BigEndianU32 newtype makes endianness conversion impossible to forget:

#[repr(transparent)]
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct BeU16([u8; 2]);

impl BeU16 {
    pub const fn new(val: u16) -> Self {
        Self(val.to_be_bytes())
    }

    pub const fn get(self) -> u16 {
        u16::from_be_bytes(self.0)
    }
}

fn main() {
    let port = BeU16::new(8080);
    println!("Wire bytes: {:02x?}, decoded: {}", port.0, port.get());
}
Wire bytes: [1f, 90], decoded: 8080