Introduction
In Part I, we looked at the basics of RAPI2 and implemented a small 'dir'-like application. This article will revisit our old application while examining the benefits and pitfalls of the IRAPIStream
interface.
About IRAPIStream
You may have seen IStream
before in the MSXML API or in the ISAPI framework. It allows us to send and receive data in a way very similar to the Winsock functions. The IRAPIStream
interface builds on the functionality of IStream
and provides two new methods.
Those two new methods are, according to MSDN:
SetRapiStat
- Set the timeout value for the IStream::Read
method.
GetRapiStat
- Get the timeout value for the IStream::Read
method.
Just as with the Winsock recv()
function, the IRAPIStream::Read()
function is blocking. So, it is advisable to have some method of canceling the call. The IRAPIStream
timeout methods give us that capability.
Enabling Stream Mode
Stream mode can be enabled simply by providing a non-NULL
value for the ppIRAPIStream
parameter of CeRapiInvoke()
. In this mode, CeRapiInvoke()
will return immediately. The pcbOutput
and ppOutput
parameters can still be populated, and the RAPI extension DLL will still receive valid pointers for those values. But, when CeRapiInvoke()
returns, it won't have any information from the extension DLL in those values. So, don't bother with them. They're effectively useless in stream mode.
HRESULT hr = S_OK;
CComPtr< IRAPIStream > stream;
hr = rapi_session->CeRapiInvoke( L"CeDirLib.dll",
L"CeDir_GetDirectoryListing",
folder.size() * sizeof( wchar_t ),
( BYTE* )folder.c_str(),
NULL,
NULL,
&stream,
0 );
if( NULL != stream )
{
}
Writing to the Stream
We will consider two methods of writing data to the IStream
. The first is, perhaps, the most obvious method. It is what we would use for any trivial program that only needs to write one type of data to the stream and do it only once. The second method abstracts the IStream
and FindXFile
APIs to standard library compatible iterators. Though our 'dir' application is trivial, we will implement the scalable method for demonstration purposes.
The Trivial Method
The direct approach to writing to the stream is presented below. We use the typical pattern for iterating over each file object found with the FindXFile
API and write each found item to the stream. We end by writing an empty WIN32_FIND_DATA
structure to the stream to indicate to the recipient that we are finished.
WIN32_FIND_DATA file = { 0 };
HANDLE find_file = ::FindFirstFile( folder.c_str(), &file );
if( INVALID_HANDLE_VALUE != find_file )
{
DWORD written = 0;
do
{
pStream->Write( &file, sizeof( WIN32_FIND_DATA ), &written );
} while ( SUCCEEDED( hr ) && ::FindNextFile( find_file, &file ) );
::FindClose( find_file );
ZeroMemory( &file, sizeof( WIN32_FIND_DATA ) );
pStream->Write( &file, sizeof( WIN32_FIND_DATA ), &written );
}
The Scalable Method
Now, we consider a non-trivial application where we send the contents of more than one directory or where we also enumerate all running processes. We would need to copy and paste those 13 lines of code each time for each enumeration. Every copy-paste operation is an opportunity to duplicate a defect. If later testing reveals a bug in any one of these sections, we must make sure to fix it in them all. How much better would it be to have the enumeration encapsulated separately from the stream communications?
RapiOStreamBuf< WIN32_FIND_DATA > sb( pStream );
FindFile find( folder );
std::copy( find.begin(),
find.end(),
RapiOStreamBuf_iterator< WIN32_FIND_DATA >( &sb ) );
For this method, we must define three new reusable objects:
FindFile
RapiOstreamBuf<>
RapiOstreamBuf_iterator<>
The FindFile
class provides iterator support for the standard FindXFile
API. A sample implementation is provided in the attached code, but more complete implementations, such as WinSTL, are also available.
RapiOStreamBuf<>
is derived from a std::basic_streambuf<>
. We override overflow
and sync
to send WIN32_FIND_DATA
structures on the IStream
. When the stream closes, an empty WIN32_FIND_DATA
structure is sent to indicate an EOF.
template< typename T >
class RapiOStreamBuf : public std::basic_streambuf< byte >;
RapiOStreamBuf_iterator<>
is a trivial iterator adapter for RapiOstreamBuf<>
. It overrides the assignment operator such that the idiom stream_iterator = find_iterator
sends the wrapped WIN32_FIND_DATA
structure on the wrapped IStream
. We specify the std::output_iterator_tag
to indicate this iterator can only be incremented and written to. Since we only implement writing to this stream, we cannot read data from this iterator.
template< typename T >
class RapiOStreamBuf_iterator : public std::iterator< std::output_iterator_tag,
void, void, void, void >;
Reading from the Stream
As before, we will consider two methods for reading from the IStream
. The trivial method is easy to write, but does not lend itself to a larger, more complex application. The scalable method encapsulates the type of data being read separately from the method used to read it.
The Trivial Method
In this approach, we use the IRAPIStream
interface to read CE_FIND_DATA
structures from the stream until there is an error or we read an empty CE_FIND_DATA
structure indicating an EOF.
CE_FIND_DATA eof = { 0 };
CE_FIND_DATA file = { 0 };
while( ( hr = stream->Read( &file, sizeof( CE_FIND_DATA ), NULL ) ) >= 0 &&
( file != eof ) )
{
}
For this method, it is necessary to also define an inequality operator to test for the EOF condition.
bool operator!=( const CE_FIND_DATA& i, const CE_FIND_DATA& j );
The Scalable Method
In this method, we no longer have to worry about checking for the EOF; that is done for us by the stream buffer.
RapiIStreamBuf< CE_FIND_DATA > sb( stream );
std::vector< CE_FIND_DATA > listing;
std::copy( RapiIStreamBuf_iterator< CE_FIND_DATA >( &sb ),
RapiIStreamBuf_iterator< CE_FIND_DATA >(),
std::back_inserter( listing ) );
For this method, we define two new reusable objects:
RapiIStreamBuf<>
RapiIStreamBuf_iterator<>
These objects are templatized such that we could easily adapt them to receive other data structures. We could even take the abstraction a step further and provide a common interface between RAPI and the CoreCon API, allowing the user the choice of connecting to the device over a TCP/IP connection. But, that will have to wait for another article.
RapiIStreamBuf<>
is derived from a std::basic_streambuf
. We override underflow
to read CE_FIND_DATA
structures from the IStream
until an empty structure is read indicating an EOF.
template< typename T >
class RapiIStreamBuf : public std::basic_streambuf< byte >;
RapiIStreamBuf_iterator<>
is a trivial iterator adapter for RapiIStreamBuf<>
. It overrides the addition operator to get another CE_FIND_DATA
structure from the stream, and the dereference operator to provide access to the received CE_FIND_DATA
structure. We specify the std::input_iterator_tag
to indicate this iterator can only be incremented and dereferenced. Since we only implement reading from the stream, this iterator is immutable.
template< typename T >
class RapiIStreamBuf_iterator : public std::iterator< std::input_iterator_tag, T >;
A Note on Timeouts
Like any good socket, IRAPIStream::Read
will continue to wait for data to read until the End of Days. Let's consider what would happen if our EOF flag (an empty CE_FIND_DATA
structure) were never received. Our 'dir' program would, to the user's point of view, lock up as IRAPISTREAM::Read
waits for more data that will never come. To keep that from happening, we will set an arbitrary, but reasonable, timeout value for IRAPIStream::Read
using IRAPIStream::SetRapiStat
. For this example application, we will use a value of 3 seconds. Considering that we are using a low-latency USB connection; this would seem an equitable compromise between ensuring our data arrives and an impatient user hitting Ctrl-C.
hr = stream->SetRapiStat( STREAM_TIMEOUT_READ, 3 );
At present, STREAM_TIMEOUT_READ
is the only option available to IRapiStream::SetRapiStat
.