Numbers · Lesson 16.10

Endian

Source
Store and load integers as bytes in an explicit order, big endian or little endian, and see what reading them in the wrong order does.

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;
OrderByte 0Byte 1Byte 2Byte 3
Big endian12345678
Little endian78563412

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:

FunctionDoes
StoreBigEndianWrite a value into bytes, most significant first
StoreLittleEndianWrite a value into bytes, least significant first
LoadBigEndianRead a value back out of big-endian bytes
LoadLittleEndianRead a value back out of little-endian bytes
ReverseBytesSwap 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.

Src/Main.rux
// 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

Pointing at the whole array.
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].
Storing an untyped literal.
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.
Relying on the machine's order.
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

  1. Store the uint16 0xBEEF both ways and print the bytes with ShowBytes.
  2. Store value big endian, then read it back with LoadLittleEndian. Which number do you get — and which function gives the same answer without any bytes?
  3. Store the uint64 1 big endian into a uint8[8] and print the bytes. Where did the 01 go?

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