I have rephrased this question.
When .net objects are exposed to COM Clients through COM iterop, a CCW (COM Callable Wrapper) is created, this sits between
I haven't verified this, but here is what I would try:
First, here is a CBrumme Blog article about the clr's default implementation of IMarshal. If your utilities are used across COM apartments you won't get proper com behavior from a direct port of VB6 to the CLR. Com objects implemented by the CLR act as if they aggregated the free threaded marshaller rather than apartment threaded model that VB6 exposed.
You can implement IMarshal (on the clr class you are exposing as a com object). My understanding is that will allow you control over creating the COM proxy (not the interop proxy). I think this will allow you to trap the Release calls in the object you returned from UnmarshalInterface and signal back to your original object. I'd wrap the standard marshaller (e.g. pinvoke CoGetStandardMarshaler) and forward all calls to it. I believe that object will have a lifetime tied to the lifetime of the the CCW.
again ... this is what I'd try if I had to solve it in C#.
On the other hand, would this kind of solution really be easier than implementing in ATL? Just because the magic part is written in C# doesn't make the solution simple. If what I propose above does solve the problem, you'll need to write a really big comment explaining what was going on.