March 2017

Volume 32 Number 3

C++ - CComSafeArray による C++ でのセーフ配列プログラミングの簡素化

Giovanni Dicanio | March 2017

さまざまな言語で作成されたコンポーネントで構築される複雑なソフトウェア システムを開発するのは珍しいことではありません。たとえば、C インターフェイス、ダイナミックリンク ライブラリ (DLL)、または COM コンポーネントに埋め込まれる、ある程度パフォーマンスの高い C++ コードが考えられます。あるいは、COM インターフェイスをいくつか公開する Windows サービスを C++ で記述することもあります。C# で記述されたクライアントから、COM 相互運用機能経由でそのサービスと対話します。

ほとんど場合、このようなコンポーネント間では、なんらかのデータを配列の形式で交換することを考えます。たとえば、ハードウェアと対話して、入力デバイスから読み取った画像のピクセルを表すバイト値の配列、センサーから読み取った測定結果を表す浮動小数点数の配列など、データの配列を生成する C++ コンポーネントが考えられます。他にも、低レベルのモジュールと対話し、C# やスクリプト言語で記述された GUI クライアントで利用するために、文字列配列を返す Windows サービスを C++ で記述することもあるでしょう。モジュールの境界を越えてデータを渡すのは簡単なことではなく、適切に設計および作成されたデータ構造を使用する必要があります。

Windows プログラミング プラットフォームには、この目的に適した SAFEARRAY というすぐに使える便利なデータ構造体があります。この構造体の定義については、Windows デベロッパー センター (bit.ly/2fLXY6K、英語) で確認できます。基本的に、SAFEARRAY データ構造体はセーフ配列の特別なインスタンスを表し、次元数や、セーフ配列の実際のデータへのポインターなどの属性を指定します。セーフ配列は、通常、コード内で SAFEARRAY 記述子へのポインター (SAFEARRAY*) を使って処理されます。また、セーフ配列を操作するための C インターフェイス Windows API もあります。この API には、作成と削除用の SafeArrayCreate や SafeArrayDestroy、セーフ配列インスタンスをロックしてそのデータを安全に利用するための関数などが含まれています。SAFEARRAY C データ構造体と、いくつかのネイティブ C インターフェイス API の詳細については、本稿の姉妹編「SAFEARRAY データ構造体について」(msdn.com/magazine/mt778923、英語) を参照してください。

ただし、C++ プログラマの場合は、C インターフェイス レベルで作業するのではなく、Active Template Library (ATL) の CComSafeArray など、高いレベルの C++ クラスを使用する方が便利です。

今回は、やや複雑な C++ コード サンプルを紹介します。このサンプルでは、CComSafeArray などの ATL ヘルパー クラスを使用して、さまざまな種類のデータを格納するセーフ配列を作成します。

セーフ配列と STL ベクターの比較

std::vector などの Standard Template Library (STL) クラス テンプレートは、モジュールの境界内にある C++ コードのコンテナーに最適で、その状況での使用が推奨されます。たとえば、std::vector はセーフ配列よりも効率よくコンテンツを動的に追加して拡張でき、STL や他のサードパーティ製のクロスプラットフォーム C++ ライブラリ (Boost など) のアルゴリズムとの統合が容易です。そのうえ、セーフ配列が Windows プラットフォーム専用なのに対し、std::vector ではクロスプラットフォーム標準の C++ コードを開発できるようになります。

一方、セーフ配列が勝っているのは、モジュール境界での機能です。実際に、std::vector はモジュールの境界を安全に越えることができず、C++ とは異なる言語で記述されたクライアントからは利用できません。セーフ配列は、そのような状況での使用こそ適しています。優れたコーディング パターンとしては、まず、標準の C++ と、std::vector などの STL コンテナーを使用して主な処理を行います。その後、モジュールの境界を越えて配列データを転送する必要がある場合、std::vector のコンテンツを、C++ 以外の言語で記述された境界外部のクライアントから利用するのに適したセーフ配列にすることを計画します。

