Share via


COM+ 1.5: Discover Powerful Low-Level Programming in Windows XP with New COM+ APIs

MSDN Magazine

COM+ 1.5

Discover Powerful Low-Level Programming in Windows XP with New COM+ APIs

Craig Andera and Tim Ewald
This article assumes you're familiar with COM+ and C++
Level of Difficulty     1   2   3 
Download the code for this article: COMXP.exe (200KB)
SUMMARY The new version of COM+ that ships as part of Windows XP includes APIs for low-level context programming. These functions allow you to create contexts that use COM+ runtime services, independent of objects and without registering anything in the COM+ Catalog. Designed for advanced COM+ developers who understand the COM+ context model, these APIs make it easy to integrate runtime services with code in nonstandard ways. This article explains how these low-level context APIs work, discusses when you'd want to use them, and provides a .NET-based wrapper to make it simpler to use the APIs from C#.

W indows® XP introduces a host of new features, including the next iteration of COM+, version 1.5. COM+ 1.5 includes many new features, mostly in the form of new runtime services. These have already been described in Juval Lowy's August 2001 MSDN® Magazine article, "Windows XP: Make Your Components More Robust with COM+ 1.5 Innovations". In this article, we'll focus on the other new aspect of COM+ programming in Windows XP: a new programming model for using runtime services. As you'll see, some new APIs allow you to break the long-standing connection between contexts and objects, leading to some powerful new ways to utilize the services COM+ provides.

A Problem: Semi-transactional Objects

      COM+ provides runtime services that have helped many developers successfully build complex transactional systems. One of its most-used services is the ability to initiate, manage, and monitor distributed transactions. While distributed transactions are beneficial and indeed necessary in some cases, there are also cases in which a distributed transaction is not necessary. For example, a database INSERT of a single row generally does not need the additional protection offered by a COM+-managed transaction.
      If a class has a method that encapsulates such an INSERT and has another method that interacts in a complicated way with multiple databases, thus requiring a distributed transaction to ensure correct operation, you're faced with a bit of a dilemma. COM+ lets you turn the transaction service on and off on a class-by-class basis. In this case what you'd really like is to specify that your class's second method needs a transaction, but that its first method does not. In short, you want a semi-transactional class.
      Unfortunately this is impossible in COM+ 1.0. The only solution is to split the class into two, configuring one to require transactions and the other to refuse them. This bifurcation often leads to more complex, less elegant designs. Fortunately, COM+ 1.5 offers a more sophisticated solution.

The Heart of the Problem: Objects and Contexts

      The problem we just described comes from the way COM+ services have been exposed to programmers. Before Windows XP, COM+ services were only invoked when client code accessed an object that ran in a context protected by an interceptor, as shown in Figure 1. Cross-context calls were always intercepted, giving COM+ a chance to pre- and post-process calls so that things like transaction creation and termination could be provided automatically. Objects interacted with COM+ services by calling CoGetObjectContext and using the context object that was returned.

Figure 1 COM+ Contexts and Interception
Figure 1 COM+ Contexts and Interception

      The main advantage of this interceptor-driven approach is that it cleanly integrates objects with COM+ services. The main disadvantage is that it assumes that all of the methods of a given class share the same requirements for COM+ services. We've already described one situation where this is not true: semi-transactional objects. If you want a semi-transactional object, the one-to-one association between an object and a context that the interceptor-applied services architecture requires is actually a drawback. A given context is either transactional or not. This means that as long as an object always lives in that context, the dream of semi-transactional objects is hopeless.

The Solution: Contexts without Objects

      COM+ 1.5 allows you to solve this problem by using services independent from objects. If you examine the COM+ header file, comsvcs.h, you'll see several new interfaces and APIs that contain exactly what you need to implement semi-transactional classes, starting with the CoEnterServiceDomain API:

  HRESULT CoEnterServiceDomain(IUnknown* pConfigObject);
  

 

CoEnterServiceDomain allows a thread to create and immediately enter a new context. The new context behaves in all respects like the contexts COM+ 1.0 creates to house your objects. You can even call CoGetObjectContext to retrieve a reference to the object context object. CoEnterServiceDomain's partner in crime is CoLeaveServiceDomain.

  void CoLeaveServiceDomain(IUnknown *pUnkStatus);
  

 

