オートメーション - メソッド呼び出しの実装

いちごパック > COM/ActiveXの解説 > オートメーション - メソッド呼び出しの実装
COMでは、Invoke()やGetIDsOfNames()を含むIDispatchを簡単に実装するための枠組みを用意しています。 実用上はその枠組みを利用することになるでしょう。 その方法は別途説明することにして、今回はInvoke()を直接実装していきます。
なお、このページの説明を含むビルド可能なコードはオートメーション - サンプルオートメーションサーバにおきました。

Invokeの引数とその内容

オートメーションでは、メソッド呼び出しのためにIDispatch::Invoke()メソッドが呼び出されます。
HRESULT STDMETHODCALLTYPE Invoke(
    DISPID dispIdMember, REFIID riidNull, LCID lcid,
    WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult,
    EXCEPINFO* pExcepInfo, UINT* puArgErr);
引数は次の通りです。
引数内容
DISPID dispIdMember呼び出すメソッド名に対応したID
REFIID riidNull常にIID_NULL
LCID lcid呼び出すメソッド名に対応したロケールID
WORD wFlags呼び出しの種類
DISPPARAMS* pDispParamsメソッドに渡す引数情報。DISPARAMS構造体内の引数は逆順で渡される
VARIANT* pVarResultメソッドの戻り値
EXCEPINFO* pExcepInfoメソッドの例外情報
UINT* puArgErr引数が間違っている場合に、最初に間違った引数のインデックス。間違った引数のインデックスのうちで最大のインデックスになる
ロケールIDは、例えば英単語のメソッド名と漢字のメソッド名の両方に対応する場合に利用可能です。 利用しない場合は値を無視してかまいません。今回のサンプルでは利用しません。 wFlagsはメソッド呼び出しの種類を表し、メソッド呼び出しであればDISPATCH_METHOD、 データメンバ値の取得であればDISPATCH_PROPERTYGET、 データメンバ値の設定であればDISPATCH_PROPERTYPUTを与えます。

VARIANT

