On the whole idea of giving away a reference to yourself at destruction

Raymond Chen

One of the responses to my discussion of how to give away a COM reference to yourself at destruction was roughly “The crazy convolutions required to accomplish this demonstrate how COM is a disaster.”

Well, some people may feel that COM is a disaster, but at least this specific piece of COM isn’t a disaster.

In a non-disastrous language, say, C++ with the standard library,¹ if somebody asked you, “How do I run some code when my shared_ptr reference count drops to 1?”, the answer is simple: You can’t do it! C++’s shared_ptr does not give you that level of access to the internal reference count. You can peek at the reference count by asking for the use_count(), but that is necessarily just an approximation due to multithreading. Even knowing that the use count is 1 doesn’t prove that you have the last shared_ptr. The reference count might go back up to 2 due to a race against a weak_ptr::lock(), and your attempt to detect when the reference count has dropped to 1 has failed.

In C# and Java, you are allowed to rescue an object in its finalizer, known as object resurrection. Java weak references expire before the object is submitted for finalization, so even if you rescue the object in its finalizer, it’s too late to preserve the weak references. C# weak references default to Java style, but you can ask for a “long” weak reference which retains its connection to the object even past finalization. Unfortunately, the object cannot control the types of weak references that people create, and if somebody creates a long weak reference, then they can access your object while it is finalizing! That is likely to result in unhappiness if the finalizer cleaned up external resources (that being the primary purpose of finalizers), since the object no longer has its external resources and consequently is unable to perform any useful operations.

Maybe the real problem is the design that required an object to hand out a reference to itself at destruction. That’s what forced us into the weird contortions. But at least it’s possible with COM. That’s more than can be said for other frameworks.

Bonus chatter: I perhaps did not note clearly enough that the intended design of the Closed event is that it is raised only the first time the last application reference is released. If the application rescues the object in its Closed event handler, and then subsequently releases the rescued object, the Closed event does not get raised again. The rules for the Windows Runtime is that the IClosable.Close method tells the object, “I have no further intention of using this object,” and further operations² will fail with RO_E_CLOSED.

The “did I already raise the Closed event?” flag is atomic because knowing that the reference count has dropped to 1 does not prove that only one thread can be using the object: A weak reference might increment the reference count back up, and then down to 1 a second time. C++/WinRT and C++/WRL objects support weak references by default, so this is something to worry about. If your framework doesn’t support weak references (or if you’ve disabled them), then the flag doesn’t need to be atomic because you know that there are no competing threads.

¹ Some people feel that C++ itself is also a disaster.

² With the exception of event handler unregistration and calling the IClosable.Close itself.

16 comments

