Click Here to Install Silverlight*
United StatesChange|All Microsoft Sites
MSDN
|Developer Centers|Library|Downloads|Code Center|Subscriptions|MSDN Worldwide
Search for


Advanced Search
MSDN Home > MSJ > September 1999
September 1999


Don Box is a co-founder of DevelopMentor, a COM think tank that educates the software industry in COM, MTS, and ATL. Don wrote Essential COM and coauthored the follow-up Effective COM (Addison-Wesley). Reach Don at http://www.develop.com/dbox.

I've spent the last 12 months of my life thinking, explaining, and writing about context. Developers often come up to me and ask, "Are contexts real?" My usual reply is that contexts are as real as processes or threads. Ultimately, processes and threads are OS-level abstractions that have a supporting API and underlying implementation. Contexts (and activities, apartments, and transaction streams) are COM-level abstractions that also have a supporting API and implementation.
For a lot of folks, seeing a concrete example goes a long way toward understanding a technology, so this month's column tries to pull back the curtain and look at a prototypical implementation of COM+. Be aware that the implementation presented here is by no means based on the actual code used in Windows 2000®—rather, it is a radically simplified version meant to illustrate the semantics of COM+.
Before I start looking at code, it is important to define context. In COM+, like Microsoft® Transaction Server (MTS) before it, a context is the innermost scope of an object. All objects exist in a particular context. Additionally, object references are context-relative and only usable from the context they are initialized in. To pass object references from one context to another, you can either pass the reference as a method argument to some other object (this is the recommended way) or by using CoMarshalInterface/IGlobalInterfaceTable to context-neutralize the reference prior to sharing it across context boundaries.
In general, an object is associated with a context at CoCreateInstance time, and CoCreateInstance is the primary creator of new contexts. Contexts have properties that are determined by the attributes of the newly instantiated object's class. Additionally, the context properties of an object's creator can also factor into the new object's context. Contexts subdivide a process into mini-processes and share many of the same characteristics of a Win32®-based process (such as property inheritance).

Call Context Versus Object Context

No discussion of context is complete without a brief look at the distinction between call context and object context. When a method executes, it has access to two sorts of context. One aspect of context is transient to the particular invocation and will disappear as soon as the method call returns control to the caller. This type of context is a call context. A more important type of context is method-invariant, and sticks around for the lifetime of the object (often longer). This type of "context" is called object context, but in this column when the term context is used without a qualifier, I am referring to object context.
Call context is used primarily for security and call cancellation and is exposed to the programmer via the CoGetCallContext API.

 HRESULT CoGetCallContext(REFIID riid, void **ppv);
Object context is used for concurrency and transaction management, thread affinity, just-in-time (JIT) activation and some security bits and pieces. Object context is exposed via the CoGetObjectContext API.

 HRESULT CoGetObjectContext(REFIID riid, void **ppv);
For backward compatibility with MTS components, COM+ provides an implementation of GetObjectContext that looks more or less like this:

 HRESULT GetObjectContext(IObjectContext **ppoc) {
   return CoGetObjectContext(IID_IObjectContext,
       (void**)ppoc);
 }
Components that are targeting Windows 2000 or later are encouraged to move away from GetObjectContext. Additionally, IObjectContext has been refactored (see IObjectContextInfo, IContextState, and ISecurityCallContext for more information) and should also be considered deprecated and for legacy support only. The object model for both object context and call context is shown in Figure 1.
Figure 1  Context Object Model
Figure 1 Context Object Model

In many ways, CoGetObjectContext is analogous to the Win32 API GetCurrentProcess. Both APIs return a reference to the current runtime environment. GetCurrentProcess can cheat by simply returning a pseudo-handle that the system recognizes as meaning "the current process" whenever it is passed to a system call. Because CoGetObjectContext must return a valid COM object reference with vptrs and vtbls, some other approach is needed. Enter interceptors.

Interceptors

