Next Chapter Return to Table of Contents Previous Chapter

CHAPTER 6: STRUCTURES

Structures are useful for grouping a collection of objects into a single aggregate object. Structures can even be subdivided down to the level of machine bits, using bit-fields. Unions are syntactically very similar to structures, allowing the same memory to be used in different ways.

In this chapter, we will examine various uses for structures and unions, as well as relevant reliability rules. The chapter concludes with a complex data structure known as a "menu," which provides a convenient generalized user interface.

6.1 Records

Topics assumed from previous book: structures [8.1], members [8.2].

One of the oldest uses of structures is the record, a collection of information about a single entity. For example, a structure can be used to collect information about a specific part in an assembly:

struct part

{

char part_no[14];   /* part number: string */

short lead_time;    /* lead time (in weeks): {0:99} */

char unit_meas[6];  /* unit of measure: string {"each", "lb", "box"} */

float unit_cost;    /* cost for one unit_meas: {0.00:9999.99} */

short cost_qty;     /* quantity required for price: {0:9999} */

};

In a typical environment where short takes two bytes and float takes four bytes, the memory layout of a struct part might look like this:

Each data type has its own alignment requirement, a requirement (imposed by the CPU hardware and/or the compiler) that the address of this type of data must be evenly divisible by some number. In the example above, we have assumed that each alignment requirement is no more restrictive than "even address" (divisible by two). In such implementations, a struct part occupies 28 bytes.

Suppose, however, that we are compiling for a machine on which float data is required to have an divisible-by-four address. Such an alignment requirement would force the compiler to leave a hole in the structure, an unused portion of the storage which exists solely to preserve alignment. Furthermore, a structure has its own alignment requirement, which is the most restrictive of its members' alignment requirements. Thus, in this implementation, a struct part would be required to have a divisible-by-four address. Another hole would be required at the end, to cause the size of a struct part to be evenly divisible by four. (If a structure's size were not an even multiple of its alignment requirement, an array of structures would come out mis-aligned.) In this more restrictive implementation, the memory layout would look like this:

And the size of a struct part would be 32 bytes, in this more restrictive implementation.

Rule 6-1: In portable programming, do not hard-code the numeric values of structure offsets. The values may be different in each environment. Refer to members by their symbolic member names only.

The advantage of a structure over an array is that the members can have different data types. In this example, the information recorded for each part consists of two char arrays for strings, two short integers, and one float.

The declaration given above does not reserve any storage for any variable; it merely describes a "shape" or template for a part record. The template is identified by a tag name (part, in this example). We can declare one or more variables to have this template by using the type struct part in a declaration:

struct part part1;

This creates storage for the variable part1. Thus, the entire structure part1 and also its individual members (part1.part_no, part1.lead_time, partl. unit_meas, ...) are all lvalues.

6.2 Structures for Information-Hiding

One of the important uses for C structures is to encapsulate interface information. Consider, for example, the FILE structure provided by the standard I/O library. On many systems, the header stdio.h defines the name FILE something like this:

#define FILE  struct_file

struct_file

{

char  *_cur_ptr;  /* where to get/put the next character: !NULL */

int   _size;      /* how big is the buffer: {0:INT_MAX} */

char  *_buffer;   /* where is the buffer: char[_size] | NULL */

int   _rcount;    /* how many more gets left: {0:_size} */

int   _wcount;    /* how many more puts left: {0:_size} */

char  _status;    /* file state: bits */

char  _fd;        /* file descriptor: {0:_NFILES-1} */

};

Thus the name FILE becomes a synonym for the type struct_file. The tag name (_file) and each of the member names (_cur_ptr, _size, etc.) are given names that start with a leading underscore. There is an associated reliability rule:

Rule 6-2: Names with leading underscore should only appear in code that is privy to the internal details of the associated data structure, not in "user-level" portable code.

In particular, you can see that if a supposedly portable program were to use one of these "private names," the program would probably not even compile correctly on another system, since the actual names themselves will very likely be different. If the user uses only the name FILE, the program will portably conform to each target environment.

In C, the "information-hiding" of private names is purely advisory, enforced by style conventions rather than compilers. The advantage for simplicity in programming is that the header provided with a functional package (such as the stdio functions) can easily define macros that allow efficient use of the data structure, just by using the private member names needed.

The main advantage of this defined-type approach is that the internal details of the representation are somewhat more "hidden" from the functions that use the type. This allows the internal details to be tailored to each particular target system, or to be changed to incorporate more efficient algorithms when needed. Furthermore, those functions that do not examine the internal representation of the object need no source-code changes if the object is changed from a structure to a scalar (or to a union - see Section 6.4).

Creating defined-types for structures can also be done with typedef. For example, the FILE type could be declared like this:

typedef struct_file

{

char  *_cur_ptr;  /* where to get/put the next character */

int   _size;      /* how big is the buffer: {0:INT_MAX} */

char  *_buffer;   /* where is the buffer: char[_size] | NULL */

int   _rcount;    /* how many more gets left: {0:_size} */

int  _wcount;     /* how many more puts left: {0:_size} */

char  _status;    /* file state: bits */

char  _fd;        /* file descriptor: {0:_NFILES-1} */

} FILE;

The typedef approach has the minor advantage of preventing any subsequent re-definition of the name, but either approach will serve adequately.

Using either approach, we will assume that for each generally-useable structure, both the defined (upper-case) name and the tag (lower-case) name will be available. Here is another useful convention: Structures used in several different source files should be declared in a header to be included wherever needed. The "part-record" structure might, for example, be declared like this:

