RAPI2:你从未知道的朋友
安全有效地使用 RAPI2 接口。
引言
RAPI2 COM 接口虽然功能强大且灵活,但也提供了无尽的泄漏句柄和内存的机会。在本文中,我们将使用 ATL 和标准库提供的强大模板方法,探索一个简单的 RAPI “dir” 命令实现,以确保您的代码安全有效地处理 RAPI。如果您不熟悉 COM,我强烈推荐文章“COM 简介 - 它是如何工作的以及如何使用它”。
ATL::CComPtr<> 的基础知识
ATL::CComPtr<>
为我们提供了三个主要优势:
- 简单性 - 无需使用
AddRef()
和Release()
来跟踪引用计数。 - 更少的代码 - 正如您将看到的,使用智能指针将显著减少您必须编写的代码量。您编写的代码将更简单,更易于维护。还有什么比这更好的呢?!
- 异常安全 - 如果您的代码抛出异常,您不必担心 COM 句柄泄漏。在对象展开期间,
ATL::CComPtr<>
会为您处理这些。
简单性和代码缩减
让我们暂时假设您正在编写一个不像附加的应用程序那样微不足道的 RAPI2 应用程序。您对 RAPI 的使用不会局限于一个函数,甚至不一定局限于一个类。您需要传递 RAPI COM 对象,并且不让它们超出作用域或泄漏句柄。如果没有像 CComPtr<>
这样的引用计数智能指针类,跟踪对象的每个引用就成了您的责任。例如:
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;
}
哎呀!正如 Raymond Chen 所说,“引用计数很难”。现在,让我们考虑使用 CComPtr<>
的相同代码。请注意,我们不再需要使用 AddRef()
和 Release()
。所有这些都在幕后为我们完成了。
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;
}
这两个示例都不会泄漏内存或句柄,但使用 ATL 智能指针将我们的代码减少了三分之一,这还没有加上生产应用程序所需的所有错误处理代码。
ATL::CComPtr<> 的异常安全性
我们已经看到了 ATL::CComPtr<>
如何使我们的代码更简单,现在让我们看看如何使其更安全。如果在一个 COM 对象的实例存在时抛出异常会发生什么?例如:
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;
}
在异常发生时,有三个对 IXYZ
的引用处于打开状态。无论我们决定在哪里处理异常都足够好,因为我们不需要担心清理任何 COM 对象引用。仔细查看对象展开过程,我们看到:
- 调用
~b()
。释放其对IXYZ
的持有。RefCount
= 2。 - 调用
~a()
。释放其对IXYZ
的持有。RefCount
= 1。 - 调用
~obj()
。释放其对IXYZ
的持有。RefCount
= 0。 - 由于引用计数现在为 0,调用
~IXYZ()
。
之后,异常在 _tmain()
中被 catch()
语句捕获。没有内存泄漏。
使用 ATL::CComPtr<> 封装 IRAPI 接口
既然我们已经看到 ATL::CComPtr<>
智能指针类有多么有用,让我们来看看如何将其应用于我们的 RAPI 应用程序。我们首先需要一个到 IRAPIDesktop
的接口。这个接口允许我们查找连接的 Windows Mobile 和基于 Windows CE 的设备。
HRESULT hr = S_OK;
CComPtr< IRAPIDesktop > rapi_desktop;
hr = rapi_desktop.CoCreateInstance( CLSID_RAPI );
if( FAILED( hr ) )
return hr;
很简单,对吧?接下来,我们需要获取一个到附加设备的接口。为此,IRAPIDesktop
提供了 EnumDevices
方法,以在 IRAPIEnumDevices
中获取连接的 RAPI 设备列表。然后我们可以使用 Next
方法获取实际的 IRAPIDevice
接口句柄。我注意到,尽管名称暗示您可以连接多个设备,但所有当前版本的 Windows CE 一次只支持一个 RAPI 连接。因此,在下面的代码中,我们将假设用户想要连接到第一个 ActiveSync 设备:
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;
现在,我们进入了重点。所有早期的接口只为我们提供了通用信息和统计数据。但是,IRAPISession
允许我们与设备交互。使用 CeRapiInvoke
,您可以做任何事情。对于我们的“dir”克隆示例,我们将使用 CeRapiInvoke
调用位于我们 Windows Mobile 设备上名为 CeDirLib.dll 的 DLL 中的函数 CeDir_GetDirectoryListing
。我们将提供一个包含我们想要获取目录列表的文件夹的宽字符字符串,它将返回给我们一个 CE_FIND_DATA
结构数组。每个文件和指定路径中的每个目录对应一个。
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();
在移动设备上执行代码
那么,通过 ActiveSync 连接在 Windows Mobile 设备上实际运行任意代码需要什么呢?我们必须有一个函数,允许我们在 Windows Mobile 设备上的任意库中执行任意函数。幸运的是,微软为我们提供了这样的机制。让我们看一个执行与“dir”命令相同功能的示例应用程序。
IRAPISession::CeRapiInvoke
这是 RAPI 的瑰宝。所有其他 IRAPISession
接口函数都是锦上添花,但有了 CeRapiInvoke
,我们可以按照我们想要的任何方式实现任何算法。CeRapiInvoke
函数是一个通用机制,它在连接的 Windows Mobile 设备上加载 DLL 并执行该 DLL 中指定的函数。
让我们看一个示例,我们向一个函数提供目录路径,该函数返回一个 CE_FIND_DATA
结构数组,其中包含该路径中的每个文件和目录对象。请注意,当我们完成 CeRapiInvoke
返回的缓冲区时,我们必须使用 LocalFree()
来释放它。稍后会详细介绍。
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 );
}
实现 RAPI 扩展 DLL
我们已经定义了我们期望如何与我们的 RAPI DLL 接口。现在,我们必须创建实现该接口的 DLL 函数。对于我们的“dir”示例,我们将使用 ::FindFirstFile()
/ ::FindNextFile()
API。请注意,RAPI 确实提供了做同样事情的 IRAPISession::CeFindAllFiles()
API。实现非平凡的 RAPI 接口函数留给感兴趣的读者作为练习。
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();
}
幕后发生了什么?(或者“为什么我不能使用 new()/delete()?”)
尽管 RAPI 文档明确指出需要将 LocalAlloc()
和 LocalFree()
与 CeRapiInvoke()
一起使用,但它看起来可疑地像我们应该能够使用任何我们想要的方法分配内存。毕竟,是我们的 DLL 分配内存,是我们的程序释放内存。对吗?不对,兰多。要理解为什么,让我们仔细看看当我们使用 CeRapiInvoke()
在名为 dll_name 的 DLL 中调用一个想象力不足的函数 fcn
时 RAPI 内部发生了什么:
在此示例中,我们看到我们在 DLL 中使用 LocalAlloc()
分配的内存与我们在可执行文件中释放的内存不同。毕竟,怎么可能相同呢?DLL 运行在我们的 Windows Mobile 设备上,该设备有自己的 RAM 和自己的内存空间。我们在那里分配的内存由在设备上运行的 RAPI 客户端解除分配。而且,由于母亲总是说 混合内存分配器是不好的,我们将按照文档的建议使用 LocalAlloc()
和 LocalFree()
。
使用目录列表
固定长度结构数组绝对适合标准库算法。例如,假设我们想先按目录,然后按名称对目录列表进行排序。我们可以很好地使用 std::sort<>
算法来实现这一点。
/// 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 );
或者,也许我们想知道目录列表中所有文件的总大小。我们有 std::accumulate<>
来解救!
/// 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 ) );
我们列表中的文件和目录数量?没问题。
size_t dir_count = std::count_if( listing_begin, listing_end, is_directory );
size_t file_count = listing_end - listing_begin - dir_count;
调试我们的 RAPI DLL
调试我们新的 RAPI DLL 很容易。在 Visual Studio 2008 中,选择“调试”菜单,然后选择“附加到进程”。将“传输”设置为“智能设备”,将“限定符”设置为“Windows Mobile 6 专业版设备”(或您对应的平台)。然后,选择“rapiclnt.exe”并单击“附加”按钮。
您可以在 RAPI DLL 代码中的任何位置设置断点。现在,启动您的可执行文件。当它调用 CeRapiInvoke()
时,您的 DLL 将被加载,并且调试器将像往常一样工作。完成后,转到“调试”->“全部分离”,以免终止 rapiclnt.exe 进程。