Skip to content

Strategies for managing COM object lifetime and release

bclothier edited this page Aug 12, 2018 · 1 revision

Rubberduck, written and compiled as a .NET assembly runs as an add-in to a COM host. Thus, it has very demanding requirements for COM interop, several which will violate the assumptions if we allow COM interop to do the magic for us. By default, COM interop does lot of things in the background to make it easy for anyone using .NET code to use COM object. However, several assumptions it makes about the lifetime and ownership of the COM objects and its associated RCW object are violated, necessitating that the Rubberduck take more manual control over how the lifetime and release of COM object should be managed. Whenever it does, it must ensure it follows the conventions that the COM expects from an interface. Most of the convention will not be enforced by the programming construct; it wold be up to the programmer to ensure that the implementation is sensible and conforms to what COM expects, which requires fair amount of documentation reading.

Terminology

In the article we will refer to terms Dispose and Release. Both has a specific and separate meaning. You can only Releasee a COM object; it cannot be disposed. Only .NET classes may be Disposed, and to do so, must implement the IDisposable interface. The distinction is important because it speaks to the fundamental difference in the garbage collection as implemented by COM and by the .NET runtime. COM garbage collection is ref-counted, so for each reference we gain to a given COM object, its internal reference count is incremented and it is our responsibility to call the Release method on the same COM object when we are done with it. .NET garbage collection, on the other hand is basically a generational garbage collection, which will run on its own thread, and the cleanup may occur at any time sometime after the runtime engine has determined that the object is now unreachable. This is in direct conflict with the COM's deterministic garbage collection approach. Thus, most of pains around proper release of COM object comes in ensure that we invoke the Marshal.ReleaseComObject at exactly the right time and when it is actually necessary. However, it's relatively uncommon to actually invoke this method; we are more likely to use the Dispose method to indirectly invoke the Marshal.ReleaseComObject, as explained below.

SafeComWrapper class

The first measure Rubberduck take is to consistently wrap the COM object in a generic SafeComWrapper class. All COM objects must have a corresponding wrapping class and must be wrapped as soon as they are received. The primary function of the SafeComWrapper is to leverage the disposable pattern so that we can ensure that a Marshal.ReleaseComObject is called on the object when we are done with it.

For that reasons, any local variables that references a COM object typically should be wrapped in a using block which will ensure that it gets released properly. On the other hand, when it references a COM object that came from a repository or by a different object, it would be not appropriate to attempt to release that COM object because we do not own that COM object. Thus, it is important that we have a clear and unambiguous ownership of the COM object at all time.

Safety net: COM Safe

For each SafeComWrapper created, it will be automatically added to the COM safe. WeakComSafe is one of such implementation. That provides us with a fallback mechanism in case where we forget to properly release a wrapped COM object. Being a fallback mechanism, it's ideal that when we exit the process, there are nothing in the COM safe. But if we have some objects in there, the safe will proceed to Marshal.ReleaseComObject those dangling objects to ensure that we do not attempt to exit the process with dangling pointers, which can cause crash on exit. The use of COM safe is transparent and completely encapsulated; there should be no need to interact with the COM safe directly.

Repository of wrapped COM objects

Particularly for a collection with non-fixed members, it can be difficult to track the lifetime of the members and the collection. A mechanism to help ease the challenge is to use a repository. An example of this would be ProjectRepository. It takes the responsibility of managing the lifetime of the COM object that are contained within the collection(s). This way, the consumers who need the child COM object can simply ask for it and use it without any Dispose required on their part. The repository will then monitor the membership and manage the changes, ensuring proper disposal of the wrapped COM object when they are removed from the collection.

For that reason, the consumer must take care to not wrap those objects provided by the repository in a using block nor attempt to dispose the wrapped COM object.

Aggregated COM interfaces

Whether we find that we are dealing with a buggy implementation that prevents us from using COM interop in the usual way, we may opt to use an aggregated interface. An example of this is with DockableWindowHost. There is a bug with the .NET's implementation of the UserControl class that causes an access violation because it does not properly handle the teardown. To work around this bug, we aggregate the COM interface, which allow us to basically intercept the native calls into other implemented interfaces (in this case, the IOleObject** interfaces) and thus handle it differently from what the .NET runtime's implementation would have done, enabling us to correctly tear down the UserControl without crashing the VBE host.

Aggregation of the interface require that we define a compatible interface and provide sensible implementations of the methods required by the interface. Extra care is needed to do this correctly.

COM Event management

For COM objects that can raise events, those require additional functionalities and thus should be wrapped in SafeEventedComWrapper which provide the encapsulation for setting up and tearing down the event sinks. Because the events require additional COM objects, the wrapper has additional methods AttachEvent and DetachEvents to handle the associated COM objects.

Generally speaking, it is recommended that .NET consumers of the events be funneled toward a single source. An example of this implementation can be found in the VBEEvents class. That ensures that there is a central shut-off valve which greatly simplify the shutdown process. It does not matter if the .NET consumers are left dangling, but it would be very bad if COM event sinks were left behind dangling.

However, for some, it is not practical to centralize. An example of this is the command bar button. For this reason, the implementation is slightly different; its AttachEvents will be only called on the first subscriber and its DetachEvents only when its last subscriber has unsubscribed.

Note that for some COM objects (e.g. VB6's implementation), the source of event may be an entirely different object. For that reason, SafeRedirectedEventedComWrapper should be used instead.

Custom COM enumeration

Rubberduck uses a customized ComWrapperEnumerator to enable us to be able to enumerate a COM collection without leaking the enumerator itself and providing some control over how the child item are returned.

Interop references

By convention, none of the Rubberduck projects should have any direct COM references with the exception of the Rubberduck.VBEditor.VB* projects which provide the concrete implementation of the various safe COM wrappers. All other projects should reference only the Rubberduck.VBEditor which provide all the abstract interfaces of the safe COM wrapper. The Rubberduck.Main is the only project that may reference the Rubberduck.VBEditor.VB* as it will provide one of those as the implementation for the abstractions via the Rubberduck.VBEditor. This design ensures that none of the projects will accidentally reference a naked COM object. It is also important that those projects minimize their COM access as much as possible, preferring to use QualifiedModuleName rather than SafeComWrapper objects, especially when marshaling across events or between objects. This design help to simplify the scope and identify who is responsible for disposing of the wrapped objects.

Clone this wiki locally