Porting from DOS to Windows

WINGate's client/server framework minimizes recoding

Walter Oney

Walter is a Boston-based freelance developer and consultant specializing in system tools and interfaces between applications and NT, Windows, and DOS. He can be reached on CompuServe at 73730,553.


Although 16-bit Windows doesn't support preemptive multitasking of Windows apps, it does allow for preemptive, hardware-supported multitasking of "DOS boxes." As you may know (especially if you have been following Andrew Schulman's "Undocumented Corner" column in DDJ), this is accomplished by a protected-mode, virtual-machine operating system (the Virtual Machine Manager, or VMM) that kicks in when running Windows in Enhanced mode. Armed with this knowledge and faced with an aging DOS application crying for a modern user interface, what developer wouldn't want the ability to graft a Windows front-end client onto a concurrently executing DOS back-end server? This is the premise behind WINGate from WINGate Technologies.

WINGate allows a DOS program to communicate directly with a Windows application using a transaction-based API. Once this communication possibility is enabled, it leads to many interesting ways of exploiting the Windows environment.

The Components

WINGate Version 1.3x consists of a set of development components for building your own client/server applications, a Windows Virtual Device Driver (VxD) for coordinating client and server processes, and a set of prebuilt client and server applets that you can use to perform a few Windows operations--such as launching and killing Windows applications--from the command level in a DOS box. Libraries are available for 16-bit C and Visual C++, Pascal, Basic, Visual Basic, Clipper, Foxpro, dBase, 32-bit C (Watcom, Zortech, and MetaWare), and more. WINGate requires Enhanced-mode Windows, which of course implies an 80386 CPU.

WINGate's target market is developers who want to preserve the value of an existing DOS application by attaching a Windows GUI. Accordingly, I tried it out by using a simple client/server database. I found this aspect of WINGate to be eminently usable and well thought-out. I did, however, encounter a performance problem that might require attention in a commercial-grade product (more on this later). The WINGate package, in addition to its libraries, includes ancillary components such as prebuilt applets and an installation program.

WINGate comes on a single 3.5-inch diskette with a Windows-based installation program, which copies the files and adds a DEVICE= line to your SYSTEM.INI. While generally smooth, the install program does suffer from minor glitches. For example, unless you prevent it, it replaces your DOS PATH setting with a single entry for the WINGATE directory. Another problem has to do with version stamping. The Windows 3.1 API provides a standard facility for stamping executable files with version information via the resource script. An install program should compare the version of a diskette-based file with any pre-existing copy on your hard drive, to avoid overwriting a more recent version. WINGate includes files that will presumably be redistributed by many developers. Since these files lack the resource version information, it's possible that the wrong version could end up on an unsuspecting end user's machine.

The Simplest Client/Server Application

To investigate WINGate, I built a simple client/server database application. The "database server" (Listing One, page 100) is a real-mode DOS application that provides, on request, the name of the capital city of any country in the known universe--provided the country is within a small table located within the program. The client (Listing Two, page 100), a simple database-query generator, is a Windows app that asks the user for a country and then displays that country's capital.

This simple project was a breeze. I used C8 (the command-line compiler that forms the backbone of Visual C++ 1.0) on a 486/33 and had a working example in about two hours. The printed manual contains excellent and accurate documentation of the 32 functions in the API; the API routines themselves are aptly named. A short overview of client and server programming at the beginning of the manual gives a step-by-step cookbook for building the programs, and sample code illustrates all the steps. The sample code shows a DOS-based client and a Windows-based server, however. Since I was building a DOS-based server and a Windows-based client, an example of this converse situation would have helped.

Many programmers may have trouble using WINGate successfully, I fear. This has much less to do with WINGate itself than with the difficulty of client/server applications in general and communication-based protocols in particular. I find WINGate easier to use than most network protocols because it imposes much less of a burden on me to establish the initial connection.

