W komentarzu do wpisu o flagach bitowych zostałem poproszony przez jednego z komentujących, Bartosza Wójcika, o opisanie obsługi wartości typu DWORD w PHP. Wychodząc naprzeciw tej prośbie, rozbiłem temat na pewnego rodzaju wprowadzenie we wpisie o odczytywaniu wartości bajtów w zmiennych liczbowych, a dzisiaj podejmuję temat właściwy. Zapraszam do lektury, a także zgłaszania własnych pomysłów na tematy wpisów, o których chcielibyście przeczytać. Żadne zgłoszenie nie zostanie przeze mnie pozostawione bez echa. ;]

Fotografia: Syntopia, CC-BY.

DWORD? A co to?

PHP to język wysokiego poziomu, ukrywający przed programistą pod płaszczem abstrakcji wiele “brzydkich” funkcjonalności, którymi taki programista mógłby sobie i serwerowi zrobić “krzywdę”. Nie wszyscy są aż tak dociekliwi, żeby jednak zajrzeć pod maskę interpretera, dlatego zanim przejdziemy do właściwych wyjaśnień, zobaczmy, o czym dzisiaj będę pisać, a Wy czytać. ;]

A więc pani przedszkolanka pyta dzieci: “co to jest słowo”? Dzieci grzecznie odpowiadają: “słowo, jest to nominalny rozmiar danych, jakim posługuje się procesor. Rozmiar ten zazwyczaj to wielokrotność potęgi dwójki w bajtach, a we współczesnych komputerach przyjęło się, że wynosi on ogólnie 2 bajty, czyli 16 bitów”.

Pani przedszkolanka, nieco zdziwiona i zaskoczona, ale także zadowolona z poziomu wykształcenia swoich wychowanków nie daje jednak za wygraną. Nie pokazując po sobie wewnętrznych emocji krótko i celnie strzela w tarczę umysłów dzieci pytaniem: “a co to jest DWORD?”. Uśmiechy maluchów, rozochocone pierwszym pytaniem zdają się chować w ciemności poważnych min. “Już po nas”, przemknęło przez myśl siedzącym w ławkach uczniom. W pewnym momencie jednak niczym iglica wieży wśród płaszczyzny spuszczonych głów wystrzeliła do góry ręka siedzącego cicho Jasia. “Taak?”, spytała z ciekawością pani. “DWORD, to podwójne słowo - double word” - mówił dostojnym głosem Jan - “jest to 32-bitowa wartość typu unsigned int”. Wszystkie dzieci zwróciły głowę w stronę ławki swojego kolegi, a potem na biurko samej pani przedszkolanki. Ta, czując na sobie światło trzydziestu laserowych celowników spojrzeń zdołała jedynie wykrzesać z siebie krótkie i urywane: “T… ttak… tak, zgadza się”.

“Dobrze” - powiedziała pani przedszkolanka - “skoro jesteście tacy mądrzy, to jako pracę domową przygotujecie informacje na temat obsługi wartości typu DWORD… w języku PHP”. W klasie słychać było pomruk niezadowolenia. “Koniec zajęć, zmykajcie do domów” - tymi słowami pani zakończyła zajęcia.

Zapraszam do lektury mojej pracy domowej. ;]

Zagadnienia.

Obsługa wartości DWORD w kodzie PHP nie jest zbytnio skomplikowana, aczkolwiek jest kilka problemów, z jakimi można się w tym przypadku zetknąć. W dzisiejszym wpisie skupimy się następujących zagadnieniach:
  • odczytywanie wartości DWORD,
  • zapisywanie wartości DWORD.
Do dzieła!

Tytułem wstępu: tworzenie wartości DWORD.

Szukając informacji na temat tworzenia wartości DWORD w Internecie, natrafiłem na 3 różne sposoby uzyskania tego typu zmiennej. Opierają się one na następujących pomysłach:
  • przesunięcie bitowe,
  • mnożenie
  • kombinacja funkcji pack() i unpack() w PHP.
Rozważmy więc przykładowy kod. W tablicy są zapisane kolejne bajty, które będą razem stanowić wartość finalną:

$arr = array(127, 0, 0, 1); // localhost ;]

