RegQueryValueExで値を取得する際のバッファサイズ問題
Windows レジストリから文字列やバイナリを読み出すとき、RegQueryValueEx の使い方で特に混乱しやすいのがバッファサイズです。lpcbData が示す値は、単純な文字数ではなく、データの種類に応じたバイト数として扱われます。
サイズを小さく指定したまま取得を実行すると、値が途中で切れたり、ERROR_MORE_DATA が返ったりします。反対に、文字列の終端を考慮せずにメモリを確保すると、読み出した後の処理で範囲外アクセスや不正な文字列参照につながります。
この問題は、ANSI版とUnicode版の違い、REG_SZ や REG_MULTI_SZ などのレジストリ型、32ビットと64ビットのデータ幅を意識していない場合に起きやすくなります。APIが返すサイズの意味を正確に理解することが、安全な実装の出発点です。
特に、最初に必要サイズを問い合わせてからバッファを確保する二段階方式が基本になります。ただし、問い合わせと取得の間にレジストリ値が変更される可能性もあるため、実際の取得時には返り値を再確認しなければなりません。
lpcbDataが表すサイズの意味
RegQueryValueEx の第5引数 lpcbData は、入力時にはバッファの容量をバイト単位で渡し、出力時には実際に格納されたデータのサイズを受け取る変数です。REG_SZ の文字列であっても、文字数ではなくバイト数で指定する点が重要です。
たとえばUnicode版の RegQueryValueExW で20文字分を格納するなら、基本的には20ではなく、20にwchar_tのサイズを掛けた容量を考えます。sizeof(wchar_t)はWindowsでは通常2ですが、コード上では型のサイズを直接利用すると、文字コードの違いによる誤りを避けられます。
lpDataにNULLを渡し、lpcbDataだけを有効にして呼び出すと、必要なデータサイズを調べられます。このとき値の種類は第4引数のlpTypeに返されるため、サイズと型を取得してから適切なバッファを準備できます。
二段階取得でバッファを確保する
C++では、最初の問い合わせで得たサイズを使ってstd::vector<BYTE>などを確保する方法が扱いやすい構成です。サンプルは次のようになります。
DWORD type = 0;
DWORD size = 0;
LONG result = RegQueryValueExW(
hKey, L"SampleValue", nullptr, &type, nullptr, &size);
if (result == ERROR_SUCCESS) {
std::vector<BYTE> buffer(size);
DWORD actualSize = size;
result = RegQueryValueExW(
hKey, L"SampleValue", nullptr, &type,
buffer.data(), &actualSize);
}
ただし、最初の呼び出しが成功したからといって、次の呼び出しも必ず成功するとは限りません。別のプロセスが値を書き換えれば、取得時に必要な容量が増えている可能性があります。その場合はERROR_MORE_DATAを処理し、返されたサイズを使って再確保する設計が必要です。
確保と再試行を一定回数行うループにすると、実行中の変更にも対応できます。無制限に繰り返すのではなく、数回で打ち切ってエラーとして扱うと、他プロセスによる頻繁な書き換えで処理が停滞するのを防げます。
文字列の終端文字に注意する
REG_SZ、REG_EXPAND_SZ、REG_MULTI_SZの値では、保存されたデータに終端のゼロ文字が含まれるとは限りません。Windowsのレジストリに通常の形式で格納された値でも、RegQueryValueExは取得した文字列への終端追加を保証しないため、サイズ分だけ確保した領域をそのまま文字列として扱うのは危険です。
取得後に文字列として利用するなら、要素数に換算して終端用の領域を追加する方法があります。Unicodeの場合はsize / sizeof(wchar_t)で要素数を求め、追加領域を確保してから終端を書き込みます。ただし、元のデータが正しい文字列形式であるかを確認し、バイナリ値を誤って文字列として扱わないことも必要です。
REG_MULTI_SZは複数の文字列を終端ゼロで区切り、最後にさらにゼロを置く形式です。単一のREG_SZと同じ感覚で1個の終端だけを付けると、列挙処理で不正な読み取りが起こることがあります。型を判定して、必要な終端数を意識して処理します。
レジストリ型とデータ解釈を分ける
REG_DWORDのサイズは通常4バイト、REG_QWORDは8バイトですが、ホスト環境のポインターサイズとは別の仕様です。64ビット環境だからREG_DWORDが8バイトになるわけではありません。型に応じて固定サイズを確認し、取得したバイト列を適切な整数型へ変換します。
REG_BINARYは内容の意味をAPIが解釈しないため、サイズをそのままバイト列として扱います。構造体を保存した独自データの場合は、構造体のパッキング、バージョン、エンディアン、将来的なサイズ変更まで考慮しなければなりません。
REG_EXPAND_SZに環境変数が含まれていても、RegQueryValueExが自動展開するわけではありません。取得した文字列に対してExpandEnvironmentStringsを別途使用します。レジストリ値の取得と、取得後の意味付けや展開を分離すると、バッファサイズの計算も整理しやすくなります。
変更競合とエラー処理
サイズ問い合わせとデータ取得の間には、必ず時間差があります。その間に値が短くなれば余った容量が発生し、長くなればERROR_MORE_DATAになる可能性があります。取得後のactualSizeを確認し、返された実サイズだけを有効データとして扱うことが大切です。
APIの戻り値はWin32エラーコードであり、GetLastErrorを呼んで判定するものではありません。ERROR_SUCCESS、ERROR_FILE_NOT_FOUND、ERROR_MORE_DATAなどを戻り値から直接確認します。キーや値が存在しない場合と、容量不足の場合を同じ失敗として処理しないことが重要です。
レジストリを使って外部プログラムの設定や実行情報を管理する場合は、値の読み取りだけでなく、プロセス起動側の仕様も確認します。たとえばCreateProcess解説のようなWindows APIの説明と合わせると、取得したパスや引数を後続処理へ渡す際の注意点も整理できます。
安全な実装へつなげる確認事項
実装時には、レジストリ値の型、必要サイズの単位、終端文字の有無、取得後の変換処理を個別に確認します。特にsizeofを使わず固定値を記述すると、ANSI版とUnicode版を切り替えたときに容量計算が壊れやすくなります。
文字列を格納する場合は、バイトサイズから文字数へ変換した後に終端領域を確保し、コピー後の終端を明示します。バイナリの場合は文字列関数を使わず、取得されたバイト数を境界として処理します。入力値を信頼しすぎず、異常に大きなサイズを拒否する上限も有効です。
APIの引数や戻り値を確認するときは、Windows APIの関数仕様をまとめたWindows API資料も参照先になります。古いコードではDWORD、BYTE*、TCHARなどが混在しやすいため、使用する文字セットと型の対応を一つずつ確認すると、移植時の不具合を見つけやすくなります。
RegQueryValueExで値を取得する際のバッファサイズ問題は、単に大きな配列を用意すれば解決するものではありません。サイズはバイト単位で扱い、型を判定し、終端を自分で保証し、ERROR_MORE_DATAと値の変更競合に備える必要があります。
覚えておくべき要点は、必要サイズの問い合わせ、適切な容量の確保、実取得後のサイズ確認、そしてデータ型に応じた安全な解釈を一体として実装することです。