Click here to Skip to main content
65,938 articles
CodeProject is changing. Read more.
Articles
(untagged)

RAPI2: The Friend You Never Knew You Had

0.00/5 (No votes)
17 May 2010 6  
Using the RAPI2 interface safely and effectively.

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:

  1. Simplicity - There's no need to keep track of reference counting using AddRef() and Release().
  2. 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?!
  3. 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_;
}; // class Foo

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_;
}; // class Foo

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:

  1. ~b() is called. Release()ing its hold on IXYZ. RefCount = 2.
  2. ~a() is called. Release()ing its hold on IXYZ. RefCount = 1.
  3. ~obj() is called. Release()ing its hold on IXYZ. RefCount = 0.
  4. 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;
    
// Our RAPI session is ready to go!

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 );
        
    // use the returned data in an interesting way...
    
    // free the memory returned from RAPI
    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* /*pStream*/ )
{
    // verify the input parameters
    if( NULL == pcbOutput || NULL == ppOutput )
        return E_INVALIDARG;

    // Get the folder the user wants to search. If none is specified, use
    // the root folder as default.
    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:

RAPI BTS - 24K

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.

/// determine if a particular item is a directory or not.
bool is_directory( const CE_FIND_DATA& data )
{
    return ( data.dwFileAttributes & FILE_ATTRIBUTE_DIRECTORY ) != 0;
}

/// return true if i < j
bool descending( const CE_FIND_DATA& i, const CE_FIND_DATA& j )
{
    // we sort directories first, then by filename
    if( is_directory( i ) != is_directory( j ) )
        return is_directory( i );
    return _wcsicmp( i.cFileName, j.cFileName ) < 0;
}

// sort the returned data
std::sort( listing_begin, listing_end, descending );

Or, perhaps we want to know the total size of all the files in our directory listing. We have std::accumulate<> to the rescue!

/// Get the 64-bit size of the file.
__int64 FileSize( const CE_FIND_DATA& i )
{
    return ( static_cast< __int64 >( i.nFileSizeHigh ) << 32 ) + i.nFileSizeLow;
}

/// add the file size of a CE_FIND_DATA
__int64 operator+( __int64 i, const CE_FIND_DATA& j )
{
    if( !is_directory( j ) )
        i += FileSize( j );
    return i;
}

// get the total size of all files in our listing
__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.

RAPI debugging - 24K

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.

License

This article has no explicit license attached to it but may contain usage terms in the article text or the download files themselves. If in doubt please contact the author via the discussion board below.

A list of licenses authors might use can be found here