EternalWindows
オートメーション / 動的な実行

COMの設計目標の1つとして、どのような言語からでもオブジェクトを利用できるようにするというものがありますが、 これを実現することはそれほど容易ではありません。 理由は、オブジェクトを操作するためのインターフェースの定義を どの言語でも静的に宣言できるわけではないからです。 たとえば、スクリプト言語のコードは事前にコンパイルされるわけではありませんから、 実行時においてコードが正しいかを確認し、メソッド呼び出しの手続きを行うことになります。

話だけでは分かりにくいので、次に示すJScriptのコードを例に考えてみます。 実際にコードを実行する場合は、メモ帳などにコードを張り付けて拡張子.jsで保存し、それを開いてください。

var object = WScript.CreateObject("Word.Application");
object.Visible = true;

拡張子.jsのファイルを開くと、WSH(Windows Script Host)が起動されることになります。 このアプリケーションは、スクリプト内のコードを解析して実行することが目的であり、 たとえばCreateObjectという文字列を発見した場合は、 引数のProIDから関連するCLSIDを特定し、そのCLSIDで識別されるオブジェクトを作成します。 上記の例で言えばこのオブジェクトはWordに相当しますから、Wordが起動されることになります。 後は、オブジェクトを受け取った変数を基にオブジェクトのメソッドやプロパティを使用することになるのですが、 ここである1つの問題が生じます。 それは、オブジェクトが本当にそのメソッドやプロパティを実装しているのかという点です。 この確認には、IDispatchという特別なインターフェースが使用されることになっており、 もしオブジェクトがIDispatchを実装していれば、IDispatch::GetIDsOfNamesを呼び出すことによってメソッドの有無を確認することができます。 また、IDispatch::Invokeを呼び出せば、メソッドやプロパティを実際に使用することができます。 上記のコードでWordが起動されるのは、正にWordのオブジェクトがIDispatchを実装しているからであり、 それをWSHが使用してGetIDsOfNamesやInvokeを呼び出しているからなのです。 このように、IDispatchを通じてオブジェクトを操作できる仕組みのことをオートメーションと呼びます。

C++の視点からすれば、メソッドとプロパティはどちらも純粋仮想関数のように見えますが、その位置づけは互いに異なります。 プロパティはオブジェクトが内部的に維持している属性であり、 クライアントはこの属性に対して設定と取得を行うことができます。 たとえば、IDLファイルにVisibleというプロパティを記述してコンパイルした場合、 put_Visibleという関数とget_Visibleという関数がヘッダーファイルに定義され、 これらのどちらかがVisible指定時に呼び出されることになります。 一方、メソッドは何らかの命令を実行する関数です。

IDispatchを使用してメソッドやプロパティを呼び出すには、基本的に2つの手順を踏むことになります。 まず1つは、次に示すGetIDsOfNamesを呼び出してメソッドまたはプロパティのDISPIDを取得することです。 DISPIDとは、メソッドやプロパティを識別する番号のことです。

HRESULT IDispatch::GetIDsOfNames(  
  REFIID riid,                  
  LPOLESTR *rgszNames,  
  UINT cNames,          
  LCID lcid,                   
  DISPID *rgDispId          
);

riidは、将来のために予約されているためNULLを指定します。 rgszNamesは、DISPIDを取得したいメソッドやプロパティの名前を指定します。 cNamesは、rgszNamesの要素数を指定します。 lcidは、ローケルIDを指定します。 rgDispIdは、DISPIDを受け取る変数のアドレスを指定します。

DISPIDを取得したら、Invokeを呼び出すことでメソッドやプロパティを呼び出すことができます。

HRESULT IDispatch::Invoke(  
  DISPID dispIdMember,      
  REFIID riid,              
  LCID lcid,                
  WORD wFlags,              
  DISPPARAMS *pDispParams,  
  VARIANT *pVarResult,  
  EXCEPINFO *pExcepInfo,  
  UINT *puArgErr  
);

