Home > Articles > Software Development & Management > Object Technology

  • Print
  • + Share This
From the author of

Limiting Context

If a class uses one or more COM+ runtime services, each instance of that class needs a context configured to suit its needs. But what if a class does not use any services? Consider a class that encapsulates the details of validating some sort of data. Other classes use this helper class to validate input passed to them by client programs, but client programs never use the validation class directly. The implementation of the validation class simply examines the data it receives and returns a boolean value indicating whether it is valid. Classes that use the validation class interpret its boolean return code to decide whether to proceed with a client request. The validation class neither knows nor cares about context; it does not rely on it and does not do anything with it. It never calls either CoGetObjectContext or CoGetCallContext. Each instance of the validation class can do its job in any environment because it is oblivious of the fact that there is an environment. Each validation class instance can accomplish its task without the additional space overhead of a new context or the additional time overhead of calls through the interception layer. Given that, the validation class should not be a normal configured class. If it were, the default catalog settings would force each instance into a new context of its own, which is neither necessary nor efficient. Instead, the validation class should be registered such that the SCM always puts each new instance directly in its creator's context.

Nonconfigured Classes

One way to achieve this effect is by using a nonconfigured class. Nonconfigured classes have entries in the Registry, but not in the catalog. Remember that the SCM puts a new object into a new context and introduces interception only if its creator's context cannot meet its needs. If the creator's context is a suitable environment for the new object, the SCM will put the object there and return a raw pointer to it. The SCM assumes that nonconfigured classes do not know and do not care about context; if they did, they would be configured classes with specific declarative attributes recorded in the catalog. The SCM always puts new instances of nonconfigured classes directly into their creators' contexts, and CoCreateInstance[Ex] always returns a raw reference, never a proxy, to the new object. (There is one exception to this rule related to threading, which is addressed in Chapter 4.)

Raw-Configured Classes

It is possible to set a configured class's declarative attributes so that the SCM treats it like a nonconfigured class at creation time. If a configured class is registered this way, each new instance will always live in its creator's context. Here too CoCreateInstance[Ex] will always return a raw reference to the new object, so I call configured classes that are registered this way raw-configured classes.

Raw-configured classes turn off all the runtime services that are implemented via interception, which is just about all of them. (Later chapters explain how raw-configured classes should set their attributes to turn these features off.) Raw-configured classes can use a constructor string (Construction Enabled = true, ConstructorString = "This string will be passed to every new instance of this class") if they want to. They can also use the object pooling service if they want to.2 Neither of these features is implemented via interception, so turning them on does not force a new object into a new context.

You should also set a raw-configured class's MustRunInClientContext attribute, described in Table 3-3, to true. This boolean flag tells the SCM that instances of a class must activate in their creator's context. If the SCM cannot meet this requirement for some reason, it fails the activation request and returns CO_E_ATTEMPT_TO_CREATE_OUTSIDE_CLIENT_CONTEXT. Whether or not a runtime services requires interception may vary depending on the configuration of a creator's context. By enabling a raw-configured class's MustRunInClientContext attribute, you can ensure that its instances will use services only when interception is not required.3

Table 3-3 The MustRunInClientContext class attributes

Attribute

Type

Default

Notes

MustRunInClientContext

Boolean

False

If SCM cannot put new instances in creator's context, activation fails


Raw-configured classes are typically installed in a library application of their own. This makes it possible to load their code into any process where a context might exist. It also solves a thorny security problem. The SCM always puts new instances of classes deployed in an application configured to support component-level security into individual contexts of their own. To avoid this, you have to disable support for component-level security by setting an application's AccessChecksLevel attribute, which is shown in Table 3-4, to COMAdminAccessChecksApplicationLevel.

Table 3-4 The AccessChecksLevel application attribute

Attribute

Type

Default

Notes

AccessChecks

COMAdmin

COMAdminAccess

Controls granularity

AccessChecks

ChecksApplication

ComponentLevel

Level of security access checks


If raw-configured classes are deployed in a library application of their own, you can disable component-level security for that application without affecting other applications. In other words, the configured classes in other server or library applications that use your raw-configured classes can still use component-level security if they want to.

