syghの新フラグメント置き場

プログラミングTipsやコード断片の保管場所です。お絵描きもときどき載せます。

BSTRはwchar_tへのポインタではありません

COMの文字列にはBSTRというデータ型が使われています。
BSTRが定義されているWindows SDKのヘッダーを見てみましょう。

// <wtypes.h> in Windows SDK 8.1

#ifndef _PREFAST_
typedef /* [wire_marshal] */ OLECHAR *BSTR;
#else // _PREFAST_
typedef _Null_terminated_ OLECHAR *BSTR;
#endif

OLECHARはもともとCOMの前身であるOLE (Object Linking and Embedding) で使われていた文字型の名残で、当時はANSI MBCSのcharでしたが、現在はWCHARすなわちwchar_tと等価です。Windows上ではsizeof(wchar_t) == 2であり、UTF-16エンコーディングUnicode文字を表現するのに使われます。

// <WTypesbase.h> in Windows SDK 8.1

#if defined(_WIN32) && !defined(OLE2ANSI)
typedef WCHAR OLECHAR;
typedef /* [string] */  __RPC_string OLECHAR *LPOLESTR;
typedef /* [string] */  __RPC_string const OLECHAR *LPCOLESTR;
#define OLESTR(str) L##str
#else
typedef char      OLECHAR;
typedef LPSTR     LPOLESTR;
typedef LPCSTR    LPCOLESTR;
#define OLESTR(str) str
#endif

「ふーん、じゃあBSTRwchar_tへのポインタなんだね。てことは、BSTRを要求するAPIにはwchar_tへのポインタを渡してもいいんだ!」

ダメです。絶対にやめてください。

そもそもBSTRとは?

BSTRはCOMだけでなく旧VB/VBAでも使われている、長さ情報を持った自己記述的文字列型の一種です。Windows SDKの <oaidl.h> で定義されているVARIANT型とも深い関わりがあります。
BSTRのデータ構造に関しては、以下の資料をよく読めばイメージできるはずです。

実際にコードで試してみます。

#define NOMINMAX
#include <Windows.h>
#include <tchar.h>
#include <cstdio>
#include <clocale>
#include <initializer_list>

int main() {
    _tsetlocale(LC_ALL, _T(""));

    for (auto str : { L"abc", L"hoge", L"Hello" }) {
        // BSTR の型自体は OLECHAR* すなわち wchar_t* の typedef エイリアスだが、
        // 実際の BSTR データ構造は先頭に4バイトの長さ情報(符号無し整数)を持っている。
        // Pascal の自己記述型文字列と、C の NUL 終端文字列の両方をミックスしたような構造。

        // SysAllocString() は、終端 NUL 文字の分を加味した (4 + UTF-16 文字列バイト長 + 2) の長さを持つバイト配列を確保し、
        // 5バイト目以降へのポインタを BSTR 型として返却しているにすぎない。
        // SysFreeString() は、渡されたポインタからマイナス方向に4バイトオフセットした位置を先頭とするバイト配列を解放する。
        // SysStringByteLen() や SysStringLen() は、渡されたポインタからマイナス方向に4バイトオフセットした位置に長さ情報が格納されていると仮定して値を読み取っている。
        // これにより、SysStringByteLen() と SysStringLen() の計算量オーダーは O(1) となる。
        // したがって、BSTR を要求する API に wchar_t* を渡してはいけない。
        // const wchar_t* や const wchar_t[] を BSTR にキャストするのもダメ。
        BSTR bstr = ::SysAllocString(str);
        const UINT sizeInBytes = *(reinterpret_cast<UINT32*>(bstr) - 1);
        const UINT sysByteLen = ::SysStringByteLen(bstr);
        const UINT sysStrLen = ::SysStringLen(bstr);
        wprintf(L"BSTR: wsz = \"%ls\", size = %u[bytes], byte-len = %u, len = %u\n", bstr, sizeInBytes, sysByteLen, sysStrLen);
        ::SysFreeString(bstr);
        bstr = nullptr;
    }
    // BSTR を偽装してみる。通常はマネしないように。
    // 本来の BSTR データ構造であれば、リトルエンディアンのバイト長として (2 * 3) == 6 が格納されていなければならない。
    // また、バイト長は2の倍数すなわち偶数でなければならない。
    struct { INT32 len; wchar_t str[128]; } dummyData = { 65535, L"abc" };
    BSTR bstrIllegal = dummyData.str;
    const UINT sysByteLen = ::SysStringByteLen(bstrIllegal);
    const UINT sysStrLen = ::SysStringLen(bstrIllegal);
    wprintf(L"Illegal BSTR: byte-len = %u, len = %u\n", sysByteLen, sysStrLen);
}

以上のように、BSTRはメモリの確保/解放や長さ取得に専用のAPIを利用します。
BSTRをC/C++で読み取る際はwchar_tのゼロ終端文字列として扱うこともできますが、その逆は不可です。決してwchar_tのゼロ終端文字列をBSTRとして扱ってはいけません。未定義の異常動作を引き起こす原因になります。

BSTRのラッパー

Visual C++ではATL::CComBSTR_bstr_tといったラッパークラス型(スマートポインタ)が用意されており、ライフサイクル管理の煩雑なBSTRを比較的簡単に扱うことができます。
WIL (Windows Implementation Library) にもwil::unique_bstrが用意されています。

ただし、BSTRラッパーの内部では結局システムAPIをコールするので、ヒープ管理にCRT (C Runtime) を利用するATL::CStringWや標準C++std::wstringと違い、速度やメモリ効率などのパフォーマンス面で不利です。
BSTRラッパーはBSTRを要求するCOMコンポーネントとの境界面 (API) にのみ使用し、普段は文字列の管理にATL::CStringWstd::wstringを使うべきです。