ATL CComSafeArray ラッパー

本稿の姉妹版で説明されているように、セーフ配列の「ネイティブ」プログラミング インターフェイスは Win32 C インターフェイス API を使用します。C++ コードでこのような C 関数を使用することは可能ですが、扱いにくく、バグが起きやすいコードになりがちです。たとえば、ロックとロックの解除を正しく対応させるため、セーフ配列の割り当て解除とセーフ配列の割り当てとをすべて注意深く対応付ける必要があります。

さいわい、C++ では、RAII のような便利なコーディング パターンとデストラクターを使用して、C スタイルのコードを大幅に簡素化することができます。たとえば、SAFEARRAY 記述子をそのままラップするクラスを作成して、コンストラクターから SafeArrayLock を呼び出し、デストラクターから対応する SafeArrayUnlock 関数を呼び出すことができます。この方法では、ラッパー オブジェクトがいったんスコープ外に出ると、セーフ配列のロックが自動的に解除されます。クラスをテンプレートとして形成し、セーフ配列をタイプ セーフにすることには意味があります。実際、C におけるセーフ配列の汎用性は、SAFEARRAY::pvData フィールドに void ポインターを使用していることに表れています。一方、C++ ではテンプレートを使用することで、コンパイル時に適切な型チェックを実行できます。

さいわい、このようなクラス テンプレートをゼロから作成する必要はありません。実際には、ATL は便利な C++ クラス テンプレートを既に用意しており、セーフ配列のプログラミングが簡素化されています。 このクラス テンプレートが CComSafeArray で、<atlsafe.h> ヘッダーで宣言されています。ATL CComSafeArray<T> のテンプレート パラメーター T は、セーフ配列に格納されるデータの型を表します。たとえば、バイトのセーフ配列には CComSafeArray<BYTE> を使用し、浮動小数点のセーフ配列には CComSafeArray<float> を使用してラップします。内部にラップされるセーフ配列は、その時点でも、ポリモーフィックな void ポインターベースの C スタイルの配列のままです。ただし、CComSafeArray で構築される C++ のラッピング層がもたらす抽象化のレベルは C の void* よりも高く、安全で、セーフ配列の有効期間の自動管理と、優れたタイプ セーフ性の両方を含みます。

CComSafeArray<T> のデータ メンバー (m_psa) は 1 つだけで、CComSafeArray オブジェクトによりラップされたセーフ配列記述子へのポインター (SAFEARRAY*) です。

CComSafeArray インスタンスの構築

既定のコンストラクター CComSafeArray は、セーフ配列記述子へのポインターを Null ポインターとして含むラッパー オブジェクトを作成します。基本的には、このオブジェクトは何もラップしません。

さらに、コンストラクターの便利なオーバーロードもいくつかあります。たとえば、要素数を渡すオーバーロードでは、指定した要素数を持つセーフ配列を作成できます。

// Create a SAFEARRAY containing 1KB of data
CComSafeArray<BYTE> data(1024);

配列インデックスの下限を既定のゼロ以外にすることもできまます。

// Create a SAFEARRAY containing 1KB of data
// with index starting from 1 instead of 0
CComSafeArray<BYTE> data(1024, 1);

ただし、C++ や C#/.NET クライアントが処理するセーフ配列を操作するコードを作成する場合は、通常の表記法を変えずに、インデックスの下限値にゼロ (1 ではなく) を使用します。

CComSafeArray コンストラクターは SafeArrayLock を自動的に呼び出します。そのため、ラップされたセーフ配列は既にロックされていて、ユーザー コード内で読み取り操作や書き込み操作を行う準備が整っています。

既存のセーフ配列記述子へのポインターがある場合は、CComSafeArray コンストラクターの別のオーバーロードに渡すことができます。

// psa is a pointer to an existing safe array descriptor
// (SAFEARRAY* psa)
CComSafeArray<BYTE> sa(psa);

この場合、CComSafeArray は、“psa” によって指される元のバイトのセーフ配列のディープ コピーの作成を試みます。