Under COM+, interceptors are used to cross context boundaries. An interceptor acts as a proxy to the "real" object that is valid in some other context. Interceptors implement the same set of interfaces as the real object. The primary job of the interceptor is to set up the object context (and call context) for the method prior to dispatching the call to the real object. To achieve this, the interceptor must hold two references, one to the real object and one to its object context.

 struct Interceptor : IYourInterface {
   IYourInterface *pRealObj; // the real object
   OBJECT_CONTEXT *pOC;      // the object's ctx
 };
For simplicity, this column assumes that all user-defined objects implement some canned interface, IYourInterface, and nothing else. In reality, user-defined objects can implement any interface for which the system has a type library or /Oicf-based marshaler. In the spirit of keeping things concrete, let's assume that IYourInterface is defined as follows:

 interface IYourInterface : IUnknown {
   HRESULT YourMethod([in] long n, 
                      [out, retval] BSTR *pb);
 }
Another simplifying assumption I am making in this column is that no object references are passed as parameters. I will address this assumption when it comes up in future columns, but be aware that passing object references makes the job of the interceptor considerably more complex.
Drilling into the interceptor, consider the minimalist implementation shown here:

 HRESULT Interceptor::YourMethod(long n, BSTR *pb){
   return pRealObj->YourMethod(n, pb);
 }
This is a no-op interceptor that does absolutely nothing except add a meaningless layer of fat to the object. In particular, it is not doing the primary task of an interceptor, which is to set up the context for method invocation.
Since user-defined objects do not have any facility for maintaining a reference to their context, the interceptor must somehow make the context available to the method while it executes (hence the CoGetObjectContext API). To achieve this, the interceptor and CoGetObjectContext must conspire to build the concept of the "current" context, which the interceptor must set up prior to invocation and restore when invocation is complete.

 HRESULT Interceptor::YourMethod(long n, BSTR *pb){
   OBJECT_CONTEXT *pSaved = GetCurrentCtx();
   SetCurrentCtx(this->pOC);
   HRESULT hr = pRealObj->YourMethod(n, pb);
   SetCurrentCtx(pSaved);
   return hr;
 }
The only issue is where should SetCurrentCtx/GetCurrentCtx store the reference? A processwide global variable would not work, since multiple threads in a process can service method calls concurrently. However, because a given thread is only in one method at any given time, a thread-specific variable is in order.
To implement the notion of current context, each thread needs a thread local storage (TLS)-based reference to an object context.

 __declspec(thread) OBJECT_CONTEXT *tls_pCurCtx;
It is the job of the interceptor to set up this variable at invocation time:

 HRESULT Interceptor::YourMethod(long n, BSTR *pb){
   OBJECT_CONTEXT *pSaved = tls_pCurCtx;
   tls_pCurCtx = this->pOC;
   HRESULT hr = pRealObj->YourMethod(n, pb);
   tls_pCurCtx = pSaved;
   return hr;
 }
Given the previous interceptor, the CoGetObjectContext API is trivial to implement:

 HRESULT CoGetObjectContext(REFIID riid, void **ppv) {
   return tls_pCurCtx->QueryInterface(riid, ppv);
 }
This has the effect of returning the pOC data member of the interceptor used to invoke the current call (which is exactly what you want).

Implementing OBJECT_CONTEXT

This is a reasonable time to start looking at the OBJECT_CONTEXT implementation. Remember that OBJECT_CONTEXT needs to adhere to the object model shown in Figure 1, so that it will look something like the code in Figure 2. Most of the context properties are controlled by attributes stored on disk.
The ACTIVITY property is used for concurrency control, and is controlled via the Synchronization attribute. The TX_STREAM property is used for transaction management, and is controlled via the Transaction attribute. The APARTMENT property controls thread affinity, and is controlled via the ThreadingModel attribute. Unlike every other attribute, ThreadingModel is not technically stored as an attribute in the COM+ catalog; it is simply a registry value. Classes that only have a ThreadingModel but no COM+ attributes are called nonconfigured components/classes. Classes that are installed into the COM+ catalog will have a full set of attributes and are called configured components/classes.
Recall that contexts are created at CoCreateInstance time. CoCreateInstance looks at the requirements of the target class and compares them to the current context. In general, CoCreateInstance would like the new object to inherit as many of the creator's context properties as possible (this is analogous to CreateProcess allowing child processes to inherit properties of the parent, such as a console or environment variables). This concept is illustrated in Figure 3.
Figure 3  Context Property Propagation
Figure 3 Context Property Propagation

