RCW & reference counting when using COM interop in

2019-01-13 05:12发布

问题:

I have an application that uses Office interop assemblies. I am aware about the "Runtime Callable Wrapper (RCW)" managed by the runtime. But I am not very sure how the reference count gets incremented. MSDN says,

RCW keeps just one reference to the wrapped COM object regardless of the number of managed clients calling it.

If I understand it correctly, on the following example,

using Microsoft.Office.Interop.Word;

static void Foo(Application wrd)
{
    /* .... */
}

static void Main(string[] args)
{
    var wrd = new Application();
    Foo(wrd);
    /* .... */
}

I am passing the instance wrd to another method. But this doesn't increment the internal reference count. So I am wondering on what scenarios the reference count gets incremented? Can anyone point out a scenario where the reference count gets incremented?

Also I read some blog which says avoid using double dots when programming with COM objects. Something like, wrd.ActiveDocument.ActiveWindow. The author claims that compiler creates separate variables to hold the values which will increment the reference counter. IMHO, this is wrong and the first example proves this. Is that correct?

Any help would be great!

回答1:

I have been researching this question too, working on a COM/.Net-Interop-centric application, fighting leaks, hangs and crashes.

Short answer: Every time the COM object is passed from COM environment to .NET.

Long answer:

  1. For each COM object there is one RCW object [Test 1] [Ref 4]
  2. Reference count is incremented each time the object is requested from within COM object (calling property or method on COM object that return COM object, the returned COM object reference count will be incremented by one) [Test 1]
  3. Reference count is not incremented by casting to other COM interfaces of the object or moving the RCW reference around [Test 2]
  4. Reference count is incremented each time an object is passed as a parameter in event raised by COM [Ref 1]

On a side note: You should ALWAYS release COM objects as soon as you are finished using them. Leaving this work to the GC can lead to leaks, unexpected behavior and event deadlocks. This is tenfold more important if you access object not on the STA thread it was created on. [Ref 2] [Ref 3] [Painful personal experience]

I'm hope I have covered all cases, but COM is a tough cookie. Cheers.

Test 1 - reference count

private void Test1( _Application outlookApp )
{
    var explorer1 = outlookApp.ActiveExplorer();
    var count1 = Marshal.ReleaseComObject(explorer1);
    MessageBox.Show("Count 1:" + count1);

    var explorer2 = outlookApp.ActiveExplorer();
    var explorer3 = outlookApp.ActiveExplorer();
    var explorer4 = outlookApp.ActiveExplorer();

    var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer4);
    var count2 = Marshal.ReleaseComObject(explorer4);
    MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals);
}
Output:
Count 1: 4
Count 2: 6, Equals: True

Test 2 - reference count cont.

private static void Test2(_Application outlookApp)
{
    var explorer1 = outlookApp.ActiveExplorer();
    var count1 = Marshal.ReleaseComObject(explorer1);
    MessageBox.Show("Count 1:" + count1);

    var explorer2 = outlookApp.ActiveExplorer();

    var explorer3 = explorer2 as _Explorer;
    var explorer4 = (ExplorerEvents_10_Event)explorer2;
    var explorerObject = (object)explorer2;
    var explorer5 = (Explorer)explorerObject;

    var equals = explorer2 == explorer3 && ReferenceEquals(explorer2, explorer5);
    var count2 = Marshal.ReleaseComObject(explorer4);
    MessageBox.Show("Count 2:" + count2 + ", Equals: " + equals);
}
Output:
Count 1: 4
Count 2: 4, Equals: True

Sources I relay on in addition to my experience and testing:

1. Johannes Passing's - RCW Reference Counting Rules != COM Reference Counting Rules

2. Eran Sandler - Runtime Callable Wrapper Internals and common pitfalls

3. Eran Sandler - Marshal.ReleaseComObject and CPU Spinning

4. MSDN - Runtime Callable Wrapper



回答2:

I haven't seen the code for the RCW -- not even sure it's part of the SSCLI -- but I had to implement a similar system for tracking COM object lifetime in SlimDX and had to do a fair bit of research into the RCW. This is what I remember, hopefully it's reasonably accurate but take it with a touch of salt.

When the system first sees a COM interface pointer, it just goes to a cache to see if there is an RCW for that interface pointer. Presumably the cache would be using weak references, so as not to prevent finalization and collection of the RCW.

If there is a live wrapper for that pointer, the system returns the wrapper -- if the interface was obtained in a fashion that incremented the interface's reference count, presumably the RCW system would call Release() at this point. It has found a live wrapper, so it knows that wrapper is a single reference and it wants to maintain exactly one reference. If there is no live wrapper in the cache, it creates a new one and returns it.

The wrapper calls Release on the underlying COM interface pointer(s) from the finalizer.

The wrapper sits between you and the COM object, and handles all parameter marshaling. This also allows allow it to take the raw result of any interface method that is itself another interface pointer and run that pointer through the RCW caching system to see if it exists yet before returning you the wrapped interface pointer.

Unfortunately I don't have a good understanding of how the RCW system handles proxy object generation for sending stuff across application domains or thread apartments; it wasn't an aspect of the system I needed to copy for SlimDX.



回答3:

You shouldn't need any special treatment. The runtime only keeps one reference to the COM object. The reason for this is that the GC tracks all managed references, so when the RCW goes out of scope and is collected, the COM reference is released. When you pass around a managed reference, the GC is tracking it for you - this is one of the biggest advantages of a GC-based runtime over the old AddRef/Release scheme.

You don't need to manually call Marshal.ReleaseComObject unless you want more deterministic release.



回答4:

The accepted solution is valid, but here's some additional background information.

A RCW contains one or more native COM object interface references internally for its COM object.

When a RCW releases its underlying COM object, either due to getting garbage collected or due to Marshal.ReleaseComObject() getting called on it, it releases all of its internally held COM object interfaces.

There are actually many reference counts here - one determining when .NET's RCW should release its underlying COM object interfaces, and then each of those raw COM interfaces has its own reference count as in regular COM.

Here's code to get raw COM IUnknown interface reference count:

int getIUnknownReferenceCount(object comobject)
{
    var iUnknown = Marshal.GetIUnknownForObject(comObject);
    return Marshal.Release(iUnknown);
}

And you can get the same for the object's other COM interfaces using Marshal.GetComInterfaceForObject().

In addition to the ways listed in the accepted solution, we can also increase the .NET RCW reference count artificially by calling something like Marshal.GetObjectForIUnknown().

Here's example code making use of that technique to get a given COM object's RCW reference count:

int comObjectReferenceCount(object comObject)
{
    var iUnknown = Marshal.GetIUnknownForObject(comObject);
    Marshal.GetObjectForIUnknown(iUnknown);
    Marshal.Release(iUnknown);
    return Marshal.ReleaseComObject(comObject);
}


回答5:

You need to call Marshal.ReleaseComObject on your wrd variable to release your reference to the word application.

That way if Word is not visible and you shut your application, the exe will unload as well unless you have made it visible to the user.