CComSafeArray によるリソースの自動クリーンアップ

CComSafeArray オブジェクトがスコープ外になると、デストラクターはラップされたセーフ配列が割り当てたメモリとリソースを自動的にクリーンアップします。これは非常に便利で、C++ コードを大幅に簡素化します。この方法では、C++ プログラマはセーフ配列のメモリのクリーンアップの詳細ではなく、コードの重要な部分に集中でき、やっかいなメモリ リークのバグも回避できます。

ATL CComSafeArray のコードの内部を詳しく調べると、CComSafeArray デストラクターが SafeArrayUnlock を最初に呼び出した後に SafeArrayDestroy を呼び出しているため、クライアント コードではセーフ配列のロックを明示的に解除して破棄する必要がないことがわかります。実際には、これを行うと、2 重に破棄するバグになります。

SafeArray の一般的な操作に役立つメソッド

CComSafeArray クラス テンプレートは、セーフ配列の有効期間を自動管理するのに加え、セーフ配列での操作を簡素化するのに役立つメソッドをいくつか提供します。たとえば、GetCount メソッドを呼び出すだけで、ラップされているセーフ配列の要素数を取得できます。セーフ配列の項目にアクセスするには、項目のインデックスを指定して、CComSafeArray::GetAt メソッドと SetAt メソッドを呼び出します。

他にも、既存のセーフ配列内の要素を反復処理する場合は、次のようなコードを使用できます。

// Assume sa is a CComSafeArray instance wrapping an existing safe array.
// Note that this code is generic enough to handle safe arrays
// having lower bounds different than the usual zero.
const LONG lb = sa.GetLowerBound();
const LONG ub = sa.GetUpperBound();
// Note that the upper bound is *included* (<=)
for (LONG i = lb; i <= ub; i++)
{
  // ... use sa.GetAt(i) to access the i-th item
}

また、CComSafeArray は演算子 [] をオーバーロードして、セーフ配列の項目にアクセスするシンプルな構文も提供しています。ここからは、これらのメソッドの動作を見ていきます。

CComSafeArray::Add メソッドを呼び出すと、既存のセーフ配列に新しい項目を追加することもできます。さらに、CCom­SafeArray::Resize は、名前が示すとおり、ラップされているセーフ配列のサイズを変更するのに使用します。ただし、一般に、サイズ変更に関しては、std::vector を操作する C++ コードを作成する方がセーフ配列より高速で、他の STL アルゴリズムやサードパーティ製のクロスプラットフォーム C++ ライブラリにも組み込みやすいため、そちらをお勧めします。その後、std::vector のコンテンツの操作が完了した後データをセーフ配列にコピーすれば、(std::vector とは異なり) モジュールの境界を安全に越え、C++ 以外の言語で記述されたクライアントからも利用できるようになります。

セーフ配列のコピー操作は、オーバーロードされたコピー代入演算子 (=) を使用するか、CComSafeArray::CopyFrom などのメソッドを呼び出して行います。

CComSafeArray での「移動のセマンティクス」

C++11 の厳格な感覚では、CComSafeArray クラスは「移動のセマンティクス」を実装しません。実際、この ATL クラスは C++11 より前から存在し、少なくとも Visual Studio 2015 までは、移動コンストラクターや移動代入演算子など、C++11 の移動操作は追加されていません。ただし、CComSafeArray では異なる種類の移動のセマンティクスを提供します。これは Attach と Detach という 2 つのメソッドに基づきます。基本的に、セーフ配列記述子へのポインター (SAFEARRAY*) がある場合、これを Attach メソッドに渡します。すると、CComSafeArray がそのセーフ配列そのものの所有権を受け取ります。CComSafeArray オブジェクトにラップされ、以前から存在するすべてのセーフ配列は正しくクリーンアップされていることに注意してください。

