Object Identity

[Previous] [Next]

Before tackling COM identity, let's consider the meaning of identity for a normal C++ class. When you want an instance of a C++ class, you simply call on operator new to produce one for you. If operator new succeeds, you get back a pointer to the top of the object, after which the compiler lets you access any of the public data members and functions. In the following example, notice how operator new gives you a pointer to the object in its entirety:

 class CSomeClass { public:     short x;     void DoIt();     void DoItAgain(); }; CSomeClass* pSomeClass = new CSomeClass; pSomeClass->x = 42; pSomeClass->DoIt(); pSomeClass->DoItAgain(); 

In the example above, pSomeClass represents the object's identity; that is, pSomeClass represents the whole object and nothing but the object. If you need to know the identity of the C++ object, you can cast the pointer to the object using void*, like this:

 CSomeClass* pSomeClass = new CSomeClass; void* pvIdentity = NULL; pvIdentity = (void*) pSomeClass; 

Doing this establishes the C++ object's position in memory, which turns out to represent the object's run-time identity.

Recall that a COM object is just some piece of code (remember—a COM object is a component, so the internal workings should be left as implementation details). What distinguishes the COM object from random memory is that the COM object implements a COM interface (a function table) named IUnknown. IUnknown includes three functions, which can be expressed in C++ like this:

 struct IUnknown {     virtual HRESULT QueryInterface(REFIID riid,          void** ppv) = 0;     virtual ULONG AddRef() = 0;     virtual ULONG Release() = 0; }; 

As you saw in Chapter 2, the key to grasping IUnknown is to understand these points:

  • A COM object needs to implement these three functions.
  • All COM interfaces you'll ever encounter begin with these three functions.

Remember that there's no this pointer when it comes to binary objects, which means that IUnknown establishes the identity of a COM object.

Most C++ developers are comfortable writing client code that talks directly to a C++ object. In COM—unlike normal C++—you never talk to an actual object. Instead, you talk to one of the object's interfaces. For example, a similar COM-based example of working with an object might look like this:

 struct ISomeInterface  : IUnknown {     virtual void DoIt() = 0; }; ISomeInterface* pSomeInterface; CoCreateInstance(CLSID_CoSomeClass,     NULL,     CLSCTX_ALL,     ISomeInterface,     (void**)&pSomeInterface); if(pSomeInterface) {     pSomeInterface->DoIt();     pSomeInterface->Release(); } 

When the client code talks to pSomeInterface, the client isn't touching the actual instance of CoSomeClass—it's touching an interface attached to the CoSomeClass object. Now imagine that CoSomeClass implements another interface named IAnotherInterface. Once the client obtains an interface (any interface) to the object, the client can try to (arbitrarily) widen its connection to the object, like this:

 struct IAnotherInterface : IUnknown {     void DoItAgain(); } void UseSomeInterface(ISomeInterface *pSomeInterface) {     IAnotherInterface* pAnotherInterface = 0;     pSomeInterface->QueryInterface(IID_IAnotherInterface,          (void**)&pAnotherInterface);     if(pAnotherInterface) {         pAnotherInterface->DoItAgain();         pAnotherInterface->Release();     } } 

The client can do this because QueryInterface (a member of the IUnknown interface) is the first function of every COM interface you'll ever see.

At this point, we're staring the notion of COM and object identity right in the face. Remember the lollipop diagrams that appear so often throughout the COM literature? Each lollipop represents an interface, and the rounded rectangle represents the body of code implementing the interface.

Switching Interfaces

COM's IUnknown interface allows objects to have multiple personalities. CoSomeObject understands how to implement two interfaces—ISomeInterface and IAnotherInterface. A client can hold a pointer to ISomeInterface and a pointer to IAnotherInterface, and both pointers might be pointing to the same object.

By permitting this functionality (the mechanism for arbitrarily widening the connection to an object at run time), COM effectively provides a means for extending objects arbitrarily without breaking old clients. Older clients that understand the existing interfaces remain content because the interfaces still exist. Clients that want to use the newer interfaces simply have to ask for a new interface.

In COM, IUnknown represents a pointer to an object; that is, IUnknown denotes an object's identity. COM establishes some basic rules about COM identity. At first, these rules seem unimportant and perhaps a bit pointless. However, once you understand these rules, you can use them to your advantage and compose COM classes by a variety of means.

COM's Identity Rules

To help remember COM's rules of identity, just remember three letters: STR. IUnknown::QueryInterface is symmetric, transitive, and reflexive.

COM interfaces are symmetric. For example, if you're using the ISomeInterface pointer on CoSomeObject, and you call QueryInterface to successfully retrieve IAnotherInterface, you should be able to call QueryInterface through IAnotherInterface to successfully retrieve the ISomeInterface again. COM interfaces are also reflexive. If you're using the ISomeInterface pointer on CoSomeObject, you should be able to successfully call QueryInterface to retrieve ISomeInterface again. Finally, COM interfaces are transitive. Let's say you're using ISomeInterface on the CoSomeObject, and you call QueryInterface and successfully retrieve IAnotherInterface. Now imagine that you're using IAnotherInterface to retrieve the IUnknown pointer. If that succeeds, you should be able to call QueryInterface through ISomeInterface to successfully retrieve the IUnknown interface.

The upshot of these rules is that the interfaces on a COM object are peers—you can get from one interface to any other interface that is implemented by an object. The rules of symmetry, transitivity, and reflexivity make it unnecessary for COM client code authors to worry about the order in which interfaces are acquired. If a COM object required interfaces to be acquired in a certain order, any COM code that didn't adhere to that order or that changed the order of acquisition between versions would break.

In addition to the rules of symmetry, transitivity, and reflexivity, COM imposes two other stipulations on interfaces. One stipulation is that COM requires a query for IUnknown always to return the same actual pointer value. This requirement allows clients to compare two IUnknown pointers to determine whether they both point to the same object. However, it's OK for QueryInterface to return different pointers whenever it is called to acquire other interface pointers. As we'll see in a moment, the fact that QueryInterface can return different pointers when asked to render the same interface more than once lets you implement objects in a variety of ways, including using tear-offs and aggregation.

The second stipulation is that COM requires the set of interfaces accessible on an object via QueryInterface to be static during the lifetime of the object instance. In other words, if you call QueryInterface to retrieve ISomeInterface once during the life of the object, you should be able to retrieve ISomeInterface again at any time during the life of the object. If a query for ISomeInterface through QueryInterface fails once, the same query must fail for the life of the object. This means that you can't implement QueryInterface in such a way that it succeeds only under certain conditions. The main reason for making the available set of interfaces static during the life of the object is that COM doesn't guarantee that all client QueryInterface requests will be forwarded to the object when accessed remotely. Client-side proxies can take advantage of this fact to cache the results of QueryInterface and avoid excessive client-object communications, thereby reducing the number of round-trips and ultimately increasing the network performance of the object.

So now that you know the COM identity rules, what good are they? The easiest way to make sense of these rules is to remember that all interfaces supported during the life of an object end up being peers. The interfaces form more of a landscape than a hierarchy. These rules are important because if you fail to adhere to them, the remoting layer will yell at you. Additionally, understanding these rules is critical to making sense of the various class composition techniques.

Now we're ready to take a look at how ATL handles QueryInterface and examine some class composition techniques to see how they apply to ATL.



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