Swiftでバイナリファイル扱うことはそんなに頻繁にはないと思うのですが、
案件の中でAndroidチームの人が、巨大なJSONファイルをバイナリに圧縮して処理していて、
iOSアプリでも同じようにできるんじゃないかということで、バイナリを扱うことになったので、その知見を残します。
なお僕がやりたかったのは、倍精度浮動小数点数(漢字多すぎですよね)のバイナリファイルを、
Swift上でDouble型の配列に落として使いたい、というのがゴールでした。
※バイナリの絵文字として01が出したかったのですが、🔟しかなかったので、仕方なく🔟を使っています。
x進数のリテラル
バイナリファイルを扱う前に、Swiftのリテラルを確認しましょう。
let decimalInteger = 17
let binaryInteger = 0b10001 // 17 の 2 進数
let octalInteger = 0o21 // 17 の 8 進数
let hexadecimalInteger = 0x11 // 17 の 16 進数
2進数、8進数を直書きすることはあまりないと思います。
0xを先頭につけると16進数扱いになる、が重要です。
特にデバッグで、バイナリファイルじゃなくて、バイナリを扱う処理のロジックを確認したいときに、
サンプルデータとして0x1234みたいにしてよく使いました。
バイナリファイルの扱い
バイナリファイルを扱うのがはじめてだったので、何かお作法があるのかなあと思っていましたが、特別なことは何もなく、
バイナリファイルをXcodeから追加して、そのファイルをData型として読むだけでした。
let filename = "xxx" //<-バイナリファイル🔟の名前を入れてね
guard let path = Bundle.main.path(forResource: filename, ofType: nil) else { return }
guard let data = try? Data(contentsOf: URL(fileURLWithPath: path)) else {
fatalError("load failed.") }
これで読み込みは完了です👍
Data型→String型の変換は意外と簡単
Dataの中身をprintしてみましょう。
print(data)
4016 bytes
____________
| あれっ? >🐧
‾‾‾‾‾‾‾‾‾‾‾‾
Data型はprintしてもByte数が表示されます。
個人的には16進数で吐いてくれると嬉しいのですが、そういう仕様ではありません。
(※NSDataを使うと中身見れます)
単にデバッグなら、Stringに変換するのが楽でした。
こちらを参考にしつつ、
print(data.map { String(format: "%02X", $0)})
としてやると……
["12", "34", "56", "78",……(略)
と出力できました〜🎊
ちなみに。
Stringに変換しない場合、つまり、
print(data.map { $0 })
とするとどうなるのでしょう?
興味本位でやってみました。
[12, 52, 86, 120,……(略)
____________
| ?! >🐧
‾‾‾‾‾‾‾‾‾‾‾‾
なんかよくわからないですが、数字が出力されましたね。
実はData型のmapはデフォルトがUInt8で処理する仕様なので、
1Byte(8bit)の符号なし整数型として処理されました。
つまり
0x12→12
0x34→52
0x56→86
0x78→120
と変換がかかったわけです。
さらにちなみに、String(format: "%02X", $0)のformatの指定子ですが、
Stringの公式ドキュメント見ても指定子の詳細なフォーマットが発見できませんでした。
(もし公式知ってる人いたら教えてください🙏)
C言語のprintf()に指定するフォーマット指定子と一緒らしいので、
Objective-CからSwiftに来ている人には常識なんでしょうか……🐧
(僕はC言語系ちゃんとやったことないので……)
"%02X"で、インプット整数値をアウトプット0詰めアリの2文字の16進数で表示して、という指定になるっぽいです。
Stringも奥が深いですね。
浮動小数点数・バイナリを扱う上でのツール
浮動小数点数内部表現シミュレーター
ところで皆さんは浮動小数点数の扱い得意ですか???
頭の中で普通に変換できますか???
僕は無理です。
でも世の中には便利なものがありました。
これを駆使して、ロジックが正しいか検証しましょう。
バイナリエディタ
Macの標準アプリで、バイナリを表示できるものはありません(たぶん)。
前職ではWindows環境だったので、Windows上でバイナリ見なきゃいけないときはBzというソフトを使っていた気がします。
(基本的にはメインフレーム入って見てましたが。メインフレーム(というかTSO)は逆にバイナリ見やすかったですね)
Mac環境であれば、下記のフリーソフトがいいでしょう。
計算機
Mac標準の電卓アプリ(計算機)を、プログラマモードにすると16進数が扱えるので、活用しましょう💪💪💪
Data型→数値型の変換
String型に比べると、Data型→数値型の変換はちょっとしんどいです。
なんでこんなめんどくさいんだとムカつきながらやっていましたが、
冷静に考えると、Stringってただのバイトストリームで、バイト長の問題さえ認識があえば処理できるのに対して、
数値型はデータ形式がちょっと複雑なのかなと思ったりしました。
まあでもStringも文字コードが絡むと、文字化けの問題がしんどいですね。。。
あくまで数値のバイナリに限定した話として。
Swift5 での Data.withUnsafeBytes
Data型→Double型への変換をゴールとして、話を進めます。
Swift5.0以降で微妙にData型のwithUnsafeBytesの仕様が変わっているので、
テキトーにググってサンプルコード使うと動かないので気をつけてください。
8Byte(64bit)だけ処理するのでいいのであれば、
let value = data.withUnsafeBytes {
$0.load(fromByteOffset: 0, as: Double.self)
}
でできます。
10進数の1.0が、Doubule型の16進数表記だと0x3FF00000_00000000らしいので、
もしdataの中身が0x3FF00000_00000000であればvalueの値は1.0になります!
バイトオーダー
……が、しかし、何度デバッグしても想定していた値とは違う、めちゃくちゃな少数が出てきました。
インプットには正の値しかないはずなのに、なんならマイナスの値が出てきます。
iOSはバイトオーダーがリトルエンディアンです。
大事なことなのでもう1度いいますが、iOSはバイトオーダーがリトルエンディアンです。
var hoge = UInt32(0x1234ABCD)
print(NSData(bytes: &hoge, length: 4))
<cdab3412>
____________
| えー?! >🐧
‾‾‾‾‾‾‾‾‾‾‾‾
つまりどういうことだってばよ
つまりこういうことです。
ビッグエンディアンであれば、
0x3FF00000_00000000
↓
3F→F0→00→00→00→00→00→00
という順で処理をします。
これは直感的ですね。
IBMのメインフレーム(及び互換機)、モトローラのMC68000(及び後継)、サン・マイクロシステムズのSPARC等はビッグエンディアンを採用し、DECのVAX、インテルのx86等はリトルエンディアンを採用している。ARMアーキテクチャ、PowerPCなど、エンディアンを切り替えられるバイエンディアン (bi-endian) のプロセッサも存在する。
言語処理系などの仮想マシンの類では、プラットフォームに応じ使い分ける設計のものもあれば、片方に寄せる設計のものもある。例えば、Java仮想マシンはプラットフォームを問わずビッグエンディアンである。
iPhoneはARMアーキテクチャのCPUなので、バイエンディアンってやつだと思うのですが、デフォルトはリトルエンディアンになっています。
Int型であれば、直前にエンディアン変えられるオプションもあるんですが、Float/Doubleだとちょっと厳しそうですし、
やったとして可読性がはちゃめちゃに悪くなるので、元データの生成をリトルエンディアンでやりなおしました。
Data型→[Double]型に
長い道のりでしたが、これで完成です!
let value = data.withUnsafeBytes {
Array(
UnsafeBufferPointer(
start : $0.baseAddress!.assumingMemoryBound( to: Double.self ),
count : $0.count / MemoryLayout<Double>.size
)
)
}
$0にUnsafeRawBufferPointer型のポインタが入っているわけですが、
Arrayに食わせるためにRawじゃないUnsafeBufferPointerにDouble型のアライメントを指定してポインタを再作成しているのが、
ちょっと冗長に感じるので、何かUnsafeRawBufferPointerをちょっと変えてArrayにできないか試行錯誤してみたんですが、結局できませんでした。
Swiftのポインタは雰囲気ではわかるんですが、種類が多くて、イマイチ全容をつかめていない感じがあります。
配列に落としたあとは、煮るなり焼くなり。
まとめ
無事Swiftでバイナリファイルを扱うことができました。
Swiftでマジメに低レイヤーの処理するのははじめてで、
「そもそもできるのか?」「結局Objective-Cの方がやりやすかったりするんじゃないの?」と思いながらスタートしましたが、一通りのことはできるみたいです。
Data型でも配列や辞書型みたいに高階関数が使えたのはちょっと感動しましたが、それでもバイナリが出てくると途端に泥臭くなりますね。。。😥
何かのご参考になれば幸いです。
追記
Memory Management Programming Guide for Core Foundation
むかーしのAppleのドキュメントにいいものを発見しました。