ほぼどのような引数や戻り値でも扱えるように、 オートメーションの引数や戻り値はVARIANTと呼ばれる可変型でデータを受け渡しします。 VARIANTは可変型で値を保持するための構造体で、 例えばVARIANT.vtがVT_I4であればVARIANT.lValの32ビットint値が有効、 VARIANT.vtがVT_BSTRであればVARIANT.bstrValのBSTR文字列が有効といった形で、 VARIANT.vtの内容によって中身が変化します。
VARIANTは次のように利用します。
  • 未初期化のVARIANT変数は、VariantInit()で初期化します。VARIANT.vtはVT_EMPTYになります。
  • VARIANT.vt==VT_EMPTYのVARIANT変数に値を設定する場合は、VARIANT.vtに型を設定し、VARIANTの対応するフィールドに実際の値を設定します。
  • 初期化済みのVARIANT変数の値を解放したい場合は、VariantClear()を呼び出します。VARIANT.vtはVT_EMPTYになります。
  • 初期化済みのVARIANT変数の値を初期化済みの別のVARIANT変数にコピーしたい場合は、VariantCopy()を呼び出します。
  • VariantInit()はVARIANT.vtにVT_EMPTYをセットするだけの関数です。 VariantClear()は、例えば型がVT_BSTR値であれば文字列を解放するなど、値の解放を行ってからVARIANT.vtにVT_EMPTYをセットします。
    VARIANT.vtとVARIANTの値との関係の一部を表で示します。 VARIANT.vtの種類は拡張される可能性がありますので、VARIANT値の解放が必要な場合はVariantClear()を利用したほうが無難でしょう。
    VARIANT.vt有効なフィールド
    VT_EMPTYなしなし
    VT_NULLなしなし(EMPTYとは別)
    VT_UI1BYTEbVal
    VT_I1CHARcVal
    VT_UI2USHORTuiVal
    VT_I2SHORTiVal
    VT_UI4ULONGulVal
    VT_I4LONGlVal
    VT_UINTUINTuintVal
    VT_INTINTintVal
    VT_R4FLOATfltVal
    VT_R8DOUBLEdblVal
    VT_CYCYcyVal
    VT_DATEDATEdate
    VT_BSTRBSTRbstrVal
    VT_BOOLVARIANT_BOOLboolVal (0xffff=true, 0x0000=false)
    VT_UNKNOWNIUnknown*punkVal
    VT_DISPATCHIDispatch*pdispVal
    VT_ERRORSCODEscode
    VT_DECIMALDECIMALdecVal
    VT_BYREF | VT_UI1BYTE*pbVal
    VT_BYREF | VT_I1CHAR*pcVal
    VT_BYREF | VT_UI2USHORT*puiVal
    VT_BYREF | VT_I2SHORT*piVal
    VT_BYREF | VT_UI4ULONG*pulVal
    VT_BYREF | VT_I4LONG*plVal
    VT_BYREF | VT_UINTUINT*puintVal
    VT_BYREF | VT_INTINT*pintVal
    VT_BYREF | VT_R4FLOAT*pfltVal
    VT_BYREF | VT_R8DOUBLE*pdblVal
    VT_BYREF | VT_CYCY*pcyVal
    VT_BYREF | VT_DATEDATE*pdate
    VT_BYREF | VT_BSTRBSTR*pbstrVal
    VT_BYREF | VT_BOOLVARIANT_BOOL*pboolVal
    VT_BYREF | VT_UNKNOWNIUnknown**ppunkVal
    VT_BYREF | VT_DISPATCHIDispatch**ppdispVal
    VT_BYREF | VT_ERRORSCODE*pscode
    VT_BYREF | VT_DECIMALDECIMAL*pdecVal
    VT_ARRAY | (配列の型)SAFEARRAY*parray

    DISPPARAMS

    DISPPARAMSはメソッドの引数情報を表す次の構造体です。
    typedef struct {
        VARIANTARG* rgvarg;
        DISPID*     rgdispidNamedArgs;
        UINT        cArgs;
        UINT        cNamedArgs;
    } DISPPARAMS;
    
    VARIANTARGはVARIANTと同一と考えて問題ありません。 呼び出し側はVARIANTの配列を確保して引数を準備し、cArgsに引数の数、rgvargに引数を逆順に並べた配列へのポインタを格納します。 DISPATCH_PROPERTYPUTの場合は、rgdispidNamedArgs[0]にDISPID_PROPERTYPUTを、cNamedArgsに1をセットします。 それ以外の場合、cNamedArgsおよびはrgdispidNamedArgsはそれぞれ0、NULLをセットすれば良いようです。

    EXCEPINFO

    メソッド呼び出し中に例外が発生した場合、 IDispatch::Invoke()メソッドは、EXCEPINFOに例外の情報を格納してDISP_E_EXCEPTIONを返すことになっています。 EXCEPINFOの内容は次の通りです。
    typedef struct {
        WORD    wCode;
        WORD    wReserved;
        BSTR    bstrSource;
        BSTR    bstrDescription;
        BSTR    bstrHelpFile;
        DWORD   dwHelpContext;
        PVOID   pvReserved;
        HRESULT (__stdcall* pfnDeferredFillIn)(EXCEPINFO*);
        SCODE   scode;
    } EXCEPINFO;
    
    多くのフィールドがあり、使わないフィールド、予約されたフィールドにはすべて0やNULLをセットする必要がありますので、 COMサーバはEXCEPINFOをゼロクリアしてから値を設定すると良いでしょう。
    エラーコードは32ビット以上のCOMサーバであればHRESULT値をscodeに、 16ビットWindows用のCOMサーバであればwCodeに格納し、使わないほうのフィールドは0を設定します。 bstrSourceはアプリケーション名を表し、必ずセットする必要があります。 bstrDescriptionはエラーの内容、bstrHelpFileはヘルプファイル名、 dwHelpContextはヘルプファイル内のIDを表しますが、これら3つのフィールドは使わないのであればNULLや0でかまいません。 pfnDeferredFillInがNULLでない場合は、 bstrDescription、bstrHelpFile、dwHelpContextの3つのフィールドを参照する前に、 COMクライアントはこの関数を呼び出すことになっています。 これら3つのフィールドは必ず参照されるわけではありませんので、 これら3つのフィールドの設定に時間がかかる場合はpfnDeferredFillInを設定しておくと、値の設定を後回しにできます。

    Invokeの実装

    ここでは例として、Write()という(privateの)メソッドを呼び出すInvoke()の実装を示します。 予め、WriteのDISPIDを決めておきます。 ここで決めるDISPIDは提供するcoclassに対してのみ有効ですので、 Invoke()と後述のGetIDsOfNames()で値が一致していればどんな値でも、 例えば1でも10でも123でもかまいません。 ここではcoclassのクラス名をCIchigo3とし、DISPIDとして次のマクロDISPID_Ichigo3_Writeを参照することにします。
    #define DISPID_Ichigo3_Write 1
    
    CIchigo3::Invokeは、引数チェックや例外処理を省くと次のようになります。
    HRESULT STDMETHODCALLTYPE CIchigo3::Invoke(
        DISPID dispIdMember, REFIID riidNull, LCID lcid,
        WORD wFlags, DISPPARAMS* pDispParams, VARIANT* pVarResult,
        EXCEPINFO* pExcepInfo, UINT* puArgErr)
    {
        HRESULT hr;
    
    if ( (wFlags & DISPATCH_METHOD) != 0 && dispIdMember == DISPID_Ichigo3_Write ) { if ( pDispParams->cArgs != 1 ) { return DISP_E_BADPARAMCOUNT; } if ( pDispParams->rgvarg[0].vt != VT_BSTR ) { *puArgErr = 0; return DISP_E_TYPEMISMATCH; } hr = Write( pDispParams->rgvarg[0].bstrVal ); } else { return DISP_E_MEMBERNOTFOUND; } if ( FAILED(hr) ) { if ( pExcepInfo != NULL ) { pExcepInfo->scode = hr; pExcepInfo->wCode = 0; pExcepInfo->wReserved = 0; pExcepInfo->bstrSource = NULL; pExcepInfo->bstrDescription = NULL; pExcepInfo->bstrHelpFile = NULL; pExcepInfo->dwHelpContext = 0; pExcepInfo->pvReserved = NULL; pExcepInfo->pfnDeferredFillIn = NULL; } // pExcepInfoに値を設定する return DISP_E_EXCEPTION; } return hr; } HRESULT CIchigo3::Write( BSTR bstrWrite ) { // bstrWriteをファイルに書き出す }
    今回は型がBSTRでない場合にエラーとしていますが、そのような引数をBSTRに変換してもかまいません。 また、CIchigo3::Write()メソッドは引数が1つですが、複数の引数をとるメソッドではpDispParams->rgvargは逆順になります。

    GetIDsOfNamesとその実装

    IDispatch::GetIDsOfNamesは次の形をしています。
    HRESULT STDMETHODCALLTYPE GetIDsOfNames(
        REFIID riidNull, LPOLESTR* rgszNames, UINT cNames,
        LCID lcid, DISPID* rgDispId);
    
    引数は次の通りです。
    引数内容
    REFIID riidNull常にIID_NULL
    LPOLESTR* rgszNamesIDを取得したい文字列へのポインタの配列
    UINT cNamesIDを取得したい文字列の数
    LCID lcid呼び出すメソッド名に対応したロケールID
    DISPID* rgDispId返されたID
    ロケールIDは、例えば英単語のメソッド名と漢字のメソッド名の両方に対応する場合に利用可能です。 そのようなメソッド名を利用する場合は、GetIDsOfNames()メソッドで返されるIDをlcidと紐づけておき、 Invoke()メソッドでlcidとして同じ値が渡されていることを確認します。 そのようなメソッド名を利用しない場合、lcidは無視してかまいません。 指定された文字列がメソッド名でない場合はDISPIDとしてDISPID_UNKNOWNをセットし、 GetIDsOfNames()の戻り値としてDISP_E_UNKNOWNNAMEを返します。
    CIchigo3では、GetIDsOfNames()メソッドの実装は次のようになります。
    HRESULT STDMETHODCALLTYPE CIchigo3::GetIDsOfNames(
        REFIID riidNull, LPOLESTR* rgszNames, UINT cNames,
        LCID lcid, DISPID* rgDispId)
    {
        UINT    iName;
        HRESULT hr = S_OK;
        if ( riidNull != IID_NULL ) {
            return DISP_E_UNKNOWNINTERFACE;
        }
        for ( iName = 0; iName < cNames; ++iName ) {
            DISPID  id = DISPID_UNKNOWN;
            if ( lstrcmpW( rgszNames[iName], L"Write" ) == 0 ) {
                id = DISPID_Ichigo3_Write;
            }
            rgDispId[iName] = id;
            if ( id == DISPID_UNKNOWN ) {
                hr = DISP_E_UNKNOWNNAME;
            }
        }
        return hr;
    }
    

    例外処理

    オートメーションサーバでは、メソッドが失敗した場合に次の処理を行います。
  • メソッド内でIErrorInfoインターフェースのインスタンスを作成する。
  • COMの関数SetErrorInfo()を呼び出して例外情報をシステムに登録し、メソッドの戻り値としてFAILED(hr)が真となる値を返す。
  • Invoke()は、メソッドがエラーを返されたときにCOMの関数GetErrorInfo()を呼び出して例外情報を取得し、EXCEPINFOにその情報をセットする。
  • これらの項目を順に説明していきます。

    メソッドの例外処理

    IErrorInfoは次のインターフェースを持ちます。
    class IErrorInfo : public IUnknown
    {
    public:
        virtual HRESULT STDMETHODCALLTYPE GetGUID(
            GUID* pGUID) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetSource(
            BSTR* pBstrSource) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetDescription(
            BSTR* pBstrDescription) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetHelpFile(
            BSTR* pBstrHelpFile) = 0;
        virtual HRESULT STDMETHODCALLTYPE GetHelpContext(
            DWORD* pdwHelpContext) = 0;
    };
    
    このインターフェースを実装することは可能ですが、 COM側でもこのインターフェースの実装を提供しています。 この実装を利用する場合は、CreateErrorInfo()というCOMの関数を呼び出します。 CreateErrorInfo()はICreateErrorInfoインターフェースを返します。 ICreateErrorInfoインターフェースは、IErrorInfoインターフェースのメソッド名GetXXXがSetXXXになった、 値の設定メソッドを持ちます。 また、CreateErrorInfo()の返したオブジェクトのQueryInterface()を呼び出すと、 IErrorInfoインターフェースへのポインタを取得できます。
    CreateErrorInfo()を用いたメソッドの例外処理の例は次のようになります。
    HRESULT hr;
    ICreateErrorInfo* pcreateei = NULL;
    IErrorInfo* pei = NULL;
    hr = CreateErrorInfo(&pcreateei);
    if ( SUCCEEDED(hr) ) {
        pcreateei->SetSource( PROGID_Ichigo3 );
        pcreateei->SetDescription( UNICODE文字列 );
        hr = pcreateei->QueryInterface(
            IID_IErrorInfo, (LPVOID*)&pei );
        if ( SUCCEEDED(hr) ) {
            SetErrorInfo(0, pei);
            pei->Release();
        }
        pcreateei->Release();
    }
    return FAILED(hr)が真になるエラーコード;
    

    Invokeの例外処理

    Invoke()メソッド側では、COMの関数GetErrorInfo()を用いて例外情報を取得します。 処理の例は次のようになります。
    (メソッド呼び出しを実行)
    if ( FAILED(hr) ) {
        if ( pExcepInfo != NULL ) {
            pExcepInfo->scode = hr;
            pExcepInfo->wCode = 0;
            pExcepInfo->wReserved = 0;
            pExcepInfo->bstrSource = NULL;
            pExcepInfo->bstrDescription = NULL;
            pExcepInfo->bstrHelpFile = NULL;
            pExcepInfo->dwHelpContext = 0;
            pExcepInfo->pvReserved = NULL;
            pExcepInfo->pfnDeferredFillIn = NULL;
        }
        IErrorInfo* pei = NULL;
        if ( S_OK == GetErrorInfo(0, &pei) ) {
            if ( pExcepInfo != NULL ) {
                if ( FAILED(pei->GetSource( &pExcepInfo->bstrSource )) )
                    pExcepInfo->bstrSource = NULL;
                if ( FAILED(pei->GetDescription( &pExcepInfo->bstrDescription )) )
                    pExcepInfo->bstrDescription = NULL;
                if ( FAILED(pei->GetHelpFile( &pExcepInfo->bstrHelpFile )) )
                    pExcepInfo->bstrHelpFile = NULL;
                if ( FAILED(pei->GetHelpContext( &pExcepInfo->dwHelpContext )) )
                    pExcepInfo->dwHelpContext = 0;
            }
            pei->Release();
        }
        if ( pExcepInfo != NULL && pExcepInfo->bstrSource == NULL )
            pExcepInfo->bstrSource = SysAllocString( PROGID_Ichigo3 );
        return DISP_E_EXCEPTION;
    }
    return hr;
    
    bstrSourceは必ず設定する必要があるため、 メソッド側で設定されなかった場合はInvoke()側で設定しています。
    今回のようにInvoke()を直接実装する場合は、IErrorInfoを利用しなくてもEXCEPINFOに例外情報を返す方法はあるでしょう。 しかしCOMがInvoke()を実装するために提供する枠組みを利用する場合、 EXCEPINFOに例外の詳細情報を容易に返せるようにするために、 IErrorInfoを利用してメソッドを実装することが望ましいでしょう。

    関連ページ

  • COM/ActiveXの解説ページ 目次
  • 前の項目: IDispatchインターフェース
  • サンプルオートメーションサーバ