IStreamのReadとWriteでバイナリデータを自在に読み書きする
IStreamは、COM (Component Object Model) で定義されているインターフェースの一つで、バイト単位のデータを抽象的に扱うための仕組みを提供します。ファイルの読み書き、メモリ上のデータ操作、ネットワーク越しのデータ送受信など、さまざまな場面でIStreamを共通基盤として利用することで、コードの再利用性と保守性が大きく向上します。
特にReadとWriteは、ストリームの中核を担う二つのメソッドです。両者の戻り値やバッファ管理の仕組みを理解し、適切にエラーチェックを行えば、大容量のバイナリデータも安定した操作が可能になります。以降では、それぞれのメソッドの内部仕様から実装上の注意点までを順に整理していきます。
IStreamインターフェースの基本構造
IStreamはISequentialStreamを継承し、Seek、CopyTo、Commit、Revert、LockRegion、UnlockRegion、Stat、Cloneといったメソッドが追加された複合インターフェースです。読み書きに必要な機能はISequentialStreamで定義されており、IStream全体としてはストリームの位置管理や複製といった高レベルな操作まで網羅しています。
メソッド呼び出しはすべてHRESULTを返し、成功時はS_OK、ファイルの終端に達した場合はS_FALSE、読み込み要求がストリーム末尾を超えた分は部分的に成功してSTG_S_REWINDのような特殊な値が返ることもあります。戻り値を厳密にチェックする習慣が、堅牢なデータ処理への第一歩です。
ストリームオブジェクトはCoCreateInstanceで作成する方法、SHCreateMemStreamやCreateStreamOnHGlobalのようなヘルパー関数で取得する方法があります。用途に合わせて選択することで、開発効率が大きく変わります。
Readメソッドの詳細な動作仕様
Readメソッドは、ストリームから指定したバイト数を読み出し、呼び出し側が用意したバッファに格納します。プロトタイプは HRESULT Read(void *pv, ULONG cb, ULONG *pcbRead) で、第一引数がデータを格納するバッファ、第二引数が読み取りたいバイト数、第三引数が実際に読み取られたバイト数を受け取るポインタです。
cbよりも実際のデータが少ない場合、pcbReadには読み取られた実バイト数が格納され、戻り値はS_FALSEまたはS_OKになります。一方、エラーが発生したときはSTG_E_INVALIDPOINTERやSTG_E_READFAULTなど、適切なエラーコードが返ります。バッファのサイズをcbと同じにしておかないとバッファオーバーランが発生するため、必ず一致させる必要があります。
メモリ読み出しでは高速に動作しますが、ディスクやネットワーク越しのストリームでは内部バッファリングの都合でpcbReadがcbより小さくなるケースがあります。ループでReadを呼び出し、累計バイト数が要求サイズに達するまで処理を継続するのが一般的な実装パターンです。
Writeメソッドの詳細な動作仕様
Writeメソッドは、バッファからストリームへデータを書き出すためのもので、プロトタイプは HRESULT Write(const void *pv, ULONG cb, ULONG *pcbWritten) です。読み取りと対称的なシグネチャになっており、戻り値のHRESULTも同様にS_OKやエラーコードを使い分けます。
書き込みに成功したバイト数はpcbWrittenに格納されます。ディスクベースのストリームでは空き容量不足などで部分書き込みになることもあるため、戻り値だけでなくpcbWrittenも必ず確認する必要があります。cbとpcbWrittenが一致しない場合は再書き込みや例外処理を検討します。
ストリームによっては、書き込み時に内部バッファがフラッシュされるまで、データが実際に物理的に反映されないことがあります。ファイルを閉じた後に再度開いて内容を確認するなど、想定通りの書き込みができたかをテストすると安心です。バッファ確保の方法によってメモリ効率が変わる点も意識しておくと、設計段階で適切な選択肢を選べます。
ストリーム位置の管理とSeekメソッド
ReadやWriteは内部にカレントポジションを持っており、読み書きが進むと自動的に位置が移動します。任意の位置に移動したい場合はSeekメソッドを利用します。プロトタイプは HRESULT Seek(LARGE_INTEGER dlibMove, DWORD dwOrigin, ULARGE_INTEGER *plibNewPosition) で、dwOriginにSTREAM_SEEK_SET、STREAM_SEEK_CUR、STREAM_SEEK_ENDのいずれかを指定して基準点を切り替えます。
ファイル形式によってはデータの途中から走らせたほうが効率的です。例えば、ヘッダ部を読み飛ばしてから本体データを処理する場合などにSeekを活用します。逆に、シーケンシャルにしかアクセスしない場合は、毎回先頭から読むよりも位置をそのまま流用するほうが無駄がありません。
Seekは大きなストリームを扱う際にコストがかかることもあります。頻繁に位置を移動する設計では、ランダムアクセス専用のAPIや別インターフェースの利用を比較検討するとよいでしょう。
メモリストリームとファイルストリームの使い分け
メモリ上のストリームとしてはCreateStreamOnHGlobalが代表的で、HGLOBALハンドルをベースに動作します。ファイルベースのストリームとしては、IFileStreamを実装するSHCreateStreamOnFileExや、ファイルパスから直接ストリームを生成する関数群が用意されています。
メモリストリームはランダムアクセスが容易で、サイズが小さいデータを一時的に保持するのに向いています。一方、大容量データや永続化が必要なデータはファイルストリームのほうが適しています。用途によって適切な選択をすることで、パフォーマンスとリソース消費の両面でバランスを取れます。
プロセス間通信でデータを受け渡す場合、メモリストリームは相性が良いですが、別の通信手段を使ったほうが効率的なケースもあります。例えば、軽量なメッセージ送受信には別の仕組みを採用することも視野に入れると、アプリケーション全体の構成がすっきりします。プロセス間でデータをやりとりする場合の選択肢として、名前付きパイプでの簡易通信も検討に値します。ストリーム形式の選択は単なる性能比較だけでなく、運用シナリオ全体で判断すべきです。
ReadとWriteの戻り値とメモリ管理の比較
実装時に混乱しやすいReadとWriteの違いを、表形式で整理しておきます。メモリ確保の選択がパフォーマンスや安全性に直結するため、ここではHeapAllocとLocalAllocの使い分けも併せて押さえておくと、設計が一貫します。
| 項目 | Read | Write |
|---|---|---|
| バッファの所有権 | 呼び出し側が確保し、メソッドは書き込む | 呼び出し側がデータを保持し、メソッドが読み取る |
| バッファサイズの最低条件 | cb以上のサイズが必須 | cb以上のサイズが必要 |
| 戻り値の代表値 | S_OK / S_FALSE / エラーコード | S_OK / エラーコード |
| 実転送バイト数 | pcbReadに出力 | pcbWrittenに出力 |
| 部分転送の挙動 | 末尾到達で停止 | 容量不足などで停止する場合あり |
| 推奨メモリ確保 | HeapAllocで十分 | HeapAllocまたは専用バッファ |
両メソッドとも、引数cbと実際に転送されたバイト数を比較するループを使うことで、信頼性の高い読み書きが実現できます。バッファオーバーランとリソースリークを防ぐには、使用後にReleaseパターンで自身の確保したメモリを解放する処理を必ず入れることが肝心です。
ここまでのポイントを押さえておけば、IStreamを使ったデータ読み書きで迷うことは大幅に減ります。エラーチェックを怠らず、ストリームの種類とメモリ管理を意識した実装を心がけることが、現場で長く使えるコードへの近道です。