Lifecycle Management and Marshaling

Earlier in this chapter, the relationship between the stub manager and the object was discussed. A stub manager is created the first time CoMarshalInterface is called on a particular object identity. The stub manager holds outstanding references to the object it represents, and the stub manager stays alive as long as there is at least one outstanding external reference to the stub. These external references are usually proxies, although marshaled object references are counted as well, as they represent potential proxies. When all external references to a stub manager are destroyed, the stub manager deletes itself and releases all references it holds on the actual object. This default behavior exactly simulates the normal in-process semantics of AddRef and Release. Many objects do not have any special lifecycle requirements and are perfectly satisfied with this default behavior. Some objects wish to customize the relationships between external references, the stub manager, and the object. Fortunately, COM provides ample hooks into the lifecycle of the stub manager, to allow a variety of policies to be implemented. To understand how stub lifecycle management works, it is first necessary to examine COM’s distributed garbage collection algorithm.

When a stub manager is created, its Object Identifier (OID) is registered with COM’s distributed garbage collector, which currently is implemented by the OXID Resolver service. The OR keeps track of which OIDs are exported from each apartment on the local host machine. When a proxy manager is created, CoUnmarshalInterface informs the local OR that an object reference is being imported into an apartment. This means that the local OR also knows which OIDs have been imported by each apartment on the local host machine. When a particular OID is first imported on a host machine, the importing host’s OR establishes a ping relationship with the exporting host’s OR. The import-side OR will then send a periodic ping message via RPC, indicating that the importing host machine is still operational and is reachable on the network. The current implementation sends this ping message once every two minutes. If no additional OIDs have been imported in the last ping interval, then a simple ping notification is sent. If new references have been imported or existing references have been released, then a more complex ping message will be sent, indicating the delta between the previous set of held references and the current set.

Under the Windows NT 4.0 implementation of COM, if three consecutive ping intervals (six minutes) elapse without receiving a ping notification from a particular host, the OR will assume that the host has either crashed or become unreachable due to a network failure. At this time, the OR will inform each stub manager that had been imported by the now-dead host that the outstanding references are now invalid and should be released. If a particular object had been used only by clients on the now-dead host, then the stub manager will have no more outstanding references and will delete itself, which in turn will release the COM references to the object.

The previous scenario describes what happens when a host machine becomes unreachable on the network. The more interesting scenario is what happens when a process prematurely exits while holding outstanding proxies. If a process exits without calling CoUninitialize the appropriate number of times (e.g., the process simply crashes), the COM library has no chance to clean up the leaked references. When this happens, the local OR will eventually detect the death of the process and will release all of its imported references when it sends the next ping message to the exporter’s OR. If the process held imported references to objects on the local machine, these can be released soon after detecting the death of the client.14

The COM distributed garbage collector is sometimes criticized for being inefficient. In reality, if objects need to detect client liveness reliably, the COM approach is likely to be much more efficient than ad hoc application-specific solutions. This is because COM’s garbage collector can aggregate keep-alive messages for all references held on a particular machine into a single periodic message. Application-specific techniques do not have the same global knowledge and would likely result in a different keep-alive handshake per application, not per host machine. For scenarios in which COM’s garbage collector actually affects performance, pinging can be disabled for a particular stub manager by using the MSHLFLAGS_NOPING flag; however, the default behavior of the garbage collector is suitable for most applications and will outperform many application-specific solutions.

The stub manager keeps track of how many external references are outstanding. When the stub is created, this count begins at zero. When a call to CoMarshalInterface is made using MSHLFLAGS_NORMAL, this count is increased by some number n that is written to the marshaled object reference. When the proxy manager unmarshals the reference, it adds n to its count of held references. If CoMarshalInterface is called on the proxy manager to pass a copy of the reference to another apartment, the proxy manager is free to give out some number of references in order to initialize the second proxy. If a proxy has only one remaining reference, it must go back to the stub manager to request additional references.

It is often useful to store marshaled interface references in a central location that can be accessed by one or more clients. The Running Object Table used by some moniker implementations is the canonical example of this. If the marshaled interface pointer were to be created using MSHLFLAGS_NORMAL, then only one client could ever unmarshal the object reference. If multiple clients are expected to unmarshal the object reference, then the reference needs to be marshaled using either MSHLFLAGS_TABLESTRONG or MSHLFLAGS_TABLEWEAK. In either case, the marshaled object reference can be unmarshaled multiple times.

