作成日: 2003/09/08

ATL で作成した COM オブジェクトに自前の構造体を定義して渡す

概要

ATL で作成した COM オブジェクトのメソッドに引数を渡したいときがあります。 渡さなければならない値の個数が少なければ、そのまま引数として並べてしまえば良いのですが、 引数の個数が多すぎる、あるいは、みてくれが悪いという理由で、どうしても構造体が使いたい場合があります。
カスタムインタフェース経由で渡す方法とディスパッチインタフェース経由で渡す方法の 2 つについて書いてあります。 なお、ディスパッチインタフェース経由での方法は本当の意味での構造体を渡してはいませんので、 本質的な解決にはなっていません。ご注意ください。 また、カスタムインタフェースとディスパッチインタフェースの詳細については、 私は上手に説明できるほど詳しくありませんので、申し訳ありませんが、ネットや書籍を参考にしてください。 なお、書籍については参考書籍としてあげてあります。 以下の説明では VC++ 6 と VB 6 を使用しています。

カスタムインタフェース

この方法で作成したメソッドは、C++ や VB でのカスタムインタフェース側へのバインドなどの、 アーリーバインディングが行える環境でのみ使用できます。
方法自体は簡単で、IDL に自分が作成したい構造体を定義しておき、 インタフェース内のメソッドの引数で使用するだけです。

プロジェクトを作成する

VC を起動し、メニューから [ファイル] - [新規作成]を選びます。 表示される [新規作成] ダイアログボックスから [ATL COM AppWizard] を選択し、 適当なプロジェクト名を入力し、[OK] をクリックします。ここでは、custatlobj としました。

新規作成

[ATL COM AppWizard - ステップ 1/1] が表示されますので、そのまま [終了] をクリックします。

ATL COM AppWizard - ステップ 1/1

[新規プロジェクト情報] が表示されるので、これも、そのまま [OK] をクリックします。

ATL オブジェクトを挿入する

メニューから [挿入] - [ATL オブジェクトの新規作成] を選びます。
[ATL オブジェクトウィザード] が表示されるので、[カテゴリ] に [オブジェクト]、 [オブジェクト] に [シンプルオブジェクト] を選択して、[次へ] をクリックします。

ATL オブジェクトウィザード

[ATL オブジェクトウィザードのプロパティ] が表示されるので、[名前] タブをクリックし、 ショートネームに適当な名前を入力します。ここでは Custobj としました。

ATL オブジェクトウィザードのプロパティ - 名前

[アトリビュート] タブをクリックし、[インタフェース] に [カスタム] を選択し、 [OK] をクリックします。

ATL オブジェクトウィザードのプロパティ - アトリビュート

オブジェクトが挿入されたのを [ClassView] などで確認してください。

構造体を定義する

自分の使いたい構造体を IDL に定義します。ここでは、long の変数を 2 つもつ MyCustParam という名前の構造体を定義しました。

typedef struct MyCustParam {
    long m1;
    long m2;
} MyCustParam;

この内容をプロジェクトのフォルダ内に拡張子を .idl として、適当な名前で保存します。ここでは MyCustParam.idl として保存しました。
次に、[FileView] からプロジェクトのメインの IDL (custatlobj.idl) を開き、 作成した MyCustParam.idl をインポートするように指示します。

MyCustParam.idl をインポートする

次に、[FileView] で右クリックし表示されるポップアップメニューから [ファイルをプロジェクトへ追加] を選択します。
[プロジェクトへファイルを追加] ダイアログが表示されるので、MyCustParam.idl を選択し、[OK] をクリックします。
※プロジェクトに MyCustParam.idl を追加するのを忘れると、MyCustParam.idl から生成されるはずの MyCustParam.h が生成されず、コンパイル時にエラーになります。

MyCustParam.idl をプロジェクトに追加する

インタフェースにメソッドを定義する

[ClassView] でインタフェース ICustobj を右クリックします。表示されるポップアップメニューから [メソッドの追加] を選択します。
[インターフェースへメソッドを追加] ダイアログが表示されるので、[メソッド名] に m1、[パラメータ] に

