2

Update 2021-04-20: The code presented here is for illustration purposes only. As pointed out by Simon Mourier, for marshaling in-process of such a simple class there is no need for all the TLB shenanigans. In reality, the TLB is provided by a third-party, with the interface in question serving for callbacks. The object calling the interface resides in another process, however, so I really do have to marshal the interface after implementing it. As demonstrating the whole inter-process flow is tedious, I opted for something simpler - in-process inter-apartment marshaling.


Suppose I have the following type library:

import "oaidl.idl";
import "ocidl.idl";

[
    uuid(99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82),
    version(1.0),
]
library IsThisRealMarshal
{
    [
        uuid(80997EA1-0144-41EC-ABCF-5FAD08D5A498),
        nonextensible,
    ]
    dispinterface IMyInterface
    {
        properties:
        methods:
            [id(1)]
            void Method();
    };
};

I would like to marshal IMyInterface to another apartment. Since it's a dispinterface, I would like to use the OLE marshaler for this. And so, I register the type library:

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Classes\TypeLib\{99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82}]

[HKEY_CURRENT_USER\SOFTWARE\Classes\TypeLib\{99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82}\1.0]

[HKEY_CURRENT_USER\SOFTWARE\Classes\TypeLib\{99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82}\1.0\0]

[HKEY_CURRENT_USER\SOFTWARE\Classes\TypeLib\{99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82}\1.0\0\win32]
@="path\\to\\library.tlb"

And the interface (setting the proxy CLSID to that of the OLE marshaler):

Windows Registry Editor Version 5.00

[HKEY_CURRENT_USER\SOFTWARE\Classes\Interface\{80997EA1-0144-41EC-ABCF-5FAD08D5A498}]

[HKEY_CURRENT_USER\SOFTWARE\Classes\Interface\{80997EA1-0144-41EC-ABCF-5FAD08D5A498}\ProxyStubClsid32]
@="{00020424-0000-0000-C000-000000000046}"

[HKEY_CURRENT_USER\SOFTWARE\Classes\Interface\{80997EA1-0144-41EC-ABCF-5FAD08D5A498}\TypeLib]
@="{99CF9EB9-9B6E-4D44-B73C-6BB8FCD45B82}"
"Version"="1.0"

And I try to marshal (error-checking omitted for brevity):

CoInitializeEx(nullptr, COINIT_MULTITHREADED);

CComPtr<IMyInterface> object {};
object.Attach(new MyObject);

CComPtr<IGlobalInterfaceTable> git {};
git.CoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, CLSCTX_INPROC_SERVER);

DWORD cookie = 0;
git->RegisterInterfaceInGlobal(object, __uuidof(IMyInterface), &cookie);

auto thread = std::thread([cookie]
{
    CoInitializeEx(nullptr, COINIT_APARTMENTTHREADED);
    
    CComPtr<IGlobalInterfaceTable> git {};
    git.CoCreateInstance(CLSID_StdGlobalInterfaceTable, nullptr, CLSCTX_INPROC_SERVER);

    CComPtr<IMyInterface> object {};
    git->GetInterfaceFromGlobal(cookie, __uuidof(IMyInterface), (void **)&object);
});
thread.join();

Where the MyObject class implements the bare minimum COM functionality:

class MyObject : public IMyInterface
{
private:
    std::atomic<ULONG> _refcount = 1;

public:
    MyObject() = default;
    
    MyObject(MyObject const &) = delete;
    MyObject & operator=(MyObject const &) = delete;
    
    HRESULT QueryInterface(const IID& riid, void** ppvObject) override
    {
        if (nullptr == ppvObject)
        {
            return E_POINTER;
        }

        if (riid == __uuidof(IUnknown))
        {
            *ppvObject = static_cast<IUnknown *>(this);
        }
        else if (riid == __uuidof(IDispatch))
        {
            *ppvObject = static_cast<IDispatch *>(this);
        }
        else if (riid == __uuidof(IMyInterface))
        {
            *ppvObject = static_cast<IMyInterface *>(this);
        }
        else
        {
            *ppvObject = nullptr;
            return E_NOINTERFACE;
        }

        static_cast<IUnknown *>(*ppvObject)->AddRef();

        return S_OK;
    }
    
    ULONG AddRef() override
    {
        return ++_refcount;
    }
    
    ULONG Release() override
    {
        auto const new_refcount = --_refcount;
        if (0 == new_refcount)
        {
            delete this;
        }
        return new_refcount;
    }
    
    HRESULT GetTypeInfoCount(UINT* pctinfo) override
    {
        return E_NOTIMPL;
    }
    
