Endianness adalah urutan byte yang dipakai mesin untuk menyimpan nilai multibyte (mis. uint32_t) di memori. Arsitektur x86, x86-64, dan ARM (mode default) menggunakan little-endian: byte paling tidak signifikan disimpan di alamat terendah. Arsitektur lain (mis. SPARC, PowerPC, sebagian mikrokontroler ARM big-endian) menggunakan big-endian: byte paling signifikan di alamat terendah. Protokol jaringan TCP/IP dan banyak format file biner memakai big-endian sebagai “network byte order”. Bab ini menunjukkan tiga cara mendeteksi endianness mesin (cast ke unsigned char*, union, dan macro preprocessor), mendemonstrasikan fungsi htonl/ntohl untuk konversi jaringan, dan menulis parser byte-order yang portabel tanpa mengandalkan endianness mesin. Kita juga akan melihat bagaimana trik memcpy menghindari pelanggaran strict aliasing saat mengkonversi antara tipe.
Mendeteksi Endianness Mesin #
Cara paling langsung mendeteksi endianness adalah menulis nilai 32-bit yang byte-nya mudah dibedakan, lalu meng-cast alamatnya ke unsigned char* dan memeriksa byte pertama. Pada mesin little-endian, byte pertama berisi byte paling tidak signifikan; pada mesin big-endian, byte pertama berisi byte paling signifikan.
// File: endian_1.c - Deteksi endianness dengan char* dan union
#include <stdio.h>
#include <stdint.h>
#include <arpa/inet.h>
int main(void) {
uint32_t nilai = 0x41424344; // ASCII: 'A' 'B' 'C' 'D'
// Metode 1: cast ke unsigned char* dan lihat byte pertama
unsigned char *p = (unsigned char *)&nilai;
printf("Nilai asli : 0x%08x\n", nilai);
printf("Byte 0 (terendah) : 0x%02x\n", p[0]);
printf("Byte 1 : 0x%02x\n", p[1]);
printf("Byte 2 : 0x%02x\n", p[2]);
printf("Byte 3 (tertinggi) : 0x%02x\n", p[3]);
if (p[0] == 0x44) {
printf("=> Mesin LITTLE-ENDIAN (byte rendah dulu)\n");
} else if (p[0] == 0x41) {
printf("=> Mesin BIG-ENDIAN (byte tinggi dulu)\n");
} else {
printf("=> Endianness tidak dikenali\n");
}
// Metode 2: union (pola umum di kode lama)
union {
uint32_t angka;
unsigned char byte[4];
} u;
u.angka = 0x41424344;
printf("\nMetode union - byte[0] = 0x%02x\n", u.byte[0]);
// Metode 3: htonl untuk verifikasi network byte order
uint32_t net = htonl(nilai);
printf("\nhtonl(0x%08x) = 0x%08x (network byte order)\n", nilai, net);
printf("ntohl(htonl(x)) = 0x%08x (kembali ke host order)\n", ntohl(net));
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -pedantic -o endian_1 endian_1.c
./endian_1Output yang Dihasilkan:
Nilai asli : 0x41424344
Byte 0 (terendah) : 0x44
Byte 1 : 0x43
Byte 2 : 0x42
Byte 3 (tertinggi) : 0x41
=> Mesin LITTLE-ENDIAN (byte rendah dulu)
Metode union - byte[0] = 0x44
htonl(0x41424344) = 0x44434241 (network byte order)
ntohl(htonl(x)) = 0x41424344 (kembali ke host order)Mari kita telaah baris-baris kode di atas:
- Baris 9-22 (cast ke
unsigned char*): Variabelnilaiberisi0x41424344. Saat di-cast keunsigned char *dan dibaca byte-per-byte, hasilnya0x44 0x43 0x42 0x41di mesin little-endian x86-64. Byte paling tidak signifikan (0x44) disimpan di alamat terendah. Inilah ciri khas little-endian. - Baris 24-31 (
union { uint32_t angka; unsigned char byte[4]; } u;): Trik union membuat dua anggota berbagi alamat yang sama. Karenau.angkadanu.byteberada di alamat yang sama, menulisu.angka = 0x41424344sekaligus mengisiu.byte[0..3]dengan representasi byte mesin. Trik ini umum di kode lama tetapi secara teknis undefined behavior di C standar ketika menulis satu anggota lalu membaca anggota lain—lebih aman pakai castunsigned char*seperti Metode 1. - Baris 34-37 (
uint32_t net = htonl(nilai); ... ntohl(net);): Fungsihtonl(host-to-network-long) mengkonversi 32-bit dari host byte order ke network byte order (big-endian). Di mesin little-endian,htonl(0x41424344)menghasilkan0x44434241—byte dibalik. Fungsintohladalah kebalikannya. Di mesin big-endian,htonldanntohladalah no-op karena host order sudah sama dengan network order.
Konversi Byte Order yang Portabel Tanpa htonl #
Fungsi htonl/ntohl adalah bagian dari POSIX (header <arpa/inet.h>), bukan standar ISO C. Untuk kode yang harus portabel ke compiler non-POSIX (mis. MSVC di Windows, compiler embedded), kita perlu menulis fungsi konversi sendiri. Strateginya: baca byte-per-byte dari buffer dan susun kembali ke uint32_t dengan operasi shift, tanpa mengandalkan endianness mesin.
// File: endian_2.c - Konversi manual big-endian ke host (portable)
#include <stdio.h>
#include <stdint.h>
#include <string.h>
// Parse 4 byte dari buffer big-endian (network order) ke uint32_t host
static uint32_t parse_be_u32(const unsigned char *buf) {
return ((uint32_t)buf[0] << 24) |
((uint32_t)buf[1] << 16) |
((uint32_t)buf[2] << 8) |
((uint32_t)buf[3] );
}
// Tulis uint32_t host ke buffer big-endian
static void write_be_u32(unsigned char *buf, uint32_t val) {
buf[0] = (unsigned char)(val >> 24);
buf[1] = (unsigned char)(val >> 16);
buf[2] = (unsigned char)(val >> 8);
buf[3] = (unsigned char)(val );
}
// Parse 4 byte little-endian
static uint32_t parse_le_u32(const unsigned char *buf) {
return ((uint32_t)buf[3] << 24) |
((uint32_t)buf[2] << 16) |
((uint32_t)buf[1] << 8) |
((uint32_t)buf[0] );
}
int main(void) {
// Simulasi data jaringan (big-endian) untuk angka 0x12345678
unsigned char paket[] = { 0x12, 0x34, 0x56, 0x78 };
uint32_t parsed = parse_be_u32(paket);
printf("Buffer BE {0x12,0x34,0x56,0x78} -> 0x%08x\n", parsed);
// Tulis balik ke buffer BE
unsigned char out[4];
write_be_u32(out, 0xAABBCCDD);
printf("write_be_u32(0xAABBCCDD) -> {%02x,%02x,%02x,%02x}\n",
out[0], out[1], out[2], out[3]);
// Data little-endian (mis. format file WAV)
unsigned char wav_size[] = { 0x78, 0x56, 0x34, 0x12 };
uint32_t sz = parse_le_u32(wav_size);
printf("Buffer LE {0x78,0x56,0x34,0x12} -> 0x%08x (= %u)\n", sz, sz);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -pedantic -o endian_2 endian_2.c
./endian_2Output yang Dihasilkan:
Buffer BE {0x12,0x34,0x56,0x78} -> 0x12345678
write_be_u32(0xAABBCCDD) -> {aa,bb,cc,dd}
Buffer LE {0x78,0x56,0x34,0x12} -> 0x12345678 (= 305419896)Fungsi parse_be_u32 membaca empat byte dari buffer dan menyusunnya menjadi uint32_t dengan urutan big-endian: byte index 0 menjadi byte paling signifikan (di-shift ke kiri 24), byte index 3 menjadi byte paling tidak signifikan. Operasi ini tidak bergantung pada endianness mesin—hasilnya sama baik di x86 (little-endian) maupun di SPARC (big-endian). Fungsi write_be_u32 adalah kebalikannya: menulis uint32_t ke buffer dalam urutan big-endian. Fungsi parse_le_u32 menangani format little-endian yang dipakai format file seperti WAV, BMP, atau FAT32.
Trik memcpy untuk Konversi Aman #
Untuk konversi antara tipe yang berbeda ukuran (mis. uint32_t ke float), standar C melarang cast pointer langsung karena melanggar strict aliasing. Cara yang aman adalah memcpy dari satu tipe ke tipe lain—compiler modern mengoptimalkan memcpy untuk ukuran kecil menjadi satu instruction move, jadi tidak ada overhead.
// File: endian_3.c - memcpy untuk reinterpretasi tipe aman
#include <stdio.h>
#include <stdint.h>
#include <string.h>
static float bytes_to_float_be(const unsigned char *buf) {
uint32_t raw = ((uint32_t)buf[0] << 24) |
((uint32_t)buf[1] << 16) |
((uint32_t)buf[2] << 8) |
((uint32_t)buf[3] );
float f;
memcpy(&f, &raw, sizeof f); // bitwise copy, tidak UB
return f;
}
static void float_to_bytes_be(unsigned char *buf, float val) {
uint32_t raw;
memcpy(&raw, &val, sizeof raw);
buf[0] = (unsigned char)(raw >> 24);
buf[1] = (unsigned char)(raw >> 16);
buf[2] = (unsigned char)(raw >> 8);
buf[3] = (unsigned char)(raw );
}
int main(void) {
// Float 3.14f dalam IEEE-754 binary32 = 0x4048F5C3
unsigned char buf[4];
float_to_bytes_be(buf, 3.14f);
printf("float 3.14f -> bytes BE : {%02x,%02x,%02x,%02x}\n",
buf[0], buf[1], buf[2], buf[3]);
float back = bytes_to_float_be(buf);
printf("bytes -> float : %.6f\n", back);
// Bandingkan dengan uint32_t
uint32_t raw = ((uint32_t)buf[0] << 24) | ((uint32_t)buf[1] << 16) |
((uint32_t)buf[2] << 8) | ((uint32_t)buf[3] );
printf("Raw uint32_t : 0x%08x\n", raw);
return 0;
}Compile dan jalankan:
gcc -std=c11 -Wall -Wextra -pedantic -o endian_3 endian_3.c
./endian_3Output yang Dihasilkan:
float 3.14f -> bytes BE : {40,48,f5,c3}
bytes -> float : 3.140000
Raw uint32_t : 0x4048f5c3Trik memcpy(&f, &raw, sizeof f) menyalin byte raw ke f secara bitwise. Karena memcpy mengambil void *, ini tidak melanggar strict aliasing. Compiler modern (GCC, Clang, MSVC) mengoptimalkan memcpy dengan ukuran tetap (4 byte) menjadi satu instruction mov, sehingga tidak ada overhead runtime. Pola memcpy ini adalah cara standar untuk reinterpretasi bit pattern di kode portable—banyak dipakai di library serialisasi seperti protobuf-c, MessagePack, dan CBOR.