To understand how CoCreateInstance works, let's take a look at a simplified implementation that focuses on the interesting bits.

 HRESULT CoCreateInstance(REFCLSID rclsid, ...,
                          REFIID riid, void**ppv){
 // discover requirements of target class
   ATTRIBUTES *pTarget = LookUpInCatalog(rclsid);
 // grab current context (the parent)
   OBJECT_CONTEXT *pParent = tls_pCurCtx;
   if (TotallyCompatible(pTarget, pParent))
     return SimpleCreate(rclsid, riid, ppv);
   else
     return ComplexCreate(rclsid, pTarget, pParent,
                          riid, ppv);
 }
Note that this code uses four underlying functions. LookUpInCatalog looks up the declarative attributes for the target class. This is the union of the COM+ attributes (such as Synchronization) and the base COM registry settings (ThreadingModel, for example). The TotallyCompatible function compares the target class's requirements against the current runtime environment. SimpleCreate will simply instantiate the object directly. ComplexCreate will perform some magic shown later. To understand how all of this works, however, you must first look at TotallyCompatible.
Because CoCreateInstance wants the new object to inherit as many context properties as possible, CoCreateInstance checks the target class to see if it is possible to share all of the context properties. If it is, then there is no reason to create a new context for the object. Rather, the new object can simply share the context of the creator. The pseudocode for TotallyCompatible is shown in Figure 4.
The code in Figure 4 illustrates three important facts:
  • Nonconfigured components share the context of their creator modulo ThreadingModel incompatibilities.
  • JIT activation forces the new object into a new context.
  • Some attributes (such as Synchronization and Transaction) provide explicit control over context propagation.
At the risk of making a sweeping generalization, you should assume that nonconfigured components share their creator's context and that configured components get a new context. This is certainly the statistical norm, if not a hard-and-fast rule.
With the TotallyCompatible function in place, let's look at SimpleCreate and ComplexCreate. As the name implies, SimpleCreate is the simpler, so let's look at it first:

 HRESULT SimpleCreate(REFCLSID rclsid, ...) {
   DLLGCO pfnDllGetClassObject = 
       LookupDllInCache(rclsid);
   CComPtr<IClassFactory> pcf = 0;
   pfnDllGetClassObject(rclsid, IID_IClassFactory, 
                        (void**)&pcf);
   return pcf->CreateInstance(pUnkOuter,riid,ppv);
 }
SimpleCreate should look rather comforting, as it is simply an implementation of the Brockschmidtian COM you learned as a youngster. The new object will be created in whatever context is current when this function executes.
Unlike SimpleCreate, ComplexCreate has to perform context magic. Recall that ComplexCreate is called when the target class mandates that a different context be used. If the target class is a configured class, then a new context will be created. However, the target class may be a nonconfigured class in the case of apartment incompatibilities (more on this later). The basic implementation is shown in Figure 5.
Recall that the code in Figure 5 is called when the new object must live in a different context than its creator. Note that this code simply finds or creates the child context and creates an interceptor that binds the new object to the child context. As you saw earlier, this interceptor will ensure that when methods of the new object are invoked, the "current" context will be the child context, not the caller's context.
The function FindContext is responsible for propagating context properties from the parent to the child. It must look at the target class's attributes (which are read from the component catalog) and the context of the creator. In general, you want all of the context properties to propagate, barring attributes that prohibit it. Here's the pseudocode:

 OBJECT_CONTEXT *FindContext(ATTRIBUTES *pTarget,
     OBJECT_CONTEXT *pParent) {
 // non-configured components get default context
 // of appropriate apartment
   if (!pTarget->bConfiguredComponent)
     return GetDefaultCtxOfApt(
                         pTarget->ThreadingModel);
 // configured components get new contexts
   OBJECT_CONTEXT *pChild  = new OBJECT_CONTEXT();
   PropagateProperties(pParent, pChild, pTarget);
   return pChild;
 } 
