【実務・中級編】VB.NETでのBinaryReaderとBinaryWriterによる独自バイナリファイルフォーマットの読み書き実装 – Visual Basic (VB / VB.NET)解析バイブル

スポンサーリンク

【VB.NET極限最適化】BinaryReader / BinaryWriterで挑む! 高速・セキュアな独自バイナリフォーマット実装術

業務システムを開発していると、CSVやJSONでは「速度が出ない」「データ構造が複雑すぎてフラット化できない」「知的財産の保護(リバースエンジニアリング対策)のために中身を隠蔽したい」という壁にぶ当たる。

そんな時、古くから.NETに備わっている `BinaryReader` と `BinaryWriter` は最強の武器となる。しかし、甘い設計で実装すると「エンディアンの罠」「ストリームのリーク」「バージョンアップ時の構造崩壊」という地獄を見る。

今回は、現場のプロが実践している、バグを一切寄せ付けない堅牢かつ超高速な独自バイナリシリアライザの設計手法をコード付きで伝授する。

なぜCSVやJSONではダメなのか?(バイナリ選択の合理性)

1. パースコストの圧倒的な差

  • テキスト形式は「文字列 ⇄ 数値」の変換(Parse / ToString)に膨大なCPUサイクルを消費する。
  • バイナリ形式は、メモリ上のバイト列をそのままストリームに流し込む(またはその逆)ため、限界まで高速。

2. ストレージ効率とメモリ効率

  • 巨大な数値データをテキストで持つと肥大化するが、Int32なら問答無用で4バイト。

3. カプセル化(難読化)

  • メモ帳で開いて読めるテキストとは異なり、独自の構造を持つバイナリはそれだけで簡易的なプロテクトになる。

堅牢な独自バイナリ設計の3大鉄則

プロダクションコードでバイナリを扱う際、以下の設計思想が欠落しているコードは「技術的負債」でしかない。

1. マジックナンバーとバージョニングの担保
ファイルの先頭数バイトに「我が社のフォーマットである」という識別子(マジックナンバー)と、構造変更に耐えるためのバージョン番号を必ず持たせること。
2. リソースの確実な解放(IDisposableの徹底)
Streamはアンマネージド・リソースに近い挙動をする。`Using` ステートメントを強制し、例外発生時でも確実にロックを解除する。
3. エンディアン(バイトオーダー)の意識
.NETのBinaryReader/Writerは標準でリトルエンディアンで動作する。Windows環境同士なら問題ないが、ネットワークや他システム連携時は常に意識せよ。

実装:プロダクション品質のバイナリI/Oクラス

ここでは、業務データ(ヘッダ情報 + 可変長レコードの本体)を効率よく読み書きするサンプルコードを示す。VB.NETのモダンな言語仕様(Option Strict On、Using文)をフル活用した決定版だ。

Imports System.IO
Imports System.Text

Namespace Enterprise.DataIO

‘ 読み込むデータ構造体(例:センサーログや取引明細)
Public Structure TransactionRecord
Public Id As Integer
Public Timestamp As Long
Public Amount As Decimal
Public Memo As String
End Structure

Public NotInheritable Class BinaryDataHandler

‘ 独自フォーマットの識別用マジックナンバー (“EBDF” = Enterprise Binary Data Format)
Private Shared ReadOnly MagicNumber() As Byte = {CByte(Asc(“E”)), CByte(Asc(“B”)), CByte(Asc(“D”)), CByte(Asc(“F”))}
Private Const CurrentVersion As UShort = 1

”’

”’ データをバイナリファイルとして高速書き出しする
”’

Public Sub WriteBinaryFile(filePath As String, records As IEnumerable(Of TransactionRecord))
‘ FileStreamのバッファサイズを明示的に指定し、I/Oオーバーヘッドを極限まで削る (64KBバッファ)
Using fs As New FileStream(filePath, FileMode.Create, FileAccess.Write, FileShare.None, 65536, FileOptions.SequentialScan),
writer As New BinaryWriter(fs, Encoding.UTF8, leaveOpen:=False)

‘ 1. ヘッダー書き込み (マジックナンバー + バージョン)
writer.Write(MagicNumber)
writer.Write(CurrentVersion)

‘ 2. レコード件数の書き込み
Dim recordList = records.ToList()
writer.Write(recordList.Count)