dispIdMemberは、呼び出したいメソッドまたはプロパティを表すDISPIDを指定します。 riidは、将来のために予約されているためNULLを指定します。 lcidは、ローケルIDを指定します。 wFlagsは、呼び出しの種類を表す定数を指定します。 メソッドを呼び出す場合はDISPATCH_METHOD、 プロパティを設定する場合はDISPATCH_PROPERTYPUT、 プロパティを取得する場合はDISPATCH_PROPERTYGETを指定します。 pDispParamsは、DISPPARAMS構造体のアドレスを指定します。 pVarResultは、戻り値を受け取る変数のアドレスを指定します。 wFlagsがDISPATCH_PROPERTYPUTの場合は考慮されません。 pExcepInfoは、発生した例外を受け取るEXCEPINFO構造体のアドレスを指定します。 puArgErrは、型が合わなかった引数のインデックスを受け取る変数のアドレスを指定します。 引数の型が型が合わなかった場合は、戻り値がDISP_E_TYPEMISMATCHになります。

DISPPARAMS構造体は、次のように定義されています。

typedef struct tagDISPPARAMS {
  VARIANTARG *rgvarg;
  DISPID *rgdispidNamedArgs;
  UINT cArgs;
  UINT cNamedArgs;
};

rgvargは、VARIANT構造体の配列を指定します。 この構造体は、メソッドまたはプロパティの引数を格納している必要があります。 rgdispidNamedArgsは、DISPIDの配列を指定します。 プロパティを設定する場合は適切な配列を指定します。 cArgsは、rgvargの要素数を指定します。 cNamedArgsは、rgdispidNamedArgsの要素数を指定します。

DISPPARAMS構造体が引数をVARIANT構造体で受け取るのは、 どのような型の引数でも統一的に扱いたいからです。 たとえば、引数をVARIANT構造体ではなくLONG型にすると、 LONG型を要求するメソッドやプロパティしか呼び出せなくなり問題といえます。 VARIANT構造体は、LONG型やBOOL型など多数の型を共用体として扱うため、 どのような型のデータもこの構造体に格納することができます。 実際にどのようなデータの型が格納されているかを確認するには、 VARTYPE型のvtメンバを調べることになります。 このメンバには、VARTYPEと呼ばれる定数のいずれかが格納されます。

VARTYPE 使用するメンバ 意味
VT_EMPTYなしデータなし。VariantInitがこれを設定する。
VT_I2iValSHORT型
VT_I4lValLONG型
VT_R4fltValFLOAT型
VT_R8dblValDOUBLE型
VT_CYcyVal通過型
VT_DATEdate日付型
VT_BSTRbstrValBSTR型(文字列)
VT_DISPATCHpdispValIDispatch型
VT_ERRORscodeエラーコード
VT_BOOLboolValBOOL型
VT_ARRAYparraySAFEARRAY型

たとえば、アプリケーションがBSTR型を要求するメソッドを呼び出す場合は、 次のようにVARIANT構造体を初期化します。

VARIANT var;
BSTR    bstr = SysAllocString(L"data");

var.vt = VT_BSTR;
var.bstrVal = bstr;

BSTR型を指定したい場合は、vtメンバにVT_BSTRを指定します。 これによりbstrValメンバが考慮されるようになるため、このメンバにBSTR型の変数を指定することになります。 SysAllocStringは、LPWSTR型をBSTR型に変換する関数です。 BSTR型はLPWSTR型と同じようにUNICODE文字列を指すことができますが、 文字列の先頭4バイトには文字のサイズが格納されるという特徴があります。 ただし、SysAllocStringが返すアドレスはこの4バイトをスキップし、 実際の文字列が格納されている先頭を指しているため、 事実上LPWSTR型と同じように扱うことができます。 ちなみに、BSTRはBasic Stringの略であり、Visual Basicで使用される文字列と互換性があります。

