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 fifth (and final!) installment of my series on CMENU, a small, portable pseudo-compiled language for implementation of full-screen menus. This month we'll examine how rmenu, the runtime menu interpreter, executes actions selected by the user. We'll also see how the machine-dependent portions of the rmenu program are isolated from the more generally portable code modules.
Running an Item
The beginning of rmenu3.c (Listing 1) is where an action item selected by the user begins execution. The do_item function takes parameters identifying the item to be executed and the current default path, and constructs the new incremental path from that information. Then, depending upon the type of action required, it dispatches control to one of several secondary functions: do_cmnd to execute a system command action, do_emenu to run an external menu, or sub_menu (covered earlier) to run a local menu.do_item supplies do_cmnd with pointers to an ITEM structure and the new incremental path. The system command that executes the specified action is constructed by the call to make_cmd in line 101.
Constructing the System Command
make_cmd contains system dependent code, so it has been placed in rmenu4.c (Listing 2, lines 252-285).make_cmd takes two string parameters, path and action. Under DOS, any forward slash characters (/) in the path string are converted into backslash (\) characters, for compatibility with the standard DOS command processor. A static character array named cmd_line will contain the constructed command string.
If the supplied path is non-null, then a cd command with the given path as the argument is written to the beginning of cmd_line. If running under DOS, a drive selection command is appended (lines 272-278) to make sure the drive specified in the path string is selected as the current drive. This step is unnecessary under UNIX, since UNIX does not maintain "current drives" the way DOS does. After selecting the new current path, the last thing appended onto cmd_line is the action text itself (line 279).
When the supplied path is null, then the action text is directly copied to cmd_line with no further processing (line 282.)
Back at do_cmnd (Listing 1, line 103), it is time to prepare for execution of the new command string. First the pre_clear flag is tested to see if the screen needs to be cleared before running the command. There are two conditions under which the screen gets cleared. Either pre_clear has been explicitly set by a preclear option in the item specification, or there were no explicit instructions given about clearing the screen and the default action is to clear it (if DEF_PRECLEAR is YES).
The screen cursor is then moved to the home position and the screen is refreshed. Before executing the system command, the pre_shell function is called to prepare for the excursion. pre_shell (lines 184-195) calls push_path to save the current path (DOS only) and calls tty_shell to set the terminal to normal interactive mode (all environments).
Finally, in line 114, the system0 function is called to execute the constructed command string.
Running the System Command
system0 (Listing 2, line 288) is another system dependent function. First, note there are two complete versions of this function. The UNIX/XENIX version is so trivial that I wrote it out separately to keep from cluttering the DOS version with conditional directives. All shells under UNIX/XENIX (sh, csh, etc.,) already support multiple commands and preservation of the parent process' current directory. So, all system0 needs to do is pass the command string directly to a new command processor instance via the system function.Under DOS, these mechanisms must be simulated. The current path has already been saved by pre_shell's call to push_path. All that remains is to detect the presence of multiple commands, and to feed each individual command in sequence to DOS's command processor.
The way I handled multiple commands is cheap and effective. First, the original command string is replicated (so as not to butcher the original) and the pointer cp is set to point to the beginning of the copy. In the main loop, any leading semicolons are first skipped (lines 320-321) and the pointer cmdp is set to the start of the next command. Then cp is advanced to the end of that command by scanning for one of two possible terminating characters: either a semicolon or a null. When one of these is found, its value is saved in lastc and the original character is forced to a null ('\0'). Now cmdp points to a single null-terminated system command, and in line 327 that command is finally passed to the DOS command processor.
Upon return from the command processor, the original terminating character is restored and the loop continues on to process the remaining commands (if any).
The value returned by system0 is the return value of the last system call. This value cannot be trusted under DOS, however, since many DOS programs do not return any meaningful value for the command processor to pass back.
Running an External Menu
When do_item encounters an external menu, it calls upon the do_emenu function (Listing 1, line 159) to process that external menu. do_emenu checks if we're already at the maximum level of external menu nesting; if so, it aborts with a fatal error.If not already at the maximum level of external menu nesting, a make_path call combines the current default path and the action text into the complete pathname for the new external menu. That pathname is stored in filename (line 174). The global nesting level variable nestlev is then incremented, and do_menu is called, recursively, to begin processing the external menu.
After the do_menu call, do_emenu decrements the nesting level and passes back the value that was returned by do_menu.
More System-Dependent Functions
rmenu4.c (Listing 2) contains most of the rmenu code that is system dependent. The file is organized into three sections: Curses functions, path management functions, and the system0 function.
The Path Stack
Two path management functions, push_path and pop_path (lines 154-191), maintain a path stack when running under DOS.Before doing a shell escape or executing any action command, rmenu always calls the push_path function.Under UNIX based systems, this function is a no-op, because there is no way for any child process to alter the parent's current directory.
Under DOS there is no such security. While shelled out to a command processor like COMMAND.COM, a user may select a different disk drive or path. Upon return from the shell session that drive and path will remain selected, potentially confusing the parent program's idea of where the current directory is. To guard against that possibility, the push_path function saves the current drive and path in an internal path stack before any system call that might potentially change them. The pop_path function restores the drive and path saved in the last call to push_path.
Each time push_path is called, the currently logged drive and current working directory are pushed on the stack represented by the static structure array named path_stack, and stack pointer path_stackp. The current drive and directory are obtained via calls to the DOS standard library functions getdisk and getcwd.
pop_path pops the last pushed drive and directory off the path stack, and calls the DOS standard library functions setdisk and chdir to restore the current drive and path.
Path Construction
The make_path function (lines 215-249) glues two pathname specifications together to form a single pathname. make_path takes two parameters: old_path represents the current default path, and incr_path is the new incremental path to be merged with the current path. (Under DOS, both strings are run through trans_slash to convert forward slashes into backslashes.) The method of merging depends on the nature of incr_path: if incr_path is an absolute path (beginning with a slash character or, under DOS, with either a slash or a disk designator), then make_path returns incr_path as the final new path specification.If incr_path begins with anything other than a slash or drive designator, then it is treated as a relative path to be appended onto old_path to yield the new incremental path. This happens in lines 237-247.
First, old_path is copied into newpath (the buffer to hold the result). If both the relative path incr_path and the old path (now stored in newpath) are non-null, a path delimiter character is appended onto newpath. Finally, the new relative path is appended onto newpath and the incremental path is complete.
Setting Curses Modes
The first two Curses functions, init_win and close_win (lines 39-80), are called at the beginning and end of an rmenu session, respectively, to initialize and close down Curses support. Under DOS, init_win also reads the name of the active command processor from the system environment.The next two Curses functions set the terminal mode for either Curses or non-Curses operation. tty_shell sets the mode for interactive, non-Curses use by calling the Curses library function reset_tty. tty_curses sets the mode for use with Curses by calling several Curses library functions to set things up as necessary. Since I've only been able to test this program under XENIX and DOS, I suspect that tty_shell and tty_curses will be the first functions to require some modifications when porting rmenu to other systems.
The Mystery of the Missing Memory
My brother-in-law from Boston, his wife and their five kids visited with us in Lawrence last Christmas. Knowing the kids enjoyed computer games, I created a CMENU menu to organize all the best game programs on my system for the kids.A few of those game programs, like Willy Beamish and Chessmaster 3000, wouldn't run when invoked from CMENU. Evidently, CMENU took up too much base RAM space when resident. Not having diddled very much with C's memory allocation faclilities under DOS, my first approach to this problem was to try to reduce rmenu's memory requirements by tweaking some of the parameters in cmenu.h. I decreased the values of MAX_MENUS, MAX_NEXT, and MAX_ITEMS, and ran a make. To gauge the effect, I shelled out to DOS (using the ! command) while running under both the old and new versions of rmenu, and ran DOS's mem command to check free memory. No difference! While shelled out to DOS, I got 530KB of free memory under both the old and new configurations. Hmmmmm... reducing MAX_NEXT alone should have had some impact. Clearly, there was something going on that I didn't understand.
Next, I analyzed my memory usage. With nothing except my startup TSR's resident, running mem told me I have 627KB of free RAM (that's with QEMM386 and all its optional gizmos fully deployed on my VGA-based system.) The size of the RMENU.EXE executable module is about 33KB. If I was only getting 530KB free when running rmenu, then rmenu's code plus its data were taking up a total of 97KB, and the data portion of that was therefore (97KB-33KB) = 64KB. Interesting number! Hunting through the Borland manuals, I located a reference to an external variable named _heaplen. This variable controls the allocation of the near heap, the default memory pool for small- and medium-model programs. If the initialized value of _heaplen is zero, then the system automatically allocates 64KB of memory for the near heap! I had never heard of _heaplen before, so of course my program never initialized it.
I added the code in lines 18-20 and 29-31 of rmenu4.c to support the initialization of _heaplen to some arbitrary value. If HEAPSIZ is left undefined (as in the listing, where the definition line is commented out), then 64KB of heap space will be allocated. If HEAPSIZ is defined, either by removing the opening /* sequence from line 19 or by including an option such as
--DHEAPSIZ=20000on the compiler command line, then the value specified for HEAPSIZ will be plugged into the _heaplen initialization in line 30.For memory-critical applications, the best value can be found by trial-and-error. I discovered that my memory-hogging games have just enough room to run when _heaplen is initialized to 20000. Sure enough, when shelled out from rmenu, the mem command now reports 574KB free.
Getting This Code
This concludes my series of columns on CMENU. If you would like to obtain the complete source code for the program in machine readable form, you may order the CUJ code disk for any of the issues in which a segment of this series has appeared. Each such code disk contains the combined CMENU source code from all installments.Another way to get the code is to download the appropriate code disk file from the UUNET CUJ code disk archives.Elsewhere in this issue of CUJ is a box explaining how to obtain CUJ code from UUNET using standard uucp software.
Exercises
1) The symbolic constants having names beginning with DEF_ control the default behavior of CMENU language options that are not explicitly controlled in an item definition. Make these default options, presently hard-wired at compile time, into CMENU language options that may be specified as part of a menu source file's options section (that is, at the same level as the title and spacing options).2) A bit tougher: extend the scope of the default specifications created in 1) above, as well as the escape/noescape permission status, to any and all submenus that may be invoked. Hint: you'll need to add parameters specifying the current state of all the affected options to the sub_menu function's parameter list.
3) Extend the rmenu memory-allocation strategy to include COORDS structure information as part of the dynamic ITEM allocation scheme (i.e., make it so that storage for the COORDS data is only allocated when necessary). After doing that, calculate how much memory is saved, and contrast the value of saving that memory with the price of the added complexity necessary to support the savings. Was it worth it? The answer probably will depend on how neatly the mechanism has been incorporated into the existing framework.
4) Remove the hardwired aspect of the screen characteristic definitions, so that rmenu does not have to be recompiled to support different terminal sizes. There are at least two ways to accomplish this: a) extend the CMENU language to allow screen dimensions to be written into menu specification files (this approach will most likely require major changes to all portions of the system); b) allow some or all of the screen characteristics to be specified at rmenu run time. This may be implemented in terms of individual command line options to rmenu, or perhaps just one option that gives the name of a special ASCII initialization file that rmenu can read at the beginning of a session to determine what screen characteristics to use.
5) Under DOS, any semicolon characters found in action strings are treated as delimiters between multiple commands. Thus, the semicolon character is precluded from being part of any actual system command. Extend the system0 function to support a method for "escaping" semicolons.