Dr. Dobb's is part of the Informa Tech Division of Informa PLC

This site is operated by a business or businesses owned by Informa PLC and all copyright resides with them. Informa PLC's registered office is 5 Howick Place, London SW1P 1WG. Registered in England and Wales. Number 8860726.


Channels ▼
RSS

.NET

The Windows Communications API


MAY92: THE WINDOWS COMMUNICATIONS API

This article contains the following executables: WINCOM.ARC

Mike is a Windows programming and development consultant. He has been programming in Windows since the advent of version 1.02 and can be reached on CompuServe at 75470,1403.


Windows 3 communications programs suffer from excess overhead mainly because they can't concentrate on just one task at a time. Tasks that must be attended to include monitoring the communications port; giving control to individual applications; keeping the user interface running interactively while communications are in progress; using virtual memory; and accessing the disk while communications are in progress. As a result, even well-implemented communications programs run somewhat slower under Windows 3 than under DOS at speeds of 9600 baud or higher. All this has given Windows 3 communications a bad name.

Still, Windows communications services provide a portable way to gain full control over the communications port and perform buffered I/O. Furthermore, under Windows 3.1, communications support has been significantly enhanced by fully exploiting the FIFO stack capabilities of the 16550 UART chip built into most newer PCs. In addition, Windows 3.1 applications can be notified through messages when certain communications events occur, so the application does not have to constantly poll the communications port in a continuous loop.

In this article, I'll discuss the Windows communications API and some key issues involved in porting DOS applications to the Windows environment.

XModem and the Windows Communications API

The Windows communications API is a collection of 16 functions. For starters, when you open a Windows communications port using the OpenComm function, the return value is an integer, which is used to identify the port in subsequent calls to the Windows communications API.

Once you have opened the comm port, you set the communications parameters (baud rate, handshaking, parity, stop bits, and so on) by filling in a data structure called the "Device Control Block" (DCB) and passing it to the Set-CommState function. The DCB data structure provides control over the comm port, and filling it in can be quite complex. The SetComPortParameters function in COMM.C (Listing One) does all the hard work for you. (Listing Two is COMM.H.).

If a communications error occurs, Windows locks the comm port until you call the GetCommError function. This function lets you retrieve information about the error, then unlocks the comm port. Because calling GetCommError after every line in which you access the comm port requires extra code, you might want to use higher-level functions such as those in Listing One.

After low-level functions have been addressed, you can move on to higher-level issues. In discussing these issues, I'll implement the XModem file-transfer protocol for Windows. For a discussion on the internal workings of XModem, refer to Al Stevens's "C Programming" column in the April 1989 issue of DDJ from which I "borrowed" the core code.

There are two important issues to consider when converting Al's XModem implementation to Windows. First, the user interface code is different. In Windows, you have a dialog box that shows status information, and this enables the user to abort the transfer by pressing a Cancel button. Secondly, the current version of Windows implements non-preemptive multitasking for all Windows applications. This means that your program has to give control to the system to allow other Windows applications to run. In this discussion, I'll put the UI issues aside, focusing instead on the core-application multitasking requirements.

Your system and all other Windows programs will be locked during tbe entire XModem transfer, and any attempt by the user to use the keyboard or mouse will fail. We not only have to give control to other Windows applications, but also to process any messages that arrive in the application's message queue. Consequently, we need to write a function, DoEvents, which processes all messages in our message queue and gives control to Windows for a short period of time.

Before writing DoEvents, you should be aware that both the GetMessage() and PeekMessage() Windows functions retrieve a message from the application's message queue. The main difference between them is that PeekMessage() will immediately return control to your program, even if no messages are waiting. A call to GetMessage() will not return until there is a message in your message queue, so this function cannot be used for background processing. Another difference is the return value of these functions. PeekMessage() returns TRUE if it has retrieved a message from the message queue, or FALSE if the message queue is empty. GetMessage() always returns TRUE, except when the message it retrieves is a WM_QUIT message.

The Yield() function gives control to other Windows applications if no messages are waiting in the message queue. But if there are messages in the queue and you are only using Yield(), how will the application process these messages? The answer is simple: It won't. The messages will remain unprocessed, and other Windows applications will not get control until we call PeekMessage() or GetMessage() to empty the message queue.

DoEvents() in XMODEM.C calls PeekMessage until all the messages in the message queue have been processed (in which case PeekMessage will return FALSE). When no more messages are left to process, PeekMessage automatically gives control to other applications.