The previous code always puts nonconfigured components in the default context of the appropriate apartment. Be aware, however, that this function is only called if the nonconfigured class had a ThreadingModel incompatibility with the creator. Had the ThreadingModel been compatible, the previous code would never execute and the new (nonconfigured) object would have been created in its creator's context.

Propagation

The PropagateProperties function is where the real action takes place. PropagateProperties must compare the requirements of the target class against the parent context, propagating as much as possible. Here's a very skeletal implementation:

 void PropagateProperties(pParent, pChild, pTarget){
   CoCreateGuid(&pChild->id);
   pChild->bJITAEnabled = pTarget->bJITAEnabled;
   pChild->bDone = false;
   pChild->nCallsInProgress = 0;
   PropagateActivity(pParent, pChild, pTarget);
   PropagateTxStream(pParent, pChild, pTarget);
   PropagateApartment(pParent, pChild, pTarget);
 }
The more interesting action happens in the three subroutines that propagate activity, transaction stream, and apartment. The activity property (which is controlled by the Synchronization attribute) is the simplest of the three, and is shown in Figure 6. Note the subtle distinction between Synchronization=Disabled and Synchronization=Supported. Both indicate that the presence or absence of an activity in the creator should not trigger a new context to be created. You can verify this by looking at the TotallyCompatible function shown in Figure 4. The difference between Disabled and Supported only kicks in when a new context must be created due to some other attribute. Synchronization=Supported indicates that the creator's activity property must always be propagated. Synchronization=Disabled indicates that the creator's activity property is ignored and the new context will not have an activity. This difference is reflected in the PropagateActivity function shown in Figure 6.
Transaction stream propagation is very similar to activity propagation. The primary difference is that transaction streams need to keep track of which context is the root, as the root is treated specially with respect to transaction termination (more on this in a future column). You'll find the transaction propagation pseudocode in Figure 7. Note the striking similarity between this function and the PropagateActivity function.
Apartment propagation is a bit more complex than either of the two properties examined so far. Recall that each process has several distinguished apartments. The multithreaded apartment (MTA) is the apartment a thread enters when it calls CoInitializeEx(COINIT_MULTITHREADED). The thread-neutral apartment (TNA) is the apartment that no thread resides in, but any thread can enter (more on this next time). The main apartment is the first single-threaded apartment (STA) to be created in a process. Finally, the host apartment is a COM-managed STA used to house ThreadingModel=Apartment objects created by MTA threads. Classes control which apartments they can execute in via their ThreadingModel setting. The legal values for ThreadingModel are:
  • Both—share creator's apartment
  • Free—must use MTA
  • Neutral—must use TNA
  • (Absent)—must use main apartment
  • Apartment—can live in any STA
Armed with this information, the basics of apartment propagation aren't quite so difficult to grasp (see Figure 8). The function in Figure 8 assumes the existence of two other functions. GetDistinguishedApt is a routine that simply returns the requested apartment of the process. Its implementation is fairly simple, as shown in Figure 9. Note that because the MTA and TNA do not need dedicated threads, this function can simply create new APARTMENT data structures and return. The host apartment requires a dedicated thread, so more work is involved. (This code assumes that the host thread will create the new apartment data structure and return it via the thread proc parameter.) Also note that if the host apartment is the first STA of the process, it doubles as the main apartment. Similarly, the host apartment will be spawned if the main apartment is requested prior to any STAs being created in the process.
Let's look at the second function, GetHomeAptOfThread. This function assumes that there is a thread-specific variable declared as follows:

 __declspec(thread) APARTMENT *tls_pHomeApt = 0;