Once a thread has done everything it wants to do in the context it created, a call to CoLeaveServiceDomain then exits the context, performing any necessary cleanup.

Figure 2 COM+ Contexts without Interception
Figure 2 COM+ Contexts without Interception

      Any work done between calls to CoEnterServiceDomain and CoLeaveServiceDomain is done in the newly created context, the same as work done during a call into a COM+ 1.0 object. Note, however, that the link between contexts and objects is completely broken. Because CoEnterServiceDomain and CoLeaveServiceDomain provide an API-based way to create and use contexts, you can use COM+ services, including transaction management, without an object in sight (see Figure 2). Note that while CoEnterServiceDomain creates a new context, it does not create an interceptor.

Configuring a Standalone Context

      In traditional COM+ programming, a configured class requests runtime services by setting attributes in the COM+ Catalog. Given that a class is not needed when you call CoEnterServiceDomain, how do you tell COM+ what services you want from the newly created context?
      CoEnterServiceDomain accepts a single parameter, pConfigObject, which is a reference to a service configuration object. The service configuration object contains the details about the services that you want a new context to provide. You must use the system-provided implementation of this object, whose ProgID is COMSVCS.CServiceConfig.1.
      The service configuration object implements several interfaces that allow you to specify what services you want a new context to use. For example, if you want the new service to include a COM+ managed transaction, you must set the appropriate options using the IServiceTransactionConfig interface (see Figure 3). Figure 4 includes a list of the most interesting configuration interfaces.
      You might have expected to see interfaces for controlling object pooling and just-in-time (JIT) activation, but no such interfaces exist. Remember that we're talking about creating new contexts without objects. Neither JIT activation nor pooling make sense if objects aren't present.

Declarative Transactions Using IServiceTransactionConfig

      So let's look at the topic that started this discussion, declarative transactions. One of the interfaces the service configuration object implements is IServiceTransactionConfig (see Figure 3). You can use this interface to control whether a transaction will be present in the context you are creating, its isolation level, and even how long the transaction will have to execute before it times out.
      The main method of interest in IServiceTransactionConfig is ConfigureTransaction, which takes a single parameter. This parameter is one of the four available values from the CSC_TransactionConfig enumerated type, shown in Figure 5. These four values correspond to the four major available settings in the Component Services Explorer, as you can see in Figure 5. For example, CSC_IfContainerTransactional has the same semantics as the Transaction = Required setting in the COM+ Explorer.
      The code in Figure 6 uses CoEnterServiceDomain to create a new context that uses a COM+ managed transaction. Notice that this is a simple C++ console application and that we make use of the ATL CComPtr template classes merely as a convenience, not because we are implementing any objects. The code is linked against comsvcs.lib, where the new context APIs are defined.
      After calling CoInitialize, the main thread creates an instance of the service configuration object, requesting its IServiceTransactionConfig interface. Then it calls ConfigureTransaction to set the configuration object to require a new transaction, passing CSC_CreateTransactionIfNecessary.
      After setting up the configuration object, the main thread calls CoEnterServiceDomain, passing the object to it. Once the call to CoEnterServiceDomain returns, the thread executes inside the new transactional context. Calls to CoGetObjectContext will reflect this fact, and if you use ADO or OLE DB to access a database, the connection will auto-enlist on the transaction just as if executing in a traditional COM+ object-based context.
      Remember that COM+ transactions don't end until the transactional context is exited. To transition out of the context and let the runtime either commit or abort the transaction (after calling IContextState::SetMyTransactionVote to request that the transaction be committed), the thread calls CoLeaveServiceDomain.
      When the main thread calls CoLeaveServiceDomain, it passes a reference to an instance of an object that implements the ITransactionStatus interface. (There is no system-provided implementation of ITransactionStatus, but writing one is trivial.)

  interface ITransactionStatus : IUnknown
  
{
HRESULT SetTransactionStatus([in] HRESULT hrStatus);
HRESULT GetTransactionStatus([out] HRESULT *pHrStatus);
};

 