‘ 3. 本体データのシリアライズ
For Each rec In recordList
writer.Write(rec.Id)
writer.Write(rec.Timestamp)
writer.Write(rec.Amount)

‘ 文字列は長さが可変のため、BinaryWriterの標準機能(プレフィックス付きUTF-8)を利用
writer.Write(If(rec.Memo, String.Empty))
Next

writer.Flush()
End Using
End Sub

”’

”’ バイナリファイルから安全かつ高速にデータを読み込む
”’

Public Function ReadBinaryFile(filePath As String) As List(Of TransactionRecord)
Dim results As New List(Of TransactionRecord)()

If Not File.Exists(filePath) Then
Throw New FileNotFoundException(“指定されたデータファイルが見つかりません。”, filePath)
End Function

Using fs As New FileStream(filePath, FileMode.Open, FileAccess.Read, FileShare.Read, 65536, FileOptions.SequentialScan),
reader As New BinaryReader(fs, Encoding.UTF8, leaveOpen:=False)

‘ 1. マジックナンバーの検証(不正ファイルの即座の弾き落とし)
Dim fileMagic = reader.ReadBytes(4)
If fileMagic.Length <> 4 OrElse Not fileMagic.SequenceEqual(MagicNumber) Then
Throw New InvalidDataException(“ファイルのフォーマットが無効です(マジックナンバー不一致)。”)
End If

‘ 2. バージョン検証
Dim version = reader.ReadUInt16()
If version > CurrentVersion Then
Throw New NotSupportedException($”未対応のファイルバージョンです (Version: {version})。”)
End If

‘ 3. レコード件数の取得
CInt(reader.ReadInt32())
Dim count = reader.ReadInt32()

‘ 4. デシリアライズ処理
results.Capacity = count ‘ 事前にCapacityを確保し、リストの再割り当てコストを排除
For i As Integer = 0 To count – 1
Dim rec As New TransactionRecord With {
.Id = reader.ReadInt32(),
.Timestamp = reader.ReadInt64(),
.Amount = reader.ReadDecimal(),
.Memo = reader.ReadString()
}
results.Add(rec)
Next
End Using

Return results
End Function

End Class

End Namespace

アーキテクトが教える! 現場でハマる「3つの罠」と対策

1. 文字列書き込みの罠 (`BinaryWriter.Write(String)`)

`BinaryWriter` は文字列を書き込む際、文字列のバイト長(プレフィックス)を自動的に先頭に付与して書き込む。
そのため、もし他のシステム(C++やPythonなど)とバイナリを連携する場合、Python側で `struct` モジュール等を使ってパースする際にハマる原因になる。他言語連携時は、固定長バッファにするか、明示的にバイト配列に変換して書き出す設計にすること。

2. ストリームのバッファリングとパフォーマンス

今回のコードでは `FileStream` のコンストラクタで `65536`(64KB)のバッファ`FileOptions.SequentialScan`(シーケンシャルスキャン最適化) を指定している。
デフォルトのままで書き込みを繰り返すと、OSへのシステムコールが頻発し、激遅になる。.NETで高速なI/Oを語るなら、ストリームのコンストラクタチューニングは必須教養である。

3. 将来の仕様変更(マイグレーション)への備え

「カラムを追加したい」となった時、マジックナンバーの次にある `Version` 番号がモノを言う。
Version 1では読んでいなかったフィールドを、`If version >= 2 Then` の条件分岐で安全に読み飛ばす、あるいはデフォルト値を代入する構造にしておくことで、古いファイルが読めなくなるという現場の悲劇を防ぐことができる。

結びにかえて

独自バイナリフォーマットの扱いは、一見するとレガシーで面倒な技術に思えるかもしれない。しかし、「メモリとCPUの限界を引き出し、システムを極限まで高速化する」というエンジニアリングの醍醐味がつまった領域だ。

VB.NETであっても、基盤となる.NETランタイムの仕組みを正しく理解し、適切なストリーム制御を行えば、C#やC++に引けを取らない高パフォーマンスなシステムを構築できる。
ぜひ、あなたの現場の「重い処理・セキュアに扱いたいデータ」の解決策として、この知見を役立ててほしい。

タイトルとURLをコピーしました