EternalWindows
COMサーバー / デュアルインターフェース

オブジェクトが実装しているインターフェースの定義は、オブジェクトへアクセスするために本来ならば必要なものです。 たとえば、前節のオブジェクトはIFileControlを実装していましたから、 このオブジェクトに対してアクセスするには、IFileControlの定義が必要であるのは当然のことにように思えます。 しかし、実際にはこうしたインターフェースを定義できなくても、オブジェクトへアクセスするための方法は存在します。 実行したいメソッドを直接呼び出すという方法から、実行したいメソッドの名前を渡すという方法に切り替えれば、 その名前を渡すためのインターフェースが存在するだけで、あらゆるメソッドを間接的に呼び出せるようになるからです。 この役割を果たすインターフェースはディスパッチインターフェース(IDispatch)と呼ばれ、 オブジェクトがIDispatchを実装するようになれば、 オブジェクトのメソッドを動的に呼び出すことが可能になります。

IDispatchの機能が最も役に立つのは、インターフェースを定義できないスクリプト言語などからのアクセスをサポートする場合です。 例として、JScriptで記述された次のコードを考えます。

var object = WScript.CreateObject("Sample.MyServer2");
object.CreateFile("C:\\sample.txt", 0);
var str = object.ReadFile(3);
WScript.Echo(str);
object.CloseFile();

.jsファイルを開くとWSH(wscript.exe)が起動され、ファイルの中身が解析されます。 CreateObjectを発見すると、第1引数のProgIdからオブジェクトのCLSIDが求められ、 そのオブジェクトが作成されます。 今回のオブジェクトはインプロセスサーバーであるため、 オブジェクトのDLLがwscript.exeにアドレス空間にロードされます。 CreateFileなどの呼び出しに差し掛かった場合は、 オブジェクトからIDispatchが照会され、 IDispatch::GetIDsOfNamesによってメソッドの名前がオブジェクトに渡されます。 このとき、呼び出し側(WSH)はDISPIDと呼ばれるメソッドのIDを受け取り、 これをIDispatch::Invokeに指定することでメソッドを実行しようとします。

上記のようなオートメーション(IDispatchによるアクセス)をサポートする場合は、 オブジェクトのクラスがIDispatchを継承するようになっていなければなりません。 次に、IDispatchを継承したCMyServerを示します。

class CMyServer : public IDispatch
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();
	
	STDMETHODIMP GetTypeInfoCount(UINT *pctinfo);
	STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
	STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
	STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);

	CMyServer();
	~CMyServer();

private:
	STDMETHODIMP CreateFile(BSTR bstrFileName, FILEMODE filemode);
	STDMETHODIMP ReadFile(DWORD dwLength, BSTR *lp);
	STDMETHODIMP WriteFile(BSTR bstrData, DWORD dwLength);
	STDMETHODIMP CloseFile();
	STDMETHODIMP FilePos(DWORD dwPos);

private:
	LONG   m_cRef;
	HANDLE m_hFile;
};

GetTypeInfoCountからInvokeまでがIDispatchのメソッドになります。 CreateFileなどのメソッドは、クライアントから直接呼ばれることはないため、privateとして定義されています。 IDispatchでは文字列をBSTR型で表すことになっているため、 CreateFileなどの引数もBSTR型になっています。 IDispatchのメソッドの実装は次のようになります。

STDMETHODIMP CMyServer::GetTypeInfoCount(UINT *pctinfo)
{
	*pctinfo = 0;
	
	return S_OK;
}

STDMETHODIMP CMyServer::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo)
{
	return E_NOTIMPL;
}

STDMETHODIMP CMyServer::GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId)
{
	if (lstrcmpW(*rgszNames, L"CreateFile") == 0)
		*rgDispId = 1;
	else if (lstrcmpW(*rgszNames, L"ReadFile") == 0){
		*rgDispId = 2;}
	else if (lstrcmpW(*rgszNames, L"WriteFile") == 0)
		*rgDispId = 3;
	else if (lstrcmpW(*rgszNames, L"CloseFile") == 0)
		*rgDispId = 4;
	else if (lstrcmpW(*rgszNames, L"FilePos") == 0)
		*rgDispId = 5;
	else
		return DISP_E_UNKNOWNNAME;

	return S_OK;
}

STDMETHODIMP CMyServer::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr)
{
	if (dispIdMember == 1)
		return CreateFile(pDispParams->rgvarg[1].bstrVal, (FILEMODE)pDispParams->rgvarg[0].lVal);
	else if (dispIdMember == 2) {
		HRESULT hr;
		BSTR bstr;
		hr = ReadFile(pDispParams->rgvarg[0].lVal, &bstr);
		pVarResult->vt = VT_BSTR;
		pVarResult->bstrVal = bstr;
		return hr;
	}
	else if (dispIdMember == 3)
		return WriteFile(pDispParams->rgvarg[1].bstrVal, pDispParams->rgvarg[0].lVal);
	else if (dispIdMember == 4)
		return CloseFile();
	else if (dispIdMember == 5)
		return FilePos(pDispParams->rgvarg[0].lVal);
	else
		return E_FAIL;

	return S_OK;
}