同じように、CComSafeArray::Detach メソッドを呼び出すことができます。すると、CComSafeArray ラッパーは、ラップされたセーフ配列の所有権を呼び出し元に解放し、前に所有していたセーフ配列記述子へのポインターを返します。C++ コードで CComSafeArray を使用してセーフ配列を作成し、COM インターフェイス メソッドや C インターフェイス DLL 関数などで呼び出し元への出力ポインター パラメーターとして扱うときは、この Detach メソッドが便利です。実際、C++ クラス CComSafeArray は COM や C インターフェイス DLL の境界を越えることはできません。これが可能なのは、SAFEARRAY* 記述子ポインターそのもののみです。

COM と C DLL の境界を越えられない C++ 例外

Create、CopyFrom、SetAt、Add、Resize など、一部の CComSafeArray メソッドは、COM プログラミングの通例に従い、成功またはエラー状態を通知する HRESULT を返します。ただし、CComSafeArray コンストラクターの一部のオーバーロードなどのメソッド、GetAt、演算子 [] は、エラー時に実際に例外をスローします。既定では、ATL は CAtlException 型の C++ 例外をスローします。この型は、HRESULT を囲む小さなラッパーです。

しかし、C++ の例外は COM メソッドや C インターフェイス DLL 関数の境界を越えることはできません。COM メソッドの場合、エラーを通知するために必ず HRESULT を返します。このオプションは、C インターフェイス DLL でも使用できます。そのため、CComSafeArray ラッパー (または同様に例外をスローするコンポーネント) を使用する C++ コードを作成するときは、そのコードを try/catch ブロックを使用して保護することが重要です。たとえば、HRESULT を返す COM メソッドの実装内部や、DLL の境界関数内部では、以下のようなコードを記述できます。

try
{
  // Do something that can potentially throw exceptions as CAtlException ...
}
catch (const CAtlException& e)
{
  // Exception object implicitly converted to HRESULT,
  // and returned as an error code to the caller
  return e;
}
// All right
return S_OK;

バイト列のセーフ配列の作成

ここまではセーフ配列と便利な C++ CComSafeArray ATL ラッパーを導入する概念的な枠組みを示しました。ここからは、セーフ配列プログラミングの実践的な応用例を紹介しながら、CComSafeArray の動作を示します。最初はシンプルなケースとして、C++ コードからバイト列のセーフ配列を作成します。このセーフ配列は、COM インターフェイス メソッドや C インターフェイス DLL 関数の出力パラメーターとして渡すことができます。

通常、セーフ配列はその配列記述子へのポインター経由で参照されます。このポインターは、C++ コードでは SAFEARRAY* と解釈されます。セーフ配列の出力パラメーターがある場合は、別のレベルの関節参照が必要になります。そのため、出力パラメーターとして渡すセーフ配列は SAFEARRAY** の形式 (SAFEARRAY 記述子へのポインターのポインター) をとります。

STDMETHODIMP CMyComComponent::DoSomething(
    /* [out] */ SAFEARRAY** ppsa)
{
  // ...
}

前述のように、COM メソッド内部では、try/catch による保護を忘れずに使用して、例外をキャッチし、その例外を対応する HRESULT に変換します (STDMETHODIMP プリプロセッサ マクロは、そのメソッドが HRESULT を返すことを示します)。

try ブロック内では、BYTE をテンプレート パラメーターとして指定して、CComSafeArray クラス テンプレートのインスタンスを作成します。

// Create a safe array storing 'count' BYTEs
CComSafeArray<BYTE> sa(count);

その後、CComSafeArray のオーバーロード演算子 [] を以下のように使用して、セーフ配列の要素に単純にアクセスできます。

for (LONG i = 0; i < count; i++)
{
  sa[i] = /* some value */;
}

セーフ配列にデータを完全に書き込んだら (たとえば std::vector からデータをコピーしたら)、CComSafeArray の Detach メソッドを呼び出して、セーフ配列を出力パラメーターとして返すことができます。

*ppsa = sa.Detach();

