User Interfaces


Porting Command Line User Interfaces to GUIs

William Smith


William Smith is the engineering manager at Montana Software, a software development company specializing in custom applications for MS-DOS and Windows. You may contact him by mail at P.O Box 663, Bozeman, MT 59771-0663.

Not very long ago the computer industry's direction concerning operating systems and user interfaces was uncertain and confusing. You had to be psychic, lucky, or just plain good at reading the writing on the wall to assess the direction the industry was going. To name just a few of the possibilities, there was UNIX and X-Window, OS/2 and Presentation Manager, MS-DOS and a primitive Windows 2.0, and MS-DOS and countless lesser known third party user interface libraries. Right at the height of this confusion, I was faced with choosing an operating system and user interface for a software development project. I was developing a data acquisition and data management system for an automated athletic weight training system. The customer also wanted the program to have a modern graphical user interface (GUI).

For economic reasons and availability of development tools, I decided to write the software in C for a target 80386 personal computer running MS-DOS. The user interface decision was much harder. I, like many people when faced with a tough decision, decided not to decide. I made an engineering decision to do the user interface last. This allowed me to postpone the commitment to a specific user interface library. This was contrary to many opinions about designing software at the time. Instead of designing the software from the user interface and screen level first, I designed the software by identifying core functionality and operations independent of the user interface. To get the project going, I specified that the software was to be first developed using a simple command line interface (CLI). Later, when a graphical user interface library was chosen, I would port the program to work with the GUI library. As soon as a clear direction in the software industry emerged, I would decide upon what GUI to use. Much to my delight and the satisfaction of the customer, this approach eventually paid off.

Well as most of you know, Windows 3.0 came out and quickly became a success. It became obvious what user interface to use. Eventually the project was over and the customer ended up with both a command line version and after a successful port, a Windows version of the software. Along the way I learned a lot about the approach involved in making this port easy. I am going to share with you some of what I learned about porting command line interfaces to GUI's in general and Windows in particular.

The concept of porting I am going to discuss requires work. It is not a simple recompile under a different environment. Granted, there are some features built into Microsoft QuickC for Windows and version 3.0 of Borland C++ that allow you to recompile your code under Windows with no changes. The approach is easy, but all it gets you is a command line within a window. I do not consider this a true GUI. To get true GUI behavior in your program you are going to have to write some code and make some changes. GUIs are here to stay and will become increasingly popular in the future. The effort to port your code to a GUI is worth it. With proper planning, it does not have to be that painful either.

CLI Versus GUI

With the popularity of GUIs, CLIs may seem antiquated, but they have their place. They are easy to write and if you stick to the standard C library, very portable. You can generate a test program for exercising new code quicker using a CLI then a GUI. CLIs are fast and can be easy to use. All that the program requires of the user is to type in the program name and some options at the operating system prompt. The command line interface is elegant in its simplicity and appreciated by the skilled software user. CLIs are also very adaptable to batch mode processing. Unfortunately, CLIs are cryptic and require the user to have knowledge and memory of the command line syntax. By providing a help screen defining proper usage, you can relieve this challenge somewhat for the inexperienced user. An easy to use CLI program should provide a provision to invoke this help screen when an h or ? option is passed on the command line. The program should also display the help information when there is an error in the command line syntax.

GUIs are visually appealing and easier to negotiate then CLIs especially for the unskilled user. GUIs can require more steps to accomplish the same task then a CLI and are not as conducive to batch mode processing as CLIs. With some effort, you can add key stroke short cuts and batch ability to GUIs to satisfy the demands of the advanced user.

Table 1 contains a list of user interface characteristics and features. It rates CLIs and GUIs for comparison.

CLI Translated to GUI

Programs in the simplest terms require input, perform some task and generate output. The input information is in the form of instructions and data. The output information consists of results and data. With a command line interface your choices of how to communicate instructions to a program are limited. Typically, CLIs use single characters, sometimes preceded by a delimiting character such as /, to specify options, commands, or flags. The user passes data to CLIs in the form of file names or lists of strings. On the other hand there are many more options available on how to communicate information to a program that employs a GUI. Table 2, lists the basic command line interface elements and corresponding GUI elements. The nomenclature is based on Windows. Notice there is not a one-to-one correspondence between a command line element and a GUI element. With a GUI, there can be many different ways to accomplish the same task. This gives the developer some flexibility in designing the interface.

Listing 1 contains a code fragment from the main function of a program that processes a command line and performs database operations. This is an excerpt from a data base utility program that I first created as a CLI program and later ported to Windows. The command line is simple. To specify an operation, the program requires a single character as the first argument on the command line. The second and third arguments are a file name and a key name. The operation chosen determines which of these last two arguments are required.