Figure 3-12 shows how a raw-configured class is deployed and used. In this case, object A is an instance of a normal configured class. The SCM put it in its own context A when it was created. Object A creates object B, an instance of a raw-configured class deployed in a library application with component-level security turned off. The SCM puts object B in context A.

Figure 3-12
Using a raw-configured class

There are three advantages to using raw-configured classes instead of nonconfigured classes. First, raw-configured classes have entries in the catalog. Nonconfigured classes do not appear in the catalog; they are registered directly in the Registry using classic COM techniques. Having all your classes in the catalog makes it easier to manage your system's configuration because you can find all your classes' configuration information in one place.

Second, raw-configured classes are part of an application, usually a library application. Applications are the standard unit of deployment in COM+. Because there are tools for exporting applications and installing them on other machines, being in an application can help ease configuration management.

Third, as mentioned before, raw-configured classes can use the constructor string mechanism and the object pooling service because neither relies on interception. For all these reasons, in general, raw-configured classes are preferred. The rest of this book discusses raw-configured classes and ignores nonconfigured classes except in situations where their behavior differs.

Context-Aware Code Revisited

Each new instance of a raw-configured class lives in its creator's context. If a class is aware of that context and its implementation calls CoGetObjectContext or CoGetCallContext, can it still be a raw-configured class? The answer is a qualified yes. Remember that COM+ maps runtime services to contexts, not to individual objects. Any code executing in a context can make use of the services the context provides. Developing a context-aware raw-configured class is fine as long as you guarantee that it behaves correctly in any context. Consider a class that encapsulates, sending event notifications using Microsoft Message Queue (MSMQ). MSMQ relies on transactions to guarantee that messages are delivered in the order they were sent and that each message arrives exactly once. The event notification class can be implemented as a raw-configured class that uses a context's declarative transaction if there is one, or an internal MSMQ transaction if there is not. In short, this class can be used from any context. Its instances do not need new contexts of their own because their environmental needs are always the same as the environmental needs of their creators.

This may seem a risky way to design your classes, but it is not. It is simply leveraging your understanding of how contexts and interception work. Consider a method call into an instance of a configured class running in some context. The COM+ plumbing intercepts the call and makes sure the appropriate runtime services are invoked. Inside the method, the object can access context by calling either CoGetObjectContext or CoGetCallContext. If the object calls another one of its own methods, that method can access context by calling one of these APIs as well. If the object creates a language-level object and calls one of its methods, that method can also access context. Finally, if the object creates another COM object in the same context and calls one of its methods, then that method can access context, too. This is exactly what happens when a configured class makes use of a raw-configured class, as shown in Figure 3-13.

Figure 3-13
How a context-aware raw-configured class works

If object A' is an instance of a raw-configured class, it can acquire references to context A's object context because it is executing on a thread in context A. It can retrieve a reference to the current call context, too. Remember that both CoGetObjectContext and CoGetCallContext retrieve references to context based solely on the calling thread. Neither API cares which object is calling it. Any code a thread executes can retrieve either reference, until the thread passes through another interceptor and they are either removed or replaced. If object A called another one of its own methods, that second method could access context. Object A' accesses context the same way. Putting context-aware code into a nonconfigured or raw-configured class makes it a reusable, updatable, deployable unit that can be written in any language you like. In short, it gives you all the advantages of traditional in-process classic COM without the additional overhead of a new context and interception incurred by each instance of a configured class using the same services as its creator.

There is no real risk in this approach as long as your raw-configured classes behave correctly no matter what services the context they are created in provides. Remember that, while a class can use the current object context to find out about its environment, it cannot find out everything about its context. It is up to you to ensure that your raw-configured classes do not depend on aspects of context that cannot be detected. This is not hard once you understand how the COM+ services work.

If this seems to be taking advantage of some kind of undocumented loophole that might someday be closed, consider three things. First, key pieces of the COM+ infrastructure rely on nonconfigured and raw-configured classes. OLE DB and ADO use nonconfigured classes so they execute in their creator's context specifically so they can access and automatically enlist against a declarative transaction if one is present. Internet Information Server (IIS) uses raw-configured classes to dispatch HTTP requests. If the basic system plumbing relies on this feature, you can too.