The code for a Windows-based client is found in CLIENT.C. As you can see, the client registers itself as a WINGate user during processing of a WM_CREATE message via a call to WGRegister(), passing an argument (in this case, client), which is a unique, system-wide name identifying this particular WINGate client. My example's identifier isn't particularly unique; you might want to use wsprintf to generate a unique string based on your task or instance handle.

My example does not do much error checking; you certainly would need to add this in a production application. For example, a production application should ensure that the corresponding server is up and running. You could use WinExec() to launch the server and then use various WINGate services to establish a connection. Alternatively, you could use the WGServerInfo API to get a list of available servers and verify that the one you wanted is up and running. For this demonstration, I manually started the server in a DOS box.

Selecting a country within the list box which occupies the client area triggers a database query using the code in Example 1. The first three API calls setup a transaction packet to ask the database server what the capital of the selected country is. WGExecute transmits the packet to the server, and WGGetResponse waits until a response occurs. WGGetTransString extracts the answer to the query as a null-terminated string, and WGDestroyTrans cleans up by releasing the resources associated with the transaction.

The server-side code that responds to queries is equally simple. You use a DOS-only API function named WGInit to initialize the WINGate package and another function called WGRegisterServer to register the program as a WINGate server. You now enter a loop in which you poll for transactions; see Example 2. The additional API calls in the server are WGPostResponse, which sends a response back to the client, and WGGetTransID, which gets the query transaction's ID for use in posting the response. I detect a shutdown or query request within the transaction-processing code by checking for a zero-return from WGGetTransValue(trans, 1, &code).

A WINGate transaction includes eight 32-bit data fields whose use is entirely up to the client and server programs. WGSetTransValue is used to set these fields, and WGGetTransValue, to get their values. I used field number 1 as an opcode, with 0 meaning "shutdown" and 1 meaning "query the database." Looking at Listing One, you'll notice that the WM_DESTROY message sends a transaction, to which no response is expected, containing a 0 opcode. This is the only normal way the server can be made to exit. The use of 0 here is deliberate, by the way, since WGGetTransValue also returns 0 if there's an error.

Within the DOS-based server program, I've shown you the call to WGGetTrans that polls for a query transaction. But suppose no transaction is available at a particular time. The WINGate manual doesn't say what to do. Normally, the right thing to do is to give up your time slice by issuing interrupt 2F/1680, and that's what my sample program does. The Windows scheduler then allows other virtual machines to run. Unfortunately, the scheduler then also imposes a 50-millisecond execution penalty before the yielding virtual machine is again eligible to run. In the case of my sample, I saw a noticeable delay between selecting a country and seeing the response, and I infer this is due to the scheduling penalty.

The WINGate VxD could overcome the scheduling penalty by using the Wake_Up_VM and Set_Execution_Focus services judiciously. Alternately, WINGate could include a "wait-for-transaction" API that would cause the VM to block on a semaphore. The driver does appear to use the Suspend_VM and No_Fail_Resume_VM services to suspend and resume a DOS virtual machine in some cases (perhaps when a DOS process does a WGGetResponse), but these services perform more slowly than VxD semaphore services.

Prebuilt Clients and Servers

WINGate includes several matched pairs of client and server applets. For example, there is a DOS-mode client program, WINSPAWN, that communicates with a WinApp server named WGSPAWN in order to launch new DOS or Windows programs. You're supposed to use these applets by starting the Windows-side server program (perhaps using the LOAD= directive in WIN.INI) ahead of time. You then use the DOS-side client program as necessary to direct the server to perform some function or another.