This variable will only be modified by CoInitializeEx (see Figure 10). Note that the home apartment of a thread will never be the TNA since no thread can call this apartment home. Finally, with the code in Figure 10 in place, it is possible to look at the GetHomeAptOfThread routine:

 APARTMENT *GetHomeAptOfThread() {
   return tls_pHomeApt;
 }
This straightforward function simply returns the home apartment associated with the current thread at CoInitializeEx time. Because this association does not change over time (unless CoUninitialize is called to disassociate the current thread with its home apartment), the apartment returned will always be an MTA or STA, never a TNA. (Remember that the main and host apartments are simply distinguished STAs.)
The entire reason for describing the notion of a home apartment per thread was to examine the case of a CoCreateInstance call against a ThreadingModel=Apartment class. By reexamining the PropagateApartment routine, you can see that if the caller's thread calls an STA home, the new object lives in the calling thread's home apartment. If, on the other hand, the caller's thread calls the MTA home, the new object lives in the host STA apartment of the process.
It is important to note that a current apartment is somewhat different than the home apartment of a thread. In essence, one cannot safely make the following assertion:

 assert(tls_pCurCtx->pApt == tls_pHomeApt);
This assertion is not universally true because it is possible that a thread may enter the TNA temporarily to dispatch a call via an interceptor. This has the effect of changing tls_ pCurCtx (that's the primary job of an interceptor), but not tls_pHomeApt (which is only modified by CoInitializeEx/CoUninitialize). One can safely make the following assertion, however:

 assert(!tls_pCurCtx 
        || tls_pCurCtx->pApt == tls_pHomeApt
        || tls_pCurCtx->pApt == g_pTNA);
This assertion not only takes into account visits to the TNA, it also deals with the case of threads that have not yet called CoInitializeEx.

Apartments and Contexts

One final topic to look at is the relationship between apartments and contexts. Recall that each context has an apartment property. This means that a given context belongs to exactly one apartment. Additionally, each apartment has a distinguished context that is created when the apartment is initialized. This context is called the default context of the apartment. The default context never inherits context properties. Here is the representation of an apartment assumed in this column:

 struct APARTMENT {
   OXID            id;
   APT_TYPE        apt_type;
   HWND            hwndPortIfSTA;
   OBJECT_CONTEXT  defaultCtx;
   APARTMENT(APT_TYPE type);
 };
When an apartment is initialized, it simply initializes the default context to match base COM semantics:

 APARTMENT::APARTMENT(APT_TYPE type) {
   apt_type = type;
   if (type == MTA || type == TNA)
     hwndPortIfSTA = NULL;
   else
     hwndPortIfSTA = CreateWindow(...);
   id = AllocObjectExporterID(hwndPortIfSTA);
   CoCreateGuid(&defaultCtx.id);
   defaultCtx.pApt = this; // back ptr to self
   defaultCtx.pActivity = NULL;
   defaultCtx.pTxStm = NULL;
   defaultCtx.bJITAEnabled = false;
   defaultCtx.nCallsInProgress = 0;
 }
When CoInitializeEx creates a new apartment, it also sets the current context pointer to point to the default context of the new apartment:

 // from coinitex
 if (tls_pHomeApt == 0) {
 // create apartment
     tls_pHomeApt = new APARTMENT(type);
 // set current ctx to point to default ctx
     tls_pCurCtx = &tls_pHomeApt->defaultCtx;
 }
You can safely assume that once a thread has called CoInitializeEx, the following assertion will never fail:

 assert(!tls_pHomeApt == !tls_pCurCtx);
That is, either both references will be null or neither reference will be null.
The default context of an apartment houses instances of nonconfigured classes only. It is used in two common scenarios. First, if a thread that is simply sitting idle in the default context calls CoCreateInstance against a nonconfigured class that is ThreadingModel-compatible with the thread's home apartment, the new object will reside in the default context of the calling thread. Second, the default apartment is used when CoCreateInstance is called against a nonconfigured class that is ThreadingModel-incompatible with its creator. (Look at the FindContext code shown earlier for an example of where this happens.) In this case, COM will put or create the new object in the default context of the appropriate apartment. Here's the pseudocode for GetDefaultCtxOfApt:

 OBJECT_CONTEXT *GetDefaultCtxOfApt(TM model) {
   switch (model) {
   case FREE:
     return &GetDistinguishedApt(MTA)->defaultCtx;
   case NEUTRAL:
     return &GetDistinguishedApt(TNA)->defaultCtx;
   case ABSENT:
     return &GetDistinguishedApt(MAIN)->defaultCtx;
   case APARTMENT:
     if (tls_pHomeApt->apt_type == MTA)
       return &GetDistinguishedApt(HOST)->defaultCtx;
     else
       return &tls_pHomeApt->defaultCtx;
   }
 }
The interesting bit of the function is how it handles ThreadingModel=Apartment activation requests. If the calling thread is an STA, the default context of the thread's home apartment is used; if the calling thread is an MTA thread, the default context of the host STA is used. In either case, the fact that the current thread may be visiting the TNA is largely ignored.
Finally, two similar-looking functions were referred to in this column, but never defined. The function ApartmentCompatible was called indirectly by CoCreateInstance to determine whether a target class could share the apartment property of its creator. Its implementation is fairly simple:

 bool ApartmentCompatible(TM model, APARTMENT *pApt) {
   switch (model) {
     case BOTH:      // all apts ok
       return true;
     case FREE:      // only MTA
       return pApt->apt_type == MTA;
     case NEUTRAL:   // only TNA
       return pApt->apt_type == NEUTRAL;
     case MAIN:      // only main STA
       return pApt->apt_type == MAIN;
     case APARTMENT: // any STA
       return pApt->hwndForSTA != 0;
   }
 }
Note that the pApt parameter is derived from the context that CoCreateInstance was called from.
The second function, ThisThreadCanEnterCtx, was called to determine whether a given thread could enter a context. This function uses the home apartment of the calling thread to determine whether entry into the apartment is possible without a thread switch. Its implementation is shown here:

 bool ThisThreadCanEnterCtx(OBJECT_CONTEXT *pCtx){
   switch (pCtx->pApt->apt_type) {
     case TNA: // any thread can enter ctx
       return true;
     case MTA: // only MTA threads can enter ctx
       return tls_pHomeApt->apt_type == MTA;
     default:  // apt is STA so must be same
       return tls_pHomeApt == pCtx->pApt;
   }
 }
This test is performed any time a thread needs to invoke a method on an object. Note that contexts (and their objects) that live in the TNA exhibit no thread affinity; that is, any thread can enter them. Contexts or objects that live in the MTA exhibit low thread affinity, as they can be accessed by any MTA thread, but not by STA threads. Finally, contexts or objects that live in an STA exhibit high thread affinity, as only one thread can ever access them.

Conclusion

In this column, I demonstrated a sample implementation of the COM+ context architecture. The focus here was how context propagation works. A future column will examine how interceptors work in the face of context. In the meantime, you can check out bits and pieces of the code discussed here at http://www.develop.com/dbox/.

From the September 1999 issue of Microsoft Systems Journal. Get it at your local newsstand, or better yet, subscribe.

Have a question about programming with COM? Send your questions via email to Don Box: dbox@develop.com or http://www.develop.com/dbox.

© 1999 Microsoft Corporation. All rights reserved.
Terms of Use      Privacy Policy.

© 2015 Microsoft Corporation. All rights reserved. Contact Us |Terms of Use |Trademarks |Privacy & Cookies
Microsoft