Search code examples
c++comshared-ptr

CComPtr and std::shared_ptr interoperability


Consider following scenario. There is a ATL wrapped C++ class (CMyComClass which implements IMyComClass), this class is used in async operations so it (should derive) derives from std::enable_shared_from_this to provide shared_from_this() (instead of passing this to async function) which will ensure that when the async operation is invoked the object still exists. On the other hand there is a COM interface which, for example can ask for the above class to be returned as com object, something like get(IUnknown** myobject) or addObject(IUnkown* mynewobject) to add the object. At this situation I'm in a deadlock, I cant just take raw pointer from the COM and assign to the shared_ptr since I do not transfer the ownership, and the reference counting of shared_ptr will be wrong since it doesnt count previous COM references, in addition and increase in shared_ptr count will not affect the CComPtr ref count, meaning the pointer could be destroyed any time. In addition there are member functions of CMyComClass which could create, for example, std::async operation, passing this into it, again, the wrapping COM could be destroyed and I will be left with dangling pointer. Is there a way to overcome this problem? Is there an equivalent of shared_from_this on IUnknown.
Yes, I know, the design is flawed and no, I cant change it now

EDIT001: I think I overcomplicated the question. Lets start from the root problem. Consider following COM class

class ATL_NO_VTABLE CMyComObject
    : public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMyComObject, &CLSID_MyComObject>,
      public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
{
public:
    CMyComObject() { m_pUnkMarshaler = NULL; }

    DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT)

    BEGIN_COM_MAP(CMyComObject)
    COM_INTERFACE_ENTRY(IMyComObject)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
    END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()
    DECLARE_GET_CONTROLLING_UNKNOWN()

    HRESULT FinalConstruct()
    {
        m_asyncTask = std::async(std::launch::async, [self{this}]() { std::cout << typeid(self).name() << std::endl; });
        return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
    }

    void FinalRelease() { m_pUnkMarshaler.Release(); }

    CComPtr<IUnknown> m_pUnkMarshaler;

private:
    std::future<void> m_asyncTask;
};

Note the std::async in the FinalConstruct (not most suitable place, but pretend it is a regular COM method), async is called, task is scheduled, then the instance (instance of COM object) is destroyed, for example because reference count of the instance dropped to zero. Obviously the scheduled task will fail, lets say with access violation. How one would prevent it from happening?

EDIT002: just for lulz
Voila, the solution!

class ATL_NO_VTABLE CMyComObject
    : public CComObjectRootEx<CComMultiThreadModel>,
      public CComCoClass<CMyComObject, &CLSID_MyComObject>,
      public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
{
public:
    CMyComObject()
        : m_pUnkMarshaler(nullptr), m_self(this, [](CMyComObject* p) {
              // check if still have COM references
              if(p->m_dwRef == 0)
                  delete p;
          })
    {
    }

    DECLARE_REGISTRY_RESOURCEID(IDR_MYCOMOBJECT)

    BEGIN_COM_MAP(CMyComObject)
    COM_INTERFACE_ENTRY(IMyComObject)
    COM_INTERFACE_ENTRY(IDispatch)
    COM_INTERFACE_ENTRY_AGGREGATE(IID_IMarshal, m_pUnkMarshaler.p)
    END_COM_MAP()

    DECLARE_PROTECT_FINAL_CONSTRUCT()
    DECLARE_GET_CONTROLLING_UNKNOWN()

    HRESULT FinalConstruct()
    {
        m_asyncTask = std::async(
            std::launch::async, [self{SharedFromThis()}]() { std::cout << typeid(self).name() << std::endl; });
        return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
    }

    void FinalRelease() { m_pUnkMarshaler.Release(); }

    ULONG InternalRelease()
    {
        if(m_dwRef > 0)
        {
            _ThreadModel::Decrement(&m_dwRef);
        }
        // Dont let COM delete the instance if there is shared ptrs in the wild
        return m_dwRef + m_self.use_count();
    }
    std::shared_ptr<CMyComObject> SharedFromThis() { return m_self; }

    CComPtr<IUnknown> m_pUnkMarshaler;

private:
    std::future<void> m_asyncTask;
    std::shared_ptr<CMyComObject> m_self;
};

It would be nice and elegant solution which works great. However, the class itself holding one reference to the object, so the deleter of the std::shared_ptr will never kick in. Alas! Do the right thing and get the stuff out of COM class and then keep the extracted stuff as shared_ptr in the COM class.


Solution

  • As others have mentioned, COM has pretty strict rules for using interfaces across threads, and you're just inviting bugs by not following those rules. This is one of the many reasons that many COM developers create their core logic as C++ classes and then wrap those C++ classes in a thin COM object. This is my recommendation. Your core object should have nothing that is COM aware. If it currently has data members that are other COM interface pointers, do the same thing -- extract that child object into a C++ class and provide a COM wrapper for the child object that is owned by the COM wrapper of the parent object.

    class CMyObject : std::enable_shared_from_this
    {
    public:
        void Startup()
        {
            // non-COM stuff
            auto self = shared_from_this();
            m_asyncTask = std::async(std::launch::async, [self]() {
                 std::cout << typeid(self).name() << std::endl;
            });
        }
    private:
        std::future<void> m_asyncTask;
    }
    
    class ATL_NO_VTABLE CMyComObject
        : public CComObjectRootEx<CComMultiThreadModel>,
          public CComCoClass<CMyComObject, &CLSID_MyComObject>,
          public IDispatchImpl<IMyComObject, &IID_IMyComObject, &LIBID_ATLProject2Lib, /*wMajor =*/1, /*wMinor =*/0>
    {
        ...
        HRESULT FinalConstruct()
        {
            m_internal = make_shared<CMyObject>();
            m_internal->Startup();
            // COM stuff
            return CoCreateFreeThreadedMarshaler(GetControllingUnknown(), &m_pUnkMarshaler.p);
        }
        ...
    private:
        shared_ptr<CMyObject> m_internal;
        ...
    }