A customer (VSP) was using the COM Shim and identified a scenario where a bug in the shim code could cause the host application to remain in memory indefinitely – thanks, VSP for finding this and bringing it to our attention! Misha did some ninja debugging and pinpointed the problem.

The shim is described here and here.

The problem is that the ManagedAggregator.CreateAggregatedInstance method is taking in an IComAggregator object (the CConnectProxy object, which is the outer object in the aggregation), but is not releasing it. This is a problem because the object was passed from the native shim code to the managed aggregator code and wrapped as an RCW. If we don’t release the RCW, the CLR will potentially hold on to it (and the underlying COM object) until the corresponding AppDomain is unloaded. However, in the current implementation the AppDomain is unloaded when COM object’s reference counter reaches zero. This essentially constitutes a circular reference problem thus preventing the host from terminating cleanly. The fix is simple: in the finally block, add a call to Marshal.ReleaseComObject on the outer object:

public void CreateAggregatedInstance(

    string assemblyName, string typeName, IComAggregator outerObject)

{

    IntPtr pOuter = IntPtr.Zero;

    IntPtr pInner = IntPtr.Zero;

 

    try

    {

        pOuter = Marshal.GetIUnknownForObject(outerObject);

        object innerObject =

            AppDomain.CurrentDomain.CreateInstanceAndUnwrap(

            assemblyName, typeName);

        pInner = Marshal.CreateAggregatedObject(pOuter, innerObject);

        outerObject.SetInnerPointer(pInner);

    }

    finally

    {

        if (pOuter != IntPtr.Zero)

        {

            Marshal.Release(pOuter);

        }

        if (pInner != IntPtr.Zero)

        {

            Marshal.Release(pInner);

        }

 

        // FIX: Bug discovered after release of 2.3.1.0.

        // We call ReleaseComObject on the outer object (ConnectProxy)

        // to make sure we delete the RCW, and prevent the CLR from

        // holding onto it indefinitely (and keeping the host alive).

        Marshal.ReleaseComObject(outerObject);

    }

}


While we were in the code, Misha identified another potential problem. There are scenarios where it is possible that the CConnectProxy::FinalRelease may not get called – for example, if you have some other add-in or automation client that connects to this shimmed add-in via the COMAddIns collection. Right now, the FinalRelease is implemented to clean up and unload the appdomain. To make sure this always happens, you can simply move all that code to the end of the OnDisconnection method instead:

HRESULT __stdcall CConnectProxy::OnDisconnection(

    ext_DisconnectMode RemoveMode, SAFEARRAY **custom)

{

    HRESULT hr = S_OK;

    hr =  m_pConnect->OnDisconnection(RemoveMode, custom);

    if (SUCCEEDED(hr))

    {

        m_pConnect->Release();

        m_pConnect = NULL;

    }

 

    // FIX: Bug discovered after release of 2.3.1.0.

    // Move the code that releases the aggregated innner object

    // from the CConnectProxy::FinalRelease to
    // CConnectProxy::OnDisconnection.

    // This ensures that the code gets called even if this add-in gets

    // handed out via the COMAddIns collection to other consumers.

    if (m_pUnknownInner)

    {

        m_pUnknownInner->Release();

    }

 

    if (m_pCLRLoader)

    {

        m_pCLRLoader->Unload();

        delete m_pCLRLoader;

        m_pCLRLoader = NULL;

    }

 

    return hr;

}

 

// FinalRelease will be the last thing called in the shim/add-in, after

// OnBeginShutdown and OnDisconnection.

void CConnectProxy::FinalRelease()

{

}


Thanks again to VSP for spotting the original problem, and to Misha for identifying the cause and the fix.