The command line syntax is

PROGRAM OPERATION FILE KEY
The possible operations are shown in Table 3.

Listing 2 contains an excerpt from a Windows program. This code is taken from a program that accomplishes the same tasks as the program that contains Listing 1 . The code is from the windows procedure for the main window that responds to messages for the main window. The code fragment contains a switch statement that reacts to menu choices that are in the form of messages communicated from Windows. There is correspondence between the command line options and the menu choices. The user specifies the file and key through interaction with dialog boxes. Notice that the calls to the functions, add_data_to_db, del_data_from_db, get_data_from_db, replace_data_in_db, vrfy_data_in_db are the same for both the CLI version and the GUI version. The code for these function should be portable across user interfaces.

Table 4 lists the CLI arguments and the corresponding GUI elements used to accomplish the same operations.

GUI Portability Strategy

There are three major guidelines that form the foundation of a strategy for portability between user interfaces:

1. Identify high level functionality and data structures

2. Isolate program functionality from user interface code

3. Use standard library and standard types

Planning for user interface portability requires adopting a design philosophy of first identifying high level program functionality and avoiding the specifics of screen design. The idea is to isolate the major data structures and operations that the program supports. If you can wrap a command line interface around the operations that your program performs, you are on the right track. Granted some programs such as word processors do not lend themselves to command line interfaces. Even in this situation, you can isolate individual functions a word processor performs and group them in a utility program with a command line interface.

Once you define the major functionality, make sure you strongly segregate the code you write to implement this functionality from the user interface code. The modules that contain the core operations of your program should be portable and not make any function calls to a user interface library.

To help with portability, use the standard library functions and the standard types. Some of the issues encountered when porting among GUIs and operating systems are sizes of standard types, structure packing, and alignment. The size of some of the standard types will change from one platform to another. An example is the default int type. Under some compilers an int is 16 bits while with others it is 32 bits. If you do not care what size it is and want to use the native most efficient size, use just a plain int. If you only need 16 bits and want to conserve space in a platform where int is 32 bits use short int. If you need 32 bits even in a platform where int is 16 bits use long int. Avoid typedefing int and encoding the size in the type such as int16 or int32. Some claim that this increases portability, but I have found the exact opposite to be true. I also recommend using the size_t type defined in standard C. size_t is defined as an unsigned integer. It is convenient to use variables of type size_t as array indexes.

Related to type size is structure packing and alignment. In most situations, compilers align structure members (except chars) on boundaries that correspond to the most efficient type size. On a 16-bit system, structure members are aligned on word boundaries. On a 32-bit system, structure members are aligned on double word boundaries. Some compilers allow the program to control structure alignment.

Try to avoid dependencies in your code on type size and structure alignment. The sizeof operator can help with types and the offsetof macro can help with structures. Pay careful attention to third party libraries if you use them. They may have size and structure alignment dependencies that could bite you later.

Buffer sizes and string lengths should be set using manifest constants. For example, the maximum length of a string to hold a file name may change from one system to another. It is far easier to change the definition of a manifest constant in one place than find all places where space for a string is allocated.

Porting to Windows — the Gruesome Details

Since Windows is such a popular GUI, it is worth talking about some of the specific issues encountered when porting existing C code to this environment. As a first step in porting your CLI program to the Windows GUI, you may want to create a user interface shell and spawn the CLI version of your program using the Windows WinExec function call. WinExec is similar to the spawn function family in standard C. This approach will get you up and running, but I found it unacceptable for a finished product. The major drawback is the lack of and the difficulty involved in communicating between MS-DOS and Windows programs.

The next step is to replace the CLI interface and compile your code as a Windows application. Unfortunately, even if you prepared ahead for portability there are some problems that may surface.

Types and Structure Alignment

I have already mentioned data types and structure packing. They are especially important issues under Windows. Windows, in its present incarnation, is a 16-bit environment and the default int type is 16 bits, but Windows requires structures to be aligned on eight-bit (byte) boundaries. If your code expects structures to be aligned on 16-bit (word) boundaries this may affect you. I ran into alignment problems with a third party database library. The situation forced me into hand padding my structures so members greater than a single byte in size were aligned on word boundaries. I inserted eight-bit padding members of type char after an odd number of single byte members.

The Windows programing environment contains many new types defined in the include file, WINDOWS.H. I recommend you use these types, but confine their usage to the user interface portions of your code.

Memory Models