Second, the MustRunInClientContext attribute exists specifically to allow a configured class to insist that it wants to run in its creator's context, as a raw-configured class does. As you will see in later chapters, other services provide attribute settings specifically designed to support raw-configured classes. The creators of COM+ designed the runtime with the notion of raw-configured classes in mind.

Third, if for some reason the context and interception architecture were changed such that new instances of raw-configured classes were no longer put in their creators' contexts, classes that are not aware of context will not care, and classes that are aware of context can simply be redeployed as configured classes with the right options set.

A Different Way of Looking at the World

Using raw-configured classes may seem like a strange idea, especially if they make use of context. It definitely represents a very different way of looking at the world of contexts. It reduces the number of contexts that exist, lowering memory consumption. It also reduces the number of calls through the interception plumbing, because calls to instances of raw-configured classes are not intercepted.

Here is an extended version of the formula introduced earlier for estimating the memory consumption of a COM+ process. This version still assumes the object-per-client model, but it has been extended to consider the impact of using instances of raw-configured classes.

((n2 x (s2 + 3,072)) + (n1 x (s1 + 2,048)) + (n0 x s0)) ÷ 1,024 = k KB

The new variable n0 represents the total number of objects that can be accessed without a proxy—degree 0 interception. Instances of raw-configured classes fall into this category. The new variable s0 is the average size of the n0 objects in bytes.

In the earlier example, 500 clients each used a single object in a server application process, so n2 = 500. Each of those objects used three additional objects that lived in contexts of their own that could be reached without a thread switch, so n1 = 1,500. All together, there were 2,000 objects in 2,000 contexts. With objects that consumed an average of s2 = s1 = 32 bytes of memory, the total footprint for the process was approximately k = 4,560 KB, or 4.5 MB. However, if each of the n2 = 500 clients' objects used three additional objects that did not have to live in their own contexts, memory consumption would be greatly reduced. If each of those additional objects' requirements could be met by their creator's context—either because they do not care about context or because they rely on the same services their creator needs—they could live there and be accessed directly. In this case, n2 = 500, n1 = 0, and n0 = (500 x 3) = 1,500. Assuming the objects are still the same average size, s2 = so = 32 bytes, the total memory consumed by the process drops to approximately k = 1,560 KB, or 1.5 MB. At 62 KB, the object still represent only 4 percent of that total; however, there is a 66 percent reduction in the memory consumed by contexts.

Figure 3-14 shows the memory consumption statistics for this new scenario with four average sizes for objects. Again the vertical axis measures memory consumption in KB, and the horizontal measures average object size. The shaded area at the bottom of each bar still represents the space consumed by contexts and interception plumbing. It is the same in all four cases because there are always 500 contexts. It is 66 percent lower than the previous case, shown in Figure 3-11, because 1,500 fewer contexts exist. The numbers that are above the bars indicate the percentage of memory the objects consume in each case. They are all higher than in the previous case because the amount of memory consumed by contexts has decreased.

Figure 3-14
Memory consumed by 2,000 objects in 500 contexts

Two things should be apparent from these numbers. First, reducing the number of contexts dramatically reduces the overall memory consumption of a process and dramatically increases the percentage of memory dedicated to objects themselves. Second, the larger the objects get, the less beneficial this reduction is. However, as noted earlier, objects would have to be quite large indeed to dull the impact of putting each one in a new context of its own.

Some might argue that this is a premature optimization that is too dependent on the implementation details of the COM+ plumbing, but I do not see it that way. This approach simply uses contexts efficiently, never paying more than is necessary to leverage the services COM+ provides. It is codified in Rule 3.2.


Rule 3.2

Give raw-configured classes preference over configured classes whenever possible to reduce context and interception overhead. Always use this technique for classes that do not care about context. Consider using this technique for classes that do care about context if you can guarantee that they will behave correctly in any context.


Subtle Complexities

Using an instance of a raw-configured class within a single context is fine; however, returning a reference to one of these objects to a caller in another context can lead to some subtle complexities. A reference to an object must be marshaled when it is passed from one context to another. This happens automatically when interface pointers are passed as arguments to and from calls to COM methods and APIs. If a client calls to an instance of a configured class living in a context, and that object returns a reference to an instance of a raw-configured class living in the same context, the interface pointer it returns will be marshaled. The client will get a reference to a proxy that forwards calls directly to the instance of the raw-configured class. Figure 3-15 illustrates this situation.

