0

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.

Armen Michaeli
  • 8,625
  • 8
  • 58
  • 95
  • Not sure what you're after, but COM Automation is not only about IDispatch but also a strict list of supported types for universal marshaling support (OleAut32.dll): https://docs.microsoft.com/en-us/openspecs/windows_protocols/ms-oaut/7b5fa59b-d8f6-4a47-9695-630d3c10363e In short, the only way to present Automation-compatible interfaces over a non compatible one it to write a wrapper, probably by hand. For example, that's what Microsoft did here: https://docs.microsoft.com/en-us/windows/win32/shell/objects (these are Automation compatible interfaces over IUnknown stuff: IShellFolder, etc.) – Simon Mourier Dec 02 '19 at 12:22
  • @SimonMourier Thanks. If you read my question you can see that a wrapper is exactly what I am attempting. I have also taken a look at both of the resources you have linked to. I do not see any indication that the types I use either violate any of the compatibility constraints documented in the first article. As for the second, I don't know what to take away from the fact that IShellFolder does or doesn't implement IDispatch -- it's at any rate closed source component, even if interfaces are public -- however they wrap it behind a IDispatch, if at all, isn't documented, is it? – Armen Michaeli Dec 02 '19 at 13:51
  • Also, to be on the safe side: even though I am de-facto dealing with COM Automation, UIAutomation is about assistive technologies and is conceptually unrelated to COM Automation. It could have been any COM class, really, that doesn't implement IDispatch. It just happens to be CUIAutomation, which is something my application is using to solve a concrete problem/requirement. – Armen Michaeli Dec 02 '19 at 13:54
  • I don't think we're talking about the same kind of wrapper. What I mean by a wrapper is a piece of code that supports IDispatch and automation types (any type that is not in that list violates Automation), but decoupled from the inner IUnknown-derived interfaces. So to answer your question "is it possible to implement dispatching for an object that doesn't implement IDispatch [for scripting clients]", yes, but not in an automatic way. You have to build new objects, new methods, etc. 100% compatible with Automation. – Simon Mourier Dec 02 '19 at 14:38
  • PS: I know perfectly what UI Automation is, and that it has nothing to do with COM Automation. As for UI Automation even if many of the types it uses are automation-compatible, not all are, for example: IUIAutomation::ElementFromPoint uses a POINT pointer. – Simon Mourier Dec 02 '19 at 14:39
  • Thank you, I think you have a very fair point -- there are all kinds of methods on the `IUIAutomation` interface, and some of them have rather exotic parameter types, which may be a real problem for automatic dispatch through a script. I'll see if I in fact can "manually" invoke `ElementFromPoint` with something that works, first. I'll do some tests when I get home. – Armen Michaeli Dec 02 '19 at 15:10
  • The notion that createObject("{ff48dba4-60ef-4201-aa87-54103eef594e}") will help you use your DispatchProxy class is just wrong. It creates the stock implementation in c:\windows\system32\uiautomationcore.dll provided by Microsoft, at no point is it aware of your DispatchProxy. Doctoring the InprocServer32 registry key so it uses your DLL instead is possible, but too ugly to consider. That {guid} can be anything you want of course, you'll surely favor registering DispatchProxy and using its guid in the scripting code. – Hans Passant Dec 02 '19 at 15:47
  • I think I know what you mean. Yes, `createObject` creates a `CUIAutomation` object as defined by `uiautomationcore.dll`. But that's not what it returns. It creates a proxy that wraps the former and returns that. If that's what you mean is wrong -- from an architectural point, then you may be right. Certainly, if I cannot transparently delegate to the original object, returning a proxy in place may seem misleading to script author (a case of CLSID mismatch), but the original (`IUnknown`) is useless, although it may be returned. Doctoring is absolutely out of the question. Own GUID -- maybe. – Armen Michaeli Dec 02 '19 at 19:36

0 Answers0