seraphyの日記

日記というよりは過去を振り返るときのための単なる備忘録

ActiveScriptHostのスクリプトに渡すIDispatchの作り方(レジストリなし)

ActiveScriptHostのスクリプトに渡すIDispatchの作り方のメモ。

スクリプトはオートメーション型でのみ動作するので、IDispatchを実装したオブジェクトを渡す必要がある。
DUALインタフェースを手作業で作るのは煩雑すぎるが、ATLのウィザードを使うと余計なものまで作られてかえって面倒である。
ここでは、IDLを使ってDUALインタフェースを定義し、あとは手作業で実装する。

// ファイル名: AXScriptTest1.idl
import "oaidl.idl";
import "ocidl.idl";

[
	object,
	uuid(E3CD01CE-DA29-41ce-8C92-E65DDED662A7),
	dual,
	pointer_default(unique)
]
interface ISayHello : IDispatch
{
	[id(1)] HRESULT SayHello();
};

[
	uuid(3E46DC0A-DC9E-433c-89AB-9E271E1DBA0A),
]
library AXScriptTest1
{
	importlib("stdole32.tlb");
	interface ISayHello;
};

このIDLには、COMクラス(coclass)を定義する必要はない。
なお、インタフェースのIID(UUID)は意味があるが、LIBID(UUID)はレジストリに登録するわけではないので意味がない。
このIDLをコンパイルすると、*_h.hと、*_c.c、*_p.cファイルが作成されるが、使うのは*_h.hだけである。

このデュアルインタフェースを実装するクラスを定義する。

#include "AXScriptTest1_h.h"

class __declspec(uuid("{B57645F0-9C30-4935-8348-DA52AF84D341}")) CSayHello
	: public CComObjectRoot
	, public CComCoClass<CSayHello, &__uuidof(CSayHello)>
	, public IDispatchImpl<ISayHello, &__uuidof(ISayHello), &CAtlModule::m_libid, -1, -1>
{
public:

	BEGIN_COM_MAP(CSayHello)
		COM_INTERFACE_ENTRY(ISayHello)
		COM_INTERFACE_ENTRY(IDispatch)
	END_COM_MAP( )

	DECLARE_NO_REGISTRY()
	DECLARE_CLASSFACTORY()
	DECLARE_PROTECT_FINAL_CONSTRUCT()

	virtual HRESULT __stdcall SayHello(void) throw()
	{
		_tprintf(_TEXT("Hello, World!\n"));
		return S_OK;
	}

};

IDispatchImplの標準のCComTypeInfoHolderは、バージョンが(0xffff, 0xffff)で、且つ、LIBIDが、自身のモジュールのLIBIDと同じ場合に、自分自身のリソースからタイプライブラリをロードする。
ここがポイント。
(そんなこと、ドキュメントには書いてないが、そうゆう実装になっている。CComTypeInfoHolderクラスを使うことはIDispatchImplの説明に書かれているが、そのCComTypeInfoHolderそのものについての説明がない。)

自身のLIBIDと等しい、とは、つまり、CAtl???ModuleTの中で宣言するDECLARE_LIBID(xxxx)と等しければよい、ということである。

IDLをコンパイルした結果、*_c.hの中にはIDLのlibraryで指定したLIBID_AXScriptTest1ができているので、これをプロジェクトに加えて使ってもよいし、
この場合、公開するわけでもなし自分以外に使う予定がないのでデタラメでも機能する。(下の例は*_c.cを読み込んでいないけど、一応、UUIDはそろえてある。)

class __declspec(uuid("{3E46DC0A-DC9E-433c-89AB-9E271E1DBA0A}")) MyAXScriptHostTest1
	: public CAtlExeModuleT<MyAXScriptHostTest1>
{
public:
	DECLARE_LIBID(__uuidof(MyAXScriptHostTest1))
...
};

忘れてはならないのは、MIDLによって作成された*.tlbをリソースに含めること。
リソースエディタで*.tlbをインポートし、リソースタイプに「TYPELIB」と指定する。(IDR_xxxxという名前は不要なので1とかにしておく。)
リソースをテキストエディタで開いて編集したほうが早いかもしれない。

CComTypeInfoHolderでは最初のタイプライブラリを読み込む。(Win32APIのLoadTypeLibは、dll/exeを指定した場合、末尾に「\〜」のように指定しないかぎり、最初のタイプライブラリを読み込むのであるが、CComTypeInfoHolderはモジュール名しか指定していないためである。)

/////////////////////////////////////////////////////////////////////////////
//
// TYPELIB
//

1            TYPELIB                 "AXScriptTest1.tlb"

あとは、普通にオブジェクトを構築すればよい。
これでActiveScriptHostのスクリプトエンジンの中でのみ有効なオブジェクトとして公開できる。