Figure 3-15
An external reference to an instance of a raw-configured class

In Figure 3-15, a client created object A, an instance of a configured class that the SCM put into context A. Object A created object A', an instance of a raw-configured class that the SCM also put into context A. Object A returned a reference to object A' to its client. What exactly happens when the client calls to object A' depends on the configuration of context A.

Remember that runtime services are applied to contexts, not objects. A new context is configured to provide the services an initial new object needs, as defined by its class's declarative attributes. That first object is known as the context's distinguished object because it is the object the context was originally constructed for.4 COM+ implements services by intercepting calls and invoking infrastructure code as needed. This happens not for calls to a distinguished object, but for all calls into a context. In the example, the client's calls directly to object A' will be intercepted at the context boundary, and whatever runtime services the distinguished object A requires will be applied. This may or may not present a problem.

Imagine that object A is an instance of a configured class set up to use the COM+ statistics service (it is registered in the catalog with EventTrackingEnabled = true). The SCM makes sure context A is set up to supply this service. All calls into context A are intercepted so statistics about the use of class A can be gathered. If the client calls to object A' in context A, the call will still be intercepted, and the statistics service will record the call as an invocation on an instance of class A. This may or may not be a problem. It is certainly misleading.

Now imagine that object A is an instance of a configured class set up to use role-based security. Assume it is configured to allow the user Alice to call any method of the interface IA and to deny calls from any other users or through any other interfaces. This security check is implemented automatically via interception when calls to object A enter context A. The reference to object A' that object A returns to the client is of type IA'. If the client attempts to call to object A' the call will be intercepted at the context boundary. Context A's environment is configured only to allow Alice to call through interface IA. Calls through IA', even if Alice makes them, are not allowed. So the client's attempt to call IA' will be rejected with the standard error code E_ACCESSDENIED. This is a problem.

What exactly happens when a client calls to an instance of a raw-configured class running in a context originally created for an instance of some other configured class depends on the specific combination of services the configured class is using. The exact behavior is difficult to predict and, in some cases, can be very strange indeed. You should avoid this situation by following Rule 3.3.


Rule 3.3

Use instances of raw-configured (or nonconfigured) classes to help implement the methods of configured classes, but do not return references to them to callers.


Custom Marshaling

Actually, there is one exception to Rule 3.3. In the example above, it would be okay for object A to return a reference to object A' to the client if object A' supported some form of custom marshaling, such as, marshal-by-value or free-threaded marshaling. Custom marshaling allows a COM object to control what happens when its interface pointers are passed from one context to another. An object expresses its desire to custom marshal by implementing the standard IMarshal interface. If an object exposes this interface, it will never be attached to a standard proxy/channel/stub connection. Without that, there is no interception, runtime services are never invoked, and the problems outlined never occur.

It is a common misconception that instances of configured classes cannot custom marshal; this is not the case. It is the case that a context's distinguished object cannot custom marshal. If the SCM creates a new context to meet a new object's needs, the object is going to live there, and calls to it are going to be intercepted. Many configured classes use runtime services that make each instance the distinguished object in its own context, so it is natural to assume that instances of configured classes cannot custom marshal. However, the SCM never creates new contexts for instances of raw-configured classes, and they can custom marshal if they want to. In fact, this is exactly how ADO's disconnected Recordsets—instances of a nonconfigured class—work. If you are planning to return a reference to an instance of a raw-configured class to a caller outside the context the object lives in, the object should definitely custom marshal. This is Rule 3.4.


Rule 3.4

If a configured class does return a reference to an instance of a raw-configured class to a caller, make sure the raw-configured class uses custom marshaling.


Footnotes

2. Chapter 5, Objects, addresses object pooling in detail.

3. Chapter 4, Threads, provides an excellent example of why the MustRunInClientContext attribute is useful.

4. As you'll see in later chapters, some runtime services are tied directly to a context's distinguished object.

  • + Share This
  • 🔖 Save To Your Account