[in] MyCustParam* p1, [in, out] MyCustParam* p2, [out, retval] MyCustParam* p3

と入力して [OK] をクリックします。

インターフェースへメソッドを追加する

この作業で、IDL ファイル (custatlobj.idl) に IDL 定義が追加され、対応する C++ のクラス定義とメソッドが、それぞれ、 Custobj.h、Custobj.cpp に追加されますので、確認してください。
※メソッド定義の際には、[in]、[in, out]、[out, retval] を正しく入力してください。 なお、作成された IDL 定義とクラス定義を見ればわかるのですが、C++ のクラス定義ではこれらの文字はコメント化され、コードに影響しません。

C++ のクラス定義とメソッドを確認する

が、IDL の方には、きちんと残っています。

IDL を確認する

これらの指示文字列は、COM が引数値のマーシャリングを行う際に、引数をメソッドに渡すのか、あるいは、メソッドから値を戻すのか、 などの情報を得るために必要になります。
※IDL から生成されるスタブ/プロキシのコードに影響します。
そのため、この指示文字列が間違っていると、マーシャリング時に引数値を正しく渡せなかったり、 戻せなかったりしますので、気をつけてください。

メソッドを実装する

メソッドの中身を書きます。インタフェースにメソッドを定義する 作業で、 Custobj.cpp に以下のようにメソッドが作成されていますので、ここにコードを追加します。

/////////////////////////////////////////////////////////////////////////////
// CCustobj


STDMETHODIMP CCustobj::m1(MyCustParam *p1, MyCustParam *p2, MyCustParam *p3)
{
    // TODO: この位置にインプリメント用のコードを追加してください

    return S_OK;
}

メソッドを定義した際に、マーシャリング時の引数の渡される方向を指定しました ([in]、[in, out] など)。
その意味は、

  • [in] MyCustParam* p1: p1 はメソッドに渡される。
  • [in, out] MyCustParam* p2: p2 はメソッドに渡されるとともに、メソッドから値を返すためにも使用できる。
  • [out, retval] MyCustParam* p3: p3 はメソッドから返されるとともに、返り値となる

となります。これを踏まえて、メソッド内の関数の処理は以下のようにします。なお、ここの処理は適当で、 要は、構造体引数を通して、値のやり取りができることを確認するためにしているだけです。

  • 第1引数 p1 のメンバ m2 + 10 の値を、第2引数 p2 のメンバ m2 に設定する。
  • 第1引数 p1 のメンバ m1 の値を、第3引数 p3 のメンバ m1 に設定する。
  • 第2引数 p2 のメンバ m2 の値を、第3引数 p3 のメンバ m2 に設定する。

コードは、例えば以下のようになります。

/////////////////////////////////////////////////////////////////////////////
// CCustobj


STDMETHODIMP CCustobj::m1(MyCustParam *p1, MyCustParam *p2, MyCustParam *p3)
{
    // 第1引数 p1 のメンバ m2 + 10 の値を、第2引数 p2 のメンバ m2 に設定する。
    p2->m2 = p1->m2 + 10;

    // 第1引数 p1 のメンバ m1 の値を、第3引数 p3 のメンバ m1 に設定する。
    p3->m1 = p1->m1;

    // 第2引数 p2 のメンバ m2 の値を、第3引数 p3 のメンバ m2 に設定する。
    p3->m2 = p2->m2;

    return S_OK;
}

コードを記述したら、コンパイル/リンクしてください。エラーがなければ、完成です。

オブジェクトを使用する

では、作成した ATL オブジェクトが正しく動作するか確認します。動作確認は、コードを書くのが簡単なので、VB で行います。 VB を起動し、適当なプロジェクトを作成してください。
ここでは、フォームにボタンを 1 つ載せて、ボタンの Click イベントに処理を記述しました。

オブジェクトを使用する VB プログラムを作る

なお、事前に、プロジェクトの参照設定で、先ほど作成したオブジェクトを参照しておいてください。
参照設定の方法は、メニューから [プロジェクト] - [参照設定] を選び、[参照設定] ダイアログを開き、 [参照可能なライブラリファイル] で [custatlobj 1.0 タイプライブラリ] にチェックを入れ、[OK] をクリックします。