さて、IDispatchを使用するためにはこれを実装するサーバーを見つけなければなりません。 WordやExcelといったOfficeアプリケーションはIDispatchを実装しているため、 今回はWordを例にIDispatchの使い方を確認することにします。 ちなみに、OfficeアプリケーションにおけるIDispatchは、 スクリプト言語だけでなくC/C++アプリケーションにとっても非常に有用なものです。 なぜなら、Officeアプリケーションを操作するためのインターフェースや各種定義がWindows SDKに含まれていないからです。 つまり、IWordApplicationのような直感的なインターフェースが用意されているわけではないので、 IDispatchを使用して動的にメソッドを呼び出す以外に方法がないのです。 各種定義がなければメソッド名も分からないようにも思えますが、これについては次節で説明します。

今回のプログラムは、前述の通りWordを起動して表示します。

#include <windows.h>

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IDispatch *pApplication;
	CLSID     clsid;
	HRESULT   hr;
	VARIANT   var;

	CoInitialize(NULL);
	
	hr = CLSIDFromProgID(L"Word.Application", &clsid);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = CoCreateInstance(clsid, NULL, CLSCTX_LOCAL_SERVER, IID_PPV_ARGS(&pApplication));
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	var.vt = VT_I4;
	var.lVal = 1;
	Invoke(pApplication, L"Visible", DISPATCH_PROPERTYPUT, &var, 1, NULL);

	pApplication->Release();
	CoUninitialize();
	
	return 0;
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}

まず、WordのProgIDをCLSIDに変換し、これをCoCreateInstanceに指定します。 WordはEXEファイルとして存在するため、 アウトプロセスサーバーとして起動するべくCLSCTX_LOCAL_SERVERを指定します。 WordはIDispatchで識別することになるため、 pApplicationの型はIDispatchでなければなりません。

アウトプロセスサーバーを起動した時点ではウインドウが表示されないため、 まずはこれを表示状態にする必要があります。 このためのプロパティとして、WordにはVisibleが用意されているため、 これをIDispatch::Invokeで実行する必要があります。 Invokeという自作関数は、このIDispatch::InvokeとIDispatch::GetIDsOfNamesをラッピングしており、 第1引数にIDispatchへのポインタ、第2引数に呼び出したいメソッドまたはプロパティの名前、 第3引数に呼び出しの種類を指定します。 今回はプロパティを設定するため、DISPATCH_PROPERTYPUTを指定しています。 第4引数は引数の配列で、第5引数は引数の数です。 Visibleは数値型の引数を1つ要求するため、1つのVARIANT構造体を宣言し、 vtにVT_I4を指定してlValに数値を指定します。 0でない値を指定すればウインドウが表示されることになります。 最後の引数は、メソッドまたはプロパティの戻り値を受け取る変数のアドレスを指定します。 プロパティを設定する場合は戻り値がありませんからNULLを指定しています。 Invokeの実装は次のようになっています。

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;
	
	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}

まず、GetIDsOfNamesを呼び出してメソッドまたはプロパティのDISPIDを取得します。 次に、渡された引数の数と引数をDISPPARAMS構造体のcArgsとrgvargに指定します。 wFlagsにDISPATCH_PROPERTYPUTが含まれている場合は、 cNamedArgsにrgdispidNamedArgsの要素数を指定し、 rgdispidNamedArgsにはDISPIDの配列を指定します。 通常は、DISPID_PROPERTYPUTを格納した変数のアドレスを指定するだけで構いません。 wFlagsにDISPATCH_PROPERTYPUTが含まれていない場合は、 cNamedArgsに0を指定し、rgdispidNamedArgsにNULLを指定します。 DISPIDの取得とDISPPARAMS構造体の初期化が終われば、IDispatch::Invokeを呼び出してメソッドまたはプロパティを実行することができます。 例外情報とエラー情報は不要ということで、第7引数と第8引数はNULLを指定しています。

