Home » Articles » COM »Tutorials »Unique Product Features » A Smart Pointer for p6COM

A Smart Pointer for p6COM

By Jim Susoy - October 12, 2010 @ 9:32 pm

Introduction

p6ComPtr<> is a smart pointer implementation designed for use with p6COM, P6R’s lite implementation of the Component Object Model. p6COM is the basis for our cross platform server framework as well as the library loader for our library products. p6ComPtr<> is a tool to help prevent component leaks and simplify the use and management of p6COM interfaces. COM interfaces have compelling advantages, but also have drawbacks. One advantage is reference counting which allows an interface’s lifetime to be managed intelligently. It turns out that this advantage is also a disadvantage because even the most fastidious developers make mistakes when managing an interfaces reference count manually through calls to addref() and release().

Enter p6ComPtr<>

p6ComPtr<> simplifies the management of reference counting, attempts to prevent component leaks, improves type safety in some situations, while reducing the amount of code needed to manage components. One example is that you no longer need to put any code in your destructor to handle releasing interfaces.

The design goals of p6ComPtr<> are:

  • Low overhead – does not take any more space than a raw interface pointer.
  • Reduce or remove interface leaks.
  • Improve readability and correctness.
  • Reducing the amount of code developers have to write.
  • Provide a platform/Compiler independent solution.

Before We Begin – Some Review

If you already understand the concepts of COM ownership and reference counting, go ahead and skip to the next section. You need to have a good grasp of these concepts before using p6ComPtr<>. If you are not familiar with these concepts, a good place to learn would be “Inside COM” [1] by Dale Rogerson, and “Essential COM” [2] by Don Box.

For a quick refresher on COM reference counting, see Microsoft’s “The Rules of the Component Object Model” [3] on the MSDN website. Here is very brief refresher on COM ownership.

Ownership Basics

An owning reference to a interface, is any reference through which you will call addref() and release() to manage the reference. Think of addref() as “take ownership” and release() as “relinquish ownership”. Holding an owning reference to an interface will prevent it from being destroyed. When you are done, calling release() will decrement the reference count, and when it drops to zero, the interface may delete itself (and possibly the underlying object).

  • If an interfaces lifetime is greater than yours, you don’t need to own it. For example, and interface passed into a method does not need to be be managed with addref() or release() because its scope is larger than that of the method. If you were to store that interface pointer in a member variable before returning, that would require taking ownership, and in that case you would need to addref() it before returning.
  • You should not have cyclic ownership. This will cause leaks. You would need to add an extra out of band mechanism to break the cycle to allow this case to work properly.
  • Containers should own their contents.
  • If an interface is passed in as an argument, you do not need an owning reference because it’s scope is greater than yours.

Using p6comPtr<>

p6ComPtr’s methods are always accessed using the dot (.) operator while the underlying interface methods are always accessed using via the arrow (->) operator. Never call the underlying interface’s release() method when the interface is managed by p6ComPtr<>, instead set the smart pointer to NULL. Here’s quick example:

// Prefer construction over assignment when possible
// It's more effecient
p6ComPtr<p6IConsole> cpConsole(pConsole);
p6ComPtr<p6IConsole> cpConsole2;

// Access the console interfaces queryInterface() method
// storing the requested interface in cpConsole2.  Made
// type-safe via the use of the VALIDATECOMPTR macro
cpConsole->queryInterface(VALIDATECOMPTR(p6IConsole,cpConsole2));

// Access the p6ComPtr's queryInterface method
// storing the requested interface in cpConsole2.
// p6ComPtr's method is type-safe.
cpConsole.queryInterface(IID_p6IConsole,cpConsole2);

// Tell p6ComPtr<> to release() the interface
cpConsole = NULL;

// NEVER do this!
// cpConsole->release();

Basic Usage

// Always prefer construction over assignment
// This assigns a raw pointer to the smart pointer
p6ComPtr<p6IConsole> cpConsole(pConsole);

// a) Assign an interface pointer to the p6ComPtr<>
cpConsole = pConsole;

// b) Retrieve the console interface from the runtime/loader
p6GetRuntimeIface(VALIDATECOMPTR(p6IConsole,cpConsole));