The difference between strong and weak table marshaling revolves around the relationship between the marshaled object reference and the stub manager. When a marshaled object reference is created using MSHLFLAGS_ TABLEWEAK, the external reference count in the stub manager is not incremented. This means that the marshaled object reference will contain zero references, and each proxy manager will need to contact the stub manager to acquire one or more external references. Because the weak table-marshaled reference does not represent a counted external reference on the stub manager, when the last proxy manager disconnects from the stub manager, the stub manager will destroy itself and, of course, release any held COM references on the object. If no proxy managers ever connect to the stub manager, the stub manager will remain alive indefinitely. The net effect is that the outstanding marshaled object reference does not force the lifecycle of the stub manager or the object to remain alive. Conversely, when a marshaled object reference is created using MSHLFLAGS_TABLESTRONG, the external reference count in the stub manager is incremented. This means that the marshaled object reference represents a counted external reference on the stub manager. As with a weak table marshal, each proxy manager will need to contact the stub manager to acquire one or more additional external references. Because the strong table-marshaled reference does represent an external reference count on the stub manager, when the last proxy manager disconnects from the stub manager, the stub manager will not destroy itself and will in fact continue to hold COM references on the object. The net effect of strong table marshaling is that the outstanding marshaled object reference does affect the lifecycle of the stub manager or the object. This means that there must be some mechanism to release the references held by the strong table-marshaled object reference. COM provides an API function, CoReleaseMarshalData, that informs the stub manager that a marshaled object reference is being destroyed:

HRESULT CoReleaseMarshalData([in] IStream *pStm);

Like CoUnmarshalInterface, CoReleaseMarshalData takes an IStream interface pointer to a marshaled object reference. CoReleaseMarshalData must be called to revoke a table marshal when it is no longer needed. CoReleaseMarshalData should also be called if for some reason a normal-marshaled object reference will not be unmarshaled by CoUnmarshalInterface.

Object implementors can access the external reference count of the stub manager manually to ensure that the stub manager stays alive during critical phases of an object’s lifecycle. COM provides a function, CoLockObjectExternal, that increments or decrements the stub manager’s external reference count:

HRESULT CoLockObjectExternal([in] IUnknown *pUnkObject,
                           [in] BOOL bLock,
                          [in] BOOL bLastUnlockKillsStub);

The first parameter to CoLockObjectExternal must point to the actual object; it cannot point to a proxy. The second parameter, bLock, indicates whether to increment or decrement the stub manager’s external reference count. The third parameter indicates whether or not the stub manager should be destroyed if this call removes the last external reference. To understand why CoLockObjectExternal is necessary, consider an object that monitors some hardware device and is registered in the Running Object Table using weak table marshaling. While the object is actively monitoring, it wants to ensure that its stub manager remains valid so that new clients can connect to the object to check the state of the hardware. However, if the object is not actively monitoring, it would like the stub manager to disappear unless there are outstanding proxies connected to it. To implement this functionality, the object might have a method that begins monitoring:

STDMETHODIMP Monitor::StartMonitoring(void) {
// ensure that stub manager/object stays alive
   HRESULT hr = CoLockObjectExternal(this, TRUE, FALSE);
// start hardware monitoring
   if (SUCCEEDED(hr)) 
      hr = this->EnableHardwareProbe();
   return hr;
}

and another method that tells the object to end its monitoring activity:

STDMETHODIMP Monitor::StopMonitoring(void) {
// stop hardware monitoring
   this->DisableHardwareProbe();
// allow stub manager/object to die when no clients exist
   hr = CoLockObjectExternal(this, FALSE, TRUE);
   return hr;
}

Assuming that the object was originally marshaled using weak table marshaling, the preceding code ensures that the stub manager and the object remain alive as long as there is at least one outstanding proxy or the object is actively monitoring the underlying hardware.

In addition to allowing object implementors to adjust the external reference count on the stub manager, COM allows object implementors to explicitly destroy the stub manager irrespective of the number of outstanding object references. COM provides an API function, CoDisconnectObject, that finds an object’s stub manager and destroys it, disconnecting any extant proxies:

HRESULT CoDisconnectObject(
      [in] IUnknown * pUnkObject,   // ptr. to object
      [in] DWORD dwReserved);   // reserved, must be zero
Like CoLockObjectExternal, CoDisconnectObject must be called from within the process of the actual object and cannot be called on a proxy. To apply CoDisconnectObject to the hardware monitor object presented earlier, consider what would happen if the state of the object were to become corrupted. To prevent additional method invocations on the object that might return erroneous results, the object could call CoDisconnectObject to rudely disconnect any extant proxies:
STDMETHODIMP Monitor::GetSample(/*[out]*/ SAMPLEDATA *ps) {
   HRESULT hr = this->GetSampleFromProbe(ps);
   if (FAILED(hr)) // probe or object may be corrupted
      CoDisconnectObject(this, 0);
   return hr;
}

