(Began sometime in the middle of 2008, though we are still “struggling valiantly”)
A coworker and I have been struggling (valiantly, I would say) to solve some rather hairy lifetime-related issues at work this week. A sizable portion of our current work involves the interop between managed (C#) and native (C++/COM) code, which, well, doesn’t work out all that well in sufficiently complicated scenarios.
Not that this is an easy problem to solve, mind you. The problem is one of semantics.
Bring on the analogies!
Let’s pick a suitable analogy. I’m going to go with, uh, urinating – I find that urination is a suitable reference for a lot of things in life.
First off, sorry ladies, but I’m going to be talking about urinals here, cause they produce a much more exciting mental image.
First off, we have your normal urinal – you walk up to it to take a piss, then you flush the toilet when you are done. If you forget to flush, your nasty sits around for hours, stinking up the bathroom.
This is like the C model of explicit memory management. The person who allocates the memory (which, I suppose, we can think of as the act of walking up to the urinal and unzipping your fly) is in charge of releasing it when you are done.
In fact, as a side note, I think once we do get USB devices that create smells, we should hook up the device as a debugger for, say, a CLR application – as more memory makes its way into the gen 2 heap, it stinks worse and worse. It would certainly push people to flush, don’t you think?
Now, let’s assume that instead of your normal bathroom having something like 3 urinals, it has one wide urinal that everyone has to use. Also, since we don’t want the urinal to flush while people are using it, we have a new flushing mechanism – when you walk up to the urinal, you pull a lever to inform it that you’ve arrived, and when you leave, you pull the flush lever. However, the flush lever will only flush once everyone has pushed it.
You can imagine how awkward this is going to be – everyone has to remember to pull a lever when they enter and leave, and that’s not gonna be easy. If one person forgets to pull the lever when they enter, either they are gonna get flushed on (if everyone else finishes first), or if they finish first and pull the flush lever, someone else is gonna get flushed on. Vice versa, if they pull the “I’m here” lever but forget to pull the flush lever, the urinal is never going to flush.
And that is how reference counting works. And those problems are literally the same as what happens in reference counted scenarios – people forget to pull the “I’m here” lever (AddRef in COM land) or the flush lever (Release in COM), and everyone gets screwed cause one person forgot something.
Now, back to urinals, and this time we’re going to go upscale – urinals with automatic flushers! That’s right – when you walk away from it, it flushes (eventually – sometimes it takes awhile). Even in the scenario with a few people using the same über-urinal, until everyone leaves, the urinal won’t flush, because it still senses that people are using it.
Welcome to garbage collection. The only real problem we run into in this case is if some asshole decides to have a phone conversation while standing in front of big pappa urinal, in which case it will never flush. He’s not using it anymore, he just forgot to walk away.
Wow, that was disgusting
Yes, yes it was. Moving along.
The problem with interop is that it mixes reference counting and garbage collection (henceforth, to relate it to C#/COM, I’m going to refer to reference counting as the “native world” and garbage collection as the “managed world”, which is how it tends to map out). This isn’t necessarily a problem from the perspective of the native component holding on to managed components – they won’t disappear early, but they will disappear at some point after you release them, by which time you don’t really care about them anymore.
But from the managed side, there is a slight problem. Your native stuff wants to get Released when you are done with it, except that you don’t really know when you are done; after all, this garbage collection thing is implicit, and you’ll just get flushed whenever people are done with you. That’s not too big of a deal, though, I suppose.
Oh, but it gets worse.
In order for garbage collection to really work, the garbage collector has to have a way of detecting and breaking cycles. If A references B and B references A, but nobody else references either A or B, then they can both get collected. Otherwise, you would end up with “unreachable” memory that would never go away, and that would certainly be a memory leak.
To digress yet again, when I use the term “memory leak”, I mean it in the following sense:
A memory leak is when something that is no longer “reachable” from some root of memory does not have its resources freed.
So you can’t really have memory leaks in managed code, per se, unless you replace “reachable” with “used by anything”. If you stick a reference to something in a static field someplace, it is probably still reachable, but if you never intend it to use it again, it is a memory leak at the semantic of your program. In other words, while the memory has not technically leaked, your program is holding on to certain resources unnecessarily.
Back to it!
Icky cycles
That is my official term for cycles that cross the managed/native boundary. The CLR can detect cycles within your managed resources, but what about once they cross over into the native world?
The answer is that the CLR is at a loss:
- Managed component M is holding on to a COM component C
- C is holding on to a different managed component, N
- N, in turn, has a reference to M
That’s the way the world looks when you can see both the managed and native sides, but what does each side see?
The managed side:
- N is held in memory by some reference called C (which happens to come from a COM-Callable Wrapper, or CCW, which was created on the native side so C could treat N like a regular COM component)
- M is held in memory by N
So, both will get freed whenever this C reference goes away.
The native side (really, just C):
- C’s reference count will go to 0 whenever this thing, M,
Releases it. This reference happens to come from an Runtime Callable Wrapper (RCW) created from the managed side, so M can treat C like a normal managed object.
And you can see the problem.
So what do you do?
Cry, mostly.
Really, it is worth crying about, because anytime you end up with cycles like this, you have to break it manually, and that means making your managed code not-so-garbage-collected. You use things like IDisposable, or add other explicit lifetime management to your managed objects.
In other words, one of the reasons to use managed code is because you don’t have to do your own memory management, but as soon as you have some type of cycle between managed and native code, then you do anyways. Effectively, if you think of there being a hierarchy of managing memory, with garbage collection at the top, COM in the middle, and C at the bottom, in any type of mixed scenario, you generally have to program to the lowest common denominator. In the simplest of scenarios (e.g. no cycles in managed/native code in the example above), you can still be a bit ignorant of what is going on underneath, but as soon as you end up with some type of hybrid scenario, you are really working at the lowest level in the hybrid.
Bleh.