GetTypeInfoCountとGetTypeInfoは、上記のような実装で問題ありません。 GetIDsOfNamesはメソッドに対応するDISPIDを返し、 InvokeではDISPIDに対応するメソッドを呼び出すようにします。 メソッドの引数はpDispParams->rgvargに格納されていますが、 後ろの引数から順に格納されている点に注意してください。 ReadFileは文字列を返すことになっていますが、 このような戻り値はpVarResultに格納するようにします。 なお、タイプライブラリを用意する場合は上記の実装が単純になりますが、 それについては次節で説明します。

オブジェクトがIDispatchを実装したことにより、メソッドを動的に呼び出せるようになりましたが、 そのためにカスタムインターフェースが使用できなくなるのは面白くありません。 できればオブジェクトには、カスタムインターフェースでもディスパッチインターフェースでもアクセスできるようにしたいため、 このような両方のインターフェース(デュアルインターフェース)を持たせる方法を考えたいものです。 実はこれは、IDLファイルを次のように変更することで容易に可能です。

[
	object,
	dual,
	uuid(A09AFC29-31EA-462a-9DF0-40A7D2D16E76)
]
interface IFileControl : IDispatch
{
	typedef [v1_enum] enum tagFILEMODE {
		FM_READ = 0,
		FM_WRITE = 1
	} FILEMODE;

	[id(1)] HRESULT CreateFile([in] BSTR bstrFileName, [in] FILEMODE filemode); 
	[id(2)] HRESULT ReadFile([in] DWORD dwLength, [out, retval] BSTR *lp);
	[id(3)] HRESULT WriteFile([in] BSTR bstrData, [in] DWORD dwLength);
	[id(4)] HRESULT CloseFile();
	[id(5), propput] HRESULT FilePos([in] DWORD dwPos);
}

ヘッダにdualを指定し、intarfaceの部分でIDispatchを継承するようにします。 これで、デュアルインターフェースを定義したことになります。 メソッドの横に指定するIDはメソッドのDISPIDであり、IDispatch::Invokeで使用される値と矛盾があってはなりません。 メソッドの中でも、何らかの値の設定や取得を目的としたものはプロパティと呼ばれることがあります。 FilePosは、ファイルポインタの位置を設定することを目的としているため、 propputを指定することでプロパティとして扱うようにしています。 逆に、値を取得する場合はpropgetを指定することになります。 ReadFileの最後の引数にはoutを指定しているため、 呼び出し側は引数を通じて文字列を取得することになりますが、 このような引数を通じたデータの取得というのは、スクリプト言語ではサポートされていません。 こうしたケースに対応するには、メソッドの戻り値を通じてデータを取得できるようretvalを指定します。 retvalを指定する引数は必ず最後の引数でなければならないことに注意してください。 後は、IFileControlをクラスが継承すればよいだけです。

class CMyServer : public IFileControl
{
public:
	STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject);
	STDMETHODIMP_(ULONG) AddRef();
	STDMETHODIMP_(ULONG) Release();

	STDMETHODIMP GetTypeInfoCount(UINT *pctinfo);
	STDMETHODIMP GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo **ppTInfo);
	STDMETHODIMP GetIDsOfNames(REFIID riid, LPOLESTR *rgszNames, UINT cNames, LCID lcid, DISPID *rgDispId);
	STDMETHODIMP Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS *pDispParams, VARIANT *pVarResult, EXCEPINFO *pExcepInfo, UINT *puArgErr);

	STDMETHODIMP CreateFile(BSTR bstrFileName, FILEMODE filemode);
	STDMETHODIMP ReadFile(DWORD dwLength, BSTR *lp);
	STDMETHODIMP WriteFile(BSTR bstrData, DWORD dwLength);
	STDMETHODIMP CloseFile();
	STDMETHODIMP put_FilePos(DWORD dwPos);
	
	CMyServer();
	~CMyServer();
	
private:
	LONG   m_cRef;
	HANDLE m_hFile;
};

IFileControlはIDispatchを継承していましたから、クラスはIDispatchのメソッドをオーバーライドすることになります。 当然ながらIFileControlのメソッドもオーバーライドすることになりますが、 FilePosというプロパティはput_FilePosという名前に変更されています。 これは、MIDLがpropgetを持つプロパティの名前にget_を付加し、 propputを持つプロパティの名前にput_を付加するようになっているからです。 こうした文字列が付加されることにより、名前からプロパティとメソッドの区別がつきやすくなります。


戻る