Leor Zolman has been involved with microcomputer programming for 15 years. He is the author of BDS C, the first C compiler targeted exclusively for personal computers. Leor's first book, Illustrated C, is now available from R&D Publications, Inc. Leor and his family live in Lawrence, KS.
This is the third in a series of columns describing the implementation of CMENU, a general-purpose menu specification language. The system is composed primarily of two programs: cmenu compiles CMENU source files into an intermediate object format, and rmenu interprets the precompiled files. The first two installments provided an overview of the system and covered the cmenu program in detail. For the rest of the series, I'll be discussing the construction of the rmenu program.
Introducing rmenu
When a CMENU specification file has been successfully compiled by cmenu, the menu is ready to be run (or interpreted, to be more precise) by the rmenu program. Interpretation of intermediate code often carries a connotation of inefficiency. Perhaps this is based on many experiences with early BASIC interpreters that were inefficient when compared with compiled programs. Interpretation, however, can be implemented efficiently when1) the primary objects of manipulation are addressed directly (as opposed to symbolically or associatively), and
2) the primary operations on those objects are I/O bound rather than computation intensive.
Based upon those criteria, I chose to implement the CMENU system as a pseudo-compiler/interpreter program pair. Now, we'll take a closer look at the rmenu interpreter.
rmenu Basic Operation: An Overview
From the user's perspective, rmenu runs a precompiled menu system made up of one or more menu specification files (having the filename extension .mnc). While any single .mnc file may contain more than one menu definition, it is always the first menu in the file named on the rmenu command line that appears on the user's screen at the start of an rmenu session. Other menus of the system, if any, can only be accessed by making the appropriate selections from that top level menu.The command line syntax for invoking rmenu is:
rmenu [menufile]menufile is the name of the .mnc file that is the root node of a compiled menu system (i.e., all other menus of the system may be accessed through that file). If the filename is omitted altogether, rmenu looks for a specification file named menu.mnc in the current directory. If found, it runs that file by default.Once the root specification file has been found, the first menu contained therein is displayed on the screen.
The menu title appears at the top of the screen. The menu items are arranged in a rectangular array. The exact configuration depends upon the number of items present and the options specified in the menu definition. The items are numbered sequentially, and a highlight bar is placed on the first menu item. A prompt line at the bottom of the screen lists the general user commands and the keys to invoke them. At the end of the prompt line, the number associated with the highlighted item (1, for starters) is shown with the cursor sitting to its immediate right.
If the first menu item has any help text associated with it, then the message HELP: appears on the screen below the item array, and the help text associated with that item appears, highlighted and centered, on the following line.
After displaying the initial screen configuration, rmenu waits for the user to make a menu selection. The user does this by moving the highlight bar to the desired item and pressing the ENTER key. There are several ways the highlight bar may be moved:
1) using the arrow keys
2) using the space bar (equivalent to the down-arrow)
3) entering an item number directly through the numeric keys.
The arrow keys and space bar work as might be expected. When they reach the end of a row or column, they wrap around to the opposite side of that row or column.
The direct numeric addressing option tries to be intelligent about the selection value entered. A string of digits is considered part of a single selection value as long as there is an item on the screen labeled by that value. As soon as a key is pressed that would make the cumulative selection value too large, the old selection value is discarded and the new digit alone becomes the new selection value. If even that single digit is greater than the number of items on display, the bell is rung and the key has no effect on the current selection value.
Every time the selection value changes, the highlight bar is moved from the item represented by the old selection value over to the item represented by the new one. To execute a menu item, the user presses the ENTER key when the highlight bar is positioned on the desired choice.
There are four types of menu items:
1) an executable system command
2) a local menu
3) an external menu
4) an exit item.
The last type of menu item, the exit item, need not explicitly appear because there are built-in commands (e and x) for exiting the current menu. It is provided only for supporting additional help text to describe the exit option (for example, a reminder to the user of the parent menu's content).
When an executable action command is run, rmenu's exact behavior is controlled by option specifications on two different levels. On one level, the CMENU specification file may contain item options to explicitly determine:
1) how rmenu handles screen clearing before a menu action is executed
2) whether rmenu prompts for a keystroke after the item completes execution
3) which menu item is the next to be highlighted after all processing for the current item is completed.
On another level, the source code to rmenu itself contains a configuration section that specifies defaults for the behavior of the above mechanisms. These defaults apply whenever any of the options that control those mechanisms are omitted from an item specification (i.e., most of the time if the defaults are chosen appropriately).
When a local or external menu is chosen, the specified submenu is called with the state of the current menu preserved. Upon return from the submenu, processing of the current menu resumes. There is no apparent difference to the user between a call to a local menu (one specified within the same physical .mnc file as the currently active menu) and a call to an external menu (a separate .mnc file).
The general user interface commands available on every menu screen, besides the ones described above to move the highlight bar, are:
e Return to the previous menu, or exit rmenu if currently in the root menu
x Exit rmenu immediately, no matter how deeply nested in submenus
! Escape to a system prompt (this may be optionally disabled)
There are two additional commands available to the user but not documented anywhere on the screen. These commands are intended more for use by the system administrator than the end user. The commands are:
v Display CMENU's version number
a Display the action text associated with the currently highlighted item. If that item is an action, all cd statements that would be executed, and the action text itself, are shown. If the item is an external menu, then its full pathname is shown. If the item is a local menu, then a message announcing that fact is displayed and no additional inoformation is given.
Beyond the User Interface
The above description of rmenu could serve as a user's guide. Now, we'll delve into rmenu's implementation.First, some guideposts to the source code; the rmenu source consists of four major program files (rmenu1.c through rmenu4.c), rmenu's personal header file rcmenu.h and the CMENU system header file cmenu.h (shared with the cmenu program).
The rmenu specific header file, rcmenu.h (Listing 1) defines all configuration options and data structures for the program. The data structures build upon the elementary MENU and ITEM types defined in cmenu.h (see CUJ, 1/92). Both header files rely upon the specification of the target operating system, supplied on the C compiler command line through use of the -D compiler option. This is handled automatically by the makefile.
rmenu1.c contains the main function, several utility functions, and the code to load a compiled menu file into memory for interpretation. Most of the code to handle the user interface, including all the display logic and keyboard command processing, resides in rmenu2.c. rmenu3.c contains routines to execute a selected menu item. rmenu4.c is the file where as much as possible of rmenu's system dependent code has been localized.
Configuration
The first several sections of rcmenu.h contain an abundance of customization options affecting screen layout and menu behavior.While most terminals are laid out in a 24 row by 80 column format, other screen configurations are easily supported by changing the symbolic constant values in lines 69-90.
MAX_IROWS controls the size of the menu item block, telling how many physical screen lines are to be used for menu items alone. The value I use, 18, supports either nine double spaced menu items or eighteen single spaced ones. The value of MAX_IROWS should always be even, so the screen placement algorithms do not become confused by double spacing.
The HOME_X and HOME_Y symbols specify the upper left hand corner of the menu item block. SCREEN_COLS specifies the physical number of columns on the screen. Note that the last column is never used (to avoid wrap-around problems). The last five constants in this section (lines 74-78) specify row and column values for several key features of the menu system, and are self documenting.
Lines 14-38 allow rmenu to work with both new and older variations of the UNIX Curses library. Older versions of Curses did not contain predefined mappings for the cursor motion keys, among other minor differences. To test which flavor of Curses is available, the conditional preprocessor directive in line 14 tests to see if a symbol named KEY_UP has been defined. If so, we're running under a newer Curses library, if not, it's an old one. For old Curses, EMACS movement controls are used to define the cursor motion keys. This is my personal choice; any four key code values may be substituted.
Lines 81-90 define some additional system-dependent terminal characteristics. The number of lines available on the screen is one such characteristic. On a standard DOS screen, there are twenty-five lines available; serial terminals under UNIX typically support only twenty-four lines. The LAST_ROW symbol is set accordingly.
Lines 95-104 define the appearance of various portions of rmenu's general command prompt string. Depending on whether shell escapes are enabled at any particular time during the interpretation of a menu system, the prompt string seen by the user may or may not mention the availability of the shell escape (!) command.
When shell escapes are enabled and the user selects that command, another compile time option comes into play. If the SHELL_PROMPT symbol (defined in line 56) has a value of TRUE, then the prompt string represented by the SH_PROMPT_STR symbol is shown, giving the user the opportunity to abort the she11 escape. This feature is provided for the benefit of users who might not know how to handle an accidental foray into "shell-land" under DOS.
The system command to invoke a subordinate shell under non-DOS systems is defined in lines 106-109. A special system prompt is constructed to remind the user that the menu system is still active. This prompt is then active during the lifespan of the subordinate shell.
Under DOS, the command to invoke a subordinate command interpreter is taken directly from the COMSPEC environment variable during the init_win call at the start of an rmenu session. This code will be covered in a later installment of this series.
Data Structures
To support recursive menu interpretation, rmenu's data structures begin with LMenus, an array of structures of type levels. Each structure of type levels completely describes an active .mnc file. Within it are a number of substructures of type menu2. Each of these substructures describes a single menu definition from the parent .mnc file. Going one level deeper, the contents of each menu2 structure includes a copy of the MENU header structure (named Menu), and an array (named Items) containing pointers to all the ITEM structures in the current menu. Additional information controlling the display of those individual menu items on the screen is included in an array of structures named coords.To shorten definitions and declarations of pointers to these varying structure types, I've created typedef aliases for them in lines 147-149.
The placement information in coords is computed at the time the .mnc file is first loaded into memory and remains constant throughout the period of that menu file's interpretation (i.e., until the user exits from the top level menu in the file). This information includes the item's position on the screen, and the quantity of filler characters (spaces) needed to clear the trailing portion of the item text field when the text length is less than the maximum field width. Figuring all those values for each menu item is a computationally intensive task; it is done just once for each menu invocation at the time the menu is loaded into memory. The reason this is done by rmenu at all, rather than by cmenu at compile time, is to support the ability to use the same compiled .mnc file(s) under different terminal configurations. In other words, the .mnu source to a particular menu system would not require recompilation if some characteristic of the user terminals, such as number of lines, ever changes. Only rmenu would need to be recompiled. Or, if rmenu were ever adapted for network use, then this strategy allows for straightforward extension to support different kinds of user terminals while sharing the same .mnc files among all users; instead of hard-coding the terminal parameters in rcmenu.h, those parameters could be obtained from each user's personal environment variables.
Memory Allocation Strategy
The memory blocks for storage of MENU2 structures are allocated dynamically, as is the storage for each ITEM structure to be represented within them.One possible way to implement memory management for recursive menu files would be to allocate storage for each of an .mnc file's MENU2 and ITEM structures when the file is opened, and to later free all that storage when the user exits the top level menu. If, however, the menu system spans several files, then each transition across compiled units would generate a number of alloc and free calls, proportional to the number of menus and items involved.
Rather than having rmenu fully allocate and release these blocks of memory on every external submenu call, I chose to add a bit of complexity to the allocation scheme in the interests of efficiency. First, note that every MENU2 structure takes up exactly the same amount of storage. The same consistency applies to ITEM structures. Since Menus and Items are arrays of pointers each already dimensioned to the largest possible number of elements, there is no reason why storage allocated for one set of menus cannot be reused for every subsequent set that might require it. Additional memory would only be allocated when the previous maximum number of menus or items in a menu is exceeded, and then only the incremental number of memory blocks would have to be allocated. Also, none of the memory would ever have to be freed until the rmenu program terminated. At that point, all the memory could be freed at once.
That is the strategy employed in rmenu. In each nesting level, represented by a LEVELS structure in the LMenus array, an integer counter named max_menus keeps track of the largest number of menus in any single .mnc file loaded into that nesting (recursion) level during the current rmenu session.
Then, within each of the MENU2 structures of each nesting level, a counter named most_items tracks the most ITEM structures loaded into the Items array during the entire rmenu session.
In a later section, we'll look at how the ld_menu function maintains these counters during the process of loading a menu file into memory.
Other Shared Data
Most menu information is maintained in the large data structures described above. To support seamless recursion, a few items of data are made available globally for convenience.The nestlev variable indicates the current nesting, or recursion, level of the menu system. Level 0 indicates the master .mnc file is active, in LMenus[0]; the first execution of the emenu action bumps the nesting level to 1 and loads the specified external .mnc file into L-Menus[1], and so on. Maintaining this value as a global eliminates the need to keep passing it "up and down" through the recursive sub_menu calls.
The SysShell string (line 157) holds name of the system command to be invoked when the user requests a shell escape with the ! command. At the start of an rmenu session, the init_win function initializes SysShell with the value of the COMSPEC environment variable if running under DOS, or else the value of the SHELL_ESC string as defined in rcmenu.h.
The other global data includes two state variables used with the Curses functions to save the screen coordinates of the user input echo area, and a debugging flag that I used during development (lines 154-155).
A Menu Is Run
When rmenu is invoked to interpret a menu system, there are three major subsystems, or categories of functionality:1) screen display management (via the Curses library)
2) menu nesting management (memory allocation, data management and path construction, all supporting recursive submenus)
3) user input processing (responding to keyboard commands).
These subsystems are distributed so that not every function in the program necessarily belongs to one specific subsystem, although many functions can be classified in one or another of the categories. To explain how rmenu works, I'll focus first on each conceptual subsystem individually then show how they all interact.
Screen Management
One very powerful feature of the Curses library is delayed screen refresh. This means that updates made to an internal screen image do not show up on the user's terminal automatically. One of several special functions (e.g., refresh) must be called to update the screen to match the internal memory image.There are many benefits to this scheme. Curses can optimize the refresh process to require as few I/O operations as possible to update the screen image. For example, a program deletes some portion of text from the memory image, then later restores it to the original form before ever calling the refresh function. If the refresh were then immediately performed, nothing would have to be written to that portion of the screen at all. A side effect of this mechanism is that there is no need to worry about the order in which a set of related screen updates is performed. During the refresh, a set of chaotic updates becomes a smoothly executed repaint.
There is one aspect of delayed refresh that takes some getting used to: you need to remember to call the refresh function. As I began to debug rmenu (my first project using Curses), I kept trying to figure out why some of my screen updates weren't happening. They were done with Curses calls, but I had forgotten to refresh the screen...
Curses requires initialization at the beginning of a session and some housecleaning at the session's end. If the screen is ever to be manipulated by software other than the Curses library while a Curses session is in progress (for example, when a user requests a system shell escape during an rmenu session), special calls must then be made to switch the user's keyboard and screen characteristics between Curses mode and normal interactive mode, as necessary. For example, the Curses library uses "raw" tty mode under UNIX, while normal shell interaction requires the tty mode be set to "cooked."
Unfortunately, I have not been able to find any single, universally portable set of system calls that switch back and forth between these terminal modes correctly under both XENIX Curses and PC Curses for DOS. The closest I've come is to write a pair of functions, named tty_shell and tty_Curses, employing one conditional compilation directive in the tty_shell function. The functions work as written for both DOS and XENIX, but I haven't tried them elsewhere. XENIX's Curses is purported to be nonstandard. My guess is that a little bit of tweaking will be necessary for other kinds of UNIX systems. Such nonportable tweaking, however, should not be required outside of these two functions, since all the other Curses calls I use are supposed to be portable.
Menu Memory Management
The trickiest parts of maintaining a recursive menu structure have been described above. Look at some key points in the menu perusal code to get a sharper picture of how recursion is implemented.In rmenu1.c (Listing 2) , the top level menu processing function do_menu begins on line 73. The main function calls do_menu just once per rmenu session, to process the menu file named on the command line (or menu.mnc if a menu name was omitted). do_menu is also used to process emenu (external menu) action statements.
do_menu takes two strings as function parameters, representing the path and filename of the .mnc file to be run. Lines 85-92 construct the full pathname and attempt to load the file into memory with the ld_menu function. If ld_menu experiences an error in opening the file, then do_menu returns the EXITALL value to its calling function, signalling a fatal error condition and forcing the program to unravel all recursive calls and terminate.
If ld_menu loads the .mnc file successfully, then the sub_menu function (top of rmenu2.c, Listing 3) is called to run the main menu of the new file. sub_menu calls always run a menu at the current menu file nesting level (i.e., within the active .mnc file). Since all menus in a single .mnc file are loaded at once when the file is opened, calls to sub_menu never require any memory management services (except, of course, for the normal stack operations always generated automatically by the C compiler as part of the standard function calling sequence).
sub_menu is the function that interprets user keyboard commands. sub_menu takes two parameters: the index number of a local menu to be executed, and the current default path specification. When the user selects an item for execution, a call is placed to the do_item function for execution of the item's action clause.
So as not to displace all other material in this issue of CUJ, the listings for the action-processing functions (including do_item as mentioned above) are being deferred to the next installment of this series. However, I'll need to refer to some of the functions in that code in order to complete my discussion on recursion. Please bear with me; if you're really anxious to see the remainder of the code right away, however, it is available right now on the CUJ 10.3 code disk.
In do_item, we finally see recursion in action. The first case occurs when an lmenu clause is encountered. In line 36 of rmenu3.c, do_item places a direct call back to the sub_menu function to run the local menu that was specified in the lmenu clause.
The next case of recursion involves the emenu action clause. In line 41 of the same listing, a special function named do_menu (line 102) is called to set up the external menu call, and do_emenu in turn calls do_menu the function that started it all.
I've skipped the nitty-gritty details of these key functions so far, to highlight the design's recursive flow. These functions will be more fully illustrated in a later installment.