$dword = (($arr[0] &amp; 0xFF) << 24) | (($arr[1] &amp; 0xFF) << 16) | (($arr[2] &amp; 0xFF) << 8) | ($arr[3] &amp; 0xFF);
echo $dword."\n";

$dword = $arr[3] + $arr[2] * 0x100 + $arr[1] * 0x10000 + $arr[0] * 0x1000000;
echo $dword."\n";

$dword = array_shift(unpack('L', pack('CCCC', $arr[3], $arr[2], $arr[1], $arr[0])));
echo $dword."\n";

// result:
// 2130706433
// 2130706433
// 2130706433

W każdym przypadku otrzymujemy wartość będącą połączenie wartości binarnych każdej z wartości tablicy. Mając na uwadze, że liczba 127 jest równa 0x7F w systemie szesnastkowym, otrzymaliśmy ekwiwalent wartości 0x7F000001. Liczba ta jest dosyć duża (2130706433), ale nie ma to znaczenia - mieści się w zakresie liczb całkowitych obsługiwanym przez interpreter. Zobaczmy jednak, co się stanie, jeśli wprowadzimy do manipulacji tablicę zawierającą maksymalną wartość dopuszczalną - 0xFFFFFFFF:

$arr = array(255, 255, 255, 255);

(...)

// result
-1
4294967295
-1

Jedynie mnożenie dało nam oczekiwaną wartość - w innych przypadkach został zgłoszony błąd przepełnienia, co zostało obsłużone poprzez podstawienie wartości -1. Wynika to z faktu, że typ integer w PHP jest znakowany [signed], a więc zdolny obsłużyć jedynie połowę zakresu 32 bitowego nieznakowanego [unsigned] typu całkowitego - 2^31 - 1 = 2147483647 = 0x7FFFFFFF. Większe wartości [i tym samym realizację pełnego zakresu DWORD] możemy otrzymać jedynie poprzez rzutowanie do typu float.

Ze względu na fakt, że nie mamy wpływu na działanie funkcji pack() i unpack(), a operacje przesunięcia bitowego, bitowy AND z 0xFF i bitowy OR wyników wykonywane na poszczególnych wartościach bezpośrednio implikują użycie przez interpreter typu całkowitego, nie otrzymamy w powyższych przypadkach poprawnego wyniku. Jedyna możliwość to mnożenie, którego wynik na bieżąco jest kontrolowany i w razie potrzeby konwertowany do większego zakresu. Skupimy się zatem na tym właśnie sposobie.

Tworzenie wartości DWORD ze zbioru bajtów.

Aby otrzymać wartość DWORD z tablicy wartości bajtów, należy pomnożyć ich wartości przez cztery kolejne potęgi liczby 256 [2^8]. Zauważmy, że w przypadku liczby z wpisu o odczytywaniu bajtów [która notabene jest wartością DWORD] wartość 0x11223344 możemy otrzymać wykonując następujące operacje:

$val = 0x11223344;
echo 'val: '.$val."\n"; // val: 287454020
$arr = array(0x11, 0x22, 0x33, 0x44);
$result = 0;
for($i = 3; $i >= 0; $i--)
	{
	echo ($arr[$i] * pow(256, $i))."\n";
	$result += $arr[$i] * pow(256, 3 - $i);
	}
echo 'res: '.$result."\n"; // res: 287454020

Wynika to z faktu, że wynik mnożenia 0x7F * 0x100 wynosi 0x7F00, a więc liczba 7F “wskakuje” na pozycję wskazaną przez heksadecymalną jedynkę.

Przygotowałem więc dla Was gotową - kopiuj-wklej - funkcję array2dword(), która realizuje powyższe założenia - konwersja tablicy bajtów na wartość DWORD. Zauważcie, że zostały tu także wprowadzone zabezpieczenia przed podaniem większej niż 255 wartości pojedynczego bajtu - proste modulo, a cieszy. Oto i ona:

/**
 * Convert array of four bytes into single DWORD value. If array consists of
 * more than 4 elements, only first 4 are used.
 *
 * @param array $bytes Input bytes.
 * @return integer DWORD value.
 */
function array2dword(array $bytes)
	{
	$bytesNumber = count($bytes);
	if($bytesNumber > 4)
		{
		$bytesNumber = 4;
		}
	$result = 0;
	for($i = $bytesNumber - 1; $i >= 0; $i--)
		{
		$result += ($bytes[$i] % 0xFF) * pow(256, $bytesNumber - $i - 1);
		}
	return $result;
	}

