keeping RCW afloat

I

Igor R.

Hello,

In my C# project I use a COM-based SDK. Some of its COM objects
generate events.
Assume the following scenario (all the objects below are from the
SDK):

var child = new SDKLib.Child();
child.onSomeEvent += () => { doSomething() };
someParent.setChild(child);

In the above code "child" COM object is held by "someParent" - by
design, preventing it from getting disposed. *But*, IUUC, no one holds
its RCW, because "child" variable is local. So when "child" goes out
of scope, it's GC-ed, which in turn causes it to Unadvise() from the
event source. Thus, after a few seconds "onSomeEvent" stops firing.
Certainly, I can create a List<Object>, which would hold all these
objects - just to prevent their garbage-collection, but maybe there's
a more elegant way to workarnoud this issue?

Thanks.
 
A

Alberto Poblacion

Igor R. said:
In my C# project I use a COM-based SDK. Some of its COM objects
generate events.
Assume the following scenario (all the objects below are from the
SDK):

var child = new SDKLib.Child();
child.onSomeEvent += () => { doSomething() };
someParent.setChild(child);

In the above code "child" COM object is held by "someParent" - by
design, preventing it from getting disposed. *But*, IUUC, no one holds
its RCW, because "child" variable is local. So when "child" goes out
of scope, it's GC-ed, which in turn causes it to Unadvise() from the
event source. Thus, after a few seconds "onSomeEvent" stops firing.
Certainly, I can create a List<Object>, which would hold all these
objects - just to prevent their garbage-collection, but maybe there's
a more elegant way to workarnoud this issue?

Have you actually tried this out? In theory, the Garbage Collector does
NOT free objects that are reachable, either directly or indirectly. That is,
if someParent has a reference to child, and child has a reference to the
RCW, then neither child nor the RCW will be Collected even if child has gone
out of scope, because both objects are still reachable (through someParent).
 
I

Igor R.

    Have you actually tried this out?

Sure, I did. I observed a weird situation: the object stopped firing
events few seconds after it was created. Then I had to debug the ATL
connection-points implementation to realize what actually happened.
In theory, the Garbage Collector does NOT free objects that are reachable, either directly or indirectly.

That's the point - the "child" is NOT accessible. The accessible
entity is the underlying COM object, but it's not managed by .NET
anyway.
That is, if someParent has a reference to child, and child has a reference to the RCW

It seems that the child doesn't reference the RCW. IIUC, the reference
is one-way: RCW references the COM object, but not vice versa.
 
A

Alberto Poblacion

Igor R. said:
That's the point - the "child" is NOT accessible. The accessible
entity is the underlying COM object, but it's not managed by .NET
anyway.

But the .Net code never references the COM object. It only references
the RCW, which *is* a .Net assembly and is subject to Garbage Collection.
When you do this:

var child = new SDKLib.Child();

"child" is not the COM object, it is a reference to an instance of the RCW
that wraps the COM object.
 
P

Peter Duniho

Igor said:
Hello,

In my C# project I use a COM-based SDK. Some of its COM objects
generate events.
Assume the following scenario (all the objects below are from the
SDK):

var child = new SDKLib.Child();
child.onSomeEvent += () => { doSomething() };
someParent.setChild(child);

In the above code "child" COM object is held by "someParent" - by
design, preventing it from getting disposed. *But*, IUUC, no one holds
its RCW, because "child" variable is local. So when "child" goes out
of scope, it's GC-ed, which in turn causes it to Unadvise() from the
event source. [...]

Frankly, your code example is so sparse and vague, it's hard to know
what you're talking about.

But, let's assume that the object returned by "SDKLib.Child()" is a COM
object wrapped in RCW. Let's further assume that
"someParent.setChild(child)" for whatever reason does _not_ retain a
reference to the RCW object that was returned by "SDKLib.Child()"
(perhaps it itself is an RCW, and thus the only thing it internally is
retaining is the unmanaged COM interface reference).

If all that's true, then yes…without any references to the wrapper
object, it will be GC'ed, and the COM object released when that happens.

As for the solution, it's the same as if you had a managed object with
an event that you subscribed to. You have to keep a reference to it,
otherwise the object with the event will be collected. Why should the
RCW be any different? .NET doesn't have any effective way to track the
lifetime of the underlying COM object; all it can do is manage the
managed object.