参照設定

ボタンの Click イベント内に以下のようなコードを記述します。 なお、結果はデバッグ(イミディエイト)ウィンドウに表示します。

Private Sub cmdCall_Click()
    ' パラメータを設定する。
    Dim p1 As MyCustParam
    Dim p2 As MyCustParam
    Dim p3 As MyCustParam

    p1.m1 = 11
    p1.m2 = 12

    p2.m1 = 21
    p2.m2 = 22

    ' 呼び出す。
    Dim obj As Custobj
    Set obj = New Custobj
    p3 = obj.m1(p1, p2)

    ' 結果を表示する。
    Debug.Print "p1:", p1.m1, p1.m2
    Debug.Print "p2:", p2.m1, p2.m2
    Debug.Print "p3:", p3.m1, p3.m2
End Sub

コードを記述したら、実行してみてください。[イミディエイト] ウィンドウに以下のように表示されたでしょうか?。

p1:            11            12
p2:            21            22
p3:            11            22

もし、オブジェクトが作成できないなどのラインタイムエラーが出る場合は、参照情報が正しく設定されているか確認してください。

ランタイムエラー

VB はプロジェクトファイル (*.vbp) 内に参照情報へのパスを記録しているため、このページの下の サンプルソースをダウンロードしてそのまま使用した場合、 作成時のオブジェクトのパス(私の環境)とロード時のオブジェクトのパスが違うために、うまく読み込めない可能性がありますので、 参照情報を設定し直して、実行してみてください。

ディスパッチインタフェース

カスタムインタフェースは簡単ですが、 アーリーバインディングでしか使用できないという欠点があります。 最近では、IE などのクライアントサイドスクリプトから VBScript などでオブジェクトを作成し、 メソッドを呼び出さなければならないこともよくあります。VBScript などにはアーリーバインディングのための仕組みがないため、 カスタムインタフェースを使用することができません。
これらのスクリプト言語から呼び出せるようにするために、 オブジェクトはレイトバインディングを利用できる、ディスパッチインタフェースを実装する必要があります。
しかし、単純にディスパッチインタフェースを実装しようとしても、やっぱりそのままでは引数に構造体を使うことはできません。 というのは、ディスパッチインタフェースのマーシャリングでは通常、汎用マーシャラが使われるため、 メソッドの引数として使える変数の型は、オートメーション型に限定されてしまうのです (たぶん。。。)。
オートメーション型では、 BSTR, short, long, float, double, Byte, BOOL, CURRENCY, DATE, VARIANT などの型を使うことができるのですが、当然ながら、 自前の構造体はこの中にはありません。
残念ながら自前でマーシャリングコードを書く方法を私は知らないため、 代わりに COM オブジェクトを構造体に見立てて引数として利用すれば、 汎用マーシャラの恩恵に預かりつつ、構造体らしき構文を利用できるのではと思い、やってみました。
※本質的な解決で無くてすみません。

プロジェクトを作成する。

カスタムインタフェースのプロジェクトを作成するを参照ください。 なお、プロジェクト名は dispatlobj とします。

構造体代わりのオブジェクトを挿入する

カスタムインタフェースでは、構造体を使用しましたが、 構造体の代わりに使用するオブジェクトを定義します。オブジェクトの挿入は、 カスタムインタフェースのATL オブジェクトを挿入するとまったく同じ手順です。 なお、ATL オブジェクトの名前は MyDispParam とし、[インタフェース] には [デュアル] を選択してください。

※1 デュアルを選択すると、カスタムインタフェースとディスパッチインタフェースの両方が実装されます。 また、ATL オブジェクトウィザードにはディスパッチインタフェースだけを実装するオプションは無さそうです。