I found the rationale for these applets a bit mysterious and also encountered a problem when using one of them. After first launching all the necessary Windows-side server programs, I started a DOS session and used the WINSPAWN client to launch a copy of NOTEPAD; see Example 3(a). An instance of NotePad then appeared on my desktop. Presumably, the number (11591) displayed on the confirmation line is NotePad's task handle, in decimal. I then used WINCTRL to resize and move the NotePad window; see Example 3(b). Predictably, the NotePad window moved. Finally, I used WINKILL to close out this copy of NotePad; see Example 3(c). NotePad then asked me if I wanted to lose my unsaved changes (I'd done some typing in the NotePad window) and exited. Throughout it all, the DOS "client" commands executed asynchronously from the Windows side. In other words, WINSPAWN returned before NotePad was actually up and running, WINCTRL returned before NotePad's window was moved, and so on. This sequence left my Windows session in a sorry state--I couldn't pull down any menus! Accelerator keys worked correctly, however, so I tried to exit from Windows. At this point a system modal dialog told me that Windows was extremely low on memory, whereupon I rebooted my computer.

Even assuming my experience was atypical, I still wonder whether these little utilities are useful. Since you obviously won't type these commands in a DOS box if you can just move the mouse, they will likely be used from a .BAT file. It isn't easy, however, to capture stdout output like "Execution OK 11591" to save the task handle you'll need for later commands. You can, of course, simply use the name of an application in WINCTRL or WINKILL, but you then face the possibility of moving (or closing!) the wrong window if multiple instances are active. Moreover, no synchronization primitives are available at the command level that let you wait until an app is launched before you try to kill it, for example.

The Clipboard Applets

At first glance, it seems that the WINCLIP/WGCLIP pair of applets provide a useful function. Normally in Windows, if you want to paste a bitmap or spreadsheet file from one app to another, you first open an application that understands the file, mark the portion you want to copy, copy it to the clipboard, paste it into the target application, and then close the originating app. With WINCLIP, you can put the whole file directly onto the clipboard with a single DOS command: winclip dib to @\windows\cars.bmp.

Leaving aside the messy command syntax, I find the implementation of WINCLIP disappointing. The Windows shell already exports an INT 2F/17xx interface by which a DOS box can interact with the clipboard. (See Tom Olsen's article, "Making Windows and DOS Programs Talk," Windows/DOS Developer's Journal, May 1992.) Apparently, WINCLIP doesn't use this interface. If it did, it wouldn't need to have a matching server or use the WINGate VxD. There would, moreover, possibly be more clipboard formats available.

Conclusion

Despite my quibbles about the ancillary pieces of WINGate, I think it is a very worthwhile tool. The WINGate concept of coupling DOS and Windows applications with a client/server API mediated by a VxD has considerable technical charm. The author of WINGate has done a good job of creating an API and a set of libraries to facilitate the task.

Many commercial-product developers will prefer to move whole-hog to a Windows implementation without considering the potential simplification that products like WINGate offer. On the other hand, I've fielded enough questions at industry forums to know that corporate developers and individual consultants will appreciate a less thorough-going and more pragmatic alternative.

Example 1: A Windows client creating a transaction using WINGate.

WGTRANS trans = WGCreateTrans("client", 80);
WGSetTransString(trans, country[i]);
WGSetTransValue(trans, 1, 1);
long tid = WGExecute(trans, "server", 1000,
   WG_STAT_WAIT_RESPONSE, &code);
WGGetResponse(trans, tid, WG_STAT_WAIT_RESPONSE);
WGGetTransString(trans, capital, sizeof(capital));

WGDestroyTrans(trans);

Example 2: The corresponding DOS server responding to the transaction.

WGTRANS trans = WGGetTrans();
if (trans)
{                  // process transaction
   WGGetTransString(trans, country, sizeof(country));
   ...             // [code that looks up capital city name]
   WGTRANS response = WGCreateTrans("server",strlen(capital)+1);
   WGSetTransString(response, capital);
   long tid = WGGetTransID(trans, &code);
   WGPostResponse(response, tid);
   WGDestroyTrans(response);
}

Example 3: (a) Using WINSPAWN to launch Windows NotePad from DOS; (b) using WINCNTRL to resize the NotePad window; (c) using WINKILL to shut down NotePad.

(a)
     C:\WINGATE>winspawn notepad
     Execution OK ==> 11591

(b)
     C:\WINGATE>winctrl -ms
        #11591 300 240 300 240
     Execution OK ==> 0

