なんか考えてることとか

変な人が主にプログラミング関連で考えていることをまとめる。

PHPでバイナリを扱う方法

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()関数を使えば良い。非常にややこしいが、PHPstrlen()関数は文字数を取得するのではなく、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

符号付き整数を生成したい場合

残念ながら、PHPunpack()関数にはバイトオーダーを指定して符号付き整数を生成する方法が存在しない。バイナリから符号付き整数を生成するフォーマットは"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

*1:ただしPHPの場合はダブルクォーテーション("")で囲む必要がある

*2:ちなみにちゃんとした文字数を得るためにはmb_strlen()関数を使うと良い

*3:先ほどさらっとコードで書いたがフォーマットを1つだけ指定する場合においても配列が返されるので配列から要素を取り出す必要がある

*4:必ずと言うわけではないが、必須と言っても過言ではない

*5:もっとも、"c"は1byteなので関係ないのだが・・・

*6:効率化のためにシフト演算を使ったりしているが