Detach を呼び出した後、CComSafeArray ラッパー オブジェクトがセーフ配列の所有権を呼び出し元に移します。そのため、CComSafeArray オブジェクトがスコープ外になっても、上記のコードで作成したセーフ配列は破棄されません。Detach メソッドにより、セーフ配列のデータは、最初にセーフ配列を作成したコードから呼び出し元へと転送 (「移動」) されます。これで、セーフ配列が必要なくなった場合に破棄するのは、呼び出し元の役割になります。

セーフ配列は、DLL モジュールの境界を越えて配列データを交換する場合にも役立ちます。たとえば、C++ で C インターフェイス DLL があるとします。この DLL は、C# クライアント コードで利用されるセーフ配列を作成します。DLL によってエクスポートされる境界関数のプロトタイプが以下のようになっているとします。

extern "C" HRESULT __stdcall ProduceByteArrayData(/* [out] */ SAFEARRAY** ppsa)

これに対応する C# Plnvoke 宣言は、以下のようになります。

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void ProduceByteArrayData(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_UI1)]
  out byte[] result);

UnmanagedType.SafeArray は、関数境界における実際のネイティブ配列型が SAFEARRAY であることを意味します。SafeArraySubType に代入されている値 Var­Enum.VT_UI1 は、セーフ配列に格納されるデータ型が BYTE (サイズが正確に 1 バイトの符号なし整数) であることを指定します。4 バイトの符号付き整数を格納しているセーフ配列の場合、C++ 側では CComSafe­Array<int> になり、対応する PInvoke VarEnum 型は VT_I4 (4 バイト サイズの符号付き整数) になります。このセーフ配列は、C# では byte[] 配列にマップされ、出力パラメーターとして渡されます。

PreserveSig = false 属性は、ネイティブ関数によって返されたエラーの HRESULT を C# では例外として解釈するよう PInvoke に指示します。

図 1 に、C++ で CComSafeArray を使用してバイト列のセーフ配列を作成するコードの完全なサンプルを示します。このコードは架空の COM メソッドの一部です。ただし、CComSafeArray をべースとする同じコードを、C インターフェイス DLL の境界関数にも同様に使用できます。

図 1 CComSafeArray を使用したバイト列のセーフ配列の作成

STDMETHODIMP CMyComComponent::DoSomething(/* [out] */ SAFEARRAY** ppsa) noexcept
{
  try
  {
    // Create a safe array storing 'count' BYTEs
    const LONG count = /* some count value */;
    CComSafeArray<BYTE> sa(count);
    // Fill the safe array with some data
    for (LONG i = 0; i < count; i++)
    {
      sa[i] = /* some value */;
    }
    // Return ("move") the safe array to the caller
    // as an output parameter
    *ppsa = sa.Detach();
  }
  catch (const CAtlException& e)
  {
    // Convert ATL exceptions to HRESULTs
    return e;
  }
  // All right
  return S_OK;
}

文字列のセーフ配列の作成

ここまでに、バイト列のセーフ配列と、C インターフェイス DLL 関数でセーフ配列を出力パラメーターとして渡す際に C# で使用する PInvoke 宣言シグネチャを作成する方法を説明しました。このようなコーディング パターンは、int、float、double などのスカラー型を格納するセーフ配列でも適切に機能します。これらの型は C++ でも C# でもバイナリ表現は同じになるため、その 2 つの領域どうしや、COM 要素の境界を越えて簡単にマーシャリングされます。

ただし、文字列を格納するセーフ配列にはいくつか追加の注意が必要です。文字列は、バイト列、整数、浮動小数点数などの 1 つのスカラーよりも複雑になるため、特に注意する必要があります。BSTR 型は、モジュールの境界を安全に越える文字列を表すのに役立ちます。この型の汎用性は高くはありませんが、事前に長さが決まっている Unicode UTF-16 文字列ポインターと考えることができます。既定のマーシャラーは、BSTR のコピー方法と、BSTR に COM や C インターフェイス関数の境界を越えさせる方法を理解しています。BSTR のセマンティクスの興味深い詳しい説明については、Eric Lippert のブログ記事 (bit.ly/2fLXTfY、英語) をご覧ください。