IDispatchを実装するサーバーは、自身の機能を必要とする言語がスクリプトに限らないことを意識するべきです。 C/C++アプリケーションにとってIDispatch::Invokeを実行するまでの手順は非常に複雑であり、 できることならpApplication->put_Visible(1)のような分かりやすい呼び出しをしたいからです。 こうした要望に応えるためには、サーバーはIDispatchの他に通常のカスタムインターフェースを実装することになるでしょう。 このようにすれば、C/C++アプリケーションはCoCreateInstanceの呼び出し時にそのインターフェースのIIDを指定しますから、 その取得したインターフェースのメソッドをvtblから直接呼び出すことができます。 専門的な用語では、IDispatchによる呼び出しはレイトバインディングと呼ばれ、 vtblからの呼び出しはアーリーバインディングと呼ばれています。 また、両方のバインディングをサポートしているオブジェクトは、 デュアルインターフェースを持っていると呼ばれたりします。

登録されたオブジェクトを取得

アプリケーションが操作したいオブジェクトというのは、 何もこれから実行するオブジェクトであるとは限りません。 既にWordが実行されているのであれば、 そのWordが登録したオブジェクトを操作したいこともあるでしょう。 このように、既に登録されたオブジェクトを取得する例を次に示します。

#include <windows.h>

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult);

int WINAPI WinMain(HINSTANCE hinst, HINSTANCE hinstPrev, LPSTR lpszCmdLine, int nCmdShow)
{
	IUnknown  *pUnknown;
	IDispatch *pApplication;
	CLSID     clsid;
	HRESULT   hr;

	CoInitialize(NULL);
	
	hr = CLSIDFromProgID(L"Word.Application", &clsid);
	if (FAILED(hr)) {
		CoUninitialize();
		return 0;
	}
	
	hr = GetActiveObject(clsid, NULL, &pUnknown);
	if (FAILED(hr)) {
		MessageBox(NULL, TEXT("オブジェクトが登録されていません。"), NULL, MB_ICONWARNING);
		CoUninitialize();
		return 0;
	}
	
	pUnknown->QueryInterface(IID_PPV_ARGS(&pApplication));
	Invoke(pApplication, L"Quit", DISPATCH_METHOD, NULL, 0, NULL);

	pUnknown->Release();
	pApplication->Release();
	CoUninitialize();
	
	return 0;
}

HRESULT Invoke(IDispatch *pDispatch, LPOLESTR lpszName, WORD wFlags, VARIANT *pVarArray, int nArgs, VARIANT *pVarResult)
{
	DISPPARAMS dispParams;
	DISPID     dispid;
	DISPID     dispidName = DISPID_PROPERTYPUT;
	HRESULT    hr;

	hr = pDispatch->GetIDsOfNames(IID_NULL, &lpszName, 1, LOCALE_USER_DEFAULT, &dispid);
	if (FAILED(hr))
		return hr;
	
	dispParams.cArgs = nArgs;
	dispParams.rgvarg = pVarArray;
	if (wFlags & DISPATCH_PROPERTYPUT) {
		dispParams.cNamedArgs = 1;
		dispParams.rgdispidNamedArgs = &dispidName;
	}
	else {
		dispParams.cNamedArgs = 0;
		dispParams.rgdispidNamedArgs = NULL;
	}

	hr = pDispatch->Invoke(dispid, IID_NULL, LOCALE_SYSTEM_DEFAULT, wFlags, &dispParams, pVarResult, NULL, NULL);

	return hr;
}

GetActiveObjectという関数は、第1引数に指定されたCLSIDを持つオブジェクトが実行されているかを調べ、 もし実行されている場合は、そのオブジェクトが登録しているオブジェクトのポインタを第3引数に返します。 WordなどのOfficeアプリケーションの場合は、これはApplicationオブジェクト(次節で説明)になりますから、 QueryInterfaceでIDispatchを取得し、動的にプロパティやメソッドを呼び出すことができます。 上記ではQuitメソッドを呼び出していますから、現在起動しているWordを終了させることになります。



戻る