#240 Jul 4, 2026

240. to_le_bytes / from_le_bytes — Serialize Integers Without unsafe or Bit-Shifting

Packing a u32 into four bytes by hand means a stack of >> and as u8 casts — and one wrong shift silently corrupts your data. Every integer type already knows how to lay itself out, in the exact endianness you ask for.

The hand-rolled shift-and-mask

Serializing an integer to bytes the manual way is easy to get subtly wrong:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
let n: u32 = 300;

// big-endian by hand: shift, cast, repeat
let bytes = [
    (n >> 24) as u8,
    (n >> 16) as u8,
    (n >> 8) as u8,
    n as u8,
];
assert_eq!(bytes, [0, 0, 1, 44]);

It works, but the shift amounts are magic numbers, the byte order is implicit, and reversing it (parsing bytes back into a u32) means another shift ladder in the opposite direction.

to_*_bytes hands you the array

Every integer type has to_be_bytes, to_le_bytes, and to_ne_bytes, each returning a fixed-size [u8; N]:

1
2
3
4
let n: u32 = 0x12345678;

assert_eq!(n.to_be_bytes(), [0x12, 0x34, 0x56, 0x78]); // big-endian
assert_eq!(n.to_le_bytes(), [0x78, 0x56, 0x34, 0x12]); // little-endian

The endianness is spelled out in the method name, the array length is checked at compile time ([u8; 4] for a u32), and there’s no cast to fumble.

from_*_bytes parses it back

The inverse takes the same fixed-size array and rebuilds the integer:

1
2
3
4
let n: u32 = 300;
let wire = n.to_be_bytes();

assert_eq!(u32::from_be_bytes(wire), 300); // exact round-trip

Reading an integer out of a buffer

The array size is part of the type, so slice a &[u8] and try_into an array before parsing — the length check happens for you:

1
2
3
4
let buf = [0x00, 0x00, 0x01, 0x2c, 0xff]; // 4-byte int + trailing byte

let n = u32::from_be_bytes(buf[..4].try_into().unwrap());
assert_eq!(n, 300);

If the slice is the wrong length, try_into returns an Err instead of reading past the end — no unsafe, no out-of-bounds read.

Use to_be_bytes / from_be_bytes for network and file formats (big-endian is the usual “wire” order), _le_ when a spec demands little-endian, and reach for _ne_ (native) only for in-memory blobs that never leave the machine. Floats have the same methods, and to_bits / from_bits if you want the raw u32/u64 first.

← Previous 239. Vec::drain — Remove a Range and Keep What You Pulled Out Next → 241. slice::split_first — Peel the Head Off a Slice, Keep the Tail, No Indexing