Context-Based Programming

[Previous] [Next]

We've just spent nearly an entire book examining Microsoft's version of the discipline known as interface-based programming—but hold on! A whole new development paradigm is emerging, and it's called context-based programming.

Before you get too worried, everything you've learned about interface-based development (specifically using ATL) holds true when you're doing context-based development. In fact, context-based development is integral to many aspects of COM development.

To get an idea of what context-based development is all about and to make understanding interception and contexts easier, we'll take a look at COM apartments and see how they affect developers of COM-based software. Interception and context already exist as solid concepts in Microsoft Transaction Server (MTS), and they'll play an important role in COM+ and Windows 2000, in which MTS will become Microsoft Component Services.

The Long and Winding Road to Thread Management

What a great industry we work in—just when you get a handle on the current technology, something new comes along. After years of developing software using APIs, we moved to an interface-based programming paradigm. And now that we have a handle on interface-based programming, we need to look at new ideas made possible by interception and context. To understand what lies ahead for the collective software development community, let's look back and review how certain aspects of software development have evolved.

Remember when Microsoft Windows NT (or OS/2 if you're a real diehard) came out? A whole API existed just for managing threads—it let you create threads, start threads, suspend threads, and even kill them if you wanted to. Because of the nature of threads, however, you had to assume that data visible to multiple threads could be accessed by those threads simultaneously. Of course, this accessibility created the possibility of data becoming corrupted.

To solve this problem, the Microsoft Win32 API provided a number of thread synchronization mechanisms, including mutexes, semaphores, events, and critical sections. (Mutexes, semaphores, and events are represented by handles, and critical sections are represented by structures.) These thread synchronization mechanisms all work in more or less the same way. As a thread goes along its merry way, it might decide to access a piece of global data. Before doing so, it tries to acquire a thread synchronization token. If the token is unavailable (that is, another thread acquired it first), the thread trying to acquire the synchronization token has to wait until the other thread frees the token.

The bottom line is that developers need to sprinkle their code with calls to API synchronization functions to make their code thread-safe. Inserting these calls and paying close attention avoids race conditions and deadlock. For example, if a developer uses a critical section to lock out other threads from accessing a resource, a certain function might call EnterCriticalSection, access the data, and then call LeaveCriticalSection.

One way to simplify the development of multithreaded software is to move the thread synchronization management code into the operating system. As we saw in the last chapter, that's exactly what COM does. In reviewing COM apartments, we'll get to see the concepts of interception and contexts firsthand.

Apartments and the Concept of Separation

Recall that COM is all about software distribution and integration. The only way to effectively distribute and integrate software components is to decouple the components as much as possible. To this end, the main tenet of COM is to hide from the client as many details about an object's implementation as possible.

As we've seen so far, by separating an object's programmatic interface from its implementation, COM systematically removes knowledge of an object's implementation from the client. Forcing the client to access an object through interfaces truly enforces encapsulation. By shoving the actual location of a server's DLL or EXE into the Registry, COM conceals an object's location from the client. Another detail removed from the client is information about the object's concurrency requirements. Clients should be able to use an object in a thread-intensive environment—whether or not the component was written to be thread-safe.

The mechanism that makes this separation of the client from the object implementation possible is the COM apartment. (See Chapter 2 for more information about COM apartments.) COM apartments are useful for grouping objects with the same concurrency requirements. Modern COM (Windows NT 4.0) defines two kinds of apartments: a single-threaded apartment (STA) and the multithreaded apartment (MTA). An STA can house only one thread, whereas the MTA can house many threads.

Client threads move into an apartment by calling a variant of CoInitialize (CoInitialize, CoInitializeEx, or OleInitialize). After a thread calls CoInitialize, it might choose to enter either the MTA or an STA. Then, as the thread executes, it might decide to create a COM object. If it does, when the client calls CoCreateInstance, COM looks in the class's entry in the Registry to find the kind of apartment into which the object wants to move. If the object's advertised apartment is compatible with the calling thread's apartment, the object moves into the same apartment as the calling thread. If the object's advertised apartment is incompatible with the calling thread's apartment, the object moves into a separate apartment, and the client thread has to access the object via a proxy. The proxy (on the client side) and the stub (on the object side) work in concert to serialize calls into the object, avoiding concurrent access to the object's state.

Interception

You can see the fundamental difference between COM software development during the 1990s and COM development as we head into the next century from the basic example of COM apartments, and it can be summed up in two words: interception and context.

Recall that in typical C++ development, nothing sits between your client code and the objects the client code creates. When your client code calls operator new to get a new object, it gets a raw pointer to the object. COM is all about indirection. When it creates a new COM object, your client code has to go through an API function such as CoCreateInstance. By inserting a layer of indirection between the client and the object, COM enables the operating system to intercept creation calls, giving COM the chance to perform intermediate processing such as setting up a separate apartment for the object to live in. For example, recall the process of creating an object that declares itself to be single threaded from a multithreaded environment:

  1. A client thread that is running along decides to call CoCreateInstance. The class advertises its single-threaded nature by placing the words Apartment Threaded in HKEY_CLASSES_ROOT\{…}\InprocServer32\ThreadingModel.
  2. COM examines the Registry and determines first that the client thread is running in the MTA, and second that the object needs to run in an STA.
  3. COM finds or creates a compatible environment, places the new object in that environment (an STA), and returns a pointer to a proxy for the client thread.

The proxy switches threads before and after method invocations to ensure that the object is protected and that the object's run-time environment, or apartment, is compatible with the class's requirements.

Notice the big advantage here: the method synchronization inside the object code is provided via the operating system instead of through lots of calls to some threading API.

You can see that if you're familiar with apartments, you already understand the basic notions underlying interception and contexts: COM intercepts calls to an object and makes sure that object lives in an appropriate context. Microsoft Component Services and COM+ in Windows 2000 expand on this idea of interception, layering other services using it.

How Interception Works in Windows 2000

COM+ and Microsoft Component Services provide advanced services such as concurrency management to components that want them (as does the Windows NT 4.0-style apartment model, in which COM components declare their concurrency requirements in the Registry). Components indicate their desires for these services via declarative attributes stored in the catalog.

Whereas in plain-vanilla COM you advertise the threading requirements of your object in the Registry, in Windows 2000 you use configured components. Instead of having to code certain services in your components, you can tack the services on at deployment by configuring them through deployment tools (as Microsoft Component Services Explorer does now).

Configured components are registered in the Catalog Manager. The Catalog Manager controls the HKEY_CLASSES_ROOT\CLSID entries in the Registry as well as a configuration database. Application-level information, such as the activation type (in-process DLL or surrogate) and details about configuring security, is included in configuring a class. The catalog also includes information about configuring interfaces, methods, and services such as transactions and object pooling.

The basic idea is that services can be layered onto your object by having COM intercept calls to the object. When COM intercepts a method call, it can provide preprocessing and postprocessing for the call, which allows COM to perform such services as setting up a run-time environment for your object (as the apartment architecture does now) and making system calls for you.

For interception to work, COM needs to know what the interface looks like so that it can manufacture the interceptors when necessary. To that end, an interface exposed by a configured component requires a type library or a specially prepared proxy/stub DLL. If you can use type-library marshaling for your interface (that is, your interface is VARIANT-compliant and marked with the Dual or the Ole_Automation attribute), you're good to go. Otherwise, you build a proxy/stub DLL after compiling the IDL code with the /Oicf switches thrown.

Once the component is deployed, CoCreateInstance checks the configuration settings (the auxiliary attribute information) at activation. Depending on how the component is configured, COM will either move the object into the same context as the calling thread or move the object into a different context. But what is a context?

Contexts

A context is a group of objects within a process. (Wow—sounds like an apartment, doesn't it?) Contexts separate incompatible objects from one another, just as apartments separate objects with different concurrency requirements.

In COM+, every object belongs to exactly one context. Objects that are incompatible (by even a single attribute) live in different contexts. However, a process can contain multiple contexts. (Notice that apartments work in much the same way.)

A call is made between incompatible contexts through an interceptor (a proxy). The proxy is created automatically by COM and implements the same interfaces as the real object. The proxy does whatever it takes to deal with the incompatibilities between two contexts.

Fortunately, contexts are a bit easier to grasp than apartments. Whereas apartments are more or less abstract entities in COM, contexts are represented by real objects inside a process. COM contexts are broken into two pieces: the per-method invocation context (named the call context) and the method-independent context (named the object context). Each context has properties. As a developer, you can access an object's call context and object context via the API functions CoGetCallContext and CoGetObjectContext. When you call these functions, interfaces that represent a call context or an object context are returned. Currently, the interface you can retrieve using CoGetCallContext is IServerSecurity, which provides information about the security settings. Here are the function prototypes for retrieving call contexts as well as the IServerSecurity interface:

 HRESULT CoGetCallContext(REFIID riid, void **ppInterface);     IServerSecurity : public IUnknown {     HRESULT QueryBlanket(DWORD* pAuthnSvc,      DWORD* pAuthzSvc,     OLECHAR** pServerPrincName,     DWORD* pAuthnLevel,     DWORD* pImpLevel,     void** pPrivs,     DWORD* pCapabilities) = 0;     HRESULT ImpersonateClient(void)=0;     HRESULT RevertToSelf(void) = 0;     BOOL IsImpersonating(void) = 0; }; 

An object's context exposes several interfaces, the most important of which is IObjectContext. Here's the prototype for retrieving an object's context and the IObjectContext interface:

 HRESULT CoGetObjectContext([in] REFIID riid,      [out, retval, iid_is(riid)] void **ppv); interface IObjectContext : public IUnknown {     HRESULT CreateInstance(REFCLSID rclsid,     REFIID riid,     LPVOID*ppv) = 0;     HRESULT SetComplete(void) = 0;     HRESULT SetAbort(void) = 0;     HRESULT EnableCommit(void) = 0;     HRESULT DisableCommit(void) = 0;     BOOL IsInTransaction(void) = 0;     BOOL IsSecurityEnabled(void) = 0;     HRESULT IsCallerInRole(BSTR bstrRole,     BOOL *bInRole) = 0; }; 

The context makes several different services available to the object; security is one example. The function IsCallerInRole (as shown in the preceding code for IObjectContext) enables role-based security. Role-based security lets you assign roles and access rights at deployment time, which in turn allows the objects to examine their callers to determine whether to let callers execute particular functions. This reduces programming complexity significantly—wrapping an object with a context makes it possible to layer orthogonal services on top of your object with the client being none the wiser.

Now let's take a closer look at one of the most important applications of interception and context-based programming—the COM way to handle transactions.



Inside Atl
Inside ATL (Programming Languages/C)
ISBN: 1572318589
EAN: 2147483647
Year: 1998
Pages: 127

flylib.com © 2008-2017.
If you may any questions please contact us: flylib@qtcs.net