Monday, June 16, 2008

The Joy of Smart Pointers

One of the biggest mistakes people new to DirectShow (and COM, for that matter) is using raw COM pointers. This article aims to present a compelling alternative.

All COM objects implement the IUnknown interface. IUnknown defines three standard methods, of which two are a concern of this article: AddRef() and Release(). When a COM object is created, its "reference count" (the number of objects holding claim to the newly created object) is set to 1. A program never directly deletes these objects; instead, a program calls Release(), which de-increments the reference count. If the reference count reaches 0, the object self-destructs.

The benefit of this is an object will remain in memory only as long as it needs to; the lifetime of an object is managed by its reference count. A programmer cannot accidentally delete an object from memory that another module or portion of the application may be using. Only when the object's reference count goes to 0 (as a result of all involved parties calling Release() on their interface pointers) does the object automatically destroy itself.

This is, sadly, a double-edged sword; by mismanaging reference counts, you can easily get yourself in trouble, and leak memory in a very real way.

Take the following function as an example:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
IMediaControl * pIMediaControl;
hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

return pIMediaControl->Run(); // THIS WILL LEAK A REFERENCE COUNT
}

This is how we would normally start a filter graph in DirectShow. This code technically works, but it has a serious flaw. Because it queries the filter graph manager for the IMediaControl interface, this increments the reference count on pIGraphBuilder. However, when we return, because IMediaControl was declared locally, it goes out of scope without ever being released, thus leaving behind an extra reference count on pIGraphBuilder. The above code is leaking a reference count, which means abandoned COM objects galore!

One way to (sort of) fix the above code would be:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
IMediaControl * pIMediaControl;
HRESULT hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

hr = pIMediaControl->Run();
if( FAILED( hr ) )
return hr; // THIS WILL LEAK A REFERENCE COUNT!

// Release our interface....
SAFE_RELEASE( pIMediaControl );
return hr;
}

There are still major flaws with this code. We successfully release the interface before exiting the function at the end; this will automatically de-increment the reference count on the filter graph manager. However, what if the call to IMediaControl->Run() fails? Again, we leak a reference count because we fail to release pIMediaControl! We successfully queried for the interface, which adds a reference to our Filter Graph Manager, but by exiting before we've called SAFE_RELEASE() we've again leaked a reference count. We could easily add another SAFE_RELEASE() call to the middle return, but consider this: what if this method was 100 lines long? 200 lines long? What if it had elaborate loops or other conditional logic? It becomes easy to miss a release, and the code is visually convoluted by clean-up logic.

The solution to this problem is to use Smart Pointers to manage reference counting. When a COM pointer wrapped with a Smart Pointer goes out of scope, it automatically calls Release(), thus alleviating the pain of manually releasing:


#include ; // For Smart Pointers!

...

HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
CComPtr pIMediaControl;
HRESULT hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );
if( FAILED( hr ) )
return hr;

return pIMediaControl->Run();
}

Notice that we don't have to release the pointer at the end of the method; upon going out of scope, the Smart Pointer will automatically call Release().

Alternately, you can also use CComQIPtr (QI = Query Interface) for slightly cleaner code; the above could be written like so:


HRESULT StartGraph( IGraphBuilder * pIGraphBuilder )
{
CComQIPtr pIMediaControl( pIGraphBuilder );
if( !pIMediaControl )
return E_FAIL;

return pIMediaControl->Run();
}

Either method will work; the second is slightly cleaner but I prefer the former for more accurate HRESULTs.

There are still instances where you'll need to manually release a smart pointer--take the following GetPin() function for example:


//---------------------------------------------------------------//
// GetPin - returns a pin of the specified direction on the
// specified filter. Note that this will return connected or
// unconnected pins.
HRESULT GetPin( IBaseFilter * pFilter, PIN_DIRECTION PinDir, IPin ** ppPin )
{
if( ppPin == NULL || pFilter == NULL )
{
return E_POINTER;
}

CComPtr pEnum;
HRESULT hr = pFilter->EnumPins( &pEnum );
if( FAILED( hr ) )
{
return hr;
}

CComPtr pPin;
hr = pEnum->Next( 1, &pPin, 0 );
while( hr == S_OK )
{
PIN_DIRECTION ThisPinDirection;
hr = pPin->QueryDirection( &ThisPinDirection );
if( FAILED( hr ) )
{
return hr;
}

if( PinDir == ThisPinDirection )
{
// Found a match. Return the IPin pointer to the caller.
return pPin.CopyTo( ppPin );
}
// Release the pin for the next time through the loop.
pPin.Release();
hr = pEnum->Next( 1, &pPin, 0 );
}

// No more pins. We did not find a match.
if( hr == VFW_E_ENUM_OUT_OF_SYNC )
{
return VFW_E_ENUM_OUT_OF_SYNC;
}
else
{
return E_FAIL;
}
}

