Developers who are accustomed to the IDisposable pattern or to C#’s ‘using’ syntax sometimes ask why COM Interop doesn’t support IDisposable on every Runtime Callable Wrapper (RCW). That way, managed code could indicate that it is finished using the unmanaged COM resource. This would allow the resources to be cleaned up much earlier than they would be if we waited for a GC. Also, it might better approximate the way an unmanaged client would have used this COM object through explicit Release calls.
There’s a service called System.Runtime.InteropServices.Marshal.ReleaseComObject() that looks suspiciously like it could be used as a Dispose() call. However, this is misleading. ReleaseComObject is quite different from Dispose() and it’s also quite different from IUnknown::Release() as I’ll explain.
The COM Interop layer in the CLR can make do with a single reference count against the unmanaged pUnk, regardless of how many managed clients refer to that object. In other words, the Interop layer does not hold a reference count for each managed client of that pUnk. Instead, we rely on the reachability magic of the GC to determine when nobody needs that pUnk anymore. When nobody needs the pUnk, then we drop our single reference count on that pUnk.
Furthermore, negotiation for interfaces in managed code via COM Interop does not necessarily affect the unmanaged refcount of the COM object. For instance, the managed wrapper might have already cached a pUnk for this interface.
Regardless of the actual refcount that the wrapper holds on the underlying COM object, ReleaseComObject will release all these refcounts at one time.
However, the return value from ReleaseComObject reveals that there’s an additional refcounting scheme involved. This is unrelated to the COM refcount. The same pUnk might be marshaled into the managed process a number of times. We keep track of this count. You can then call ReleaseComObject that same number of times before we will call IUnknown::Release on the pUnks held by the wrapper and start giving throwing InvalidComObjectExceptions. If you are passing the pUnk backwards and forwards across the layer, this means that the “marshaling count” will be a large and arbitrary number. But, for some usage patterns, the number of times the pUnk is marshaled across may correspond to the number of distinct managed clients that have got their hands on the wrapper. If this happens to be the case, then that many managed clients can independently call ReleaseComObject before the wrapper is zombied and the underlying pUnks are Release’d.
I guess that this behavior is slightly more useful than a simple Release in some circumstances. And you can turn it into the equivalent of IUnknown::Release by calling it in a loop until it returns 0. At that point, our internal “marshaling count” has been decremented to 0 and we have Release’d the pUnks. (We really need to add a ReleaseComObjectFully() service to avoid that silly loop).
Application code can either be on the GC plan, where we track whether there are references outstanding – but in a non-deterministic manner that is guided by memory pressure – or application code can do the tracking itself. But if the application does the tracking, it is responsible for knowing whether there are other managed clients still using the COM object.
One way you might do this is by subtyping the wrapper and adding a Dispose protocol on the managed side that is reference counted. But all managed clients in the process must observe the discipline you define. A more practical approach is to ensure that you are the only client of the pUnk by creating the COM object yourself and then never sharing that reference with anyone else.
If you are using a COM object in a scoped, single-threaded manner then you can safely call ReleaseComObject on that object when you are done with it. This will eagerly release any unmanaged resources associated with that object. Subsequent calls would get an InvalidComObjectException. So don’t make subsequent calls.
But if you are using a COM object from multiple places or multiple threads in your application (or from other applications in the same process), you should not call ReleaseComObject. That’s because you will inflict InvalidComObjectExceptions on those other parts of the application.
So my advice is:
- If you are a server application, calling ReleaseComObject may be an important and necessary requirement for getting good throughput. This is especially true if the COM objects live in a Single Threaded Apartment (STA). For example, ASP compatibility mode uses the DCOM STA threadpool. In these scenarios, you would create a COM object, use it, then eagerly call ReleaseComObject on each request.
- If you are a client application using a modest number of COM objects that are passed around freely in your managed code, you should not use ReleaseComObject. You would likely inflict Disconnected errors on parts of the application by doing so. The performance benefit of eagerly cleaning up the resources isn’t worth the problems you are causing.
- If you have a case where you are creating COM objects at a high rate, passing them around freely, choking the Finalizer thread, and consuming a lot of unmanaged resources… you are out of luck.