part.h(#1):

/* part.h - header for PART structure */

#ifndef PART_H

#define PART_H

typedef struct part

{

char part_no[14];   /* part number: string */

short lead_time;    /* lead time (in weeks): {0:99} */

char unit_meas[6];  /* unit of measure: string {"each", "lb", "box"} */

float unit_cost;    /* cost for one unit_meas: {0.00:9999.99} */

short cost_qty;     /* quantity required for price: {0:9999} */

} PART;

#endif

Rule 6-3: Use the "leading underscore" name format for tag and member names if the internal details of the structure are not to be inspected by functions outside of the package. Conversely, avoid leading underscores if the details of the structure are available for inspection by functions that use the structure.

6.3 Properties of Structures

Recall from Section 2.9 that an object is well-defined if it conforms to the programmer's stated defining property. Since structures are an aggregate of individual members, it will be useful to provide a default meaning for "well-defined" as applied to structures. In the absence of a specific defining property, a structure will be said to be well-defined if all its scalar constituents are well-defined. Referring back to the definition of the PART structure, in order for a PART to be well-defined, all of the following must be true:

part_no    must be a (nul-terminated) string

lead_time  must be within the range {0:99}

unit_meas  must contain the string "each", "lb", or "box"

unit_cost  must be within the range {0.00:9999.99}

cost_qty   must be within the range {0:9999}

Why is it important to be clear about whether a structure is well-defined? We desire that we be able to talk about the behavior of a program in simple terms without a lot of qualifying phrases. For example, in talking about a particular program, we want to be able to say "part1 is a well-defined PART structure" rather than having to verbalize a whole list of qualifications such as "the part_no member is a nul-terminated string, lead_time is within its allowable range, ... ."

One of the common abbreviations allowed by C is the initialization of a structure to "all zeros":

PART partl = {0};

This convention of zero-initialization is sufficiently common that it deserves a reliability rule:

Rule 6-4: If a structure is not well-defined when initialized to zero, document this fact in a comment. (The program will in general be simpler if the members are defined such that the zero-initialized structure is well-defined.)

Examine for a moment the declaration of a PART structure. Most of its members conform to their defining property when initialized to zero, all except for unit_meas, which is required to be one of three specific strings. Since the result of initializing a string to all zeros is a null string, the rule above suggests that we consider re-defining the properties of the unit_meas member like this:

char unit_meas[6];  /* unit of measure; string {" ", "each", "lb", "box"} */

Alternatively, we would have to add a comment to the declaration of PART to the effect that it is not well-defined upon zero-initialization.

Oftentimes a structure has other properties that go beyond "well-definedness" as described above. Regarding the FILE structure, for example, we might wish to document two properties of a FILE structure called "is-open" and "is-closed." Hypothetically, let us say that the cur_ptr member is NULL if the file is closed and non-NULL if the file is open. We might then add these lines to the declaration:

/* cur_ptr != NULL ==> FILE is open */

/* cur_ptr == NULL ==> FILE is closed */

Consideration of the "zero-initialization" rule would suggest one more comment:

/* A zero-initialized FILE is closed */

6.4 Unions

A union is basically a struct in which all the offsets are zero. That is, all the members of a union start at byte zero of the object's storage, and hence occupy the same space.

The most portable use for a union is to overlay two or more data objects in the same space. For example,

struct rec

{

char name[20];

char type;

union

{

TYPE_A_INFO a;

TYPE_B_INFO b;

} info;

};

contains a member named info which contains either "type-a" information or "type-b" information. Presumably, the type member tells which of the alternatives is used in each particular instance. In C, there is no automatic way for the compiler to tell which member of the union is being used; it is entirely the programmer's responsibility to keep track of the state of the union. For portable programming, the state of a union object includes (at least in concept) the name of the member last assigned into. Any use of the value of a union member should be used through the last member assigned into.

Another use for union is alignment-forcing:

union buffer

{

int align;

char buf[BUFSIZ];

};

union buffer dsk_block;

will provide an array of characters (named dsk_block) which is aligned on an int boundary. The program will probably never refer to the align member except in the declaration; the only purpose of the member is to force alignment. The actual array of characters is accessed by the expression dsk_block.buf (the buf member of dsk_block). One of the more common reasons for alignment-forcing is interfacing with specific hardware or operating system interfaces.

6.5 Bit Fields

C provides a storage-compaction capability for structure members, in which each member occupies only a specified number of bits. Such a member is known as a bit-field. Bit-fields can be useful for reducing the storage needed for a large array of structures. They are also useful for defining various hardware interfaces which specify the individual bits within a machine word.

Consider the representation of time-of-day in hours, minutes, seconds and milliseconds. Bit-fields provide one way to represent such times:

time_day.h(#1):

/* time day.h - bit-field structure for hh:mm:ss.fff */

#ifndef TIME_DAY_H

#define TIME_DAY_H

typedef struct time_day

{

unsigned h1 : 2;    /* tens digit of hours      {0:2} */

unsigned h2 : 4;    /* units digit of hours     {0:9} */

unsigned m1 : 3;    /* tens digit of minutes    {0:5} */

unsigned m2 : 4;    /* units digit of minutes   {0:9} */

unsigned s1 : 3;    /* tens digit of seconds    {0:5} */

unsigned s2 : 4;    /* units digit of seconds   {0:9} */

unsigned f1 : 4;    /* first digit of fraction  {0:9} */

unsigned f2 : 4;    /* second digit of fraction {0:9} */

unsigned f3 : 4;    /* third digit of fraction  {0:9} */

} TIME_DAY;     /* 32 bits total */

#endif

The last millisecond of the day is

23:59:59.999 (hh:mm:ss.fff)

Each member (bit-field) is declared to be unsigned (int); this is the only bit-field type that is guaranteed to be portable to all current compilers. Each member is declared to have only as many bits as are necessary to represent the possible digits at its position in the time representation. Representing h1 (first digit of hours) takes only two bits to represent the possible values (0, 1, and 2). And the largest members need only four bits to represent ten digits, 0 through 9. The total number of bits is 32.

Consecutive bit-field members are allocated by the compiler to the same int-sized word, as long as they fit completely. Thus, on a 32-bit machine, a TIME_DAY object will occupy exactly one int-sized word. On a 16-bit machine, the first five members (totalling 16 bits) will fit into one int-sized word, and the last four members will fit into an immediately following word. Such an exact fit is rare, however. Add another member such as "day-of-year" to the structure, and the nice size-fitting property disappears. Thus, bit-fields are useful for storage-saving only if they occupy most or all of the space of an int, and if the storage-saving property is to be reasonably portable, they must occupy most of the space in a 32-bit integer [6-1].

The order of allocation within a word is different in different implementations. Some implementations are "right-to-left": the first member occupies the low-order position of the word. Most PDP-11 and VAX compilers allocate right-to-left. Following the convention that the low-order bit of a word is on the right, the right-to-left allocation would look like this:

Most other implementations are "left-to-right":

This variability of allocation order leads to a portability rule:

Rule 6-5: In portable code, do not depend upon the allocation order of bit-fields within a word.

Of course, in machine-specific non-portable code one knows exactly how the bit-fields are laid out, and the internal details can be inspected with bitwise operations. A union provides a convenient way to say what is going on:

typedef union time_overlay      /* MACHINE DEPENDENT */

{

struct time_day time_as_fields;

long time_as_long;

} TIME_OVERLAY;

TIME_OVERLAY time_port;

This allows bitwise operations like time_port.time_as_long & OxFOO as well as providing access via bit-field names like time_port.time_as_fields.h1.

Specifying a field size of zero causes any subsequent allocations to begin on a new word boundary. Un-named bit-fields are allowed; they occupy space but are inaccessible, which is useful for "padding" within a structure.

Since most C machines do not support bit-addressing, the "address-of" (&) operator is not allowed upon bit-field members.

Aside from these complications, bit-fields can be treated just like any other structure member. The following declaration

#include "time_day.h"

struct time_day last_msec = {2, 3, 5, 9, 5, 9, 9, 9, 9};

initializes last_msec to the last millisecond of the day.

struct time_day now;

/* ... */

if (now.h1 == 0 | | (now.h1 == 1 && now.h2 < 2))

tests whether now is less than "noon."

If we wish to use the TIME_DAY structure for an "information-hiding" purpose, so that it could be changed without affecting the programs that use it, we should employ the "leading-underscore" convention mentioned earlier in Section 6.2:

time_day.h(#2):

/* time_day.h - bit-field structure for hh:mm:ss.fff */

#ifndef TIME_DAY_H

#define TIME_DAY_H

typedef struct_time_day

{

unsigned _h1 : 2;   /* tens digit of hours      {0:2} */

unsigned _h2 : 4;   /* units digit of hours     {0:9} */

unsigned _m1 : 3;   /* tens digit of minutes    {0:5} */

unsigned _m2 : 4;   /* units digit of minutes   {0:9} */

unsigned _s1 : 3;   /* tens digit of seconds    {0:5} */

unsigned _s2 : 4;   /* units digit of seconds   {0:9} */

unsigned _f1 : 4;   /* first digit of fraction  {0:9} */

unsigned _f2 : 4;   /* second digit of fraction {0:9} */

unsigned _f3 : 4;   /* third digit of fraction  {0:9} */

} TIME _DAY;    /* 32 bits total */

#endif

We chose the TIME_DAY example because it illustrates the use of bitfields nicely, but there are numerous other ways to represent time-of-day. One obvious method is simply to count milliseconds since midnight. Adding and subtracting times is easier this way, but printing or displaying times is easier with the bit-field structure. The choice of representation ultimately depends upon the intended uses for the data.

6.6 Structures of Strings

One of the simplest and most useful structures is one containing nothing but (nul-terminated) strings. For ordinary string data (such as names and addresses), this approach requires only that the programmer be careful to allow space for each nul-terminator.

Numeric data, if present, is stored in readable string form. This may necessitate conversion from numeric representation to string representation, which takes a significant amount of CPU time.

Coded data (a choice among a small number of alternatives) can usually be represented by a single char. But in order to preserve the consistent "string" property of all the structure members, an array of two char's is needed, to allow space for the nul terminator.

The advantage is that data stored in this fashion can be processed in a uniform manner; each member of the structure is a nul-terminated string. We will shortly describe a "menu" program which deals with data stored uniformly in strings. Its version of the part structure will look like this:

part.h(#2):

/* part.h - header for parts structure

*/

#ifndef PART_H

#define PART_H

typedef struct part

{

char part_no[14];   /* part number: string */

char lead_time[3];  /* lead time (in weeks): num string {0:99} */

char unit_meas[2];  /* unit of measure: coded string */

/* {"0"=each, "1"=lb, "2"=box, other=each} */

char unit_cost[8];  /* cost: num string {0.00 : 9999.99} */

char cost_qty[5];   /* qty required for price: num string {0:9999} */

} PART;

extern PART part1;

#endif

6.7 Pointers to Structures

Structures and unions may be used as operands for address-of (&), member (.), and sizeof operators. If part1 has the type struct part, then the expression &part1 has the type struct part * (pointer to struct part), and produces the address of part1. The member operator, as in part1.part_no, we have seen before. And sizeof(part1) gives the size (in bytes) of part1.

Recent versions of C also allow assignment, argument passing, and function return for structures and unions. One could, for example, write a function which would accept a structure argument, update the structure, and return it to the calling function, where the returned value could be assigned to a structure variable:

#include "part.h"

main()

{

struct part part1;

struct part chg_part( );

/* ... */

part1 = chg_part(part1);

/* ... */

}

struct part chg_part(arg_part)

struct part arg_part;

/* get new values for arg_part.lead_time, ... */

return (arg_part);

}

However, passing and returning structures can cost considerable CPU time; the entire structure is copied each time. Often it is more efficient to pass a pointer to a structure. The declaration

struct part *part_ptr;

declares that part_ptr points to struct part's. To access the members of the structure that part_ptr points to, we use the "arrow" (->) operator, as in

part_ptr->lead_time

Changing the definition of the function chg_part for use with a pointer to struct part's would produce something like this:

#include "part.h"

main()

{

struct part part1;

void chg_part();

/* ... */

chg_part(&part1);

/* ... */

}

void chg_part(ppart)

struct part *ppart;

{

/* Get new values for ppart -> lead_time, ... */

/* The changes take place directly in the pointed-to structure. */

}

The "dot" and "arrow" member operators can be written in terms of each other:

part_ptr->lead_time  is equivalent to (*part_ptr).lead_time

part1.lead_time      is equivalent to (&part1)->lead_time

The rules for when to use each form are simple: If ps is a pointer to a structure, the members of the pointed-to structure are accessed via the "arrow"; if s is a structure, its members are accessed via the "dot."

Now let us consider the properties of pointers to structures. As discussed in Section 4.2, a pointer value can be either undefined (not valid in any pointer contexts), or defined (either NULL or pointing to a valid object of the proper type). In order to speak simply about programs, if a pointer points to a structure, we will say that the pointer is well-defined if the structure that it points to is well-defined.

Since one of the common uses of pointers to structures is as function parameters, we should consider some rules for their reliable usage. Taking the simplest case first, an "out" pointer (used only to modify the pointed-to object) does not care what properties the object has when its address is passed to the function. It is usually an error to pass NULL to an "out" parameter, since it does not point to any object. (A NULL parameter could be used to mean"do not store anything this time," but an explicit comment should be given.) An "in" pointer (used only to read values from the pointed-to object) should, for reliability, be passed the address of a "well-defined" structure. If, however, the structure is supposed to have some other defined property, the parameter declaration should so indicate in a comment. The same conditions apply to an "in-out" pointer (used both to read values from, and change values in, the pointed-to structure).

Rule 6-6: Regarding parameters which are pointers to structures, an "out" pointer parameter is assumed to be non-NULL, pointing to the storage for a structure of the specified type. "In" and "in-out" pointer parameters are assumed to point to a well-defined structure of the specified type. Any exceptions should be noted in a comment on the parameter declaration.

6.8 Example: Menu Processor

Topics assumed from previous book: array of structures [8.5].

We can now apply these observations about structures to a practical problem, the execution of screen-oriented menus.

In screen-oriented programming, the keyboard is handled in a "raw no-echo" fashion. The typed key does not automatically echo on the screen. The program determines whether a character will be echoed to the screen, or whether it will instead cause some action.

As described in Section 5.5, we can make use of scr_getkey, which returns the user's latest input character with no intermediate buffering and no echoing of the input. The scr_getkey function returns each ordinary character entered, but also returns special coded values for the keys EXIT, UP, DOWN, RIGHT, LEFT, and HOME.

The particular menu processor that we will present is somewhat generalized. We will call each component of a menu a field. In the most conventional usage, the fields of the screen are actions to be performed:

MAIN PART MENU

Add a part record

Change a part record

Delete a part record

In this example, there are three fields, each one a possible action.

The location of the screen cursor indicates the "current field." (On our diagrams, the cursor is indicated by an underscore.) The cursor is moved among the fields by cursor-movement keys. The DOWN key moves to the next field downward; the UP key moves upward; the HOME key moves to the first field. The UP and DOWN movements "wrap around" from first to last, or vice-versa. When the current field is an action field, hitting RETURN causes that action to be invoked, and typing the first letter of a field description (A, C, or D, here) moves the cursor to that field.

Thus, in our sample MAIN PART MENU, if the user were to type C then RETURN, the action field entitled "Change a part record" would be invoked. An action can lead to the presentation of another menu, and a menu can contain data fields as well as, or instead of, action fields:

CHANGE A PART RECORD

Part number: 39849-02_

Lead time (weeks): 12

Unit of measure: each

Unit cost: 17.25

Cost qty: 100

All fields of the CHANGE A PART RECORD menu are data fields. Cursor keys UP, DOWN, and HOME work as before. Hitting RETURN moves the cursor to the next field. The cursor always appears at the righthand end of the data value. Hitting BACKSPACE moves the cursor leftward, erasing the rightmost character of the data value. Hitting the EXIT key returns control to the place where the current menu was invoked, back to the main menu, in this example. Typing an ordinary text character appends that character to the current field, until the maximum field length is reached. (Special handling of a "coded choice" field will be described later in this section.)

Before we turn to the overall design of the menu processor, it will be helpful to show the details involved in updating one data field's value. A string data value is described by a STRING_VAL structure:

typedef struct string_val

{

char edit_type; /* 'n' = numeric, 'a' = anything */

char *string;   /* ptr to the string value */

short maxsize;  /* maximum size of string; {2:SHORT_MAX} */

short *pnum;    /* ptr to short; NULL=store as string */

} STRING_VAL;

The function which updates string values is mu_sval ("menu string value"). It is called when the cursor is sitting just to the right of the displayed contents of the current field's value, like this:

Part number: 39849-02_

...

The associated STRING_VAL structure would look something like this, when mu_sval is called:

mu_sval reads the next input character. If it is a command character, work on this string is complete for now, and mu_sval returns. If it is a backspace, the prior character is erased (if there is one). If the character is invalid for entry in a numeric field, mu_sval calls the scr_beep function to indicate the problem. If there is space to store the new character, it is added to the string and echoed. Otherwise, a scr_beep indicates the out-of-bounds situation.

Before showing the mu_sval function, one refinement needs discussion. The STRING_VAL structure allows for the possibility that the field is actually stored as a short int instead of as a string. All of the editing and user interaction are the same as is provided for numeric strings, using an auxiliary string of the appropriate size. Upon leaving the field, this numeric string is converted into an integer value in *pnun. During the modification of the string, the function preserves a property that we can call "string-sync": the displayed image on the screen agrees with the memory contents of the string. Furthermore, the string contents are valid, according to the specification of the field: the length is within the stated bound, and numeric strings consist only of digits with a possible leading minus sign.

Here, then, is the code for the mu_sval function.

mu_sval:

/* mu_sval - get the string contents for a STRING_VAL field

* Assumes cursor is just to right of string

*/

#include "menu.h"

SCR_CMDCHAR mu_sval (psv)

STRING_VAL *psv;      /* ptr to the STRING_VAL structure : str_sync */

/* str_sync = screen agrees with contents of */

/*            psv->string */

{

char *s;             /* ptr to start of string */

short i;             /* index of current nul-terminator */

SCR_CMDCHAR c;       /* latest SCR_CMDCHAR returned from scr_getkey */

s = psv->string;

i = strlen(s);

FOREVER

{

c = scr_getkey();

if (SCR_CMD(c))             /* if c is a command character, */

return (c);             /* return it */

else if (c == '\b')

{

if (i > 0)

{

scr_print("\b \b"); /* erase previous character */

s[--i] = '\0';      /* shorten the string */

}

}

else if (psv->edit_type == 'n' && !isdigit(c) &&

!(i == 0 && c == '-'))

{

scr_beep( );              /* non-digit invalid */

}

else if (i < psv->maxsize - 1)

{

scr_putc(s[i++] = c);     /* append the char, and echo it */

s[i] = '\0';

}

else

scr_beep( );              /* no room left in string */

}

}

Notice that the parameter, psv, is a pointer to STRING_VALS. The edit_type member of the object pointed to by psv is accessed by writing psv->edit_type. Similarly, we see psv->string and psv->maxsize.

A data field may be a "choice field" instead of a string field. A choice field has a fixed set of possible values. In the "part record" example, the "unit-of-measure" field (unit_meas) might allow the values "each", "lb", and "box", which are coded into the strings "0", "1", and "2".

The user cycles among the possible values by typing LEFT or RIGHT; like the other special keys, these are interpreted by scr_getkey. The structure which controls a choice field is called a CHOICE_VAL:

typedef struct choice_val

{

short nchoices;     /* number of possible choices: {2:10} */

char **choices;     /* ptr to start of array of choice strings */

char *pc;           /* ptr to the one-char coded choice value */

} CHOICE_VAL;

A CHOICE_VAL structure for the unit_meas component of the part record might look like this:

This diagram corresponds to the following initialized declarations:

PART part1 = {0};

char *Tunit_meas[] = ("each", "lb", "box"}; /* table of code meanings */

CHOICE_VAL Cunit_meas =

{DIM(Tunit_meas), Tunit_meas, part1.unit_meas};

Having seen how structures like STRING_VAL and CHOICE_VAL are used to control the access to an individual data value, let us look now at the overall design. Each field is specified by a C structure, FIELD:

typedef struct field

{

short line, col;    /* co-ordinates of screen display position */

char *desc;         /* description (title) of this field: string */

char val_type;      /* what type of field is this? */

STRING_VAL *psv;    /* non-NULL iff val_type == 's' or 'S' */

CHOICE_VAL *pcv;    /* non-NULL iff val_type == 'c' */

struct menu *pmenu; /* non-NULL iff val_type == 'm' */

bool (*pfn)( );     /* function to do after string or choice field */

} FIELD;

A more sophisticated way of declaring val_type would have involved defining an enumeration,

enum val_types {STRING_TYPE, CHOICE_TYPE, MENU_TYPE, FN_TYPE};

and declaring val_type as

enum val_types val_type;

We chose the more primitive method, a simple char, mainly because enum is still not universally supported in existing compilers, and because the enumeration does not add any noticeable clarity.

If a FIELD structure's val_type equals 's' or 'S', then this field is a string field. The function mu_str accepts a pointer to a string FIELD, positions the cursor just past its currently-displayed string value, and calls mu_sval to update the string value. The relevant property ("field-sync") is that the screen display of the field agrees with the field's stored contents.

mu_str:

/* mu_str - get a string for a STRING_VAL field */

#include "menu.h"

SCR_CMDCHAR mu_str(pf)

FIELD *pf;    /* : fld_sync */

{             /* fld sync = field in data rec agrees with screen */

SCR_CMDCHAR ret;

STRING_VAL *psv;

psv = pf->psv;     /*get the string-value pointer for this field */

/* position scr_curs just to right of string value */

scr_curs(pf->line, pf->col + strlen(pf->desc) + 2 + strlen(psv->string));

ret = mu_sval(psv);                     /* get string value */

if (psv->pnum != NULL)                 /* pf => !fld_sync */

*psv->pnum = atoi(psv->string);   /* pf => fld_sync */

return (ret);

}

If a FIELD structure's val_type equals 'c', then this field is a choice field. The function mu_chv ("get choice value") allows the user to change the current value of a choice field:

mu_chv:

/* mu_chv - get a value for a CHOICE_VAL field

* Return the SCR_CMDCHAR that terminated the input of this field.

* fld_sync = field in data rec agrees with screen

*/

#include "menu.h"

SCR_CMDCHAR mu_chv(pf)

FIELD *pf;          /* ptr to current FIELD: fld_sync */

{

short nchoice;      /* current choice value: {0:9} */

CHOICE_VAL *pcv;    /* ptr to this field's CHOICE_VAL */

SCR_CMDCHAR c;      /* most recent user input char */

short prevlen;      /* length of previous choice display string */

short curlen;       /* length of current choice display string */

short i;

prevlen = 0;

pcv = pf->pcv;

if ('0' <= *pcv->pc && *pcv->pc <= '9')

nchoice = *pcv->pc - '0';

else

nchoice = 0;

FOREVER

{

/* scr_curs to start of choice display, display current choice */

scr_curs(pf->line, pf->col + strlen(pf->desc) + 2);

scr_print("%s", pcv->choices[nchoice]);       /* pf => fld_sync */

/* erase any leftover from previous choice */

curlen = strlen(pcv->choices[nchoice]);

for (i = 0; i < prevlen - curlen; ++i)

scr_putc( ' ' );

for (i = 0; i < prevlen - curlen; ++i)

scr_putc('\b');

prevlen = curlen;

/* get user input and process it */

c = scr_getkey();

if (c != SCR_LEFT && c != SCR_RIGHT)

return (c);

else if (c == SCR_RIGHT)

nchoice = IMOD(nchoice + 1, pcv->nchoices);

else /* c == SCR_LEFT */

nchoice = IMOD(nchoice - 1, pcv->nchoices);

*pcv->pc = '0' + nchoice;                     /* pf => !fld_sync */

}

}

Each time the keys LEFT or RIGHT are hit, nchoice is decremented or incremented, modulo pcv->nchoices, the number of choices for this field. (The IMOD macro from Section 2.7 is used to ensure that the new nchoice is positive.)

For both string and choice fields, the user can terminate the entry of the current field by typing the cursor keys UP, DOWN, or HOME. Additionally, the user can leave a choice field by typing a letter which matches the initial letter of a field description. In either case, if a non-NULL pointer to function (the "post-function") is provided for this field, that function is called (with a pointer to the current menu). If the post-function returns NO, the cursor returns to the current field. Thus the post-function could perform arbitrarily complicated validation of the current field.

A menu is specified by a structure, MENU:

typedef struct menu

{

char *title;          /* alphabetic title for menu: string */

FIELD *fields;        /* ptr to beginning of array of FIELDs */

short nfields;        /* how many fields in this menu */

short cur_field;      /* index of the current field */

} MENU;

In English translation, these structures are saying that a menu has a title, a specified number of fields, a current field, and an array of individual fields. Each field has screen co-ordinates for its display, a description, a value-type (string, choice, menu, or function), and four pointers: a pointer to a STRING_VAL structure, to a CHOICE_VAL structure, to another menu, and to a C function. (If the field is a function field, the menu processor simply calls the function that is pointed to. For simplicity of design, the processor ignores any return value from the function.) Here is a schematic picture of the data structures:

There is a function mu_pr for the displaying of menus. It makes use of two environment-dependent functions: scr_clear (to erase the screen) and scr_curs (to move the cursor to a specified (line, col) location). Its job is to create a certain property for the menu, namely that the screen display agrees with the contents of all the field descriptions and field contents ("menu-sync"). It accomplishes this by clearing the screen and then looping over all the fields, moving the cursor to the proper place on the screen for each field, and then displaying the field description and field contents.

mu_pr:

/* mu_pr - display a menu on the screen */

#include "menu.h"

void mu_pr(pm)      /* at return, pm => menu-sync */

MENU *pm;       /* ptr to the MENU to be displayed */

{

short i;        /* index for loop over fields */

FIELD *pf;      /* pointer to each FIELD */

short i_choice; /* numeric value of choice field */

scr_clear( );

/* print the menu title, centered on top line */

scr_curs(0, (scr_cols - strlen(pm->title))/2 - 3);

scr_print("%s", pm->title);

for (i = 0; i < pm->nfields; ++i)         /* for each field */

{

pf = &pm->fields[i];

/* print the field description at proper x,y location */

scr_curs(pf->line, pf->col);

scr_print("%s ", pf->desc);

/* for string and choice fields, print current value */

if (tolower(pf->val_type) == 's')

{

if (pf->psv->pnum != NULL)  /* field has numeric form */

itoa(*pf->psv->pnum, pf->psv->string, pf->psv->maxsize -1);

scr_print("%s", pf->psv->string);

}

else if (pf->val_type == 'c')

{

if ('0' <= *pf->pcv->pc && *pf->pcv->pc <= '9')

i_choice = *pf->pcv->pc - '0';

else

i_choice = 0;

scr_print("%s", pf->pcv->choices[i_choice]);

}

}

scr_refresh( );

}

Now we are ready for the function which processes the user responses to a menu. The mu_ask function allows the user to move around among the fields of a menu, entering or altering the values of data fields, until the user selects a MENU field, selects a function field, or hits EXIT. It is the job of mu_ask to call any associated post-function for each field. If the post-function returns failure (NO), mu_ask prevents the user from moving to another field. (EXIT is allowed, to prevent locking the user into a "no-exit" situation.) mu_ask embodies one special case: a string field with capitalized value-type 's' is "mandatory": mu_ask will not allow movement to another field until a non-null string has been entered in this field.

mu_ask:

/* mu_ask - get responses to menu items

* Keeps looping over string and choice fields;

* terminates when user selects an action field, or hits SCR_EXIT.

*/

#include "menu.h"

FIELD *mu_ask(pm)

MENU *pm;        /* ptr to current MENU */

{

FIELD *pf;       /* ptr to current FIELD */

FIELD *pfj;      /* ptr to j-th FIELD of this MENU */

SCR_CMDCHAR c;   /* most recent user key input */

char vtype;      /* value type of current FIELD */

bool legal;      /* is c valid input in this state? */

short j;         /* index over FlELDs of this MENU */

pm->cur_field = 0;     /* start with first field */

FOREVER

{

pf = &pm->fields [pm->cur_field];

legal = YES;

vtype = pf->val_type;

/* accept user input, get user's SCR_CMDCHAR */

if (vtype == 'c' || tolower(vtype) == 's')

{

if (vtype == 'c')

c = mu_chv(pf);

else            /* vtype == 's' OR 'S' */

c = mu_str(pf);

if (pf->pfn != NULL)

legal = (*pf->pfn)(pm);

if (c == SCR_EXIT)

return (NULL);

}

else

{

scr_curs(pf->line, pf->col);

c = scr_getkey();

}

/* at this point, c is the user's response, and */

/* legal records validity of 'c' or 's' field */

/* (more) */

/* determine next action, based on c */

if (c == SCR_RETURN && (vtype == 'm' || vtype == 'f'))

return (pf);

else if (c == SCR_EXIT)

return (NULL);

else if (!legal)

;       /* no actions allowed if validation failed */

else if (vtype == 'S' && pf->psv->string[0] == '\0')

legal = NO;

else if (!SCR_CMD(c) && tolower(vtype) != 's')

{

legal = NO; /* tentatively */

for (j = 0; j < pm->nfields; ++j)

{

pfj = &pm->fields[j];

if (toupper(pfj->desc[0]) == toupper(c))

{

pm->cur_field = j;

legal = YES;

break;

}

}

}

else if (SCR_CMD(c))

{

switch (c)

{

case SCR_UP:

pm->cur_field = IMOD(pm->cur_field - 1, pm->nfields);

break;

case SCR_DOWN:

case SCR_RETURN:

pm->cur_field = IMOD(pm->cur_field + 1, pm->nfields);

break;

case SCR_HOME:

pm->cur_field = 0;

break;

default:

legal = NO;

break;

}

}

if (!legal)

scr_beep();

} /* end FOREVER */

}

The top-level function of the package is mu_do. In a FOREVER loop, mu_do prints a menu and calls mu_ask. As long as the user remains within string fields and choice fields, mu_ask handles the dialog. If mu_ask returns NULL, the user requested to exit, and mu_do returns. Otherwise, the return specifies either a menu field or a function field. If a menu field, mu_do is called recursively to process the sub-menu; if a function field, the function is called.

mu_do:

#include "menu.h"

/* mu_do - present a menu, process actions until user SCR_EXIT

*/

void mu_do(pm)

MENU *pm;

{

FIELD *pf;

static short level = 0;

if (level == 0)

scr_open( ); /* initialize the terminal state */

++level;

FOREVER

{

mu_pr(pm);                        /* print the menu */

pf = mu_ask(pm);                  /* get a new field ptr */

if (pf == NULL)

break;

else if (pf->val_type == 'm')    /* new field is a menu */

mu_do(pf->pmenu);

else /* pf->val_type == 'f' -- new field is a C fn */

{

asserts(pf->val_type == 'f', "val_type is 'f' ");

(void)(*pf->pfn)(pm);

}

}

--level;

if (level == 0)

{

scr_curs(scr_lins-1, 0);

scr_close();    /* restore terminal state */

}

}

One utility function is provided for use by the user program. It is called mu_reply, and it behaves just like the getreply function shown in Section 5.3, except that in SCR_RAW mode the dialog takes place at the bottom of the screen.

mu_reply:

/* mu_reply -- get a reply in response to a prompt

*              on the last screen line

*/

#include "local.h"

#include "menu.h"

mu_reply(prompt, reply, size)

char prompt[];      /* : string */

char reply[];       /* : !NULL */

int size;           /* max number of chars allowed in reply */

{

STRING_VAL rsv;     /* reply string value structure */

if (scr_mode == SCR_TTY)

return (getreply(prompt, reply, size));

else

{

scr_curs(scr_lins - 1, 0);

scr_print("%s", prompt);

reply[0] = '\0';        /* reply string initially empty */

rsv.edit_type = 'a';    /* initialize the STRING_VAL rsv */

rsv.string = reply;

rsv.maxsize = size;

rsv.pnum = NULL;        /* rsv => well-defined */

mu_sval(&rsv);          /* let mu_sval handle the dialog */

return (strlen(reply));

}

}

Finally, here is the header menu.h:

menu.h:

/* menu.h - header for menu pgm */

#ifndef MENU_H

#define MENU_H

#include "local.h"

#include "screen.h"

typedef struct string_val

{

char edit_type;      /* 'n' = numeric, 'd' = short int, 'a' = anything */

char *string;        /* ptr to the string value */

short maxsize;       /* maximum size of string: {2:SHORT_MAX} */

short *pnum;         /* if non-NULL, ptr to numeric storage */

} STRING_VAL;

typedef struct choice_val

{

short nchoices;      /* number of possible choices: {2:10} */

char **choices;      /* ptr to start of array of choice strings */

char *pc;            /* ptr to the one-char coded choice value */

} CHOICE_VAL;

/* (more)

*/

typedef struct field

{

short line, col;      /* co-ordinates of screen display position */

char *desc;           /* description (title) of this field: string */

char val_type;        /* what type of field is this? */

STRING_VAL *psv;      /* non-NULL iff val_type == 's' or 'S' */

CHOICE_VAL *pcv;      /* non-NULL iff val_type == 'c' */

struct menu *pmenu;   /* non-NULL iff val_type == 'm' */

bool (*pfn)();        /* function to do after str or choice field */

} FIELD;

typedef struct menu

{

char *title;          /* alphabetic title for menu: string */

FIELD *fields;        /* ptr to beginning of array of FlELDs */

short nfields;        /* how many fields in this menu */

short cur_field;      /* index of the current field */

} MENU;

FIELD *mu_ask( );        /* PARMS(MENU *pm) */

SCR_CMDCHAR mu_chv( );   /* PARMS(FIELD *pf) */

void mu_do( );           /* PARMS(MENU *pm) */

void mu_pr( );           /* PARMS(MENU *pm) */

int mu_reply( );         /* PARMS(char *prompt, char *reply, int size) */

SCR_CMDCHAR mu_str( );   /* PARMS(FIELD *pf) */

SCR_CMDCHAR mu_sval( );  /* PARMS(STRING_VAL *psv) */

#endif

This completes the source listing for the menu processor. It is a rather concise program, considering the capabilities that it provides. This is largely due to the encoding of so much information into the structures that drive it.

Much more flexibility and many more features are available in commercially available menu and screen-generator software. Here are a few simple extensions that could be added to this menu processor, left as exercises:

Exercise 6-1. An extra member (e.g., horizontal_ok) could be added to the MENU structure; if its value is YES, this menu allows the LEFT key as synonym for UP and RIGHT for DOWN. (This is an intuitive behavior for horizontal menus.)

Exercise 6-2. The cursor can be turned off except when entering strings. The current field can instead be highlighted (underscore, reverse video, etc.).

Exercise 6-3. The distinction between string fields and other types of field can be emphasized by emboldening the first character of other-field names.

Exercise 6-4. All data fields are currently stored as nul-terminated strings. Revise the program so that fields are blank-padded to their full size. (This creates a more traditional data processing record.)

For a concrete example of the use of the menu processor, let us consider the "change part record" menu. The first field ("part number") is mandatory, as indicated by the capitalized 's' in its field entry. Its description string "Part number " is to be displayed at row 2 and column 0. The field specifies the address of a STRING VAL structure named Spart_no. This field has a C function, is_part, associated with it; when the user attempts to leave this field, the is_part function will be called. Movement to another field succeeds only if a YES return is received. (More about is_part shortly.) The "unit of measure" field is a choice field, and all the others are string fields. Here is the C source file that defines the "change part record" menu:

chg_menu.c:

/* chg_menu - CHANGE A PART RECORD */

#include "local.h"

#include "menu.h"

#include "part.h"

extern bool is_part();

static STRING_VAL Spart_no =

{'a', (part1).part_no, sizeof((part 1).part_no), NULL};

static STRING_VAL Slead_time =

{'n', (part1).lead_time, sizeof((part1).lead_time), NULL};

static char *Tunit_meas[]=

{

"each", "lb", "box",

};

static CHOICE_VAL Cunit_meas =

{DIM(Tunit_meas), Tunit_meas, (part1).unit_meas};

static STRING_VAL Sunit_cost =

{'a', (part1).unit_cost, sizeof((part1).unit_cost), NULL};

static STRING_VAL Scost_qty =

{'n', (part1).cost_qty, sizeof((part1).cost_qty), NULL};

static FIELD Fchg_menu[ ] =

{

{2, 0, "Part number", 'S', &Spart_no, NULL, NULL, is_part},

{4, 0, "Lead time (in weeks)", 's', &Slead_time, NULL, NULL, 0},

{6, 0, "Unit of measure", 'c', NULL, &Cunit_meas, NULL, 0},

{8, 0, "Cost per unit", 's', &Sunit_cost, NULL, NULL, 0},

{10, 0, "Qty required for price", 's', &Scost_qty, NULL, NULL, 0},

};

MENU chg_menu = {"CHANGE A PART RECORD", Fchg_menu, DIM(Fchg_menu), 0};

The other submenus (add_menu, acd_menu, and del_menu) are similar. The source file for the main program, part_menu, also contains, for convenience, the functions called by each of the menus.

part_menu.c:

# include "menu.h"

# include "part.h"

/* main menu */

/* menu-sync = screen agrees with menu contents */

PART part1 = {0};

PART null_part = {0};

extern MENU add_menu, chg_menu, del_menu, acd_menu;

/* isn_part - verify that no record exists for this key */

bool isn_part(pm)

MENU *pm;         /* : menu-sync */

{

if (db_find_part(&part1))

{

/* pm => !menu-sync, new part contents */

remark("This part is entered already", "");

STRUCTASST(part1, null_part);   /* clear the part struct */

mu_pr(pm);                      /* pm => menu-sync */

return (NO);

}

return (YES);

}

/* is_part - verify that record exists and read it into part1 */

bool is_part(pm)

MENU *pm;

{

if (!db_find_part(&part1))

{

remark("Unknown part number", "");

STRUCTASST(part1, null_part);       /* clear the part struct */

mu_pr(pm);      /* display the cleared record, pm => menu-sync */

return (NO);

}

/* pm => !menu-sync, new part contents */

mu_pr(pm);           /* display the found record, pm => menu-sync */

return (YES);

}

/* addpart - add a part record */

bool addpart(pm)

MENU *pm;

{

char reply[2];

STRUCTASST(part1, null_part);

mu_do(&add_menu);

/* if no valid transaction occured, exit without change to db */

if (part1.part_no[0] == '\0')

return (NO);

mu_reply("Add this part [y/n]?", reply, 2);

if (reply[0] == 'y')

{

return (db_add_part(&part1));

}

else

return (NO);

}

/* chgpart - change a part record */

bool chgpart(pm)

MENU *pm;

{

char reply[2];

STRUCTASST(part1, null_part);

mu_do(&chg_menu);

/* if no valid transaction occured, exit without change to db */

if (part1.part_no[0] == '\0')

return (NO);

mu_reply("Change this part [y/n]? ", reply, 2);

if (reply[0] == 'y')

return (db_chg_part(&part1));

else

return (NO);

}

/* delpart - delete a part record */

bool delpart(pm)

MENU *pm;

{

char reply[2];

STRUCTASST(part1, null_part);

mu_do(&del_menu);

/* if no valid transaction occured, exit without change to db */

if (part1.part_no[0] == '\0')

return (NO);

mu_reply("Delete this part [y/n]? ", reply, 2);

if (reply[0] == 'y')

return (db_del_part(&part1));

else

return (NO);

}

/* part_menu (main) - execute the parts menu */

main()

{

db_open_part();

mu_do(&acd_menu);

db_close_part();

}

The simulated database of parts records is provided by part_db.c, containing several functions which manipulate a small array of PART records.

part_db.c:

/* part_db.c - simulated database access to parts */

# include "local.h"

# include "part.h"

static PART parts[3] = {0};

static short n_part = 0;

/* db_locate_part -- see if a part is in the database */

static int db_locate_part(partp)

PART *partp;

{

short i;

for (i = 0; i < n_part; ++i)

if (STREQ(partp-> part_no, parts[i].part_no))

return (i);       /* this is the part, return its index */

return (-1); /* no part from 0 to n_part matches this part_no */

}

/* db_add_part - add a part record to database */

bool db_add_part(partp)

PART *partp;

{

if (n_part >= DIM(parts))

{

remark("Part storage is full", "");

return (NO);

}

STRUCTASST(parts[n_part], *partp);

++n_part;

return (YES);

}

/* db_find_part - find a part record in database */

bool db_find_part(partp)

PART *partp;

{

short i;

i = db_locate_part(partp);

if (i == -1)

return(NO);

STRUCTASST(*partp, parts[i]);

return(YES);

}

/* db_del_part - delete a part record in database */

bool db_del_part(partp)

PART *partp;

{

short i;

for (i = 0; i < n_part; ++i)

{

if (STREQ(partp->part_no, parts[i].part_no))

{

--n_part;

STRUCTASST(parts[i], parts[n_part]);

return (YES);

}

}

return (NO);

}

/* db_chg_part - change a part record in database */

bool db_chg_part(partp)

PART *partp;

{

short i;

for (i = 0; i < n_part; ++i)

{

if (STREQ(partp->part_no, parts[i].part_no))

{

STRUCTASST(parts[i], *partp);

return (YES);

}

}

return (NO);

}

/* db_open_part - open the parts database

* Dummy - no action needed

*/

void db_open_part()

{

}

/* db_close_part - close the parts database

* Dummy - no action needed

*/

void db_close_part()

{

}

Thus, the entire program consists of the main program file (part_menu), the part_db package, and the three source files for the submenus ( add_menu, chg_menu, and del_menu.)

Lest you think that I am teaching you the wrong lessons, let me add a few words about the overall functionality. This example is chosen primarily for its recognizability and its simplicity. In a real application, the separation of "add," "change," and "delete" would be considered rather awkward; the three operations could be combined into one somewhat more complicated menu. Also, the user must EXIT from each menu in order to affect a change to the database, which is distracting.

All these shortcomings could be overcome with a different approach to the problem, which is left as an exercise to the reader:

Exercise 6-5. Program part_menu with all operations combined into one menu that would look something like this:

UPDATE PART RECORDS

Part number:

Action [a, c, d, p]:

Lead time (weeks):

Unit of measure:

Unit cost:

Cost qty:

In your design, you may find it useful to add a new function (or macro) to the menu package: mu_set_fld(n) will set the current field number to n.

6.9 A Translator for Menus

Preparing the C source code for a menu is highly repetitIve work with the potential for introduction of errors. We will look at a simple program which will allow menus to be written in a more human-readable form. The user is assumed to be a C programmer who is familiar with the conventions of the menu package; the translator will just make the programmer's job a bit easier.

The human-readable form for a menu looks something like this:

chg_menu.mu:

Menu name: chg_menu     Menu title: CHANGE A PART RECORD

Header: part.h        C-struct: part1

Field name: part_no     Desc: Part number

Line: 2 Col: 0    Type: S     Edit: a     Post-fn: is_part

Field name: lead_time     Desc: Lead time (in weeks)

Line: 4 Col: 0    Type: s      Edit: n     Post-fn: 0

Field name: unit_meas     Desc: Unit of measure

Line: 6 Col: 0   Type: c              Post-fn: 0

{

"each", "lb", "box",

};

Field name: unit_cost    Desc: Cost per unit

Line: 8 Col: 0   Type: s     Edit: a      Post-fn: 0

Field name: cost_qty    Desc: Qty required for price

Line: 10 Col: 0  Type: s     Edit: n     Post-fn: 0

Compare this with the corresponding C source given at the end of the previous section.

The menu generator itself is a straightforward application of the functions gestpstr and getpl in shown in the previous chapter. Whenever the data required is a single "word," gestpstr will read it for us. Whenever an input string may contain more than one word, we use getpl in. Thus, the input format is extremely free-form, and the menu inputs may be formatted ad-lib. The only inputs that are not read with either of these functions are the list of alternatives for a choice field. Here were simple accept a comma-separated list of C strings. The end of the list is marked by a line consisting only of initial whitespace followed by };.

Purely for purposes of simplification, the Edit: input is coded like this:

a       String of any characters

n       String of digits only (with possible leading '-')

n1      short integer, 1 digit

n2      short integer, 2 digits (with possible leading '-')

... etc. ...

n6      short integer, 6 digits (with possible leading '-')

The full program is quite large but unsophisticated. The only deviation from a straight translation of input to output is the buffering of FIELD lines so that they can be printed at the end to initialize the array of FIELDS.

gen_menu.c:

/* gen_menu - generate C source for menu from description file */

#include "local.h"

#define MAXBFLDS 8000

#define C_ID_LEN 31

#define FILE_NM_LEN 64   /* max length of file name */

#define C_EXPR_LEN 80    /* arbitrarly limit on length of struct ref */

#define TITLE_LEN 80     /* max length of displayed title */

#define SCR_POS_LEN 3    /* max digits in screen position */

#define FTYPE_LEN 1      /* field type is only one char */

#define EDIT_LEN 2       /* edit type is 1 char plus 1 optional digit */

#define MAXFLINE (TITLE_LEN+FTYPE_LEN+EDIT_LEN+SCR_POS_LEN+SCR_POS_LEN+50)

#define GETPSTR(p, s) \

{  if (!getpstr(p, s, sizeof(s))) error("Cannot match" , p); }

#define OPTPSTR(p, s) \

getpstr(p, s, sizeof(s))

#define GETPLIN(p, s) \

{  if (!getplin(p, s, sizeof(s))) error("Cannot match" , p); }

/* internal variables */

static char buf[BUFSIZ] = {""};

static char buf_flds[MAXBFLDS] = {""};

static short iflds = 0;

static char mname[C_ID_LEN+1] = {""};

static char mstruct[C_EXPR_LEN+1] = {""};

static char mtitle[TlTLE_LEN+1] = {""};

static char mheader[FILE_NM_LEN+1] = {""};

static char fname[C_ID_LEN+1] = {""};

static char fline[SCR_POS_LEN+1] = {""};

static char fcol[SCR_POS_LEN+1] = {""};

static char ffn[C_ID_LEN+1] = {"");

static char ftype[FTYPE_LEN+1] = {""};

static char fedit[EDIT_LEN+1] = {""};

static char fdesc[TITLE_LEN+1] = {""};

void do_1_menu(), do_1_fld(), pr_s_fld(), pr_c_fld(), pr_m_fld(), pr_f_fld();

/* gen_menu (main) */

void main()

{

do_1_menu();

while (OPTPSTR("Field name:", fname))

do_1_fld(); /* process one field */

printf("static FIELD F%s[] =\n", mname);

printf("\t{\n");

for (i_flds = 0; buf_flds[i_flds] != '\0'; ++i_flds)

putchar(buf_flds[i_flds]);

printf("\t);\n");

printf("MENU %s = {\"%s\", F%s, DIM(F%s), 0};\n",

mname, mtitle, mname, mname);

}

/* do_1_menu - process the menu information */

void do_1_menu()

{

GETPSTR("Menu name:", mname);

GETPLIN("Menu title:", mtitle);

GETPSTR("Header:", mheader);

GETPSTR("C-struct:", mstruct);

printf("/* %s - %s */\n", mname, mtitle);

printf("#include \"local.h\"\n");

printf("#include \"menu.h\"\n");

printf("#include \"%s\ "\n", mheader);

}

/* do_1_fld - process the info for one field */

void do_1_fld()

{

asserts(IN_RANGE(i_flds, 0, MAXBFLDS - MAXFLINE), "buffer space ok");

GETPLIN("Desc:", fdesc);

GETPSTR("Line:", fline);

GETPSTR("Col:", fcol);

GETPSTR("Type:", ftype);

if (tolower(ftype[0]) == 's')

GETPSTR("Edit:", fedit);

if (ftype[0] != 'm')

GETPSTR("Post-fn:", ffn);

if (!STREQ(ffn, "0") && !STREQ(ffn, "NULL"))

printf("extern bool %s();\n", ffn);

switch (ftype[0])

{

case 's':

case 'S':

pr_s_fld();

break;

case 'c':

pr_c_fld();

break;

case 'm':

pr_m_fld();

break;

case 'f':

pr_f_fld();

break;

default:

error("Unknown field type", ftype);

}

i_flds += strlen(&buf_flds[i_flds]);

if (i_flds > MAXBFLDS - MAXFLINE)

error("out of buffer space", "");

}

/* pr_s_fld - print the info for a string field */

void pr_s_fld()

{

sprintf(&buf_flds[i_flds],

"\t{%s, %s, \"%s\", '%c', &S%s, NULL, NULL, %s},\n",

fline, fcol, fdesc, ftype[0], fname, ffn);

if (fedit[1] == '\0')

{

printf("static STRING_VAL S%s =\n", fname);

printf("\t{'%c', (%s).%s, sizeof((%s).%s), NULL};\n",

fedit[0], mstruct, fname, mstruct, fname);

}

else

{

printf("static char N%s[%c+1] = {0};\n",

fname, fedit[1]);

printf("static STRING_VAL S%s =\n", fname);

printf("\t{'%c', N%s, sizeof(N%s), &(%s).%s};\n",

fedit[0], fname, fname, mstruct, fname);

}

}

/* pr_c_fld - print the info for a choice field */

void pr_c_fld()

{

int ret;

sprintf(&buf_flds[i_flds,

"\t{%s, %s, \"%s\", '%c', NULL, &C%s, NULL, %s},\n",

fline, fcol, fdesc, ftype[0], fname, ffn);

printf("static char *T%s[]=\n", fname);

do {

ret = getsnn(buf, BUFSIZ);

if (ret != EOF && ret > 0) /* skip blank lines */

printf("%s\n", buf);

} while (ret != EOF && strchr(buf, ';') == NULL);

printf("static CHOICE_VAL C%s =\n", fname);

printf("\t{DIM(T%s), T%s, (%s).%s};\n",

fname, fname, mstruct, fname);

}

/* pr_m_fld - print the info for a menu field */

void pr_m_fld()

{

printf("extern MENU %s;\n", fname);

sprintf(&buf_flds[i_flds],

"\t{%s, %s, \"%s\", '%c', NULL, NULL, &%s, %s},\n",

fline, fcol, fdesc, ftype[0], fname, ffn);

}

/* pr_f_fld - print the info for a function field */

void pr_f_fld()

{

sprintf(&buf_flds[i_flds],

"\t{%s, %s, \"%s\", '%c', NULL, NULL, NULL, %s},\n",

fline, fcol, fdesc, ftype[0], ffn);

}

Exercise 6-6. Write a program (using menus) which will write ".mu"menu files.

Go to Chapter 7 Return to Table of Contents