The above function is the same function defined in the DirectShow documentation, with several errors corrected. And, of course, no more sloppy raw COM pointers.

Note the IPin object pPin--because we're enumerating the filter for a pin, we have to manually call pPin.Release() before reusing the object. Also of note is the pPin.CopyTo() call, which is another convenient feature of smart pointers. CopyTo() should be used in the event of copying a smart pointer to a dumb pointer. However, in the above function, since the passed in pPin is either going to be a dereferenced smart pointer (which is, again, a dumb pointer) or a regular dumb pointer, the CopyTo() is appropriate.

One other feature of Smart Pointers is the overloaded assignment operator. Observe:


CComPtr pSomeFilter;


//Somewhere else
HRESULT CreateFauxFilter()
{
CComPtr pSomeOtherFilter;
HRESULT hr = pSomeOtherFilter.CoCreateInstance( CLSID_OfSomeOtherFilter );
if( FAILED( hr ) )
{
return hr;
}
else
{
// Here, we can use the "=" operator to copy one smart pointer
// to another smart pointer.
pSomeFilter = pSomeOtherFilter;
return hr;
}
}

So, pSomeFilter is assigned to pSomeOtherFilter, and when pSomeOtherFilter goes out of scope, it'll clean up any remaining reference counting issues. It's a handy way to only keep around a pointer if we know everything has occurred successfully. A common use of this is to instantiate all of your filter graphs with locally declared smart pointers, and only assign the smart pointers to your member variables at the end of the function.

You may think these are equivalent to the CopyTo() method of a CComPtr, but the assignment operator should only be used when explicitly copying one smart pointer to another. The CopyTo() should be used when copying a smart pointer to a dumb pointer.

Also, be mindful of accidentally mixing smart pointers, dumb pointers, and the assignment operator:


IBaseFilter * pDumbPointer;
CComPtr pSmartPointer;

pSmartPointer = pDumbPointer; // THIS IS OKAY (sort of)

pDumbPointer = pSmartPointer; // THIS IS VERY, VERY BAD!!

The really nasty thing about the latter assignment is that pDumbPointer will not equal NULL (thus evading many common checks for validity) and subsequent attempt to call any methods on the dumb pointer will result in Very Bad Things™. If you're mixing smart pointers with dumb pointers, consider removing the dumb pointers entirely, as the use of dumb pointers is generally a sign of a greater problem and using both is generally confusing and/or odd.

4 comments:

Anonymous said...

Thanks for this nice introduction to COM smart pointers! One thing though: your blogging system seems to have swallowed all angle brackets (the includes and, worse still, all templates). It took me some time to realize what was wrong with the code!

Daniel

kidjan said...

Daniel,

Thanks. Yeah, I've known for a while that blogger has trouble with code blocks. I've yet to find a good solution to this. Part of me suspects the real solution is something besides blogger. But thanks, I hope this was helpful for you.

SteveS said...

Hi,

Also, Thanks for your article. Very helpful indeed. I have also read and re-read your "N Habits of Highly Defective DirectShow Applications" :)

Near the top of the article you state that this line of code:

hr = pIGraphBuilder->QueryInterface( IID_IMediaControl, ( void ** ) &pIMediaControl );

AddRefs the graph builder... Doesn't it add ref the interface pointer that is returned (pIMediaControl)?

Unknown said...

Hi SteveS,

IMediaControl is an interface implemented by IGraphBuilder, so in essence, it increments the reference count on the IGraphBuilder object. Think of it this way: if the graph builder had a separate reference count from IMediaControl, then if the graph builder's reference count went to zero, then IMediaControl would no longer be valid. In essence, they must share a reference count.

Almost all of DirectShow (and COM) is like this--querying for an interface on some object increments the reference count of the underlying object--not the interface. Adding a reference to an Interface increments the object's reference count. Take a look at the base classes if you want to see how this works in code (in particular, CSource and CSourceStream