Skip to main content

Byte Order

Byte order (endianness) determines how multi-byte values are stored in memory.

Big-Endian vs Little-Endian

Consider the integer 0x12345678 stored in a 4-byte buffer:

Most significant byte first (network byte order)
BIG-ENDIAN
[0]
12
[1]
34
[2]
56
[3]
78
Least significant byte first (x86/x64 native)
LITTLE-ENDIAN
[0]
78
[1]
56
[2]
34
[3]
12

Specifying Byte Order

import com.ditchoom.buffer.ByteOrder

// Big-endian (default)
val bigEndian = PlatformBuffer.allocate(
size = 1024,
byteOrder = ByteOrder.BIG_ENDIAN
)

// Little-endian
val littleEndian = PlatformBuffer.allocate(
size = 1024,
byteOrder = ByteOrder.LITTLE_ENDIAN
)

Checking Byte Order

val buffer = PlatformBuffer.allocate(1024)
println(buffer.byteOrder) // BIG_ENDIAN

When to Use Each

Big-Endian (BIG_ENDIAN)

  • Network protocols: TCP/IP, HTTP, TLS
  • File formats: PNG, JPEG, PDF
  • Default for ByteBuffer
// Network packet parsing
val buffer = PlatformBuffer.allocate(1024, byteOrder = ByteOrder.BIG_ENDIAN)
buffer.writeShort(80) // Port number in network byte order

Little-Endian (LITTLE_ENDIAN)

  • x86/x64 native: Intel/AMD processors
  • Windows file formats: BMP, WAV
  • Some protocols: USB, PCIe
// Windows BMP file parsing
val buffer = PlatformBuffer.allocate(1024, byteOrder = ByteOrder.LITTLE_ENDIAN)
buffer.writeInt(fileSize)

Common Protocol Byte Orders

Protocol/FormatByte Order
TCP/IPBig-Endian
HTTPBig-Endian
MQTTBig-Endian
WebSocketBig-Endian
USBLittle-Endian
BMPLittle-Endian
WAVLittle-Endian
Protocol BuffersLittle-Endian (varint)

Example: Protocol Parsing

// MQTT uses big-endian for packet length
val mqttBuffer = PlatformBuffer.allocate(1024, byteOrder = ByteOrder.BIG_ENDIAN)

// Write packet type and length
mqttBuffer.writeByte(0x30) // PUBLISH packet
mqttBuffer.writeShort(topicLength.toShort())
mqttBuffer.writeString(topic)

Mixing Byte Orders

If you need to read/write different byte orders in the same buffer, read the largest primitive and swap bytes:

// Read a little-endian int from a big-endian buffer
// Uses one memory read (readInt) instead of 4 separate readByte() calls
fun ReadBuffer.readLittleEndianInt(): Int {
val bigEndian = readInt()
return ((bigEndian and 0xFF) shl 24) or
((bigEndian and 0xFF00) shl 8) or
((bigEndian and 0xFF0000) ushr 8) or
((bigEndian and 0xFF000000.toInt()) ushr 24)
}

// Similarly for Long (swap all 8 bytes)
fun ReadBuffer.readLittleEndianLong(): Long {
val bigEndian = readLong()
return ((bigEndian and 0xFF) shl 56) or
((bigEndian and 0xFF00) shl 40) or
((bigEndian and 0xFF0000) shl 24) or
((bigEndian and 0xFF000000) shl 8) or
((bigEndian and 0xFF00000000) ushr 8) or
((bigEndian and 0xFF0000000000) ushr 24) or
((bigEndian and 0xFF000000000000) ushr 40) or
((bigEndian ushr 56) and 0xFF)
}

Best Practices

  1. Use big-endian for network code - it's the standard
  2. Match the protocol spec - always check documentation
  3. Be explicit - specify byte order even when using default
  4. Document byte order - especially at API boundaries
  5. Read largest primitive - swap bytes after rather than reading byte-by-byte