About Us
Education and Training
Consulting
Gifts
Contacting Us
Employment
Journal
Journal

Home

The Pantheon Systems Journal
Using the Component Object Model (COM)
Part II

By Niranjan Ramakrishnan


The previous article in this series ended with an introduction to IUnknown, which forms the essence of COM interface-building. In this article we shall examine IUnknown in greater depth.

Why IUnknown?

The universal approach

When somebody gives you their telephone number, you dial it and are able to get through to them. Whether they live in one part of town or another (of course, in some places this matters, since a long distance means you have to dial a 1, etc.) is of no account to you.You are not concerned about what color their telephone is, where it sits in their home or office, whether it is a cordless or a corded phone, or even whether it is their "real" phone number or just an forwarded one. You dial, and someone puts you through.

That 'someone', here, is COM. It manages the entire universe of phone numbers, in this sense. Instead of our having to figure out the connections, connectivity, and making sure the signals get to the proper destination, COM does so. But for this to happen, COM relies on two standards. One of these is IUnknown. The other is UUID, of which more later.

IUnknown

"The COM architecture provides a top-level interface called IUnknown. It is useful because it provides standard ways of examining an object. It is rather like a front-desk of an office. Whenever a visitor wants to interact with anyone in the office, they go via the front-desk. So, anyone who knows how to inquire at the front-desk knows how to deal with the system to be able to get further information. Thus, all inquiries can be viewed as extensions of a basic inquiry to the front-desk. Similarly in COM, all interfaces are derived from IUnknown. IUnknown has three standard methods - one of which we shall use for our illustration. This method is called QueryInterface(). QueryInterface() allows us to inquire if an object supports a particular interface. For example, we might want to first find out if the store provides an ATM before going ahead with our use of it."

That was our introduction to IUnknown in our last article. Now let's look at the three methods that IUnknown has - QueryInterface(), AddRef(), and Release().

The IUnknown Interface
//=============================================================
class IUnknown {
//=============================================================
   virtual HRESULT QueryInterface(const IID& iid, void** ppv) = 0;
   virtual ULONG AddRef() = 0;
   virtual ULONG Release() = 0;
};
The IUnknown Interface

QueryInterface()

All three functions in IUnknown are pure virtuals (must be overridden in a derived class). Of these, QueryInterface() is the most important. It is rather like the constructor of a class. Users of the interface will use it to obtain a handle to the interface. If successful, it returns a pointer to an instance of an interface. Underneath, there is an actual object that gets instantiated (or is already in existence) which supports this interface.

AddRef() and Release()

AddRef() and Release() are a standard way for a COM object to keep track of its lifetime. Let us look at both functions in some detail.

Reference Counting

Why reference counting?

As we have seen, the fundamental feature of COM usage is the interface. When a client uses a COM object, what the client deals with is not the object itself, but an interface. Which brings up our next question, when does a COM object come into existence, and when does it go away? The answer is, the client does not know. When the client acquires a reference to an interface, all the client knows is that there is an object which supports such an interface. The client does not know or care where the object is located, as we have seen. Similarly, the client does not know when the object is created and when it goes away.

But that opens a more troubling question. When does an object get discarded? Clearly, when an interface exists, an object must exist to support that interface. But if the client has no control over deleting the object, and the client does not know where the object is, then how does the client discard the object once the client is done with it? This is where AddRef() and Release() make sense.

The functions AddRef() and Release() are pure virtual, functions belonging to IUnknown(). Thus, any object that implements an interface is guaranteed to have implementations for AddRef() and Release(). The client does not have to worry about the maintenance of references. So long as the client follows a few simple rules, reference counting works properly. What are these rules?

Reference Counting Rules

There are three basic rules of reference counting, quite easy to understand from a simple, underlying logic. Whenever a new reference to an object is created, the reference count must go up. Whenever a reference to an object is destroyed, the reference count must go down.

To bump up the reference count, we use AddRef(). To bump down the reference count, we use Release().

So, we use AddRef() when
  • We create a reference to an interface,
  • We assign a reference to an interface to another reference to an interface.
We use Release() when
  • We are done with an interface, and wish to get rid of it.


Note that calling AddRef() and Release() are not essential to the client logic. However, these are the rules by which clients must play to maintain "good order" among the objects they deal with. Not doing so could result in a range of bad results, from memory leaks to objects being destroyed while they are still in use.

Using AddRef() and Release()

Here's an example of a COM object which implements a COM interface.

The Store
//===================================================
class Store : public IUnknown {
//===================================================
   public:
      HRESULT QueryInterface(const IID& iid, void** ppv) {
         // IID stands for interface id
         // ppv is for returning a pointer to the interface
      }
      ULONG AddRef() {
         // Add to reference count, 
         //  return number of references
      }
      ULONG Release() {
         // Add to reference count, 
         //  return number of references
      }
      void deposit(double amt) { // ATM-specific COM method
      }
      void withdraw(double amt) { // ATM-specific COM method
      }
      double balance() { // ATM-specific COM method
      }

private:
   ATMImplementation atm;
};
The Store


The Client
void main() {
   // Assuming accounts have been added 
   //   to the ATMImplementation, etc.
   //....
   IUnknown maybeAStore;  // Somehow obtain this
   //...
   ATMInterface *pAtmi;
   HRESULT hr = maybeAStore.QueryInterface(IID_ATM, &pAtmi);
      // Note: Assume AddRef() called in QueryInterface()
   if (SUCCEEDED(hr)) {
      pAtmi->login(52365, 2445342);
      pAtmi->deposit(200.00);
      pAtmi->withdraw(300.00);
      //...      
      ATMInterface pAtmi2 = pAtmi; // Adding another reference.
      pAtmi2.AddRef(); //Note!!!
      // Do stuff using pAtmi
      pAtmi->Release();//Note!!!
      // Done with pAtmi. Can still use pAtmi2.
      pAtmi2->login(24141, 2346234);
      pAtmi2->deposit(1250.00);
      pAtmi2->withdraw(500.00);
      double bal = pAtmi2->balance();
      pAtmi2->Release();//Note!!!
   }
}
The Client

What's going on?

The Client obtains a reference to the interface. It does so by calling QueryInterface(). Once a reference is obtained, it uses the interface to deal with the underlying object. After it is done with the object, the client calls Release() to bump down the reference count.

Implementing the COM object

In this issue we have examined the use of the interface from the client side, with emphasis on the use of AddRef() and Release(). In the next issue we shall examine the implementation of AddRef() and Release() as part of the implementation of a COM object.


Top
Journal Archives | Send Feedback

Copyright© Pantheon Systems, Inc., All rights reserved.