When you receive a message using PeekMessage, you can use TranslateMessage and DispatchMessage, just as you would in a normal message loop. Because we're using a modeless dialog box, you also have to call the IsDialogMessage() function to make the status window not only look like a dialog box but also act like one.

Also, if you receive a WM_QUIT message inside the main message loop, this means that Windows wants our program to terminate as soon as possible, so we post the WM_QUIT message using PostQuitMessage and abort the XModem transfer by setting the gbUserCanceled global variable to TRUE, just as if the user had pressed the Cancel button in our status dialog.

The rest of the XModem code isn't much different from the original DOS code in Al Stevens's article. I just replaced a few calls to the communications functions with calls to the high-level functions in Listing One and replaced the console I/O calls with calls to SetDlgItemText and SetDlgItemInt.

Writing a Windows Terminal Emulator

In DOS, it's easy to implement TTY terminal emulation because the DOS text screen is basically a sophisticated TTY terminal. But Windows uses a graphical user interface, and there's no such thing as a standard TTY control. To create a terminal emulator for Windows, I implemented a new window class, TERMINAL, which is a virtual DOS text screen in a window, much like the screen you get when running a DOS application in Windows Enhanced Mode.

If you want to use the terminal control in your application, you should call InitTerminal at the start of the WinMain function. This function initializes global variables that contain the handle and the dimensions of the OEM text font.

The cbWndExtra field in the class structure is set to the size of a local memory handle. When a new terminal window is created, and the terminal window function receives a WM_CREATE message, a local memory block will hold the virtual screen buffer along with other information, such as the current cursor position, the number of rows and columns displayed, and so on. The local memory handle is then stored in the first extra bytes of the newly created window. Of course, when the window is destroyed and receives a WM_DESTROY message, the local memory block is freed.

If the terminal window is too small to display the entire virtual screen, the user should be able to scroll the window. When the window is resized and it receives a WM_SIZE message, the function sets the new scroll ranges for the horizontal and vertical scroll bars. When the user clicks the scroll bar, the terminal window receives a WM_HSCROLL or WM_VSCROLL message. Because the code to handle vertical and horizontal scrolling is similar, we have created a HandleScroll function, which checks the value of wParam and returns the new value of the scroll-bar position.

The terminal window should also respond to the WM_GETDLGCODE. When the terminal window is part of a dialog box, the dialog box sends this message to the control to determine who should processes certain keyboard messages (such as pressing the tab and arrow keys). The terminal-window function returns DLGC_WANTALLKWYS to indicate that it wants to processes all key codes.

What should happen when the user enters characters in the window? We might send the characters to the comm port inside the terminal control's window function, but it's always a good idea to write modular, reusable code. The standard Windows controls send notification messages to their parent windows when something interesting happens. A notification message is basically a WM_COMMAND message with wParam equal to the child control's ID and more information in lParam. So when the terminal window receives a WM_CHAR message, it sends a notification message to its parent window, with the low word of lParam equal to the character code.

Likewise, we could use a function to print characters on a terminal window, but it's more in the "spirit of Windows" to use a message for this. All the standard controls have user-defined messages (WM_USER plus a certain value) to control their behavior, so why shouldn't our terminal control do the same? To print a character to the terminal window, you can send a TW_SENDCHAR message to the terminal window.

Putting it all Together

At this point, we have high-level communications functions. a Windows XModem implementation, and a terminal-window control. It's time to put it all together and create a terminal program.

The terminal program's main window is a dialog box that has its own class. In addition to a terminal window and a status bar, it contains a few comboboxes to let the user change the current communications settings. We use a dialog box because the user has to be able to navigate through the comboboxes using the tab keys.

Standard Windows dialog boxes automatically use the default dialog box class. But if we include a CLASS statement in the dialog definition in our .RC or .DLG file, the dialog box will have its own class. The advantage of having our own class is that we don't have to use the default fields in the standard dialog box class structure.

When you send or post a message to a window, Windows will call the window function defined in the lpfnWndProc field of the class structure. So normally, we should have a window function for every class we define. But because we want our main window to act like a standard dialog box, we let the lpfnWndProc point to DefDlgProc, the Windows function used in standard dialog boxes.

When the user presses Escape or Enter in one of the comboboxes, the focus should be set to the terminal window again. To avoid subclassing the comboboxes, we can create two invisible buttons which have the standard IDs used for OK and Cancel--IDOK and IDCANCEL, respectively. This way, the Windows dialog-box code will immediately send a WM_COMMAND message to our dialog function with wParam either equal to IDOK or IDCANCEL. If we receiv such a message, we set the focus to the terminal window.

