Endian
An integer wider than one byte is stored as several bytes, and something has to decide their order. There are two conventions:
- Little endian puts the least significant byte first.
- Big endian puts the most significant byte first — the order the number is written in.
Today's desktop processors are little endian, while network protocols and many file formats are big endian. So a program that reads or writes bytes — to a file, a socket, a packet — has to say which order it means, rather than take whatever the machine happens to do. Otherwise the same file means different numbers on different machines.
One value, two orders
The program picks a value whose four bytes are all different, so the order is easy to see:
let value: uint32 = 0x12345678;
| Order | Byte 0 | Byte 1 | Byte 2 | Byte 3 |
|---|---|---|---|---|
| Big endian | 12 | 34 | 56 | 78 |
| Little endian | 78 | 56 | 34 | 12 |
Big endian reads like the hexadecimal you wrote; little endian starts at the small end.
Writing bytes in a chosen order
Core has a function for each direction and each order:
| Function | Does |
|---|---|
StoreBigEndian | Write a value into bytes, most significant first |
StoreLittleEndian | Write a value into bytes, least significant first |
LoadBigEndian | Read a value back out of big-endian bytes |
LoadLittleEndian | Read a value back out of little-endian bytes |
ReverseBytes | Swap a value's bytes, turning one order into the other |
They work through pointers. A store writes from a pointer to the first byte onwards, as many bytes as the value's type has:
var little: uint8[4];
var big: uint8[4];
StoreLittleEndian(value, @little[0]);
StoreBigEndian(value, @big[0]);
@little[0] is a pointer to the first element. A uint32 is four bytes, so each store fills the whole array.
flowchart LR
v["uint32 0x12345678"] -- "StoreBigEndian" --> b["12 34 56 78"]
v -- "StoreLittleEndian" --> l["78 56 34 12"]TargetIsLittleEndian() tells you which order this machine uses itself — the program prints true on an ordinary desktop. You rarely need it: the store and load functions give the same bytes on every machine, which is the point.
Reading bytes back
A load reads from a pointer to the first byte and hands the value back through an out-parameter. Here are two bytes from a network packet, where a port number is big endian:
let packet: uint8[2] = [0x01, 0xBB];
var port: uint16 = 0;
LoadBigEndian(@packet[0], @port);
0x01BB is 443, the HTTPS port. The type of port decides how many bytes are read — two for a uint16.
The order is part of the data
Bytes written big endian must be read big endian, on every machine. Read the same two bytes in the wrong order and you get a different number entirely:
PrintLine("wrong order {}", ReverseBytes(port));
0xBB01 is 47873. Nothing fails — the bytes are just misread. That is why a file format or protocol always says which order it uses, and why code that reads one should name that order explicitly.
The program
The whole lesson is one package in the Examples repository. Its comments explain every step.
// An integer wider than one byte is stored as several bytes, and something has to decide their
// order. Little endian puts the least significant byte first; big endian puts the most significant
// first, the order the number is written in. Today's desktop processors are little endian, while
// network protocols and many file formats are big endian, so a program that reads or writes bytes
// has to say which order it means rather than take whatever the machine does.
//
// `Core` provides both directions for any integer type:
//
// StoreBigEndian, StoreLittleEndian write a value into bytes, in the order named
// LoadBigEndian, LoadLittleEndian read a value back out of bytes
// ReverseBytes swap a value's bytes, turning one order into the other
//
// They work through pointers, as in the Memory part: a store writes from a pointer to the first
// byte onwards, and a load reads from one and hands the value back through an out-parameter.
// The order is part of the data: bytes written big endian must be read big endian, on every
// machine.
import Core::{ LoadBigEndian, ReverseBytes, StoreBigEndian, StoreLittleEndian,
TargetIsLittleEndian };
import Io::{ Print, PrintLine };
func ShowBytes(label: char8[..], bytes: uint8[..]) {
Print("{}", label);
for i in 0..bytes.length {
Print(" {:02x}", bytes[i]);
}
PrintLine();
}
func Main() -> int {
PrintLine("this machine is little endian: {}", TargetIsLittleEndian());
// One value with four distinct bytes, so the order is easy to see.
let value: uint32 = 0x12345678;
PrintLine("value {:#x}", value);
var little: uint8[4];
var big: uint8[4];
StoreLittleEndian(value, @little[0]);
StoreBigEndian(value, @big[0]);
ShowBytes("little endian ", little[..]);
ShowBytes("big endian ", big[..]);
// Reversing the bytes turns one order into the other.
PrintLine("ReverseBytes {:#x}", ReverseBytes(value));
PrintLine("");
// Reading: two bytes from a network packet, where a port number is big endian.
let packet: uint8[2] = [0x01, 0xBB];
var port: uint16 = 0;
LoadBigEndian(@packet[0], @port);
PrintLine("port {}", port);
// The same bytes read in the wrong order give a different number entirely.
PrintLine("wrong order {}", ReverseBytes(port));
return 0;
}
Besides Io, its Rux.toml lists Core under [Dependencies].
Run it
cd Examples/Numbers/Endian
rux run
this machine is little endian: true
value 0x12345678
little endian 78 56 34 12
big endian 12 34 56 78
ReverseBytes 0x78563412
port 443
wrong order 47873
Common mistakes
The functions want a pointer to the first byte, not to the array.
StoreBigEndian(value, @big) fails with error: argument 1 to 'StoreBigEndian' has type 'uint32', but parameter 'value' requires 'T' — the message names the value, but the fix is the pointer: @big[0].The value's type decides how many bytes are written.
StoreBigEndian(0x12345678, @x[0]) stores an int — eight bytes, not four — and a four-byte array has no room for them. Store a typed value, such as the program's let value: uint32.Writing a number's memory straight to a file works until the file is read on a machine of the other order. Choose an order for the data and use the matching
Store… and Load… functions, whatever TargetIsLittleEndian() says.Try it yourself
- Store the
uint160xBEEFboth ways and print the bytes withShowBytes. - Store
valuebig endian, then read it back withLoadLittleEndian. Which number do you get — and which function gives the same answer without any bytes? - Store the
uint641big endian into auint8[8]and print the bytes. Where did the01go?
Learn more
- Pointer and Out-parameter — the pointers these functions work through
- Shift — taking a number apart byte by byte by hand
- Binary — reading and writing binary files
- Pointers in the Rux Reference