CoLeaveServiceDomain calls SetTransactionStatus after the transaction has either committed or aborted, passing an appropriate HRESULT indicating the transaction's outcome. The value that COM+ passes to SetTransactionStatus is slightly counterintuitive. Because COM+ assumes that the caller of CoLeaveServiceDomain will be aware of how the code in the transactional context voted, it will set the transaction outcome to S_OK if the vote was to commit. If the transaction actually aborted, the outcome will be CONTEXT_E_ABORTED. This is exactly the way that the declarative transaction mechanism behaves in COM+ 1.0.

Context Property Flow

      Some of you might be wondering if context properties such as transactions still flow downstream to newly created contexts. For example, in traditional COM+ programming, you often had two classes marked Transaction = Required. If an instance of class A created an instance of class B, both instances would be protected by the same transaction. As objects are no longer required, is there a parallel concept in the world of CoEnterServiceDomain?
      The answer is yes; context properties can be inherited by an explicitly created context. If a thread calls CoEnterServiceDomain while it is already executing in a context created by a previous call to CoEnterServiceDomain, the new context may take on some of the settings of the context from which it was created. Whether or not a new context inherits settings from its creator's context depends on the settings applied to the service configuration object through its interfaces.
      Consider the code in Figure 7. It uses CoEnterServiceDomain to create a series of new contexts, specifying various IServiceTransactionConfig settings to demonstrate context property inheritance. At various points throughout the code, the current context ID and transaction ID are printed to the console using the PrintContextAndTransactionId function. (It's interesting to observe that PrintContextAndTransactionId uses CoGetObjectContext just as if it were in a context created by COM+ in the "normal" way—and it works just fine!)
      After COM has been initialized, the main thread is in the default context, which is nontransactional. As soon as it calls CoEnterServiceDomain, the thread is executing in a newly created transactional context. The call to WillInherit occurs next, where CoEnterServiceDomain is called again. This creates another, separate context, but because the configuration object's transactional setting is CSC_CreateTransactionIfNecessary, the new context shares the transaction of its creator. In that sense, it provides the same semantics as the Transaction = Required setting from traditional COM+; a transaction must be present, but the creator's transaction should be used if possible.
      Contrast that with the thread's subsequent call to WontInherit. In this case, the configuration object's transaction option is set to CSC_NewTransaction, which has the same semantics as the traditional Transaction = RequiresNew setting, and a new transaction must be created.

Figure 8 Context Property Flow
Figure 8 Context Property Flow

      Figure 8 shows the contexts and transactions created in this example. The output from the program is shown in Figure 9. It clearly illustrates that although there are two contexts in each case, the transaction can be made to either flow or not, as desired.

What About .NET?

      While researching this article, we thought it would be fun to see if this new, low-level context programming model could be used from the Microsoft® .NET Common Language Runtime (CLR). Those of you who have been reading MSDN Magazine know that the CLR is the latest development platform from Microsoft, and that it offers a lot of advantages over COM. In the future, it is more likely that programmers will want to access new APIs from .NET languages like C# and Visual Basic® .NET. As you'll see, wrapping the new COM+ APIs in a C# library makes them somewhat easier to use. In any case, writing C# is fun. The code in Figure 10 shows most of the source code for our C# wrapper library.
      We won't go into all of the details of how the CLR interoperates with ordinary Windows DLLs or with COM objects since you can read all about it in David Platt's August 2001 MSDN Magazine article, "NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications" and in the Don Box House of COM column in the May 2001 issue.
      The center of our wrapper library is the ComPlusUtil class. This class—which is defined inside the MSDN namespace—contains several extern methods. This keyword, along with the DllImport attribute, signals the CLR that these methods are really implemented in comsvcs.dll, the unmanaged DLL where COM+ functionality like CoEnterServiceDomain lives. We've set PreserveSig=false and specified a void return type to allow the CLR to transform failed HRESULTs from the API calls to CLR exceptions.
      Note that we didn't have to do anything in our wrapper to define the IServiceTransactionConfig interface. Instead, we were able to rely on the Type Library Importer, tlbimp.exe. By running tlbimp.exe against the type library found in comsvcs.dll, we produced the managed assembly COMSVCSLib.dll. Adding a reference in this assembly to a project gives the CLR all the information it needs to use the interfaces.
      The wrapper library also provides a class, MyTxStatus, that implements ITransactionStatus and can be used to recover a transaction's outcome after CoLeaveServiceDomain is called. Having this wrapper library allows us to rewrite our earlier transactional client code in C#. Not only is this good news for .NET programmers, it actually makes our example code easier to read because a lot of extraneous COM details are removed.
      .NET ships with a great COM+ interoperability layer (although it has not yet been extended to include the new COM+ 1.5 features that we're discussing in this article). The code in Figure 11 takes advantage of the built-in wrapper for object context, System.EnterpriseSerivces.ContextUtil, and uses it to influence the transaction's outcome. For more information on this class and on .NET/COM+ interoperability in general, see Tim Ewald's article "COM+ Integration: How .NET Enterprise Services Can Help You Build Distributed Applications" in the October 2001 issue of MSDN Magazine.

Applications for These New APIs

      There are a lot of interesting applications for these new low-level context APIs. One is to solve our original problem: implementing semi-transactional classes. Semi-transactional classes are a breeze with CoEnterServiceDomain and friends. The code in Figure 12 demonstrates exactly how to do this in C#.
      Because this code uses COM+ contexts, everything still works the way you would expect. ADO.NET connections will automatically enlist against the distributed transaction used by the Transactional method and any downstream COM+ configured objects that are created can share the method's transaction if desired. The NonTransactional method never uses a distributed transaction.
      As an interesting side effect, note that the semi-transactional class retains all of its state after the end of the transaction. Since JIT activation is an interceptor-based service, we are not forced to deactivate at transaction boundaries. This is another reason to apply these low-level APIs, but be careful! While the ability to keep state across transactional boundaries is useful in situations where the data is transaction-invariant, it can lead to horrendous data inconsistencies if the data is transaction variant, and therefore, should be approached with extreme care.
      A third reason to use these APIs is that they bypass the COM+ Catalog, which simplifies deployment.

A Spoonful of Medicine

      As is always the case, with great power comes great responsibility. Although the new low-level context APIs give you the ability to go beyond the limits of the COM+ 1.0 interceptor-based architecture, you still must watch out for a few pitfalls.
      First, interceptors perform two functions for you: interceptions make sure that the transition to the appropriate context is made, and they marshal interface pointer parameters. While we have CoEnterServiceDomain and CoLeaveServiceDomain to perform the former for us, responsibility for the latter now falls squarely on our shoulders.
      If a thread calls CoEnterServiceDomain, creates an object, and then calls CoLeaveServiceDomain, the object reference it acquired is no good because object references are context-relative. If you are lucky, the object is a proxy. Proxies contain code that looks for just such a situation and will return the error RPC_E_WRONGTHREAD, alerting you to your mistake. If it's not a proxy, you'll get no such warning and behavior is undefined.
      One option is for the thread to marshal the object reference into the Global Interface Table (GIT) before calling CoLeaveServiceDomain. The other option is to never let this situation arise, except when the object you are dealing with uses the COM freethreaded marshaler (for example, an ADO RecordSet or an MSXML FreeThreadedDocument). This problem is somewhat simpler to solve if you are working with .NET because the CLR handles context-relative object references more intelligently.
      Second, if you write functions like Transactional and NonTransactional (shown in Figure 12) you need to make sure you call CoLeaveServiceDomain on all code paths that could exit the functions. That's why both methods include a finally block to exit the context they create even if an exception is thrown.

Keeping a Context over Time

      CoEnterServiceDomain and CoLeaveServiceDomain allow a thread to enter and leave as many contexts as it likes. It is important to note, however, that when CoLeaveServiceDomain is called, the current context is destroyed. What if you want to create a context and keep it over time, entering and leaving it multiple times as required to accomplish some task? The APIs we've discussed so far do not really support this model, but there is another new API that does: CoCreateActivity.

  HRESULT CoCreateActivity(IUnknown* pIUnknown, REFIID riid,
  
void** ppObj );

 

CoCreateActivity creates a new context and returns a reference to it. The context stays in existence until you release it.
      The first parameter to CoCreateActivity is the same service configuration object that is passed to CoEnterServiceDomain. Incidentally, don't be confused by the name CoCreateActivity. This API is all about creating contexts and does not necessarily create a new COM+ activity.
      Once you have a reference to a context created by CoCreateActivity, you can use it to dispatch work into the context. Together, the IServiceActivity and IServiceCall interfaces in Figure 13 provide this functionality.
      The context object that CoCreateActivity returns implements the IServiceActivity interface. Its SynchronousCall method accepts a reference to an object that implements IServiceCall, which you must provide. When you invoke SynchronousCall, a transition into the context occurs. Then the callback to IServiceCall::OnCall is made from inside the context, and the HRESULT returned is passed back through SynchronousCall. This interaction between the service configuration object, CoCreateActivity, the newly created context, and your IServiceCall implementation is shown in Figure 14.

Figure 14 IServiceCall
Figure 14 IServiceCall

      It is important to note a difference between CoEnterServiceDomain and CoCreateActivity. Before you call CoCreateActivity, you have to specify a thread pool for the new context by calling the service configuration object's IServiceThreadPoolConfig::SelectThreadPool method, passing one of the values of the CSC_ThreadPool enumerated type, as shown in the following:

  typedef enum tagCSC_ThreadPool {    
  
CSC_ThreadPoolNone = 0,
CSC_ThreadPoolInherit = CSC_ThreadPoolNone + 1,
CSC_STAThreadPool = CSC_ThreadPoolInherit + 1,
CSC_MTAThreadPool = CSC_STAThreadPool + 1
} CSC_ThreadPool;

 

      Passing CSC_MTAThreadPool places the new context in the multithreaded apartment (MTA), where it can be entered by any of the MTA threads in the process. Passing CSC_STAThreadPool is a little bit subtler. The new context will live in one of the single-threaded apartment (STA) threads in the COM+-managed thread pool unless the thread calling CoCreateActivity is already an STA thread. In that case, the thread calling CoCreateActivity is used.
      If the new context is associated with the STA thread pool, you have to be a bit careful. Dispatching work through IServiceActivity is not guaranteed to wind up on the same STA thread or in the same context every time. If you need to ensure that your calls will wind up in the same STA every time, use IServiceThreadPoolConfig::SetBindingInfo, passing CSC_BindToPoolThread.
      Making an asynchronous call into a context created by CoCreateActivity is just as easy. Simply call IServiceActivity::AsynchronousCall instead of SynchronousCall, passing the same implementation of IServiceCall. The big difference is that the call will return immediately, queuing the work for later dispatch on another thread. It is up to your implementation of IServiceCall to signal completion of the work to the main thread.

Conclusion

      COM+ 1.5 offers three new, very powerful APIs for low-level context programming. While these APIs are definitely not for the faint of heart, they are extremely useful for advanced COM+ programmers who want to make the most of their chosen infrastructure. By allowing you to deal with contexts directly, CoEnterServiceDomain and friends let you integrate your code with COM+ runtime services in a variety of nonstandard ways. If you understand how contexts work and keep your wits about you, breaking the link between contexts and objects is the perfect path to a powerful new model for the COM+ runtime environment.
For related articles see:
Windows XP: Make Your Components More Robust with COM+ 1.5 Innovations
COM+ Integration: How .NET Enterprise Services Can Help You Build Distributed Applications
House of COM: Migrating Native Code to the .NET CLR
.NET Interop: Get Ready for Microsoft .NET by Using Wrappers to Interact with COM-based Applications
For background information see:
Transactional COM+: Building Scalable Applications by Tim Ewald (Addison-Wesley, 2001)
Craig Andera (candera@develop.com) works for the Minneapolis-based consulting company Digital Agility, where he holds the rank of Jedi Master. When not teaching classes for DevelopMentor, he consults on the design and optimization of Microsoft-based systems.
Tim Ewald is an independent consultant who specializes in building distributed systems with .NET and XML. He is the author of Transactional COM+ (Addison-Wesley, 2001).