When the user selects a new value from the combobox to change any of the communications parameters, the dialog function will receive a WM_COMMAND notification message with the combobox child ID in wParam and CBN_SELCHANGE in the high word of lParam. The dialog function will automatically retrieve the current communications parameters from the comboboxes and use the high-level SetComParameters function from Listing One. If the attempt to change the current communications parameters fails, an error message will be shown in the status bar.

Because we also have to receive characters from the comm port in the background, we have created a DoEvents function, just like in the XModem transfer. But the DoEvents function in our terminal program returns a BOOL instead of void. This is because DoEvents will return FALSE if the last message retrieved was a WM_QUIT message. So in the WinMain message loop we can process characters from the comm port as long as DoEvents return TRUE.

When the user enters a character in the terminal window, the terminal window will send a notification message to our main window. When the dialog function retrieves a WM_COMMAND message with wParam equal to the terminal window's ID, the dialog function sends the value in the high word of lParam to the communications port.

Conclusion

We now have a fully functional Windows terminal program in less than 1200 lines. Windows programming can be tough sometimes, but the results are almost always rewarding. The message-oriented nature of Windows makes it easy to write reusable code in Windows. The high-level communications functions, the XModem transfers, and the TTY terminal control can be incorporated into any program without changes. Windows communications may not be perfect, but it sure has come a long way. And it's getting better every time a new version of Windows is released.



_THE WINDOWS COMMUNICATIONS API_
by Mike Sax



[LISTING ONE]
<a name="0111_0009">

///////////////////////////////////////////////////////////////////////////
//  COMM.C  - by Mike Sax for Dr. Dobb's Journal
//  This file implements a few higher-level communicatons functions under
//  Microsoft Windows. This file contains eight public functions:
//  int OpenComPort(int nPort);
//  BOOL CloseComPort(int nPortID);
//  BOOL SetComPortParameters(int nPortID, int nSpeed, char chParity,
//      int nDataBits, int nStopBits, BOOL bXOnXOff, BOOL bHardware);
//  int CharsWaitingToBeRead(int nPortID);
//  int ComReadChar(int nPortID);
//  int ComReadChars(int nPortID, char *pchBuffer, int cbBuffer);
//  BOOL ComWriteChar(int nPortID, int nChar);
///////////////////////////////////////////////////////////////////////////

#define USECOMM 1               // for 3.1 windows.h
#include <windows.h>
#include "comm.h"

// Opens comm port (1 = COM1) and returns its ID value.
// If an error occurs, return value is negative
int OpenComPort(int nPort)
    {
    char szPort[10];

    wsprintf(szPort, "COM%d", nPort);
    // Open the port with a 4K input queue and a 2K output queue
    return OpenComm(szPort, 4096, 2048);
    }
// Closes comm port specified by nPortID: returns TRUE if success,
// FALSE if failure
BOOL CloseComPort(int nPortID)
    {
    if (nPortID < 0)
        return FALSE;
    FlushComm(nPortID,0);       // Flush transmit queue
    FlushComm(nPortID,1);       // Flush receive queue
    return !CloseComm(nPortID);
    }
// Sets communications parameters of port specified by nPortID:
// returns TRUE if success, FALSE if failure
BOOL SetComPortParameters(int nPortID, int nSpeed, char chParity,
                  int nDataBits, int nStopBits, BOOL bXOnXOff, BOOL bHardware)
    {
    DCB dcb;

    if (nPortID < 0)
        return FALSE;
    dcb.Id = nPortID;
    dcb.BaudRate = nSpeed;
    dcb.ByteSize = (BYTE)nDataBits;
    // Convert chParity to uppercase:
    AnsiUpperBuff(&chParity, 1);
    dcb.Parity =  (chParity == 'N') ? NOPARITY :
                  (chParity == 'O') ? ODDPARITY :
                  (chParity == 'E') ? EVENPARITY :
                  (chParity == 'M') ? MARKPARITY : SPACEPARITY;
    dcb.StopBits = (BYTE)((nStopBits == 1) ? ONESTOPBIT :
                (nStopBits == 2) ? TWOSTOPBITS : ONE5STOPBITS);
    dcb.RlsTimeout= 0;
    dcb.CtsTimeout = bHardware ? 30 : 0;
    dcb.DsrTimeout = 0;
    dcb.fBinary = TRUE;
    dcb.fRtsDisable = FALSE;
    dcb.fParity = FALSE;
    dcb.fOutxCtsFlow = (BYTE)bHardware;
    dcb.fOutxDsrFlow = FALSE;
    dcb.fDummy = 0;
    dcb.fDtrDisable = FALSE;
    dcb.fOutX = (BYTE)bXOnXOff;
    dcb.fInX = (BYTE)bXOnXOff;
    dcb.fPeChar = FALSE;
    dcb.fNull = FALSE;
    dcb.fChEvt = FALSE;
    dcb.fDtrflow = (BYTE)FALSE;
    dcb.fRtsflow = (BYTE)bHardware;
    dcb.fDummy2 = 0;
    dcb.XonChar = 17;
    dcb.XoffChar = 19;
    dcb.XonLim = 4096 / 4;              // Receive buffer size / 4
    dcb.XoffLim = dcb.XonLim;
    dcb.EofChar = 26;
    dcb.EvtChar = 0;
    dcb.TxDelay = 0;
    return !SetCommState(&dcb);
    }