CoDisconnectObject is also used when a process wishes to shut down while one or more of its objects may have outstanding proxies. By explicitly calling CoDisconnectObject prior to destroying any objects that may have extant proxies, there is no risk that incoming ORPC requests will be serviced after the object is destroyed. If an incoming ORPC request were to arrive after the object is destroyed but while the stub manager remains alive, the interface stub would blithely invoke the corresponding method on the memory formerly known as the object, causing unnecessary suffering due to futile debugging efforts.

CoLockObjectExternal and CoDisconnectObject are both techniques that an object implementor can use to manipulate the stub manager. It is often useful to know whether any extant proxies or strong marshals exist on the stub manager. To inform objects that there are outstanding external references on the stub manager, COM defines an interface, IExternalConnection, that objects can export:

[ uuid(00000019-0000-0000-C000-000000000046),object,local ]
interface IExternalConnection : IUnknown {
   DWORD AddConnection(
      [in] DWORD extconn,   // type of reference
      [in] DWORD reserved   // reserved, must be zero
   );
   DWORD ReleaseConnection(
      [in] DWORD extconn,   // type of reference
      [in] DWORD reserved,   // reserved, must be zero
      [in] BOOL fLastReleaseCloses // should kill stub?
   );
}

When the stub manager first attaches to an object, it asks the object if it would like to be notified when external references are created or destroyed. It does this via a QueryInterface request for the IExternalConnection interface. If the object does not implement IExternalConnection, then the stub manager will use its own reference count to decide when to destroy the stub manager. If the object elects to implement IExternalConnection, then the stub manager will live until the object explicitly destroys it via a call to CoDisconnectObject.

Objects that implement IExternalConnection are expected to maintain a lock count that records the number of calls to AddConnection and ReleaseConnection. For efficiency, COM does not call AddConnection each time a proxy is created. This means that if the object maintains a lock count based on calls to AddConnection and ReleaseConnection, the object’s lock count will not accurately reflect the number of extant object references. However, COM does guarantee that the nonzeroness of the lock count does indicate that at least one outstanding reference exists and the zeroness of the lock count will indicate that no outstanding references exist. Calls to CoLockObjectExternal will affect this count as well. This information is especially useful for objects that care about the existence of external clients. For example, assume that the hardware monitoring object presented earlier spawns a thread to do background logging of sample data. Also assume that sample error might occur if logging were to take place while the object is actively monitoring data or being controlled by external clients. To avoid this problem, the logging thread could check the lock count maintained by the object’s IExternalConnection implementation and perform the logging operation only when no external references exist. This assumes that the object implements IExternalConnection as follows:

class Monitor : public IExternalConnection,
               public IMonitor {
   LONG m_cRef;    // normal COM reference count
   LONG m_cExtRef; // external reference count
   Monitor(void) : m_cRef(0), m_cExtRef(0) { ... }

   STDMETHODIMP_(DWORD) AddConnection(DWORD extconn, DWORD) {
      if (extconn & EXTCONN_STRONG) // must check for this bit
      return InterlockedIncrement(&m_cExtRef);
   }

   STDMETHODIMP_(DWORD) ReleaseConnection(DWORD extconn, DWORD,
                             BOOL bLastUnlockKillsStub) {
      DWORD res = 0;
      if (extconn & EXTCONN_STRONG){ //must check for this bit
      res = InterlockedDecrement(&m_cExtRef);
      if (res == 0 && bLastUnlockKillsStub)
         CoDisconnectObject(this, 0);
      }
      return res;
   }
  :    :    :
  :    :    :
}

Given this implementation, the thread routine could check the state of the object and decide whether or not to perform the logging operation based on the activity level of the object:

DWORD WINAPI ThreadProc(void *pv) {
// assume ptr to real object is passed to CreateThread 
   Monitor *pm = (Monitor*)pv; 
   while (1) {
// sleep for 10 seconds
      Sleep(10000); 
// if object is not in use, perform a log operation
      if (pm->m_cExtRef == 0)
      pm->TryToLogSampleData();
   }
   return 0;
}

Assuming that the object’s TryToLogSampleData method handles concurrency correctly, this thread procedure will log data only when the object is not in use by external clients or actively monitoring (recall that when monitoring, the object raises the external reference count via CoLockObjectExternal). Although this example may seem somewhat contrived, there are cases in which tracking external references is critical to correct operation. One classic example is described in Chapter 6 and is related to registering class objects in out-of-process servers.

14 Technically, the local OR waits for a short period of time to ensure that any extant marshaled object references created by the dead client have a chance to be unmarshaled.

© 1998 by Addison Wesley Longman, Inc. All rights reserved.