Introduction
My very first article on CodeProject discussed one good reason why you might want to use overlapped I/O[^ ]. In that article I used pseudocode to outline the steps needed to use overlapped I/O in order to demonstrate just why you might choose to use it. This time around I'm going to present a usable class that encapsulates the basics of overlapped I/O. What follows assumes that you've read my earlier article.
COverlappedIO
looks like this.
class COverlappedIO
{
public:
COverlappedIO(void);
virtual ~COverlappedIO(void);
void Attach(HANDLE hIO);
OVERLAPPED *operator&() { return &m_op; }
virtual bool Read(BYTE *pbData, DWORD dwBufferSize,
LPDWORD pdwBytesRead,
HANDLE hStopEvent = INVALID_HANDLE_VALUE);
virtual bool Write(BYTE *pbData, DWORD dwBufferSize,
HANDLE hStopEvent = INVALID_HANDLE_VALUE);
virtual bool Wait(LPDWORD pdwBytesTransferred,
HANDLE hStopEvent = INVALID_HANDLE_VALUE);
private:
HANDLE m_hIO;
OVERLAPPED m_op;
};
The class uses the create/attach idiom. You create an instance of COverlappedIO
, either as a class member, a local variable or on the heap and then attach an open handle to an object you can write to or read from. This can be a file handle, a handle to a named or anonymous pipe, a handle to a comms port or handle to a maislot.
Once you've done the Attach()
you Read()
or Write()
through the object as appropriate. The Write()
method takes a pointer to the buffer to be written and the length, in bytes, of the buffer. The Read()
method takes the same parameters plus a pointer to a DWORD
variable which receives the number of bytes actually read. Both methods also take an optional HANDLE
to an event object. If the handle isn't specified in the call it defaults to INVALID_HANDLE_VALUE
. If you use the default value the Read()
or Write()
methods return immediately, having initiated an overlapped I/O operation. If you pass a handle the methods wait until the I/O operation completes or until the handle is signalled. Note that neither method can actually verify that what you passed is a valid event handle; all they can check is if the handle you passed was the default handle of INVALID_HANDLE_VALUE
.
The class also doesn't know if the operation you're about to perform on the handle you attached is a valid operation for that handle. If you want to read from the handle you need to ensure the handle was opened with read permissions; likewise for writing. This lack of knowing isn't due to carelessness; there are no API's that I know of that you can call to determine if a handle supports the operation you're about to perform on it, so the class has no choice but to trust you.
Read
looks like this. Write()
is similar.
bool COverlappedIO::Read(BYTE *pbData, DWORD dwBufferSize,
LPDWORD pdwBytesRead, HANDLE hStopEvent)
{
assert(pbData);
assert(dwBufferSize);
assert(pdwBytesRead);
if (m_hIO != INVALID_HANDLE_VALUE)
{
::ReadFile(m_hIO, pbData, dwBufferSize, pdwBytesRead, &m_op);
if (hStopEvent != INVALID_HANDLE_VALUE)
return Wait(pdwBytesRead, hStopEvent);
else
return true;
}
return false;
}
We do a bunch of assert
s on the parameters and proceed. We check if Attach()
has been called to attach a valid handle. If so we initiate a ReadFile()
call using overlapped I/O and, if that succeeds, we either return the result of a call to Wait()
or true
. Which we return depends on whether a valid stop handle was passed.
If you called Read()
or Write()
without specifying a valid stop handle the call returns after setting up the I/O operation but possibly before it completed. This is normal for overlapped I/O (it's called overlapped for a reason). You initiate an I/O operation and then continue with your normal work. Some time later you need to know if the I/O has completed so that you know it's safe to continue processing. That's where the Wait()
method comes in. As with the Read()
and Write()
methods the Wait()
method takes an optional stop event handle. The Wait()
method looks like this.
bool COverlappedIO::Wait(LPDWORD pdwBytesTransferred, HANDLE hStopEvent)
{
HANDLE hOverlapped[2] = { m_op.hEvent, hStopEvent };
if (m_hIO != INVALID_HANDLE_VALUE)
{
switch (WaitForMultipleObjects(
hStopEvent == INVALID_HANDLE_VALUE ? 1 : 2,
hOverlapped, FALSE, INFINITE))
{
case WAIT_OBJECT_0:
if (GetOverlappedResult(m_hIO, &m_op, pdwBytesTransferred, FALSE))
return true;
case WAIT_OBJECT_0 + 1:
return false;
}
}
return false;
}
Nothing terribly earth-shattering there. We create a 2 member array containing the overlapped event handle (the one that gets signalled when the overlapped I/O operation completes) and the stop handle. Then we check if the object has actually had an I/O handle attached and if so we fall into a call to WaitForMultipleObjects()
, passing it the 2 member array we created earlier and a handle count telling it how many handles to actually monitor. If the stop handle we passed is the default (INVALID_HANDLE_VALUE
) then we pass a count of 1 to WaitForMultipleOjects()
otherwise we pass a count of 2. Either way, WaitForMultipleObjects()
will wait until something happens. If the something is that the overlapped event handle was signalled we then call GetOverlappedResult()
to determine if the I/O operation succeeded and return an appropriate status. If anything else happens we return false
.
Cleanup
The class cleans up after itself for any objects it created. It won't (and shouldn't) call CloseHandle()
on the handle you attached to it. That's the reason there's an Attach()
method but there isn't a Detach()
method.
History
August 21, 2004 - Initial version.
September 3, 2004 - Minor changes to the class interface.