Discussion is closed. Login to edit/delete existing comments.

  • Neil Owen 0

    I can agree that both COM and C++ are a disaster 😜, at least in the sense that the parts of C++ that necessitated the creation of COM are a disaster (such a lack of a binary standard). COMs main problem to me is that it tries to solve way too many problems with one giant solution (common binary interface, object lifetime, cross-process communication, cross-computer communication, threading models, etc). This makes it complicated, difficult to learn, and it’s so tied together and integrated it’s difficult for it to really move forward as other technologies and ideas have advanced. That said, Object Resurrection seems like a terrible idea, so supporting it isn’t really a point in it’s favor. The “this framework allows me to do really terrible things” isn’t really a great argument for it not being a disaster.

    • Stuart Ballard 0

      Seems to me the issue is that object resurrection is in the weird middle ground of being unsupported and (almost always) broken, but not actually impossible – and in particular, all too possible to do accidentally.

      If there were a way for an object to explicitly tell the framework/runtime/infrastructure something to the effect of “hey, I know I was about to destruct, but actually I changed my mind, resurrect me”, then the infrastructure could also provide well defined answers to all the corner cases and tradeoffs. Conversely, if the language had something like Rust’s language-level ownership and lifetime tracking, the compiler could enforce that resurrection can’t happen.

      Since neither of those things apply, we end up in a world where objects can get into this weird limbo half-destructed state that there’s no good way to resolve or escape from.

    • 紅樓鍮 0

      …the parts of C++ that necessitated the creation of COM are a disaster (such a lack of a binary standard)

      Many consider not constantly breaking the ABI to be one of C++’s original sins (search: std::regex), and many of them admire Rust’s complete lack of ABI stability outside of designated FFI types.

      Actually, most languages don’t have a stable ABI for non-FFI types; for those languages, the impact of the lack of a stable ABI is usually small because most libraries are open source and compiled from source (or at least an intermediate form, such as CIL for .NET).

    • Paulo Pinto 0

      What makes COM a disaster is Microsoft lack of willigness to provide good tooling for using it, similar to how C++ Builder extensions. There was a timid effort with C++/CX that was killed due to internal politics in name of “portability” for what is and will remain a Windows only technology.

      That after 30 years COM tooling still lags behing similar technologies on other platforms (XPC, AIDL, D-BUS), despite its surface exposure in Windows APIs, is quite telling on how Microsoft values the experience of using COM.

      The problem isn’t COM by itself, rather how its development experience has been managed.

    • gast128 0

      COM is a specification for component development. It isn’t a big specification either; the book ‘Inside COM’ from Dale Rogerson covers the most important aspects in a reader friendly way. Getting the reference counting correct used to be a challenge but with smart pointers (e.g. CComPtr) one can hardly go wrong these days. Apartments o.t.h. are still misty and counterproductive amplified by conflicting or confusing documentation.

      For me C++is not a disaster though I am not a fan of all the bells and whistles (e.g. fold expressions) added to it lately. Agree that a separate binary / component interface would be nice for the C++ standard though not sure how that works out with a heavy templated standard library. The C++ committee half-baked decision to freeze parts because of an implicit binary interface stops progression and bug fixing.

  • 紅樓鍮 0

    Rust has Arc::into_inner, which decreases the refcount but, if the refcount drops to 0, moves the value out of the heap (returning it to the caller) instead of destroying it. You can then do whatever you want with the value you’ve got. The object identity has changed (it has been moved out of where it was stored, and of course weakrefs have been disconnected), but in 99% of the cases you don’t care about object identity; you simply care that the value is still there and can be recycled for profit (if anything you do specifically requires that the value is inside an Arc, you can simply construct a new Arc and put the value back in).

    Arc::into_inner is almost exactly what I meant when I mentioned how, in theory, final_release could’ve been designed to be an object pooling mechanism. Of course, doing so soundly requires the weakref control block (WCB henceforth) to be disconnected, and a new WCB to be allocated and attached, so it’s really not the same COM object any more; but again, you usually don’t care about object identity.

    PS: Note how the requirement for object pooling with final_release is that the existing WCB needs to be disconnected, and then a new WCB attached; in reality, final_release already disconnects the existing WCB (it’s required for correctness), so what needs to be added is simply a new function

    namespace winrt {
      template <typename T> requires { /* ... */ }
      winrt::com_ptr<T>
      as_com(std::unique_ptr<T> the_thing_you_get_from_final_release);
    }

    which takes the std::unique_ptr<T> you get from final_release, attaches a new WCB, and returns a com_ptr which now points to a healthy (and new) COM object. final_release itself doesn’t actually need to be changed.

  • Joshua Hudson 0

    COM is a disaster.

    The only part of COM that should ever have existed was the fake COM created for Windows 95 while they were playing COM chicken; and the only reason that needs to exist is for shell plugins.

    The entire rest of COM is dead weight; and any time a developer encounters it it means Microsoft designed the API surface wrong. These random COM APIs for windowing functions (application icon/prevent pinning/multi-desktop support) should be replaced outright. Not wrapped, replaced. Make the COM API call the replacement, and the replacement live in User32.dll and not depend on any part of COM in any way.

    • Raymond ChenMicrosoft employee 0

      The examples you give (application icon, prevent pinning, multi-desktop) aren’t features of user32, so why should user32 export them?

      • Joshua Hudson 0

        1) Because accessing them shouldn’t impose a thread model that persists after the call finishes.

        2) Because it ought not to import shell32 into the calling process that wouldn’t otherwise have it, and definitely shouldn’t start importing any shell plugin dlls.

        3) Because the shell ought to be a replaceable component. It’s not the rock solid shell we used to have fifteen years ago. I should be able to actually use the registry key for replace shell to do something useful rather than render half of the windows configuration screens unusable and have no possibility of providing multi-desktop support.

        • Joe Beans 0

          The shell functionality should be a per-user service. Right now there are a bunch of things you can’t do unless explorer.exe is running as “the shell”. Particularly, UWP apps stop working and PIDL update events (SHChangeNotify) don’t get broadcast. Plus there’s something internal and weird about the way explorer cloaks windows to make “virtual desktops” work that can’t really be reproduced. Cheesy.

    • alan robinson 0

      While I mostly agree with you, your statement is very much unsubstantiated opinion. Giving some actual examples of why COM is the worst way to provide functionality would make your point a lot more compelling and debatable.

      Continuing the opinion based attacks on COM for a second*, the reason I don’t like it is how much work it is to call from vanilla C/C++ or even MFC as compared to the dead** simple win32 API.

      * yes I see the irony

      ** reports of it’s death are hopefully quite exaggerated.

      • Joshua Hudson 0

        It’s the memory model constraints. I’ve had too many cases of having to launch another thread or even another process because something was only exposed as a COM API. And then I have to link in a COM support library or a reference library that I simply don’t need but trying to get rid of is too much work. And now I’m calling APIs that don’t exist on server core so I have to deal with that too.

        The things like app pinning path/no pin should have been SetProp() not a shell COM invocation.

        Most of the Windows Update COM APIs should have been dism commands.

      • Joe Beans 0

        What really sucks is how COM is called from C#. On the one hand it’s nice that every typecast is an implicit QueryInterface call. On the other hand, the rules for calling Marshal.ReleaseComObject() are shady and usually result in crashes. And worse, you’re not allowed to use interface inheritance, so every time you define a derived interface you have to re-declare all the inherited methods with it.

      • Paulo Pinto 0

        C++/CX was a sweet spot, finally Microsoft had something that could rival C++ Builder in productivity for Windows desktop applications in C++, including COM authoring.

        It was killed by a group of folks clamouring language extensions are bad, for a technology that isn’t portable to other platforms to start with, and strangely the same group doesn’t have any issues using language extensions on other C++ compilers.

    • Joe Beans 0

      COM has been used to publish undocumented features as interfaces that have to be discovered using other means than simply dumping DLL exports. Two examples that come to mind are IPolicyConfig and IApplicationView.

  • Dmitry 0

    After reading that ”C++ with the standard library is a non-disastrous language” I was going to write that at least 7 billion people would argue that and Raymond sometimes has not only social skills of a thermonuclear device (as we all now from his blog posts) but sense of humour of the same kind as well.

    And then I saw the footnote.

Feedback usabilla icon