Since Windows runs under MS-DOS and is subject to the caveats of the Intel segmented architecture, you will have to deal with near and far pointer issues and memory models. Since I wanted as much of my code to be as standard C-like as possible, I tried to avoid sprinkling my code with the keywords near and far that are not standard C keywords. The general wisdom on Windows claims that programs compiled using the small or medium memory model behave better under Windows than those compiled with the large or compact memory models. Using the small or medium memory model forces you to declare pointers with the far keyword if they happen to be in far heap space. Even though using the large and compact memory models is discouraged, many of the Windows library functions also require far pointers as parameters. The only way to get far pointers without adding the far keyword to every declaration is to use the large or compact memory model. Windows does not like programs compiled under these memory models because they may contain multiple data segments. Windows fixes multiple data segments in memory. This situation prevents Windows from running more than one instance of such programs and may cause inefficiencies in Windows memory management. This is the case with Microsoft C, but not always with Borland C++. Yes, that is right, the two compilers have a slightly different implementation of the large and compact memory models. Microsoft C creates multiple data segments when using the large or compact memory model and you have little control over the outcome. Borland C++ creates a single data segment unless you specifically tell it to create more. You can run multiple instances of a program compiled under Borland C++ using the large or compact memory model. The exact program compiled with Microsoft C using the large memory model will run as a single instance only.

I have tried the large memory model under Windows and did not notice any performance problems with Windows in standard or enhanced mode. I never even tried real mode. Since Windows 3.1 eliminates real mode, I probably never will use real mode. If you need pointers to far data, your choices are to use the large memory model or to use mixed model programming by declaring pointers with the far keyword.

Dynamic Memory

Windows does support the malloc family of standard C library memory management functions. Unfortunately, they may have slightly different behavior then what you are use to. Depending on the memory model, malloc may allocate memory in the near heap. If you want to force allocation in the far heap independent of memory model, you will have to use the function _fmalloc. Since Windows maps _fmalloc to the Windows function GlobalAlloc, there is a limitation on how many times you can call _fmalloc. Every time you call GlobalAlloc, Windows uses a segment selector. There is a finite number of segment selectors available. This happens to be 8192 for all of Windows - not just your application. If your program requires the allocation of a lot of small pieces of memory, you can quickly run out of selectors even if you still have lots of free memory. The solution to this problem is to call GlobalAlloc sparingly and use subsegment allocation. This means you will have to write your own memory manager or buy one of the third party libraries on the market. Version 3.0 of Borland C++ supports subsegment memory allocation and eliminates this problem. I expect eventually all compilers that support Windows will support this feature.

WINSTUB.EXE

Windows allows a non-Windows program to be bound to your Windows program. You specify the stub program in the linker definition file. MS-DOS executes the stub program when you invoke the program from the MS-DOS prompt. I took advantage of this feature to bind the command line or MS-DOS version of a program to the GUI or Windows version of the same program. The only problem I encountered with this was that Borland C++ enforced a 64KB maximum size limitation on the stub program. Microsoft C allowed the stub program to be any size.

UAEs and New Bugs under Windows

When I first compiled my program under Windows as a Windows application, I was extremely disappointed when it would not run without generating UAEs (Unrecoverable Application Errors). Upon tracking down the offending lines of code, a pattern started to emerge. The majority of the UAEs where caused by dereferencing null pointers. This was occurring in the code I had written and also in the standard library code that I was passing null pointers to as parameters. Since the program worked fine under MS-DOS, there was some argument among co-workers about whether these were actual bugs. One of my partners claimed that functions such as strcmp should be able to handle a null pointer parameter. Since I could not find any reference that specified how some of the standard library functions responded to null pointers as parameters, I decided to be conservative on this issue and actually modified the program's code to avoid passing null pointers to functions where the behavior was not defined and caused UAEs. I recommend that you be careful about dereferencing null pointers and passing null pointers to standard library functions where the behavior is not specifically defined by the standard or defined in the function description that comes with your compiler's documentation.

Conclusions

Ease in portability between user interfaces requires planning. A decision to first develop an application with a command line interface and then port it to Windows made me deal head on with GUI portability problems. What I eventually ended up with is a program where most of the code will port to any GUI without rewriting it.

Planning for portability requires you to identify the needed operations and the high level data elements independent of any user interface issues. You should isolate user interface specific code from the core program code. You should be able to access the functionality of your program through a simple command line interface. This can be handy for testing. For a CLI, the main function should do nothing but process the command line and make the requested function calls. These same function calls can then be called in a similar way when responding to messages or events in a program with a GUI. The GUI program will have to be more than just a simple main function module and may require many new modules that support the GUI functionality and screens. The goal is to have the business end of the code that does the data crunching and calculating remain unchanged when porting from one user interface to another.