// Returns the number of characters waiting in the input queue
int CharsWaitingToBeRead(int nPortID)
    {
    COMSTAT ComStat;

    if (nPortID < 0)
        return 0;
    GetCommError(nPortID, &ComStat);
    return ComStat.cbInQue;
    }
// Read character from port specified by nPortID:
// returns -1 if no character available
int ComReadChar(int nPortID)
    {
    int iResult = 0;

    if (nPortID < 0)
        return -1;
    if (ReadComm(nPortID, (LPSTR)&iResult, 1) != 1)
        {
        iResult = -1;
        GetCommError(nPortID, NULL);
        }
    return iResult;
    }
// Read character from port specified by nPortID:
// returns the number of characters read or -1 if an error occurs.
int ComReadChars(int nPortID, char *pchBuffer, int cbBuffer)
    {
    int iResult = 0;

    if (nPortID < 0)
        return -1;
    iResult = ReadComm(nPortID, pchBuffer, cbBuffer);
    if (iResult < 0)
        {
        iResult = -1;
        GetCommError(nPortID, NULL);
        }
    return iResult;
    }
// Write a character to the port specified by nPortID
// returns TRUE if success, FALSE if failure
BOOL ComWriteChar(int nPortID, int nChar)
    {
    if (nPortID < 0)
        return FALSE;
    if (1 != WriteComm(nPortID, (LPSTR)&nChar, 1))
        {
        GetCommError(nPortID, NULL);
        return FALSE;
        }
    return TRUE;
    }




<a name="0111_000a">
<a name="0111_000b">
[LISTING TWO]
<a name="0111_000b">

BOOL ComWriteChar(int nPortID, int nChar);
int ComReadChar(int nPortID);
int ComReadChars(int nPortID, char *pchBuffer, int cbBuffer);
int CharsWaitingToBeRead(int nPortID);
BOOL SetComPortParameters(int nPortID, int nSpeed, char chParity,
                 int nDataBits, int nStopBits, BOOL bXOnXOff, BOOL bHardware);
BOOL CloseComPort(int nPortID);
int OpenComPort(int nPort);


Copyright © 1992, Dr. Dobb's Journal


Related Reading


More Insights






Currently we allow the following HTML tags in comments:

Single tags

These tags can be used alone and don't need an ending tag.

<br> Defines a single line break

<hr> Defines a horizontal line

Matching tags

These require an ending tag - e.g. <i>italic text</i>

<a> Defines an anchor

<b> Defines bold text

<big> Defines big text

<blockquote> Defines a long quotation

<caption> Defines a table caption

<cite> Defines a citation

<code> Defines computer code text

<em> Defines emphasized text

<fieldset> Defines a border around elements in a form

<h1> This is heading 1

<h2> This is heading 2

<h3> This is heading 3

<h4> This is heading 4

<h5> This is heading 5

<h6> This is heading 6

<i> Defines italic text

<p> Defines a paragraph

<pre> Defines preformatted text

<q> Defines a short quotation

<samp> Defines sample computer code text

<small> Defines small text

<span> Defines a section in a document

<s> Defines strikethrough text

<strike> Defines strikethrough text

<strong> Defines strong text

<sub> Defines subscripted text

<sup> Defines superscripted text

<u> Defines underlined text

Dr. Dobb's encourages readers to engage in spirited, healthy debate, including taking us to task. However, Dr. Dobb's moderates all comments posted to our site, and reserves the right to modify or remove any content that it determines to be derogatory, offensive, inflammatory, vulgar, irrelevant/off-topic, racist or obvious marketing or spam. Dr. Dobb's further reserves the right to disable the profile of any commenter participating in said activities.

 
Disqus Tips To upload an avatar photo, first complete your Disqus profile. | View the list of supported HTML tags you can use to style comments. | Please read our commenting policy.