(c)
     C:\WINGATE>winkill #11591
     Execution OK ==> 0


Products Mentioned

WINGate 1.38
WINGate Technologies Inc.
High Street Court, Suite 303
Morristown, NJ 07960
800-946-4283

[LISTING ONE]


/**********************************************************************/
/* CLIENT.C -- Database client program (Windows app). By Walter Oney  */
/**********************************************************************/

#include <windows.h>
#include <windowsx.h>
#include <string.h>
#include <wingate.h>

static HINSTANCE hInst;

LRESULT CALLBACK MainWndProc(HWND, UINT, WPARAM, LPARAM);

#define arraysize(p) (sizeof(p)/sizeof((p)[0]))
/**********************************************************************/
int NEAR PASCAL WinMain(HINSTANCE hInstance, HINSTANCE hPrev,
    LPSTR lpCmd, int nShow)                          // WinMain
{   HWND hwnd;              // main window handle
    MSG msg;                // current message
    WNDCLASS wc;            // window class descriptor

    if (hPrev)              // only allow 1 instance at a time
        return 0;
    hInst = hInstance;
    memset(&wc, 0, sizeof(wc));
    wc.lpszClassName = "clientwindow";
    wc.hInstance = hInstance;
    wc.lpfnWndProc = MainWndProc;
    wc.hCursor = LoadCursor(NULL, IDC_ARROW);
    wc.hIcon = LoadIcon(NULL, IDI_APPLICATION);

    wc.hbrBackground = (HBRUSH) (COLOR_WINDOW + 1);
    if (!RegisterClass(&wc))
        return 0;
    hwnd = CreateWindow("clientwindow", "WINGate Database Demonstration",
        WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, CW_USEDEFAULT, CW_USEDEFAULT,
        CW_USEDEFAULT, 0, NULL, hInstance, NULL);
    if (!hwnd)
        return 0;
    ShowWindow(hwnd, nShow);

    while (GetMessage(&msg, 0, 0, 0))
    {                                 // process messages
        TranslateMessage(&msg);
        DispatchMessage(&msg);
    }                                 // process messages end
    return msg.wParam;
}                                     // WinMain end
/**********************************************************************/
LRESULT CALLBACK MainWndProc(HWND hwnd,UINT msg,WPARAM wParam,LPARAM lParam)
{                                     // MainWndProc
    static char *country[] =
    {   "Afghanistan","Algeria","Angola","Argentina","Australia","Austria"
    };
    static HWND hwndList;           // list of countries
    static BOOL bRegistered;        // TRUE if registered with WINGate
    switch (msg)                    // process message
    {   case WM_CREATE:             // WM_CREATE
        {   int i;
            if (WGRegisterClient("client"))
            {                      // couldn't initialize WINGate
                MessageBox(hwnd, "Unable to initialize WINGate", "ERROR",
                                   MB_OK | MB_ICONHAND);
                return -1;
            }                      // couldn't initialize WINGate
            bRegistered = TRUE;
            hwndList = CreateWindow("listbox", "",
                        WS_CHILD | WS_VISIBLE | WS_VSCROLL | LBS_NOTIFY,
                        0, 0, 0, 0, hwnd, (HMENU) 100, hInst, NULL);
            if (!hwndList)
                return -1;
            for (i = 0; i < arraysize(country); ++i)
                ListBox_AddString(hwndList, country[i]);
            break;
        }                                // WM_CREATE end
    case WM_SIZE:
        {   RECT rc;                     // WM_SIZE
            GetClientRect(hwnd, &rc);
            if (hwndList)
                MoveWindow(hwndList, rc.left, rc.top, rc.right-rc.left,
                    rc.bottom-rc.top, TRUE);
            break;
        }                                // WM_SIZE end
    case WM_COMMAND:
        switch (LOWORD(wParam))          // select on control id
        { case 100:                      // the list box

            switch (HIWORD(lParam))      // select on notification code
            {
            case LBN_SELCHANGE:          // LBN_SELCHANGE
                {   char msgbuf[80];     // message assembly buffer
                    int i;               // selection (country) index
                    char capital[80];    // response from server
                    WGTRANS trans;       // query transaction
                    int code;            // error code
                    long tid;            // query transaction id
                    i = ListBox_GetCurSel(hwndList);
                    trans = WGCreateTrans("client", 80);
                    WGSetTransString(trans, country[i]);
                    WGSetTransValue(trans, 1, 1);
                    tid = WGExecute(trans, "server", 1000,
                        WG_STAT_WAIT_RESPONSE, &code);
                    WGGetResponse(trans, tid, WG_STAT_WAIT_RESPONSE);
                    WGGetTransString(trans, capital, sizeof(capital));
                    WGDestroyTrans(trans);
                    wsprintf(msgbuf, "The capital of %s is %s",
                        (LPSTR) country[i], (LPSTR) capital);
                    MessageBox(hwnd, msgbuf, "Geographical Information",
                        MB_OK | MB_ICONINFORMATION);
                    break;
                }                        // LBN_SELCHANGE end
            }                            // select on notification code end
        }                                // select on control id end
        break;
    case WM_DESTROY:
        if (bRegistered)
        {   int code;                    // close out WINGate connection
            WGTRANS trans = WGCreateTrans("client", 0);
            WGSetTransValue(trans, 1, 0);
            WGExecute(trans, "server", 1000, WG_STAT_NO_RESPONSE, &code);
            WGDestroyTrans(trans);
            WGUnregisterClient("client");
        }                                // close out WINGate connection end
        PostQuitMessage(0);
        break;
    default:
        return DefWindowProc(hwnd, msg, wParam, lParam);
    }                                    // process message end
    return 0;
}                                        // MainWndProc end