Od czasu wysłuchania wykładu o TDD przez Rowana Merewooda na konferencji 4Developers mam ponownie chęć “wdrożyć się” w ten sposób tworzenia oprogramowania. Nie to, żebym miał coś do samej idei TDD, jednak dosyć ciężko jest faktycznie zmusić się do pisania kodu sprawdzającego, szczególnie, jeśli terminy gonią. Dlatego załączam garść własnoręcznie zrobionych testów, bez “bawienia się” w PHPUnit, czy podobne biblioteki - rzekłbym “chałupnicze TDD” ;]:

function testSuite($tests)
	{
	$id = 0;
	foreach($tests as $key => $test)
		{
		$val = array2dword($test);
		echo
			'test '.$id.': '
			.'['.($key == $val ? 'PASSED': 'FAILED').'] '
			.sprintf('%10s == %10s', $key, $val)."\n";
		$id++;
		}
	}

$tests = array(
	0x11223344 => array(0x11, 0x22, 0x33, 0x44),
	0x11223300 => array(0x11, 0x22, 0x33, 0x00),
	0x00223344 => array(0x00, 0x22, 0x33, 0x44),
	0x01010101 => array(0x100, 0x100, 0x100, 0x100),
	);

testSuite($tests);

/*
test 0: [PASSED]  287454020 ==  287454020
test 1: [PASSED]  287453952 ==  287453952
test 2: [PASSED]    2241348 ==    2241348
test 3: [PASSED]   16843009 ==   16843009
*/

Połowa drogi za nami - mamy już wartości DWORD spakowane ładnie w jedną zmienną. Spróbujmy więc teraz odczytać je z powrotem, czyniąc je ponownie przydatnymi do manipulacji dla programisty.

Odczytywanie poszczególnych bajtów z wartości DWORD.

Aby odczytać wartości kolejnych bajtów DWORDa musimy posłużyć się informacjami zawartymi we wpisie o odczytywaniu bajtów z liczby. Wykorzystamy do tego nieco zmienioną funkcję getBytes(), która będzie odczytywała z podanej liczby maksymalnie 4 pierwsze bajty i podawała je w tablicy z odpowiednią podstawą liczbową. Po raz kolejny, kopiuj-wklej funkcja dword2array(), specjalnie dla Was. ;]

/**
 * Convert passed DWORD value into array of bytes. If value exceeds this type
 * value range, only first four bytes are returned.
 *
 * @param integer $number Input DWORD value.
 * @param integer $base Number base for returned results.
 * @return array Array of bytes from input value.
 */
function dword2array($number, $base = 10)
	{
	$bytesNumber = ceil(ceil(log($number, 2)) / 8);
	$bytes = array();
	for($i = $bytesNumber - 1; $i >= 0; $i--)
		{
		$bytes[] = base_convert(($number >> ($i * 8)) &amp; 0xFF, 10, $base);
		}
	return $bytes;
	}

“Testy”:

function testSuite($tests)
	{
	$id = 0;
	foreach($tests as $key => $test)
		{
		$val = dword2array($key, 10);
		echo
			'test '.$id.': '
			.'['.($test == $val ? 'PASSED': 'FAILED').'] '
			.sprintf('%16s == %16s', implode(',', $test), implode(',', $val))."\n";
		$id++;
		}
	}

$tests = array(
	0x11223344 => array(0x11, 0x22, 0x33, 0x44),
	0x11223300 => array(0x11, 0x22, 0x33, 0x00),
	0x00223344 => array(0x22, 0x33, 0x44),
	);

testSuite($tests);

/*
test 0: [PASSED]      17,34,51,68 ==      17,34,51,68
test 1: [PASSED]       17,34,51,0 ==       17,34,51,0
test 2: [PASSED]         34,51,68 ==         34,51,68
*/

Podsumowanie.

I to wszystko w temacie odczytywania i zapisywania wartości DWORD w kodzie PHP. Zapraszam do wyrażania swoich opinii w komentarzach. Być może chcielibyście, aby artykuł został poszerzony o pewne informacje, być może znaleźliście jakiś błąd - będę wdzięczny za wszelkie informacje na ten temat.