    HRESULT GetTypeInfo(UINT iTInfo, LCID lcid, ITypeInfo** ppTInfo) override
    {
        return E_NOTIMPL;
    }
    
    HRESULT GetIDsOfNames(const IID& riid, LPOLESTR* rgszNames, UINT cNames, LCID lcid, DISPID* rgDispId) override
    {
        return E_NOTIMPL;
    }
    
    HRESULT Invoke(DISPID dispIdMember, const IID& riid, LCID lcid, WORD wFlags, DISPPARAMS* pDispParams,
        VARIANT* pVarResult, EXCEPINFO* pExcepInfo, UINT* puArgErr) override
    {
        return E_NOTIMPL;
    }
};

Unfortunately, the call to GetInterfaceFromGlobal fails with E_FAIL.

Debugging reveals that none of the IDispatch methods are called, only the IUnknown ones. Additionally, it appears that the E_FAIL originates from combase!CheckTypeInfo. First, this function uses ITypeInfo::GetTypeAttr to retrieve information about IMyInterface:

WinDbg display of the TYPEATTR structure for IMyInterface

It then proceeds to check whether the flags TYPEFLAG_FDUAL (0x40) or TYPEFLAG_FOLEAUTOMATION (0x100) are present in the wTypeFlags field of the TYPEATTR structure:

Disassembly of the CheckTypeInfo function, with the check for TYPEFLAG_FDUAL highlighted

Disassembly of the CheckTypeInfo function, with the check for TYPEFLAG_FOLEAUTOMATION highlighted

Since neither of these flags are present (the field has the value 0x1080, and indeed the IDL doesn't mark the interface as either [oleautomation] or [dual]), the function fails with E_FAIL.

What am I doing wrong? And if the OLE marshaler indeed cannot marshal this interface, is there anything I can do apart from implementing IMarshal myself, assuming I cannot modify the IDL?

Michael Bikovitsky
  • 903
  • 2
  • 10
  • 20
  • Not sure what you're ultimately trying to accomplish (sounds like an XY problem https://meta.stackexchange.com/questions/66377/what-is-the-xy-problem). If your scenario is really like in your code (inprocess, an object that is not a coclass, not cocreatable, in MTA, obviously supporting multithreads), don't use any .TLB, don't register anything. It should work as is. – Simon Mourier Apr 20 '21 at 08:20
  • @SimonMourier you're right, if the case were that simple I would not have bothered with the TLB. The reality is more complex, and I was merely trying to provide a minimal example. I've edited the question to perhaps better convey this point. – Michael Bikovitsky Apr 20 '21 at 10:22
  • I'm still not sure what you're trying to achieve with this sample. Change HKEY_CURRENT_USER by [HKEY_CLASSES_ROOT, it'll work. Your MyObject is not a coclass, not sure how the GIT sees it. Just create a project with ATL, add a simple ATL object, change the created interface by a dispinterface like yours, it will work too. – Simon Mourier Apr 20 '21 at 12:27
  • I tried that, it didn't work. Created a full-fledged ATL project, registered everything in the registry. `CoCreateInstance` on my class that implements the interface works, so long as the calling apartment matches the threading model of the object. As soon as it crosses apartment boundaries: `E_FAIL`. In fact, it fails in the exact same place as in my question. The OLE marshaler simply doesn't want to marshal this interface. – Michael Bikovitsky Apr 20 '21 at 13:43
  • 1
    Works fine for me: https://1drv.ms/u/s!AsmmEDGvydk2njUuR2POsleKAK0B?e=LG9cpx – Simon Mourier Apr 20 '21 at 16:21
  • Now I am genuinely perplexed. I'm running your code on my machine, and the first `CoCreateInstance` fails with `E_FAIL`! Inside `combase!CheckTypeInfo`, exactly as before! @SimonMourier, could you please share how you registered the coclass, interface, and type library? Perhaps I'm messing up the registration on my end somehow. – Michael Bikovitsky Apr 20 '21 at 17:35
  • 1
    Make sure you build both projects of course :-). At the end of its build, the ATLProject should register itself in HKCU (https://i.imgur.com/axnzPmB.png). PS: you code does return E_FAIL on my PC. – Simon Mourier Apr 20 '21 at 17:42

1 Answers1

1

With the help of Simon Mourier's code, I managed to find the problem. The problem was that I used the PSOAInterface proxy ({00020424-0000-0000-C000-000000000046}). Since IMyInterface is not an OLE Automation interface (i.e. not marked with [oleautomation]), this rightly failed.

The solution is to use the PSDispatch proxy ({00020420-0000-0000-C000-000000000046}), which is capable of marshaling pure IDispatch interfaces.

Michael Bikovitsky
  • 903
  • 2
  • 10
  • 20