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.