CreateStreamOnHGlobalでメモリストリームを作成する
CreateStreamOnHGlobalは、グローバルメモリとして確保した領域を、COMのIStreamインターフェースから扱えるメモリストリームへ変換するWindows API関数です。ファイルを作成せずに、バイナリデータや一時的な文書データを読み書きできます。
この関数は、クリップボード形式のデータ、画像、シリアル化したオブジェクト、OLE関連データの処理で利用されます。ストリームを受け取るAPIへメモリ上のデータを渡したい場合にも適しています。
HGLOBALという名前から固定アドレスのメモリを想像しやすいものの、通常はGMEM_MOVEABLEを指定して確保します。ストリーム側が必要に応じてメモリを再配置できるようにするためです。
Windows APIの宣言や関連する型を調べるときは、関数単体だけでなく、COMの参照カウント、メモリ所有権、Unicode対応も確認する必要があります。基本的なAPI情報を探す資料として、Windows API資料も参照できます。
関数の役割と引数
宣言は次の形式です。
HRESULT CreateStreamOnHGlobal(
HGLOBAL hGlobal,
BOOL fDeleteOnRelease,
LPSTREAM* ppstm
);
第1引数には、対象となるグローバルメモリのハンドルを指定します。NULLを渡すと、空のメモリストリームを作成するためのメモリをAPI側で確保します。既存のバッファを使う場合は、そのHGLOBALを渡します。
第2引数のfDeleteOnReleaseは、作成されたストリームが解放されるときに、関連するHGLOBALも解放するかどうかを決めます。TRUEならストリームが所有権を持つため、呼び出し側で同じメモリを解放してはいけません。
空のメモリストリームを作成する
新しいストリームを作成するだけなら、次のように書けます。
#include <windows.h>
#include <objidl.h>
IStream* stream = nullptr;
HRESULT hr = CreateStreamOnHGlobal(
nullptr,
TRUE,
&stream
);
if (SUCCEEDED(hr)) {
// streamに対して読み書きを行う
stream->Release();
}
IStreamはCOMインターフェースなので、処理が終わったらReleaseを呼びます。C++であれば、Microsoft::WRL::ComPtr<IStream>や_com_ptr_tなどのスマートポインターを使うと、例外や早期リターンがある場合も解放漏れを減らせます。
この例ではメモリの所有権をストリームへ移しているため、Release後に元のメモリを個別に解放する必要はありません。APIの戻り値は必ず確認し、失敗時に無効なストリームを使わないようにします。
データを書き込み読み戻す
ストリームへの書き込みにはIStream::Writeを使います。現在位置からデータが書き込まれ、書き込み後の位置は末尾へ移動します。
const char data[] = "sample data";
ULONG written = 0;
HRESULT hr = stream->Write(
data,
static_cast<ULONG>(sizeof(data) - 1),
&written
);
先頭から読み直すには、IStream::Seekで位置を変更します。
LARGE_INTEGER origin{};
ULARGE_INTEGER newPosition{};
origin.QuadPart = 0;
hr = stream->Seek(
origin,
STREAM_SEEK_SET,
&newPosition
);
char buffer[32]{};
ULONG read = 0;
if (SUCCEEDED(hr)) {
hr = stream->Read(
buffer,
sizeof(buffer) - 1,
&read
);
}
Readが要求したバイト数より少ない値を返すことはあります。末尾に達した場合はS_FALSEになることもあるため、戻り値だけでなく、実際に読み取ったバイト数も確認します。
HGLOBALの所有権を管理する
既存のメモリをストリームへ渡す場合は、GlobalAllocで確保します。代表的な形は次のとおりです。
HGLOBAL hGlobal = GlobalAlloc(
GMEM_MOVEABLE | GMEM_ZEROINIT,
1024
);
if (hGlobal != nullptr) {
IStream* stream = nullptr;
HRESULT hr = CreateStreamOnHGlobal(
hGlobal,
TRUE,
&stream
);
if (FAILED(hr)) {
GlobalFree(hGlobal);
} else {
// streamを使用する
stream->Release();
}
}
CreateStreamOnHGlobalが成功し、TRUEを指定した場合は、hGlobalの解放責任がストリームへ移ります。失敗した場合は所有権が移っていないため、呼び出し側でGlobalFreeを実行します。
GlobalLockで得たポインターを長期間保持する設計は避けます。メモリが移動する可能性があるため、必要な範囲だけロックし、処理後にGlobalUnlockを呼び出します。ストリームからハンドルを取得する場合はGetHGlobalFromStreamを使えますが、取得したハンドルを勝手に解放してはいけません。
利用場面と関連する処理
メモリストリームは、ディスク上の一時ファイルを作らずにデータを別のAPIへ渡せる点が利点です。画像をメモリから読み込む、XMLや構造化データを一時保存する、OLEデータオブジェクトを構築するといった処理で利用できます。
たとえば、描画用の文字列や表示データを別形式へ変換する処理では、生成したバイト列をストリームへ格納して後段へ渡せます。GDIの文字描画に関するAPIと組み合わせて確認する場合は、文字描画の補足も関連する参考情報になります。
ただし、単純な文字列操作だけならstd::stringやstd::vector<std::byte>のほうが扱いやすい場合があります。IStreamを要求するWindows APIへ接続する必要があるかどうかを基準に選択します。
実装時に確認する事項
メモリストリームを安定して使うため、次の点を実装前に確認します。
CreateStreamOnHGlobalの戻り値をSUCCEEDEDまたはFAILEDで判定するfDeleteOnReleaseの値と、HGLOBALの解放責任を対応させるGlobalAllocでは通常GMEM_MOVEABLEを指定するReadとWriteの実際のバイト数を確認する- 読み取り前に
Seekでストリーム位置を明示する IStreamを使い終えたらReleaseまたはスマートポインターで解放する
バッファの内容を直接扱う必要がある場合は、GetHGlobalFromStreamでハンドルを取得し、GlobalSizeやGlobalLockを使って確認できます。ただし、ストリームの所有権や現在位置とは別の概念なので、取得したポインターを保存し続けないことが重要です。
実装を仕上げる確認手順
まず、空のストリームを作成して短いデータを書き込み、Seekで先頭へ戻して同じ内容を読み取るテストを行います。この段階で、戻り値、読み書きしたサイズ、終端位置をログに出すと、基本的な不具合を発見しやすくなります。
次に、既存のHGLOBALを渡すケースと、作成に失敗したケースを追加します。特にfDeleteOnReleaseをTRUEにした場合、Release後に元のハンドルへアクセスしていないことを確認します。
実装対象のソースにobjidl.hと必要なCOM初期化処理を追加し、空のストリームへの書き込みと読み戻しを行うテストコードを作成します。