Introduction
While powerful and flexible, the RAPI2 COM interface provides endless opportunities to leak handles and memory. In this article, we'll explore a simple RAPI implementation of the "dir" command using powerful template methods provided by ATL and the standard library to ensure your code handles RAPI safely and effectively. If you're unfamiliar with COM, I highly recommend the article "Introduction to COM - What it is and How to Use it".
The Basics of ATL::CComPtr<>
ATL::CComPtr<>
provides us with three major advantages:
- Simplicity - There's no need to keep track of reference counting using
AddRef()
and Release()
.
- Less code - As you'll see, using smart pointers will significantly reduce the amount of code you have to write. What code you do write will be simpler and easier to maintain. What could be better?!
- Exception safety - If your code throws an exception, you don't have to worry about leaking COM handles. During object unwinding,
ATL::CComPtr<>
takes care of that for you.
Simplicity and Code Reduction
Let's assume for the moment that you're writing a RAPI2 application that isn't ridiculously trivial like the one attached. Your use of RAPI won't be limited to one function or necessarily even one class. You need to pass around RAPI COM objects and not have them go out of scope or leak handles. Without a reference-counted smart-pointer class like CComPtr<>
, it becomes your responsibility to keep track of each reference to your object. For example:
class Foo
{
public:
Foo() : c_( NULL ) {};
Foo( const Foo& other ) : c_( other.c_ )
{
c_->AddRef();
};
explicit Foo( IXYZ* i ) : c_( i )
{
c_->AddRef();
};
~Foo()
{
c_->Release();
};
IXYZ* Get() const
{
c_->AddRef();
return c_;
};
private:
IXYZ* c_;
};
IXYZ* SomeFunc()
{
IXYZ* obj = NULL;
::CoCreateInstance( CLSID_XYZ,
NULL,
CLSCTX_INPROC_SERVER,
IID_IXYZ,
reinterpret_cast< void** >( &obj ) );
Foo a;
{
Foo b( obj );
a = b;
}
return a.Get();
}
int _tmain( int argc, _TCHAR* argv[] )
{
IXYZ* obj = SomeFunc();
obj->DoSomethingInteresting();
obj->Release();
return 0;
}
Whew! As Raymond Chen said, "Reference Counting is Hard". Now, let's consider that same code using CComPtr<>
. Notice how we no longer need to use AddRef()
and Release()
. That's all done for us behind the scenes.
typedef CComPtr< IXYZ > XYZPtr;
class Foo
{
public:
Foo() {};
Foo( const Foo& other ) : c_( other.c_ ) {};
explicit Foo( const XYZPtr& i ) : c_( i ) {};
XYZPtr Get() const { return c_; };
private:
XYZPtr c_;
};
XYZPtr SomeFunc()
{
XYZPtr obj;
obj.CoCreateInstance( CLSID_XYZ );
Foo a;
{
Foo b( obj );
a = b;
}
return a.Get();
}
int _tmain( int argc, _TCHAR* argv[] )
{
XYZPtr obj = SomeFunc();
obj->DoSomethingInteresting();
return 0;
}
Neither of these examples will leak memory or handles, but using the ATL smart-pointers cut our code by a third, and that's before all of the error handling code needed by a production application has been added.
Exception Safety with ATL::CComPtr<>
We've seen how ATL::CComPtr<>
can make our code simpler, now let's look at how it can be made safer. What happens if an exception is thrown while you have an instance of a COM object? For example:
XYZPtr SomeFunc()
{
XYZPtr obj;
obj.CoCreateInstance( CLSID_XYZ );
Foo a;
{
Foo b( obj );
a = b;
throw std::runtime_error( "error!" );
}
return a.Get();
}
int _tmain( int argc, _TCHAR* argv[] )
{
try
{
XYZPtr obj = SomeFunc();
obj->DoSomethingInteresting();
}
catch( const std::runtime_error& e )
{
}
return 0;
}
At the time of the exception, there are three references to IXYZ
open. Wherever we decide to handle the exception is good enough, because we don't need to worry about cleaning any COM object references. Looking closely at the object unwinding process, we see:
~b()
is called. Release()
ing its hold on IXYZ
. RefCount
= 2.
~a()
is called. Release()
ing its hold on IXYZ
. RefCount
= 1.
~obj()
is called. Release()
ing its hold on IXYZ
. RefCount
= 0.
- Since the reference count is now 0,
~IXYZ()
is called.
After that, the exception is caught in _tmain()
by the catch()
statement. No memory is leaked.
Using ATL::CComPtr<> to Wrap the IRAPI Interface
Now that we see just how useful the ATL::CComPtr<>
smart-pointer class is, let's look at applying it to our RAPI application. The first thing we need is an interface to IRAPIDesktop
. This interface allows us to find the connected Windows Mobile and Windows CE based devices.
HRESULT hr = S_OK;
CComPtr< IRAPIDesktop > rapi_desktop;
hr = rapi_desktop.CoCreateInstance( CLSID_RAPI );
if( FAILED( hr ) )
return hr;
Easy, right? Next, we need to get an interface to the attached device. To do this, IRAPIDesktop
provides the method EnumDevices
to get a list of connected RAPI devices in IRAPIEnumDevices
. We can then use the Next
method to get a handle to the actual IRAPIDevice
interface. I note that despite the name suggesting you can connect more than one device, all current versions of Windows CE only support one RAPI connection at a time. So, in the following code, we will make the assumption that the user wants to connect to the first ActiveSync device:
CComPtr< IRAPIEnumDevices > rapi_device_list;
hr = rapi_desktop->EnumDevices( &rapi_device_list );
if( FAILED( hr ) )
return hr;
CComPtr< IRAPIDevice > rapi_device;
hr = rapi_device_list->Next( &rapi_device );
if( FAILED( hr ) )
return hr;
Now, we get to the good stuff. All of the earlier interfaces only provide us with general information and statistics. But, IRAPISession
allows us to interact with the device. With CeRapiInvoke
, you can do anything. For our 'dir' clone example, we will use CeRapiInvoke
to call a function CeDir_GetDirectoryListing
in a DLL that resides on our Windows Mobile device named CeDirLib.dll. We will provide a wide-character string containing the folder we want the directory listing of, and it will return to us an array of CE_FIND_DATA
structures. One for each file and directory in the path we specified.
CComPtr< IRAPISession > rapi_session;
hr = rapi_device->CreateSession( &rapi_session );
if( FAILED( hr ) )
return hr;
hr = rapi_session->CeRapiInit();
if( FAILED( hr ) )
return hr;
rapi_session->CeRapiUninit();
Executing Code on the Mobile Device
So, what does it take to actually run arbitrary code on our Windows Mobile device over the ActiveSync connection? We must have a function that allows us to execute an arbitrary function in an arbitrary library on the Windows Mobile device. Fortunately, Microsoft has provided us with just such a mechanism. Let's look at an example application that performs the same function as the 'dir' command.
IRAPISession::CeRapiInvoke
This is the crown jewel of RAPI. All of the other IRAPISession
interface functions are icing on the cake, but with CeRapiInvoke
, we can implement any algorithm any way we want. The CeRapiInvoke
function is a general-purpose mechanism that loads a DLL on the attached Windows Mobile device and executes a specified function in that DLL.
Let's look at an example where we provide a directory path to a function that returns an array of CE_FIND_DATA
structures containing every file and directory object in that path. Take careful note that when we're finished with the buffer returned by CeRapiInvoke
, we must use LocalFree()
to release it. More on that later.
std::wstring folder = L"\Program Files\Foo";
CE_FIND_DATA* listing_begin = NULL;
DWORD listing_size = 0;
hr = rapi_session->CeRapiInvoke( L"CeDirLib.dll",
L"CeDir_GetDirectoryListing",
folder.size() * sizeof( wchar_t ),
( BYTE* )folder.c_str(),
&listing_size,
( BYTE** )&listing_begin,
NULL,
0 );
if( NULL != listing_begin )
{
CE_FIND_DATA* const listing_end = reinterpret_cast< CE_FIND_DATA* >(
reinterpret_cast< BYTE* >( listing_begin ) + listing_size );
LocalFree( ( HLOCAL )listing_begin );
}
Implementing the RAPI Extension DLL
We have defined how we expect to interface with our RAPI DLL. Now, we must create the DLL function that implements that interface. For our "dir" example, we will use the ::FindFirstFile()
/ ::FindNextFile()
API. Note that RAPI does provide a IRAPISession::CeFindAllFiles()
API that does the same thing. Implementing non-trivial RAPI interface functions is left as an exercise to the interested reader.
CEDIRLIB_API int CeDir_GetDirectoryListing( DWORD cbInput,
BYTE* pInput,
DWORD* pcbOutput,
BYTE** ppOutput,
IRAPIStream* )
{
if( NULL == pcbOutput || NULL == ppOutput )
return E_INVALIDARG;
std::wstring folder = L"\\";
if( NULL != pInput && cbInput > 0 )
{
folder = std::wstring( reinterpret_cast< wchar_t* >( pInput ),
reinterpret_cast< wchar_t* >( pInput + cbInput ) );
}
// If the user did not specify a search-string, then we build one here.
if( folder.find_first_of( L'*', 0 ) == std::wstring::npos )
{
if( *folder.rbegin() != L'\\' )
folder += L"\\*";
else
folder += L"*";
}
// Build a list of files stored in a dynamically sized array of
// WIN32_FIND_DATA structures.
WIN32_FIND_DATA* cur = reinterpret_cast< WIN32_FIND_DATA* >(
LocalAlloc( LPTR, sizeof( WIN32_FIND_DATA ) ) );
WIN32_FIND_DATA* old = NULL;
int size = 1;
HANDLE find_file = ::FindFirstFile( folder.c_str(), cur );
if( INVALID_HANDLE_VALUE != find_file )
{
do
{
old = cur;
cur = reinterpret_cast< WIN32_FIND_DATA* >(
LocalReAlloc( old,
++size * sizeof( WIN32_FIND_DATA ),
LMEM_MOVEABLE | LMEM_ZEROINIT ) );
} while ( NULL != cur && ::FindNextFile( find_file, cur + size - 1 ) );
::FindClose( find_file );
// check to see if the memory allocation failed
if( NULL == cur )
{
LocalFree( old );
return E_OUTOFMEMORY;
}
// we return the directory listing to the user
*pcbOutput = ( size - 1 ) * sizeof( WIN32_FIND_DATA );
*ppOutput = reinterpret_cast< BYTE* >( cur );
return S_OK;
}
// FindFirstFile() failed.
LocalFree( cur );
return GetLastError();
}
What Happens Behind the Scenes? (or "Why Can't I Use new()/delete()?")
Though the RAPI documentation is specific about needing to use LocalAlloc()
and LocalFree()
with CeRapiInvoke()
, it does look suspiciously like we should be able to allocate memory using any method we want. After all, it is our DLL that allocates the memory and our program that is freeing it. Right? Negatory, Rando. To understand why, let's take a closer look at what's happening inside RAPI when we use CeRapiInvoke()
on an unimaginatively named function fcn
in a DLL named dll_name:
In this example, we see that the memory we are allocating in our DLL with LocalAlloc()
isn't the same as the memory we are freeing in our executable. After all, how could it be? The DLL is running on our Windows-Mobile device, which has its own RAM and its own memory space. The memory we allocate there is deallocated by the RAPI client running on the device. And, since mother always said mixing memory allocators is naughty, we will use LocalAlloc()
and LocalFree()
as the documentation suggests.
Using the Directory Listing
An array of fixed-length structures is absolutely screaming to be used by Standard Library algorithms. For example, let's say we want to sort our directory listing first by directories, then by names. We can use the std::sort<>
algorithm to excellent effect for that.
bool is_directory( const CE_FIND_DATA& data )
{
return ( data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) != 0;
}
Or, perhaps we want to know the total size of all the files in our directory listing. We have std::accumulate<>
to the rescue!
__int64 FileSize( const CE_FIND_DATA& i )
{
return ( static_cast< __int64 >( i.nFileSizeHigh ) << 32 ) + i.nFileSizeLow;
}
__int64 operator+( __int64 i, const CE_FIND_DATA& j )
{
if( !is_directory( j ) )
i += FileSize( j );
return i;
}
__int64 bytes = std::accumulate( listing_begin, listing_end, __int64( 0 ) );
The number of files and directories in our listing? No problem.
size_t dir_count = std::count_if( listing_begin, listing_end, is_directory );
size_t file_count = listing_end - listing_begin - dir_count;
Debugging Our RAPI DLL
Debugging our new RAPI DLL is easy. In Visual Studio 2008, select the "Debug" menu, then "Attach to Process". Set "Transport" to "Smart Device" and the "Qualifier" to "Windows Mobile 6 Professional Device" (or your equivalent platform). Then, select "rapiclnt.exe" and hit the "Attach" button.
You may set breakpoints wherever you like in your RAPI DLL code. Now, start your executable. When it calls CeRapiInvoke()
, your DLL will be loaded and the debugger will work as usual. When you're finished, go to "Debug"->"Detach All" so as not to kill the rapiclnt.exe process.