If you want your managed objects to stay alive, you need to keep
references to them.

Pete
 
I

Igor R.

But, let's assume that the object returned by "SDKLib.Child()" is a COM object wrapped in RCW.

Right. I deal with COM-objects in C#, so all the entities in my
example are COM-objects as they're seen from within .NET (i.e. RCWs).
 Let's further assume that "someParent.setChild(child)" for whatever reason does _not_ retain a reference to the RCW object that was returned by "SDKLib.Child()"
(perhaps it itself is an RCW, and thus the only thing it internally is retaining is the unmanaged COM interface reference).
Exaclty.

If all that's true, then yes…without any references to the wrapper object, it will be GC'ed, and the COM object released when that happens.

The COM object is not released, as it's referenced internally by
"someParent" (i.e. by its underlying COM object) after I apply
setChild(). But the wrapping RCW is really GC-ed.
As for the solution, it's the same as if you had a managed object with anevent that you subscribed to.  You have to keep a reference to it, otherwise the object with the event will be collected.  Why should the RCW be any different?

Ok, I see... That's what I'm doing.

I just thought that maybe there exists some way to reference the RCW
*inside* setChild() method. I.e. I'd like to cause the following
expression to increase child's RCW reference count:
aParent.setChild(child) -- where aParent itself is a RCW of a COM
object and setChild is COM method.
By the way, what if "child" were a regular C# object exposing a COM
interface? IIUC, then setChild() would receive its CCW, and the CCW
would reference the underlying .NET object -- right? So it wouldn't
get GC-ed! What I want is to get a similar effect with RCW.
What I don't quite understand is what happens exactly when I pass
"child" RCW to the setChild() method. Inside setChild, do I still have
an access to the "child" RCW, or it's "unwrapped", and the "naked" COM
object is passed as a parameter?
 
P

Peter Duniho

Igor said:
[...]
The COM object is not released, as it's referenced internally by
"someParent" (i.e. by its underlying COM object) after I apply
setChild(). But the wrapping RCW is really GC-ed.

It's released by the RCW (as in, the RCW calls the IUnknown.Release()
method). Just because its reference count didn't reach zero when it was
released doesn't mean it wasn't released.
[...]
I just thought that maybe there exists some way to reference the RCW
*inside* setChild() method. I.e. I'd like to cause the following
expression to increase child's RCW reference count:
aParent.setChild(child) -- where aParent itself is a RCW of a COM
object and setChild is COM method.

..NET doesn't use reference counting. The RCW doesn't itself have a
reference count to be increased. The COM object's reference count may
be increased by the setChild() method itself, but that only affects the
COM object itself, not the RCW that is wrapping it.
By the way, what if "child" were a regular C# object exposing a COM
interface? IIUC, then setChild() would receive its CCW, and the CCW
would reference the underlying .NET object -- right?

Yes. Going the other way, with a managed implementation of a COM object
referenced outside managed code, obviously the reference count from
outside managed code must affect the lifetime of the managed implementation.
So it wouldn't
get GC-ed! What I want is to get a similar effect with RCW.

The "similar effect" follows the same rules for any other managed
object: you have to keep a reference to it somewhere.
What I don't quite understand is what happens exactly when I pass
"child" RCW to the setChild() method. Inside setChild, do I still have
an access to the "child" RCW, or it's "unwrapped", and the "naked" COM
object is passed as a parameter?

The unmanaged COM object's setChild() method will see only the unmanaged
COM object that was wrapped by the RCW. .NET's COM marshaling has
"unwrapped" the object by that point in the call chain. It cannot be
any other way; after all, what would unmanaged code expecting a plain,
unmanaged COM interface instance do with a managed object reference?

Pete
 
I

Igor R.

It's released by the RCW (as in, the RCW calls the IUnknown.Release()
method). Just because its reference count didn't reach zero when it was
released doesn't mean it wasn't released.

Ah, sorry, by saying "released" I meant "totally released", i.e. freed/
disposed.

The unmanaged COM object's setChild() method will see only the unmanaged
COM object that was wrapped by the RCW. .NET's COM marshaling has
"unwrapped" the object by that point in the call chain. It cannot be
any other way; after all, what would unmanaged code expecting a plain,
unmanaged COM interface instance do with a managed object reference?