C++ で文字列を格納するセーフ配列を作成するには、BSTR 型を使用して CComSafeArray クラス テンプレートのインスタンスを作成します。

// Creates a SAFEARRAY containing 'count' BSTR strings
CComSafeArray<BSTR> sa(count);

ここで指定するセーフ配列の型は BSTR C 型そのものですが、(利便性と安全性のため) BSTR そのものを囲む RAII ラッパーを使用する方が適切です。ATL ではこのような簡易ラッパーを、CComBSTR クラスの形式で提供します。Microsoft Visual C++ (MSVC) でコンパイルされた Win32/C++ コードでは、std::wstring クラスを使って Unicode UTF-16 文字列を表現できます。そのため、std::wstring を ATL::CComBSTR に変換するヘルパー関数をビルドするのが妥当です。

// Convert from STL wstring to the ATL BSTR wrapper
inline CComBSTR ToBstr(const std::wstring& s)
{
  // Special case of empty string
  if (s.empty())
  {
    return CComBSTR();
  }
  return CComBSTR(static_cast<int>(s.size()), s.data());
}

CComSafeArray<BSTR> に STL vector<wstring> からコピーした文字列を設定するには、このベクター全体を反復処理し、前述のヘルパー関数を呼び出して、ベクター内の wstring ごとに、対応する CComBSTR オブジェクトを作成します。

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

次に、CComSafeArray::SetAt メソッドを呼び出して、返された bstr をセーフ配列オブジェクトにコピーします。

hr = sa.SetAt(i, bstr);

SetAt メソッドは HRESULT を返すため、以下のようにその値をチェックしてエラーの場合に例外をスローするのが優れたプログラミング プラクティスです。

if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

この例外は COM メソッドまたは C インターフェイス DLL 関数の境界で、HRESULT に変換することになります。また、エラー HRESULT を前述のコードから直接返してもかまいません。

先ほどの CComSafeArray<BYTE> のサンプルと比較した場合、BSTR セーフ配列の主な違いは、BSTR 文字列を囲む中間 CComBSTR ラッパーを作成している点にあります。このようなラッパーは、バイト、整数、浮動小数点数のような単純なスカラー型には必要ありませんが、適切な有効期間管理を必要とする BSTR などの複合型には、C++ の RAII ラッパーが役立ちます。CComBSTR などのラッパーは、既存の C スタイルの文字列ポインターから新しい BSTR を作成する SysAllocString などの関数呼び出しを隠ぺいします。同様に、CComBSTR デストラクターは SysFreeString を自動的に呼び出し、BSTR のメモリを解放します。こうした BSTR の有効期間管理の詳細はすべて CComBSTR クラスの実装内部に隠ぺいされるため、C++ プログラマはコードの高度なロジックに専念できます。

BSTR のセーフ配列向けの「移動セマンティクス」の最適化

前述の CComSafeArray<BSTR>::SetAt メソッド呼び出しでは、セーフ配列への入力 BSTR のディープ コピーを実行します。実際、SetAt メソッドには、ブール型の bCopy という 3 番目のパラメーターが追加されています。このパラメータの既定値は TRUE です。この bCopy フラグ パラメーターは、バイト、整数、浮動小数点数などのスカラー型では重要ではありません。スカラー型はすべてセーフ配列にディープ コピーされます。ただし、有効期間管理やコピーのセマンティクスが簡単でない BSTR などの複合型にとっては重要です。たとえば、CComSafeArray<BSTR> の場合、SetAt メソッドの 3 番目のパラメーターに FALSE を指定すると、セーフ配列はディープ コピーを行う代わりに、単純に入力 BSTR の所有権を受け取ります。これはディープ コピーではなく、移動操作の簡単な最適化のようなものです。この最適化では、CComBSTR ラッパーの Detach メソッドを呼び出して、BSTR の所有権を CComBSTR から CComSafeArray へと適切に移すことも必要になります。

