Kawałek Kodu

Dawno, dawno temu, na dzikim zachodzie żyły dwa plemiona Indian. Jedni posługiwali się komputerami PC, drudzy komputerami Apple. Przez tysiące lat nie mogli porozumieć się ze sobą nadając znaki dymne poprzez sterowane komputerami dymiarki. Każde z plemion uważało, że drugie plemię posiada niezrównoważonego teledymiarza. Całe szczęście, że przypadek doprowadził do spotkania wodzów obydwu plemion na giełdzie komputerowej. Chwila na fajce ułatwiła nawiązanie kontaktu i zrozumienie zasady nadawania przekazów. Jeden z nich postanowił nazwać swe plemię Little Endian, a drugi Big Endian.

Dlaczego się nie dogadali?

Jak się okazało obydwa plemiona przekazywały informacje w dwóch różnych zapisach. Do dziś zapisy te noszą nazwy szczepów. Już we wpisie The Fast & The Canvas, czyli szybki dostęp do pikseli na Canvas wspominałem o tym, że komputery stosują różne zapisy i czasem trzeba samemu dbać o ich poprawność. Pokażę Ci na czym polegają różnice. Będziemy się posługiwać zapisem szesnastkowym liczb, bo ten w najprostszy sposób ukaże odmienności. Do tych odmienności dochodzi przy zapisie conajmniej dwóch bajtów.

Załóżmy, że mamy liczbę FF01. Potraktujemy ją jako liczbę bez znaku, czyli w postaci dziesiętnej będzie to 65281 (FF to 255, 01 to 1, a więc 256*255+1). Gwoli ścisłości, dla zapisu nie ma znaczenia czy to liczba ze znakiem czy bez - znak zawsze jeśli jest, jest przechowywany w najstarszym bajcie.
Najmniejszą jednostką informacji powodującą różnice w zapisie jest bajt (8 bitów). Skoro mamy tu dwa bajty, to ta różnica wystąpi między nimi.

little endian big endian
01FF FF01

Jak widzisz zapis big endian jest bardziej naturalnym zapisem, bo bajty reprezentujące liczbę zapisywane są w takiej kolejności jak my widzimy, a dokładnie od najbardziej znaczącego bajtu. W przypadku little endian jest zupełnie odwrotnie.

Jeśli liczba będzie 32 bitowa (4 bajty), np. 77AEBB50, to wtedy sytuacja wygląda tak:

little endian big endian
50BBAE77 77AEBB50

czyli analogicznie do dwóch bajtów - w little endian od najmniej znaczącego bajtu.

Jak się porozumiewać.

Jeśli chcemy odczytywać lub zapisywać dane, musimy więć zadbać o odpowiednią kolejność bajtów, aby albo uzyskać, albo wstawić do "pamięci" poprawną liczbę. Zajmijmy się najpierw odczytem danych zakładając, że są one w zapisie little endian.

bajt przesunięcie w lewo (<<) wynik cząstkowy poprzednia wartość wynik (OR)
50 0 50 0 50
BB 8 BB00 50 BB50
AE 16 AE0000 BB50 AEBB50
77 24 77000000 AEBB50 77AEBB50

Kiedy odczytujemy dane w zapisie big endian "pchamy" kolejny bajty w lewo.

bajt przesunięcie w prawo (>>) wynik cząstkowy poprzednia wartość wynik (OR)
77 24 77000000 0 77000000
AE 16 AE0000 77000000 77AE0000
BB 8 BB00 77AE0000 77AEBB00
50 0 50 77AEBB00 77AEBB50

Przy zapisie danych postępujemy analogicznie odwrotnie. Czyli dla little endian:

liczba maska wynik cząstkowy przesunięcie w prawo (>>) bajt
77AEBB50 000000FF 50 0 50
77AEBB50 0000FF00 BB00 8 BB
77AEBB50 00FF0000 AE0000 16 AE
77AEBB50 FF000000 77000000 24 77

Możemy też postąpić trochę inaczej:

aktualna wartość maska (AND) bajt przesunięcie w prawo (>>) pozostała wartość
77AEBB50 000000FF 50 8 0077AEBB
0077AEBB 000000FF BB 8 000077AE
000077AE 000000FF AE 8 00000077
00000077 000000FF 77 - -

Dla big endian trzeba zrobić tak:

liczba maska (AND) wynik cząstkowy przesunięcie w prawo (>>) bajt
77AEBB50 FF000000 77000000 24 77
77AEBB50 00FF0000 00AE0000 16 AE
77AEBB50 0000FF00 0000BB00 8 BB
77AEBB50 000000FF 00000050 0 50

Te przykłady dotyczą sytuacji kiedy zapisując wynikowe bajty możesz zwiększać indeks tablicy o 1. Jeśli jednak masz możliwość manipulowania indeksem tablicy, to przy zapisie big endian możesz wykorzystać metodę zapisu little endian, ale wstawiając kolejne bajty pod indeksy 3, 2, 1, 0, a nie 0, 1, 2, 3. Albo wykorzystać metodę dla big endian przy zapisie little endian, również zmieniając kolejność wstawiania bajtów.

Kod pokoju.

Skoro już wiesz jak przechowywane są dane, to zanim zajmiemy się kodem do konwersji, pokażę Ci jak sprawdzić jaki format zapisu stosuje platforma, na której pracujesz. Wykorzystamy tu typowane tablice (o nich w przyszłym wpisie), a dokładnie Uint16Array oraz Uint8Array. Zapiszemy daną 16-bitową (00 01), a następnie odczytamy bajt spod indeksu 0. Jeśli to najmniej znaczący bajt liczby, to pracujesz na platformie obsługującej little endian (zapis w typowanej tablicy "uwalnia" nas od dbania o sposób zapisu, bo ten jest uzależniony od komputera, na którym jest uruchamiany kod).

const isLittleEndian = new Uint8Array(new Uint16Array([1]).buffer)[0] === 1;

No dobra, kod. Konwersja przy odczycie:

/* z little endian */
let bytes = [0x50, 0xBB, 0xAE, 0x77];
let value = ((((bytes[3] << 8) | bytes[2]) << 8) | bytes[1]) << 8 | bytes[0];

/* z big endian */
let bytes = [0x77, 0xAE, 0xBB, 0x50];
let value = ((((bytes[0] << 8) | bytes[1]) << 8) | bytes[2]) << 8 | bytes[3];

Konwersja przy zapisie:

/* na little endian */
let value = 0x77AEBB50;
let bytes = [];
bytes[0] = value & 0xFF;
value >>= 8;
bytes[1] = value & 0xFF;
value >>= 8;
bytes[2] = value & 0xFF;
value >>= 8;
bytes[3] = value & 0xFF;

/* na big endian */
let value = 0x77AEBB50;
let bytes = [];
bytes[0] = (value & 0xFF000000) >> 24;
bytes[1] = (value & 0x00FF0000) >> 16;
bytes[2] = (value & 0x0000FF00) >> 8;
bytes[3] = (value & 0x000000FF);

I odtąd plemiona pozostawały w przyjaźni...
Tak jak obiecałem w jakimś przyszłym wpisie opowiem o typowanych tablicach oraz pogadamy o... słowach.