※2 カスタムインタフェースでは、先にオブジェクトを定義してから構造体を定義しましたが、ここでは、 先に構造体代わりのオブジェクトを定義しています。これには、一応、意味があります。 VC のウィザードを使って、インタフェースを作成すると、IDL ファイルには、 インタフェースを定義した順番に、定義が追加されていきます。先にメインのオブジェクトを定義してから、 構造体代わりのオブジェクトを定義すると、構造体代わりのオブジェクトが、 メインのオブジェクトよりも後に定義されてしまいます。 C のスコープと同じで、IDL でも後から定義したオブジェクトを、先に定義したオブジェクトから使おうとすると、 先に定義したオブジェクトのスコープ上に後から定義したオブジェクトがないため、 IDL のコンパイル時にエラーになってしまいます。

    [
        ...
    ]
    interface IDispobj : IDispatch
    {
        // この位置では、IMyDispParam のインタフェースが参照できない。
        [id(1), helpstring("メソッド m1")] HRESULT m1([in] IMyDispParam *p1, ...);
    };
    [
        ...
    ]
    interface IMyDispParam : IDispatch
    {
        ...
    };

以下のように、使用前に名前だけ宣言しておくことで、名前解決はできます。

    // 名前解決のために、ここで宣言する。
    interface IMyDispParam;
    [
        ...
    ]
    interface IDispobj : IDispatch
    {
        [id(1), helpstring("メソッド m1")] HRESULT m1([in] IMyDispParam *p1, ...);
    };
    [
        ...
    ]
    interface IMyDispParam : IDispatch
    {
        ...
    };

が、この説明では、なるべく IDL を手作業でいじらないようにしたかったので、順番に定義されるように、 説明の順番を変えました。 このため、参照される順番にインタフェースが定義されるため、IDL をいじらなくとも名前解決がなされ、エラーになりません。

    [
        ...
    ]
    interface IMyDispParam : IDispatch
    {
        ...
    };
    [
        ...
    ]
    interface IDispobj : IDispatch
    {
        [id(1), helpstring("メソッド m1")] HRESULT m1([in] IMyDispParam *p1, ...);
    };

構造体代わりのオブジェクトを仕上げる

先に構造体代わりのオブジェクトを仕上げてしまいます。ここでは、メンバ変数の代わりにプロパティを使います。
プロパティは long の値で、m1 と m2 の 2 つのプロパティを定義します。
[ClassView] からインタフェース IMyDispParam を右クリックします。表示されるポップアップメニューから [プロパティの追加] を選択します。 [インターフェイスへプロパティを追加] ダイアログが表示されるので、[プロパティの種類] に long、[プロパティ名] に m1 として [OK] をクリックします。m1 を追加したら、m2 も同じようにして追加します。

インターフェイスへプロパティを追加する

次に、インタフェースのメンバの値を保存するたの変数とコードを実装します。
まず、MyDispParam.h を開き、クラス定義に、メンバ変数を追加するとともに、コンストラクタに変数の初期設定コードを追加します。

    CMyDispParam() : m_1(0), m_2(0)
    {
    }

    ...

// IMyDispParam
public:
    long m_1;
    long m_2;

次に、MyDispParam.cpp を開き、インタフェースのメソッドが呼ばれた際に、値を参照/設定できるように、コードを追加します。

/////////////////////////////////////////////////////////////////////////////
// CMyDispParam


STDMETHODIMP CMyDispParam::get_m1(long *pVal)
{
	*pVal = m_1;
	return S_OK;
}

STDMETHODIMP CMyDispParam::put_m1(long newVal)
{
	m_1 = newVal;
	return S_OK;
}

STDMETHODIMP CMyDispParam::get_m2(long *pVal)
{
	*pVal = m_2;
	return S_OK;
}

STDMETHODIMP CMyDispParam::put_m2(long newVal)
{
	m_2 = newVal;
	return S_OK;
}

以上で、構造体代わりのオブジェクトは完成です。

ATL オブジェクトを挿入する

カスタムインタフェースのATL オブジェクトを挿入するを参照ください。 なお、ATL オブジェクトの名前は Dispobj とし、[インタフェース] には [デュアル] を選択します。

ATL オブジェクトウィザードのプロパティ - 名前 ATL オブジェクトウィザードのプロパティ - アトリビュート

インタフェースにメソッドを定義する

