Recall: COM
Identity
From a client perspective, the rules of
AddRef and Release are fairly stringent. Unless
the client is careful about their use, objects can go away before
expected or can stay around too long. However, the object is
allowed to implement AddRef and Release in any
number of ways, depending on how it wants to manage its own
lifetimefor example, as a heap-based, stack-based, or cached
object.
On the other hand, QueryInterface is
easy to get right on the client side. Any client can ask an object
if it supports any other functionality with a simple call. However,
clients expect certain relationships between the interfaces on a
COM object. These expectations form the laws of COM identity.
The Laws of COM
Identity
The laws of COM identity say the following
things about how an object must expose its interfaces via
QueryInterface:
-
A client must be capable of getting directly to
any interface implemented by the object via
QueryInterface.
-
An object's interfaces must be static throughout
its lifetime.
-
QueryInterface for IUnknown
must always succeed and must always return the same pointer
value.
Direct Access to
All Interfaces
The COM Specification states that an
implementation of QueryInterface must be "reflexive,
symmetric, and transitive." This means that, given an interface, a
client must be capable of using an interface to get
directly to any interface implemented on the object, including the
interface the client is using to perform the query. These
relationships are mandated to maintain an object's identity in the
face of multiple references to the same object. If these
relationships are not upheld, a client could find itself with some
code that doesn't work just because it asked for the interfaces in
the wrong order. With a properly implemented
QueryInterface, query order does not matter.
Static Types
Each object can decide for itself whether it
wants to expose an interface via QueryInterface,
regardless of the class to which it belongs. However, after it has
been asked and has answered either "Yes, I support that interface"
or "No, I don't support that interface," it must stick to that
answer. The reason for this is simple: After an object answers the
query, it may never be asked again. For example, a client can pass
a resultant interface pointer to another client, which never has to
ask the object at all.
The potential for clients "talking among
themselves" means that an object cannot use QueryInterface
to make client-specific decisionsfor example, those based on
security constraints. The object also cannot use
QueryInterface to make context decisions that could change
during the life of an object, such as time of day. If a client
caches an interface pointer returned when the context is favorable,
it might not ask again when the context has changed.
An Object's
Apartment-Specific Identifier
The remoting layer of COM uses the pointer
returned when querying for IUnknown as an object's unique
identifier in that apartment. Clients can also compare
IUnknown*s as an identity test:
bool AreEqualObjects(IUnknown* punk1, IUnknown* punk2) {
if( punk1 == null && punk2 == null ) return true;
if( !punk1 || !punk2 ) return false;
IUnknown* punka = 0; punk1->QueryInterface(IID_IUnknown,
(void**)&punka);
IUnknown* punkb = 0; punk2->QueryInterface(IID_IUnknown,
(void**)&punkb);
bool b = (punka == punkb);
punka->Release(); punkb->Release();
return b;
}
In fact, the ATL smart pointer classes have a
method called IsEqualObject for performing just this
comparison:
STDMETHODIMP CBall::SetPlaySurface(IRollSurface* prsNew) {
if( m_sprs.IsEqualObject(prsNew) ) return S_OK;
...
}
However, although COM dictates that the pointer
value of IUnknown must always be the same, it places no
such restrictions on any other interface. This particular loophole
leads to such techniques as tear-off interfaces, discussed further
in this chapter.
Nothing Else
As long as these three laws are upheld, an
implementation of QueryInterface can be developed using
scenes from your most vivid fever dreams. Frankly, I doubt you'll
be able to come up with any techniques wackier than those already
known, as I present during the rest of this chapter. However, if
you do, ATL's implementation of QueryInterface is fully
extensible, as you'll see.
|