Is it safe to call an RCW from a finalizer?
Asked Answered
T

2

18

I have a managed object that calls a COM server to allocate some memory. The managed object must call the COM server again to free that memory before the managed object goes away to avoid a memory leak. This object implements IDisposable to help ensure that the correct memory-releasing COM call is made.

In the event that the Dispose method is not called, I would like the object's finalizer to free the memory. The trouble is, the rules of finalization is that you must not access any reference because you don't know what other objects have already been GC'd and/or finalized before you. This leaves the only touchable object state to be fields (handles being the most common).

But calling a COM server involves going through an runtime callable wrapper (RCW) in order to free the memory that I have a cookie to stored in a field. Is that RCW safe to call from a finalizer (is it guaranteed to have not been GC'd or finalized at this point)?

For those of you not familiar with finalization, although the finalizer thread runs in the background of a managed appdomain while its running, at for those cases touching references would theoretically be OK, finalization also happens at appdomain shutdown, and in any order -- not just in reference relationship order. This limits what you can assume is safe to touch from your finalizer. Any reference to a managed object might be "bad" (collected memory) even though the reference is non-null.

Update: I just tried it and got this:

An unhandled exception of type 'System.Runtime.InteropServices.InvalidComObjectException' occurred in myassembly.dll

Additional information: COM object that has been separated from its underlying RCW cannot be used.

Tracheid answered 15/10, 2009 at 17:45 Comment(1)
A method on the COM server (why would anyone call Dispose on an RCW itself? I'd be surprised if that's even possible).Tracheid
T
16

I found out from the CLR team themselves that indeed it is not safe -- unless you allocate a GCHandle on the RCW while it's still safe to do so (when you first acquire the RCW). This ensures that the GC and finalizer haven't totaled the RCW before the managed object that needs to invoke it is finalized.

class MyManagedObject : IDisposable
{
    private ISomeObject comServer;
    private GCHandle rcwHandle;
    private IServiceProvider serviceProvider;
    private uint cookie;

    public MyManagedObject(IServiceProvider serviceProvider)
    {
        this.serviceProvider = serviceProvider;
        this.comServer = this. serviceProvider.GetService(/*some service*/) as ISomeObject;
        this.rcwHandle = GCHandle.Alloc(this.comServer, GCHandleType.Normal);
        this.cookie = comServer.GetCookie();
    }

    ~MyManagedObject()
    {
        this.Dispose(false);
    }

    public void Dispose()
    {
        this.Dispose(true);
        GC.SuppressFinalize(this);
    }

    protected virtual void Dispose(bool disposing)
    {
        if (disposing)
        {
            // dispose owned managed objects here.
        }

        if (this.rcwHandle.IsAllocated)
        {
            // calling this RCW is safe because we have a GC handle to it.
            this.comServer.ReleaseCookie(this.cookie);

            // Now release the GC handle on the RCW so it can be freed as well
            this.rcwHandle.Free();
        }
    }
}

It turns out in my particular case, my app is hosting the CLR itself. Therefore, it's calling mscoree!CoEEShutdownCOM before the finalizer thread gets to run, which kills the RCW and results in the InvalidComObjectException error I was seeing.

But in normal cases where the CLR is not hosting itself, I'm told this should work.

Tracheid answered 15/10, 2009 at 22:4 Comment(1)
Hello Andrew, and thanks for this post. I'm trying to apply your suggested solution to a class wrapping the IAudioSessionControl2 COM interface. Class implements the IDisposable interface, calls Marshal.ReleaseComObject over the IAudioSessionControl2 interface-instance, which results in the error you're referring-to in your post. Coming to apply your suggestion, I replaced ISomeObject (which I'm not familiar with) with object, but do not know how to use the serviceProvider argument of the constructor, and what service should be stated on the call to GetService? Much obliged.Auriculate
L
7

No it is not safe to access a RCW from the finalizer thread. Once you reach the finalizer thread you have no guarantee that the RCW is still alive. It is possible for it to be ahead of your object in the finalizer queue and hence released by the time your destructor runs on the finalizer thread.

Lorenzo answered 15/10, 2009 at 17:54 Comment(1)
Removed my answer as it appears in my case I got lucky w/ the finalizer thread.Burgoo

© 2022 - 2024 — McMap. All rights reserved.