When we first met QueryInterface, we learned that it was the fundamental mechanism through which a client could ask an object about the features it supported, by asking for pointers to specific interfaces. The QueryInterface function itself is quite simple: pass an IID and an out-parameter for the pointer, and if the function returns NOERROR, you have a new interface pointer. For example, if you have an IUnknown pointer in a variable pIUnknown and you want to ask an object whether it has any type information, query for IProvideClassInfo as follows:
//pIUnknown was obtained through other means.
IProvideClassInfo *pPCI;
HRESULT hr;
hr=pIUnknown->QueryInterface(IID_IProvideClassInfo, (void **)&pPCI);
if (SUCCEEDED(hr))
{
//Use pCPI to do whatever you want.
pCPI->Release();
}
else
{
//QueryInterface failed; object doesn't support interface.
}
The call to QueryInterface asks the object whether it supports a feature, and the feature is identified by the IID of the appropriate interface. If QueryInterface is successful, it will call AddRef through the out-parameter (&pCPI) before returning, so the client must call Release through that pointer when it is through with it.
There are a number of benefits to being able to ask this question. The first is that a client can make dynamic decisions about how to treat an object based on that object's capabilities, instead of rigidly compiling such behavior. If a query fails with one object, the client can take action different from what it would if the query succeeded. For example, a client that wants to work with any object's persistence model might first query for IPersistStorage. If that works, the client tells the object to save into an IStorage. If the query fails, the client can ask for IPersistStream, and if that works, have the object save into an IStream. Failing this second query, the client can try IPersistFile, and failing that, it could try to retrieve the object's native data through IDataObject. With this sort of code, the client would first work preferentially with storage-based persistence, then with stream-based, and then with file-based, and would then resort to other means of saving data for objects that don't support persistence at all.
A second benefit is that without successfully calling QueryInterface first you cannot possibly ask an object to perform any operation expressed through any interface. That is, in order to call an interface member function, you have to have a pointer to that interface. The only way to obtain such a pointer is by calling QueryInterface or by calling a creation function, which implicitly calls QueryInterface. If the object doesn't support the interface you request, it returns a NULL pointer, and you cannot make calls through a NULL pointer. Therefore, the object is always protected from malignant clients who think that they can bully objects into doing things the objects are not capable of doing. In other words, no one can insult you verbally in a language you don't understand! (I've heard people try; thankfully objects don't have emotions.) Contrast this with the traditional handle-based or structure-based sort of service APIs of the past, in which you can throw any garbage handle or any garbage structure to one of those API functions, and the function has to protect itself with all sorts of validation checks. This not only hurts performance but makes it very easy for bugs to creep into the code when you forget to validate something. In OLE, all validation on the function-call level happens in one place—QueryInterface—and validation is very simple to achieve, as we'll see in some code a little later.
The third benefit of QueryInterface lies in what we call "robust evolution of functionality over time." This deserves its own section.
The process of asking an object about the features it supports is also called interface negotiation, although it is a simple negotiation. The process allows any arbitrary client to dynamically (at run time) determine the largest number of interfaces that the object implements from the set of interfaces the client knows how to use. In other words, it allows the client to determine the largest intersection between the interfaces the client knows how to call and the interfaces the object implements. The more interfaces the two share, the richer the integration the two can achieve.
As an analogy, consider each human language as an interface, which is really an accurate description. Let's say that I work at the United Nations in New York and that I speak English and German. I walk into a room with 10 international delegates with whom I need to discuss a few issues. I go up to one of the delegates and ask, "Do you speak English?" This query is met with an affirmative "Yes." Great, now we can talk. Partway through our conversation, I find that I simply cannot express one of my ideas in English, but I know I could express it in German—some languages have words without equivalents in other languages. So I ask, "Sprechen Sie Deutsch?" To this, the other person responds, "Ja." Because my partner also speaks German, I can now express my idea in that language, and the integration between us is much richer than if I were talking to someone who spoke only English. (The nice thing about IUnknown is that all objects speak that language, so there is always some rudimentary form of snort-and-grunt communication that you can use.)
The point is that the ability to communicate is limited by the number of languages, or interfaces, two components have in common, which is determined at run time. This is a vast improvement over building components that are hard-coded at compile time to work with some least common denominator and that are thus unable to take advantage of a richer component should it appear in the future. QueryInterface allows you to create a client or an object with as many features as you see fit that will work perfectly well with another component that doesn't necessarily support all of those features. For example, if I learn another language, perhaps Spanish, I don't lose any compatibility with my friend at the UN who speaks only English and German. If she goes on to learn Spanish and French, we can continue to communicate not only in English and German but now also in Spanish. If I then learn French and Russian, we add yet another language of integration, and so on.
The process whereby components add capabilities and features yet still remain compatible with one another through the changes is exactly the idea of "robust evolution of functionality over time." Not only is the idea powerful, but it is also very efficient because the negotiation happens on an interface-by-interface level, and not a function-by-function level, which would require much more overhead.
To illustrate the extent to which QueryInterface is a true cornerstone in COM, let's imagine that we have a client that wants to display the contents of a number of text files and that knows that for each file format (ASCII, RTF, Unicode, and so on) there is some component class associated with that format. (By "associated" I mean that we don't know what that component can do with the format, but we know that it exists.) The client's ultimate purpose is to display the contents of these files by using as much of the component as possible to do the work. We would write the client as follows:
Find the component class associated with a file format.
Instantiate an object of that class, obtaining a pointer to IUnknown in return.
Check whether the object supports loading data from a file by calling IUnknown::QueryInterface and requesting a pointer to IPersistFile. If that's successful, ask the object to load the file through IPersistFile::Load.
Check whether the object can provide a metafile or bitmap rendering of the file contents that the client could draw in its own window. Such renderings are obtained through IDataObject, so queries for this interface are made through either IUnknown or IPersistFile pointers. If successful, ask the object for a rendering and draw it on the screen.
If a component class exists for every file format in the client's file list, and all those objects implement all three interfaces, the client can display all the contents of all the files. But in an imperfect world, the object class for the ASCII text format might not support IDataObject—that is, the object can load text from a file and save the text to another file if necessary, but it can't render the text into a graphical format. When the client code, written as described above, encounters this object, the QueryInterface for IDataObject fails, and the contents are not viewable. Oh well….
The ASCII component programmers now realize that they are losing market share because they don't support graphical rendering, so they update the component to support IDataObject. An end user installs this new component on the machine that has the existing client there already. Nothing else changes in the entire system but the ASCII component. What happens the next time someone runs the same old client?
Because of QueryInterface, the client immediately begins to use IDataObject on the updated component. Where before the query for this interface failed, it now succeeds, and the client can now retrieve a rendering and display ASCII file contents.
This again is the raw power of QueryInterface: you can write a client to exploit as many interfaces and as much functionality as you want for whatever component you encounter. Ideally you would like to have components that support everything you do, but that is not generally the case. When your client encounters such less capable components, you still use as much functionality as those components actually implement. When the object is updated later to support new interfaces, the same client, without any recompilation, redeployment, or changes whatsoever, automatically takes advantage of those additional interfaces. This is true component software: components evolve independently and retain full compatibility.
This process also works in the other direction. Imagine that since the client application described above was shipped, many of the components were improved by adding support for the IViewObject2 interface so that instead of always having to ask an object for a rendering, the client could now ask an object to draw directly in the client's window. Each component is upgraded independently of the client, but because the client never queries for IViewObject2, all components continue to work perfectly. By implementing this new functionality—this additional interface—the components do not lose compatibility with the existing client and require no changes at all to the client.
At a later time, however, we might notice that the client isn't taking advantage of the performance improvements that could be realized if it supported direct rendering through the improved display components. Traditionally, before COM and QueryInterface, we'd worry tremendously about whether we should implement the new functionality and lose compatibility with components that still don't support direct rendering or whether we should simply not implement the new functionality at all and suffer from poor performance. Either way, black-or-white decisions such as these are difficult.
However, such concerns are totally irrelevant with QueryInterface. We simply add step 3a to the client's earlier steps: after we have the component load the file, we query for IViewObject2. If that interface is supported, we call IViewObject2::Draw to perform the high-speed, high-quality direct rendering. If the query fails, we can still resort to the old method of using IDataObject. With this simple addition to the client, we work optimally with newer components while still working with old components. We didn't have to change any of the code we used for working with old objects, and so we didn't risk any loss of compatibility. Support for the new feature was accomplished entirely through the addition of code, not the modification of old code.
Of course, the client might also add support for some new interface at this time, even though no components yet support it. When they do, the client will immediately begin to be integrated with those components through the new interface. The objects can leapfrog the client once again, with even newer interfaces. This process continues, back and forth, ad infinitum.
Before COM, repeated and independent versioning of components and clients such as this was simply not possible: new features required upgrading all clients and all components together. Yuck! But now, and for all time, QueryInterface solves the problem and removes the barriers. Time is ripe for rapid software innovation without the growing pains.
To wrap up our discussion of QueryInterface, let's look at a number of rules concerning its behavior. The first rule is that any call to QueryInterface asking for IUnknown through any interface on the object must always return the same pointer value, which is the run-time identity of the object instance. The specific reasoning for this is that given two arbitrary interface pointers, you can determine whether they belong to the same object by asking each for an IUnknown pointer and comparing the actual pointer values. If they match, application of this rule allows both interface pointers to refer to the same object.
The second rule is that after an object is instantiated, it must support the same interfaces throughout its lifetime: if QueryInterface succeeds once for a given IID, it must succeed again until that object is destroyed. This does not mean that the exact pointer values returned from both calls will be identical—it means only that the interface is always available. Again, this applies to a single instantiation of an object, not the class. Different instances of objects from the same class can support different sets of interfaces as long as the available interfaces are stable through each object's lifetime.
The third rule is that any implementation of QueryInterface must be reflexive, symmetric, and transitive, as described in the table on the following page (in which IOne, ITwo, and IThree are hypothetical).
Property | Meaning |
Reflexive | pIOne->QueryInterface(IID_IOne) must succeed. |
Symmetric | If pITwo was obtained from pIOne->QueryInterface(IID_ITwo), then pITwo->QueryInterface(IID_IOne) must also succeed. |
Transitive | If pITwo was obtained from pIOne->QueryInterface(IID_ITwo) and pIThree was obtained from pITwo->QueryInterface(IID_IThree), then pIThree->QueryInterface(IID_IOne) must also succeed. |
In all these cases, "must succeed" is not so strong as to imply that these calls cannot fail under the most catastrophic situations. In addition, these properties do not require that the same pointer value be always returned for a given interface, with the exception of IUnknown.