// c) Get a console interface from some method/function
getConsole(cpConsole.addressof());

// d) Get a console interface from some method/function safely
// This method is preferred over c) because it prevents leaks.
getConsole(cpConsole.addressofWithRelease());

// e) Call an interface method
cpConsole->writeStdout( ... );

// f) Call a p6ComPtr<> method
cpConsole.detach(&pMyConsole);

// g) Pass a p6ComPtr<> to function
void someFunction(p6IConsole *pConsole);
someFunction(cpConsole);

p6ComPtr<> is designed to be used wherever a raw interface pointer with an owning reference would be used.

p6ComPtr<> Methods

addressof()
The addressof() method return the address of the underlying interface pointer and can be used to store an interface pointer in the p6ComPtr<>. It does NOT release() any preexisting interface pointer that the smart pointer may be already manager. Therefore it should be used
with caution, because of the chance of overwriting an exiting pointer resulting in an interface leak.

addressofWithRelease()
This method is the preferred way of storing an interface in the smart pointer when taking the address of the underlying pointer. It ensures that any previous interface which the smart pointer was managing has been released before providing the address. This prevents interface leaks, and reduces the amount of code needed (when retrieving interfaces in a loop for example) as you do not have to explicitly release() the interface your self.

swap()
Exchanges ownership with another interface or p6ComPtr<>. No reference counting is performed on either interface, this is a straight exchange of ownership. Here’s an example:

p6ComPtr<p6IConsole> cpConsole(pConsole); // Construct p6ComPtr<> to manage pConsole

// Perform the exchange
cpConsole.swap(pMyOtherConsole);

In this example, after the exchange, cpConsole now manages the interface that was pointed to by pMyOtherConsole and pMyOtherConsole now points to the interface that cpConsole was previously managing.

get()
This method returns the underlying raw interface pointer without increasing it’s reference count.

attach()
This is used to take ownership of the provided raw interface pointer. The reference count of the interface is not altered. After the call, the smart pointer is managing the interface pointer, which means that you should not use the previous pointer.

detach()
Gives ownership of the underlying raw interface pointer to the caller. The reference count of the underlying interface is not altered. After the call, p6ComPtr<> is empty and it’s underlying raw interface pointer will be NULL. This is an efficient way to return interface pointers through an [inout] argument of a function.

   P6R::P6ERR getDOMXML(P6R::p6IDataStream *pOutStream,P6R::p6IDOMXML **ppIface)
   {
      P6ERR                err;
      p6ComPtr<p6IDOMXML>  cpDOM;

      if(P6SUCCEEDED(err = p6CreateInstance(NULL,CID_p6DOMXML,VALIDATECOMPTR(p6IDOMXML,cpDOM)))) {
         if(P6SUCCEEDED(err = cpDOM->initialize(P6DOMXML_NOFLAGS,pOutStream))) {
            //
            // Using detach() saves a reference counting operation
            // and gives ownership to *ppIface.
            //
            cpDOM.detach(ppIface);
         }
      }
      return err;
   }

Without using detach() we would need to store the pointer in *ppIface and the call (*ppIface)->addref(); to increment the reference count. Then later when cpDOM goes out of scope as the function returns, it would call release() on the interface point. So by using the detach() method, we have saved two reference counting operations and written less code to boot! Here’s the function without using detach() for comparison:

   P6R::P6ERR getDOMXML(P6R::p6IDataStream *pOutStream,P6R::p6IDOMXML **ppIface)
   {
      P6ERR                err;
      p6ComPtr<p6IDOMXML>  cpDOM;

      if(P6SUCCEEDED(err = p6CreateInstance(NULL,CID_p6DOMXML,VALIDATECOMPTR(p6IDOMXML,cpDOM)))) {
         if(P6SUCCEEDED(err = cpDOM->initialize(P6DOMXML_NOFLAGS,pOutStream))) {
            *ppIface = cpDOM
            (*ppIface)->addref();
         }
      }
      return err;
      // cpDOM call release() when it goes out of scope
   }

createInstance()
This method calls the runtime/loader’s p6CreateInstance() function and stores a pointer to the resulting interface in the smart pointer.

getRuntimeIface()
This method calls the runtime/loader’s p6getRuntimeIface() function and stores a pointer to the resulting interface in the smart pointer.

