As often happens with small affirmations, the double-dot rule needs further explanation. You can use it as a mnemonic.
The reason is that each new RCW you get in .NET will hold a reference to the respective COM object. So, if you have (assuming obj
is an RCW and this is the first time you're getting the other objects):
obj.Property[0].MethodThatReturnsAnotherObject()
// 1 2 3
you're getting 3 additional RCWs. As you can see, there is only 1 extra dot. Although properties are probably the most common way of obtaining other COM objects, it's not the only way.
Usually, each RCW will only release the underlying COM object when it's garbage collected, unless you use Marshal.ReleaseComObject
. Only use this method if you are completely sure you're the only one using the RCW you're releasing.
To be perfectly clear about this subject:
Only use ReleaseComObject
or FinalReleaseComObject
if you really, demonstrably have to, and you are completely, totally sure your piece of code is the only one referring to the RCW.
<type> propObj;
try
{
propObj = obj.Property;
<type> propArrayObj;
try
{
propArrayObj = propObj[0];
<type> propArrayObjReturn;
try
{
propArrayObjReturn = propArrayObj.MethodThatReturnsAnotherObject();
}
finally
{
if (propArrayObjReturn != null) Marshal.ReleaseComObject(propArrayObjReturn);
}
}
finally
{
if (propArrayObj != null) Marshal.ReleaseComObject(propArrayObj);
}
}
finally
{
if (propObj != null) Marshal.ReleaseComObject(propObj);
}
This is tedious, a wrapper might help here:
using System;
using System.Runtime.InteropServices;
using System.Threading;
public class ComPtr<T> : IDisposable where T : class
{
public ComPtr(T comObj)
{
if (comObj == null) throw new ArgumentNullException("comObj");
if (!typeof(T).IsInterface)
{
throw new ArgumentException("COM type must be an interface.", "T");
}
// TODO: check interface attributes: ComImport or ComVisible, and Guid
this.comObj = comObj;
}
private T comObj;
public T ComObj
{
get
{
// It's not best practice to throw exceptions in getters
// But the alternative might lead to a latent NullReferenceException
if (comObj == null)
{
throw new ObjectDisposedException("ComObj");
}
return comObj;
}
}
~ComPtr()
{
Dispose(false);
}
// IDisposable
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected void Dispose(bool disposing)
{
#if !RELEASECOMPTR
// Option 1: Safe. It might force the GC too often.
// You can probably use a global limiter, e.g. don't force GC
// for less than 5 seconds apart.
if (Interlocked.Exchange(ref comObj, null) != null)
{
// Note: GC all generations
GC.Collect();
// WARNING: Wait for ALL pending finalizers
// COM objects in other STA threads will require those threads
// to process messages in a timely manner.
// However, this is the only way to be sure GCed RCWs
// actually invoked the COM object's Release.
GC.WaitForPendingFinalizers();
}
#else
// Option 2: Dangerous! You must be sure you have no other
// reference to the RCW (Runtime Callable Wrapper).
T currentComObj = Interlocked.Exchange(ref comObj, null);
if (currentComObj != null)
{
// Note: This might (and usually does) invalidate the RCW
Marshal.ReleaseComObject(currentComObj);
// WARNING: This WILL invalidate the RCW, no matter how many
// times the object reentered the managed world.
// However, this is the only way to be sure the RCW's
// COM object is not referenced by our .NET instance.
//Marshal.FinalReleaseComObject(currentComObj);
}
#endif
}
}
This would make the previous example a bit friendlier:
using (var prop = new ComObj<type>(obj.Property))
{
using (var propArray = new ComObj<type>(prop.ComObj[0]))
{
using (var propArrayReturn = new ComPtr<type>(propArray.ComObj.MethodThatReturnsAnotherObject()))
{
}
}
}
To avoid the ComObj
property, you could implement a proxy, but I'll leave that as an exercise. Specifically, make an efficient proxy generation instead of forwarding by reflection.