hr = sa.SetAt(i, bstr.Detach(), FALSE);

CComSafeArray<BYTE> のサンプルで既に示したように、CComSafeArray<BYTE> を一度作成すれば、以下のようなコードを使ってセーフ配列を呼び出し元に渡す (移動する) ことができます。

// Return ("move") the safe array to the caller
// as an output parameter (SAFEARRAY **ppsa)
*ppsa = sa.Detach();

BSTR 文字列のセーフ配列をビルドして呼び出し元に渡する C インターフェイス DLL 関数は、C# では以下のように PInvoke を行うことができます。

[DllImport("NativeDll.dll", PreserveSig = false)]
public static extern void BuildStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_BSTR)]
  out string[] result);

VarEnum.VT_BSTR の使用は、BSTR 文字列を格納するセーフ配列の存在を示します。ネイティブ C++ コードで作成された BSTR の SAFEARRAY は、string[] 配列型を使用して出力パラメーターとして渡し、C# にマーシャリングします。

文字列を含むバリアントのセーフ配列の作成

もう少し複雑さの度合いを上げてみましょう。バイト、整数、BSTR 文字列などの型の要素を格納するセーフ配列以外にも、“ジェネリック” 型の VARIANT 型のセーフ配列を作成することもできます。バリアントは、整数、浮動小数点数、BSTR 文字列など、多種多様な型の値を格納できるポリモーフィックな型です。VARIANT C 型は、基本的には大きな共用体です。この型の定義については、bit.ly/2fMc4Bu (英語) を参照してください。BSTR C 型と同様、ATL は C VARIANT 型そのものを囲む便利な C++ ラッパーとして ATL::CComVariant クラスを提供します。C++ コードでは、バリアントの割り当て、コピー、クリア用の C 関数を直接呼び出すよりも、CComVariant ラッパーを使用してバリアントを処理する方が簡単かつ安全です。

C++ および C# で記述されたクライアントは、(前述のバイト列や BSTR の例で示したように) 型を「直接」格納するセーフ配列を理解しますが、バリアントを格納するセーフ配列のみを理解するスクリプト クライアントも存在します。したがって、C++ で配列データを構築し、このようなスクリプト クライアントでこの配列データを利用できるようにする場合、新しいレベルの間接参照 (とさらなるオーバーヘッド) を追加して、バリアントを格納するセーフ配列に構築したデータをパックする必要があります。セーフ配列の各バリアント項目は、その後、バイト、浮動小数点数、BSTR などのサポート対象の型の値のいずれかに変換されます。

BSTR 文字列のセーフ配列を構築する前述のコードを、バリアントのセーフ配列を使用するように変更するとします。バリアントには BSTR も含まれますが、ここで作成するのはバリアントのセーフ配列で、BSTR 文字列を直接含むセーフ配列ではありません。

バリアントのセーフ配列を作成するには、まず、以下の方法で CComSafeArray クラス テンプレートのインスタンスを作成します。

// Create a safe array storing VARIANTs
CComSafeArray<VARIANT> sa(count);

次に、たとえば STL vector<wstring> に格納された一連の文字列をすべて反復処理して、前のコード サンプルのように、wstring ごとに CComBSTR オブジェクトを作成します。

// 'v' is a std::vector<std::wstring>
for (LONG i = 0; i < count; i++)
{
  // Copy the i-th wstring to a BSTR string
  // safely wrapped in ATL::CComBSTR
  CComBSTR bstr = ToBstr(v[i]);

ここに、BSTR のセーフ配列との違いが生まれます。実際には、(BSTR を直接含むセーフ配列ではなく) バリアントのセーフ配列を作成しているため、SetAt メソッドを呼び出して CComSafeArray に BSTR オブジェクトを直接格納することはできません。代わりに、まず、bstr をラップするためにバリアント オブジェクトを作成する必要があります。次に、そのバリアント オブジェクトをバリアントのセーフ配列に挿入します。

// First create a variant from the CComBSTR
  CComVariant var(bstr);
  // Then add the variant to the safe array
  hr = sa.SetAt(i, var);
  if (FAILED(hr))
  {
    AtlThrow(hr);
  }
} // For loop

