Short Version
As soon as my COM automation server starts up, and registers its class objects, i check if there are any references that need to keep me alive. There aren’t. So i shutdown – before anything gets a chance to happen.
What is the pattern to stick around long enough without any references to any of my objects, but then not stick around after there are no longer any references to any of my objects?
Long Version
If you are an out-of-process COM automation server, you have a number of goals. Assume we are implementing a Single-Threaded Apartment (STA) server; meaning we will be pumping a message loop.
My sample implemenatation i think follows the standard route from our WinMain
:
Note: I will use a C#-like syntax, but i am not referring to managed code. I’m showing you code that looks like C# so you don’t have to see my native Delphi. Additionally I am asking a language-agnostic question; just like COM is desgined to be language-agnostic – that’s its entire reason for living – binary interoperability
-
Use CoRegisterClassObject to register my “class object”; which is an object that implements
IClassFactory
:// Create an instance of our class factory IClassFactory classFactory = new ApeFactory(); // Initialize the COM infrastructure CoInitialize(null); // Register our class factory, and keep the cookie to unregister it later DWORD dwCookie; HRESULT hr = CoRegisterClassObject(classFactory, out dwCookie);
This class object will then be called upon to create our Ape object.
Our
IClassFactory
does not implement reference counting with itsAddRef
/Release
methods. This is because for the duration that the COM Service Manager (CSM) is holding onto a reference to our ApeFactory our application can’t shutdown (because someone’s using one of our objects). But the CSM won’t release the references to our class objects until we call CoRevokeClassObject. This catch-22 is whyIExternalConnection
was created:IExternalConnection = interface { // Increments the count of an object's strong external connections. DWORD AddConnection(DWORD extconn, DWORD reserved); // Decrements the count of an object's strong external connections. DWORD ReleaseConnection(DWORD extconn, DWORD reserved,BOOL fLastReleaseCloses); }
This interface lets us:
- differentiate between “external” people referencing our objects
- as opposed to just COM holding onto our objects
We only really need to keep our automation server alive if there is some external client somewhere who got ahold of our
IClassFactory
. If it’s just COM holding onto our object, then we can shutdown fine.So COM will tell us when some “external” user is referencing our class factory. This means we can then maintain a global lock count:
Int64 g_ModuleReferenceCount;
Then perform an atomic increment during AddConnection, and an atomic decrement during ReleaseConnection.
-
Start our message loop
MSG msg; while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(ref msg); DispatchMessage(ref msg); // check if our external reference counts have gone to zero; meaning we can shutdown if (g_ExternalReferenceCount <= 0) PostQuitMesage(msg.wParam); }
-
Unregister our class objects during our orderly shutdown
// Unregister our ApeFactory IClassFactory CoRevokeClassObject(dwCookie);
-
And our automation server gracefully shuts down.
Except for the race condition
If you look at the code together you can see the race condition:
void WinMain()
{
// Register our class factory, and keep the cookie to unregister it later
DWORD dwCookie;
HRESULT hr = CoRegisterClassObject(classFactory, out dwCookie);
// Setup our count of external references
Int64 g_ModuleReferenceCount = 0;
MSG msg;
while (GetMessage(&msg, 0, 0, 0))
{
TranslateMessage(ref msg);
DispatchMessage(ref msg);
// check if our external reference counts have gone to zero; meaning we can shutdown
if (g_ExternalReferenceCount <= 0)
PostQuitMesage(msg.wParam);
}
// Unregister our ApeFactory IClassFactory
CoRevokeClassObject(dwCookie);
}
Our module reference count starts at zero, and so before COM has a chance to even create our ApeFactory class object, we’re unregistering the class factory and shutting down.
What is the pattern to employ here to stick around at least long enough to server some COM classes and COM objects?
“What about LockServer?”
I’m not muddying the waters with LockServer. As Don Box noted:
Although the technique of using
IExternalConnection
on class objects has worked since the early days of COM, few implementors actually used this technique. Instead, most servers would typically ignore outstanding external references to class objects and prematurely terminate their server processes. This situation was encouraged by the presence of the methodLockServer
on theIClassFactory
interface, which lulled developers into thinking that clients could actually ensure that a server would remain running. While most server implementors would properly lock their module in theirLockServer
methods, there was no reliable way for a client to call this method.Acknowledging that many server implementations were not using
IExternalConnection
to manage server lifetime properly, the Windows NT 4.0 release of COM introduced the following enhancement to compensate for these naive implementations:When marshaling a reference to a class object in response to a
CoGetClassObject
call, the SCM will call the class object’sIClassFactory::LockServer
method. Since the vast majority of servers implementIClassFactory
on their class objects, this enhancement to the COM runtimes repairs most defects. However, if a class object does not export theIClassFactory
interface or if the server must also execute on pre-Window NT 4.0 releases of COM, theIExternalConnection
technique must be used.
I want to pretend that LockServer is not an option because:
- a) i want to underststand the correct way to do it
- b) i don’t understand how LockServer doesn’t have the same race condition
https://learn.microsoft.com/en-us/windows/win32/api/combaseapi/nf-combaseapi-coregisterclassobject