I want to expose methods of CUIAutomation
COM class objects to scripts I load and run through my Active / Windows Script application (I am not implementing a script engine, I am using one, specifically the "JScript" engine). The script host is normally able to expose any IDispatch
-implementing object automatically, but CUIAutomation
class does not implement IDispatch
. Calls to QueryInterface
for an IDispatch
pointer on the object return E_NOINTERFACE
.
My entire question, on which I elaborate below, basically boils down to this: is it possible to implement dispatching for an object that doesn't implement IDispatch
? I bet having type information for the coclass of the object would be a necessary (and possibly sufficient) requirement, if it were possible. If it is possible, what is wrong with my attempt to do so as explained below? What are my alternatives?
As mentioned, my solution centers around my hypothesis that if I should have the type information (ITypeInfo
) for CUIAutomation
coclass, then I should theoretically be able to do runtime dispatching on objects of said coclass, even without it implementing IDispatch
but just through methods of ITypeInfo
like GetIDsOfNames
and Invoke
. Practically, I'd design a class of my own that does implement IDispatch
, wraps a CUIAutomation
object (or any IUnknown
for that matter that I can pair with proper type information) and delegates member dispatch to the wrapped object.
I have been successful in loading type information for at least the CUIAutomation
coclass -- it's all in the Windows Registry -- by locating the path to the module that implements it and using the LoadTypeLib
procedure:
(Note: I have assertions that check if calls succeed (by comparing to S_OK
or ERROR_SUCCESS
etc -- depends on what's code for success), but I omit said error checking in the snippets, for brevity -- if a call isn't checked for return value there is invariably an assertion in place around it, as described)
/// Return zero if and only if successful
int LoadTypeInfo(LPOLESTR szCLSID, ITypeInfo * * ppTypeInfo) {
HKEY hRegKeyCLSIDs;
RegOpenKeyEx(HKEY_CLASSES_ROOT, "CLSID", 0, KEY_READ, &hRegKeyCLSIDs); /// Only need to do this once through application lifetime, but here for context
HKEY hRegKeyCLSID;
RegOpenKeyEx(hRegKeyCLSIDs, szCLSID , 0, KEY_READ, &hRegKeyCLSID);
BYTE data[MAX_PATH];
DWORD cbData = sizeof(data);
RegGetValueW(hRegKeyCLSID, L"InprocServer32", NULL, RRF_RT_REG_SZ, NULL, data, &cbData);
ITypeLib * pTypeLib;
LoadTypeLib((LPOLESTR)data, &pTypeLib);
return (pTypeLib->GetTypeInfoOfGuid(CLSID, ppTypeInfo) == S_OK);
}
The delegating DispatchProxy
class is designed as follows:
class DispatchProxy: public IDispatch {
private:
IUnknown * pUnknown;
ITypeInfo * pTypeInfo;
public:
DispatchProxy(IUnknown * pUnknown, ITypeInfo * pTypeInfo): pUnknown(pUnknown), pTypeInfo(pTypeInfo) {
/// `pUnknown` is the object that doesn't implement `IDispatch` and `pTypeInfo` is the type information for objects like what `pUnknown` points to.
}
/// Omitting `AddRef` and `Release` -- these are rather standard.
HRESULT STDMETHODCALLTYPE DispatchProxy::QueryInterface(REFIID riid, void * * ppvObject) {
if(ppvObject == nullptr) {
return E_POINTER;
}
else
if(riid == IID_IUnknown || riid == IID_IDispatch) {
*ppvObject = this;
((IUnknown *)*ppvObject)->AddRef();
return S_OK;
}
else {
*ppvObject = NULL;
return E_NOINTERFACE;
}
}
/// NOT returning any type information -- explanation below, if you're surprised
HRESULT STDMETHODCALLTYPE DispatchProxy::GetTypeInfoCount(UINT * pctinfo) {
*pctinfo = 0;
return S_OK;
}
HRESULT STDMETHODCALLTYPE DispatchProxy::GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo ** ppTInfo) {
if(iTInfo != 0) return DISP_E_BADINDEX;
_ASSERTE(*ppTInfo == NULL);
return E_NOTIMPL; /// Even though type information for the object being delegated to, is available, obviously, I am unsure whether it technically is valid for `DispatchProxy`, which may have a completely different, incompatible, layout. Granted, `E_NOTIMPL` isn't part of the contract for this method, but like I said -- I am unsure about this one.
}
HRESULT STDMETHODCALLTYPE DispatchProxy::GetIDsOfNames(REFIID riid, LPOLESTR * rgszNames, UINT cNames, LCID lcid, DISPID * rgDispId) {
return pTypeInfo->GetIDsOfNames(rgszNames, cNames, rgDispId); /// Returns S_OK, all good. Also tried `DispGetIDsOfNames(pTypeInfo, rgszNames, cNames, rgDispId)` with same result
}
HRESULT STDMETHODCALLTYPE DispatchProxy::Invoke(DISPID dispIdMember, REFIID riid, LCID lcid, WORD wFlags, DISPPARAMS * pDispParams, VARIANT * pVarResult, EXCEPINFO * pExcepInfo, UINT * puArgErr) {
return pTypeInfo->Invoke(pUnknown, dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr); /// Fails with `E_NOTIMPL`. Also tried `DispInvoke(pUnknown, pTypeInfo, dispIdMember, wFlags, pDispParams, pVarResult, pExcepInfo, puArgErr)` with same result
}
};
On a related note, I need a way for the script to obtain references to objects like that of the CUIAutomation
class, before they (scripts) can call methods on these. I straight up allow scripts to create COM objects of specified CLSID by exposing a createObject
method on a "global" IDispatch
-implementing object, much like VBScript's CreateObject
function or new ActiveXObject(progID)
in Internet Explorer back in the day. It uses CoCreateInstance
to create an object of the COM class identified by specified CLSID:
HRESULT Global::CreateObject(VARIANT * pvCLSID, VARIANT * pvResult) {
_ASSERTE(V_VT(pvCLSID) == VT_BSTR);
CLSID CLSID;
CLSIDFromString(V_BSTR(pvCLSID), &CLSID);
IUnknown * pUnknown;
CoCreateInstance(CLSID, NULL, CLSCTX_INPROC_SERVER, IID_IUnknown, &pUnknown);
IDispatch * pDispatch;
HRESULT hResult = pUnknown->QueryInterface(&pDispatch);
if(hResult != S_OK) {
_ASSERTE(hResult == E_NOINTERFACE);
ITypeInfo * pTypeInfo;
if(LoadTypeInfo(V_BST(pvCLSID), &pTypeInfo)) { /// No type information was available -- not much choice but to return the created object as `IUnknown`
V_VT(pvResult) = VT_UNKNOWN;
V_UNKNOWN(pvResult) = pUnknown;
return S_OK;
} else {
pDispatch = new DispatchProxy(pUnknown, pTypeInfo);
}
}
if(pvResult) {
V_VT(pvResult) = VT_DISPATCH;
V_DISPATCH(pvResult) = pDispatch;
}
return S_OK;
}
A script can create a CUIAutomation
object and get a reference to the new DispatchProxy
wrapping it like so:
uiautomation = createObject("{ff48dba4-60ef-4201-aa87-54103eef594e}");
It should then be able to call methods (here GetRootElement
) on the object:
uiautomation.GetRootElement(/* parameters */);
Unfortunately, the pTypeInfo->Invoke
call at the heart of all of it returns E_NOTIMPL
. That's the immediate problem, as of now.
What is not implemented, and why? The member ID (dispIdMember
) matches what pTypeInfo->GetIDsOfNames
writes earlier, and the latter returns S_OK
, so the member ID, according to it at least, is valid. I don't think the parameter format has anything to do with it either -- I would expect another error code from the pTypeInfo->Invoke
call if it did.
Making GetTypeInfoCount
write 1
as type information count and writing pTypeInfo
as result of GetTypeInfo
has no effect on the result of subsequent ITypeInfo::Invoke
call -- it still fails.
I also tried using the actual IUIAutomation
interface type information (pTypeInfoDefaultInterface
in the snippet below) that I obtain on the original coclass ITypeInfo
object, as opposed to that of the coclass itself, even though documentation sort of implies ITypeInfo::Invoke
may recurse into referenced types automatically:
HREFTYPE hRefType;
pTypeInfo->GetRefTypeOfImplType(0, &hRefType);
ITypeInfo * pTypeInfoDefaultInterface;
pTypeInfo->GetRefTypeInfo(hRefType, &pTypeInfoDefaultInterface);
The effect is the same, regardless of whether the interface or the coclass type information is used -- ITypeInfo::Invoke
returns E_NOTIMPL
.
What am I doing wrong? Am I missing some crucial information about COM, or dispatching, or what type information can do for me? I don't write IDL files, and the DispatchProxy
isn't part of some COM server, it's strictly internal class for my application. I looked at the virtual function tables that Visual C++ lets me peek at, and I also did some investigation with GetFuncDesc
on the type information -- what it fills out seems to be solid -- there is everything -- names and parameter type and count for every expected method that I am attempting to invoke. The pointers are valid and available.
I admit that at least with GetRootElement
which expects a pointer to a pointer to an object, dispatching such method from a script that may not even be able to pass parameters of such type, may be the culprit. But according to documentation, ITypeInfo::Invoke
should probably return E_INVALIDARG
or DISP_E_EXCEPTION
, in such case.
I tried playing around with CreateStdDispatch
, too, but two things irk at me -- why shouldn't the above work, for starters? And second, I don't understand exactly what dispatches from where with CreateStdDispatch
and which pointers go as which arguments. I suppose unless it's the idiomatic alternative here, it's not my actual question, but if it will help my case I am all for getting an explanation on what exactly does it do and how to plug it in.