2014年11月4日火曜日

PHPと16進数にまつわる思いがけない挙動

0x80000000 (32bit) はintでしょうか?いいえ、floatです。

それで比較していてはまったという思い出話。

PHPでバイナリを処理しようという、酔狂(?)な方は要注意ですぞ

なんでそんなことを使用かという話は話せないし面倒なのでほっといて

次のようなコードを書いていて問題は発生した。

$binaryString = "\x7F\xFF\xFF\xFF\x80\x00\x00\x00";
// 実際は別途取ってきたバイナリデータがあるとおもいねぇ!
$unpacked = unpack("NN", $binaryString);
assert($unpacked[1] === 0x7FFFFFFF);    // <- pass 
assert($unpacked[2] === 0x80000000);    // <- assertion error !!!!

原因は:定数と変数と”暗黙の変換”の問題。

あれー!同じだろこれー!と思って散々原因を探してわかったのは

  • そもそもPHPの”すべての”整数(integer)は32ビット「符号付」しかない
    • 符号無整数は存在しない
  • unpack関数では、符号無整数を「指定は出来るが」「実際変換後は符号付整数」になる
    • 上記 $unpacked[2] は、内部的には -2,147,483,648 となっている
  • 0x80000000 は「整数の範囲を超えるので」「小数に自動変換される」
    • var_dump で見てみると、(float) 2,147,483,648 になっている
  • なので、厳密な一致テストを行うと、まあ、あいまいな一致テストでも、FALSE になる

他の方もはまっているようなそうでもないような微妙な話題である。調べようにも、わかってみればああ、そうか、だが気づくまではどはまりするという話題でもある。

ちなみに回避法は:強制的なキャスト

上記例の変数をそのまま使うと
assert($unpacked[1] === (int)0x7FFFFFFF);  // <- pass
assert($unpacked[2] === (int)0x80000000);   // <- pass, OK!
とする。符号無なので,内部的には右辺 0x80000000 は負の値になっているが、比較さえしなければ特に問題はない。少なくとも私はしない。はず。

0 件のコメント:

コメントを投稿