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_UI1 | BYTE | bVal |
VT_I1 | CHAR | cVal |
VT_UI2 | USHORT | uiVal |
VT_I2 | SHORT | iVal |
VT_UI4 | ULONG | ulVal |
VT_I4 | LONG | lVal |
VT_UINT | UINT | uintVal |
VT_INT | INT | intVal |
VT_R4 | FLOAT | fltVal |
VT_R8 | DOUBLE | dblVal |
VT_CY | CY | cyVal |
VT_DATE | DATE | date |
VT_BSTR | BSTR | bstrVal |
VT_BOOL | VARIANT_BOOL | boolVal (0xffff=true, 0x0000=false) |
VT_UNKNOWN | IUnknown* | punkVal |
VT_DISPATCH | IDispatch* | pdispVal |
VT_ERROR | SCODE | scode |
VT_DECIMAL | DECIMAL | decVal |
VT_BYREF | VT_UI1 | BYTE* | pbVal |
VT_BYREF | VT_I1 | CHAR* | pcVal |
VT_BYREF | VT_UI2 | USHORT* | puiVal |
VT_BYREF | VT_I2 | SHORT* | piVal |
VT_BYREF | VT_UI4 | ULONG* | pulVal |
VT_BYREF | VT_I4 | LONG* | plVal |
VT_BYREF | VT_UINT | UINT* | puintVal |
VT_BYREF | VT_INT | INT* | pintVal |
VT_BYREF | VT_R4 | FLOAT* | pfltVal |
VT_BYREF | VT_R8 | DOUBLE* | pdblVal |
VT_BYREF | VT_CY | CY* | pcyVal |
VT_BYREF | VT_DATE | DATE* | pdate |
VT_BYREF | VT_BSTR | BSTR* | pbstrVal |
VT_BYREF | VT_BOOL | VARIANT_BOOL* | pboolVal |
VT_BYREF | VT_UNKNOWN | IUnknown** | ppunkVal |
VT_BYREF | VT_DISPATCH | IDispatch** | ppdispVal |
VT_BYREF | VT_ERROR | SCODE* | pscode |
VT_BYREF | VT_DECIMAL | DECIMAL* | 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* rgszNames | IDを取得したい文字列へのポインタの配列 |
UINT cNames | IDを取得したい文字列の数 |
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インターフェース
サンプルオートメーションサーバ