CComVariant には、const wchar_t* ポインターを受け取るオーバーロード コンストラクターがあります。これにより、wstring c_str メソッドを呼び出して、std::wstring から CComVariant を直接作成できるようになります。ただし、今回の場合、バリアントはオリジナルの wstring のうち、最初の NUL 終端文字までの先頭ブロックを格納することになります。一方、wstring と BSTR はどちらも、潜在的に NUL が埋め込まれた状態の文字列を格納することができます。そのため、std::wstring から中間 CComBSTR を作成するときは、前にカスタム ToBstr ヘルパー関数で行ったように、ジェネリックな場合に対応するようにします。

通常通り、作成したセーフ配列を SAFEARRAY** 出力パラメーターとして呼び出し元に返すには、CComSafeArray::Detach メソッドを使用できます。

// Transfer ownership of the created safe array to the caller
*ppsa = sa.Detach();

セーフ配列は以下のように C インターフェイス関数を経由して渡します。

extern "C" HRESULT __stdcall BuildVariantStringArray(/* [out] */ SAFEARRAY** ppsa)

この場合、以下の C# PInvoke 宣言を使用できます。

[DllImport("NativeDll.dll", PreserveSig = false)]
pubic static extern void BuildVariantStringArray(
  [Out, MarshalAs(UnmanagedType.SafeArray, SafeArraySubType = VarEnum.VT_VARIANT)]
  out string[] result);

SafeArraySubType に VarEnum.VT_VARIANT を使用している点に注意してください。それは今回の場合、C++ で作成するセーフ配列がバリアント (BSTR 文字列をラップ) を含み、BSTR は直接含まないためです。

一般には、バリアントのセーフ配列しか処理できないスクリプト クライアントからデータにアクセスできるようにするという条件がある場合を除いて、バリアントを格納するセーフ配列ではなく、直接型のセーフ配列を使用してデータをエクスポートするようにします。

まとめ

セーフ配列データ構造は、異なるモジュールや言語の境界を越えてデータを交換する際に役立つツールです。セーフ配列は用途の広いデータ構造で、バイト列、整数、浮動小数点数などの単純なプリミティブ型だけでなく、BSTR 文字列やジェネリックなバリアントなどの複合型も格納できます。今回は、ATL ヘルパー クラスを使用する具体的なサンプルを示しながら、C++ でセーフ配列データ構造のプログラミングを簡素化する方法を紹介しました。


Giovanni Dicanio は、C++ と Windows OS を専門とするコンピューター プログラマであり、Pluralsight (bit.ly/GioDPS、英語) の執筆者です。また、Visual C++ MVP でもあります。プログラミングとコース作成の傍ら、C++ 専門のフォーラムやコミュニティで他の開発者をサポートしています。彼の連絡先は、giovanni.dicanio@gmail.com (英語のみ) です。また、msmvps.com/gdicanio (英語) でブログも公開しています。

この記事のレビューに協力してくれた技術スタッフの David Carvey と Marc Gregoire に心より感謝いたします。
David Cravey は、GlobalSCAPE のエンタープライズ アーキテクトです。いくつかの C++ ユーザー グループのまとめ役であり、4 度 Visual C++MVP を受賞しています。
Marc Gregoire はベルギー出身のシニア ソフトウェア エンジニアであり、Belgian C++ Users Group の創設者であり、『Professional C++』(Wiley、2014 年) の著者であり、『C++ Standard Library Quick Reference』(Apress、2016 年) の共著者でもあります。さらに、彼はさまざまな書籍のテクニカル ディレクターを務め、2007 年以降は、彼の VC++ の専門知識について MVP を毎年受賞しています。彼の連絡先は、marc.gregoire@nuonsoft.com (英語のみ) です。