[LISTING TWO]



/*********************************************************************/
/*    SERVER.C -- DOS-based database server for WINGate demo.        */
/*    Written by Walter Oney                                         */
/*********************************************************************/

#include <stdio.h>
#include <stdlib.h>
#include <wingate.h>

static void error(int code);


#define arraysize(p) (sizeof(p)/sizeof((p)[0]))
/*********************************************************************/
void main()                          // main
{   short code;                      // error code
    if ((code = WGInit(0, 16384)))
        error(code);
    if ((code = WGRegisterServer("server", 1024, 0, 0)))
    {                                // can't register server
        WGTerm();
        error(code);
    }
    while (1)                        // until told to quit
    {   WGTRANS trans = WGGetTrans();
        if (trans)                   // process transaction
        {   char country[80];        // name of country being queried
            int i;                   // loop index
            long tid;                // transaction id
            WGTRANS response;        // response to query
            static char *key[] =
            {
                "Afghanistan", "Algeria", "Angola", "Argentina",
                "Australia", "Austria"
            };
            static char *value[] =
            {
                "Kabul", "Algiers", "Luanda", "Buenos Aires",
                "Canberra", "Vienna"
            };
            tid = WGGetTransID(trans, &code);
            if (WGGetTransValue(trans, 1, &code) == 0)
                break;                  // error or "quit" request
            WGGetTransString(trans, country, sizeof(country));
            for (i = 0; i < arraysize(country); ++i)
                if (strcmp(country, key[i]) == 0)
                    break;              // found it
            response = WGCreateTrans("server", strlen(value[i])+1);
            WGSetTransString(response, value[i]);
            WGPostResponse(response, tid);
            WGDestroyTrans(response);
        }                               // process transaction end
        _asm                            // yield time slice
        {   mov   ax, 1680h
            int   2Fh
        }                               // yield time slice end
    }   // until told to quit
    WGUnregisterServer("server");
    WGTerm();
}                                       // main end
/*********************************************************************/
static void error(int code)
{                                       // error
    printf("WINGate error %d\n", code);
    exit(1);
}                                       // error end


Copyright © 1994, Dr. Dobb's Journal