Many of the previous chapters have attempted to shed some light on the Windows graphical interface, but this chapter is going to start a detour into the inner-workings of the Windows operating system foundations. In this chapter, we are going to talk about Input and Output routines. This includes (but is not limited to) File I/O, Console I/O, and even device I/O.
- 1 File API
- 2 Console API
- 3 Device IO API
- 4 Completion Ports
- 5 Next Chapter
Files, like everything else in a windows platform, are managed by handles. When you want to read a file or write to one, you must first open a handle to that file. Once the handle is open, you may use the handle in read/write operations. In fact, this is the same with all I/O, including console I/O and device I/O: you must open a handle for reading/writing, and you must use the handle to perform your operations.
We will start with a function that we will see frequently in this chapter: CreateFile. CreateFile is the generic function used to open I/O handles in your system. Even though the name doesn't indicated it, CreateFile is used to open Console Handles and Device Handles as well. As the MSDN documentation says:
The CreateFile function creates or opens a file, file stream, directory, physical disk, volume, console buffer, tape drive, communications resource, mailslot, or named pipe. The function returns a handle that can be used to access an object.
Now, this is a powerful function, and with the power comes a certain amount of difficulty in using the function. Needless to say, CreateFile is a little more involved than the standard C STDLIB fopen.
HANDLE CreateFile( LPCTSTR lpFileName, DWORD dwDesiredAccess, DWORD dwShareMode, LPSECURITY_ATTRIBUTES lpSecurityAttributes, DWORD dwCreationDisposition, DWORD dwFlagsAndAttributes, HANDLE hTemplateFile);
As can be guessed, the "lpFileName" parameter is the name of the file to be opened. "dwDesiredAccess" specifies the desired access permissions for the file handle. In the most basic sense, for a file, this parameter can specify a read operation, a write operation, or an execute operation. However, don't be fooled, there are many many different options that can be used here, for different applications. The most common operations are GENERIC_READ, GENERIC_WRITE, and GENERIC_EXECUTE. These can be bitwise-OR'd to have read+write access, if needed.
File handles can be optionally shared or locked. A shared file can be simultaneously opened and accessed by other processes. If a file is not shared, then other programs attempting to access the file will fail. The "dwShareMode" specifies whether or not the file can be accessed by other applications. Setting dwShareMode to zero means that the file access cannot be shared, and other applications attempting to access the file, while the file handle is open, will fail. Other common values are FILE_SHARE_READ and FILE_SHARE_WRITE which allow other programs to open read handles and write handles, respectfully.
The lpSecurityAttributes is a pointer to a SECURITY_ATTRIBUTES structure. This structure can help to secure the file against unwanted accesses. We will discuss security attributes in a later chapter. For now, you can always set this field to NULL.
The dwCreationDisposition member would be better named "dwCreateMode" or something similar. This bit flag allows you to determine how the file is to be opened, according to different flag values:
- Always creates a new file. If the file exists already, it will be deleted, and overwritten. If the file does not exist, it is created.
- If the file exists, the function fails. Otherwise, creates a new file.
- Opens the file, without erasing the contents, if the file exists. Creates a new file if the file does not exist.
- Opens the file, without erasing the contents, only if the file exists already. If the file does not exist, the function fails.
- Opens the file, only if the file exists. When the file is opened, all the contents are deleted, and the file is set to 0 bytes long. If the file does not exist, the function fails. When opening with TRUNCATE_EXISTING, you must specify a GENERIC_WRITE flag as the access mode, or the function will fail.
The dwFileAttributes member specifies a series of flags for controlling File I/O. If the CreateFile function is being used to create something that isn't a File handle, this parameter is not used and may be set to 0. For accessing a normal file, the flag FILE_ATTRIBUTE_NORMAL should be used. However, there are also options for FILE_ATTRIBUTE_HIDDEN, FILE_ATTRIBUTE_READONLY, FILE_ATTRIBUTE_ARCHIVE, etc.
Finally, the hTemplateFile member can be specified if you want the new file handle to mimic the properties of an existing file handle. This can be set to NULL if not used.
Once a file handle is opened, ideally we would like to interact with the specified file. We can do this most directly by using the ReadFile and WriteFile functions. Both of them take similar parameters:
BOOL ReadFile( HANDLE hFile, LPVOID lpBuffer, DWORD nNumberOfBytesToRead, LPDWORD lpNumberOfBytesRead, LPOVERLAPPED lpOverlapped);
BOOL WriteFile( HANDLE hFile, LPCVOID lpBuffer, DWORD nNumberOfBytesToWrite, LPDWORD lpNumberOfBytesWritten, LPOVERLAPPED lpOverlapped);
In both, the hFile parameter is the handle to the file that we obtained with CreateFile. The lpOverlapped parameter is used only for a special I/O mode known as "Overlapped I/O Mode", which we will discuss later. For simple I/O, the lpOverlapped parameter can be set to NULL.
In ReadFile, the lpBuffer is a pointer to a generic buffer to receive the data. This data may not be character data, so we don't call it a LPSTR type. "nNumberofBytesToRead" is the number of bytes that should be read, and "lpNumberOfBytesRead" is the actual number of bytes that were read. If lpNumberOfBytesRead is zero, the file has no more data in it.
In WriteFile, the lpBuffer parameter points to the data that should be written into the file. Again, it isn't specifcally character data. nNumberOfBytesToWrite is the maximum number of bytes to write, and the lpNumberOfBytesWritten returns the number of bytes that were actually written to the file.
When you are done with a file handle, you should close it with the CloseHandle function. CloseHandle only takes one parameter, the file handle you wish to close. If you do not close your handle, Windows will automatically close the handle when the program closes. However, it is a more expensive operation for Windows to do it for you, and can waste time on your system. It is a good idea to always explicitly close all your handles before you exit your program.
Failure to close a handle is known as "handle leaks", and are a common form of memory leakage that can cause your program, and your entire system, to lose resources and operate more slowly. The handle itself occupies only 32-bits of information, but internally the kernal maintains a large amount of data and storage for every handle. Failure to close a handle means that the kernal must maintain all the associated information about the handle. It also costs the kernel additional time and resources to check through all the old unused handles when it is looking for information about a current handle.
Memory-Mapped files provides a mechanism to read and write to a file using regular pointers and array constructs. Instead of reading from the file using ReadFile, you can read from the file using a memory pointer. The system does this by reading in the file to a memory page, and then writing changes to that page onto the physical disk. There is a certain amount of additional overhead to read the file into memory at first, and to write it back after the mapping is completed. However, if there are many accesses to the file, it can be much more convenient in the long run.
"Overlapped" I/O is the term Microsoft uses to describe asynchronous I/O. When you want to do I/O, either to a file or to an external device, you have two options:
- Synchronous (non-overlapped)
- You request the I/O from the system, and wait till the I/O has completed. The program will stop running until the I/O has completed.
- Asynchronous (overlapped)
- You send a request to the system, and the system completes that request in parallel with your program. Your program can continue to do processing work, and the system will automatically send notification when your request has been completed.
Synchronous I/O is much easier to use, and is much more straight forward. In synchronous I/O, things happen sequentially, and when the I/O function has returned, you know that the transaction is complete. However I/O is typically much slower then any other operation in your program, and waiting on a slow file read, or a slow communications port can waste lots of valuable time. In addition, if your program is waiting for a slow I/O request, the graphical interface will appear to hang and be non-responsive, which can annoy the user.
Programmers can avoid these delays by using dedicated threads or a thread pool to execute synchronous I/O operations. But threads have significant overhead, and creating too many of them exhausts system resources. Asynchronous I/O avoids this overhead, and is thus the preferrable API for high-performance high-load server applications.
Asynchronous I/O is more complicated to use: It requires the use of the OVERLAPPED structure, and the creation of a handler function that will be called automatically by the system when the I/O is complete. However, the benefits are obvious in the efficiency of the method. Your program can request multiple transactions without having to wait for any of them to complete, and it can also perform other tasks while the system is performing the required task. This means that the programs will appear more responsive to the user, and that you can spend more time on data processing, and less time waiting for data.
Allocating a Console
A console can be allocated by calling the AllocConsole function. Normally we need not do so if we are creating a "console process" (which contains the main function) because they are already attached to a console. However we can create a console for "GUI process" (which entry point is WinMain) and perform I/O operation on the newly created console. It should be noted that each process can only be associated with one console. If the process has already attached to a console, calling AllocConsole will return FALSE.
After calling AllocConsole, the Windows Command Prompt window will appear.
A console can be freed by calling FreeConsole.
Getting a Console Handle
Upon the creation of console, the standard output, standard input and standard error handles (we call them the "standard devices") will be initialized. These handles are essential for any console I/O operations. They can be obtained by calling GetStdHandle, which accepts a parameter specifying the handle of the standard device to be obtained. The parameter can be any of the following:
- Specifies the standard output device, which is used for outputting data to the console.
- Specifies the standard input device, which is used for reading input from the console.
- Specifies the standard error device, which is mainly used for outputting error.
If the function succeeded, the return value is the handle to the standard device specified. If failed, it will return INVALID_HANDLE_VALUE.
High Level I/O
The <stdio.h> or <iostream> (C++ only) header files contain the functions typically used for high level console I/O. The high level I/O are typically "buffered". Such functions including printf, scanf, fgets etc. If we wish to do unbuffered I/O, we can use the fread or fwrite functions and pass stdin, stdout or stderr to the parameter specifying the standard input, standard output and standard error devices respectively. It is generally not advisable to combine the use of both high level and low level I/O however.
These functions are designed to be portable and act as an abstraction to the low level system I/O functions.
Low Level I/O
The low level console I/O can be done by using several API functions such as WriteConsole, ReadConsole, ReadConsoleInput etc.
BOOL WriteConsole( HANDLE hConsoleOutput, const VOID *lpBuffer, DWORD dwNumberOfCharsToWrite, LPDWORD lpNumberOfCharsWritten, LPVOID lpReserved );
BOOL ReadConsole( HANDLE hConsoleInput, LPVOID lpBuffer, DWORD dwNumberOfCharsToRead, LPDWORD lpNumberOfCharsWritten, LPVOID pInputControl );
Please note that the "Chars" referred is actually the number of TCHAR, which can be 2-bytes wide when UNICODE is defined. It is NOT the number of bytes.
The ReadConsoleInput can be used to read keystrokes, which can't be done with C or C++ standard library. There are many more functions which provide powerful I/O functions.
Colors and Features
There are many exciting API functions that provide additional controls over the console. One of the more commonly used function is the SetConsoleTitle which is used to set the console title text. We can also alter the position of the cursor by using the SetConsoleCursorPosition function.
We can output text with different foreground and background colors by SetConsoleTextAttribute. We can also change the size of the screen buffer by SetConsoleScreenBufferSize.
For an extensive documentation of all the Console API one can consult MSDN.
Device IO API
Interaction between a program and a device driver can be complicated. However, there are a few standard device drivers that may be used to access standard ports and hardware. In most instances, interacting with a port or a piece of hardware is as easy as opening a handle to that device, and then reading or writing to it like a file. In most instances, these ports and devices can be opened using the CreateFile function, by calling the name of the device instead of the name of a file.
Getting a Device Handle
Device IO Functions
Warnings about Device IO
This page of the Windows Programming book is a stub. You can help by expanding it.