次にメインのオブジェクトを仕上げます。 [ClassView] でインタフェース IDispobj を右クリックします。表示されるポップアップメニューから [メソッドの追加] を選択します。
[インターフェースへメソッドを追加] ダイアログが表示されるので、[メソッド名] に m1、[パラメータ] に

[in] IMyDispParam *p1, [in, out] IMyDispParam **pp2, [out, retval] IMyDispParam **pp3

と入力して [OK] をクリックします。

インタフェースへメソッドを追加する

※引数がインタフェースで、かつ、[out] を指定している場合は、ポインタへのポインタを指定しなければなりません。 そうしない場合、MIDL のコンパイル時に

warning MIDL2284 : [out] interface pointers must use double indirection

のような警告が出ます。以下のような関数で引数を渡すときに、ポインタへのポインタを渡さないと、 値を返すことができないのと同じ理由です。

void foo(int **ppv)
{
    int *pv = (int *) malloc(sizeof(int));
    *pv = 3000;
    *ppv = pv;
}

void bar(void)
{
    int *pv;
    foo(&pv);
    // ... pv を使う。
}

メソッドを実装する

メソッドの中身を書きます。インタフェースにメソッドを定義する作業で、 Dispobj.cpp に以下のようにメソッドが作成されていますので、ここにコードを追加します。

/////////////////////////////////////////////////////////////////////////////
// CDispobj


STDMETHODIMP CDispobj::m1(IMyDispParam *p1, IMyDispParam **pp2, IMyDispParam **pp3)
{
    // TODO: この位置にインプリメント用のコードを追加してください

    return S_OK;
}

実装する処理は、カスタムインタフェースのメソッドを実装すると同じです。 が、3番目の引数を返すために、メソッド内でパラメータオブジェクト (IMyDispParam) を作成します。 例えば、以下のようになります。
※以下のコードはエラー処理を考慮していません。実際に使用するコードでは、CoCreateInstance() の戻り値や、 各取得/設定メソッド呼び出しに対しても戻り値をチェックする処理を行ってください。

/////////////////////////////////////////////////////////////////////////////
// CDispobj


STDMETHODIMP CDispobj::m1(IMyDispParam *p1, IMyDispParam **pp2, IMyDispParam **pp3)
{
    // ※エラー処理がまったくされていませんので、ご注意ください。
    long temp;
    HRESULT hr;

    // p3 を生成する。
    hr = CoCreateInstance(CLSID_MyDispParam,
                     0,
                     CLSCTX_INPROC_SERVER,
                     IID_IMyDispParam,
                     (void **) pp3);
    ATLASSERT(SUCCEEDED(hr));
    ATLASSERT(pp3);

    // 第1引数 p1 のメンバ m2 + 10 の値を、第2引数 p2 のメンバ m2 に設定する。
    hr = p1->get_m2(&temp);
    ATLASSERT(SUCCEEDED(hr));

    hr = (*pp2)->put_m2(temp + 10);
    ATLASSERT(SUCCEEDED(hr));

    // 第1引数 p1 のメンバ m1 の値を、第3引数 p3 のメンバ m1 に設定する。
    hr = p1->get_m1(&temp);
    ATLASSERT(SUCCEEDED(hr));
    hr = (*pp3)->put_m1(temp);
    ATLASSERT(SUCCEEDED(hr));

    // 第2引数 p2 のメンバ m2 の値を、第3引数 p3 のメンバ m2 に設定する。
    hr = (*pp2)->get_m2(&temp);
    ATLASSERT(SUCCEEDED(hr));
    hr = (*pp3)->put_m2(temp);
    ATLASSERT(SUCCEEDED(hr));

    return S_OK;
}

コードを記述したら、コンパイル/リンクしてください。エラーがなければ、完成です。

オブジェクトを使用する

作成した ATL オブジェクトの確認には、またまた VB を使います。 内容は、カスタムコントロールのオブジェクトを使用すると同じです。 が、参照設定は行いません。また、ディスパッチインタフェース側に接続するため、コードが以下のように変わります。
以下では、CreateObject() により、動的にオブジェクトを作成し、使用しています。

