hozugawa.net · A neutral informational page

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へ接続する必要があるかどうかを基準に選択します。

実装時に確認する事項

メモリストリームを安定して使うため、次の点を実装前に確認します。

バッファの内容を直接扱う必要がある場合は、GetHGlobalFromStreamでハンドルを取得し、GlobalSizeやGlobalLockを使って確認できます。ただし、ストリームの所有権や現在位置とは別の概念なので、取得したポインターを保存し続けないことが重要です。

実装を仕上げる確認手順

まず、空のストリームを作成して短いデータを書き込み、Seekで先頭へ戻して同じ内容を読み取るテストを行います。この段階で、戻り値、読み書きしたサイズ、終端位置をログに出すと、基本的な不具合を発見しやすくなります。

次に、既存のHGLOBALを渡すケースと、作成に失敗したケースを追加します。特にfDeleteOnReleaseをTRUEにした場合、Release後に元のハンドルへアクセスしていないことを確認します。

実装対象のソースにobjidl.hと必要なCOM初期化処理を追加し、空のストリームへの書き込みと読み戻しを行うテストコードを作成します。