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をキャッシュする仕組みもないし初期化も怪しいが、とりあえず機能するし叩き台にはなるかな。
ActiveScriptSiteに独自のオブジェクトを公開する。
上の続き。
ActiveScriptHostでスクリプトにオブジェクトを公開するには、ActiveScriptのAddNamedItemメソッドを呼び出して名前を予約する。これは、SetSiteを呼び出したあとに行う必要がある。
なお、名前を登録するだけでオブジェクトは、この時点では必要ない。
...(前略)... // スクリプトエンジンにSiteを設定 hr = pScript_->SetScriptSite(pSite); ATLASSERT(SUCCEEDED(hr)); // ルートコンテキスト上に「util」という名前のオブジェクトを登録する。 hr = pScript_->AddNamedItem(L"util", SCRIPTITEM_ISPERSISTENT | SCRIPTITEM_ISVISIBLE); ATLASSERT(SUCCEEDED(hr)); ParseScript(); ...(後略)...
ここで次のようなスクリプトを呼び出すとすると、
Call util.SayHello
ActiveScriptSiteのGetItemInfoが呼び出されるため、ここで「util」という名前であるかをチェックして、該当するのであればスクリプトに渡したいIDispatchを実装したオブジェクトを返却すればよい。(IUnknownを受け取るようになっているが。)
名前が該当しない場合は、TYPE_E_ELEMENTNOTFOUNDを返す。(しかし、このメソッドはAddNamedItemで登録された名前のみが呼び出されることに留意されたし。)
ITypeInfoはスクリプト側で利用するシチュエーションがないと思われるので常にNULLを返してよいと思われる。(返すと、なにかいいことがあるのか?)
...(前略)... virtual HRESULT STDMETHODCALLTYPE GetItemInfo( /* [in] */ LPCOLESTR pstrName, /* [in] */ DWORD dwReturnMask, /* [out] */ IUnknown **ppiunkItem, /* [out] */ ITypeInfo **ppti) throw() { _tprintf(_TEXT("GetItemInfo\n")); HRESULT hr = TYPE_E_ELEMENTNOTFOUND; static CComBSTR name(L"util"); if (name == pstrName) { hr = S_OK; if (dwReturnMask & SCRIPTINFO_IUNKNOWN) { hr = pSayHello_.QueryInterface(ppiunkItem); } if (SUCCEEDED(hr) && (dwReturnMask & SCRIPTINFO_ITYPEINFO)) { *ppti = NULL; } } return hr; } ...(後略)...
以上、おわり。