Ok, I see, that's the way it works... Too bad!
But IMHO it *could* (should?) behave another way. After all, the whole
purpose of all the RCW/CCW machinery is to make the COM <--> .NET
interaction as transparent and seemless to the user as possible. And
what we get now? Assume there're 2 COM classes: Class1 is from an
unmanaged COM server and Class2 is a .NET object - both exposing
*exactly* the same COM interface defined in the unmanaged SomeLib.

IMyInterface class1 = new SomeLib.Class1();
IMyInterface class2 = new Class2();

And an unmanaged Container COM class:

IContainer container = new SomeLib.Container();

Now, what a surprise:

container.add(class1); // class1 will be GC-ed soon
container.add(class2); // class2 will be alive!

IMHO, such a behaviour is not transparent and not intuitive. It
requires from the user to understand all the CCW/RCW machinery, and to
adjust his code accordingly.

But it could be done another way. If RCW is a managed wrapping object
that exposes the relevant interface (or pretends so), then why not to
wrap it with CCW, just like any other managed COM-object? If class1
would be wrapped with CCW *on the top* of RCW, when passing it to an
unmanaged code, the whole mess would be avoided!
 
P

Peter Duniho

Igor said:
[...]
Ok, I see, that's the way it works... Too bad!
But IMHO it *could* (should?) behave another way. After all, the whole
purpose of all the RCW/CCW machinery is to make the COM <--> .NET
interaction as transparent and seemless to the user as possible.

I'd say "as practical". But sure, it's there to help you.
And what we get now?

Something that is entirely consistent with the rest of .NET.
Assume there're 2 COM classes: Class1 is from an
unmanaged COM server and Class2 is a .NET object - both exposing
*exactly* the same COM interface defined in the unmanaged SomeLib.

IMyInterface class1 = new SomeLib.Class1();
IMyInterface class2 = new Class2();

And an unmanaged Container COM class:

IContainer container = new SomeLib.Container();

Now, what a surprise:

container.add(class1); // class1 will be GC-ed soon
container.add(class2); // class2 will be alive!

IMHO, such a behaviour is not transparent and not intuitive. It
requires from the user to understand all the CCW/RCW machinery, and to
adjust his code accordingly.

I disagree. The behavior is entirely intuitive.

The whole point of a GC system is for the GC to clean up objects that,
in the specific context of the memory managed by the GC, are no longer
needed.

In your example above, there is nothing left referencing the managed
object for "class1". Thus, it's no longer needed and the GC can clean
it up. For "class2", there is still something left referencing the
managed object, and thus it _is_ still needed and does not get cleaned up.
But it could be done another way. If RCW is a managed wrapping object
that exposes the relevant interface (or pretends so), then why not to
wrap it with CCW, just like any other managed COM-object?

Because the RCW isn't there to provide access to a managed
implementation of the COM interface. It's there to provide managed
access to an _unmanaged_ implementation of the COM interface. The RCW
needs to exist only as long as there is managed code referring to it,
because the only reason the RCW exists at all is to provide a conduit
from managed code to the unmanaged interface.

Also, the idea of marshaling COM calls through TWO layers of wrapper
rather than one, just so you can have your RCW lifetimes match that of
the unmanaged object seems rather silly to me. That's a significant
performance hit, to accomplish something your own code really ought to
be doing itself anyway (and which it has to for managed objects that
provide the exact same kind of features anyway!)
If class1
would be wrapped with CCW *on the top* of RCW, when passing it to an
unmanaged code, the whole mess would be avoided!

IMHO, the only "mess" here is your expectation that a managed object
that is used only by your managed code should somehow be magically
preserved without your managed code explicitly referring to it.

Consider this code, containing nothing involving COM at all:

class A
{
public event EventHandler Event;
}

class B
{
void Method()
{
new A().Event += Handler;
}

void Handler(object sender, EventArgs e) { }
}

How long do you think the instance of A allocated in B.Method() should
live when B.Method() is called?

I know how long _I_ think it should live: no longer than the next
collection cycle.

This is the exact same scenario you are dealing with.

Pete
 

Ask a Question

Want to reply to this thread or ask your own question?

You'll need to choose a username for the site, which only take a couple of moments. After that, you can post your question and our members will help you out.

Ask a Question

Top