queryInterface()
Arguably one of the weaknesses of COM is the use of queryInterface() with returning an un-typed pointer. The prototype for queryInterface() is:

P6R::P6ERR queryInterface(const P6R::IID &iid, void** ppv);

The problem is that it is possible to use an interface ID in the first argument that is unrelated to the interface pointer pointed to in the second argument. In p6COM we have a couple mechanisms to help with type safety in this situation.

First there are two macros which enforce type safety at compile time. P6VALIDATEIF() ensures the IID and the raw interface pointer are of the same type at compile time. The second macro, P6VALIDATECOMPTR() does the same thing for the IID and a p6ComPtr<>.

The second mechanism is p6ComPtr’s query interface methods which is a type safe version of the underlying interfaces queryInterface(). It is recommended that you use this method (or at least the macos) when querying interfaces.

p6ComPtr<> and STL Containers

Placing interface pointers in containers is a lot easier using p6ComPtr<> because
each entries scope is managed by the smart pointer. For example, lets say we wanted a vector of interfaces. We could do something like this:

typedef p6ComPtr<IProtoEngine> protoeng_t;
typedef std::vector< protoeng_t >  vproto_t;

vproto_t vProto;
protoeng_t cpProto;

for(P6UINT32 i=0;i automatically releases the previous interface
   // in cpProto before storing the new instance pointer.
   if(P6SUCCEEDED(err = getProtoEng(cpProto.addressofWithRelease()))) {
      vProto.push_back(cpProto);
   }
}

When the vector goes out of scope, each p6ComPtr<> instance that it contains will automatically release it’s underlying interface pointer when it is destroyed. So there is no need to write any cleanup code to walk through the vector releasing interface pointers. There are no interface leaks and again we ended up writing less code that we would have if we did not use p6ComPtr<>. Also note that this follows the basic ownership rules of containers owning their contents.

Some Things Not To Do

  • Methods MUST not define p6ComPtr<> arguments. They MUST always use raw interface pointers. Remember that p6ComPtr<> is used for “owning” inerface pointers and function/method arguments are not owning pointers.
  • NEVER declare a function/method to return a p6ComPtr<> directly. Always declare it to return the raw interface pointer.
  • NEVER call the underlying interface pointers release() method. This goes around p6ComPtr<>’s management mechanisms and will most likely lead to crashes. You will notice that p6ComPtr<> does not have a publicly accessible release() method. This was done to make it very easy to find any place that release() is being called on a p6ComPtr<>. Just assign NULL to the smart pointer to release its underlying interface or let it simply go out of scope.

One common mistake I have seen when using COM interfaces is people referencing an interface after it has been released. Of course this is against the rules of COM, but it happens, and the problems it can cause are very hard to find. Consider this code:

IProtoEng *pEng = getProtoEng();

addEng(pEng);    // keeps a reference to pEng
pEng->release();

// Call someFunction() assuming addEng() called addref() on pEng
pEng->someFunction(); //

Using p6ComPtr<> the code would look more incorrect and it would hopefully be more obvious that you are doing something wrong.

p6ComPtrcpEng(getProtoEng());

addEng(cpEng);    // keeps a reference to pEng
cpEng = NULL;

// It's more obvious that you are actually trying to
// dereference a NULL/invalid pointer here.
cpEng->someFunction(); //

The assumption that addEng() somehow stored a reference to the interface is not a valid assumption to make and will lead to all kinds of problem under exceptional conditions. addEng() might fail and not store the reference, another thread could have released the reference etc. Hopefully setting cpEng = NULL makes it more clear what release() actually means. After all, you would not try to dereference a raw pointer you just set to NULL?

References

  1. [1] Dale Rogerson, “Inside COM.” Microsoft Press (January 27, 1997)
  2. [2] Don Box, “Essential COM.” Addison-Wesley Professional (January 1, 1998)
  3. [3] Charlie Kindel “The Rules of the Component Object Model” MSDN Website (October 1995)

"A Smart Pointer for p6COM" was published on October 12th, 2010 and is listed in COM, Tutorials, Unique Product Features.

Follow comments via the RSS Feed | Leave a comment | Trackback URL


Leave Your Comment