Private Sub cmdCall_Click()
    ' パラメータを設定する。
    Dim p1 As Object
    Dim p2 As Object
    Dim p3 As Object

    Set p1 = CreateObject("dispatlobj.MyDispParam")
    p1.m1 = 11
    p1.m2 = 12

    Set p2 = CreateObject("dispatlobj.MyDispParam")
    p2.m1 = 21
    p2.m2 = 22

    ' 呼び出す。
    Dim obj As Object
    Set obj = CreateObject("dispatlobj.Dispobj")
    Set p3 = obj.m1(p1, p2)

    ' 結果を表示する。
    Debug.Print "p1:", p1.m1, p1.m2
    Debug.Print "p2:", p2.m1, p2.m2
    Debug.Print "p3:", p3.m1, p3.m2
End Sub

コードを記述したら、実行してみてください。[イミディエイト] ウィンドウに以下のように表示されたでしょうか?。

p1:            11            12
p2:            21            22
p3:            11            22

もし、オブジェクトが作成できないなどのラインタイムエラーが出る場合は、CreateObject() の引数が間違っていないか、 あるいは、先に、VC で作ったプロジェクトがコンパイル/リンクされ、オブジェクトが登録されているかを確認し、 再度、実行してみてください。

その他

上での説明では、シンプルオブジェクトしか作成していませんが、他の COM オブジェクトでも利用できます。 機会があれば、是非試してみてください。

サンプルソース
custatlobj.zip (22.2 KB)
カスタムインタフェース で、作成した ATL オブジェクトのソースです。
usecustobj.zip (1.45 KB)
カスタムインタフェースのオブジェクトを使用する で、使用した VB のソースです。
dispatlobj.zip (26.3 KB)
ディスパッチインタフェース で、作成した ATL オブジェクトのソースです。
usedispobj.zip (1.41 KB)
ディスパッチインタフェースのオブジェクトを使用する で、使用した VB のソースです。
参考書籍

以下の本は私が ATL を学習するのに購入して、有意義だったと思う本です。 少々古い本ですが、まだ、販売されていると思います。
※Amazon.co.jp で先ほど確認したところ(2003/09/08 17:00 現在)、 「Inside COM」 については在庫切れ、「ATL プログラミング」 については在庫がありました。

  • Inside COM
    Dale Rogerson 著 バウン グローバル 株式会社 訳
    アスキー出版局 ISBN4-7561-2176-4 初版発行 1997年11月1日
    COM の仕組みと COM コンポーネントの開発手法について書かれた本です。 本書では COM の仕組みとともに COM コンポーネントを 1 から (MFC も ATL も使わずに!) 実装する方法が解説してあります。
    解説は平易で分かりやすいのですが、集約あたりの内容はコードの複雑さに、正直頭をかきむしりました。 これを読んで、理解すれば、シンプルな COM オブジェクトなら、ベタで書くことが可能になります。 また、COM の基本が身につきますので、さらに上位の OLE や ATL などを学習する際にも強みになると思います。 いわゆるリファレンス本とは違いますので、じっくり学習したい方にお勧めです。
  • ATL プログラミング - C++ テンプレートによるスレッドセーフなコンポーネント開発
    Tom Armstrong 著 豊田 孝 監訳
    ソフトバンク株式会社 ISBN4-7973-0689-0 初版発行 1998年8月1日
    COM の基本と ATL での COM コンポーネントの開発手法について書かれた本です。 前半の 1 ~ 2 章は、COM の仕組みと共に、簡単な COM コンポーネントを ATL を使用せずに開発する方法について解説してあります。 その後、同じコンポーネントを ATL で開発する方法を解説した後、次第に ATL の高度な内容に移っていきます。
    「Inside COM」が COM の真髄を解説しているのとは違い、この本では、ATL を利用する上での動作背景として COM を解説しています。 ですが、十分に COM の内容は理解できますので、「Inside COM」 と両方買うにはお金が・・、という方は、こちらを買えば良いと思います。 なお、こちらも、リファレンス本ではありませんので、じっくり学習したい方にお勧めです。