CComPtr<IDispatch> pSayHello;
HRESULT hr = CSayHello::CreateInstance(&pSayHello);

以上、おわり。

おまけ

モジュールのLIBIDを偽装したり、IDispatchImplのアンドキュメントな(しかしソース公開されている)部分が気持ち悪いので、ためしに自前のIDispatchImplを作ってみた。

template<class T> class IDispatchImpl2 : public T
{
public:

	virtual HRESULT __stdcall GetTypeInfoCount(unsigned int FAR* pctinfo) throw()
	{
		if ( !pctinfo) {
			return E_POINTER;
		}

		HRESULT hr = EnsureTI();

		*pctinfo = SUCCEEDED(hr) ? 1 : 0;

		return S_OK;
	}

	virtual HRESULT __stdcall GetTypeInfo(unsigned int iTInfo, LCID lcid,
		ITypeInfo FAR* FAR* ppTInfo) throw()
	{
		HRESULT hr = EnsureTI();
		if (SUCCEEDED(hr)) {
			return pTypeInfo_.CopyTo(ppTInfo);
		}
		return TYPE_E_ELEMENTNOTFOUND;
	}

	virtual HRESULT __stdcall GetIDsOfNames(REFIID riid, OLECHAR FAR* FAR* rgszNames,
		unsigned int cNames, LCID lcid, DISPID FAR* rgDispId) throw()
	{
		HRESULT hr = EnsureTI();
		if (SUCCEEDED(hr)) {
			hr = pTypeInfo_->GetIDsOfNames(rgszNames, cNames, rgDispId);
		}
		return hr;
	}

	virtual HRESULT __stdcall Invoke(DISPID dispIdMember, REFIID riid, LCID lcid,
		WORD wFlags, DISPPARAMS FAR* pDispParams, VARIANT FAR* pVarResult,
		EXCEPINFO FAR* pExcepInfo, unsigned int FAR* puArgErr) throw()
	{
		HRESULT hr = EnsureTI();
		if (SUCCEEDED(hr)) {
			hr = pTypeInfo_->Invoke(this, dispIdMember, wFlags,
				pDispParams, pVarResult, pExcepInfo, puArgErr);
		}
		return hr;
	}

	static HRESULT EnsureTI() throw()
	{
		if (pTypeInfo_) {
			return S_OK;
		}
		HRESULT hr = E_FAIL;
		TCHAR szFilePath[MAX_PATH];
		DWORD dwFLen = ::GetModuleFileName(
			_AtlBaseModule.GetModuleInstance(), szFilePath, MAX_PATH);
		if( dwFLen != 0 && dwFLen != MAX_PATH ) {
			CComPtr<ITypeLib> pTypeLib;
			hr = LoadTypeLib(CT2W(szFilePath), &pTypeLib);
			ATLASSERT(SUCCEEDED(hr));
			if (SUCCEEDED(hr)) {
				hr = pTypeLib->GetTypeInfoOfGuid(
					__uuidof(T), &pTypeInfo_);
				ATLASSERT(SUCCEEDED(hr));
			}
		}
		return hr;
	}

	static CComPtr<ITypeInfo> pTypeInfo_;
};

template <class T>
CComPtr<ITypeInfo> IDispatchImpl2<T>::pTypeInfo_;

これは、以下のように使う。

class __declspec(uuid("{B57645F0-9C30-4935-8348-DA52AF84D341}")) CSayHello
	: public CComObjectRoot
	, public CComCoClass<CSayHello, &__uuidof(CSayHello)>
	, public IDispatchImpl2<ISayHello>
{
public:

	BEGIN_COM_MAP(CSayHello)
		COM_INTERFACE_ENTRY(ISayHello)
		COM_INTERFACE_ENTRY(IDispatch)
	END_COM_MAP( )

	DECLARE_NO_REGISTRY()
	DECLARE_CLASSFACTORY()
	DECLARE_PROTECT_FINAL_CONSTRUCT()

	static void __stdcall ObjectMain(bool starting) throw()
	{
		if (starting) {
			EnsureTI();
		}
	}

	virtual HRESULT __stdcall SayHello(void) throw()
	{
		_tprintf(_TEXT("Hello, World!\n"));
		return S_OK;
	}

};

OBJECT_ENTRY_NON_CREATEABLE_EX_AUTO(__uuidof(CSayHello), CSayHello);

初回、IDispatchのメソッドを使うときに、強制的に、自身のモジュールからタイプライブラリをロードしようと試みる。
あとは、ITypeInfoにお任せである。
DISPIDをキャッシュする仕組みもないし初期化も怪しいが、とりあえず機能するし叩き台にはなるかな。