Microsoft Systems Journal Volume 6

────────────────────────────────────────────────────────────────────────────

Volume 6 - Number 1

──────────────────────────────────────────────────────────────────────────── Adapting Extended Processes to the Cooperative Multitasking of Microsoft Windows William S. Hall Programming a lengthy process in the Microsoft Windows graphical environment requires unique considerations. Unlike single-tasking or preemptive multitasking operating environments, Windows1 uses message-driven, cooperative multitasking to perform tasks. Windows programs do not execute until a message is received; once received, the message must be processed quickly so that control of the CPU can be relinquished to permit other Windows programs to run. Performing a time-consuming process by coding a single uninterrupted thread of execution is completely unsatisfactory because Windows will never run any other task and will appear frozen to the user during that time. Fortunately, large tasks can often be broken down into smaller ones, each of which can be quickly executed whenever the program is allowed to run. If some means can be found for gaining control at appropriate intervals and dispatching each task in the proper sequence, then it should be possible to implement an extended process smoothly in Windows. Asynchronous file transfer between two computers serves as a good example of an extended process, because it can take hours over conventional communication lines. This article demonstrates how to coordinate an extended process into Windows using the popular Kermit file transfer protocol. One common approach to writing a Kermit program involves breaking the protocol into a sequence of tasks controlled by a finite state machine. I'll describe modifications of this approach that fit the requirements of Windows and illustrate how the program can be made to schedule its next task without the use of a timing mechanism. An implementation of Kermit that can be easily integrated into a Windows terminal emulator is provided, as well as a sample terminal program that uses this implementation. The program is capable of sending and receiving files in text and binary formats over 7- and 8-bit wide data paths while employing any of three methods of error detection and run-length encoding for efficiency. Wildcards can be used to transfer groups of files in a single operation. Although server mode and extensions such as long and attribute packets are not included, the program is quite complete. A total of 40 files are used to build Kermit and the two terminal emulators described here. Partial listings taken from the Kermit and terminal programs are included with this article. The complete source code needed to build Kermit, the simple Windows terminal emulator, and the same emulator with embedded Kermit is available on any MSJ bulletin board. Realizing Kermit's Session Layer in Windows In a Kermit session, information is exchanged by encapsulating blocks of data in various types of packets that are normally less than 100 bytes in length, unless an extended packet type is being used (see Figure 1). Although Kermit is not a layered protocol as specified in the ISO open system standards, most versions of Kermit are written with these layers in mind to isolate functionality and make it easy to extend and maintain. The Kermit session layer acts as the basic controlling mechanism for the protocol. When sending or receiving a file, the session layer is driven by the packet type received from the remote Kermit. This session layer can be thought of as a finite state machine. A public-domain tool called Wart has been developed for Kermit that allows a programmer to describe such state machines very elegantly (see the sidebar "Building a Finite State Machine with Wart" ). The Wart tool converts the protocol description into a table-driven case statement, which is entered at run time by calling the wart() function. The current state is maintained in a static variable; thepacket type is obtained by the wart() function's call to the transport layer function input() (the Kermit code implements its protocol description using wart() in the file WNKERM.W, as shown in Figure 2). In turn, input() calls a data link layer function, rpack (see Figure 3), to obtain a complete packet. Figure 4 diagrams the various states of the Kermit protocol implemented in WNKERM.W. In Kermit implementations written for single-tasking or preemptive multitasking operating systems, once wart() is entered it never returns until the entire session is complete or has been aborted. Likewise, input() does not return to wart() until a properly formed packet with the correct sequence number has been found. In turn, rpack does not return to input() until a complete packet with a correct checksum has been obtained or a time-out (usually of several seconds) has elapsed. Of course, none of this waiting around is acceptable in Windows. Suppose wart() is called from a Windows program as the result of a message. If wart() runs as above, no other Windows program can run until the file transfer has completed. Even if wart() returns after the action associated with each state has been executed, long delays could still be experienced while rpack tries to read in a complete packet from the communications line. In fact, a complete packet might never arrive. On the other hand, the actions associated with each state complete without significant delay because they involve reading or writing small chunks of data from a file, encoding or decoding this data, forming it into a packet, and writing it out to the communications buffer. The solution is to violate the layering principle and allow the session layer to recognize an incomplete packet whose associated action is simply to return from wart(). This simple addition to the packet types plus a mechanism to build a packet incrementally from successive reads of the communications line allows Windows to release the CPU to other programs in a timely fashion. This is how it works. Periodically the Windows procedure is entered with a programmer-defined message that calls wart(), which then calls input(). If a packet is ready, the action for that state is executed and another call to input() is made. This time input() most likely returns an incomplete packet and wart() returns control to Windows. Other tasks can then run; the result is a nearly seamless multitasking functionality that handles the file transfer almost transparently. Of course, the way wart() is scheduled to run has not been explained. But first, let's look at how the data link layer is handled. The Data Link Layer After the input() function calls rpack in the data link layer to get the next packet, rpack calls getpacket, which parses the input stream into the packet fields (see Figure 3). The getpacket function frames a packet by detecting the start-of-packet character; the length of the data field is used to determine when a complete packet has arrived. On any single call to getpacket, the input stream may not contain enough data to build a complete packet, and it may take several calls to this function before a packet can be delivered. This means that getpacket must be designed so that when it exhausts the current data stream, it remembers how far along it has progressed and the point from which it must continue when called again with more data. A state machine implementation is again implied; it is easily realized by a case statement because of the simplicity of getpacket. The state variable itself is maintained in a globally visible structure that also contains the other fields of the current packet. Each time getpacket is called, the case statement is executed from the current state and returns when the buffer is exhausted. This way, getpacket and its caller rpack do not need to wait for more data to arrive from the communications line, and input can return immediately to wart() with an incomplete packet type. Waking Up the Session Layer The missing piece in the above scenario is the scheduling of the session layer. An obvious solution is to use a timer and place the call to wart() on this thread. A timer, however, does not adjust to changing load conditions and extra effort must be made to set the timing interval according to the current baud rate. A more satisfactory method is to replace the conventional GetMessage call in the Windows message loop with PeekMessage. Whereas GetMessage does not return to the program until a message has been placed into its queue, PeekMessage always returns whether there is a message or not. To use PeekMessage, your program processes any Windows messages that can be pulled from the queue, or, if there are none, it does something else for a short period. In the design used here, the communications port is polled during this time, and any pending data is loaded into a local buffer. Then a programmer-defined message is posted back to the window. The next call to PeekMessage retrieves this message and the associated action is to call wart(). Getpacket is entered via calls to input and rpack with the buffer that was filled when the communications port was polled by PeekMessage. If a complete packet is built, the next action in the session layer state machine is executed. Otherwise, wart() sees an incomplete packet, and returns to Windows. Figure 5 shows the message loop and excerpts of the WinProc used in the sample terminal emulator. Sample Application The complete source code for a Windows-based terminal emulator with embedded Kermit support (see Figure 6) can be found on any MSJ bulletin board. You'll need the Microsoft Windows Software Development Kit (SDK) Version 3.0 and Microsoft C Version 5.1 or 6.0 to build the application. A typical Windows program, the terminal program consists of three parts: source, resources, and definitions. Of course, the additions needed to add Kermit functionality affect each of these three components. For the most part, the Kermit module is independent of the terminal source code. This module is a library to be added at link time. Of course the terminal program cannot be entirely ignorant of certain Kermit functions, and Kermit itself needs to be aware of certain variables such as the main window and instance handles, the current communications channel, and the location of the buffer used for reading data from the port. However, all of these details can be handled fairly easily in the terminal source. By using conditional compilation, the changes to the original code become clearly marked, making adaptation to other terminal programs fairly straightforward. Trying to maintain this same degree of isolation with respect to resources is another matter. Kermit requires a user interface, so a string table, menu item, and several dialog boxes have been added. Although the resource compiler supplied with Version 3.0 of the SDK is a definite improvement over its 2.x predecessor, it is still difficult to maintain multiple compilations with a single source. In the end, it is necessary to build one RC source file for the plain terminal emulator (without Kermit), another for the emulator with Kermit, and then #include other resource files as needed. Of course, the additional Kermit code and resources also affect the DEF file because they require more function exports and segment names. But since it is possible to have more than one SEGMENTS and EXPORTS section in a single DEF file, it is quite simple to create a combined DEF file by concatenation. Of course, segment names as well as function export numbers should be unique. There are three make files that control the sources. The first, WNTERM, simply builds the original terminal program, WNTERM.EXE. It is provided so you can see clearly what has to change when the Kermit code is added. The second, WNKERM, makes the Kermit library and compiles the Kermit resources. The third make file, WNKTERM, builds the terminal with embedded Kermit support, WNKTERM.EXE. The Kermit Header File Each of the Kermit modules as well as the terminal code that references Kermit variables or constants must include the header WNKERM.H (see Figure 7). It consists of Kermit function prototypes, a menu, resource strings, general manifests, and a number of structures associated with dialog boxes or the protocol. All Kermit variables are defined in this header file. At least one module in the terminal code should include WNKERM.H to declare the data. To avoid possible conflicts with the terminal code, all variables and functions begin with the prefix krm. All manifests include the string KRM_. Thus, IDS_KRM_XXX names a string stored in the resource file, IDM_KRM_XXX, a menu item, and IDD_KRM_XXX, a dialog box control. Strings use a reference value, and other strings' manifests base themselves from this reference. The base value is set to 10,000, but can be changed to avoid possible conflict with the terminal code. The same is true of menus. The Kermit code expects certain variables to be available from the terminal. These are copied to Kermit variables at initialization. One of these, the handle of the main window, is good for the lifetime of the program. Other values may change periodically. For example, the communications port identifier (cid) may vary if the user selects a different port. Because of this, the Kermit code uses a pointer to reference the cid. Kermit must also be able to reference a linear character buffer along with its current count, so the count and the start of the buffer are kept as pointers by Kermit. Kermit also expects that there are routines in the terminal to fill this buffer by reading the com port and delivering the input to Kermit via a call to the state switcher wart(). Kermit and the Terminal's Main WinProc Of course, the terminal's main WinProc is also affected by the addition of Kermit support. When the main terminal window is created, a call is made to Kermit at WM_CREATE time to pass along information such as the window handle, buffer information, and the location of the communications port identifier. Except for the handle, this information changes dynamically, so pointer variables are needed. During the initialization, WIN.INI is also read under the subhead [Kermit] to provide user settings pertaining to the protocol and inbound and outbound packets. The Kermit menu is also added to the end of the terminal's main menu. Finally, if the port to be used is not yet known, the Kermit menu should be initially disabled. In the event either Windows or the program is closed, any global resources used by Kermit must be freed. If termination is attempted in the middle of a transfer, the user must have a chance to change his or her mind. In the Kermit exit code, a message box appears if a transfer is still in progress. If the reply is to shut down (IDYES), an error packet is sent to the remote Kermit, all files are closed (incompletely received files are deleted if this option has been set), and the response to WM_CLOSE or WM_QUERYENDSESSION proceeds as usual. Otherwise, the session continues as if nothing happened. The Kermit menu allows the user to send a group of files (see Figure 8), receive a group of files, cancel a transfer in one of several ways, and select default values for several protocol and packet parameters (see Figures 9 and 10). The Kermit menu processor returns TRUE if a menu item was recognized and handled. Otherwise it returns FALSE; this is a signal for the terminal to execute its own handler for WM_COMMAND. When Kermit is in action, any child windows on the screen are hidden and a modeless dialog box is posted to show the state of the transfer (see Figure 11). Of course, the transfer continues even if the terminal program is minimized. If the terminal program draws its own icon rather than using a class icon, Kermit will show the number of packets exchanged like an odometer (with rollover every 10,000 packets) in the icon window. This lets the user monitor progress even when he or she is performing other tasks and the terminal is running in the background (see Figure 12). This display is managed in response to the WM_PAINT message. Finally, as already noted, Kermit calls the state machine in response to a programmer-defined message. You have already seen how such a message is generated by PeekMessage. Conclusion By breaking up a long process into naturally occurring shorter ones, it is possible to preserve the cooperative multitasking feature of Windows and still get the job done. Although Kermit has been used to illustrate specific techniques, the principles presented here can be applied to other communication protocols, as well as complex, time-consuming tasks that can be described with a finite state machine approach. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers to this Microsoft product only and is not intended to refer to such products generally. 2 As used herein, "DOS" refers to the MS-DOS and PC-DOS operating systems. The Kermit File Transfer Protocol Like its famous puppet namesake, the Kermit file transfer protocol is internationally known. Since its introduction in 1980, Kermit has become a mainstay of academic and business computing, supported on machines from the largest mainframes to the tiniest microcomputers. Operating systems supporting this protocol include VM/CMS, DOS2, OS/2, CP/M, UNIX, and even the USCD P-System. Kermit has been written in Algol, Pascal, C, FORTRAN, BASIC, BCPL, CROSS, Compass, FORTH, PAL-8, MUMPS, BLISS, PL/1, Ratfor, PL/M, Modula-2, LISP, and quite a few assembly languages. Kermit's importance is rivaled only by the XMODEM protocol. There is even an IBM 370 version of Kermit in Russian. Kermit was written to answer the need for a reliable means of transferring files from micros to mainframes. Simply uploading and downloading files is often unreliable. Unusual file formats, noisy lines, or busy mainframes expecting input at human typing speed frequently result in a trashed transfer. To address this problem, Frank da Cruz and his associates at Columbia University developed a protocol that supported operations between DECSYSTEM-20 or IBM 370 systems and microcomputers running CP/M and DOS. These early programs were made available to users along with the source code. Today, Kermit remains free and is widely available at no more than the cost of distribution. This policy has not only broadened the support base for Kermit, it has also contributed to its astounding acceptance by the computing community. At first, Kermit was only capable of transferring text files in packets containing at most 94 characters, using a simple 1-byte checksum. Since then, Kermit has been extended to include binary transfers over 7- and 8-bit wide paths, run-length encoding, CRC checksums, long packets, transfers over networks, server modes, and more recently, sliding windows. Today, you can not only send your file, you can preserve its attributes including its date and time of creation. Kermit can even translate between varying character sets such as ASCII, ISO Latin I, ISO Latin/Cyrillic, ISO Latin/Hebrew, and Japanese JIS X 0208. Kermit has special features for users with visual, hearing, or motor impairments, including speech synthesis, a simple command-line interface, and special program documentation. Most Kermit programs offer sophisticated terminal emulation as well as file transfer. For example, Version 3.0 for DOS, written by Joe Doupnik of Utah State University, emulates DEC VT320 terminals and supports Tektronix graphics. Other features include screen rollback, Windows compatibility, a visual bell for deaf users, right-to-left screen display for Hebrew and Arabic, and a complete scripting language. Kermit rivals commercially available products, but costs at most $20.00. Building a Finite State Machine with Wart Wart, written by Jeff Damens of Columbia University, is a tool that builds state table switchers. Wart contains a small subset of the UNIX lexical analyzer generator, lex, and may be freely distributed (lex, which means "word" in Latin, translates to wort in German, which sounds like wart on a frog). Wart accepts as input a C program in the following format. lines to be copied | %state <state names...> %% <state> | <state,state,...> CHAR { actions } . . . %% The %state directive declares the program's states. The section enclosed between the %% delimiters is the state table. A typical entry has the form <state>X {action} which is read as "if in state <state> with input X, perform {action}". The optional <state> field names the states the program must be in to perform the related action. If no state is specified, the action is carried out regardless of the current state. If more than one state is specified, the action is performed in any of the listed states. Multiple states are separated by commas. The required input field consists of a single literal character. In a given state, if the input is the specified character, the associated action is performed. The character . matches any input character. No pattern matching or range notation is provided. The input character itself is obtained from a function called input(), which the user must define. Input should return an alphanumeric character or one of the following characters. % - _ $ @ . The action statement is a series of zero or more C statements enclosed in curly braces. Wart also provides the BEGIN macro, which is defined as state = , as it is in lex. Wart is invoked at the command line as follows: wart file.w file.c In this example, Wart reads FILE.W and produces FILE.C, which can then be compiled as an ordinary C program. Wart's output contains the function called wart(), whose form depends on the state declarations and the state transition table. Wart loops through calls to input() and uses the result to index into a case statement created from the state table. The following program demonstrates some of the capabilities and limitations of Wart. BINTODEC.W accepts a binary number from the command line, preceded by an optional minus sign and possibly containing a fractional part, and prints the decimal equivalent. BINTODEC.W #include <stdio.h> int state, s = 1, m = 0, d; float f; char *b; /* Declare wart states */ %states sign mantissa fraction /* Begin state table */ %% <sign>- { s = -1; BEGIN mantissa; } /* Look for sign */ <sign>0 { m = 0; BEGIN mantissa; } /*Got digit,start mantissa */ <sign>1 { m = 1; BEGIN mantissa; } <sign>. { fatal("bad input"); } /* Detect bad format */ <mantissa>0 { m *= 2; } /* Accumulate mantissa */ <mantissa>1 { m = 2 * m + 1; } <mantissa>$ { printf("%d\n", s * m); return; } <mantissa>. { f=0.0; d = 1; BEGIN fraction; } /* Start fraction */ <fraction>0 { d *= 2; } /* Accumulate fraction */ <fraction>1 { d *= 2; f += 1.0 / d; } <fraction>$ { printf("%f\n", s * (m + f) ); return; } <fraction>. { fatal("bad input\n"); } %% input(void) { /* Define input() function */ int x; return(((x = *b++) = = '\0') ? '$' : x ); } fatal(char *s) { /* Error exit */ fprintf(stderr,"fatal - %s\n",s); exit(1); } main(int argc,char **argv) { /* Main program */ if (argc < 2) { fprintf(stderr, "Not enough input\n"); exit(1); } b = *++argv; state = sign; /* Initialize state */ wart(); /* Invoke state switcher */ exit(0); /* Done */ } The following code is generated by processing BINTODEC.W with WART.EXE: BINTODEC.C /* WARNING --This C source program generated by Wart preprocessor.*/ /* Don't edit this C file; edit the Wart-format .W file instead, */ /* and then run it through Wart to produce a new C source file. */ /* Wart Version Info: */ char *wartv = "Wart Version 1A(006) Jan 1989"; #include <stdio.h> int state, s = 1, m = 0, d; float f; char *b; /* Declare wart states */ #define sign 1 #define mantissa 2 #define fraction 3 /* Begin state table */ #define BEGIN state = int state = 0; wart() { int c,actno; extern short tbl[]; while (1) { c = input(); if ((actno = tbl[c + state*128]) != -1) switch(actno) { case 1: { s = -1; BEGIN mantissa; } break; case 2: { m = 0; BEGIN mantissa; } break; case 3: { m = 1; BEGIN mantissa; } break; case 4: { fatal("bad input"); } break; case 5: { m *= 2; } break; case 6: { m = 2 * m + 1; } break; case 7: { printf("%d\n", s * m); return; } break; case 8: { f = 0.0; d = 1; BEGIN fraction; } break; case 9: { d *= 2; } break; case 10: { d *= 2; f += 1.0 / d; } break; case 11: { printf("%f\n", s * (m + f) ); return; } break; case 12: { fatal("bad input\n"); } break; } } } short tbl[] = { -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, -1, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 1, 4, 4, 2, 3, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, 4, -1, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 7, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 5, 6, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 8, 0, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 11, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 9, 10, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, 12, }; input(void) { /* Define input() function */ int x; return(((x = *b++) = = '\0') ? '$' : x ); } fatal(char *s) { /* Error exit */ fprintf(stderr,"fatal - %s\n",s); exit(1); } main(int argc,char **argv) { /* Main program */ if (argc < 2) { fprintf(stderr, "Not enough input\n"); exit(1); } b = *++argv; state = sign; /* Initialize state */ wart(); /* Invoke state switcher */ exit(0); /* Done */ } Creating a Network Service Using the Client-Server Model and LAN Manager 2.0 Brendan W. Dixon As the PC industry has matured, concepts and techniques pioneered on larger systems have been adapted for PC users. Many of these, such as preemptive multitasking, virtual memory and paging, are little changed on PCs. Others have been combined with features unique to PCs, producing new uses for computers. For example, when the graphical capability of the PC was combined with the large-system approach to document preparation, a whole new industry, desktop publishing, was created. The client-server model combines the large-system concept of centralized resources with the PC's peer-to-peer approach to networking, and lays the groundwork for distributed processing. The client-server model is an application design approach that distributes resources and processing between systems in a network. Unlike the standard network model, where each client participates in resource management, the client-server model divides responsibilities and intelligence. Servers contain all of the intelligence necessary to own and manage the resource (for example, a database server, as in Figure 1). Clients contain only nominal information about the resource (that it is a database, for instance) and do not assist in its management; they pass requests to the server, which the server decides to accept or reject. Conversely, servers are unaware of the client's intentions. A server only responds to requests for the resource it is managing (say, a database transaction). Centralizing resource management onto the server simplifies error handling and reduces the network load. Removing resource management tasks from the clients reduces processing requirements on workstation hardware and allows them to focus on their primary responsibilities such as interfacing with the end user. Microsoft LAN Manager is designed to support client-server programming. While LAN Manager provides all of the facilities of standard PC networks (that is, file and printer sharing), it also provides a platform that simplifies the construction of client-server-based applications and network communication. This article examines the building of a LAN Manager service. Creating and managing a group of remote named pipes will be discussed in a future article, as well as the construction of a LAN Manager client under the Microsoft Windows and OS/2 Presentation Manager graphical environments. The LAN Manager Application Programming Interface (API) gives the programmer a robust set of function calls to communicate with LAN Manager and issue network requests. Almost any network task that can be performed from the command line or by using LAN Manager's full-screen interface can also be done through in-line code. There are nearly 130 API calls (see Figure 2), and many take modal parameters that can be used to tune the function. Most important for client-server programmers is LAN Manager's support of remote named pipes. Named pipes are one-way or bidirectional facilities that allow processes to communicate. They offer guaranteed delivery and built-in buffering, simplified error detection, and access security. Another feature of LAN Manager is nonconnection oriented messaging through mailslots. Mailslots are useful for peer-to-peer communication, message broadcasting, and client-server communication that does not require all the facilities of a named pipe. Mailslots are either first-class (with guaranteed delivery) or second-class (without guaranteed delivery). Remote first-class mailslots, which can receive messages from remote workstations, must be created on a LAN Manager server. But any workstation may send or receive remote messages on a second-class mailslot. Named pipes and mailslots enable the programmer to build applications that utilize the network without being network-aware. LAN Manager also makes it easy for the network administrator to control access to named pipes and mailslots, and therefore the network. These facilities lead to easily built, easily maintained, and easily controlled client-server applications. Sample LAN Manager Service In order to provide a consistent method of control for the network administrator, LAN Manager was designed as an extensible system, implemented as a group of services. A LAN Manager service is a detached OS/2 process (that is, no console I/O) running as an extension of LAN Manager. Such a service can be controlled (that is, started, paused, continued, or stopped) through LAN Manager. Developing a LAN Manager service requires only that the service be listed in the LANMAN.INI file, that it register itself with LAN Manager as part of its start-up processing, and that it respond to LAN Manager requests, which are sent to the service through the OS/2 signal mechanism. Additionally, a LAN Manager service may take tuning parameters; these are also normally specified in the LANMAN.INI file. In the client-server model, the server makes resources available to the client and manages those resources for the client. One server resource, normally unavailable to clients, is the ownership of remote named pipes. While a client may open and communicate on a remote named pipe, it cannot create the pipe; remote named pipes are a resource provided by a LAN Manager server and must be created on the server. A sample LAN Manager service designed for this article, PBX, can serve as the foundation for real-world client-server applications. PBX gives clients the ability to establish peer-to-peer communications using named pipes through a transparent routing mechanism. Clients register with PBX and request connections with other clients. Once a connection is established between two clients, PBX maintains the image of a peer-to-peer named pipe connection for each client until the connection is broken. PBX creates at least three threads in addition to the main thread created when PBX is given control. Each thread is named after its C routine (see Figure 3). This article examines the main thread, which initializes PBX and communicates with LAN Manager. A future article will examine the remaining threads, which create and maintain the virtual named pipe connections (see Figure 4 for the source code files used to build PBXSRV.EXE). To build a LAN Manager application, you will need either the Microsoft LAN Manager Programmer's Toolkit or the Microsoft LAN Manager Developer's Toolkit. These toolkits include the C header files and libraries utilized in a LAN Manager application. To support a LAN Manager API category in your program, you add one or more #define statements and a #include for LAN.H in your C source file. Figure 2 lists the C preprocessor constants for each LAN Manager API category. For example, to include support for mailslots, you would add the following to your program file: #define INCL_NETMAILSLOT // Include mailslots #define INCL_NETERRORS // Include error constants #include <lan.h> As for linking your application, most LAN Manager applications need to link only with LAN.LIB, so you simply add this library to those you would normally use (see Figure 5). To build a LAN Manager service you will need to include support for the service APIs by including a #define statement for INCL_NETSERVICE before you #include LAN.H. Your program must also contain an OS/2 signal handler and use the service APIs to maintain a simple communication protocol with LAN Manager. These points will be covered in more detail further on. Installing a LAN Manager Service A service is started from the command line using the LAN Manager NET command or using the LAN Manager full-screen interface. The PBX service could be started from an OS/2 command prompt by typing: NET START PBX Services can also be started and stopped automatically when the LAN Manager Workstation or Server service starts or stops by adding the service name to either the wrkservices or srvservices line in LANMAN.INI (see the Microsoft LAN Manager Administrator's Reference for details on LANMAN.INI). When requested to start a service, LAN Manager scans the services section of LANMAN.INI for an entry describing where to find the EXE to execute. Each entry under the services section names a service and supplies the location and name of the EXE file to be executed to start the service. [services] . . . service_name = service_exe_path The service_name is the name the service is known by to LAN Manager and the user; with our sample, it's PBX. The service_exe_path describes the path of the EXE file to execute. If the service EXE is in the LAN Manager directory, the pathname can be relative. This is the case for the services shipped with LAN Manager. If the EXE is outside the LAN Manager directory, the full pathname and drive must be supplied. Optionally, a service may take configuration parameters. These are also specified in LANMAN.INI (though the user may override them when starting the service), and should be placed under a section whose heading name is the same as the service. PBX has five configuration parameters; they would be listed in LANMAN.INI as shown in Figure 6. Start-up of a LAN Manager Service A LAN Manager service receives control at the C main function like any other C program. Immediately upon receiving control, a service obtains its own process identifier (PID) and registers with LAN Manager by calling the NetServiceStatus API: PIDINFO pidInfo; // OS/2 PID info struct service_status ssStatus; // LM Service info // Obtain the process ID of this process usRetCode = DosGetPID(&pidInfo); . . . // Inform LAN Manager that this service is in the // process of installation // Because the signal handler is not yet installed, // the service cannot be uninstalled or paused ssStatus.svcs_pid = pidInfo.pid; ssStatus.svcs_status = SERVICE_INSTALL_PENDING | SERVICE_NOT_UNINSTALLABLE | SERVICE_NOT_PAUSABLE; ssStatus.svcs_code=SERVICE_CCP_CODE (PBXINSTALLTIME, 0); usRetCode = NetServiceStatus((char _far *)&ssStatus, sizeof(ssStatus)); The NetServiceStatus API call takes as its primary argument a pointer to a buffer that must contain a service_status structure (see Figure 7). LAN Manager needs the PID of the service to pass control requests to the service. When a request is made to control a service (for example, NET PAUSE PBX), LAN Manager signals the service through the OS/2 signal mechanism, passing a request code in the first byte of the signal argument. NetServiceStatus is also used by the service to pass status or error information to LAN Manager. For example, if your service takes more than a few seconds to install or respond to any LAN Manager request, it should periodically inform LAN Manager that the request is still pending by calling NetServiceStatus. Status and error information is passed by setting the svcs_status and svcs_code fields in the service_status structure. Your service should set the svcs_status field to the bitwise OR of the appropriate values (see Figure 8). The service uses the SERVICE_CCP_CODE macro to set the svcs_code field during installation and while responding to requests from LAN Manager. The first macro argument is the approximate time the service will take to install or complete the request in tenths of a second. The second argument is a checkpoint counter that should be incremented each time the service informs LAN Manager a request is still pending. If an error occurs and your service cannot install, set the svcs_status field to SERVICE_UNINSTALLED and use the SERVICE_UIC_CODE macro to set the svcs_code with the uninstall information code (UIC). Some UICs allow an additional text string to be passed in the svcs_text field. The first argument to the macro is the code and the second is an optional modifier (see Figure 9). When the service is uninstalled, use the SERVICE_UIC_CODE macro to supply LAN Manager with a reason code. For instance, SERVICE_UIC_NORMAL is the proper UIC during a normal uninstall. After registering with LAN Manager, the service should next establish a signal handler for communication with LAN Manager. As mentioned, LAN Manager communicates with a service by sending OS/2 signals. It does this using the OS/2 DosFlagProcess API. OS/2 defines three process flags (that is, signals): PFLG_A, PFLG_B, and PFLG_C. LAN Manager sends requests to a service using the process flag A (PFLG_A) signal. LAN Manager also defines a special constant, SERVICE_RCV_SIG_FLAG, which equates to PFLG_A, that can be used when registering a signal handler to inform OS/2 that the service should receive signals from LAN Manager. After establishing your signal handler, inform OS/2 that your handler will ignore CTRL-C, break, and kill process signals. You want your service to shut down only if requested to by LAN Manager. Additionally, unless they are used by your service for communication, process flags B (PFLG_B) and C (PFLG_C) should be regarded as errors. The InstallSignals function in SIGNALS.C (see Figure 4) demonstrates the proper technique for establishing a signal handler to communicate with LAN Manager. Once your signal handler is in place, your service should call NetServiceStatus again and update LAN Manager with its new status. For instance, once the signal handler is installed, PBX can be uninstalled, though it cannot be paused until installation is complete. Communicating with LAN Manager LAN Manager control requests are passed to your signal handler in the low byte of the signal argument, the first argument passed to an OS/2 signal handler. Again, your handler should respond to these requests quickly or give LAN Manager status hints as your service is responding. Currently, LAN Manager passes one of four arguments (see Figure 10). Values 0 through 127 are reserved, leaving 128 through 255 available for your internal communication. The simplest way to build your signal handler is to use a switch statement with a case for each possible argument. The PBX signal handler, SignalHandler in SIGNALS.C, follows the format shown in Figure 11. The requests SERVICE_CTRL_UNINSTALL and SERVICE_CTRL_INTERROGATE must be supported by your service. When a SERVICE_CTRL_UNINSTALL is received, your service should take whatever steps are appropriate for clean up and exit. Prior to exiting, call NetServiceStatus one last time with the appropriate UIC code to inform LAN Manager that your service has successfully uninstalled. The ExitHandler routine in SIGNALS.C handles all exit requests, error or otherwise, for PBX. In response to a SERVICE_CTRL_INTERROGATE request, your service should call NetServiceStatus with the current state of the service. PBX maintains a global service_status structure that always reflects the current state of PBX and is returned in response to SERVICE_CTRL_INTERROGATE requests. If you allow pausing of your service (as PBX does), you need to process SERVICE_CTRL_PAUSE and SERVICE_CTRL_CONTINUE requests. A simple and efficient method is to use OS/2 semaphores to control child threads. If the semaphore is set, the child thread waits until it is cleared. Initializing a LAN Manager Service Once your service is registered with LAN Manager and your signal handler is in place, you can proceed with initialization. All services should redirect file handles zero, one, and two (corresponding to standard input, standard output, and standard error) to the NUL device. The technique used by the RedirectFiles routine in PBXSRV.C (see Figure 4) is the easiest: it opens the NUL device for I/O and redirects handles zero through two by calling DosDupHandle. After disabling the standard file handles, your service can perform any specific initialization it has. At this point, most services process the configurable parameters that were specified either in LANMAN.INI or by the user when the service was started. LAN Manager passes these parameters to your service as a standard OS/2 argument string. Values explicitly specified by the user when the service was started are passed first, followed by any values from your service's section in LANMAN.INI that were not overridden when starting the service. These values arrive as the standard C argc-argv pair. For example, if PBX was started from the command line as below, NET START PBX /Lines:200 /Connsperthread:10 and LANMAN.INI contained the values listed in Figure 6, PBX would receive the following in argv: argv[0] = Pathname of PBX .EXE file argv[1] = "Lines=200" argv[2] = "Connsperthread=10" argv[3] = "LINEBUFSIZE=1024" argv[4] = "OPENLINES=20" argv[5] = "AUDITING=NO" As you can see, the keywords are not in any specific order or case. LAN Manager preprocesses the keywords to remove blanks and converts the separating colon to an equal-sign (the user may specify either a colon or an equal-sign when entering the keyword and its value). Any text following the equal-sign or colon is treated as the keyword value, including comments, and is passed to your service. If while overriding a keyword in LANMAN.INI the user enters only a part of the keyword name, LAN Manager still passes the full keyword from LANMAN.INI (if it exists). Since explicitly specified keywords always come first, you may want to ignore second and subsequent occurrences of a keyword; this allows the user to override values in LANMAN.INI using short-cuts when starting the service. Making a Service Known Some services, such as PBX, offer a resource that will be used intermittently by clients. In such situations, it benefits the client application if the service can be automatically detected on the network. PBX informs clients of its existence by waiting for broadcasts on a mailslot and then responding with a message containing the name of the computer on which it is executing. A mailslot name looks like a pathname without a drive specification whose root directory is named MAILSLOT. Remote mailslot names are preceded with either the name of an individual computer (both first-class and second-class mailslots) or a logical group of computers called a domain, which is used with second-class mailslots only. A second class message sent to a mailslot with the following name \\*\MAILSLOT\mailslotname sends the message to all machines in the same domain as the sending LAN Manager workstation. It is recommended that the first qualifier in a mailslot name, after the keyword MAILSLOT, be a corporate or product identifier to avoid conflicts with other applications. PBX creates a mailslot with the following name, using MSJ as the qualifier: \mailslot\msj\pbx When a message arrives on its mailslot, PBX responds by sending the name of the computer on which it is executing prefixed with two backslashes, as in the start of a remote name. As a message from the client, PBX expects the computer name on which the client application is executing prefixed with two backslashes. PBX calls the NetWkstaGetInfo function to get the computer name on which it is executing. The call is issued twice, the first time with a zero-length buffer and the second time with the actual buffer size needed. Any LAN Manager call that returns a structure containing pointers to strings or other data (instead of a fixed-length array) returns the structure and the data addressed by the structure in the supplied buffer. Since the buffer size cannot be statically predetermined, you should make the LAN Manager API call twice. On the first call your service would pass a zero-length buffer. LAN Manager will return NERR_BufTooSmall for a return code and the actual number of bytes needed in the appropriate argument (the last argument on the NetWkstaGetInfo call). Next, you allocate a buffer of the required size and make the call again to retrieve the data. If it is necessary to ensure that only a single instance of your service exists in a particular LAN Manager domain, a similar approach may be used. Your service would create a mailslot and wait for messages to arrive, as PBX does; if a message is received, your service would send an appropriate response. To determine if an instance of your service is already executing, your service should send a broadcast message on the mailslot while it is initializing, and briefly wait for a response. If no response is received, your service could assume that it is running alone. Multiple Threads in a LAN Manager Service When writing a service it's best to use a multithreaded design. Since LAN Manager communicates with a service through the OS/2 signal mechanism, your service's main thread will be used to process LAN Manager signals. With simpler services, it won't be a problem if one thread is used for all processing. But in more complex programs, the sudden interruption of processing may leave your service in an unstable state. After initialization is completed, your main application thread should sleep. This is done most efficiently by waiting on a dummy OS/2 semaphore. When its initialization is completed, PBX uses its main thread for processing signals and responding to broadcasts arriving on its mailslot. // Run until PBX is shutdown do { // Wait until a signal is received or a message // arrives on the mailslot (which means a client is /// looking for the PBX) usRetCode = DosReadMailslot( pbPBXMem->hMailslot, // Handle pbPBXMem->pbMSlotBuf, // Buffer &usByteCnt, // Bytes read &usNextCnt, // Next msg size &usNextPriority, // Next msg priority MAILSLOT_NO_TIMEOUT); // Wait forever // If control has been returned because a client // sent a message,return to the client the computer // name of the machine on which PBX is executing if (usRetCode == NERR_Success && usByteCnt != 0 ) { strcat(pbPBXMem->pbMSlotBuf, ANNOUNCELINE); usRetCode = DosWriteMailslot( pbPBXMem->pbMSlotBuf, // Mailslot pbPBXMem->pszPBXMsg, // Message pbPBXMem->usPBXMsgSize, // Message size 9, // Priority 2, // 2nd class 0L); // No wait } } while (usRetCode == ERROR_INTERRUPT || usRetCode == ERROR_SEM_TIMEOUT ); // Control never arrives here; the ExitHandler // always exits PBX return; } Logging Events with LAN Manager To assist the network administrator, your service should log significant events with LAN Manager. LAN Manager provides API support for two event logs, an error event log and a non-error (or audit) log. At the very least, errors that cause your service to shut down should be reported to LAN Manager in the svcs_code field of the service_status structure (as noted above). However, for these and other errors, your service should also log the event with LAN Manager. Whenever a significant error is encountered, PBX writes a message to the LAN Manager error log using NetErrorLogWrite. The second argument passed to this function is the error number. There are more than 120 error numbers. Most are specific to services shipped with LAN Manager, some may be used by your service, and one, NELOG_OEM_Code, was designed for third-party services. Your service can pass NULL-separated substitution strings for the message and the number of substitution strings as the sixth and seventh parameters to NetErrorLogWrite. The NELOG_OEM_Code error message is defined as nine substitutable strings (with no predefined text). It is recommended that the first four strings be the company name, service name, error severity (such as error versus warning), and subidentifier code. The remaining five strings should contain additional data or be initialized to empty. PBX writes all of its error messages to the error log via the ErrorRpt routine in PERROR.C (see Figure 4) using the NELOG_OEM_Code error code. In addition to the recommended data, PBX supplies the filename and line number of the place where the error occurred as part of the message. While this may be unnecessary after you ship your service, it is extremely helpful during debugging when similar errors may occur along different code paths. In addition to logging errors, PBX logs significant, non-error events in the LAN Manager audit log by calling the NetAuditWrite function at the appropriate times. Audit logging is enabled/disabled in PBX by the AUDITING keyword, which defaults to YES. Each record in the LAN Manager audit log is variable length and contains a fixed header, followed by a variable amount of event-specific data, and a 2-byte field. The fixed header contains the total length of the record (the length value at the end of the record is redundant), the type of the record, and the time the record was written. Your service does not build either the fixed header nor the length field at the end of the record. You supply only the type of the audit event and the event-specific data; everything else is built and supplied by the NetAuditWrite function. Microsoft reserves audit event types 0 through 32,767; audit event types in the range 32,768 through 65,535 may be used by third-party applications. To record an audited event, your service calls NetAuditWrite, passing the appropriate audit event type, a pointer to the event specific data, and the length of the data. To simplify the logic of your program, NetAuditWrite returns NERR_Success even if LAN Manager is not currently maintaining an audit log. The user can disable all auditing when starting the server service. If your service does write audit records, you should also supply either a facility to examine the records in the audit log or at least the necessary constants and structures to interpret the records written by your service. The audit event type used by PBX and the structure that maps the variable portion of the PBX audit record is contained in PBXSRV.H (see Figure 4). Debugging a LAN Manager Service Although messages written to the LAN Manager error log can be helpful during development to track problems, it may be even more useful to build a debug version of your service that writes more than just error messages to the error log. Informational messages could be written at critical checkpoints to track flow of control and processing of requests handled by your service. Since a service is a detached OS/2 process, your service cannot write to standard output or standard error to report problems. It also makes it difficult to use tools such as the Microsoft CodeView debugger. Therefore, as you develop your service you may want to consider writing and debugging the bulk of the application prior to converting it to a LAN Manager service. Also, once your application has been converted to a service, instead of redirecting standard output to the NUL device, you could direct it to a disk file and use printf or fprintf to record debugging information. This technique was used very successfully while developing PBX. The next article will concentrate on the portions of PBX that create and manage the named pipes and demonstrate how to build a sophisticated named pipe server. It will explain multiple threads, managing multiple instances of a pipe, client-server race conditions, and offer a brief discussion on the two types of pipes supported by OS/2 and LAN Manager, byte and message mode pipes. Figure 2 Category: Access Permissions Description: Functions to examine or modify resource access permissions C Define: INCL_NETACCESS Category: Alert Description: Functions to notify services and applications of network events C Define: INCL_NETALERT Category: Auditing Description: Functions to access and control the LAN Manager audit log C Define: INCL_NETAUDIT Category: Character Device Description: Functions to control shared communication devices (such as COM ports) and their queues C Define: INCL_NETCHARDEV Category: Configuration Description: Functions to read the LANMAN.INI file C Define: INCL_NETCONFIG Category: Connection Description: Functions to list connections on a LAN Manager server C Define: INCL_NETCONNECTION Category: Domain Description: Functions to retrieve domain information C Define: INCL_NETDOMAIN Category: Error Logging Description: Functions to access and control the LAN Manager error log C Define: INCL_NETERRORLOG Category: File Description: Functions to monitor and close file, device, and pipe resources on a server C Define: INCL_NETFILE Category: Group Description: Functions to control groups of user (part of the LAN Manager user account subsystem) C Define: INCL_NETGROUP Category: Handle Description: Functions to retrieve or set information for character device or named pipe specified by a handle C Define: INCL_NETHANDLE Category: Mailslot Description: Functions supporting LAN Manager mailslots C Define: INCL_NETMAILSLOT Category: Message Descripton: Functions to send and receive messages, and manipulate message aliases C Define: INCL_NETMESSAGE Category: Print Destination Description: Functions to control printers that receive spooled jobs C Define: Uses PMSPL.H or DOSPMSPL.H Category: Print Job Description: Functions to control jobs in a printer queue C Define: Uses PMSPL.H or DOSPMSPL.H Category: Printer Queue Description: Functions to control printer queues C Define: Uses PMSPL.H or DOSPMSPL.H Category: Remote Utility Description: Functions to support remote file copy and move, the execution of remote programs, and accessing time-of-day on a remote server C Define: INCL_NETREMUTIL Category: Server Description: Functions to perform adminstrative tasks on a server C Define: INCL_NETSERVER Category: Service Description: Functions to control LAN Manager services C Define: INCL_NETSERVICE Category: Session Description: Functions to control sessions between workstations and a server C Define: INCL_NETSESSION Category: Share Description: Functions to control shared resources C Define: INCL_NETSHARE Category: Statistics Description: Functions to retrieve or clear statistics for a server or workstation C Define: INCL_NETSTATS Category: Use Description: Functions to examine or control uses between a workstation and a server C Define: INCL_NETUSE Category: User Description: Functions to control a user's account in the user account subsystem C Define: INCL_NETUSER Category: Workstation Description: Functions to control the operation of a workstation C Define: INCL_NETWKSTA Figure 5 ■ OS/2 1.2 PMSPL.LIB Print functions (DosPrintxxx) LAN.LIB All other functions ■ OS/2 1.1 NETSPOOL.LIB Print functions LAN.LIB All other functions ■ DOS 3.1 and up DOSLAN.LIB All functions ■ Windows 3.0 PMSPL.LIB Print functions LAN.LIB All other functions ■ Windows 2.x PMSPL.LIB Print functions LAN.LIB All other functions Figure 6 [pbx] lines = 100 linebufsize = 1024 openlines = 20 connsperthread = 5 auditing = no Figure 7 struct service_status { unsigned short svcs_status; /* Current service state */ unsigned long svcs_code; /* Install/Uninstall code */ unsigned short svcs_pid; /* Process identifier of service */ char svcs_text[64];/* Additional text buffer */ }; Figure 8 ■ General status SERVICE_UNINSTALLED SERVICE_INSTALL_PENDING SERVICE_UNINSTALL_PENDING SERVICE_INSTALLED ■ Paused/Active status SERVICE_ACTIVE SERVICE_CONTINUE_PENDING SERVICE_PAUSE_PENDING SERVICE_PAUSED ■ Uninstallable indication SERVICE_NOT_UNINSTALLABLE SERVICE_UNINSTALLABLE ■ Pausable status SERVICE_NOT_PAUSABLE SERVICE_PAUSABLE Figure 11 // Extract signal argument fSigArg = (UCHAR)(usSigArg & 0x00FF); // And take the appropriate action switch (fSigArg) { // Uninstall PBX case SERVICE_CTRL_UNINSTALL: // Pause PBX case SERVICE_CTRL_PAUSE: // Continue (a paused) PBX case SERVICE_CTRL_CONTINUE: // Return service information // Unrecognized arguments should be treated as // interrogate requests case SERVICE_CTRL_INTERROGATE: default: } // Acknowledge with OS/2 when signal processing is // complete DosSetSigHandler(0, 0, 0, SIGA_ACKNOWLEDGE, usSigNum); Improve Windows Application Memory Use with Subsegment Allocation and Custom Resources Paul Yao Improved memory usage is one of the most important enhancements in the Microsoft Windows graphical environment Version 3.0. Protected mode Windows1 lets you access up to 16Mb of RAM, and in 386 enhanced mode, a virtual address space as large as four times the available physical RAM can be created. On a machine with 16Mb of RAM and sufficient room on the swap disk, a 64Mb virtual address space is possible. This is quite an improvement over the 640Kb limitation that plagued earlier versions. Windows lets you choose among several types of memory. This article explores application memory use in Windows and identifies every place that you can store a byte of data, concluding with sample programs that demonstrate subsegment allocation and the creation of custom resources. Subsegment allocation uses the Windows local heap management routines, such as LocalAlloc and LocalFree, to manage a dynamically allocated segment. Programmers working in OS/2 systems will recognize this as something performed by the DosSubAlloc routine, and OS/2 Presentation Manager programmers may recall that the WinAllocMem routine provides this service. In the second half of this article, I'm going to show how to use a little assembly language programming to access this service in a Windows program. Windows has built-in support for several types of resources: dialog box templates, menu templates, icons, cursors, and so on, as well as custom-written resources. Resources are quite memory-efficient because they are read-only data objects that can be demand-loaded at any time, and can be discarded (purged) from memory when system memory is low. When a resource is needed again, it can be reloaded from disk. I'll demonstrate creating and using custom resources with a sample program. Memory Management Factors Four factors are important when dealing with memory: allocation, visibility, lifetime, and overhead. "Allocation" refers to who sets aside a piece of memory (see Figure 1). In some cases, the compiler allocates memory in response to the declaration of variables. In other cases, memory is allocated in response to calls made to dynamic allocation routines. Windows has two distinct dynamic allocation packages: one to allocate segments, and one to partition segments into smaller pieces. Memory allocation can also occur as a side effect of creating graphical and user interface objects. For example, when you create a pen in the Graphics Device Interface, space is set aside in GDI's local heap. When you create a window or a menu, the Windows user interface manager allocates memory in its own local heap. "Visibility" describes who can access memory. Some objects have a very limited visibility, such as automatic variables declared inside a function. Other objects, such as dynamically allocated segments, are visible systemwide. These objects are the most suitable for sharing data among programs, and are central to the implementation of the Windows Clipboard and Dynamic Data Exchange (DDE) mechanisms. "Lifetime" pertains to the way memory is reclaimed. Generally, when a program terminates, Windows frees the memory the program allocated. Proper reclamation of memory is essential to the health and well-being of Windows as a whole. A few pitfalls require extra care when programming. For example, programs must explicitly free GDI objects and user interface objects such as menus before ending. A future version of Windows will hopefully reclaim memory allocated for these types of objects as well. "Overhead" refers to the extra cost of using a specific type of memory over and above the actual bytes used. It is a factor primarily in the context of dynamically allocated memory. For example, every global memory object has a minimum overhead of 24 bytes. If you are used to allocating many small, dynamic memory objects, you need to rethink your use of dynamic memory. In most cases, arrays of data structures are a more efficient means of storing data in Windows than linked lists, which are popular with C programmers. Since Windows is built on top of the segmented architecture of the Intel-86 family of processors, the following discussion is organized in terms of the segments that are present in the Windows global heap. Default Data Segment Windows uses two types of executable files: application programs and dynamic-link libraries (DLLs). Applications are the active "clients" that directly interact with users. DLLs are the passive "servers" that provide code, data, and device support to applications. Both are referred to as modules. In Windows, every module can have a private data segment, which is sometimes referred to as a default data segment. The C compiler, the linker, and the Windows multitasking switcher together ensure that every module has access to its correct data segment. From an architectural point of view, this means that the processor's data segment (DS) register is set up every time a module boundary is crossed. From a programming point of view, module boundaries can only be crossed by calling exported functions. Exported functions are listed in the EXPORTS section of a module definition file or defined with the _exports pragma. Because of the manner in which Windows maintains the DS register for different modules, you must use either the small or medium compiler model. These memory models, after all, are built for a single data segment. Other compiler models such as compact, large, and huge can be used, but they impose certain restrictions on programs. One reason to use these models is that they support multiple data segments. However, Windows allows only one instance of a program to run if it has multiple data segments. Furthermore, in real mode multiple data segments must be declared FIXED in the program's DEF file. Unfortunately, this causes problems using the local heap, since a fixed heap cannot grow beyond its initial size. This is why most Windows programmers have concluded that these compiler models aren't worth the trouble. In Figure 2, HeapWalker is shown displaying the segments belonging to the Windows Calculator. The highlighted segment is CALC's default data segment. In many respects, a program's default data segment is like any other segment in the global heap: it is allocated with a call to GlobalAlloc and has a maximum size of 64Kb. In other ways, a module's default data segment is different from other segments. For one, it is automatically locked whenever a program receives a message. Unlike most data segments, a program's default data segment is automatically locked, so you don't have to lock it explicitly. Another way in which the default data segment differs from other segments is that it is accessible with near data pointers. That's because the DS register points to the default data segment. When a Windows program calls a DLL routine (such as TextOut), the processor's DS register is set up to point to the library's default data segment. The same thing happens when Windows calls a program's WinMain, or when it calls a WinProc. Handshaking ensures that the DS register is assigned so that it references the module's default data segment. A typical default data segment consists of a header, a static data area, a stack, a local heap, and an optional atom table (see Figure 3). The header is a 16-byte data area containing pointers that Windows uses to manage a default data segment. A Windows program must leave this area alone since it is automatically allocated at compile/link time and managed by Windows at run time. The header contains five pointers. Perhaps the most important is pLocalHeap, which points to the beginning of the local heap in the default data segment. The local heap management routines use this pointer to find the heap. A Windows program should not use it directly to manipulate the heap but instead should call the Windows library routines. Three of the pointers reference the stack: pStackBot, pStackMin, and pStackTop. When running the debug version of Windows, these pointers are used to check for stack overflow. It is a good idea to test your programs in this special version to detect stack overflow and other error conditions. The fifth pointer, pAtomTable, points to a local atom table if the program has created one. An atom table stores variable length strings in a common hash table. An atom is identified by a 2-byte handle, which allows variable length strings to be referenced using a fixed-length 2-byte value. The atom table is itself part of the default data segment's local heap. The header does not contain any pointers to the static data area. The compiler defines the static data area and generates code that correctly accesses it. The Static Data Area The static data area contains the static variables, which C programmers sometimes refer to as global variables. The static variables are those declared outside a function boundary, and all variables declared with the static keyword. In addition, all literal strings are stored as static data objects. In the following code fragment, four objects are allocated in the static data area: char *pchFile; long lLength; long FAR PASCAL WndProc (HWND hwnd, WORD wMsg, WORD wParam, LONG lParam) { static int iCount; PAINTSTRUCT ps; switch (wMsg) { case WM_PAINT: BeginPaint (hwnd, &ps); TextOut (ps.hdc, 10, 10, "Windows", 7); EndPaint (hwnd, &ps); The two variables defined outside the procedure, pchFile and lLength, are allocated in the static data area, as you would expect. So is iCount, which uses the static keyword. This keyword makes iCount visible only within this routine. But as a static object, it has a lifetime as long as the program itself and therefore it resides in the static data area. The fourth object that is allocated in the static data area is the string "Windows". Every literal string gets its own data area even if two strings are the same. If a string is going to be used in several places, it is a good idea to define it once as an array and then reference the array by name. Even better, use a string table to minimize the impact of literal strings on the size of the static data area. A string table is a resource that keeps strings on disk until they are needed. When needed, a string is read from disk into its own discardable data segment. This way, when your program is finished using them, string resources can be purged from system memory. The Stack The stack is a dynamic data area that is managed by high-level languages like C. The stack is so important that 80x86 processors have a set of registers dedicated to its support and maintenance: the SS (stack segment) register, the BP (base pointer) register, and the SP (stack pointer) register. When the call machine instruction is executed, a return address is automatically pushed onto the stack by the CPU. When the return (ret) instruction is executed, the return address is popped from the stack to determine the instruction to which control is to be passed. The C compiler and the CPU store three things on the stack: automatic or "local" variables, arguments to called functions, and return addresses. The STACKSIZE statement in a program's DEF file sets the size of a stack, with a minimum stack of 5Kb allocated by the Windows loader. Automatic variables are allocated on entry into a function and freed upon exit. All variables declared inside the boundaries of a function without the static keyword are automatic variables. In the code fragment shown earlier, the PAINTSTRUCT variable ps is an automatic variable. If a program uses a lot of automatic variables, the STACKSIZE may need to be increased to reflect this. Figure 4 shows a function being called in C, the corresponding assembly language instructions that are generated, and the arrangement of the stack after a function has been called and the stack set up. Each push instruction copies two bytes to the stack. In this case, the called function, y, takes three integer parameters, so three push instructions are generated to put these parameters in place. The call instruction places a return address on the stack, then passes control to the called function. Inside the called function, the compiler creates code to adjust the BP register to establish the stack frame. The base pointer then serves as the anchor point from which parameters can be referenced as positive offsets from the base pointer. For example, this assembly language instruction places the third parameter into AX. mov ax, [bp+04] If there are local variables, the compiler creates code to adjust the SP register to reserve space on the stack for them. This is done by the following assembly language instruction, which is found in Figure 4: sub sp,6 Local variables are accessed as negative offsets from the base pointer. For example, this instruction copies the third local variable in the example into the AX register. mov ax, [bp-06] Though the workings of the stack are somewhat esoteric and complex, the C compiler usually insulates you from needing to understand every detail of its operation. Hopefully this brief explanation has clarified the role of the stack, and therefore the rationale behind the size that you specify in the STACKSIZE statement. The Local Heap The local heap provides a private area in the default data segment from which memory can be dynamically allocated. Every program's default data segment has a local heap, which is automatically set up by the program loader. A local heap is set up by calling the LocalInit routine. Allocating and using memory in a local heap involves making calls to the local heap management routines (see Figure 5). Windows local heap management routines support three types of memory objects: fixed, moveable, and discardable. These are the same types that can be allocated on the global heap. Windows local heap routines are much more sophisticated than the C malloc routine, which allocates only fixed objects. To support moveable and discardable objects, the local heap manager uses a handle-based memory allocation scheme. In other words, when a call is made to the local heap allocation routine LocalAlloc, it returns a memory handle, not a pointer. A memory handle is an identifier. By hiding the location of the memory object, the local heap manager can move or discard objects when necessary to satisfy allocation requests. To access a local memory object, you lock the object by calling LocalLock. This routine does two things: it increments the lock count of an object and it returns a pointer to the object. You can keep an object locked for as long as you need to access the object. However, since locking prevents the local heap from being reorganized, it's often a good idea to keep a lock on for a very short time, perhaps only for the duration of a single message. Remove a lock by calling LocalUnlock. To free a memory object that has been allocated on the local heap, you must first unlock it, and then call LocalFree. The following code fragment demonstrates allocating a memory object and copying a string into the object. HANDLE hMem; PSTR pstr; /* Allocate a 15-byte moveable object. */ hMem = LocalAlloc (LMEM_MOVEABLE, 15); /* Lock the object, getting a pointer. */ pstr = LocalLock (hMem); if (!pstr) return (ERROR); lstrcpy (pstr, "Hello World"); /* Unlock the object. */ LocalUnlock (hMem); As you can see, error checking is important when you allocate memory. You have no guarantee that the system will be able to satisfy your allocation request. And if you write to the NULL pointer returned by LocalLock when it fails, you overwrite the bytes at the bottom of the data segment. Remember, the segment header is located at the base of your default data segment. Overwriting it could be disastrous. Local Atom Table A program can create an atom table in its default data segment with a call to InitAtomTable. This routine creates an atom table in the local heap. An atom table efficiently stores variable-length strings. Once stored, a 2-byte atom is returned, which is in some ways analogous to a handle. When a duplicate string is added to an atom table, the same atom value is returned, minimizing the duplication of variable-length strings. In addition to the local atom table, Windows provides a global atom table. DDE uses the global atom table to pass the names of data topics between programs. Dynamically Allocated Segments Dynamically allocated segments are the most flexible type of read/write memory for a program. Up to the limit of available system memory, a program can have as many dynamically allocated segments as it wants, and each allocated segment can be as large as 64Kb. In fact, you can allocate segments that are larger than 64Kb, although that is beyond the scope of this article. Windows 3.0 has a systemwide limit of 8192 segments in real mode and in 386 enhanced mode. In standard mode, the limit is 4096 segments. The standard mode limitation reflects the fact that two entries in the system handle table, also known as the Local Descriptor Table (LDT), are required for each segment. One entry is for the allocated segment and the other is for a 16-byte header that is attached to every allocated segment. Because real mode and 386 enhanced mode do not require this extra handle table entry, they are able to support twice the number of segments as standard mode. In a future version of Windows, the limit to standard mode segments should eventually be raised to 8192. This limit will change from a systemwide limit to a per-task limit for both standard mode and 386 enhanced mode. Windows will then have one LDT per task instead of the current implementation of one LDT for the entire system in protected mode. Programs allocate and manage dynamic segments using Windows global heap management routines (see Figure 6). Where applicable, routines in Figure 6 are paired as a "top slice" and a "bottom slice" in a construction I call the Windows sandwich. My Windows sandwich has three parts: two outside pieces of bread that hold the third part, the filling (see Figure 7). The first piece of bread represents the code that borrows a system resource. The second piece of bread represents the code that relinquishes the resource. The filling represents code with which a program uses the resource. The idea is that the filling is always used inside the sandwich, never by itself. The most common type of sandwich is probably the following, a standard response to a WM_PAINT message: case WM_PAINT: { PAINTSTRUCT ps; BeginPaint (hwnd, &ps); // top slice TextOut (ps.hdc, x, y, "Hi", 2); // filling EndPaint (hwnd, &ps); // bottom slice . . . Segment allocation mirrors local heap allocation in that both use a handle-based memory management scheme. A call to GlobalAlloc returns a handle, which identifies the memory but does not reveal its location. Segments can be allocated as fixed, moveable, or discardable. To determine the attributes of a specific segment, you can run HeapWalker and look for objects that are identified as type Task. Using Figure 8 as a guide, the three types of memory attributes can be easily identified. Note that a discardable segment is also a moveable segment. To access a global memory object, a call must be made to GlobalLock. Like its local heap counterpart, LocalLock, GlobalLock increments a memory object's lock count and returns a pointer to the object. Again, it is a good idea to keep a lock only as long as an object is being used, which in most cases means for the duration of a message. Unlocking an object involves making a call to GlobalUnlock, which decrements the lock count on an object. The following code fragment allocates a global memory object and copies a string into the allocated memory. HANDLE hMem; LPSTR lpstr; /* Allocate a 15-byte moveable object. */ hMem = GlobalAlloc (GMEM_MOVEABLE, 15); /* Lock the object, getting a pointer. */ lpstr = GlobalLock (hMem); if (!lpstr) return (ERROR); lstrcpy (lpstr, "Hello World"); /* Unlock the object. */ GlobalUnlock (hMem); Error checking is just as important when using global heap objects. There is no guarantee that Windows will be able to satisfy all of your requests for memory, so you should plan accordingly. The above code fragment checks the return value of GlobalLock; many Windows programmers also check for a valid (nonzero) return value from GlobalAlloc. For a program to run properly in all operating modes, it's a good idea to keep global memory objects only for the duration of a single message. However, if a program is to run solely in protected mode, it can take a shortcut that is unthinkable in real mode. It can lock global memory objects once, upon allocation, and keep them locked for as long as the object needs to be in memory. This can be fatal in real mode because global memory objects are locked in the physical address space and quickly create memory sandbars. In protected mode, however, such objects are not locked in the physical address space. These objects can be moved unbeknownst to the application program. Resources A resource is a read-only data object that has been merged into a module's executable (DLL, DRV, FON, or EXE) file by the resource compiler. When the data is needed, it can be read into a discardable segment. When the memory manager needs to use that memory for other purposes, it can discard or purge the segment containing the resource. Locating a resource with HeapWalker is easy, since resources are labeled clearly. HeapWalker knows if a particular resource is a menu template, dialog box template, or whatever. HeapWalker can even show the bitmap associated with a particular resource (see Figure 9). Windows has built-in support for accelerator tables, bitmaps, cursors, dialog box templates, fonts, icons, menu templates, and string tables. These resources are used to support several Windows user-interface objects: menus and dialog boxes, to name two. In addition, several GDI objects are stored as resources: fonts and bitmaps, for example. Each resource resides in a separate segment. A program with one menu template, three dialog box templates, and two cursors has six segments' worth of resources. Each resource is put in its own segment so that each can be loaded and discarded individually. If you are writing a Windows program that has a fairly large block of read-only data, you should consider putting the data into a resource. If your data does not fit easily into one of the predefined resource types, it's simple to create a custom resource. I will demonstrate this in the CUSTRES program. GDI Data Segment Windows programs can call GDI routines that cause objects to be allocated in GDI's default data segment. Whenever a call is made to a GDI routine that contains the word Create (such as CreateDC, CreatePen, CreateFontIndirect), you know that the routine allocates memory in GDI's local heap (see Figure 10). Fonts and bitmaps also cause memory to be allocated from the global heap. In general, if a program creates an object, it should make sure that it deletes the object. In all versions of Windows, GDI expects every program to clean up after itself. If a program forgets to delete an object, the allocated memory is lost to the system. GDI was designed this way to allow programs and instances of programs to share GDI drawing objects. For example, the first instance of a program could create the GDI drawing objects to be used by all instances. When subsequent instances start up, they access the existing drawing objects either by sending messages or calling the GetInstanceData routine, which copies the value of static data objects from one instance's data segment to another instance's data segment. Unfortunately, this design has resulted in a problem. If a program does not free a GDI object when it terminates, the memory is lost forever. For this reason, in a future version of Windows, Microsoft should consider changing GDI so that, upon termination of a program, all GDI objects created by the program are automatically destroyed. Until that time, you should ensure that your program explicitly deletes all GDI objects that it has created (and that it has not passed to other programs on the Clipboard). USER Data Segment The Windows USER module supports Windows user-interface objects, such as windows, menus, dialog boxes, cursors, carets, and accelerator tables. Unlike GDI objects, USER objects are not designed to be shared between programs. For the most part, when a program terminates, USER frees the memory that has been allocated for the user interface objects. Nevertheless, you should be aware of the ways that a Windows program can cause memory to be allocated in USER's local heap, because it is a limited resource that is shared by all applications. Quite a few user interface objects are stored in their own segments. In fact, this is true for all user interface objects that are created as resources. Others are allocated in USER's local heap, as shown in Figure 11. While these objects are small, there are a few "gotchas" to avoid. If you create several different menus and attach some of them to a window using SetMenu, you have to be careful when your program terminates. Menus that are connected to a window are automatically freed. Menus that are not connected to a window, however, must be explicitly freed by your program when it terminates. A second "gotcha" occurs when you register a window class. As you may recall, class registration involves filling in the values of a structure of type WNDCLASS and passing a pointer to the RegisterClass routine. Make sure that you fill in all elements of the WNDCLASS structure. If you don't fill in the values of two fields, cbWndExtra and cbClassExtra, you may be in for a surprise. These values define the amount of extra bytes to be allocated in USER's data segment: cbWndExtra defines the extra bytes to be allocated for each window, and cbClassExtra defines the extra bytes to be allocated for the class. If you forget to initialize these fields, the USER module allocates extra bytes using whatever values it finds in the data structure. Of course, you may wish to use the often-overlooked window or class extra bytes. They may be accessed via the functions found in Figure 12. They can be used to connect a window efficiently to its data. For example, two window extra bytes might be allocated to hold a memory handle for the data to be displayed in a window. Or two class extra bytes could be allocated for GDI drawing objects shared by the windows in a class. Window extra bytes can be very useful when creating custom dialog box controls, or for creating MDI windows. In general, if you are creating a window class that will support multiple windows in a single application, window extra bytes provide an easy way to connect a window's data to the window itself. Subsegment Allocation As already discussed, there are two dynamic memory allocation packages in Windows: local heap allocation and global memory allocation. By default, every Windows program has a local heap. The heap is created by a routine called InitApp, which is undocumented but is part of the standard start-up sequence of every program. One advantage of the local heap is that the overhead for objects is low (4 to 6 bytes), and with a granularity of 4 bytes, waste is kept to a minimum. The only problem with the local heap is that it is too small for many uses. Depending on the size of your stack and the static data, the room remaining for the largest default local heap might be 30Kb--50Kb. This problem can be solved by using the global heap. On 386 systems, this implies taking advantage of disk swap space in addition to physical RAM. With the global heap, however, the overhead per object is high. Also, at a minimum of 32 bytes per segment, the granularity of segments is too high to be used for very small objects. Only large objects or arrays of small objects are suitable for storage in objects allocated from the global heap. To get the benefits of both local and global heap management, it's possible to create a local heap in a dynamically allocated global segment. You can allocate small objects from this heap, all managed by the local heap manager, that share the segment efficiently with other objects. To do this, keep in mind that the first 16 bytes of a segment are reserved for the use of the local heap manager. At offset 06H, it stores a pointer to the local heap. This allows the local heap to sit at the end of an application's default data segment, after its static data and stack. A second concern involves local heap initialization, which is accomplished by calling LocalInit. This code fragment allocates a segment and initializes a local heap in it. HANDLE hMem; int pStart, pEnd; LPSTR lp; WORD wSeg; hMem = GlobalAlloc (GMEM_MOVEABLE, 4096L); if (!hMem) goto ErrorOut; lp = GlobalLock (hMem); wSeg = HIWORD (lp); pStart = 16; pEnd = (int)GlobalSize (hMem)-1; LocalInit (wSeg, pStart, pEnd); GlobalUnlock(hMem); GlobalUnlock(hMem); Notice the two calls to GlobalUnlock. The first counteracts the call to GlobalLock; the second is needed because LocalInit leaves a segment locked. Without the second call, the data segment would still be locked. In protected mode, this doesn't present a problem, but segments that are unnecessarily locked can create memory sandbars in real mode. As always, GlobalAlloc's return value should be checked to determine whether the requested memory is available. Even though you asked for a 4096-byte segment, because different operating modes align on different segment boundaries, call GlobalSize to make sure you know the exact size of the segment. To make room for the header, pStart is set to 16. Set pEnd to the offset of the last byte in the segment, which is the segment size minus one. Another way to do this is to set pStart to zero, and set pEnd to the actual size of the local heap. pEnd = (int)GlobalSize(hMem)-16; LocalInit(wSeg, 0, pEnd); You subtract 16 from the size of the segment to set aside space for the segment's header. Accessing the local heap requires some programming in assembly language because the segment selector of the heap must be placed in DS. Any of the local heap management routines may then be called to operate on the local heap. When the DS register is pointing at a heap segment, don't try referencing any static variables. You won't be able to access them, but you will overwrite some of the heap segment instead. If you are using Microsoft C Version 6.0, you can use the _asm pragma to embed assembly language in your C code. The following saves DS on the stack, sets up the DS register to point to the heap segment, allocates memory from the heap, and restores DS to point to the default data segment. LPSTR lp; HANDLE hmem; WORD wHeapDS; /* Must be a stack variable! */ lp = GlobalLock (hmem); /* Where local heap lives */ wHeapDS = HIWORD (lp); _asm{ push DS mov AX, wHeapDS mov DS, AX } hmem = LocalAlloc (LMEM_MOVEABLE, 16); _asm{ pop DS } GlobalUnlock (hmem); Using a local heap in a dynamically allocated segment requires calls to two routines, one for the segment and one for the local heap object. To obtain a pointer to the allocated memory, calls to GlobalLock and LocalLock are required. And to prevent fragmentation in the global heap or the local heap, you'll probably want to unlock at both levels. There are other methods, of course. A program can make all local heap objects fixed, removing the need to perform the second lock. To be most effective, it makes sense to build a small subroutine library to manage the two-level allocation scheme. This might be as simple as creating your own 32-bit handles, using half for the local handle and half for the global handle. This is the approach used in the sample program, SUBSEG (see Figure 13). Another alternative is for a subroutine package to issue its own private 16-bit handles that reference a table of segments and local memory objects. SUBSEG The SUBSEG program demonstrates subsegment allocation in a dynamically allocated segment. I borrow the term subsegment allocation from OS/2, since it describes this procedure better than the term local allocation. This program contains a set of subsegment allocation routines that mirror the Windows standard memory allocation routines. A routine called SubAlloc takes the same parameters as LocalAlloc. Four other routines provide the basic allocation services. Another function, SubInitialize, allocates and initializes the dynamically allocated segment. SUBSEG displays information about the allocated data objects (see Figure 14). To convince you that it works as advertised, it reads this information from the object itself. As you can see, the actual size of objects is a little larger than requested, reflecting the 4-byte granularity of the local heap manager. Unlike the handles generated by the local and global heap managers, the handles issued by the memory management routines in SUBMEM.C are 32-bit. The HIWORD contains a global memory handle, and the LOWORD contains a local memory handle. You may want to devise your own scheme for identifying memory objects, but for many purposes this is fast and simple enough. If you've examined the code, you may have noticed that the segment allocated by SUBSEG is never freed. Well, do as I say, not as I do. In other words, please be sure to free any memory you allocate, unlock any memory you lock, and in general undo whatever needs undoing to free any resource you use. Custom Resources Custom resources, as I said earlier, let you exploit the built-in memory management features of resources with a minimum of effort. The best custom resources are data objects that won't change. The sample resource I'll demonstrate contains a table for determining sine and cosine values. This look-up table supplies an integer sine value, which is simply a sine value multiplied by 10,000. Look-up tables are commonly used because they are often faster than on-the-fly calculations. Also, because 80386 and earlier Intel processors do not have built-in floating point support, you'll get faster performance if you limit your calculations to integer arithmetic. (By the way, this fact influenced Microsoft enough to build Windows without using any floating point arithmetic.) CUSTRES contains two routines that calculate sine and cosine values for an angle in measured degrees. With two functions and 360 degrees, there are 720 different values required for the look-up table. By taking advantage of the symmetry of sines and cosines and doing a little folding and rotating, I produce the same results with a single table of 91 sine values. I obtained the table by writing a C program that calculates sine values and writes them as ASCII text to a data file. Why ASCII text? I'm going to show you a trick that lets you build complex binary data objects from ASCII text files. The only tools required are a macro assembler, the linker, and a special converter called EXE2BIN.EXE that comes with DOS. Figure 15 contains the program files that create the custom resource. The data file created by this program is an assembly language file containing data definitions but no machine code. Instead, I use the macro assembler to convert the data definitions into a binary format. The assembly language file that SINE.EXE creates is SINEDATA.ASM (see Figure 16). After the data file SINEDATA.ASM has been run through the macro assembler and the linker, you have a "ready-to-run" EXE file that contains no code. Next, EXE2BIN is used to isolate the data into a pure binary object; this program is ordinarily used to create COM files from EXE files. A COM file is simply a memory image that can be loaded and run as is. Since that's exactly what you want--a pure, binary image--EXE2BIN does the trick to create the sine table resource. To test the sine and cosine functions, CUSTRES connects 360 points to draw a circle with a radius of 100 pixels (see Figure 17). While this is slower and rougher than you would expect from a call to GDI's Ellipse routine, it does show that the generated sine and cosine values at least look right in the range 0 to 360 degrees. CUSTRES (see Figure 18) does all its work during the WM_CREATE, WM_PAINT, and WM_DESTROY messages. All of the sine and cosine information is contained in two routines, intSin and intCos. The second function actually cheats; since a cosine is always 90 degrees out of phase with a sine, the intCos function subtracts 90 degrees from the actual angle and calls the intSin function. Using a custom resource requires the FindResource, LoadResource, and LockResource routines. The first two are called in response to the WM_CREATE message. The result is a memory handle stored in hresSinData. FindResource searches for the reference to a resource in the module database, which is simply an abbreviated memory image of the module's file header. FindResource takes three parameters: FindResource (hInstance, lpName, lpType) The first, hInstance, is an instance handle. The second, lpName, is a long pointer to a character string with the resource name. The third, lpType, is a long pointer to a character string with the resource type. Even though lpName and lpType are pointers to character strings, this is not the most efficient way to identify a resource, since a string comparison is more "expensive" than an integer comparison. Because of this, I use a macro, MAKEINTRESOURCE, which lets me define integers and use them in place of a character string. Following are the two integers defined in CUSTRES: #define TABLE 100 /* Custom resource type. */ #define SINE 100 /* ID of sine table. */ These integers are used in the call to FindResource, as follows: hRes = FindResource (hInst, MAKEINTRESOURCE(SIN), /* Name. */ MAKEINTRESOURCE(TABLE)); /* Type. */ The MAKEINTRESOURCE macro creates a pseudo-pointer, with 0 for a segment identifier and the integer value for the offset value. It casts this value as an LPSTR. When the FindResource routine sees this value, it does not treat it as a pointer (this would be a fatal error). Instead it uses the 2-byte integer value to find the resource definition, using the following line in the resource file. SIN TABLE sinedata.bin DISCARDABLE This line causes the data in the resource file, SINEDATA.BIN, to be copied entirely into CUSTRES.EXE at compile/link time. This means that CUSTRES is a standalone program and doesn't need the original resource data file to be present at run time. Once FindResource has provided a resource identifier, a call to LoadResource provides a global memory handle for the resource itself. LoadResource, the next routine called, is defined as follows: LoadResource (hInstance, hresInfo) Its first parameter, hInstance, is the instance handle; the second parameter, hresInfo, is the handle returned by the FindResource routine. In spite of its name, LoadResource does not load a resource. It allocates a global memory object with a size of 0. No memory is set aside; only a global memory handle is assigned. CUSTRES stores this value in hresSinData. The routine that causes a resource to be loaded into memory is LockResource. CUSTRES calls this routine only when it needs to access the sine table data. By postponing the loading of such a memory object, CUSTRES helps minimize the demands it makes on system memory. LockResource itself performs several tasks: it loads the resource into memory, locks it in place, and returns a pointer to the data. LockResource is defined as follows. LPSTR LockResource (hResData) hResData is the handle returned by the LoadResource function. LockResource returns a long pointer to a string. CUSTRES casts this return value to a long pointer to integers. int FAR * fpSin; fpSin = (int FAR *)LockResource (hresSinData); if (fpSin == NULL) return (0); Casting prevents the compiler from complaining about a type-mismatch error. Notice that a check is made on the return value from LockResource, in case something (like insufficient memory) prevented the resource from being loaded. The LockResource routine should always be used with UnlockResource (remember the sandwich?). This construction was discussed earlier as a way to organize the use of a shared resource, in this case memory. LockResource loads a resource and ties it down in memory. UnlockResource unties the resource to allow it to move, or to be discarded. In CUSTRES, the intSin function uses these two routines to bracket its use of the sine data, creating a Windows sandwich that ensures that the object is locked when you need it, and unlocked when not. UnlockResource is defined as follows. hResData is the handle returned by the LoadResource function. BOOL UnlockResource (hResData) The final routine involved with handling custom resources is FreeResource, which frees the memory associated with a custom resource. Again, hResData is the handle returned by the LoadResource function. FreeResource (hResData) CUSTRES calls FreeResource in response to the WM_DESTROY message to deallocate the sine resource. This program doesn't actually need to call FreeResource, since the resource will be freed when CUSTRES terminates, but good programmers clean up after themselves. This program draws using the default MM_TEXT mapping mode. The circle in Figure 17 is perfectly round only if the program is displayed on a VGA monitor with a 1:1 aspect ratio. CUSTRES moves the origin of the logical coordinate system to the middle of the window with the following code: GetClientRect (hwnd, &r); SetViewportOrg (ps.hdc, r.right/2, r.bottom/2); Conclusion As a Windows programmer, you have many options for allocating and working with memory. Knowing how to exploit the different ways of packaging data in Windows should help you write programs that take better advantage of what the system has to offer. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers to this Microsoft product and is not intended to refer to such products generally. Figure 5. Local Heap Management Routines ■ LocalAlloc Allocates memory from a local heap. ■ LocalCompact Reorganizes a local heap. ■ LocalDiscard Discards an unlocked, discardable object. ■ LocalFlags Provides information about a specific memory object. ■ LocalFree Frees a local memory object. ■ LocalHandle Provides the handle of a local memory object associated with a given memory address. ■ LocalInit Initializes a local heap. ■ LocalLock Increments the lock count on a local memory object and returns its address. ■ LocalReAlloc Changes the size of a local memory object. ■ LocalShrink Reorganizes a local heap and reduces the size of the heap (if possible) to the initial, starting size. If this routine is successful, it reduces the size of the data segment that contains the heap, so that the memory can be reclaimed by the global heap. ■ LocalSize Returns the current size of a local memory object. ■ LocalUnlock Decrements the lock count on a local memory object. Figure 10. Memory Used in GDI's Local Heap Object Size ■ Bitmap 28--32 bytes ■ Brush 32 bytes ■ Device context 100 bytes per device for fixed overhead + 200 bytes per DC allocated ■ Font 40--44 bytes ■ Palette 28 bytes ■ Pen 28 bytes ■ Region 28--104 bytes Figure 11. Memory Used in USER's Local Heap Object Size ■ Menu 20 bytes per menu +20 bytes per menu item ■ Window class 40--50 bytes ■ Window 6070 bytes Figure 12. Memory Used in USER's Local Heap Window Extra Bytes Routines Description ■ SetWindowWord Copies two bytes into window extra bytes ■ SetWindowLong Copies four bytes into window extra bytes ■ GetWindowWord Retrieves two bytes from window extra bytes ■ GetWindowLong Retrieves four bytes from window extra bytes Class Extra Bytes Routine Description ■ SetClassWord Copies two bytes into class extra bytes ■ SetClassLong Copies four bytes into class extra bytes ■ GetClassWord Retrieves two bytes from class extra bytes ■ GetClassLong Retrieves four bytes from class extra bytes Figure 15. SINE SINE.MAK sine.obj: sine.c cl -c sine.c sine.exe: sine.obj link sine; sinedata.asm: sine.exe sine sinedata.bin: sinedata.asm masm sinedata.asm, sinedata.obj; link sinedata, sinedata.exe; exe2bin sinedata.exe SINE.C /*-------------------------------------------------------------*\ | SINE.C - Creates an .ASM data file containing sine values | | from 0 to 90 degrees. This file is suitable | | for creating a custom Windows resource. | \*-------------------------------------------------------------*/ #include "stdio.h" #include "math.h" char achFileHeader[] = ";\n" "; Sine/Cosine Data Table\n" ";\n" ";\n" "; Table of Sine values from 0 to 90 degrees\n" ";\n" "SINDATA segment public\n"; char achFileFooter[] = "\n" "SINDATA ends\n" "END\n"; main() { double dbPI = 3.1415926536; double dbRad; FILE * fp; int iAngle; int iSin; if (!(fp = fopen("sinedata.asm", "w"))) { printf("Can't create sinedata.asm.\n"); exit(1); } fprintf (fp, achFileHeader); fprintf (fp, "DW "); for (iAngle = 0; iAngle <= 90; iAngle++) { dbRad = (((double)iAngle) * dbPI) / 180.0; iSin = sin(dbRad) * 10000.0 + 0.5; fprintf(fp, " %5d", iSin); if (iAngle % 8 == 7) fprintf (fp, "\nDW "); else if (iAngle != 90) fprintf (fp, ","); } fprintf(fp, achFileFooter); fclose(fp); } Figure 16. SINEDATA.ASM, Generated by SINE.EXE ; ; Sine/Cosine Data Table ; ; ; Table of Sine values from 0 to 90 degrees ; SINDATA segment public DW 0, 175, 349, 523, 698, 872, 1045, 1219 DW 1392, 1564, 1736, 1908, 2079, 2250, 2419, 2588 DW 2756, 2924, 3090, 3256, 3420, 3584, 3746, 3907 DW 4067, 4226, 4384, 4540, 4695, 4848, 5000, 5150 DW 5299, 5446, 5592, 5736, 5878, 6018, 6157, 6293 DW 6428, 6561, 6691, 6820, 6947, 7071, 7193, 7314 DW 7431, 7547, 7660, 7771, 7880, 7986, 8090, 8192 DW 8290, 8387, 8480, 8572, 8660, 8746, 8829, 8910 DW 8988, 9063, 9135, 9205, 9272, 9336, 9397, 9455 DW 9511, 9563, 9613, 9659, 9703, 9744, 9781, 9816 DW 9848, 9877, 9903, 9925, 9945, 9962, 9976, 9986 DW 9994, 9998, 10000 SINDATA ends END Learning Windows Part IV: Integrating Controls and Dialog Boxes Marc Adler Dialog boxes and the control windows within them are an integral part of the Microsoft Windows graphical environment. The topic is broad enough to devote two articles, both the previous article in this "Learning Windows" series as well as this one, entirely to it. First, I conclude the discussion of controls with a look at combo boxes, scroll bars, and user-defined controls, then I place the controls into their proper setting: a dialog box. Finally, adding dialog boxes to the sample stock quoting application gives it most of its functionality. Combo Boxes The combo box, which was introduced in Windows1 Version 3.0, combines a single-line edit control and a list box into one control window. It can be used to enter a value in an edit field or to choose one string from a predefined list of text strings. A combo box can replace a series of radio buttons in a dialog box, taking up less space. In a word processing application, you might want to enter the type size, in points, of the font you will be using. A combo box could display a list of existing point sizes and permit you to type in another number. A radio button group could be used for this task, but if there were many point sizes, the radio button group would probably take too much room in the dialog box. In addition, the number of choices in a combo box can vary throughout the lifetime of an application, but a radio button group generally has a fixed number of choices. To add another choice to a combo box control, all the developer has to do is append a text string to a combo box's list box; adding another option to a radio group probably means creating a new radio button and rearranging the entire group. The first element of a combo box is a single-line edit control. A button control to the right of the edit field is the second element, and the third element is a list box control just below the edit control. As you scroll through the list box, the current selection is displayed in the edit control. A list box control in a combo box has the standard Windows 3.0 list box features, including possible owner-draw items. Windows 3.0 supports three styles of combo boxes. The simple combo box style, which is the least used, has the CBS_SIMPLE style flag. In a simple combo box, the list box is always visible. This kind of combo box has no advantage over defining a separate edit control and list box control, except that you don't have to code the list-box-to-edit tracking logic. The other two combo box styles are drop-down (see Figure 1) and drop-down list, defined by the CBS_DROPDOWN and the CBS_DROPDOWNLIST styles. The only difference between them is that the edit control is disabled in the drop-down list combo box, so users cannot enter their own values in the edit field. This is useful if you want to make users choose from a list of predefined values only. The nice thing about the drop-down and drop-down list styles is that the list box is hidden until you pull it down. You can pull it down either by clicking on the control's button or by pressing the Alt-Down key combination. You can use the Up, Down, Page Up and Page Down keys while the focus is on the control to scroll through the list box items. The messages and notification codes that are processed by the combo box message interface are similar to those used by the standard list box and edit controls. The only difference is that the prefixes CB_ or CBN_ are used instead of the EM_, EN_, LB_, and LBN_ prefixes. The single unique message, CB_SHOWDROPDOWN, is used to display or hide the list box portion of a drop-down or drop-down list combo box. Just before the list box portion is made visible, the CBN_DROPDOWN notification code is sent to the parent of the combo box. This gives the programmer a chance to modify the contents of the list box before it is shown to the user. Scroll Bars Many Windows applications use a window as a "viewport" into some sort of large data set. A list box window is a viewport into a series of strings, a word processing document window is a viewport into a portion of the data file, and so on. Scroll bars are the primary means for the user to inform the application that he or she wants to move the viewport to a new position in the data set. A scroll bar control can be either vertical or horizontal. It has two primary attributes: the range of values that the length of the scroll bar represents, and the current position in the range. The current position is indicated with a square icon called a thumb. Scroll bars are usually attached to a window when it is created. To attach a vertical scroll bar to a window, you simply give the window the WS_VSCROLL style during creation. Similarly, the WS_HSCROLL style attaches a horizontal scroll bar to the bottom of the window. Scroll bars can also be used as standalone control windows. A good example can be seen in the Edit Colors dialog box of Windows Paintbrush (see Figure 2). A standalone scroll bar control is created in the same way as any other kind of control window--using CreateWindow or defining the control as part of a dialog box in a resource file. Vertical control scroll bars have the SBS_VERT style, and horizontal scroll bars have the SBS_HORZ style. Windows API functions allow you to query and set the current position and the range of the scroll bar. The five scroll bar functions are as follows: int GetScrollPos(hWnd, nBar) void GetScrollRange(hWnd, nBar, lpMin, lpMax) int SetScrollPos(hWnd, nBar, iPos, bRedraw) void SetScrollRange(hWnd, nBar, nMin, mMax, bRedraw) void ShowScroll bar(hWnd, nBar, bShow) The first parameter, hWnd, can be either the handle of a scroll bar (when dealing with a control scroll bar), or the handle of the window that contains the scroll bar. The second parameter, nBar, details which scroll bar you want. If nBar is SB_CTL, the hWnd parameter is the handle of a control scroll bar. If nBar is SB_VERT, hWnd is the handle of a window containing the vertical scroll bar; if nBar is SB_HORZ, hWnd is the handle of a window with a horizontal scroll bar. Scroll bars, unlike other control windows, do not generate WM_COMMAND messages when the parent window needs to be notified of some event. Instead, they generate WM_VSCROLL and WM_HSCROLL messages. WM_VSCROLL messages are sent by vertical scroll bars, and WM_HSCROLL messages are sent by horizontal scroll bars. The notification codes for these messages are the same, and are passed in wParam (see Figure 3). If the message was generated by a control scroll bar, the handle of the scroll bar is passed in the HIWORD of the lParam (otherwise, the HIWORD of lParam is 0). The LOWORD of the lParam contains the current thumb position for two of these notification messages. When the top arrow of a vertical scroll bar or the left arrow of a horizontal scroll bar is clicked, the SB_LINEUP notification code is sent to the parent window. Clicking on the down arrow or the right arrow generates an SB_LINEDOWN code. Clicking in the area between the top or left side of the scroll bar and the thumb causes the SB_PAGEUP code to be sent. And if you click between the bottom or right side of the scroll bar and the thumb, the SB_PAGEDOWN message is generated. As you drag the thumb, the scroll bar generates a series of SB_THUMBTRACK notification codes with the current position of the thumb in the LOWORD of lParam. The SB_THUMBTRACK notification codes allow the developer to vary the contents of the window continuously as the thumb is dragged. However, it is not always possible to update the contents of a window dynamically as the user is scrolling (this is often the case if the window's contents are too complex). When dragging stops, the scroll bar generates the SB_THUMBPOSITION message, which contains the final position of the thumb in the LOWORD of lParam. If dynamic scrolling of the window is not feasible, the window can be updated once at this point to reflect the new position of the viewport. When these notification codes are sent to the WinProc of the scroll bar's parent, the WinProc must update the scroll bar thumb to reflect the new position. The WinProc must scroll the data in the window as well. Windows automatically handles scrolling for list boxes, combo boxes, and edit controls that have scroll bars. Sample Scroll Bar Use Assume that you have written a simple Windows application that constantly generates a tone (like a program that aids guitarists in tuning their instruments). You attach a vertical scroll bar to the main window of the program; the range of the scroll bar represents eight octaves (there are 12 notes in an octave, for a total of 96 notes), and the current position of the thumb represents the note that is currently sounded. (You have a function called SoundTone that accepts a note value and generates the corresponding tone.) First, create a window with a scroll bar and set the range of the scroll bar from 1 to 96. Also, the initial tone is set to an A-440Hz, the 22nd note in your range (see Figure 4). To get to the next or previous note in the range, the user must click on the down or up arrow of the scroll bar. To increase or decrease the octave, set up the scroll bar so that the user can click between the thumb and the down arrow, or the thumb and the up arrow. Dragging the thumb produces a sliding glissando sound (see Figure 5). The WM_VSCROLL messages that are sent to the main window must be processed and the tone adjusted accordingly. You must also reposition the thumb so that it ends up at the correct location in the scroll bar. Dialog Boxes Dialog boxes can be thought of as the glue that binds groups of control windows together. A dialog box is simply a pop-up window containing one or more child windows. These child windows can be in one of the previously discussed control classes, or they can be custom developed. Two characteristics distinguish a dialog box from an ordinary pop-up window. First, a dialog box can be defined in an external ASCII resource file. Second, a dialog box manager in Windows handles the interaction between the user and the dialog box. The dialog box manager processes the keystrokes that occur in a dialog box and performs the actions indicated by the keystroke. If the user presses Tab, the dialog box manager must search forward, starting after the control that currently has the focus, for the next control on which the focus can be set (that is, a control with the WS_TABSTOP style). If the user presses Esc, the dialog box manager generates a WM_COMMAND message with the IDCANCEL value and sends it to the user-defined dialog box procedure. Several steps are involved in using a dialog box in your Windows application. First, you must define a dialog box in the resource file (RC), compile it using the resource compiler, and attach it to the EXE file of your application. Second, your application must define a dialog box procedure to handle the messages sent to the dialog box. Third, your application must load the dialog box from the resource file and call the dialog box manager to display it so it can interact with the user. There are two types of dialog boxes, modal and modeless. Most applications use modal dialog boxes to gather information from the user. A modal dialog box does not permit you to interact with or switch to any other window in your application until you are finished using the dialog box. In fact, when you invoke a modal dialog box, the dialog box manager goes into its own message loop and dispatches only those messages meant for the dialog box or one of its controls. Usually a user gets out of a dialog box by clicking on an OK or Cancel push button or pressing Esc or Enter. Modeless dialog boxes allow you to switch the focus from them to the rest of the application. You switch the focus by clicking the mouse on another window. The Windows PIF Editor serves as a good example. In the PIF Editor, you can activate the pull-down menu while the focus is set to one of the modeless dialog box controls. Since the methods for loading and using modeless and modal dialog boxes are quite different, both will be discussed. Defining a Dialog Box in a Resource File Defining a dialog box and its controls by hand in a resource file is something that an experienced Windows programmer avoids, because it's much simpler and easier to use the Windows Dialog Editor (see Figure 6) supplied in the Microsoft Windows Software Development Kit (SDK). Dialog boxes and control windows can have many different options and styles. The Dialog Editor allows you to draw and edit a dialog box interactively and generate the necessary resource file definition. Figure 7 shows the syntax for a sample dialog box definition. The first line specifies the name, load options, memory options, and coordinates of the dialog box. The default load and memory options are LOADONCALL and MOVEABLE. A LOADONCALL resource is not loaded into memory until the application needs it. This is one of the primary advantages of resource files. The coordinates specified in dialog box definitions work in a special way. Because Windows applications should be as device-independent as possible, your dialog boxes should look the same no matter what sort of video display adapter is used. If a dialog box is meant to encompass the entire display, it should do so whether the video adapter is at 640 x 350 or 1024 x 768 resolution. To ensure this, the dialog box coordinate system is based upon the dialog base unit. This is a unit of measurement that is calculated based on the current system font. In a dialog box definition, the x coordinate and the width are measured in 1/8 of a dialog base unit. The y coordinate and the height are 1/4 of a dialog base unit. To determine the exact number of pixels in the horizontal and vertical dialog base units, use the new Windows 3.0 function GetDialogBaseUnits. After you define the name, load options, memory options, and coordinates of the dialog box, you can define other dialog box options. The STYLE statement can be used to give the dialog box certain style attributes, the same kind that are used in the CreateWindow function. Four styles are specific to dialog boxes. These styles are DS_LOCALEDIT, DS_MODALFRAME, DS_NOIDLEMSG, and DS_SYSMODAL. The DS_SYSMODAL style creates a system modal dialog box, in which all windows in the system are disabled until you exit the dialog box. This style of dialog box could be used, for example, to signal a fatal error that would affect the performance of the Windows session. You can give the dialog box a title with the CAPTION statement. Other options are the MENU statement, the CLASS statement, and the FONT statement. The FONT statement, new to Windows 3.0, allows you to specify the type style and point size of the text inside the dialog box. Once this header information has been defined in the resource definition, all you need to do is define a list of the control windows between the BEGIN and END statements. However, the Windows SDK Dialog Editor does all of this work for you, allowing you to assemble controls graphically, while it builds the actual RC file of definitions. The definition of the Add Tick dialog box (see Figure 18) for the stock charting application is shown in Figure 7. This definition was generated by the Dialog Editor. You can usually tell if a dialog box definition was generated by the Dialog Editor because it generates the longer CONTROL-style statements to define controls as opposed to the handwritten format. Tab Stops and Groups I previously stated that the dialog box manager handles keystrokes within a dialog box, and that when Tab is pressed, the dialog box manager moves the input focus to the next control. How does it know which control to move the input focus to? The WS_TABSTOP style bit must be set for every control window to which you can move by pressing Tab or BackTab. This is especially important for programmer-defined controls, since the dialog box manager has no other way of knowing if a custom control is used for output only or can interact with the user. (If a static control has the WS_TABSTOP style set, the dialog box manager must search for the next nonstatic control after the static control in order to set the input focus to a valid control.) The dialog box manager also supports something called a control group. This is a group of controls organized so that you can move among them by pressing the arrow keys instead of Tab and BackTab. The most common example is a group box that contains radio buttons. If the first radio button in the group box has the WS_GROUP style and the first control defined outside of the group also has the WS_GROUP style, you will be able to move among the radio buttons by using the arrow keys. If you want to move to a control that resides outside of the radio group, you must use Tab. In general, to create a control group, give the first control in the group the WS_GROUP style and give the first control defined outside of the group the WS_GROUP style too. If you want to find the window handle of the next item in a control group, you can use the GetNextDlgGroupItem function. This function is useful to set up custom keyboard handling in a dialog box. For example, if you have a Windows application that offers customized keyboard mapping, the user might define a key combination to be the same as the down arrow key (for instance, Ctrl-X in WordStar). When the user presses Ctrl-X in a dialog box, you may want to set the input focus to the next member in a control group. You can use the GetNextDlgGroupItem function to get the window handle of the next control in the group and set the focus to it. A similar function, GetNextDlgTabItem, is used for controls with the WS_TABSTOP style. Loading a Dialog Box Now that the dialog box has been defined, compiled, and attached to the EXE file, you want to load it into your application. Four functions allow you to load or create a modal dialog box and have it processed by the dialog manager of the functions, DialogBox and DialogBoxParam, load the dialog box from the RES file that is attached to the EXE. The other functions, DialogBoxIndirect and DialogBoxIndirectParam, create dialog boxes from a description stored in a template in your application. Since this method is uncommon, I will not discuss it. The function used to load and invoke a dialog box is DialogBox. FARPROC lpfn; lpfn = MakeProcInstance((FARPROC) OpenDlg, hThisInstance); DialogBox(hThisInstance, "Open", hWnd, lpfn); FreeProcInstance(lpfn); The MakeProcInstance call takes the address of the programmer-defined dialog box procedure and creates a small piece of code known as a thunk. A thunk first binds the data specified by hThisInstance to the function pointed to by OpenDlg, and then branches to that function. MakeProcInstance returns a far address to the thunk. When finished with the dialog box invocation, free the thunk by calling FreeProcInstance. (Thunks are a complex mechanism and are described in greater detail in Programming Windows, Microsoft Press, 1990) The DialogBox function takes four arguments; the handle of the application's instance (remember, this is a unique program identifier so that Windows can distinguish multiple copies of the same application that are running simultaneously); the name of the dialog box to load; the handle of the owner window; and the pointer to the instance thunk. The value returned by DialogBox is the value that the programmer-defined dialog box procedure returns. (Actually, the return value is the value that is passed as the second argument to the EndDialog function.) Dialog Box Procedure Every dialog box must have a programmer-defined dialog box procedure associated with it. The dialog box procedure is similar to a standard WinProc, but dialog box procedures and WinProcs differ slightly. First, a dialog box procedure returns a Boolean value, TRUE or FALSE. Unlike a standard WinProc, your dialog box procedure must return TRUE if it processes a message and FALSE if it does not. When you invoke a modal dialog box, the dialog box manager goes into its own internal message loop. If it receives a message intended for the dialog box, it passes the message to your dialog box procedure first. If the dialog box procedure returns FALSE, the dialog box manager passes the message to its own internal default dialog box procedure. If your dialog box procedure returns TRUE, the dialog box manager does nothing further with the message and reads the next message. Windows 3.0 allows the advanced programmer to access the default dialog box procedure, DefDlgProc. Microsoft distributes the source code to DefDlgProc with the Windows 3.0 SDK. The dialog box procedure should be defined as follows: BOOL FAR PASCAL MyDialogProc(hDlg, message, wParam, lParam) HWND hDlg; WORD message; WORD wParam; DWORD lParam; This header looks exactly like a WinProc, except for the BOOL return value. And, just like a WinProc, the dialog box procedure must be exported in your application's DEF file. EXPORTS . . . MYDialogProc If a dialog box does not seem to be behaving correctly, check that you have exported the dialog box function. This is a common error among novice Windows programmers (and experienced ones too). Most simple dialog box procedures process only the WM_INITDIALOG and WM_COMMAND messages. WM_INITDIALOG, similar to the WM_CREATE message, is sent by the dialog box manager just before the dialog box is displayed for the first time. This message gives the dialog box procedure the opportunity to initialize the contents of the controls in the dialog box. The dialog box procedure, for example, could fill in the contents of edit fields with default values, fill list boxes with strings, and check certain radio buttons and check boxes. If you do not want the dialog box manager to set the initial focus to the first control with the WS_TABSTOP style automatically, you can set the focus to another control at this point. If you do change the focus, the dialog box procedure must return FALSE after processing the WM_INITDIALOG message. If it returns TRUE, the dialog box manager will set the focus automatically. (This message is the exception to the rule stated earlier about the Boolean values returned by your dialog box procedure.) The lParam of the WM_INITDIALOG message was not used in previous versions of Windows. Windows 3.0 allows you to pass the dialog box manager a long value that the dialog box manager will in turn pass to your dialog box procedure as the lParam value of the WM_INITDIALOG message. To do this, use the DialogBoxParam function. This function is identical to DialogBox, except that a fifth argument is added, the long value to pass in the WM_INITDIALOG message. Other than processing the WM_INITDIALOG message, your dialog box procedure mainly handles the WM_COMMAND messages generated when the user manipulates a control window. These WM_COMMAND messages usually come in two varieties: the ones generated when you click on a button control, and the ones sent with notification codes in the HIWORD of lParam. When the user clicks the OK or Cancel button, it usually means the user wants to dismiss the dialog box. A WM_COMMAND message is sent to the dialog box procedure with wParam set to the control identifier of the push button. The dialog box manager is informed that the user is through with the dialog box by a call to EndDialog. EndDialog takes two arguments, the handle of the dialog box, and a word-length value. This value will be used as the return value from the call to DialogBox. switch (message) { case WM_COMMAND : switch (wParam) { case IDOK : EndDialog(hDlg, TRUE); break; case IDCANCEL : EndDialog(hDlg, FALSE); break; If the user pressed OK, EndDialog is called to tell the dialog box manager that the dialog box should be dismissed. The value TRUE will be returned by the dialog box manager to the application. In the following statement the value TRUE would be returned and assigned to the variable rc. rc = DialogBox(hInstance, "AddTick", hWndMain, lpfnDialogProc); Similarly, if the user clicked on the Cancel push button, the value False would be returned to the application. EndDialog does not dismiss the dialog box immediately. It sets a bit in a private data structure that the dialog box manager allocates for each dialog box. (It also copies the value of the second argument into this structure.) When your dialog box procedure returns control to the dialog box manager, the manager examines this bit, and if it is set, the manager breaks out of its message loop. Message Boxes Windows programs commonly inform the user of a potential or existing error condition. For instance, if the user attempts to exit a word processing application without first saving the document, the application should bring up a dialog box that contains a warning message and a choice of actions. The dialog box should have a message such as "Save the document before exiting?" as well as three push buttons labeled Yes, No, and Cancel. It would be extremely tedious if the Windows programmer had to design dialog boxes for each possible error or warning condition. Fortunately, you can call a single function to display some text in a dialog box and attach one or more predefined buttons to it. This kind of dialog box is called a message box; not surprisingly, the function that creates and displays one is called MessageBox. The nice thing about using message boxes is that Windows itself contains the dialog box procedure for all message boxes and handles all the WM_COMMAND messages. You don't have to create your own dialog box procedure, export it, and load the dialog box. All you have to do is process the return code from MessageBox. The syntax of the MessageBox function is shown below. int MessageBox(HWND hWndParent, LPSTR lpText, LPSTR lpCaption, WORD wType) The first parameter is the handle of the owner window. The second parameter is the static text that will appear inside the message box. If it is too long to fit on one line, Windows wraps the text to the next line and increases the height of the message box accordingly. The third parameter is an optional title that will appear in the caption of the message box. The fourth parameter consists of bit flags that tell Windows what kinds of buttons and icons you want put into the message box. If you want a push button other than the predefined push buttons, you must define your own dialog box to be used as a message box. Windows gives you Yes, No, Cancel, Abort, Ignore, and Retry push buttons. It also allows you to place one of several predefined system icons on the left side of the message box (see Figure 8). The value returned from MessageBox is the control identifier of the push button the user selected. (If the user presses OK, the IDOK value is returned; if the user presses No, the IDNO value is returned, and so on.) The complete list of identifiers of these buttons can be found in WINDOWS.H (see Figure 9). A typical use of a message box is: rc=MessageBox(hWndMain, "Do you want to exit the application?", "Exit",MB_YESNO | MB_ICONQUESTION); if (rc == IDYES) PostQuitMessage(0); Modeless Dialog Boxes Revisited Modeless dialog boxes can be thought of as a hybrid of normal overlapped windows and modal dialog boxes. To create a modeless dialog box, use the CreateDialog function. The syntax of CreateDialog is exactly the same as the DialogBox function. However, when you create a modeless dialog box, the CreateDialog function returns immediately with the handle of the dialog box; Windows does not invoke the internal dialog box manager. A typical call to create a modeless dialog box is shown below. hDlgModeless=CreateDialog(hInstance,"MyModelessDlg", hWndMain, lpfnProc); Since a modeless dialog box is simply a standard overlapped window with child windows, messages get sent to it as they do to any other window in your application. All you need is your application's normal message loop: while (GetMessage(&msg, 0, 0, 0)) { TranslateMessage(&msg); DispatchMessage(&msg); } But there is something missing. Suppose the input focus is set to one of the controls in the modeless dialog box and you press Tab. Nothing happens, because there is no dialog box manager that processes the keystrokes and gives a special significance to the Tab key. Of course, you could put code in your application to handle Tab, but it would be a lot easier to "call the dialog box manager" temporarily. Windows allows you to do this. The function IsDialogMessage(hDlg, &msg) tests to see if the message contained in the passed MSG structure is meant for the dialog box whose handle is passed in the first argument. If so, IsDialogMessage performs all of the necessary keystroke translations and dispatches the message itself to the dialog box. It takes care of all of the keystroke interpretations. It then returns TRUE if the message has been processed, and FALSE if not. The standard message loop modified to include the use of IsDialogMessage looks like this: while (GetMessage(&msg, 0, 0, 0)) { if (!hDlgModeless || !IsDialogMessage(hDlgModeless, &msg)) { TranslateMessage(&msg); DispatchMessage(&msg); } } After obtaining the message, the message is passed to IsDialogMessage. If it returns TRUE, you know it processed the message fully and there is no reason to call TranslateMessage and DispatchMessage. The only other concern with modeless dialog boxes is that since the dialog manager is not used with modeless dialog boxes, the EndDialog function cannot be used to terminate dialog box processing and destroy the dialog box. Instead, you must use the standard DestroyWindow call. You also have to invalidate the dialog box handle so that the IsDialogMessage function won't be called in your message loop. DestroyWindow(hDlg); hDlgModeless = 0; Sample Application At the end of the previous article in this series, the stock charting application had a complete pull-down menu system and MDI capabilities. Every time you chose the File/New menu item, a new MDI child window was created. Items on the Window pull-down menu automatically tiled and cascaded the child windows, and arranged minimized windows nicely. But that was all the application did; there was no code actually relating to stocks. Now that you understand dialog boxes, an entry form will be implemented that prompts the user for information about the stocks. You'll need to decide what kind of information should be associated with each stock, and create a data structure describing a stock object. The stock information will also need to be stored for easy access. Since this application is not designed to be a commercial application (and since this application is designed to teach Windows programming, not data structures), ease of implementation and comprehension will be favored over performance. When applicable, I will discuss alternatives you can use to obtain more palatable results. The modules to build the version of STOCK.EXE discussed here are shown in Figure 10. A real-time stock charting system is not being created, so the application will not continuously monitor a stock's performance as it is traded throughout the business day. The only concern is the stock's closing price and perhaps its volume. Other types of analyses might involve the average price during the day or the high and low prices, but that is left as an exercise to the reader. You may also want to keep track of each date. Here's a structure to hold the date: typedef struct tagDate { BYTE chMonth; BYTE chDay; BYTE chYear; } DATE; Six bytes are used for each date; however, I could probably get away with two bytes if I stored the date as an offset from January 1, 1900 and put it in an unsigned int. Some analysis programs do not store the date along with every tick; they are just concerned with a series of price and volume data. To avoid the overhead of floating point arithmetic, you can store the price as a four-byte value using the following formula. price = (dollar amount) X (fraction denominator) + (fractional amount) For instance, a price of 16 7/8 would be stored as (16 * 8) + 7 = 135. Now you can define a structure that holds the daily trade information: typedef DWORD PRICE; typedef DWORD VOLUME; typedef struct tagTick { PRICE price; VOLUME dwVolume; DATE date; } TICK, FAR *LPTICK; You also need a data structure that describes the properties of a stock graph (see Figure 11). A future article will delve further into the logic of creating graphs. This data structure will be discussed more fully at that time. All this information must be transferable to and from disk, or the program would be pretty useless. Besides the ticker information, you want to save the name of the stock (an up-to-five letter symbol), a description of the stock, the graphing parameters, and the number of ticks. You also want to store a signature at the beginning of the file for data integrity to ensure that the file read from disk really is one of the stock files (see Figure 12). Finally, a structure is needed to keep track of all of the stock information while the application is running. In addition to all of the information that resides in the stock file, you need to store the filename of the stock file, the handle of the MDI child window in which the stock information is displayed, a handle to the memory buffer that holds the tick data, and some state flags that record the current status of the stock. All the stocks are linked together in a single-linked list; a handle to the next stock information data structure is kept in each structure (see Figure 13). Each stock is stored in its own separate file whose name is comprised of the stock symbol and an STO extension. For a commercial application, you would most likely use a Windows-compatible commercial database access library. Using Dialog Boxes To demonstrate dialog boxes, examine a dialog box in the stock-charting application. When the user chooses the File Open menu item, a dialog box should be displayed that lists the files with the STO extension found in the current directory. The user should be allowed to change directories and disk drives to search for other STO files. Almost all Windows applications have a File Open dialog box. Unfortunately, Windows provides no standard File Open dialog box, so each vendor implements his or her own in a slightly different way. The stock-charting application makes use of some sample code that comes with the Windows SDK for a typical File Open dialog box. You must first define the dialog box in the resource file. The resource definition is shown in Figure 14. The dialog box's name is Open; it has a caption and a system menu (see Figure 15). An edit field lets the user input a filename or specification other than the default *.STO. The edit field also tracks the current selection in the file list box, which contains the names of all the files in the current directory matching the file specification. It also contains a list of subdirectories in the current directory, and a list of all disk drives on the system. Finally, there is an OK push button that the user can click to open the file, and a CANCEL button that aborts the Open operation. The OK push button is the default, so if the user presses Enter, a WM_COMMAND message with IDOK in wParam is sent to the user-defined dialog box procedure. The WinProc of the main window in the application processes the WM_COMMAND message, and when wParam is set to ID_OPEN, invokes the File Open dialog box. case ID_OPEN : lpfn = MakeProcInstance((FARPROC) OpenDlg, hThisInstance); DialogBox(hThisInstance, "Open", hWnd, lpfn); FreeProcInstance(lpfn); break; You want the dialog box procedure to return a handle to the opened stock file rather than a TRUE/FALSE value or a control window identifier. A file handle returned by the Windows OpenFile function is a WORD value, so it can be returned to the application by passing it as the second argument to EndDialog. Here is the heading of the dialog box procedure: BOOL FAR PASCAL OpenDlg(hDlg, message, wParam, lParam) HWND hDlg; unsigned message; WORD wParam; LONG lParam; { WORD index; PSTR pTptr; HANDLE hFile; switch (message) { . . . The only two messages that you need to process in the dialog box procedure are the WM_INITDIALOG and the WM_COMMAND messages. First, look at the small function called UpdateListBox below. This function forms a complete pathname with a file specification at the end by concatenating two string variables, one holding the current path and the other holding the current filespec. It then calls a Windows function called DlgDirList. DlgDirList takes five arguments: a handle to a dialog box, the identifier of a file list box within that dialog box, the identifier of a static text field also within that dialog box, a string containing a path and file specification, and a value representing file attributes. It then searches the path for all files matching the file specification and the desired attributes. The name of each matching file is placed into the list box, and the full pathname is placed into the static text field. DlgDirList also places the subdirectory and drive names in the list box automatically. Finally, UpdateListBox sets the contents of the dialog box's edit field to the default file specification by calling SetDlgItemText. This function takes the identifier of a control window in a dialog box, determines the window handle of the control, and sends a WM_SETTEXT message to it. void UpdateListBox(hDlg) HWND hDlg; { strcpy(str, DefPath); strcat(str, DefSpec); DlgDirList(hDlg, str, IDC_LISTBOX, IDC_PATH, 0x4010); SetDlgItemText(hDlg, IDC_EDIT, DefSpec); } Next the WM_INITDIALOG message is processed. A call to UpdateListBox fills the list box with the names of the STO files and sets the contents of the file specification edit control to *.STO. The EM_SETSEL message is then sent to the edit control to highlight the entire field. If you want to send a message to any control within a dialog box, and you only know the identifier of the control, you can use SendDlgItemMessage. This is equivalent to the following statement. SendMessage(GetDlgItem(hDlg, ID_CONTROL), msg, wParam, lParam); You also set the initial focus to the edit control. case WM_INITDIALOG: /* message: initialize */ UpdateListBox(hDlg); SendDlgItemMessage(hDlg,/* dialog handle */ IDC_EDIT, /* where to send message */ EM_SETSEL, /* select characters */ NULL, /* additional information */ MAKELONG(0, 0x7fff)); /* entire contents */ SetFocus(GetDlgItem(hDlg, IDC_EDIT)); return (FALSE); /* Indicates the focus is set to a control */ Now it's time to process the WM_COMMAND message. You want the edit control to track the currently selected item in the list box. When the current selection of a list box changes, the list box notifies the dialog box by sending a WM_COMMAND message with the identifier of the list box in wParam and the LBN_SELCHANGE notification code in the HIWORD of lParam (see Figure 16). An easy way to get the text of the currently selected item is to call the built-in Windows function DlgDirSelect. Why aren't LB_GETCURSEL and LB_GETTEXT messages sent to the list box? Other than the fact that I have to write more code to do this, the DlgDirSelect function removes the square brackets surrounding the name of a directory and the square brackets and hyphens surrounding the names of disk drives in the list box. It returns zero if the selected item was a filename and nonzero if it was a directory name. In this case, if the currently selected item is a filename, you fill the edit control with the name of that file, and if it is a directory name, you refresh the list box. Double-clicking a list box selection has the same result as single-clicking a list box item and then clicking OK. When the LBN_DLBCLK notification code for the list box is received, the program simply jumps to the code that is responsible for opening the file (see Figure 17). Finally, you must process the two push buttons in the dialog box. Remember, when a button is pressed, a WM_COMMAND message is sent to the parent window with wParam set to the control identifier of the button. If the user clicks Cancel, you simply end the dialog box processing and return NULL for the value of the file handle. If the user clicks OK, you have a lot more work to do. First, examine the contents of the edit field and see if the filename contains a wildcard. If it does, assume that the user wants to search for a new filespec. You get the file specification, separate it into a pathname and a filespec, and refill the file directory list box with files matching the new spec. If the edit field is empty, put up a message box warning the user and return control to the dialog box manager. If neither is the case, you have a valid filename in the edit field: open the file and return the file handle. There you have it--your first dialog box. Once you code a few dialog boxes, you will find yourself churning out dialog box code by rote. Adding a Tick After a day of trading activity, you need to input the closing price and the total trading volume for each stock in the database. To do this, select the Add Tick item from the Edit menu. The Add Tick dialog box is displayed forthe current stock (see Figure 18); you can fill in the various fields to append a single tick to the list of tickers for that stock. The Add Tick dialog has three fields; the trading date, the volume, and the final price (the final price must be an integer for now). At this intermediate stage of the application, the date and the volume fields are basically ignored in the code--only the final price is important. Also, at this time, a tick cannot be inserted in the middle of the ticker list. This will be remedied in a future version of the code. Options Dialog Box The characteristics of a stock and how its graph is displayed can be controlled through the Options dialog box (see Figure 19). Actually, the same dialog box is displayed when you add a new stock to the database and when you change the properties of an existing stock. The only difference is that in the latter case, the field that contains the name of the stock is disabled; this is the only piece of information that cannot be altered in an existing stock. The following lines of code disable the symbol control if we are altering the characteristics of an existing stock. if (lpStockInfo->hTicks != NULL) . . . EnableWindow(GetDlgItem(hDlg, ID_SYMBOL), FALSE); As mentioned above, at this stage of the program, some of the fields in this dialog box are ignored by the code. The minimum and maximum prices define the range of the y-axis of the stock graph. The scale factor is a number that all of the numeric data is divided by before the number is plotted on the graph. If the price range of a stock is from 100 to 300, a ticker price can be 201 different values (assuming that each price is a whole number). Instead of having the y-axis of the graph divided into 201 points, you can have the y-axis represent 20 different points if you divide each price by a scale factor of 10. The tick interval is the interval between two successive tick marks on the y-axis. The price denominator field is ignored in this release. It represents the denominator in the fractional part of the stock price. (Most stocks are traded in either eighths or sixteenths.) The final component of the Options dialog box is the owner-draw combo box that contains the styles for the various pens that draw the horizontal and vertical grids of the graph. Trying It Out The function StockFileRead reads a stock file into the application. You can test this application using the stock file included with the application. StockFileRead first allocates space for the stock header and then reads the header. The number field of the header is examined to ensure that it is a valid stock file. You allocate memory to hold all of the ticker information and then read the tickers. Then, call GraphCreateWindow to create an MDI child window for this stock. The handle of the MDI child is returned and stored in the stock information structure. Likewise, the memory handle of the stock information structure is stored in the "extra-bytes" area in the window by using the SetWindowWord function. This makes it possible to determine stock information associated with a given window very quickly. When a stock window is displayed, some text about the stock is also displayed. A future installment will discuss graphics in Windows. I will then expand this application to show graphical information about each stock. This article focused on dialog boxes, which are often the most important objects in a Windows application. You saw how to define one in a resource file, load it into an application, and create a user-defined dialog box procedure to handle the messages. The next article finishes the discussion of dialog boxes, and discusses some of the graphical capabilities of Windows. 1 As used herein, "Windows" refers to the Microsoft Windows graphical environment. Windows refers only to this Microsoft product and is not intended to refer to such products generally. Figure 7. Definition of the Add Tick Dialog Box ADDTICK DIALOG LOADONCALL MOVEABLE DISCARDABLE 112, 31, 106, 86 CAPTION "Add a Tick" STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "Date:", -1, "static", SS_LEFT | WS_CHILD, 2, 7, 22, 8 CONTROL "", ID_TICK_DATE, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP | WS_CHILD, 53, 5, 48, 12 CONTROL "Closing price:", -1, "static", SS_LEFT | WS_CHILD, 2, 26, 55, 11 CONTROL "", ID_TICK_PRICE, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP | WS_CHILD, 57, 25, 44, 12 CONTROL "Volume:", -1, "static", SS_LEFT | WS_CHILD, 2, 44, 32, 8 CONTROL "", ID_TICK_VOLUME, "edit", ES_LEFT | WS_BORDER | WS_TABSTOP | WS_CHILD, 38, 43, 63, 12 CONTROL "OK", 1, "button", BS_DEFPUSHBUTTON | WS_TABSTOP | WS_CHILD, 9, 66, 28, 14 CONTROL "Cancel", 2, "button", BS_PUSHBUTTON | WS_TABSTOP | WS_CHILD, 63, 66, 32, 14 END Figure 11. Graph Data Structure /* Data structure describing how we draw the graph */ typedef struct tagGraphInfo { PRICE dwMinPrice; PRICE dwMaxPrice; DWORD dwScaleFactor; DWORD dwTickInterval; WORD iDenominator; /* the fractional amount used for this stock */ WORD iGridPen; } GRAPHINFO, FAR *LPGRAPHINFO; Figure 12. Tagging a Stock File #define MAXSTOCKNAME 5 #define MAXDESCRIPTION 32 #define MAXFILENAME 13 typedef struct tagStockFile { DWORD dwMagic; #define MAGIC_COOKIE 66666666L char szStock[MAXSTOCKNAME]; char szDescription[MAXDESCRIPTION]; GRAPHINFO graphinfo; WORD nTicks; /* TICK aTicks[1]; */ } STOCKFILE; Figure 13. Structure to Track Stock Information typedef struct tagInCoreStockInfo { char szFileName[MAXFILENAME]; /* file name where the stock data is kept */ STOCKFILE StockFile; /* a copy of the stock file header */ HANDLE hTicks; HWND hWnd; /* window in which stock is shown */ DWORD dwFlags; /* any kind of status bits we need to keep */ #define STATE_HAS_VGRID 1L #define STATE_HAS_HGRID 2L HANDLE hNextStockInfo; /* link to next stock info struct */ } STOCKINFO, FAR *LPSTOCKINFO; Figure 14. Open Dialog Box Definition Open DIALOG 10, 10, 148, 112 STYLE DS_MODALFRAME | WS_CAPTION | WS_SYSMENU CAPTION "Open " BEGIN LTEXT "Open File &Name:", IDC_FILENAME, 4, 4, 60, 10 EDITTEXT IDC_EDIT, 4, 16, 100, 12, ES_AUTOHSCROLL LTEXT "&Files in", IDC_FILES, 4, 40, 32, 10 LISTBOX, IDC_LISTBOX, 4, 52, 70, 56, WS_TABSTOP LTEXT "", IDC_PATH, 40, 40, 100, 10 DEFPUSHBUTTON "&Open" , IDOK, 87, 60, 50, 14 PUSHBUTTON "Cancel", IDCANCEL, 87, 80, 50, 14 END Figure 16. Processing the WM_COMMAND Message case WM_COMMAND: switch (wParam) { case IDC_LISTBOX: switch (HIWORD(lParam)) { case LBN_SELCHANGE: if (!DlgDirSelect(hDlg, str, IDC_LISTBOX)) { SetDlgItemText(hDlg, IDC_EDIT, str); SendDlgItemMessage(hDlg, IDC_EDIT, EM_SETSEL, NULL, MAKELONG(0, 0x7fff)); } else { strcat(str, DefSpec); DlgDirList(hDlg, str, IDC_LISTBOX, IDC_PATH, 0x4010); } break; case LBN_DBLCLK: goto openfile; } return TRUE; Figure 17. Opening a File When OK Is Pressed case IDOK: openfile: GetDlgItemText(hDlg, IDC_EDIT, OpenName, 128); if (strchr(OpenName, '*') || strchr(OpenName, '?')) { SeparateFile(hDlg, (LPSTR) str, (LPSTR) DefSpec, (LPSTR) OpenName); if (str[0]) strcpy(DefPath, str); ChangeDefExt(DefExt, DefSpec); UpdateListBox(hDlg); return TRUE; } if (!OpenName[0]) { MessageBox(hDlg, "No filename specified.", NULL, MB_OK | MB_ICONHAND); return TRUE; } AddExt(OpenName, DefExt); /* The routine to open the file would go here, and the */ /* handle would be returned instead of NULL. */ StockFileRead((LPSTR) OpenName); EndDialog(hDlg, hFile); return (TRUE); case IDCANCEL: EndDialog(hDlg, NULL); return (TRUE); } break; } return FALSE; } Questions & Answers - Windows Q: I'm curious about what a window handle (hWnd) object really is physically. Is it a global memory handle? A segment or selector? An array index? Josh Trupin Stamford, CT A: When you call CreateWindow or CreateWindowEx, you are calling functions in the Windows USER.EXE dynamic-link library (DLL). These functions perform the following call: LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, 64) and then a LocalLock, resulting in a NEAR pointer (PSTR) to 64 bytes inside of USER's data segment (DGROUP). This pointer is returned as a window handle (HWND) after USER fills out the 64 bytes that it refers to. To fill out this 64-byte "window object," USER actually employs an internal structure similar to the following. typedef struct tagWND // bytes // from { //length end GetWindow... //------ ----- ------------- WORD Undocumented[22]; // 44 -64 DWORD ExStyle; // 4 -20 GWL_EXSTYLE DWORD Style; // 4 -16 GWL_STYLE WORD ID; // 2 -12 GWW_ID WORD hWndText; // 2 -10 GWW_HWNDTEXT HWND hWndParent; // 2 -8 GWW_HWNDPARENT HANDLE hInstance; // 2 -6 GWW_HINSTANCE FARPROC WndProc; // 4 -4 GWL_WNDPROC } WND; // 64 typedef WND NEAR *PWND; The elegance in this design is that when you make USER calls (functions whose first argument is usually an HWND), hWnd is cast inside of USER as a PWND so that the above WND structure elements can be addressed directly; that is, the following refers to the window's parent. ((PWND)hWnd)->hWndParent Code that refers to the window structures is therefore very efficient. Some of the arguments in the CreateWindow or CreateWindowEx call are placed directly in the structure; others are determined by USER. ((PWND)hWnd)->ExStyle =dwExStyle; ((PWND)hWnd)->WndProc =<WndProc from WNDCLASS of ClassName>; ((PWND)hWnd)->hWndText =<another local pointer to WindowName> ; ((PWND)hWnd)->Style =dwStyle; ((PWND)hWnd)->hWndParent =hWndParent; ((PWND)hWnd)->ID =hMenu; //if child window,=child ID ((PWND)hWnd)->hInstance =hInstance; If your window class has extra bytes requested (specified by cbWndExtra in the WNDCLASS structure of the RegisterClass call), then additional bytes are allocated at the end of the 64-byte WND structure in the LocalAlloc call. In other words, the LocalAlloc call that USER performs is really as follows. LocalAlloc(LMEM_FIXED | LMEM_ZEROINIT, 64 + <cbWndExtra from WNDCLASS of ClassName> ) This allows you to attach (with SetWindowWord and SetWindowLong) window-specific information to the window in an object-oriented way in your application. From all of the above you can see how SetWindowWord, SetWindowLong, GetWindowWord, and GetWindowLong work. When you make these calls, USER adds 64 bytes to the hWnd pointer and then adds the nIndex. (PSTR)hWnd + sizeof(WND) + nIndex The resulting local offset refers to the value you are setting or getting with SetWindowWord and SetWindowLong and GetWindowWord and GetWindowLong. This is how application-specific extra window information is referred to with a base index of 0, and why the GWW_XXX and GWL_XXX indexes have negative offsets. There is little to no protection when using window handles, because no protected-mode selectors are being used. It is very easy to corrupt USER's DGROUP by using invalid window handles. A bad window handle could cause USER to corrupt its DGROUP without a protection violation occurring. Also, it is very easy to see how you could use another application's valid window handles to modify its window structures. Finally, you can see that window handles are reused. Since they are just offsets to structures that are created and destroyed, it is possible that another CreateWindow or CreateWindowEx call could return the same "handle" of a previously destroyed window, if the LocalAlloc resulted in the same physical offset inside USER's DGROUP. Therefore, a window handle is really an offset into USER's DGROUP at which a structure exists that defines a window. If you're still curious, you can look at these structures using HeapWalker. Just perform Object LocalWalk and Object Show operations on USER DATA and look for 68-byte memory objects. They're shown by HeapWalker as 68 bytes because of the additional 4 bytes at the beginning of every local memory object that KERNEL uses to manage local heaps. Q: I've written an application that uses more than 100 child windows. The windows are created when the application starts, but only some of them are visible at the same time. I have two problems. First, I can only run a few instances of my application. When Free System Resources (as reported in the Program Manager's About box, for example) gets close to 0% I get failures trying to create new windows. I have tons of memory, so I don't understand why this is happening. Second, it seems as if my application is very slow to initialize. I'm in the dark on this, since I can't find any documentation on what would cause it to be slow. Michael J. Paschal Waterbury, CT A: Windows maintains all windows inside the USER.EXE DLL. USER's data segment (DGROUP), like all C medium-model DGROUPs, is limited to 64Kb in size. All windows that are created consume space inside this DGROUP, as do menus, title-bar names, and so on, so you are bumping up against the 64Kb segment-size limit. The Free System Resources percentage reflects this constraint. The amount of global memory you have is irrelevant; the DGROUP can only grow to 64Kb. If you want to be able to start many instances of your application, you will have to change your window creation strategy. If you can get by creating your windows only as needed (Program Manager does this for its icon and application-name windows), you should operate that way. If you can destroy windows when they are not displayed, you should do that (although Program Manager doesn't). Hiding windows does not reduce their USER memory requirements. Your ability to do these things depends on your application. If you are creating all your windows in advance because you need to be positive you have them before you start, you are stuck with your current situation. If you can deal with a possible lack of windows further into run time, change your strategy. Each window you create requires a minimum of 68 bytes inside USER's 64Kb maximum DGROUP. In theory you could therefore have a maximum of about 960 windows. But that estimation ignores the amount of static memory required by USER, any window text, any menus, and so on. I wrote a small program to test the maximum number of windows I could create. It turned out that the maximum number of really bare-bones windows I could create was around 700. Assuming you would more likely have some trimmings (menus, title-bar text), a more realistic estimate is 350. Your application is slow to start up because of KERNEL.EXE's local heap memory manager. Since local heaps start off being relatively small, KERNEL has to expand them as more local memory is requested; this takes a small amount of time. What is more time consuming is that LMEM_MOVEABLE objects inside USER's DGROUP have to be moved higher so that the LMEM_FIXED window structures can be created low. Your initialization is slow because KERNEL has to move the entire upper part (where LMEM_MOVEABLE objects are kept) of your USER DGROUP and adjust all the local handle table entries, doing this more than 100 times at start-up in your case. Q: I am creating a few edit class windows in my application by using CreateWindow. They are displayed in my main client area as children of my main window. The problem I am having is that the text that gets put in or created in these windows ends up being stored in my application's local heap, and space there has gotten very tight. Is there any way that edit class windows can be coerced into having their text stored in the global heap? B.D. Beykpour San Francisco, CA A: In edit controls in dialog boxes created with calls such as DialogBox and CreateDialog, edit control text is stored in the global heap by default. To have this text stored in the local heap, you must specify the DS_LOCALEDIT style. For edit class windows outside a dialog box that are created with CreateWindow, the reverse is true. By default, edit class window text is stored in the application's local heap. To force it to be stored in the global heap, use the following trick. When you call CreateWindow, replace the hInstance argument with a global handle to a small block of zeroed memory. Specifically, create the global memory object as follows. auto GLOBALHANDLE aGhEditBuffer; aGhEditBuffer=GlobalAlloc(GMEM_MOVEABLE | GMEM_ZEROINIT, 256L); Then call CreateWindow with aGhEditBuffer as the hInstance argument. If you specify some initial text with the lpWindowName argument, it appears as the initial text in the edit window. The edit window operates as normal, but using the global heap for the edit text. The global memory block grows automatically as the text gets longer. When you destroy the edit window, the global memory buffer is automatically freed in the same way a local memory buffer is freed. Q: In WINDOWS.H a section labeled OEMRESOURCE appears to refer to the user-interface bitmaps, but I can't find much documentation on them. What are all these bitmaps and can I access them? Alex Polozoff Austin, TX A: Figure 1 identifies these bitmaps and shows which version of Windows they were developed for. The resolution of each bitmap depends on the display driver you have installed. In the Windows environment, Versions 1.x and 2.x, these were monochrome bitmaps; in Windows1 3.0 many of them are full-color format bitmaps, although only the colors black, white, and gray are actually used. In some cases (the Close bitmaps, for example) the bitmap really contains two or more equally-sized bitmaps arranged in rows; these "sub-bitmaps" would have to be extracted if you wanted to use them. The following code, which loads OBM_ZOOM, is an example of how to access these bitmaps: auto HDC ahDc; auto HBITMAP ahBm; auto BITMAP aBm; ahDc = CreateCompatibleDC(0); ahBm = LoadBitmap(0, MAKEINTRESOURCE(OBM_ZOOM)); GetObject(ahBm, sizeof(aBm), (LPSTR)&aBm); /* aBm now has the bmWidth and bmHeight for BitBlt'ing */ ahBm = SelectObject(ahDc, ahBm); /* the OBM_ZOOM bitmap can now be BitBlt'ed here from ahDc using the bmWidth and bmHeight in aBm */ ahBm = SelectObject(ahDc, ahBm); DeleteObject(ahBm); DeleteDC(ahDc); Q: I have a child window that has a title bar in my client area. The child window looks similar to the PageMaker Toolbox window. The title bar is meant simply to label the window, though; I do not want the user to be able to move the child window by grabbing the title bar with the mouse. How do I lock down a child window that has a title bar? Frank Mena San Jose, CA A: What you want to do is prevent Windows from processing the move system command for the child. The following code fragment, which would be placed at the end of the child's WndProc message switch, shows how to do this. switch (awMsg) { . . . case WM_SYSCOMMAND: if ((awParam & 0xFFF0) == SC_MOVE) break; default: return DefWindowProc(ahWnd, awMsg, awParam, alParam); } return 0L; As you can see in this code, all system commands except SC_MOVE are processed by DefWindowProc. By not sending the WM_SYSCOMMAND/SC_MOVE message to DefWindowProc, you will prevent the default processing of this message, which is for Windows to move the child window. This technique can be used to disable any WM_SYSCOMMAND subcommands. Reader Feedback Regarding the fourth question in the September Windows Q&A, MSJ (Vol. 5, No. 5), Dean White of Dallas, Texas, notes that the following rem >%temp%\dosapp.sem accomplishes the same end and is better than echo %0 >%temp%\dosapp.sem He's right. Not only is no disk sector space wasted with his method, it is faster. A file entry with a length of zero is all that is created as a semaphore in the temporary files subdirectory. 1 For ease of reading, "Windows" refers to the Microsoft Windows graphical environment. "Windows" refers only to this Microsoft product and is not intended to refer to such products generally. child window. This technique can be used to disable any WM_SYSCOMMAND subcommands.