PHPは90年発のプログラミング言語にしては、バイナリを扱う方法に乏しく、思ったようにバイナリを扱えないことが多い。バイナリを扱った事例も少ないためか、バイナリを扱うためのイディオム的な物もなかなか見つからない。
そこで、PHPでバイナリを極力効率的に扱う方法をこの記事に記すことで、より多くの人にPHPでバイナリを扱う際の助けになればと思う。
PHPではバイナリもstring
型として扱う
PHPでは文字列とバイナリの型は同じくstring
型である。Pythonのようにstring
型とbytes
型とで別れている、ということはないし、使う関数もまったく同じである。
しかし、"\x00"
と言った感じでエスケープ文字を使えば文字コードを直接文字列リテラルとして表現することは可能である*1。
たとえば、以下のように、文字コードだけで一つの文章を表現することもできる。
<?php echo "\x49\x20\x6c\x6f\x76\x65\x20\x50\x48\x50\x2e"; ?>
以上を実行した結果が以下である。
I love PHP.
これにより、PHPでは表現できないようなバイナリも文字コードと言う形で表現することが可能である。これはたとえばバイナリを1byteの整数として扱いたい場合に便利である。
整数からバイナリを生成する
また、pack()
関数を使うことで、整数からバイナリを作ることも可能である。
ビッグエンディアンオーダーのバイナリを作る
ビッグエンディアンとは、複数バイトデータの上位byteから開始することを示す。たとえば、16進データ0x12_34
はビッグエンディアンでは以下のようになる。
0x12_34 -> 0x12_34
では例として0x50_48_50_21
をビッグエンディアンオーダーの4byteバイナリデータとして生成してみる。
<?php // 4byte(32bit)のビッグエンディアン $binary = pack("N", 0x50485021); // バイナリを表示 // bin2hex()関数を使うことでバイナリを16進数で表現した文字列に変換 echo "HEX: 0x" . bin2hex($binary) . PHP_EOL; // バイナリを文字列として表示 echo "CHR: $binary" . PHP_EOL; ?>
以上のコードの結果は以下となる。
HEX: 0x50485021 CHR: PHP!
リトルエンディアンオーダーのバイナリを作る
リトルエンディアンとは、複数バイトデータの下位byteから開始することを示す。たとえば、16進データ0x12_34
はリトルエンディアンでは以下のようになる。
0x12_34 -> 0x34_12
では例として0x50_48_50_21
をリトルエンディアンオーダーの4byteバイナリデータとして生成してみる。
<?php // 4byte(32bit)のリトルエンディアン $binary = pack("V", 0x50485021); // バイナリを表示 echo "HEX: 0x" . bin2hex($binary) . PHP_EOL; // バイナリを文字列として表示 echo "CHR: $binary" . PHP_EOL; ?>
以上のコードの結果は以下となる。
HEX: 0x21504850 CHR: !PHP
複数のフォーマットを用いる
pack()
関数ではフォーマットは一つだけでなく、複数用いることもできる。
<?php $binary = pack("C" // 1byte . "n" // 2byte(ビッグエンディアン) . "v" // 2byte(リトルエンディアン) . "C3" // 1byte(3個分) . "S" // 2byte(環境依存。リトルにもビッグにもなりうる) . "c*" // 1byte(無限。pack()では無意味だが一応符号ありも扱える) , 0x50, 0x4850, 0x6920, 0x73, 0x20, 0x67, 0x6f6f, 0x64 , 0x2e); // バイナリを表示 echo "HEX: 0x" . bin2hex($binary) . PHP_EOL; // バイナリを文字列として表示 echo "CHR: $binary" . PHP_EOL; ?>
以上のコードの結果は以下となる。
HEX: 0x50485020697320676f6f642e CHR: PHP is good.
ちなみに先ほどのコードはわかりやすさのため文字列結合しているが、もちろん一気に書くこともできる。
<?php // わざわざ文字列結合しなくてもフォーマットは一気に書いて良い $binary = pack("CnvC3Sc*", 0x50, 0x4850, 0x6920, 0x73, 0x20, 0x67 , 0x6f6f, 0x64, 0x2e); // バイナリを表示 echo "HEX: 0x" . bin2hex($binary) . PHP_EOL; // バイナリを文字列として表示 echo "CHR: $binary" . PHP_EOL; ?>
ほかにもどのようなフォーマットがあるのかについては、以下を参照されたし。
www.php.net
バイナリのサイズを取得する
バイナリのサイズを取得するにはstrlen()
関数を使えば良い。非常にややこしいが、PHPのstrlen()
関数は文字数を取得するのではなく、byte数を取得する関数である*2。
<?php // なんか適当なバイナリ(4byte) $binary = "\x12\x34\x56\x78"; // バイナリのサイズを表示する // 4byteのバイナリなので4と出力されるはず echo "Binary size: " . (string)strlen($binary) . PHP_EOL; ?>
Binary size: 4
バイナリから整数を生成する
unpack()
関数を使うことで逆にバイナリから整数を生成することも可能。たとえば整数114514
をバイナリから生成したい場合、以下のようにする。
<?php // 整数114514のもとのバイナリ(ビッグエンディアン) $binaryBE = "\x00\x01\xbf\x52"; // バイナリから整数114514を生成 // 今回は4byteのビッグエンディアンなのでフォーマットは"N" $int = unpack("N", $binaryBE)[1]; // バイナリから生成した整数を表示 // 114514になっているはず echo "バイナリから ${int} を生成しました" . PHP_EOL; ?>
<?php // 整数114514のもとのバイナリ(リトルエンディアン) $binaryLE = "\x52\xbf\x01\x00"; // バイナリから整数114514を生成 // 今回は4byteのリトルエンディアンなのでフォーマットは"V" $int = unpack("V", $binaryLE)[1]; // バイナリから生成した整数を表示 // 114514になっているはず echo "バイナリから ${int} を生成しました" . PHP_EOL; ?>
バイナリから 114514 を生成しました
複数の整数を生成
pack()
関数と同じく、複数のフォーマットを使用することが可能である。そのため、unpack()
関数はint
型の配列を返す*3。
ただしpack()
関数とは違って、複数のフォーマットを使用する場合要素名と区切りを示す"/"が必要*4。たとえば1byteのバイナリ3つ("C3"
)と2byteのバイナリ1つ("n"
)から複数のバイナリを取得する場合、"C3chars/nuint16"
などと書く必要がある。
以下にunpack()
関数で複数の整数を生成する例を示す。
<?php // 先ほど整数から生成した"PHP is good."を元の形に戻す $array_int = unpack("CC/nn/vv/C3C/SS/c*c", "PHP is good."); // 生成した整数をすべて表示 $result = ""; foreach ($array_int as $key => $value) { $result .= "\"${key}\": 0x" . dechex($value) . PHP_EOL; } echo $result; ?>
"C": 0x50 "n": 0x4850 "v": 0x6920 "C1": 0x73 "C2": 0x20 "C3": 0x67 "S": 0x6f6f "c1": 0x64 "c2": 0x2e
符号付き整数を生成したい場合
残念ながら、PHPのunpack()
関数にはバイトオーダーを指定して符号付き整数を生成する方法が存在しない。バイナリから符号付き整数を生成するフォーマットは"c"
(1byte), "s"
(2byte), "i"
(サイズは環境依存), "l"
(4byte), "q"
(8byte)の5種類であるが、いずれも環境依存である*5。
そこで、PHPでは一旦unpack()
関数で符号なし整数を生成し、それを符号付き整数に変換する必要がある。
現時点で自分が思いついている効率的な手法を以下に示す。生成する整数は-114514
とする。
<?php // 整数-114514のもとのバイナリ(ビッグエンディアン) $binary = "\xff\xfe\x40\xae"; // バイナリから整数-114514を生成 // 今回は4byteのビッグエンディアンなのでフォーマットは"N" $int = unpack("N", $binary)[1]; // 符号なしから符号付きに変換 $shift = 8 * strlen($binary); if ($int >= (0x80 << ($shift - 8))) { $int |= ~0 << $shift; } // バイナリから生成した整数を表示 // -114514になっているはず echo "バイナリから ${int} を生成しました" . PHP_EOL; ?>
バイナリから -114514 を生成しました
符号なしから符号付きへの変換をしている処理は以下のとおりである。
<?php // 符号なしから符号付きに変換 $shift = 8 * strlen($binary); if ($int >= (0x80 << ($shift - 8))) { $int |= ~0 << $shift; } ?>
なぜこうなるのかについて詳細に書くと本筋からそれてしまうので省くが、整数が0x80 * (2 ^ (8 * (n - 1)))
以上だった場合に負の値に変換するようにしている*6。