エンディアンの入れ替え (uint32)
別のマシンが書き出したファイルを読んだら、数字が桁違いに大きくなっていた。バイトは 1 つも壊れていないのに、値だけが違う。こういうことが起こります。原因は、バイトを並べる向きです。
バイトは同じなのに、値が 2 通りある
4 バイトの整数をメモリやファイルに置くとき、上の桁のバイトから順に置く流儀と、下の桁のバイトから順に置く流儀があります。前者がビッグエンディアン、後者がリトルエンディアンです。
プレーンテキスト
0x12345678 を 4 バイトで置く
ビッグエンディアン 12 34 56 78
リトルエンディアン 78 56 34 12並んでいるバイトを見ても、どちらの向きで書かれたかはどこにも書いてありません。読む側が向きを取り違えると、まったく別の値として読めてしまいます。2 バイトで試すと差がはっきりします。
Python
data = bytes([0x00, 0x50])
print(int.from_bytes(data, "big")) # 80
print(int.from_bytes(data, "little")) # 20480同じ 2 バイトが、向きを変えるだけで 80 にも 20480 にもなります。80 番は HTTP のポートですから、これを取り違えると接続先ごと変わります。しかもどちらの値も、数としては正しく見えます。
向きは決めごとであって、バグではない
インターネットを流れるパケットのヘッダはビッグエンディアンと決まっていて、これをネットワークバイトオーダーと呼びます。一方、いま使っている PC やスマホの CPU はほとんどがリトルエンディアンです。つまり通信のたびに、どこかで並べ替えが起きています。C の htonl や ntohl、Java の ByteBuffer.order は、どれもこの並べ替えのための道具です。
自分でバイナリの形式を決めるときも、まず向きを仕様に書きます。書き手と読み手で約束が食い違うと、値が化けるという形でしか表面化せず、原因にたどり着くのに時間がかかります。
どちらが正しいという話ではありません。CPU の設計上の都合で分かれただけで、両方が現役です。だから橋渡しが要ります。
並べ替えは、1 バイトずつ取り出して置き直す
4 バイトの並びを逆にする手順は 2 段構えです。まず、ほしいバイトを右端まで運んでから、下 8 ビットだけを残して取り出します。次に、それを置きたい位置まで左へ運んで、重ね合わせます。取り出しと置き直しを 4 回分そろえれば、並びは反対になります。
紙に 16 進数で書き出して、どのバイトがどこへ動くかを先に決めておくと迷いません。運ぶ距離を 1 か所でも間違えると、途中のバイトが重なって消えます。取り出すときに残す幅を 8 ビットより狭くしても、同じように上のほうが欠けます。
JavaScript には別の注意があります。ビット演算のあいだ、値は 32 ビットの符号付き整数として扱われるので、最上位のバイトを触ると結果が負の数になります。
JavaScript
console.log(1 << 31); // -2147483648
console.log((1 << 31) >>> 0); // 2147483648>>> 0 を通すと符号なしの値に戻せます。Python の整数には桁の上限が無いので、この心配はいりません。
要件
- endianSwap という名前の関数を実装すること
- シフト演算とマスク 0xFF を使って各バイトを取り出すこと
- 32bit 範囲 (0 〜 0xFFFFFFFF) の入力で正しく動くこと
入出力例
endianSwap(305419896) → 2018915346
endianSwap(1) → 16777216
endianSwap(4278190080) → 255
endianSwap(0) → 0
endianSwap(2864434397) → 3721182122