Microsoft Systems Journal Volume 2 ──────────────────────────────────────────────────────────────────────────── Vol. 2 No. 1 Table of Contents ──────────────────────────────────────────────────────────────────────────── IRMA: A 3278 Terminal Emulator for Micro-to-Mainframe Communication The development team at Digital Communications Associates (DCA) took the IBM(R) PC and grafted an EBCDIC-speaking synchronous coaxial cable system into it. The result, IRMA(R), is a precedent-setting PC-to-mainframe link reflecting innovative systems design and hardware/software engineering. Upgrading Applications for Multi-user Environments Software developers who hesitated to write multi-user versions of their products have a clearer upgrade path to follow with the appearance of IBM's PC Network and software modules such as MS-DOS(R) 3.x, MS-Net, and the NetBIOS. We look at rewriting applications for multi-user environments. Expanded Memory: Writing Programs That Break the 640K Barrier Once, 640K of memory seemed like more than enough, but as programs written for DOS grew larger, the 640K limit was just too small. The LIM Expaneded Memory Specifications (EMS) defines a standard that allows developers to access expanded memory beyond 640K. Keep Track of Your Windows Memory with FREEMEM Not every Microsoft(R) Windows program is long and complex. This short (fewer than 100 lines of code), simple program not only displays the amount of available memory in an icon at the bottom of the screen and updates it every second, it also reveals a good deal about programming in Windows. A Guide to Debugging with CodeView The CodeView(TM) debugger included with the Microsoft(R) C Compiler, Version 4.00, provides flexible commands and a higher degree of integration than previous debuggers. This behind-the-scenes view of CodeView's development focuses on the design of the debugger and its use in a typical situation. Page Description Languages: High-Level Languages for Printer Independence Page description languages (PDLs) are becoming popular as a precise and formalized means of controlling printer output. We take a look at the three major languages that appear to be serious market contenders──Postscript(TM), Interpress(TM) and Document Description Language (DDL). DIAL 2.0 Provides Software Developers With Integrated Support System As part of its commitment to upgrade technical support to developers and programmers, Microsoft offers a new, improved version of Direct Information Access Line (DIAL) service, which includes access to bulletin boards, direct technical assistance, and interactive develper forums. Rich Text Format Standard Makes Transferring Text Easier Microsoft is proposing the Rich Text Format (RTF) as a standard for moving formatted text between applications. RTF allows the transfer of text from one application to another without losing the formatting and without worrying about translating text to each application's unique file format. Ask Dr. Bob Carl's Toolbox EDITOR'S NOTE We are pleased to present what might reasonably be called the first "real" issue of Microsoft Systems Journal. This issue is twice as large as previous issues and contains a good deal more of the technical support material that we had planned from the beginning. We believe that MSJ is already on its way to becoming an important source of information and support for the development community, but in order to achieve this goal we need your feedback. We hope that you will write us at the address below, or via MCI Mail (MSJ), with your comments, questions, criticisms, or complaints. One of MSJ's prime objectives is to promote excellence and innovation in software design and development. Therefore, we are especially proud to present a programmer-oriented article on IRMA, the revolutionary board from Digital Communications Associates (DCA) that enables an IBM PC to emulate IBM 3278 or 3279 terminals, allowing communication between mainframes and PCs. IRMA, first released in 1983, took the industry by storm: DCA has sold over 350,000 boards. Though considered a hardware product, IRMA's success is really due to its supporting software and firmware, representing a triumph of engineering and design. Standardization and communication between different systems is a hot topic these days, and many developers are looking for data exchange standards to help ease the disparity among different systems. This issue of MSJ has an article on the Rich Text Format, a standard proposed by Microsoft to encode formatted text. To fully illustrate RTF's utility, we include details of the specs used by three page description language (PDL) vendors to create a universally formatted text design. As a quick read of the abstracts on the cover suggests, Microsoft Systems Journal wants to explore any development or technology that is of interest to the community of professional developers. We plan to cover many diverse topics in forthcoming issues. If there are any specific topics that you think we should include, please write to us.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL BARRY OWEN Managing Editor CHRISTINA G. DYAR Associate Editor GERALD CARNEY Staff Editor DIANA PERKEL Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher BETSY KAUFER Office Manager Copyright(C) 1987 Microsoft Corporation. All rights reserved; reproduction in part or in whole without permission is prohibited. Microsoft Systems Journal is a publication of Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H. Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley, President and Chief Operating Officer; Francis J. Gaudette, Treasurer; William Neukom, Secretary. Microsoft Corporation assumes no liabilty for any damages resulting from the use of the information contained herein. Microsoft, the Microsoft logo, MS-DOS and XENIX are registered trademarks and CodeView is a trademark of the Microsoft Corporation. IBM is a registered trademark of International Business Machines Corporation. PageMaker is a registered trademark of Aldus Corporation. PostScript is a registered trademark of Adobe Systems, Inc. dBase II is a registered trademark of Ashton-Tate. Crosstalk is a registered trademark of Microstuf, Inc. WordStar is a registered trademark of MicroPro International. Above is a trademark of Intel Corporation. Advantage! is a trademark of AST Research, Inc. ████████████████████████████████████████████████████████████████████████████ IRMA: A 3278 Terminal Emulator for Micro-to-Mainframe Communications Frank J. Derfler, Jr., and Edward Halbert☼ Computer products, like plants, evolve in many different ways. Some new species of plants mutate from a single root stock, but others are hybridized by cross-pollinating different strains. One of the earliest attempts to combine an old proven stock with the wild new personal computer resulted in a product whose name is now synonymous with micro-to-mainframe communications──IRMA. The developers of the IRMA board took the PC as it was produced by IBM and programmed by Microsoft──an ASCII device with only asynchronous communications capability──and grafted an EBCDIC-speaking synchronous coaxial cable system into it. While designing IRMA, they had to make several decisions and solve many technical problems. What IRMA Is IRMA (the name is not an acronym and was chosen because it is memorable) is a circuit board that can be installed in one of the slots in the IBM(R) PC. The card has a BNC connector and is attached to the coaxial cable coming from a communications controller in front of an IBM mainframe computer. IRMA's software allows the PC to emulate IBM 3278 or 3279 terminals and to transfer files between a mainframe and a PC. IRMA is truly integrated into the PC's hardware and operating system. It is a standalone coprocessor system hosted by the PC. If you place an IRMA card on a table and connect 5 volts and the coaxial cable to it, the IBM 3274 communications controller talks to it just like any other terminal. IRMA's Lineage IRMA is now marketed by Digital Communications Associates, Inc. DCA markets products for microcomputer communications (the company recently acquired Microstuf and its Crosstalk communications package) and large-scale network communications. The full DCA product line includes network processors, statistical multiplexers, high-speed time-division multiplexers, protocol converters, X.25 PADs, integrated software products for network management and control, and modems. DCA acquired IRMA in 1983 as a result of a merger with Technical Analysis Corp. (TAC). TAC started engineering design on IRMA in 1982 and released the product in 1983. The man behind IRMA is Andrew Miyakawa. He joined TAC as a programmer in 1972; in 1978, he became TAC's director of hardware engineering. Miyakawa designed the first IRMA printed circuit card. In fact, if you have an old IRMA board you can see his signature in the corner, which was later thought to be a scratch on the photo negative and removed. The engineering staff at TAC did a lot of custom work with the IBM coax A and coax B interfaces to develop the Agile printer interface for IBM systems. The coax A interface uses polling for terminal communications. The Agile interface uses a microprocessor to control communications between the polled coax system and the dumb printer. There is an obvious parallel between this system and the PC, and the engineering team's experience with this communications interface was important in the development of IRMA. The version of the IBM PC that was available in August 1982, when the IRMA team started work, was a limited machine with even more limited development tools. The PC's eight interrupts and four DMA channels made it difficult to enhance the machine──even if most of the interrupts had not already been assigned. The PC's character set was, and still is, small, and only the most primitive assembler was available. Finally, the 8088 just isnot up to the job of responding to the 3270 communications scheme. The coax A polling system was designed to interact with terminals using hard-wired logic and allows only 5.5 microseconds for a terminal to respond. The interrupt latencies on an 8088 don't allow it to recover quickly enough to respond to the communications channel. IRMA's designers forged ahead to make the interface work, almost in spite of IBM's product. They decided to add a coprocessor to the PC that is considerably faster than the 8088. Having decided upon the design approach, the IRMA development team divided the project into four major areas: coprocessor hardware, microcode for the hardware, terminal emulation software, and the user interface. The IRMA team spent most of its development time on implementation of the interface, rather than on design. TAC received no significant help from IBM other than the standard documents available to the general public. IBM's Technical Reference manual contained much information about the user interfaces, such as the monitor, the keyboard, and memory. This information was crucial to the development of the IRMA board. The Hardware The IRMA board consists of four major components. The Signetics 8X305, IRMA's processor, is the large chip on the left side of the board. This microprocessor has the power to handle the 3270 protocol and the associated polling, data transfer, and handshaking. Even though the chip runs at 4 MIPS, it can only execute roughly 22 instructions in the 5.5 milliseconds allowed for generating a response to the 3274 communications controller. The IRMA board functions as an intelligent buffer processor and interface between the coaxial cable information flow and the IBM PC. Signals from the coax travel through the DP8340 and DP8341 3270 Coax Transmitter/Receiver Interface, which occurs in the two large chips along the top of the card. The serialization and deserialization of data takes place in these two chips. The chips also provide the interface between the coaxial cable and the microprocessor. The independence of the IRMA processor in the PC is made possible by the Decision Support Interface (DSI). DSI is implemented in the microcode of IRMA's processor and support program. DSI allows the 8X305 system to meet the requirements of the 3270 protocol by managing streams of buffered data and handling all of the timing. The technique that moves the data between the processors is called a mailbox structure (see Figure 1). The mailbox is a 4-byte, dual-ported register array (4 74LS670) located in addresses 220H to 223H. The dual port array is the bridge between the 8088 and the 8X305 processors. Flags are used to detect idle states within the DSI structure; an idle state lasts only 5.5 microseconds between messages from the controller. When DSI isn't doing anything, it looks to this array for any commands that need processing. The command words sent by the IRMA program read or write bytes of data in the screen buffer, process keystrokes, and call other special features. Data is transmitted between the two processors through the use of four address locations (220H─223H). The processor traffic is controlled by the Command Request flag located at a higher address (226H). This flag is cleared by either processor when information in the array has been successfully read. There is also an Attention Request flag (227H). The commands that are passed through these four registers are parameter driven. The command is placed in the base address, or word 0, at 220H, and up to three arguments can be specified at 221H─223H, depending on the command. Before issuing any command on the array, the Command Request flag is set to high. The command is then placed in the register(s). After it has been picked up by the receiving processor, the request flag is cleared. Address locations 224H and 225H are reserved for future use. There are sixteen commands supported by DSI: Code Command Definition 0 Read buffer data 1 Write buffer data 2 Read status/cursor position 3 Clear main status bits 4 Send keystroke 5 Light pen transmit 6 Execute Power-on Reset 7 Load trigger data and mask 8 Load trigger address 9 Load attention mask 10 Set terminal type 11 Enable auxiliary relay 12 Read terminal information 13 No-op 14 Return revision ID and OEM number 15 Reserved; Do not use The Video Interface Even with its standalone power, the IRMA coprocessor board needs the PC as a keyboard input and display device. However, the PC lacks many of the features and the flexibility found in 3270-series terminals, so the designers had to work around these limitations. Special techniques are used to move video, characters and colors are substituted on the screen, and keyboard emulation alternatives are provided. The use of an on-board screen buffer is very important for the IRMA/PC video interface. Data on the IRMA's video buffer is always active and being updated regardless of whether or not the user is looking at it. Only when the user switches to the emulation mode is the data moved into the PC's video buffer. The screen buffer is controlled by DSI and occupies a total of 8K of fast RAM on the IRMA board. This RAM resides in the four large chips located along the bottom edge of the card. The IRMA board supports displays of up to 132 columns by 43 lines, such as those used by some 3278 models. This requirement drove a RAM with a size of 3K for the characters and another 3K for the attributes. This is because each character needs 16 bits in memory: 8 bits for the ASCII code and another 8 bits for the attributes. The remaining 2K are used by the 8X305 for local storage. The attribute characters that are supported by IRMA include protected/ nonprotected, numeric/alpha/both, light pen detect, tab stops, intensified/normal, nondisplay type fields, and modified data tag. The Extended Attribute Characters control character type (normal, blinking, reverse video, underlined), character color, and character set. Using the 8X305 processor, DSI converts the keystrokes and attribute characters into 3270 functions and emulates those same functions on the PC. The field attributes are mapped to the colors on the PC, corresponding approximately to those on the 3279 terminal. If an IRMA board is in a PC with a color display, each color represents a different type of field. The attribute characters on the PC translate to the field types in Figure 2☼. In order for a field to be unprotected, numeric data only, highlighted, and detectable, the program would have to send a 11011000B, D8H to the screen buffer. This field attribute continues to be in effect until another field type is specified. IRMA replicates some of the quirks of 3270 functions such as "attribute wrap," which occurs when the programmer fails to end a field. The attribute for that field not only affects screen locations after it, but also wraps around and affects locations before it on the screen. Programmers have to be just as careful with IRMA as they are with real 3278/9 terminals. In the 3278/9 terminals, a specific bit within the attribute byte, known as the Modified Data Tag (MDT), is changed whenever a field is modified. If the byte had been sent out as D8H and read back as D9H, the program would be signaled that the operator has modified the field. The IRMA software must interpret the position of the data on the screen of the PC and then convert it to the appropriate coordinates for the 3278/9 fields. The Extended Attribute Bytes (EAB) are defined in Figure 3☼. The IRMA screen-handling software has features designed to allow developers to see screen attributes that might not otherwise be displayed. A single keystroke shows the attributes of a field. Unprotected fields can be temporarily filled to view all of their locations. Attributes are normally displayed as blanks and take up a byte on the screen just as attributes on the 3278/9 terminals do. Addressing for text in the IRMA board starts at 50H and increases in increments of 80 for each line up to 780H on line 24 during emulation of MOD 2 terminals. Emulation of MOD 3 terminals requires 32 lines and ends at A00H. Emulation for MOD 4 terminals must support 43 lines, and the last line starts at D70H. The storage for the status line in the emulation of all models starts at 0H in lower memory, but the line is displayed at the bottom of the screen. User Interface There were some significant problems in the development of the user interface for the IRMA board. More than half of the problems involved keyboard operation. The major problem was figuring out how to emulate special 3270 series terminal keys on the PC's limited keyboard. The PC's keyboard has fewer keys to work with, and the PC's BIOS does not allow all combinations of keys on the keyboard to generate a scan code. This problem was eventually solved by including software that takes control of and reprograms the entire keyboard. The 3270 terminals have 24 PF keys and more than 30 other keys that the PC must satisfy. At first, the developers felt it would be best to keep the positions of the special function keys to be emulated on the PC as close to their positions on a 3278 as possible. Most of the 3270's PF keys were originally mapped to the numeric keypad on the right side of the PC's keyboard. This was later changed because of conflicts with the use of the numeric keypad. The PF keys 1─12 are mapped to Alt-1 through Alt-=, and PF keys 13─24 are mapped to Ctrl-1 through Ctrl-=. Either IRMA's developers had great foresight, or they have set a standard for PF keys, because most current PC-to-mainframe applications now map PF13-PF24 to the PC's control key sequence. Most of the terminal control keys on the left side of the 3278 keyboard have been faithfully duplicated on the PC's keyboard for IRMA. In all cases, the normal functions of the PC's keyboard are unmodified, and the 3270 emulated functions are reached through the Alt and Ctrl keys. The only keys that were located far from their normal positions on the 3278 keyboard are the Dup, Field Mark, PA1, and PA2 keys, which are mapped to Ctrl-G, Ctrl-H, Ctrl-J, and Ctrl-K, respectively. Other special functions not offered by a 3278 terminal are the Show Attribute and Display All Unprotected Fields functions. The designers left these in for the benefit of users and developers. Another problem faced by the IRMA design team was screen display emulation. The 3278 uses icons for certain actions and messages. These icons cannot be recreated with the standard IBM ASCII character set. The designers considered drawing these icons with the IBM PC graphic display system, but a little experimentation showed that the system would be too slow for good emulation. Also, using graphic characters would limit users to graphic terminals. The compromise was to use a combination of ASCII characters to emulate the special characters available on the IBM terminal. An example of some major substitutions are listed in Figure 4☼. Through this selection of characters, the designers came up with a reasonable facsimile of those used in 3278-type terminals. Screen emulation was made easier by the fact that many operators of 3270-series terminals recognize characters by where and when they come up as much as by what they look like. Thus, almost anything appearing in the same general location on the screen at the right time would be interpreted by the operator as the same message. The alternative characters developed by the IRMA team have become standard and are used by most other manufacturers of terminal emulators, including IBM. Color selection during emulation of the 3279 color terminal was also a problem, because the IBM Color Graphics Adapter has a much smaller palette than the 3279. Until just recently, the normal protected fields were dark blue and hard to read. DCA provided a patch in the setup menu to substitute cyan for blue. Converging Limitations The board was nearly complete when the IRMA team discovered a strange quirk in the IBM 3274 controller. The 3274 would not send color or graphic characters to the IRMA board. After an extensive investigation, the designers discovered that the controller looks for a special color convergence test feature found only on the 3279 terminals to determine if the terminal at the distant end is really capable of using color. This color convergence test allows the user to align the red, blue, and green guns by using the cursor keys until they form a single white dot. This dot signifies correctly aligned electron guns in the CRT. The presence of data in a special convergence test video buffer tells the 3274 that the terminal is aligned and ready to accept color graphics. Since the 3274 controller couldn't get a color convergence test response from the IRMA board or the PC, it would not transmit color graphics. The design team had to program the IRMA board to fake the controller into thinking that it had this test feature. The controller only checks for this test at terminal power-up, so the test is faked at the board's initialization. The test code is temporarily overlaid in undisplayed video buffers. Locked Out Another problem cropped up in early 3276 controllers. The phase-locked loop devices in the 3276 controllers were not consistent, and individual machines had significant timing differences during data transmission. At first, IBM field technicians consistently blamed problems on improper cable. Since changing the length of the cable can adjust the timing of received data, this fix often worked. The programming of the IRMA board was modified to work around the 3276 problem by addressing the data separator at specific, gated times. The bug was fixed on the newer models of the 3276, and modifications were developed for older models. User Relations A major issue in the development of the user interface was user-developed software. DCA chose not to copy-protect its software. DCA feels that copy protection is just not worth the ill will that it brings. DCA ships the source code for the terminal emulation and file transfer programs with every IRMA board. DCA includes the source code for its programs because everyone benefits if someone can improve the code. The user manual includes programs written in BASIC so that even the novice programmer can see how the board communicates. The BASIC programs were never really intended for use in applications because they run very slowly under a BASIC interpreter, but many people have tried them anyway. The design team assumed that sophisticated users would provide their own file transfer protocol or use a third-party protocol, but they found that they had to supply a more complete system. The file transfer software provided for the mainframe link has been updated from the slow FT78X to the FTCMS and FTTSO programs. The old FT78X used the VM/CMS Xeditor and just copied files byte by byte from disk through a fake keyboard buffer to the file on the host. The new program can send large blocks of data and is much faster. The new programs are written in PL/1 and require that the PL/1 transient library installed on the host. The IRMA package also includes an extensive technical reference manual with all the functions and commands documented. Miyakawa speaks for all MS-DOS developers when he expresses his wish that someone had defined MS-DOS user interfaces or that some of the BIOS responsibility was taken off the developer. DCA has gained much from such large users of the IRMA board as Texaco, Coca Cola, and Blue Cross and Blue Shield. Miyakawa feels that user feedback is extremely valuable and provides the best information for user interface development. IRMA's Future A new program called IRMAX has been developed to enable the IRMA card to run as what IBM calls a Distributive Function Terminal. This allows an IBM PC to emulate the 3270 PC, running multiple sessions on a single coax connection. This configuration is more desirable because the 3270 PC has had compatibility problems running PC software. IRMAX will maintain compatibility while allowing the full capabilities of the Distributive Function Terminal. IRMAX functions by removing most of the work from the 3274 controller and placing it on the IRMA card. The card carries the SNA, a 3270 display support program, and a terminal control program similar to the one in the 3270 PC. DCA's commitment to communications will carry it into the LAN market with IRMA LAN, which will operate with any manufacturer's version of the network basic input/output software (NetBIOS) using the function calls originally developed by IBM and Sytek. IRMA LAN is configured with an IRMA card in a network server, acting as what is functionally termed a network gateway. A modified version of the IRMA software runs on a PC operating as a network workstation and communicates to the IRMA card through the NetBIOS. In an efficient high-speed network, the person using the workstation can't tell that the IRMA card is located in a remote server. In some cases (IBM, Sytek, 3COM, and other companies), a "real" NetBIOS lies between the networking software and DOS. Other companies, such as Novell and Banyan, emulate NetBIOS as part of a complete operating system. In either case, the IRMA LAN software relies on NetBIOS to carry data messages to the gateway server, where they are addressed to the IRMA card. In the LAN environment, the IRMA software generates a stream of code called a network control block (NCB), which activates NetBIOS and passes data through a set of buffers established by NetBIOS and controlled by the NCB. The software generates an interrupt 5CH that calls NetBIOS. Other fields of the NCB point to a message buffer address and tell NetBIOS where to send the information in the buffer. This is a standard implementation of the capabilities provided by the NetBIOS communications interface. IRMA Showed How IRMA is an excellent example of the integration of an advanced communications capability into the somewhat limited PC system. The development team was innovative and employed sound engineering techniques to develop a precedent-setting link between PCs and mainframe computer systems. Figure 1: Mailbox Structure ┌──COAXIAL─CABLE─── ┌────────────────────────────────────────────────────│──(TO 327X CONTROLLER) │ ┌────────┴────┐ │ │ │ │ │ │ ╔═══════════╗ ╔═════╧═════╗ ╔════╧══════╗│ │ ║ 8K RAM ║ ║ COAX ║ ║ COAX ║│ │╔══════════════╗ ║ BUFFER ║ ║TRANSMITTER║ ║ RECEIVER ║│ │║ ║ ║ MEMORY ║ ║ DP8340 ║ ║ DP8340 ║│ │║ ║ ╚══╤═════╤══╝ ╚══╤═════╤══╝ ╚══╤═════╤══╝│ │║ ║ │ │ │ │ │ │ │ │║ ╟─────────┘ └────────┘ └────────┘ │ │ │║MICROPROCESSOR║ │ │ │║ 8X305 ║ │ │ │║ ╟───────────────────────┐ ┌───────────────┘ │ │║ ║ │ │ │ │║ ║ │ │ │ │║ ║ │ │ │ │╚══════════════╝ ╔══════╤════╧══╤══╧═══╤══════╗ │ │ ║ │ │ │ ║ │ │ ║ DUAL PORTED REGISTER ║ │ │ ║ │ │ │ ║ │ │ ╚══════╧════╤══╧══╤═══╧══════╝ IRMA │ └───────────────────────────────────────│─────│───────────────────┘ SYSTEM UNIT │ BUS │ ████████████████████████████████████████████████████████████████████████████ Upgrading Applications For Multi-user Environments ─────────────────────────────────────────────────────────────────────────── Also see the related article: MS-Net Lock (Function 5CH, Code 00H) ─────────────────────────────────────────────────────────────────────────── Robert Cowart☼ With the appearance of IBM's PC Network and the accompanying software modules (MS-DOS 3.x; the Network Program Microsoft-Networks, popularly known as MS-Net; and the NetBIOS) as at least semi-stable industry performers, software developers who were previously hesitant to release multi-user versions of their products now have a clearer upgrade path to follow. This development raises several questions: What's involved in rewriting existing applications for use with MS-DOS 3.x and these new standards, or for those many products that adhere to them? Is it worth the effort? How long might it take? Do you have to rewrite your entire program? And most importantly, if you do end up deciding to go multi-user, how should the logistics of implementation be handled? Obviously, the first point to consider is whether your application stands to gain from adaptation to a multi-user environment. There are certainly applications that don't lend themselves to multi-user operations. Word processors are a case in point; it's hardly advantageous or even advisable for more than a single user to be editing a given document. But for such applications as database management, accounting systems, manufacturing control, or even certain spreadsheet packages, upgrading your product to multi-user capability may serve to increase sales and fill a need among users, particularly as the market's interest in networking swells. IBM's clear commitment to the fully integrated and networked office environment of the future reinforces this trend. Single-user Upgrades In making this decision, you should bear in mind the distinction between single-user and multi-user modes of operation on the same PC network. Running an existing program in single-user mode allows one or more people to load the program into their own computers and run it, thus creating their own independent data files. So-called multi-user operation allows users to share those same data files. The multi-user program must, in this case, offer sufficiently intelligent data management to prevent users from accidentally clobbering each other's data. Most existing single-user applications run without concern for these matters if they are in a single-user mode under the current network operating systems. More often than not, few, if any, adjustments are necessary to get such an application running reasonably well under MS-Net. At the most, certain considerations pertaining to file access modes must be taken into account to prevent data corruption. This boils down to deciding what status──the most frequently used settings are read/write or read-only──each file used by a program should be given when a user is running the program on a network. According to Gary Stroud, PC systems programmer for Berkeley, Calif.-based Centram Systems West, makers of the TOPS network, "The first thing to do when adapting a single-user application on a network is to set your primary program file and all its overlay files to read-only mode. This way, any number of people can run the program, and nobody can accidentally erase one of the necessary program files while somebody else is in the middle of using them. That can cause a catastrophe. Microsoft Word sets its files to read-only automatically, for example. Next, it's imperative that, if your application uses temporary scratch files when users are creating documents, you ensure that the names for these files are always unique. Otherwise MS-DOS will confuse, say, my temporary file and your temporary file, and we'd have a mess. Starting with MS-DOS 3.0, there is a DOS call for obtaining a unique filename. Also, there is a new DOS file create call that will fail if a file of the same name already exists. You should use these facilities." If your current program uses the old-fashioned FCB (file control block) method of opening and closing files, you should convert your program to the newer (MS-DOS 2.0 or later) calls for file handling. Otherwise, files may not be closed properly, which would prevent other people from accessing them. This was typical of programs ported over from CP/M or written for MS- DOS 1.x. For WordStar(R) and dBASE II(R), for example, the use of FCB was the only way to open and close files. Multi-user Upgrades More-serious philosophical and logistical considerations come into play when the developer intends to provide real simultaneous multi-user access to data files on the LAN. A deeper understanding of some technical aspects of data sharing and real-world LAN usage is necessary. Although MS-DOS 3.1, with its extended function calls that are available through Int 21, provides all the control necessary for most typical LAN-based data-sharing scenarios, using the calls properly and effectively takes some planning and some reading of the MS-DOS 3.x Technical Reference manual. The developer should determine how many users would typically be sharing data at one time. How many files would actually be involved? Where might bottlenecks crop up in the system? Every command or function of the single-user version of the program must be closely scrutinized to determine the demands that the multi-user environment will introduce. To assist in the upward migration process and encourage developers to write for MS-Net, Microsoft offers an Independent Software Vendor (ISV) kit for $50. The kit includes a variety of published material about MS-Net, along with the programmer's reference manual (normally $50) and a selection of applications notes. Microrim's R:base(R) As a case in point, I've investigated the migration of Microrim's R:base from single-user PC-DOS 2.x to the multi-user MS-Net environment. (Figure 1 shows R:base on a multi-user network.) Since Microrim essentially used the same network interface module for its System V, the strategies apply to that product as well. As is typical of many database companies, Microrim postponed its multi-user implementation until the Microsoft/IBM standard began to take hold and users asked for a multi-user, "record-locking" version of R:base. At this point, Microrim began the work, which, according to Collin Miller, Microrim's director of product development, required about 8 person-months to complete. The first task was to understand the distinction between true multi-user environments and a PC-based LAN, and what impact that would have on product design. "A true multi-user environment employs some central intelligence to control traffic flow through a database," says Miller. "The central control, which would run in the server, would know which database and record each user was using at any given time, on all nodes. On PCs, users are really only sharing the same data, not the R:base program itself. R:base is running individually in each user's PC." More Intelligence? Microrim considered writing a central module that would be RAM-resident in the server to better arbitrate who was using files and records. "That would have given us something more approaching the classical multi-user system," says Miller. "This way we could have established levels of priorities for individual user access. We opted not to do that, just on philosophical bases." There were several reasons for this choice. First, writing a multi-user system would have added extra levels of complexity for PC users. "PC users don't want to get into the degree of complication necessary in really sophisticated multi-user databases," says Miller. "We simply wanted something that ran as easily as the existing R:base program. Second, this meant the single-user version of R:base is essentially identical to the multi-user version in both user interface and data file structure, making it easier to maintain compatibility for users." Also, Microrim felt that attempting to write a true multi-user system runs counter to the IBM philosophy of PCs──that everybody has his own processor and copy of the program running independently. IBM encourages developers to keep things as station-independent as possible to prevent unnecessary complications. MS-DOS vs. NetBIOS There are primarily two levels of compatibility to which a network application can be written──MS-DOS 3.1 and the NetBIOS (Network Basic Input Output System). The DOS level handles most file and record operations adequately enough for most purposes and acts on a higher level than the NetBIOS, which is the interface between MS-Net/MS-DOS and the particular networking card. "Someone like Microrim has a choice. They can write to DOS level calls, or to the lower-level NetBIOS interface," says Dave Melin, product marketing manager for networking products at Microsoft. "Both are standards and both are popular, but the official recommendation from IBM and Microsoft, as is always the case, is to write to the highest possible level in terms of programming interfaces. If MS-DOS will do what you need to do, use it. If you do need to go below it for some reason such as concurrency or proprietary security, then go ahead, but most applications won't require it." It is a common belief that writing to the NetBIOS provides a performance advantage. This probably comes from programmers' experience that circumventing some other MS-DOS calls by writing directly to hardware does increase speed. According to Melin, "It is true that performance may be improved under the right conditions by bypassing the DOS and writing to the NetBIOS directly. By writing to the NetBIOS, programmers have more control over network session──related parameters. Of course, they must deal with the added complexity of writing to a low-level interface. Some people are writing software that doesn't require any networking operating system, such as MS-Net, to be present. Primarily to minimize memory requirements, they do not want to require the presence of all that networking software, such as the redirector. They want to provide all the services themselves within their application. In this case, they will likely write directly to the NetBIOS." However, there are hazards involved when writing directly to the NetBIOS, particularly because contention management at the NetBIOS level is weak. At this level, applications can bump into each other if the NetBIOS is not handled very carefully. Considering that this is what DOS is really for, it makes sense to employ it whenever possible. "If there is no reason to use the NetBIOS, it's not worth it," says Melin. "Besides, the MS-DOS interface is what stays stable between revisions, whereas lower-level things have a history of changing, jeopardizing your compatibility over time." In light of these facts, Microrim intentionally worked exclusively on the DOS level in implementing the R:base upgrade. Also, it wanted to keep the number of extended MS-DOS calls to a minimum. Fewer than five new calls were used, according to Miller. Avoiding Corruption More troublesome to Microrim were concerns over its implementation of file and record handling within R:base. Because of the peculiarities of R:base files, Microrim faced some unique roadblocks when it was planning this angle of its strategy. Unlike dBASE, for example, where database files are essentially standalone entities, a series of R:base data files is controlled by two more-complex files. The first one (file 1) contains the "schema" or database structure, information about the size and layout of each table and their relationships to one another in a multitable database. The actual data of all related tables is stored in a second, larger file (file 2). Regulating traffic flow through these two files posed some interesting challenges for network implementation on MS-Net. For example, say two people attempt to change the structure or information about the number of records in a table in the database, that is, modifying file 1. Normally, the approach would be to lock the file temporarily for use by the person changing it. But this file could bring other users to a screaming halt because access to any of the tables in file 2 requires access to file 1 first. "We wanted to have a bottleneck in file 1, since everyone has to read it," says Miller. "We wanted to lock it as little as possible, so we had to come up with some other solutions." Microrim decided that four discrete levels of locking were necessary for the amount of flexibility desired and for the type of command a user might be issuing. The first level is the Database lock. It essentially allows a single user to have exclusive read/write access to selected files. This lock is necessary because it allows for modification of file 1 and packing (eliminating unwanted records from) a database. Assurance of single-user access is mandatory for these two activities, since the database is often significantly reorganized during these periods, and these processes may take some time. The next level is used for a very quick update to the schema (part of file 1) and is called a Schema or combination lock. Microrim offers this as an alternative to the complete lockout mentioned above. Examples of situations in which this lock would be used include defining a new relation, joining tables, or doing a database "add" or "subtract," that is, creating a new database that is a subset of an existing one. At this level, file 1 is momentarily locked and then quickly released for use by others. The new table(s) being built is/are the only thing(s) locked. The third level is a Table level lock, allowing for a load or a browse/edit on one table in a database. Loading allows a user to make a batch update to a table; browse/edit allows a user to edit a screenful of records at one time. In either case, other users are allowed read/write access to other tables in the database, and may read, but not write to, the locked table. Finally, Microrim incorporated a Record level lock, which it calls concurrency control, for use in managing individual records under multi-user table access. (See Figure 2.) Normally, this would be done by using MS-DOS's range-locking function. "MS-DOS 3.1 allows you to lock a record," says Microrim's Collins. "But if it's locked, other users have no access to it for any reason, even for reading. If you're editing a record in a table and go out to lunch, everyone else is locked out of that record. Let's say someone is running a report on outstanding orders, while someone else has a record locked in the same order table. The report could be inaccurate because one record wasn't accessible. "You could call this technique collision avoidance, to use networking terminology, because it prevents the possibility of collisions. But we wanted everyone to always be able to read the database, so we developed a collision detection scheme." Microrim's detection technique operates only if two or more users actually do try to alter the same record simultaneously. The scheme keeps track of where each user is in the database, allowing access to data records at any time. Two people can even be reading or editing the same record at the same time. Only for the split second that a user actually writes the change out to disk is the record locked. If a change is actually made to the record by one user, the second user is then notified that a change has been made since his last read. At this point, the second user may want to reread it before making his edit. "Actually, the likelihood of two or more users editing the same record at the same time is not that great when you only have five or six users on the network," says Miller. "With a small number of users, collision detection is fine. But when you try to support a larger number of users, collision avoidance would be the better choice, because collision detection begins to slow down exponentially with increases in retries." Microrim's attitude is that collision avoidance comes with a high price tag, because it prevents concurrent access to the database records. However, Microrim admits that it's a scheme that works well, particularly with a large number of users on the network. "By contrast," notes Miller, "our [collision detection] scheme has more overhead during a collision. The second person has to physically resolve the collision himself, through user intervention." Karl Schulmeisters, software design engineer for systems software at Microsoft, says that the exclusive locking procedure Microrim has circumnavigated to some degree, is a protective mechanism and intentionally designed into MS-DOS to avoid loss of data integrity and to prevent incomplete data from being transferred to different users. "There are pros and cons to Microrim's solution," Schulmeisters notes. "You really only have to lock a record for a brief moment during a read or write of a critical area." (See Figure 3.) Problems and Fixes There are three other more or less generic problems that developers have to consider addressing when upgrading their applications. The first of these could be called queuing. If a number of users want access to a particular resource, whether it is a printer, a file, or a modem, some arbitration mechanism may be desirable to handle these requests are on a first-come-first-served basis. Related to the queuing question is the problem of waiting. How do you control how long a process waits for an unavailable resource? When encountering an error, such as a locked record or file, DOS performs error retries only for a short period of time. But say you want to go to lunch and let the machine retry a batch update until the resource is available. Multi-user R:base allows the user, or a custom program, to retry for as long as 4.5 hours, determined by an R:base command, SET WAIT. Finally, there is the nefarious "deadly embrace." (See Figure 4.) This impasse occurs when one user is waiting for resources, such as data tables, that are already in exclusive use by the other user. It can result in an endless tie-up, in which your program appears to be hung. Microrim solved the deadly embrace problem by providing a command, SET LOCK, that the user can issue. The command lets a user lock a number of tables in advance of beginning a batch process that would involve all those tables. If the tables are not all available, the transaction stops there and gives the user a message to that effect. Conclusion There are many points to consider before embarking on any serious rewrite of an application for LAN use, not all of which could be discussed here. I have tried to bring up the most salient issues. Keep in mind that the planning process is the hardest and most critical stage. You may want to begin by analyzing how your program works in single-user mode. Then you should consider what control mechanisms would be necessary to guarantee lack of data corruption and minimize intrusions by users upon each other. If your application includes a programming language, as R:base does, you have to map out each of the commands in the language and analyze what and when resources will have to be locked for each command. If your application lacks a programming language, things should be that much easier. All this planning may well take up the biggest part of your development time, but it should pay off in the long run. For broader system compatibility, try to stick to the MS-DOS 3 function calls, unless you are writing a really esoteric application. Finally, the MS-DOS 3.1 Technical Reference manual is complete enough to answer most questions. According to Microrim's Collins, "We lived and died by the 3.1 Technical Reference. I don't think we ever had to phone up Microsoft." Figure 1: This sample network configuration shows several combinations typical of networks. ╔══════════════════ SERVER ╗ ┌────────────────────────────────────────┐ ║ ┌────────────┐ ║ │ ┌──────────────────────────────┐ │ ║ │ Database │ ║ │ │ ╔══════════════════════════│══════│════╗ ║ ┌─┤ directory ├───│──┘ ║ ┌────┐ ┌─────────────┐ ┌─┴──┐ ┌─┴──┐ ║ ║ │ │ c:\dbfiles ├───│──┐ ║ │ a: │ │ c:\ │ │ g: │ │ e: │ ║ ║ │ │ (g:) │ ║ │ │ ║ └────┘ │ ┌─┴────┐ │ └────┘ └────┘ ║ ║ │ └────────────┘ ║ │ │ ║ Local │ Other │ │ └──┬──┘ ║ ║ │ ┌────────────┐ ║ │ │ ║ Floppy │ directory│ │ Remote ║ ║ │ │ R:base │ ║ │ │ ║ Drive │ │ │ Drives ║ ║ ┌─────┐ │ │ System V │ ║ │ │ ║ │ c:\rbfiles │ ║ ║ │ │ ├─┤ directory ├─────┐│ ║ └─────────────┘ ║ ║ │ │ │ │ c:\rbfiles │ ║ │ ││ ║ Local Hard Drive ║ ║ │ │ │ │ (f:) │ ║ │ ││ ╚═══════════════════════ WORKSTATION 2 ╝ ║ │ C:\ ├─┤ └────────────┘ ║ │ │└───────────────────────────────────┐ ║ │ │ │ ┌────────────┐ ║ │ │ │ ║ │ │ │ │ User's │ ║ │ └─────────────────────────────┐ │ ║ │ │ │ │ private │ ║ │ ┌──────────────────────┐ │ │ ║ └─────┘ ├─┤ directory ├───┘ │ ╔═════════════════│══════│══════│══════╗ ║ │ │ c:\user2 │ ║ │ ║ ┌────┐ ┌────┐ ┌─┴──┐ ┌─┴──┐ ┌─┴──┐ ║ ║ │ │ (e:) │ ║ │ ║ │ a: │ │ b: │ │ e: │ │ f: │ │ g: │ ║ ║ │ └────────────┘ ║ │ ║ └────┘ └────┘ └────┘ └────┘ └────┘ ║ ║ │ ┌────────────┐ ║ │ ║ └──┬──┘ └──────┬─────┘ ║ ║ │ │ User's │ ║ │ ║ Local Floppy Remote ║ ║ │ │ private │ ║ │ ║ Drives Drives ║ ║ └─┤ directory ├─────┘ ╚═══════════════════════ WORKSTATION 1 ╝ ║ │ c:\user1 │ ║ ║ └────────────┘ ║ ╚══════════════════════════╝ Figure 2: Conflict in Updating. When multiple users are accessing a database, they must take care when updating to avoid conflicts. As shown above, it is not enough just to facilitate multiple access;interlocks must be provided to isolate the entire update process. ┌─────────────┐ ┌─────────────┐ │ USER 1 │ VALUE IN DATABASE │ USER 2 │ └─────────────┘ ┌───────────────┐ └─────────────┘ ┌──────────────────┤ │█ ▼ Read Value │ 100 │█ Read Value ┌────────────────┐ │ │█─────────────────┐ │ Type in change │█ │ 100 │█ │ │ (subtract 20) │█ │ │█ ▼ │ ┌──────────┐ │█ │ 100 │█ ┌────────────────┐ │ │ Process │ │█ │ ┌─────────┐ │█ │ Type in change │█ │ │ change │ │█ │ │ │ │█ │ (subtract 10) │█ │ │ (100-20) │ │█───────►│ │ 80 │ │█ │ ┌──────────┐ │█ │ └──────────┘ │█ │ │ │ │█ │ │ Process │ │█ │ │█ │ └─────────┘ │█ │ │ change │ │█ │ Write │█ │ ┌─────────┐ │█ │ │ (100-10) │ │█ │ change │█ │ │ │ │█ │ └──────────┘ │█ └────────────────┘█ │ │ 80 │ │█ ◄───────┤ │█ █████████████████ │ │ │ │█ │ Write │█ │ └─────────┘ │█ │ change │█ └───────────────┘█ └────────────────┘█ ████████████████ █████████████████ Figure 3: R:base Concurrency Control and Locking ╓┌───────────────┌──────────────────┌────────────────────────────────────────╖ Command Control/Lock Description EDIT form Concurrency Allows concurrency control ENTER form for other users and items unless a table lock is issued. Command Control/Lock Description APPEND Concurrency and Table lock interrupts CHANGE Table Lock concurrency control while DELETE ROWS data is modified; table lock may also exclude other table locks issued after it. EDIT ALL Table Lock Table lock set if concurrency ENTER form control is not in effect for FROM filespec any part of the table. When LOAD set, the lock excludes other FORMS☼ table locks issued after it. REPORTS☼ SET LOCK ON UNLOAD... FOR tblname☼ VIEW☼ DEFINE Database Lock Locks all tables and allows no EXPAND other locks to be set while it Command Control/Lock Description EXPAND other locks to be set while it RELOAD is in effect. This lock will not REDEFINE be set while a table lock is in REMOVE effect. REMOVE COLUMNUN UNLOAD☼ BUILD Table Lock Used together for some FORMS☼ Database Lock operations. While the INTERSECT database structure is JOIN modified, a database lock is PROJECT in effect. After the structure RENAME is modified, the database REPORTS☼ lock is replaced by a table SUBTRACT lock. UNION VIEW☼ Figure 4: Deadlock can occur when two of more users who have access to some tables wait for access to others. Microrim solved this problem with the SET LOCK command that lets a user lock a number of tables before beginning a batch process. User 2 cannot reach ┌──────────────────┐ CUSTOMERS ┌──────────────┤ CUSTOMERS │ because user 1 │ │ ───────── │ has locked it. │ │ User 1 locks │ │ │ CUSTOMERS │ X ◄─────────┐ │ │ waits for │ │ │ │ TRANSACTIONS │ │ │ └──────────────────┘ │ │ │ │ │ │ ┌──────────────────┐ │ │ │ TRANSACTIONS ├───────────────┘ │ │ ──────────── │ │ │ User 2 locks │ └────────► X │ TRANSACTIONS │ │ waits for │ User 1 │ CUSTOMERS │ cannot reach └──────────────────┘ TRANSACTIONS because user 2 has locked it. ─────────────────────────────────────────────────────────────────────────── MS-Net Lock (Function 5CH, Code 00H) ─────────────────────────────────────────────────────────────────────────── This excerpt opens a file named DATABASE in Deny None mode and locks two portions of it: the first 128 bytes and bytes 1024 through 5119. After some (unspecified) processing, it unlocks the same portions and closes the file. Function 5CH, Code 00H denies all access (read or write) by any other process to the specified region of the file. BX must contain the handle of the file that contains the region to be locked. CX:DX (a 4-byte integer) must contain the offset in the file of the beginning of the region. SI:DI (a 4-byte integer) must contain the length of the region.If another process attempts to use (read or write) a locked region, MS-DOS retries three times; if the retries fail, MS-DOS issues Interrupt 24H for the requesting process. You can change the number of retries with Function 44H, Code 0BH (IOCTL Retry).If a program closes a file that contains a locked region or terminates with an open file that contains a locked region, the result is undefined.Programs should not rely on being denied access to a locked region. A program can determine the status of a region (locked or unlocked) by attempting to lock the region and examining look at the error code. Call AH = 5CH AL = 00H BX Handle CX:DX Offset of region to be locked SI:DI Length of region to be locked Return Carry set: AX 1 = Invalid function code 6 = Invalid handle 33 = Lock violation 36 = Sharing buffer exceeded Carry not set: No error Macro Definition: OPEN_HANDLE macro path, access mov dx,offset path mov al, access mov ah, 3DH int 21H endm ; WRITE_HANDLE macro handle,buffer,bytes mov bx,handle mov dx,offset buffer mov cx,bytes mov ah,40H int 21H endm ; LOCK macro handle,start,bytes mov bx, handle mov cx, word ptr start+2 mov dx, word ptr start mov si, word ptr bytes+2 mov di, word ptr bytes mov al, O mov ah, 5CH int 21H endm ; UNLOCK macro handle,start,bytes mov bx,handle mov cx,word ptr start+2 mov dx,word ptr start mov si,word ptr bytes+2 mov di,word ptr bytes mov al,1 mov ah,5CH int 21H endm ; CLOSE_HNDL macro handle mov bx,handle mov ah,3EH int 21H endm stdout equ 1 ; start1 dd 0 lgthl dd 128 start2 dd 1023 lgth2 dd 4096 file db "DATABASE", 0 op_msg db " opened.",ODH,OAH 11_msg db "First 128 bytes locked.",ODH,OAH 12_msg db "Bytes 1024-5119 locked.",ODH,OAH u1_msg db "First 128 bytes unlocked.",ODH,OAH u2_msg db "Bytes 1024-5119 unlocked."ODH,OAH c1_msg db "closed.",ODH,OAH handle dw ? ; begin: open_handle file,01000010b jc open_error mov handle,ax ; save handle write_handle stdout, file,8 jc write error write_handle stdout,op_msg,10 jc write error lock handle,start1,lgth1 jc lock_error write_handle stdout,11_msg,25 jc write_error lock handle, start2,lgth2 jc lock_error write_handle stdout,12_msg,25 jc write_error ; ; (Further processing here) ; unlock handle,start1,lgth1 jc unlock_error write_handle stdout,u1_msg,27 jc write_error unlock handle,start2,lgth2 jc unlock_error write_handle stdout,u2_msg,27 jc write_error close_handle handle jc close_error write_handle stdout,file,8 jc write_error write_handle stdout,cl_msg,10 jc write_error ; ; (Further processing here) ; open_error ; routine not shown write error ; routine not shown lock_error ; routine not shown unlock_error ; routine not shown close_error ; routine not shown ████████████████████████████████████████████████████████████████████████████ Expanded Memory: Writing Programs That Break the 640K Barrier Marion Hansen, Bill Krueger, and Nick Stuecklen☼ When the size of conventional memory was set at 640K, that seemed like all the memory that anyone with a PC could ever use. But as programs written for DOS grew larger, and the amount of data they could handle increased, what had once seemed inexhaustible pinched like a pair of size 8 shoes on size 10 feet. Swapping to disk, or the use of overlays, is a solution, but it often limits performance to unacceptable levels. That's why Lotus Development Corp., Intel Corp., and Microsoft Corp. got together to do something about DOS's 640K memory limit. Together they came up with the Lotus/Intel/Microsoft Expanded Memory Specification (EMS). The programming examples accompanying this article use the EMS and will run under the AST Research Enhanced Expanded Memory Specification (EEMS), a variation of the EMS, as well. Expanded memory is memory beyond DOS's 640K limit. Just as DOS manages conventional memory, the Expanded Memory Manager (EMM) manages expanded memory. The EMM can manage up to 8 megabytes (MB) of expanded memory. Programs that adhere to the EMS can use expanded memory without fear of conflict. Contrary to what you may have heard, you can put code as well as data into expanded memory. Programs can store anything in expanded memory except their stacks, which should reside in conventional memory. While placing the stack in expanded memory is theoretically possible, managing a paged stack is generally very difficult. Expanded memory is implemented in one of two ways. One way is an expanded memory board, where expanded memory physically resides on an add-in board. Intel's Above(TM) Board and AST's Advantage(TM) are examples of expanded memory boards. The other way is a LIMulator, such as the Compaq Deskpro 386's CEMM (Compaq Expanded Memory Manager), running on a 386-based system. A LIMulator emulates expanded memory in extended memory (which is memory from 1MB to 16MB) using the 80386 paging hardware. Application programs can't use expanded memory automatically. This article explains how to write programs that take advantage of expanded memory, including programming techniques and examples, and the EMM functions. Expanded Memory In the current DOS environment, code and data can reside in one of three memory locations. Each memory type has advantages and disadvantages. Conventional Memory: Conventional memory is always available, except whatever is used by application programs and resident software, and it's easily accessible. Moving about in conventional memory, whether through code or data, requires very little overhead. Segment register updates (when the software crosses segment boundaries) are the only substantial software overhead. Segment register updates are common to all three types of memory and as such are not a limitation unique to conventional memory. Conventional memory's drawback is its 640K limit. Large application programs, network software, and resident spelling checkers, to name just three types of software a typical user might have, consume prodigious amounts of conventional memory. Disk Memory: There's more than enough room on a disk for any software, but the constant paging in and out of data and code in even the simplest applications creates a great deal of overhead. This makes disk memory undesirable for speed-sensitive applications. DOS is not re-entrant, and you can invoke a terminate-and-stay-resident (TSR) program in the middle of a DOS function. For this reason, TSR programs sometimes have difficulties using DOS for disk I/O. Expanded Memory: Like conventional memory, expanded memory is nearly always available. And with fully populated expanded memory boards, it is sufficient for most applications. Accessing expanded memory requires slightly more overhead than accessing conventional memory but significantly less overhead than accessing disk memory. When an application stays within a single 64K page, expanded memory overhead is comparable to conventional memory overhead. Expanded memory is especially suitable for four types of software: TSR programs, graphics packages, databases, and network software. TSR programs permanently consume the memory they occupy. If a TSR program is large in code or data, it consumes a great deal of conventional memory. A TSR program that is designed to use expanded memory effectively keeps most of its code and data in expanded memory, while maintaining a small kernel in conventional memory for housekeeping chores, such as trapping interrupts, and activating the rest of the TSR program in expanded memory. Drawing and drafting packages frequently have to maintain multiple copies of their graphics bit map. Secondary drawings, double buffers for animations, and additional menus are all stored for later retrieval. Because recall speed is essential, these bit maps must be maintained in memory. Just one monochrome (1 bit per pixel) bit map with 640-by-350 resolution requires nearly 28K of storage. Several such bit map copies can eat up conventional memory, but they are easily accommodated in expanded memory. Database programs sort huge volumes of data, typically much more than conventional memory are able to handle. Expanded memory can be used to store and sort large databases and is much faster than swapping to disk. Network software creates large tables and volumes of resident data. Although network software may be used infrequently──usually just for peripheral sharing and file transfers──it can consume up to 50 percent of available conventional memory. Putting network software in expanded memory frees conventional memory for software that you use more frequently. Using application software efficiently is a trade-off between the convenience of generous amounts of expanded memory and the overhead of paging in 64K blocks of it at a time. You should consider two questions when deciding whether to use expanded or conventional memory for your applications. First, does the code execute a large number of far calls or jumps relative to the time it spends executing other instructions? If it does, put the code in conventional memory. If it doesn't, put the code in expanded memory. Second, does the application's data require segment register initialization each time it is accessed? If it does, use conventional memory. If it doesn't use expanded memory. As a rule of thumb, use expanded memory if both the time spent using data or executing code and the preparation overhead are large. The Page Frame Expanded memory is managed the same way, whether it resides on an add-in board or is emulated in extended memory. The Lotus/Intel/Microsoft EMS defines a 64K segment of memory that resides between 640K and 1MB. This page frame is a window into expanded memory (see Figure 1). Just after the application program starts executing, it allocates a certain number of 16K pages of expanded memory for its own use. Four pages of expanded memory can be mapped into the expanded memory page frame at one time. By mapping pages in and out of the page frame, the program can access any area of the expanded memory that it allocated. The EEMS allows the page frame to reside at any unused memory address between 0K and 1,024K. Theoretically, this allows a page frame length of 1MB. Practical considerations, such as DOS and application programs, which use conventional memory, and the BIOS and ROM on add-in boards, which use memory above 640K, restrict the page frame to fewer than the possible 64 pages. Generally, in a typical AT system with an EGA, the maximum number of mappable pages that DOS doesn't rely on is six 16K pages. When the EMM software is installed, the user selects where in memory (above 640K) the page frame resides. The page frame address is user-selectable, so that if another device uses memory at a particular address, the user can then relocate the page frame. Checking for Memory Before an application program can use expanded memory, it must determine if expanded memory and the EMM are present. There are two methods of determining if the EMM is present: the open-handle technique and the get-interrupt-vector technique. Because the EMM is implemented as a device driver, in the open-handle technique the program issues an open handle command (DOS function 3FH) to determine whether the EMM device driver is present. In the get-interrupt-vector technique, the program issues a get-interrupt- vector command (DOS function 35H) to get the contents of interrupt vector array entry number 67H. The pointer thus obtained accesses information that tells the program whether the EMM is installed. The get-interrupt-vector technique is easier to implement. Most programs can use either technique, but if a program is a device driver or if it interrupts DOS during file system operations, it must use the get-interrupt-vector technique. Residents, Transients Application programs that use expanded memory can be classified as either resident or transient. A transient application program is resident only as long as it executes. When it is finished running, the memory it used is available for other programs. Examples of resident application programs include spreadsheets, word processors, and compilers. A resident application program remains in memory after it executes. Resident application programs are usually invoked by a hardware interrupt, such as a keystroke, or a software interrupt, such as a RAMdisk. Pop-up desktop programs, RAMdisk drives, and print spoolers are examples of resident application programs. Resident programs and transient programs handle expanded memory differently. Resident programs may interrupt transient programs that might be using expanded memory, so resident programs must save and restore the state of the page-mapping registers when they use expanded memory. Transient programs don't interrupt other programs, so they don't need to save and restore state. A resident program typically keeps the EMM handles assigned to it and the expanded memory pages allocated to it by the EMM until the system is rebooted. A transient program, in contrast, should return its handle and pages just before it exits to DOS. EMM Functions The EMM functions, summarized in Figure 2, provide the tools that application programs need to use expanded memory. Functions 1 through 7 are general-purpose functions. Functions 8 and 9 are for interrupt service routines, device drivers, and other memory-resident software. Functions 10 and 11 are reserved. Functions 12 through 14 are for utility programs. Finally, Function 15 is for multitasking operating systems, although it can be used for interrupt service routines as easily as Functions 8 and 9. To use expanded memory, programs must perform these steps in the following order: 1. Check for the presence of the EMM by using the get-interrupt-vector or open-handle techniques. 2. Check whether the EMM's version number is valid (only if the application is EMM version-specific)──Function 7 (Get EMM Version). 3. Determine if enough unallocated expanded memory pages exist for the program──Function 3 (Get Unallocated Page Count). 4. Save the state of expanded memory hardware (only if it is a resident program)──Function 8 (Save Page Map) or Function 15 (Get/Set Page Map). 5. Allocate the number of 16K expanded memory pages needed by the program──Function 4 (Allocate Pages). 6. Map the set of expanded memory pages (up to four) into the page frame──Function 5 (Map Handle Page). 7. Determine the expanded memory page frame base address──Function 2 (Get Page Frame Address). 8. Read/write to the expanded memory segment within the page frame, just as you read or write to conventional memory. 9. Deallocate the expanded memory pages when the program is finished using them──Function 6 (Deallocate Pages). 10. Restore the state of expanded memory hardware (only if it is a memory- resident program)──Function 9 (Restore Page Map) or Function 15 (Get/Set Page Map). Each EMM function's number is passed in register AX. The EMM will return the function's status in the same register. Programs use Int 67 to invoke the EMM. This works like DOS Int 21: preload certain registers and issue an Int 67. All required registers are rigidly specified, and certain conventions exist; for example, the AX register always returns status. Programming The following two examples contain programs that have both code and data in expanded memory. The first example (written in Microsoft C, Version 3.00) illustrates how expanded memory can be used to save and restore data. The main program (see Figure 3) calls a series of subprocedures that allocate one 16K page of expanded memory, save the video RAM area (the user's screen) to expanded memory, clear the screen, and then restore the screen from expanded memory. The program assumes the user has a monochrome display adapter operating in text mode (nongraphics) and video page zero is displayed. The program contains four subprocedures. The detect_emm subprocedure (see Figure 4) determines whether the EMM software is installed. If it is installed, the subprocedure returns to the caller. If the EMM software isn't installed, the subprocedure generates an error message and exits the program. The get_expanded_memory_page subprocedure (see Figure 6) returns a pointer to the expanded memory page and a 16-bit tag or handle associated with that page. The subprocedure uses the EMM to allocate a page of expanded memory. If an unallocated page exists, the procedure allocates it and maps it in and returns the EMM handle that is associated with that page. The check_status subprocedure (see Figure 5) is called after each EMM function to verify that no EMM errors have occurred. The release_expanded_memory_page subprocedure (see Figure 7) releases expanded memory pages by deallocating the handle associated with those pages. The second example illustrates one program loading another program into expanded memory, which is especially applicable for developers of terminate- and-stay-resident (TSR) applications. Both programs are written in Microsoft Macro Assembler, Version 4.0. The first program, expanded_memory_dispatcher_kernel (see Figure 8), loads a set of subprocedures into expanded memory, from where they can be invoked at any time. The set of loaded subprocedures is called a pseudo-overlay. This program loads only one pseudo-overlay and immediately invokes all the subprocedures contained in it. You can easily load as many pseudo-overlays as you want by allocating additional pages in expanded memory, mapping up to four of the newly allocated pages into the page frame, and then loading additional pseudo-overlays. The program has one subprocedure, test_for_EMM (see Figure 9), which determines whether the EMM software is installed and returns the appropriate status. The kernel program loads the program OVERLAY.EXE (see Figure 10) into expanded memory. A pseudo-overlay can't be larger than 64K because of the four-page EMM page frame, so the developer must decompose the program into separate modules that contain code or data no larger than 64K. You can have up to 8MB of expanded memory and, therefore, up to 128 overlays. Although the DOS "load overlay" function (DOS function 4B03H) is used to load the pseudo-overlays, the code and any data loaded remain resident after the load takes place. The subprocedures contained in the pseudo-overlay can be accessed by using the list of pointers returned to the kernel by the initialization code in the pseudo-overlay. The pseudo-overlay program has five subprocedures. If the pseudo-overlay program is invoked from the command line, then the command_line_entry_point subprocedure (see Figure 11) tells the user that this is a pseudo-overlay and thus can't be executed. The initialization subprocedure (see Figure 13) is critical. The kernel calls this subprocedure after the program is loaded. The initialization subprocedure passes back to the kernel the data segment environment, a count of the number of callable subprocedures in the overlay, and a far pointer to each subprocedure. The sum and diff subprocedures are examples of typical applications. The sum subprocedure (see Figure 14) adds the numbers in the AX and DX registers and displays the result, while the diff subprocedure (see Figure 15) subtracts the numbers in the AX and DX registers and displays the result. The display_result procedure (see Figure 16) converts the result into printable ASCII form and then displays it. The pseudo_overlay program places data into expanded memory. The data segment for the pseudo_overlay program is shown in Figure 12. The common data area for both programs is shown in Figure 17. To Get EMS If you're interested in developing application programs that use expanded memory, call Intel for a free copy of the Lotus/Intel/Microsoft Expanded Memory Specification. In the continental United States, but outside Oregon, call (800) 538-3373. In Oregon, Alaska, Hawaii, or outside the United States (except Canada), call (503) 629-7354. In Canada, call (800) 235-0444. For more information on the AST EEMS, contact the AST Product Information Center at (714) 863-1480. Figure 1: The Lotus/Microsoft EMS defines a 64K segment of memory that resides between 640K and 1MB. 16M┌─────────────────────┐ │ │ │ │ ┌───────────┐ │ │ ┌──┴─────────┐ │ ≈ Extended Memory ≈ ┌──┴──────────┐ │ │ │ │ ┌───┴───────────┐ │ │ │ │ │ │ Expanded │ │ │ │ 1M├─────────────────────┤ .│ Memory │ │ │ │ │ Reserved by IBM │ . │ │ │ │ │ 896K├─────────────────────┤ . │ │ │ │ │ ├─────────────────────┤ . │ Available to │ │ │ │ │ 64K Page Frame │. │DOS application│ │ │ │ ├─────────────────────┤ . │ programs │ │ │ │ 786K├─────────────────────┤ . │ adhering to │ │ │ │ │ Reserved by IBM │ . │ LIM Expanded │ │ │ │ 640K├─────────────────────┤ .│ Memory │ │ ├─┘ │ Conventional │ │ Specification │ ├─┘ │ Memory │ │ ├─┘ │ │ └───────────────┘ │ Managed by DOS │ 0K└─────────────────────┘ Figure 2: EMM Functions EMM functions provide the tools that application programs need to use expanded memory. ╓┌────────┌──────────────┌──────────────────┌────────────────────────────────╖ Function Function AX Action Number Name Register 1 AH: 40 Get Status Returns a status code to tell you whether the EMM is present and the hardware/software is working Function Function AX Action Number Name Register the hardware/software is working correctly. 2 AH: 41 Get Page Frame Gives the program the location of Address the page frame. 3 AH: 42 Get Unallocated Tells the program the number of Page Count unallocated pages and the total number of pages in expanded memory. 4 AH: 43 Allocate Pages Allocates the number of expanded memory pages requested by the program; assigns a unique EMM handle to the set of pages allocated. 5 AH: 44 Map Handle Page Maps the specified logical page in expanded memory to the Function Function AX Action Number Name Register in expanded memory to the specified physical page within the page frame. 6 AH: 45 Deallocate Pages Deallocates the pages currently allocated to an EMM handle. 7 AH: 46 Get EMM Version Returns the version number of the EMM software. 8 AH: 47 Save Page Map Saves the contents of the page mapping registers of all expanded memory boards. 9 AH: 48 Restore Page Map Restores the contents of the page mapping registers. 10 AH: 49 Reserved. Function Function AX Action Number Name Register 11 AH: 4A Reserved. 12 AH: 4B Get EMM Handle Returns the number of active EMM Count handles. 13 AH: 4C Get EMM Handle Returns the number of pages Pages allocated to a specific EMM handle. 14 AH: 4D Get All EMM Returns the active EMM handles Handle Pages and the number of pages allocated to each one. 15 AH:4E; AL:00 Get/Set Saves and restores the mapping AH:4E; AL:01 Page Map context of the active EMM handle. AH:4E:AL:02 Figure 3: Main Program The main program allocates one 16K page of expanded memory, saves the video RAM area to expanded memory, clears the screen and then restores the screen to expanded memory. #include #include #define EMM_INT 0x67 /* EMM interrupt number */ #define GET_PAGE_FRAME_BASE 0x41 /* EMM func = get page frame base address * #define GET_FREE_COUNT 0x42 /* EMM Func = get unallocated pages count * #define ALLOCATE_PAGES 0x43 /* EMM Func = allocates pages */ #define MAP_PAGES 0x44 /* EMM Func = map pages */ #define DEALLOCATE_PAGES 0x45 /* EMM Func = deallocate pages */ #define GET_INT_VECTOR 0x35 /* DOS func = get interrupt vector */ #define DEVICE_NAME_LEN 8 /* Number of chars in device driver name field */ #define VIDEO_RAM_SIZE 4000 /* Total bytes in video RAM (char/attr) */ #define VIDEO_RAM_BASE 0xB0000000 /* Video RAM start address (MDA) */ union REGS input_regs, output_regs; /* Regs used for calls to EMM and DOS */ struct SREGS segment_regs; unsigned int emm_status; /* Status returned by EMM */ main () { unsigned int i; long target_time, current_time; char *video_ram_ptr = {VIDEO_RAM_BASE}; /* Pointer to video RAM */ unsigned int emm_handle; /* EMM handle */ char *expanded_memory_ptr; /* Pointer to expanded memory */ /* Ensure that the Expanded Memory Manager software is installed on the user's system. */ detect_emm(); /* Get a page of expanded memory. */ get_expanded_memory_page (&expanded_memory_ptr, &emm_handle); /* Copy the current video RAM contents to expanded memory. */ memcpy (expanded_memory_ptr, video_ram_ptr, VIDEO_RAM_SIZE); /* Clear the screen to nulls. */ memset (video_ram_ptr, '\0', VIDEO_RAM_SIZE); /* Delay for 1 second so the user can see the blanked screen. */ time (¤t_time); target_time = current_time + 1; while (current_time < target_time) { time (¤t_time); } /* Restore the video RAM contents from expanded memory. */ memcpy (video_ram_ptr, expanded_memory_ptr, VIDEO_RAM_SIZE); /* Deallocate the expanded memory page */ release_expanded_memory_page (emm_handle); exit(0); } Figure 4: Detect_EMM Subprocedure The detect_emm subprocedure determines whether the EMM driver software is installed. detect_emm () { static char EMM_device_name [DEVICE_NAME_LEN] = {"EMMXXXX0"}; char *int_67_device_name_ptr; /* Determine the address of the routine associated with INT 67 hex. */ input_regs.h.ah = GET_INT_VECTOR; /* DOS function */ input_regs.h.al = EMM_INT; /* EMM interrupt number */ intdosx (&input_regs, &output_regs, &segment_regs); int_67_device_name_ptr = (segment_regs.es * 65536) + 10; /* Create ptr to device name field */ /* Compare the device name with the known EMM device name. */ if(memcmp(EMM_device_name,int_67_device_name_ptr,DEVICE_NAME_LEN) !=0) { printf ("\x07Abort: EMM device driver not installed\n"); exit(0); } } Figure 5: Check_Status Subprocedure The check_status subprocedure is called after each EMM function to make sure that no EMM errors have occurred. check_status (emm_status) unsigned int emm_status; { static char *emm_error_strings[] = { "no error", "EMM software malfunction", "EMM hardware malfunction", "RESERVED", "Invalid EMM handle", "Invalid EMM function code", "All EMM handles being used", "Save/restore page mapping context error", "Not enough expanded memory pages", "Not enough unallocated pages", "Can not allocate zero pages", "Logical page out of range", "Physical page out of range", "Page mapping hardware state save area full", "Page mapping hardware state save area already has handle", "No handle associated with the page mapping hardware state save area", "Invalid subfunction" }; /* IF EMM error, THEN print error message and EXIT */ if (emm_status != 0) /* IF EMM error... */ { emm_status -= 0x7F; /* Make error code zero-based */ printf ("\x07Abort: EMM error = "); /* Issue error prefix */ printf ("%s\n", emm_error_strings[emm_status]); /* Issue actual error message */ exit(0); /* And then exit to DOS */ } } Figure 6: Get_Expanded_Memory_Page Subprocedure The get_expanded_memory_page subprocedure returns a pointer to the expanded memory page and a 16-bit tag or handle associated with that page. get_expanded_memory_page (expanded_memory_ptr_ptr, emm_handle_ptr) unsigned int *emm_handle_ptr; /* 16 bit handle returned by EMM */ char *(*expanded_memory_ptr_ptr); /* Pointer to expanded memory page */ { unsigned int page_frame_base; /* Expanded memory page frame base */ unsigned int physical_page = {0}; /* Physical page number */ /* Get unallocated pages count. */ input_regs.h.ah = GET_FREE_COUNT; /* EMM function */ int86x (EMM_INT, &input_regs, &output_regs, &segment_regs); emm_status = output_regs.h.ah; check_status(emm_status); /* Check for errors */ if (output_regs.x.bx < 1) /* Check unallocated page count */ { printf ("\x07Abort: insufficient unallocated expanded memory pages\n"); exit(0); } /* Allocate the specified number of pages. */ input_regs.h.ah = ALLOCATE_PAGES; /* EMM function */ input_regs.x.bx = 1; /* Number of pages to allocate */ int86x (EMM_INT, &input_regs, &output_regs, &segment_regs); emm_status = output_regs.h.ah; check_status(emm_status); /* Check for errors */ *emm_handle_ptr = output_regs.x.dx; /* Get EMM handle */ /* Map the logical page into physical page 0. */ input_regs.h.ah = MAP_PAGES; /* EMM function */ input_regs.h.al = 0; /* Logical page number */ input_regs.x.bx = physical_page; /* Physical page number */ input_regs.x.dx = *emm_handle_ptr; /* EMM handle */ int86x (EMM_INT, &input_regs, &output_regs, &segment_regs); emm_status = output_regs.h.ah; check_status(emm_status); /* Check for errors */ /* Determine the page frame address. */ input_regs.h.ah = GET_PAGE_FRAME_BASE; /* EMM function */ int86x (EMM_INT, &input_regs, &output_regs, &segment_regs); emm_status = output_regs.h.ah; check_status(emm_status); /* Check for errors */ *expanded_memory_ptr_ptr = (output_regs.x.bx * 65536) + (physical_page * 16 * 1024); /* Set the expanded memory ptr */ } Figure 7: Release_Expanded_Memory_Page Subprocedure The release_expanded_memory_page subprocedure releases the expanded memory pages by de-allocating the handle associated with those pages. release_expanded_memory_page (emm_handle) unsigned int emm_handle; /* Handle identifying which page set to deallocate */ { /* Release the expanded memory pages by deallocating the handle associated with those pages. */ input_regs.h.ah = DEALLOCATE_PAGES; /* EMM function */ input_regs.x.dx = emm_handle; /* EMM handle passed in DX */ int86x (EMM_INT, &input_regs, &output_regs, &segment_regs); emm_status = output_regs.h.ah; check_status(emm_status); /* Check for errors */ } Figure 8: Kernel Model The pseudo-overlay is loaded into expanded memory by the kernel. The kernel then calls the initialization procedure within the pseudo-overlay that returns a data structure to the kernel. The data structure describes the first object that will be located in expanded memory starting at the data and extra segments of the pseudo-overlay, the number of subprocedure entry points in the pseudo-overlay, and a list of far pointers to each of the subprocedures contained in the pseudo-overlay. The developer must establish a convention for the sequence of far pointers and what the procedures they point to do. Other information could be passed in this structure as well, for example, number and types of parameters that are required by the subprocedures in the pseudo-overlay. This example uses a literal to determine the maximum number of far pointers that may be passed. To allocate additional space for a larger number of entries, simply increase the value of max_proc_entries. The example assumes a maximum of 64 entries can be returned. CODE SEGMENT PARA PUBLIC 'CODE' ORG 100h ASSUME CS:CODE, DS:DATA, ES:NOTHING, SS:STACK max_proc_entries EQU 64 pseudo_over_struct STRUC proc_data_segment DW ? proc_extra_segment DW ? proc_entry_count DW ? proc_entry_ptr DD max_proc_entries DUP (?) pseudo_over_struct ENDS main PROC NEAR MOV AX, DATA ; Segment initialization MOV DS, AX check_for_emm_loaded: CALL test_for_EMM ; Use the "interrupt vector" JE get_emm_page_frame ; technique to determine JMP emm_err_exit ; whether EMM is loaded get_emm_page_frame: MOV AH, 41h ; Get the page frame base INT 67h ; address from EMM OR AH, AH JZ allocate_64K JMP emm_err_exit allocate_64K: MOV exp_mem_segment, BX ; Allocate 4 pages of expanded MOV AH, 43h ; ed memory for this example. MOV BX, 4 ; More can be allocated INT 67h ; depending on the number of OR AH, AH ; overlays to be loaded. JZ map_64K ; Actually, in this case, JMP emm_err_exit ; only a single page is ; required because the example ; pseudo-overlay is extremely ; small. map_64K: MOV handle, DX ; Map in the first 4 logical MOV CX, 4 ; pages at physical pages 0 map_pages_loop: ; through 3 MOV AH, 44h ; logical page 0 at MOV BX, CX ; physical page 0 DEC BX ; logical page 1 at MOV AL, BL ; physical page 1 MOV DX, handle ; logical page 2 at INT 67h ; physical page 2 OR AH, AH ; logical page 3 at LOOPE map_pages_loop ; physical page 3 JE init_load_struct ; If additional overlays were JMP emm_err_exit ; required, each overlay ; would be loaded after ; mapping and a new set of ; logical pages would be ; mapped at the same ; physical pages. init_load_struct: MOV ES, exp_mem_segment ; Initialize pseudo-overlay MOV DI, 0 ; environment and procedure MOV CX, (SIZE pseudo_over_struct) ; pointer area. This structure MOV AL, 0 ; begins at the page REP STOSB ; frame segment address. MOV AX, (SIZE pseudo_over_struct) ; Compute the load address ADD AX, 000Fh ; within expanded memory for AND AX, 0FFF0h ; the overlay. The address is MOV CX, 4 ; rounded up to the next SHR AX, CL ; higher paragraph boundary ADD AX, exp_mem_segment ; immediately following the MOV parm_block.load_segment, AX ; pseudo-overlay environment MOV parm_block.reloc_factor, AX ; & procedure pointer ; structure. This computation ; tion takes into account ; the maximum number of ; procedure entry points ; which the pseudo-overlay ; is going to return to ; this program. MOV WORD PTR entry_point[0], 100h ; Build .COM file entry MOV WORD PTR entry_point[2], AX ; point MOV AH, 4Bh ; Load the pseudo-overlay MOV AL, 03h ; using the DOS "load LEA DX, pseudo_over_name ; overlay" function PUSH DS POP ES LEA BX, parm_block INT 21h JC emm_err_exit PUSH DS ; Transfer control to the PUSH ES ; loaded pseudo-overlays CALL DWORD PTR entry_point ; initialization code POP ES POP DS OR AH, AH JZ call_over_procedures JMP emm_err_exit call_over_procedures: MOV ES, exp_mem_segment ; As an example of passing MOV BX, 0 ; control to a procedure MOV DI, 0 ; existing in expanded MOV CX, ES:[BX].proc_entry_count ; memory, each procedure JCXZ deallocate_exp_memory ; contained in the overlay will ; be called in sequence. ; Obviously, a single procedure ; could be called just as ; easily. pseudo_over_call_loop: PUSH BX PUSH CX PUSH DI PUSH ES PUSH DS LDS AX, ES:[BX+DI].proc_entry_ptr MOV WORD PTR CS:tp_ent_ptr[0], AX MOV WORD PTR CS:tp_ent_ptr[2], DS MOV AX, 123 ; Pass 2 numbers to MOV DX, 23 ; the procedures MOV DS, ES:[BX].proc_data_segment ; Set up pseudo-overlays MOV ES, ES:[BX].proc_extra_segment ; segment environment CALL DWORD PTR CS:tp_ent_ptr ; Call each procedure POP DS POP ES POP DI POP CX POP BX ADD DI, 4 ; Adjust index to the next LOOP pseudo_over_call_loop ; procedure (4 bytes long) ; pointer & loop till all ; have been called deallocate_exp_memory: MOV AH, 45h ; Return the allocated MOV DX, handle ; pages to the expanded INT 67h ; memory manager OR AH, AH JNZ emm_err_exit exit: MOV AH, 4Ch ; Return a normal exit code MOV AL, 0 INT 21h emm_err_exit: MOV AL, AH ; Display the fact that MOV AH, 09h ; an EMM error occurred LEA DX, emm_err_msg ; Go to the normal exit INT 21h JMP exit tp_ent_ptr DD ? ; CS relative far pointer ; used for transfer to the main ENDP ; procedures in the ; pseudo_overlay Figure 9: Procedure to Test for the Presence of EMM This procedure tests for the presence of the EMM in the system. The carry flag is set if the EMM is present. The carry flag is clear if the EMM is not present. test_for_EMM PROC NEAR MOV AX, 3567h ; Issue "get interrupt vector" INT 21h MOV DI, 000Ah ; Use the SEGMENT in ES ; returned by DOS, place ; the "device name field" ; OFFSET in DI. LEA SI, EMM_device_name ; Place the OFFSET of the EMM ; device name string in SI, ; the SEGMENT is already in DS. MOV CX, 8 ; Compare the name strings CLD ; Return the status of the REPE CMPSB ; compare in the ZERO flag RET test_for_EMM ENDP CODE ENDS Figure 10: Pseudo-overlay Module The kernel loads the pseudo-overlay into expanded memory.The kernel calls the initialization procedure within the pseudo-overlay. The initialization procedure returns a data structure to the kernel. The data structure describes the first object that will be located in expanded memory starting at the page frame segment address. It contains the data and extra segments of the pseudo-overlay, the number of subprocedure entry points in the pseudo-overlay, and a list of far pointers to each of the subprocedures contained in the pseudo-overlay. CODE SEGMENT PARA PUBLIC 'CODE' ASSUME CS:CODE, DS:DATA ORG 100h actual_proc_entries EQU 2 overlay_entry_struct STRUC proc_data_segment DW ? proc_extra_segment DW ? proc_entry_count DW ? proc_entry_ptr DD actual_proc_entries DUP (?) overlay_entry_struct ENDS Figure 11: Procedure to Identify Overlay This procedure merely informs a user that this is the overlay and cannot be executed from the command line. command_line_entry_point PROC NEAR MOV AX, DATA ; Set up local data MOV DS, AX ; segment LEA DX, overlay_err_msg ; Display overlay error MOV AH, 09h ; message INT 21h MOV AX, 4C00h ; Exit back to DOS INT 21h command_line_entry_point ENDP Figure 12: Data Segment for the Pseudo-overlay Module This is the data segment for the pseudo-overlay program. DATA SEGMENT PARA PUBLIC 'DATA' sum_msg DB 0Dh, 0Ah, 'Sum of numbers = ', '$' diff_msg DB 0Dh, 0Ah, 'Difference of numbers = ', '$' overlay_err_msg DB 'Overlay cannot be executed via the command line$' powers_of_ten DW 10000, 1000, 100, 10, 1 DATA ENDS END command_line_entry_point Figure 13: Pseudo-overlay Data Structure Initialization Procedure The initialization subprocedure is called by the kernel after the program is loaded. It passes to the kernel the data segment environment, a count of the number of callable subprocedures in the overlay, and a far pointer to each subprocedure. initialization PROC FAR MOV AX, DATA ; Set up a local MOV DS, AX ; data segment MOV AH, 41h ; Get the page INT 67h ; frame segment OR AH, AH ; address from EMM JNZ error MOV ES, BX ; Create pointer MOV DI, 0 ; to the page frame ; segment address MOV ES:[DI].proc_data_segment, DS ; Return local data MOV ES:[DI].proc_extra_segment, DS ; & extra segment ; back to the kernel MOV WORD PTR ES:[DI].proc_entry_count, 2 ; Return the number ; of callable ; procedures MOV WORD PTR ES:[DI].proc_entry_ptr[0], OFFSET sum ; Return MOV WORD PTR ES:[DI].proc_entry_ptr[2], SEG sum ; pointer to each MOV WORD PTR ES:[DI].proc_entry_ptr[4], OFFSET diff ; local callable MOV WORD PTR ES:[DI].proc_entry_ptr[6], SEG diff ; procedure in the ; pseudo-overlay ; back to kernel exit: MOV AH, 0 ; Set status in AH ; = passed error: RET ; Return status ; in AH Figure 14: Procedure to Add AX and DX This procedure adds AX and DX and displays the result. sum PROC FAR ADD AX, DX ; Add numbers PUSH AX ; Display sum message LEA DX, sum_msg MOV AH, 09h INT 21h POP AX CALL display_result ; Display sum RET sum ENDP Figure 15: Procedure to Subtract AX and DX This procedure subtracts AX and DX and displays the result. diff PROC FAR SUB AX, DX ; Subtract numbers PUSH AX ; Display difference message LEA DX, diff_msg MOV AH, 09h INT 21h POP AX CALL display_result ; Display difference RET diff ENDP Figure 16: Procedure to Display Number in AX in Decimal This procedure displays the number in AX in decimal display_result PROC NEAR LEA DI, powers_of_ten MOV CX, 5 display_loop: XOR DX, DX ; Divide the number passed DIV WORD PTR [DI] ; in AX by descending powers of ten ADD AL, '0' ; Convert digit to ASCII PUSH DX ; Output the digit MOV DL, AL MOV AH, 02h INT 21h POP AX ADD DI, 2 LOOP display_loop RET display_result ENDP Figure 17: Data and Stack Segment for the Kernel and the Pseudo-overlay This is the common data area for the kernel and psuedo-overlay programs. DATA SEGMENT PARA PUBLIC 'DATA' emm_err_msg DB 'EMM error occurred$' ; EMM diagnostic message pseudo_over_name DB 'OVERLAY.EXE', 0 ; Name of pseudo-overlay EMM_device_name DB 'EMMXXXX0' ; Standard EMM device name exp_mem_segment DW ? ; Temp for expanded ; memory page frame ; segment address handle DW ? ; Temp for handle allocated ; to the kernel entry_point DD ? ; Far pointer to the ; entry point for a .COM ; file parm_block_struct STRUC ; Structure definition load_segment DW ? ; for a "load overlay" reloc_factor DW ? ; parameter block parm_block_struct ENDS parm_block parm_block_struct <> ; The actual parameter ; block DATA ENDS STACK SEGMENT PARA STACK 'STACK' local_stack DW 256 DUP ('^^') STACK ENDS END main ████████████████████████████████████████████████████████████████████████████ Keep Track of Your Windows Memory With FREEMEM Charles Petzold☼ For the beginning Microsoft(R) Windows programmer, even simple, do-nothing Windows programs seem to be forbiddingly long and complex. You may have concluded that all Windows applications are monstrous collections of code. This is not true. Take the program called FREEMEM, for example, a complete and useful Microsoft Windows application in fewer than 100 lines of C code. The FREEMEM program displays the amount of available free memory in an icon at the bottom of the Windows screen. The free memory value is updated every second and is consistent with the value that is shown in the MS-DOS Executive "About" box. The short length of FREEMEM helps to clarify the structure of Windows applications and allows us to discuss extensively some details of Windows programming. And since FREEMEM is certainly an unusual Windows program, we'll explore a few of its tricks as well. Overall Structure FREEMEM.C contains only two functions: WinMain (see Figure 1) and WndProc (see Figure 2). Similar functions are found in most Windows applications. WinMain is the entry point to FREEMEM. WinMain is devoted mainly to performing all of the preliminary initialization chores needed to create and display a window. The WndClass structure passed to the RegisterClass function defines the window class. The most important item in the WndClass structure is lpfnWndProc, which specifies the function within FREEMEM that processes messages from Microsoft Windows. This is the WndProc function shown in Figure 2. The CreateWindow, ShowWindow, and UpdateWindow functions all cause Windows to generate messages to the WndProc function. Within the WndProc function, these messages are identified by names beginning with the letters WM. The names are simply macro identifiers defined in WINDOWS.H that conveniently hide the actual numbered codes. WndProc sorts out the messages through a case statement. Only a few of the many messages that Windows sends to WndProc are handled directly. The rest are sent on to the DefWindowProc function within Windows for default processing. CreateWindow will cause Microsoft Windows to generate a WM_CREATE message. ShowWindow usually generates a whole sequence of messages, among them WM_SIZE and WM_ERASEBKGND. These messages cause the window to be displayed on the screen and the background of the client area that is to be erased. When FREEMEM calls the function UpdateWindow, Microsoft Windows generates a WM_PAINT message, which instructs WndProc to paint the client area of the window. Following the UpdateWindow call, FREEMEM enters a message loop. The GetMessage call retrieves a message from FREEMEM's message queue. If no messages to FREEMEM are available, control can pass to another Windows application. Currently, Windows' nonpreemptive multitasking guarantees that a call to GetMessage is the only time that FREEMEM, in effect, stops running. It regains control when FREEMEM's message queue contains some messages and other applications' message queues are empty. It's really a little more complex than this, but I'll explain it later. TranslateMessage translates keystroke messages into character code messages. Although FREEMEM doesn't care about the keyboard, this translation is necessary to provide a keyboard interface with FREEMEM's system menu. DispatchMessage then sends the message to the WndProc procedure. FREEMEM will terminate when GetMessage retrieves a WM_QUIT message from the queue. The GetMessage function returns a zero in this case, and FREEMEM drops out of the message loop. So far, this routine is normal for most Windows applications, but let's take a look at the details. Creative Use of Icons Unlike most Windows programs, FREEMEM is intended to be displayed only as an icon. Once FREEMEM is running, you can use the keyboard or mouse to open the icon into a regular tiled window, but it won't give you any more information in that form. Most Windows applications have static pictorial icons. The programmer usually creates these icons with the ICONEDIT utility supplied with the Microsoft Windows Software Development Kit. The icon file is referenced by a name in the resource script file. When the window class structure is constructed, the icon is specified by the statement WndClass.hIcon = LoadIcon (hInstance, (LPSTR)szAppName) ; The LoadIcon function loads the icon from the section of the .EXE file where all the resources are stored and assigns a handle to it. (I'm assuming here that the icon has the same name as the application and that szAppName is a pointer to a character string with that name.) If instead you use the following statement: WndClass.hIcon = NULL ; then the application is responsible for drawing the icon. This lets your application create a dynamic icon that you can alter. The CLOCK application included with Microsoft Windows uses this same technique to display the current time even when the window is an icon. From the program's perspective, a NULL icon is just a tiny window that you can draw on in the same way that you draw on the client area of a normal window. If you need to know when your application is becoming an icon, you can find out the information from the WM_SIZE message. For instance, CLOCK takes note of this change so it can eliminate the second hand and update the clock every minute in the iconic form. Forcing the Icon Normally, to execute a Windows application from the MS-DOS Executive, you either press the Enter key while the cursor is on the program name, double- click the program name with the mouse, or select File Run from the menu. If you want to load a program as an icon instead, you just press Shift-Enter when the cursor is on the program name or select File Load from the menu. However, with FREEMEM, it doesn't matter what you do──it always loads as an icon. Most Windows applications execute the function ShowWindow (hWnd, nCmdShow) ; shortly before entering the message loop. The nCmdShow variable is passed to the program as a parameter to WinMain. If you RUN a program from the MS-DOS Executive, nCmdShow is set as equal to the handle of the window that the application is replacing on the display, which is usually the handle of the MS-DOS Executive main window. If you LOAD an application as an icon, nCmdShow is set equal to SHOW_ICONWINDOW. Your application usually doesn't have to figure this out; it simply passes this parameter on to ShowWindow. However, you don't have to use the nCmdShow variable with ShowWindow. In FREEMEM, we use this line instead: ShowWindow (hWnd, SHOW_ICONWINDOW) ; This forces the window to appear as an icon regardless of the value of nCmdShow. You can achieve other results with this technique. If you have an application that you always want to appear first as a "zoomed" full-screen display, you can use ShowWindow (hWnd, SHOW_FULLSCREEN) ; You can even force FREEMEM to occupy a particular icon position at the bottom of the display. If you replace the existing ShowWindow call in FREEMEM with ShowWindow (hWnd, (int) 0xFF8F) ; the icon will be positioned in icon slot 15, all the way over at the right of the display. This syntax is somewhat obscure, but it is documented in the Programmer's Reference manual included with the Software Development Kit. Keeping It an Icon As I mentioned before, you can easily use the keyboard or mouse to open up FREEMEM into a regular tiled window. If you'd like to prevent that and ensure that FREEMEM is always displayed as an icon, just add these two lines to the WndProc function: case WM_QUERYOPEN: break ; A reasonable place for these lines would be right above the line that reads "case WM_DESTROY." Now when you try to use the mouse to open the icon, the icon jumps back to the bottom of the display. These two lines may not seem to be doing very much, but a closer look will reveal what they're up to. Microsoft Windows will send a WM_QUERYOPEN message to a program when it wants to open an icon into a window. The documentation for the WM_QUERYOPEN message states that a windows procedure such as WndProc must return zero to prevent the icon from being opened. Under normal circumstances, the WM_QUERYOPEN message is passed on to the DefWindowProc function, which returns a nonzero value. (The DefWindowProc routine is provided with the Windows Software Development Kit in the file WINDWP.C.) Windows then opens the icon. With the two lines shown above, however, WndProc will return a value of zero for a WM_QUERYOPEN message. So, when Windows asks, "Do you want to be opened?", WndProc answers, "Zero," which in this case means "No thanks." The Timer Messages FREEMEM continues to update its display of free memory even while other applications are running. It performs this through the use of the Microsoft Windows timer, which sends WM_TIMER messages to the window procedure. In this respect, FREEMEM is similar to the CLOCK application. The timer permits a form of multitasking without requiring the application to hog precious processing time. In its WinMain function, FREEMEM requests a timer from Windows with the following statement: if (!SetTimer (hWnd, 1, 1000, NULL)) return FALSE ; The third parameter to SetTimer indicates that FREEMEM wants a WM_TIMER message every 1,000 milliseconds, or once a second. If SetTimer returns zero, it means that Windows cannot assign a timer to the program. If you've ever tried to determine how many CLOCK applications can be loaded and running in Windows at the same time, you know that the limit is 15. Any additional SetTimer calls will return a zero. Actually, there is a way to get more than 15 timers out of Windows, but it's more complex. If FREEMEM cannot get a timer, the WinMain function must return a zero value (the value of FALSE), which simply terminates the application. As you'll note from the two other "return FALSE" lines in WinMain, FREEMEM also terminates if a previous instance is already running or if FREEMEM cannot register the window class. While prohibiting multiple instances of FREEMEM from running isn't necessary, there is really no reason to run FREEMEM more than once. If you prefer that FREEMEM tell you why it must terminate when SetTimer returns zero, you can instead use the code shown in Figure 3. This is more informative, if not exactly friendly. The second parameter in the SetTimer call is a "timer ID." When WndProc receives the WM_TIMER message, the wParam parameter contains this value. By using different timer IDs, a Windows application can set multiple timers and do different processing for each one. FREEMEM must also use the timer ID to relinquish the timer with the KillTimer call when WndProc receives a WM_DESTROY message. WM_TIMER messages are not asynchronous. Specifying 1,000 milliseconds in the SetTimer call does not guarantee that the window procedure will receive a WM_TIMER message precisely every second. The WM_TIMER messages are placed in the normal message queue and synchronized with all the other messages. If another application is busy for more than a second, FREEMEM does not get any WM_TIMER messages during that time. FREEMEM receives its next WM_TIMER message from the queue only when the other applications yield control by calling GetMessage, PeekMessage, or WaitMessage. In fact, like WM_PAINT messages, WM_TIMER messages are handled by Microsoft Windows as low-priority items. I said before that control passes to other applications only when a program's message queue is empty. Actually, if a program's message queue contains only WM_PAINT or WM_TIMER messages, and the message queue of another program contains any messages other than WM_PAINT or WM_TIMER, control will pass to the other application anyway. Moreover, Windows does not continue loading up the message queue with multiple WM_TIMER messages if another application is running during this time. In that case, Windows combines several WM_TIMER messages in the message queue into a single message so that the application doesn't get a bunch of them all at once. While Windows' handling of WM_TIMER messages is adequate for a program like FREEMEM, keep these points in mind if you ever do a clock application like the Windows CLOCK. A 1,000-millisecond timer value does not guarantee 3,600 WM_TIMER messages over the course of an hour. When you get a WM_TIMER message, you'll want to determine the real time either with a C function or through MS-DOS directly. You can't keep time yourself solely with WM_TIMER messages. Calculating Memory When FREEMEM receives a WM_TIMER message, it must determine the amount of free memory. This little chore was the most difficult part of programming FREEMEM. I knew the information was available because the MS-DOS Executive "About" box showed a free memory value. But Microsoft Windows has around 400 function calls, and finding the one you need is sometimes difficult. It turns out that the MS-DOS Executive gets a free memory value by calling GlobalCompact with a parameter of zero. Windows applications normally use GlobalCompact to generate some free memory from the global heap. If necessary, Windows compacts memory and frees up segments marked as discardable when GlobalCompact is called. (The global heap is comprised of memory outside of the program's local data segment.) A close reading of the documentation of GlobalCompact reveals that "if [the parameter] is zero, the function returns a value but does not compact memory." Thus, the value that FREEMEM displays is really a potential free memory size rather than actual free memory. It represents how much memory a Windows application can get from the global heap if it needs it. If you're familiar with the HEAPWALK utility included with the Software Development Kit, you can spend lots of time attempting to reconcile the value displayed by FREEMEM with value displayed by HEAPWALK. It's definitely not obvious. In general, you'll find that GlobalCompact returns approximately the size of the large chunk of free memory in the middle of the memory space maintained by Microsoft Windows, plus some discardable segments above that block of free memory. However, it stops short of the discardable code segment that contains FREEMEM's code. Obviously, GlobalCompact cannot discard FREEMEM's code if FREEMEM is calling the function. Drawing the Icon In FREEMEM, the section of WndProc that handles WM_TIMER messages does not itself update the icon display. Instead, the line InvalidateRect (hWnd, NULL, TRUE) ; notifies Windows that the contents of FREEMEM's window are now invalid and must be repainted. The InvalidateRect function causes Windows to put a WM_PAINT message in the message queue. WndProc actually updates the window only when it receives this WM_PAINT message. There are alternatives to this method. Instead of calling InvalidateRect, WM_TIMER can update the window directly. It would need to retrieve a display context with the statement hDC = GetDC (hWnd) ; and would then call GetClientRect and DrawText, just as in the WM_PAINT logic, but using hDC as the display context rather than ps.hdc. WM_TIMER would finally release the display context with ReleaseDC (hWnd, hDC) ; While this is certainly valid, I chose not to do it this way. WndProc must also process WM_PAINT messages which occur for reasons other than an updated free memory value. For instance, if you use the mouse to move the FREEMEM icon around the screen, Windows will eventually send FREEMEM a WM_PAINT message so that FREEMEM can repaint the icon. So, we'd either have to duplicate the paint code or move it to a separate subroutine. The InvalidateRect call really does this for us by generating the WM_PAINT message. It allows us to use the same paint code for all painting jobs. WM_PAINT messages are considered low priority by Microsoft Windows. They always go to the back of the message queue and are fetched from the queue only when no other message is present. There is a way around this, however. After calling InvalidateRect, WM_TIMER could then call UpdateWindow (hWnd) ; just as we did in WinMain. This instructs Windows to call WndProc directly with a WM_PAINT message, without going through the message queue. But that's really not necessary here. I wanted to keep FREEMEM operating as a low-priority task. If something else was going on in another application, I didn't want FREEMEM to hog time continually by repainting the icon. The DrawText function used by FREEMEM when processing the WM_PAINT message is very convenient for simple word-wrapped text. Note that the line within the DrawText code that reads strlen (strcat (itoa (mem, buffer, 10), "K Free")) does the same thing as the more-common construction sprintf (buffer, "%dK Free", mem) However, sprintf is a big function. By using strlen, strcat, and itoa instead, we can reduce the size of FREEMEM.EXE by about 3K──after all, every little bit helps. Creating FREEMEM Aside from the FREEMEM.C source code, you need two other files to create FREEMEM. The first is the module definition file, which is called FREEMEM.DEF (see Figure 4). The FREEMEM.DEF file contains the standard information that you'll see in definition files for most small Windows applications. The WINSTUB.EXE program specified in the STUB line of FREEMEM.DEF is included with the Software Development Kit. The program runs when you execute FREEMEM outside of Windows. It simply displays the message "This program requires Microsoft Windows." If you want to, you can create a normal MS-DOS program that displays a free memory value comparable to the value that CHKDSK calculates. If you decide to name this normal MS-DOS program DOSFREE.EXE, for instance, you can specify STUB 'DOSFREE.EXE' in the FREEMEM.DEF file. This little trick permits FREEMEM the use of both on the MS-DOS command level and inside Windows. FREEMEM.EXE would thus contain two related, but quite dissimilar, programs in one file. The FREEMEM "make-file" (which is called simply FREEMEM, without an extension) is shown in Figure 5. When you execute MAKE FREEMEM the FREEMEM.C source code will be compiled and linked with the appropriate Windows and C libraries, along with additional information from the FREEMEM.DEF module definition file. No Resource Script? Unlike many sample Windows applications, FREEMEM has no resource script file, which is a file with the extension .RC. FREEMEM doesn't need one. FREEMEM has no menu, no pictorial icon, and no dialog boxes. The only use we would have for a resource script is for storing text strings, but the only text string FREEMEM uses is the one with the word "Free" in it. Of course, for longer programs, the use of the resource script for all text strings is highly recommended since it will eventually make translation of the program into another language easier. But to tell the truth, loading strings stored as resources is a real nuisance for small programs. FREEMEM is short enough that translation is not a big problem, and we'd risk being meretricious by using a resource script for that single text string. Uses in Development I like to keep FREEMEM loaded and running in normal Microsoft Windows use just to give me an idea of how close Windows is getting to a low-memory situation. But it has more value in program development. If you're developing a Windows application, keep a watch on the FREEMEM icon during testing. If the free memory size keeps shrinking, you may be neglecting to free up some allocated memory within the program. You may then want to call on the HEAPWALK utility, also known as Luke Heapwalker, for some assistance in tracking down those orphaned memory blocks. However, don't assume that FREEMEM will display identical values before an application is run and after it terminates. The complexity of Windows' memory management makes this unlikely for most large programs. Figure 1: WinMain Function of FREEMEM.C The first half of FREEMEM.C contains the WinMain function, which performs necessary initialization and contains the message loop. /* FreeMem.C -- Windows application that displays free memory */ #include /* all Windows functions */ #include /* itoa */ #include /* strcat & strlen */ long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ; int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow) HANDLE hInstance, hPrevInstance ; LPSTR lpszCmdLine ; int nCmdShow ; { static char szAppName [] = "FreeMem" ; WNDCLASS WndClass ; HWND hWnd ; MSG msg ; if (hPrevInstance) return FALSE ; WndClass.hCursor = LoadCursor (NULL, IDC_ARROW) ; WndClass.hIcon = NULL ; WndClass.cbClsExtra = 0 ; WndClass.cbWndExtra = 0 ; WndClass.lpszMenuName = NULL ; WndClass.lpszClassName = (LPSTR) szAppName ; WndClass.hbrBackground = (HBRUSH) GetStockObject (WHITE_BRUSH) ; WndClass.hInstance = hInstance ; WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = WndProc ; if (!RegisterClass ((LPWNDCLASS) &WndClass)) return FALSE ; hWnd = CreateWindow ((LPSTR) szAppName, (LPSTR) szAppName, WS_TILEDWINDOW, 0, 0, 0, 0, (HWND) NULL, (HMENU) NULL, (HANDLE) hInstance, (LPSTR) NULL) ; if (!SetTimer (hWnd, 1, 1000, NULL)) return FALSE ; ShowWindow (hWnd, SHOW_ICONWINDOW) ; UpdateWindow (hWnd) ; while (GetMessage ((LPMSG) &msg, NULL, 0, 0)) { TranslateMessage ((LPMSG) &msg) ; DispatchMessage ((LPMSG) &msg) ; } return (int) msg.wParam ; } Figure 2: WindProc Function of FREEMEM.C long FAR PASCAL WndProc (hWnd, message, wParam, lParam) HWND hWnd ; unsigned message ; WORD wParam ; LONG lParam ; { static int mem, lastmem ; char buffer [20] ; PAINTSTRUCT ps ; RECT rect ; switch (message) { case WM_TIMER: mem = (int) (GlobalCompact (0L) / 1024) ; if (mem != lastmem) InvalidateRect (hWnd, NULL, TRUE) ; lastmem = mem ; break ; case WM_PAINT: BeginPaint (hWnd, (LPPAINTSTRUCT) &ps) ; GetClientRect (hWnd, (LPRECT) &rect) ; DrawText (ps.hdc, (LPSTR) buffer, strlen (strcat (itoa (mem, buffer, 10), "K Free")), (LPRECT) &rect, DT_WORDBREAK) ; EndPaint (hWnd, (LPPAINTSTRUCT) &ps) ; break ; case WM_DESTROY: KillTimer (hWnd, 1) ; PostQuitMessage (0) ; break ; default: return DefWindowProc (hWnd, message, wParam, lParam) ; } return (long) 0 ; } Figure 3: Alternate SetTimer Logic This code causes FREEMEM to tell you why it must terminate when SetTimer returns zero. if (!SetTimer (hWnd, 1, 1000, NULL)) { MessageBox (hWnd, "Hey! Too many timers!", NULL, MB_OK) ; return FALSE ; } Figure 4: Module Definition File FREEMEM.DEF The module definition file FREEMEM.DEF is used by LINK4 for information about the program not implied by the source code. NAME FreeMem DESCRIPTION 'Free Memory Display by Charles Petzold' STUB 'WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS WndProc @1 Figure 5: FREEMEM Make-File This file, called simply FREEMEM, without an extension, is the "make-file" for generating FREEMEM.EXE from the source code. # Make file for FREEMEM -- assumes Microsoft C 4.0 # ------------------------------------------------ freemem.obj : freemem.c cl -c -d -Gsw -Os -W2 -Zdp freemem.c freemem.exe : freemem.obj freemem.def link4 freemem, /align:16, /map/line, slibw, freemem mapsym freemem ████████████████████████████████████████████████████████████████████████████ CodeView:Debugging Philosophy And Step-by-Step Technique David Norris and Michael J. O'Leary Most MS-DOS programmers at one time or another have had to fire up the MS- DOS debugger DEBUG to patch another program or peek into memory. Some have actually debugged programs with it. All kidding aside, DEBUG has its uses, but it is usually woefully inadequate for doing any serious debugging, as shown by the many MS-DOS debuggers now on the market──we've counted more than a dozen. While debuggers are second only to editors as a computer "religious" issue, arguments often tend to revolve around user-interface issues or "my debugger has more features than your debugger" comparisons. Yet these are secondary to the main consideration: providing facilities that lend themselves well to established debugging techniques. If DEBUG fails, it is because it has no commands that adapt themselves to user queries such as "what is the value of iter?" and no commands like "stop when iter reaches 10." During the design of the Microsoft CodeView(TM) debugger, we surveyed these debugging techniques and created a requirements document for the "ultimate" debugger. By iterating these techniques and requirements, we hope to give you a fuller understanding of the complex world of debugging and aid the future evaluation of debugging tools, thus providing a debugger yardstick. We'll also introduce CodeView's philosophy and evolution, demonstrating its strengths and weaknesses with examples. Our goal is not to teach CodeView commands and syntax (that's what the manual is for), but rather to give you an understanding of the concepts behind the commands. Techniques The different debugging techniques can be categorized into two major groups: internal and external. External debugging implies an inspection of some sort, without specific interaction with the program being debugged. Internal debugging, on the other hand, requires the use of debugging tools that exercise control over the debugged program's environment. External debugging via code reviews by peers can uncover almost three- quarters of all software bugs for some kinds of errors. While the old adage about an ounce of prevention applies here, it has been our experience that scheduling peer code reviews usually plays second fiddle to meeting other product deadlines. Another external technique is simple post-mortem debugging. What did the program output? What should it have output? Simple formatting errors are usually found this way. Embedding debugging aids directly in the program is an anachronism from a time when adequate debugging aids were not available. In this case, the programmer decides where in the code he wants something done, such as testing an assertion (is this pointer valid?) and conditional compilation. None of these external methods requires any specialized debugging tools. On the other hand, internal debugging techniques involve the use of tools that give the user varying degrees of control over his program as it executes. This usually means a debugger, but includes a class of code-flow analysis tools such as profilers, event monitors, and path generators. A debugger can be internal to the program or it can be separate. Generally, the latter approach is used, as it is more desirable to debug the target program by itself, with minimal interference from the debugger. Requirements Let's examine some of the requirements of a debugger. This list contains general ideas about debugging, not specific commands. Size and Speed Efficiency: Obviously, you want a debugger to take up as little space as possible (especially when memory space is at a premium) and to be as fast as possible. Minimum impact on program being debugged: The debugger should not affect the behavior of the program it is debugging. As some have discovered (especially on unprotected systems, such as MS-DOS), the program may crash by itself but run fine when being debugged. This has been called the Heisenberg uncertainty principle when applied to debugging──the result being a "Heisenbug." Execution Control: The user should be provided with a wide range of commands for controlling the execution of his program, mostly in the form of "trap on x." Figure 1 lists the possible event traps. State Manipulation: Users of debuggers need to be able to meaningfully examine the state of a running, suspended, or dead program, including the determination of the cause of suspension, procedure call history, examination and alteration of code and data in its source form, and execution history. User interface: The debugger should be invisible to the user. The use of the debugger should be so comfortable and intuitive that users feel they are "in" the program they are debugging. It should be like driving a car──you don't drive the car around the corner, you just go around the corner. Portability: Programmers have met with various degrees of success in making debuggers portable. Still, it's always nice not to have to learn a new debugger when you move to a new computer system. CodeView Philosophy Microsoft needed a unified debugging tool strategy. The SYMDEB debugger, a symbolic extension of the MS-DOS DEBUG debugger, was partially able to fulfill the need for language independence, but attempts to make it portable or to separate user-interface code from system-dependent code would have been extremely difficult. We realized an entirely new debugger was needed. We knew we wanted the debugger to be language-independent; one hallmark of the Microsoft language series is that it is object-compatible. Therefore, a Microsoft debugger should be able to debug whatever programs users could come up with. We also needed the debugger to have a portable user interface and system-dependent code so that an SDB-compatible debugger could be created for the XENIX operating system. (See "ISLAND Design" diagram below) Above all, we wanted a user interface that everyone could be comfortable with. This proved to be the most difficult design task, as every individual had his own idea about user interfaces. We went through many iterations of the basic screen snapshots before we arrived at the present version. The main problem was presenting the proper information to the user given the limited amount of screen real estate. Thus, there are only two horizontal border bars on the basic screen (the original screen had borders around every window). In fact, the second horizontal bar, called the dialog bar, can be eliminated completely, allowing 22 lines of source information on the screen, and more on the EGA. The registers were placed to minimize the impact on the source window. Also, a pull-down menu system was used because it is a friendly, intuitive interface, and it conserves screen space when it is not used. Finally, we decided that rather than creating a new debugger syntax for people to learn, we would adhere to the syntax of the SYMDEB and DEBUG debuggers in the dialog window. This caused, and still causes, confusion for a number of reasons──mainly because the default radix can change. Our difficulty was that while we wanted compatibility with DEBUG commands (default radix of hex), we also wanted to use an expression evaluator that mirrored C as closely as possible (default decimal radix). To complicate matters, the default radix doesn't affect the dump command, which has its own set of types (byte, word, unsigned, etc.). We felt that programmers who dealt strictly in C might never use the lower-level commands and that assembly-language programmers wouldn't be using C expressions. Introducing CodeView CodeView is currently only available with the Microsoft C Compiler, Version 4.00 and Microsoft FORTRAN Optimizing Compiler, Version 4.0, although versions are planned for other languages and operating systems. After a brief overview of its features we'll go into detail on a few specific functions in an actual debugging session. A sample CodeView screen is shown in Figure 3☼. Four windows are available for display of information: the source window, the dialog window, the register window, and the watch window. The central and most important window is the source window and can be thought of as a read-only editor attached to debugging functions. For example, the user can scroll and search through source files, and the current line and active breakpoints are shown by a background blue bar and intense video that are imposed on the source text. Navigation through the program, which can be composed of more than one source file, is possible through a number of commands. The user can locate a program label, and the source window loads and displays the source file containing that label. The dialog window was our answer to commands that did not lend themselves to a simpler windowing scheme, such as variable-length data dumps. The dialog window can be thought of as the "glass-teletype" interface to CodeView; indeed, a CodeView switch (/T) forces CodeView to act as its predecessor SYMDEB did, in that a nonwindowing user interface is presented. The dialog window views only a portion of dialog text; scrolling commands that are similar to those in the source window can be used to examine data. The register window simply presents the machine registers. The only command available here is the ability to alter a flag value by clicking on it with the mouse. Finally, the watch window is available for viewing variables and data of interest to the user, so that it doesn't have to typed over and over. The format of the watch window entries is identical to the way they would be displayed in the dialog window; also, the command syntax for variable/data display between these two windows is orthogonal. For example, "?i" displays the variable i in the dialog window, while "w?i" displays it in the watch window. The pull-down menu was designed to mimic the Windows pull-down menu system, including the mouse and the keyboard. Let's look at a debugging session with CodeView in order to demonstrate some debugging concepts. Debugging WHERE WHERE is a utility program written in C (see Figure 2) and compiled with the Microsoft C 4.00 compiler, using the -Zi switch to enable CodeView information. It takes as arguments one or more program names and tries to find them as MS-DOS would try to execute them. Thus, the tool is useful in finding out whether the command "foo" would mean "C:\BIN\FOO.EXE" or "E:\FOO.BAT." Knowing that Real Programmers get it right the first time, we'll run WHERE and hope it doesn't trash our hard disk. A>WHERE WHERE.EXE WHERE.EXE 6224 10-30-86 11:23a Let's verify our program's output by using DIR: A>DIR WHERE.EXE WHERE.EXE 6224 10-30-86 11:23p Obviously, a.m./p.m. is incorrect. A quick glance at the printstats routine verifies that there is a typo; "<=" should have been ">=". Again, since we're Real Programmers, we'll remember to fix the bug later and forget the number of times we have forgotten to fix the bug later. But we haven't fully exercised WHERE. It should be able to find ambiguous names such as "DIR," and perform the name search in the same manner that MS- DOS would: .COM, .EXE, then .BAT. Let's try a simple case: A>WHERE WHERE A> WHERE can't find itself. How about an ambiguous or unambiguous filename somewhere in the path (but not in the current directory)? A>WHERE COMMAND A> A>WHERE COMMAND.COM A> It looks like we have two bugs; we can try to find them at the same time. Let's debug: A>CV WHERE COMMAND Figure 4☼ shows the initial CodeView screen. The register window is initially off when debugging C programs, and the watch window is initially empty. Typing "t" or clicking on the menu item "Trace!" with the mouse causes a blue highlighted line to appear in the source window on line 17, indicating the current instruction. Tracing one more time makes line 23 the current instruction. You're probably wondering why the current instruction started one line after "main" and why it skipped 5 lines. The C Compiler will only output line numbers for those lines that contain executable code. Lines 18 through 22 are declarations that produce no code, while the declaration in line 23 has an assignment. We can show the relationship between source lines and emitted code (see Figure 5☼) by typing "u main", which instructs CodeView to display both source-code and assembly-language instructions in the source window. Type F3 to return CodeView to source-only mode. Single-stepping As the program is so brief, let's single-step through the program and see what happens. We can put variables of interest in the watch window so we don't have to retype them every time we want to see their values. The first interesting line is line 32; we can execute up to that line by typing "g .32" or by clicking the right mouse button on that line. Single-step by pressing the F10 key. To see the result of the getenv function, type "?envptr". CodeView displays the value 17552:4636; CodeView evaluated "envptr" as a C expression and so returned the value of the pointer (an address). The value of the expressions "&envptr", "envptr", and "*envptr" are different, and CodeView evaluates them properly. We are interested in looking at the null-terminated string pointed to by envptr, so our expression should be >w?envptr,s to put the string in the watch window (see Figure 6☼). Pointers and pointer expressions in C can be difficult to learn. Inexperienced users may find CodeView confusing in this respect, but no more so than writing in C itself; CodeView is a great learning tool for this purpose. The next two lines alter the variables ptr and namebuf, so let's trace the code and put the variables in the watch window. This can be done all in one line by separating the commands with semicolons: >t 2;w?ptr,s;w?namebuf,s The current line will execute the search_for_file function. We can use the "binary search," or divide-and-conquer, approach to debugging by stepping over the function using the Pstep command: >p The screen now looks like Figure 7☼. Note that both envptr and ptr are incorrect. The search_for_file function should not have been able to alter these variables, which are local to main. What we need is a way to stop execution when the variables change. CodeView accommodates us with the watchpoint and tracepoint commands. We don't know whether the string pointed to by envptr or envptr itself changed, so we need to trap on both conditions. First, we can reset the state of the program to just before the occurrence of the bug by typing >l;g .40 To stop execution when envptr itself changes, we can use the watchpoint command: >wp?main.envptr!=4636 We had to specify the function to which envptr belonged since we would be accessing the variable outside its normal scope (that of main). Alternatively, we could have used the Tracepoint command and typed "tpw envptr". In either case, CodeView is somewhat deficient because the user has to know either the initial value of envptr or the size of the pointer. We can break on the second condition by typing >tpb *envptr This command causes a trap when the first character pointed to by envptr changes. Now you continue by clicking on "Go!". After a couple of seconds──CodeView watchpoints and tracepoints can be quite slow, as they are emulated in software, although CodeView can utilize debugging hardware──the CodeView screen returns at line 82. The code just executed was a call to the strcat library routine. Before blaming the run- time library, though, let's take a look at its return value in fullpath: >?fullpath,s Oops (see Figure 8☼). The search_for_file function has been appending file extensions via strcat ad nauseum; we are overrunning the allocated length of the namebuf array and destroying the contents of other local variables of main. We know that envptr is being changed; unfortunately, the Microsoft C Compiler did not order main's local variables in the order given. In this case they are ordered envptr, ptr, found, namebuf, envpath, then pathbuf──completely different from the declaration order. The bug is easily fixed by removing the previous file extension using the strchr function just before the end of the file extension loop: *strchr(fullpath, '.') = '\0'; Summary CodeView is not new. Many of its commands and features have already been seen in other debuggers. What is new about CodeView is the number of commands and degree of integration that it has provided. What is most important is that programmers have a full understanding of the nature of debugging and how they can best utilize the language and debugging tools available to them. ISLAND Design The ISLAND source code is divided into three major sections: user interface, core code, and system-dependent routines. By linking the core code with a given user interface and system-dependent code, a variety of debuggers can be created. ┌──────────────────────────┐ │ CodeView │ User Interface ► ┌───┴──────────────────────┬───┘ │ SDB │ ┌───┴──────────────────────┬───┘ │ │ │ │ Core Code ► │ ISLAND │ │ │ │ │ ├──────────────────────────┤ │ DOS 2.x/3.x │ └───┬──────────────────────┴───┐ │ XENIX-286 │ System Dependent ► └───┬──────────────────────┴───┐ │ XENIX-386 │ └───┬──────────────────────┴───┐ │ Others │ └──────────────────────────┘ Figure 1: Debugger Event Traps Before/After execution of a specific instruction Before/After successful branch Single-step Data/Code Read/Write Procedure and Function Prologue/Epilogue Cross-Process Figure 2: WHERE Utility Program /* WHERE - shows where DOS is finding your executable */ #include #include #include #include #define NUMEXTS 3 #define MAXFILELEN 12 #define MAXPATHLEN 65 struct stat file_stat; char *getenv(char *); char *strchr(); main (argc, argv) int argc; char **argv; { int found; char pathbuf[MAXPATHLEN]; char namebuf[MAXFILELEN]; char *ptr; char *envpath; char *envptr; if (argc < 2) { printf("Usage: where filename[.ext]\n"); exit(2); } if (!(envptr = getenv("PATH"))) envptr = ""; /* Loop thru filenames given */ while (ptr = *++argv) { strcpy(namebuf, ptr); /* Try current directory first */ if (!(found = search_for_file(namebuf, strchr(namebuf, '.')))) { /* Loop thru paths in path environment variable */ envpath = envptr; while (*envpath) { ptr = pathbuf; /* Copy path into buffer */ while (*envpath && *envpath != ';') *ptr++ = *envpath++; /* skip path separators */ while (*envpath == ';') ++envpath; /* Don't append a '\' if looking thru a root directory */ if (*(ptr-1) != '\\') *ptr++ = '\\'; *ptr = '\0'; strcat(pathbuf, namebuf); if (search_for_file(pathbuf, strchr(namebuf, '.'))) { printstats(pathbuf); break; } } } else printstats(namebuf); } exit(found == 0); } search_for_file (fullpath, specific_file) char *fullpath; int specific_file; { int found, next_ext; static char *extension[NUMEXTS] = {".COM", ".EXE", ".BAT"}; if (specific_file) found = !stat(fullpath, &file_stat); else { for (next_ext = 0; next_ext < NUMEXTS; ++next_ext) { /* try .com .exe .bat extensions */ strcat(fullpath, extension[next_ext]); if (found = !stat(fullpath, &file_stat)) break; } } return(found); } /* * print out fullname from passed string, and stats (size, date..) * from global struct file_stat. */ printstats (fullname) char *fullname; { struct tm *tmptr; tmptr = localtime(&file_stat.st_atime); printf("%s \t%ld\t%2d-%2d-%2d\t%d:%02d%c\n", strupr(fullname), file_stat.st_size, tmptr->tm_mon+1, tmptr->tm_mday, tmptr->tm_year, (tmptr->tm_hour < 13 ? tmptr->tm_hour : tmptr->tm_hour-12), tmptr->tm_min, (tmptr->tm_hour <= 12 ? 'p' : 'a')); } ████████████████████████████████████████████████████████████████████████████ Page Description Languages: High-Level Languages for Printer Independence ─────────────────────────────────────────────────────────────────────────── Also see the following illustrations of Page Description Languages: Adobe System's PostScript A Comparison of Three Page Description Languages ─────────────────────────────────────────────────────────────────────────── Steve Rosenthal☼ As printers get steadily smarter, more and more of them are supporting command languages similarly enhanced in power and flexibility. In particular, page description languages (PDLs) are gaining popularity as a preferred way of telling printers how to put images on paper. More than just a simple list of what elements to be printed next, PDLs offer a precise and formalized means of controlling printer output. PDL language statements, which are normally created and sent to the output device invisibly to the end user, can include a combination of operations, objects, dot locations, and references to typographic elements such as fonts. You can, in fact, think of PDLs as high-level languages for printer output, compared to the machine language or assembly language approaches that have until recently been the rule. Not surprisingly, many of the advantages and disadvantages of using a PDL parallel those for using a high-level language in other types of programming, and many of the arguments about which PDL is best echo similar discussions about the relative virtues of computationally- oriented programming languages. Unfortunately, you can't run out and get all those promised advantages on very many devices or from many programs just yet. Of the three major page description languages that are likely to become major forces in the personal computer market in the immediate future──PostScript, Interpress, and Document Description Language (DDL)──only PostScript and Interpress have been implemented on any production output devices so far, and only PostScript has been implemented on hardware and software for personal and desktop computers. So for now, we'll have to settle for an overall discussion of why PDLs might be attractive, what's needed in a PDL and its implementation, and a description of the three major languages that appear to be serious market contenders. Device Independence Backers of PDLs say that the foremost advantage we can expect from them is device independence. Theoretically, an application or system can produce one single set of statements in a page language for output on any output device that supports that page language, rather than having to produce a different version for each brand, resolution level, and technology of output. Using a PDL is like using a single language, such as BASIC, that works on a wide variety of different machines. Even though the internal operation of the various printing devices may be different, that's taken care of during the interpretation of the PDL statements by each individual printer. From the outside, each device seems to be logically equivalent. Yet having a single logical connection between programs and printers affects interfacing in a dramatic way: the number of drivers that must be written is reduced to the sum of the programs and printers at issue rather than their product. Furthermore, an application written to PDL standards continues to support new devices that were not available at the time that the software was created──a feat that is at best difficult with more-traditional approaches. While that's the theory, in practice the results aren't quite as pristine. In all the announced PDLs, there are some device dependencies that sometimes must be considered, just as there often are in programming BASIC or Pascal. You can usually take a PDL file meant for one device and output it on another, but you may have to clean up a few differences or accept some slight artifacts of the retargeting process. Furthermore, every page description language may not be optimized for all new print technologies, so future compatibility cannot really be totally guaranteed. Given that most users replace computer equipment when the available alternative becomes economically more worthwhile, not when the previous generation fails to work any longer, inefficiency can be as much of a limiting factor as inability. Ease of connectability could also vanish as a major advantage unless a single page description language, or at least a small set of languages, becomes the de facto standard. Right now, several different PDLs are competing for market favor, along with several typographic description languages that take a more character-oriented and smaller-region view of the page. In addition, all of these languages compete against the virtual device approach, which has all applications produce their output as a series of calls to functions on a theoretical device. The environment then translates these calls to the actual operations needed by each printer. This last method, by the way, is the one implemented in most operating environments, such as Microsoft's own Windows. Still, given all these cautions, all it takes to convince most people that the PDL approach to universality is worthwhile is one typesetting project on the Macintosh. Because many applications on the Mac (including Microsoft Word and Microsoft Works) can produce output in PostScript, the same files can be run off first inexpensively on the Apple LaserWriter, then, when proofed and ready, can be run in finished form on a true high-quality typesetting machine from Allied Linotype. Low Overhead Economy of description is the second major advantage that page description languages offer. In all current PDLs, an application can describe intended output, where appropriate, as a series of objects rather than characters or individual dots and lines. An object-oriented description often takes much less time to communicate, as well as less memory buffer space and other system resources. For example, if an application wants to draw a circle in the output with a PDL, it generally orders up a circle object of a certain center and radius, with the outline done in the current line width setting. In contrast, if it wants to draw a circle with ordinary dot graphics, the application has to send every point on the circumference to the printer. The same idea applies to boxes, lines, shading, and even typographic fonts. Because the PDL-equipped printer starts with a large store of knowledge about elements of graphics and type, an application usually needs to send only the pointers rather than the objects themselves. In most PDLs, fonts are considered a special class of graphics. All the regular PDL commands that apply to graphics──such as those governing movement, rotation, enlargement, tint, and so on──apply to typographic characters as well. In addition, all PDLs have font-handling commands that take into account the special nature of type. On the flip side, being able to work with regular objects is no help at all for photographic information, scanned images, or other graphic elements that, unlike line drawings, have no simple tonal structure. In fact, if a PDL has to communicate such a graphic as a series of dots that require a lot of overhead for their expression, it can take the PDL more time and space to communicate this type of picture than would a simpler approach. Division of labor is another benefit that PDLs offer. Since page languages describe output in terms of objects, an originating system that contains those objects internally does not have to translate from object to ink dot──a process called rasterizing. On most printer technologies, rasterizing means translating an object into a series of on/off dots placed in a raster pattern of successive lines sweeping across the page. Rasterizing is also a necessary step in printing various sizes, styles, and weights of type. However, rasterizing is computationally intensive, and──if the page creation sequence and timing are to be decoupled from the printing technology──it needs at least one bit of memory for every possible dot placed on the page. Using a PDL permits the main processor to generate the prerasterized image; the printer can then rasterize and store the resulting bit map. PDLs that are full programming languages can also let the printer do some of the computations that precede rasterization. Since PDL statements can describe operations as well as objects, programs have the ability to send complex expressions to the output device; these expressions, once they are reduced to simpler form, will generate either the described object or the parameters of the described object. The output device will then be able to do the computations while the main processor proceeds with other work. Three Main Ingredients Basically, you need three components to implement a PDL in some workable form. The first component is the quality of language itself, which allows it to meet a number of criteria pertaining to function. For example, a page description language must be clearly and completely defined if it is to be used as a consistent standard across multiple machines and systems. It must be sufficiently fast and efficient to make the overhead involved in using it affordable. If it isn't, developers will simply bypass the PDL to write native code drivers for each device. A workable PDL should also have some sort of hierarchical or layered structure. Although not every printer has every feature, users who buy the more capable devices want to put any extra functionality to use; hence a good PDL must be able to take care of most business with a set of core commands and functions, yet still retain the potential to make use of additional capabilities. To be device independent, a PDL must also maintain a universal coordinate system visible to the applications program that is translating the points to the printer's actual coordinate system for output. The coordinate system must be sufficiently large to cover the maximum defined page size at the maximum workable resolution of any supported output device. A successful PDL must also be perceived as acceptable. The primary value of any such language depends in large part on its pervasiveness, so any language must appeal to a large number of software developers and output device manufacturers. Products that are not seen as acceptable, no matter how technically proficient, won't reach the critical mass needed to make their use worthwhile. In most cases, the bid for popularity has led to having the PDL languages themselves placed into the public domain──that is, the actual verbs and structures that comprise the language can be used by anyone, even though the programs that create the code or translate it into printed images are proprietary. The Interpreter The second component of the PDL system is the interpreter and printer controller, which translate the PDL statements into the actual dot information that the printer's marking engine needs to produce the output image. Translation is usually done on the fly by an interpreter (compiling usually isn't worthwhile because documents frequently change between every printing). Strictly speaking, the output of the interpretation process is some result sent back to the host system, but its "side effect" is the creation of the actual sequence of bits needed to control the printer marking engine. The major PDLs for the PC market are all based on threaded stack languages, so the interpreters and languages all have a very Forth-like flavor. They use reverse Polish notation (RPN) and store almost all their data and working values on a push-down stack. Because the computational power needed to make this translation quickly is so great, most of the PDL-equipped printers are more high-powered than the main system for which they are ostensibly peripherals. They also need a lot of memory. All current implementations are for page printers (where the complete page is translated and stored in dot form before the actual marking process begins), and all expect to find a complete bit map in which to store the resulting page image. For the current standard of 300 dots per inch on an 8 1/2- by 11-inch page, that means slightly more than a megabyte. PDL-based printers also need a large ROM space or a great deal of extra memory for downloading the interpreter. A typical PDL interpreter for a laser or similar xerographic page printer takes up several hundred kilobytes. The third component, producing the translation code, is also a huge challenge. Page description languages are intended as a form of communication between program and printer and are not generally designed for ease of human use. Instead, applications are expected to produce the code to be sent to the printer without any direct human intervention. The PDL code emitter that most applications require is somewhat more complex than a standard printer driver, but still within the reach of most serious programmers. For maximum efficiency, the application must specify its output as certain types of objects, and there are similar incentives toward handling type in more compatible ways. It is possible to write your own PDL code emitter, and maybe a dozen or more firms have already included such a facility in current software products. The backers of most PDLs will also write the needed code sections on a contract basis or will recommend a firm that does so. A common code generator can also be shared by multiple applications that run in a common operating environment using a virtual device interface. On the Macintosh, for example, system and applications programs write to the Quickdraw ROM routines, and a single code generator that is loaded as a "printing resource" then changes the ROM calls to PostScript code. Similarly, in Windows and Digital Research's GEM, programs that want to write PostScript don't have to have their own embedded PostScript code. A system driver turns virtual device interface calls into the code needed for a PostScript-based printer. Given all these requirements, it's not surprising that the major page description languages exhibit many similarities. But in the case of these three languages, the resemblance is more than coincidental. PostScript, Interpress, and DDL are all outgrowths of work done at the Xerox Palo Alto Research Center (PARC). So, although the exact syntax and precise list of features characterizing each language have diverged because each language was developed at a different corporate home, the overall spirit and approach are very much the same. PostScript PostScript, from Adobe Corp., was the first page description language to be implemented for personal computer software and peripherals and is still the only one that is being delivered with actual commercial products. Although its first widely available applications were on the Apple Macintosh and the Apple LaserWriter printer, PostScript is now backed by scores of software packages on various machines and operating systems, as well as close to a half dozen or more different printers. PostScript is in the public domain, and there is even a functionally compatible language/interpreter set combination that can be mixed and matched with PostScript sold by Control-C Software of Portland, Oregon. Most implementations so far have been done by Adobe. As a language, PostScript is particularly rich in general programming capabilities as well as in graphic and typographic support elements; more than 250 PostScript verbs cover arithmetic, logical, and control categories, as well as graphics. PostScript is written entirely in ASCII (printable) characters, making it easier to debug final code and to send output descriptions across simple communications links. On the other hand, this slows the transmission of nongeometric images, since they must be translated from bit maps to hex representation and back to be sent as printable characters. PostScript supports outline (vector) fonts and has provision for both built-in fonts and downloadable supplements. All fonts are defined as one single point unit in height and are then scaled to any selected size. The PostScript language definition supports color as well as shading, but so far all the delivered devices have been monochrome. For creating halftone (shaded) images, such as those found in photographs, PostScript includes a number of facilities for creating small regular patterns. PostScript applications can be written to handle documents of most any length, but the language won't supply much built-in help for the more complex projects. Formatting beyond the structure of a single page, not included in the first language definition, is currently implemented as a series of structured comment lines. For debugging or constructing composite projects, PostScript contains a fair number of file and input control statements. The AppleWriter PostScript interpreter, for example, can be used interactively to let you improve various formulations of your procedures. Interpress Interpress is Xerox Corp.'s nomination as a standard page description language. So far, it has been implemented on several larger laser printers and on many Xerox minicomputer applications as part of the Xerox Network Architecture (XNS) system. However, applications in the personal computer field should begin to appear very shortly. Xerox's Ventura Publisher will support Interpress, as will Microsoft Windows. Xerox says that several personal computer printer manufacturers plan to announce Interpress support, starting in the first quarter of 1987. The language itself is also in the public domain. Compared with PostScript, Interpress has more facilities for controlling overall document structure and distribution but slightly less power for general computation. The language can be written in a Forth-like ASCII representation, but the actual code sent to Interpress printers is a binary representation. Bidirectional translators are available for debugging, testing, and learning. Using binary for Interpress files obviously saves space, but perhaps more importantly, it cuts transmission time for complex documents. Because Interpress was explicitly designed for use with networks, transmitting a file more than once was expected to be the rule rather than the exception. The most important document control feature is page independence. In an Interpress output file, each page is dependent only on an initial header and information local to that single page. This feature guarantees that printers that actually image in some order other than first through last will produce the right output, and many of the higher performance printers that do two- sided copying or binding do indeed print documents in various orders. Interpress also features an explicit mechanism for setting imaging priority. On many output devices the order with which overlapping images are laid down matters to the final result; on others, printing can be done faster by using an order that may be different from the order in which the instructions arrive. Hence, Interpress allows the user to specify strict sequencing when needed. As part of an overall network strategy, Interpress also links to a large number of complementary output-related Xerox standards. Many of these define a standard solution to issues that have yet to be addressed in the DOS and Macintosh environments. That includes standards for font handling and naming, encoding of scanned and other raster images, and character coding for extended character sets. DDL Document Description Language (DDL) is Imagen Corp.'s entry into the standard page description language derby. As the latest of the three contenders, and as Imagen's second generation of page description languages, it naturally combines features from the prior languages as well as adding some innovations of its own. DDL devices and programs will not be publicly available until sometime this year, but agreements between Imagen, Hewlett-Packard, and Microsoft ensure the language a substantial launch. It will be Hewlett-Packard's language of choice for sophisticated applications speaking to the HP Laserjet printer and will be supported as an output driver in Microsoft Windows. DDL shares with Interpress an enhanced emphasis on overall document structure as well as on the geometry of each page and perhaps goes even further in this direction. It also makes more-extensive use of caching to reduce the time required in repetitively translating objects from description to dot form. Document control includes page independence and an explicit mechanism for specifying page order for output. The latter feature is particularly important when multiple logical pages are imaged onto each physical page, and the resulting sheets have to be folded and assembled into a complete document. Caching attempts to use all available memory, which is constantly getting cheaper, to increase the speed of performance. Objects, including both fonts and geometric shapes, are held in memory in translated form as long as there is room; they can be reused if called again. That cuts down translation time substantially for repetitive elements. DDL files are transmitted in binary, which makes them more compact and speedier to transmit than an ASCII representation. Compactness is particularly important for scanned images and halftones (photographs), which are normally sent as arrays of dots. For debugging purposes, DDL printers will also accept an ASCII equivalent. DDL can also support both bit-mapped and outline fonts. When scaling bit- mapped fonts, which are made of lists of actual dot positions, the system applies various typographic design rules to produce a more intelligent result than does simple geometric multiplication. Besides the standard fonts found on both computerized and traditional systems, Imagen has licensed several special computer-oriented type families, including a face called Lucida that its originators, Bigelow & Holmes, claim is the first designed explicitly for electronic printing. For times when exact dot placement is important, such as in tiny fonts or in plotter-type output, DDL guarantees that if a target resolution is defined before image creation, dot placement at that same output resolution will follow the original exactly without any errors due to translation back and forth into universal coordinates. Close positioning control will increase greatly in importance when more color devices become available, especially if they are able to intermix colors to provide a wide range of hues and shades ("process color"). Tight dot positioning control also makes it possible to guarantee a close level of correspondence between screen and printed images. Like PostScript, DDL includes a full complement of control structures and is extensible, which makes it possible to write routines that handle both common and special requirements. While a wide variety of programs can be written in DDL, the expected use of this facility is for printer drivers or output filters and formatters. Making a Choice Although end users normally choose a page description language only implicitly through their choice of printers, developers and programmers face a tougher dilemma. They can pick a single PDL and support it as their output language of choice, support multiple PDLs, or rely on an operating environment for PDL support. The problem is made still more difficult by the rising expectations of end users. While the first laser printers that supported graphics and typographic-style fonts initially seemed sufficient, users are now asking for higher performance, faster output, greater detail, and increased ease of use. The makers of PDLs recognize this and are all working on improvements and refinements. But because the interpreters are normally implemented in ROM firmware, updating a PDL is a major market headache. PDLs are not changed very lightly. Furthermore, optimizing the choice of language itself is not the total solution. PDLs are part of a complex web that includes experienced programmers, supporting applications, tools, and output devices. The availability and popularity of resources and collateral material also matter. Which page description language is the best? Perhaps that question won't be settled any more than which programming language should be everyone's choice. At some point, it's not technology but theology. The good news is that developers writing programs for Microsoft Windows do not have to choose. Since Windows provides a device-independent interface, a developer can write an application that can output to a device using a PDL simply by having the appropriate device driver installed. In doing so, Windows permits innovation in PDLs while maintaining a standard application interface. A PostScript driver is already available, and DDL and Interpress drivers are expected to be ready in the near future. ─────────────────────────────────────────────────────────────────────────── Adobe System's PostScript ─────────────────────────────────────────────────────────────────────────── The "B" cube on page 50 of the printed version was created from the sample of Adobe's PostScript Language which follows. %! PS-Adobe- Adobe Systems Incorporated-Colophon 3 ShowPage Graphics 1986 %% DocumentFonts: Palatino-Roman %% Title: B.cube.ps % B.cube.ps produces a cube drawn with a character on each face. % To change the font subtitute a chosen font for Palatino-Roman in the % definition of "masterfont". /cube { /masterfont /Palatino-Roman findfont def % Letter strings that allow you to assign particular letters to each cube face. /front exch def /leftside exch def /back exch def /rightside exch def /top exch def /bottom exch def % The height of the letter is reduced with respect to its angle. (stan) % Height is multiplied by sin/cos. % This helps for perspective. /stan {a sin a cos div sy mul} def /getfont {masterfont [sx 0 stan sy 0 0] makefont setfont} def % type (s)ize and (a)ngle of oblique gsave /sy 100 def /sx 100 def /a 0A def getfont 40 50 moveto .75 setgray back show grestore gsave /a 45 def /sy sy 2 sqrt div def /sx 70 def getfont 0 0 moveto .60 setgray bottom show grestore gsave getfont 70 0 moveto 45 rotate .1 setgray rightside show grestore gsave getfont 0 7 moveto 45 rotate .40 setgray leftside show grestore gsave /a 0 def /sy 100 def /sx 100 def getfont 0 0 moveto .20 setgray front show grestore gsave /sy sy 2 sqrt div def /a 45 def /sx 70 def getfont 0 70 moveto top show grestore } def %cube %% EndProlog % To set cube: % gsave % x y translate % x y scale ...no scale here gives a cube of 100 pts % (bottom) (top) (rightside) (back) (leftside) (front) cube % grestore gsave 100 200 translate 3 3 scale (B) (B) (B) (B) (B) (B) cube grestore ─────────────────────────────────────────────────────────────────────────── A Comparison of Three Page Description Languages ─────────────────────────────────────────────────────────────────────────── We asked three PDL vendors to create a sample output consisting of the words "Microsoft Systems Journal" in a box. ■ Postscript Sample ■ DDL Sample ■ Interpress Sample Postscript Sample The PostScript programming from Adobe produces "Microsoft Systems Journal" shown on page 53 of the printed version.(The misspelling of the corporate name was due to creative license on the part of the programmers.) !PS-Adobe-2.0 %% Creator: pasteup %% CreationDate: Tue Dec 10 1986 %% For: Microsoft Systems Journal %% Pages: 1 %% DocumentFonts: Optima %% BeginProcSet: text_tools v1.0 revl.0 /box { % takes relativeX relativeY on stack, draws a box dup 0 rlineto 0 3 -1 roll rlineto neg 0 rlineto closepath stroke } bind def % short names for frequently used PostScript operators: /s /show load def /m /moveto load def /S /save load def /RS { restore save } bind def /R /restore load def %% EndProcSet %% END Prolog %% Page: 1 1 S 100 100 moveto 35 306 box RS 120 110 m /Optima findfont 24 scalefont setfont (MicroSoft Systems Journal) show R showpage %% Trailer DDL Sample The DDL output from Imagen Corporation appears on page 54 of the printed version. Uopbind.ddl (this program uses the standard DDL include file "opbind.ddl") "opbind.ddl" (specify the name of a file for the binding of operators and constants) :40 (open the specified file and push its source descriptor code on the stack) \srcn ; 3C (assign a name to the source descriptor code of specified file) srcn :3 (interpret the contents of specified source file) srcn :42 (close the specified source file) ; define an operator to convert points to image units { 24 POINTSPERINCH / ImageMetrics 5 index * } \pointstounits = @S (end of preamble and the start of section 1) @Uhelvr (this program uses the font file Helvetica Regular) "helvr" \SymbolStyle = ; set the state variable SymbolStyle to the name of the font file 24 pointstounits \SymbolDesignSize = ; convert 24 points into image units 128 array \CompositeMap = ; specify the size of the CompositeMap array 33 \i = ; set the starting index for the Composite map array to 33 95 { i symbol area\ CompositeMap i = ; store the graphic object for an i 1 + \i = ; ASCII character and increment the array index } repeat SymbolDesignSize 1 + 3 / relx \ CompositeMap 32 = ; calculate the width of the ASCII character SP (space) 656 \Xvalue = ; set the starting X coordinate for printing specified words 600 \Yvalue = ; set the starting Y coordinate for printing specified words Xvalue Yvalue absxy ! ; go to the coordinates Xvalue Yvalue "Microsoft Systems Journal" composite \text = ; create a composite object called text text ! ; print the words "Microsoft Systems Journal" text bbox ; calculate the smallest box that can enclose the specified three words \maxy = ; get the maximum Y coordinate of the bounding box from the DDL stack \miny = ; get the minimum Y coordinate of the bounding box from the DDL stack \maxx = ; get the maximum X coordinate of the bounding box from the DDL stack \minx = ; get the minimum X coordinate of the bounding box from the DDL stack maxy miny - \height = ; height of the bounding box maxx minx - \width = ; width of the bounding box height 2 / \offset = ; calculate the offset from the starting position width height = \width = ; increase the box width by box height Xvalue minx + offset - \Xvalue = ; calculate the starting X and Y coordinates Yvalue miny + offset - \Yvalue = ; the printing of a box Xvalue Yvalue absxy ! ; go to the starting coordinates width height height + rectangle line ! ; print a box twice as high as the bounding box endimage @E Interpress Sample The Interpress output from Xerox Corporation appears on page 57 of the printed version. ──Date: 8-Dec-86 14:42:23 PST - Object file Name: MICROSOFT2.ip ── Object to Source Conversion parameters: ── Object File Name: MICROSOFT2.ip, ── Source File Name: MICROSOFT2.ial. ── Op codes: Source Only. ── File Results: SourceOnly, Sequence Data: Decimal/ASCII. ── Conversion: Normalized, Items Per Line: Multiple. ── Large Arrays: NotSupressed. Interpress/Xerox/2.2 BEGIN { Identifier "Xerox" Identifier "XC1-1-1" Identifier "Modern" 3 MAKEVEC FINDFONT 99.576 SCALE MODIFYFONT O FSET } { 1/11811 SCALE CONCATT 375 2950 SETXY O SETFONT String "Microsoft" SHOW 40 SETXREL String "Systems" SHOW 40 SETXREL String "Journal" SHOW 5 15 ISET 350 2900 350 3050 MASKVECTOR 350 3050 1630 3050 MASKVECTOR 1630 3050 1630 2900 MASKVECTOR 1630 2900 350 2900 MASKVECTOR } END ████████████████████████████████████████████████████████████████████████████ DIAL 2.0 Provides Software Developers with Integrated Support System Barbara Krasnoff☼ Microsoft's on-line DIAL 2.0 support service, which supplies assistance for applications developers, has been recently upgraded and expanded. We asked Sunny Baker, Director of Planning and Marketing, to explain the new developments. MSJ: What exactly is DIAL? SUNNY BAKER: DIAL is an integrated set of on-line support services that use the PC as the interface to our own software. There are three components to DIAL. First, there is the on-line bulletin board system, a knowledge bank that is one of the most sophisticated bulletin board systems in the entire industry. It uses artificial intelligence techniques for keyword search capabilities. It's very fast and powerful. Second, we have an on-line Forum, similar to the ones you get through CompuServe, especially for the ISV (independent software vendor) community. Finally, we offer technical assistance through our TAR (technical assistance request processing) system. We supply both on-line TAR processing and callback for high-priority TARs in order to give people immediate assistance over the telephone. DIAL also transfers both binary and ASCII files, and we do furnish some DIAL software to subscribers. MSJ: When did DIAL first go on-line? BAKER: The first version went on-line at the end of 1984, and, quite honestly, it was not very capable. It was basically a prototype version. At that time, we limited access to DIAL to our OEM customers and some of the specialized ISVs. It wasn't until last year that we really opened DIAL up to the ISV community, and now we have a completely new version. DIAL 2.0 is very similar to what people have been using from the interface standpoint, but functionally and operationally, it's a completely new product. For example, we used to run DIAL on very small machines because of its prototype nature, and now we've moved the actual server for DIAL to a full-sized VAX. MSJ: What are some of the other differences between 2.0 and previous versions? BAKER: The Bulletin Board has been entirely restructured. It has keyword search capabilities, it contains much more information, and the organization and speed of the board are entirely different. The Forum is a totally new feature, and transmission error-checking capabilities have been added as well. The product now uses an ANSI driver rather than a VT52 driver, which is something that our customers asked for. The VT52 was a Microsoft standard that we used in-house, but nobody else uses it, so it was a big problem. MSJ: What kind of service does DIAL contribute to the community? Why is there a need for it? BAKER: There have been complaints in the user community for some time that Windows development has not been supported properly. DIAL is our effort to supply the high level of support that people who are doing software development need. Access to the Bulletin Board gives software developers immediate information for any known problem. We summarize every TAR that's presented to us from whatever source, and place it on the Bulletin Board, giving developers and programmers everywhere full access to bug lists for Microsoft products. There is immediate access to any new software updates; it's the most efficient way to get new tools to users. We also offer example software that they can download and then use in their own applications. From our point of view, the Bulletin Board is the fastest and most efficient way for subscribers to get support from Microsoft, since it contains answers to every question that somebody has asked before. The Bulletin Board is so quick that it's faster than calling somebody or sending in a technical assistance request. We try to get subscribers to DIAL to really use the Bulletin Board actively, especially when they first start developing applications with a Microsoft product. MSJ: Who is the typical DIAL subscriber? BAKER: Right now, our typical users are software developers doing Windows development, OEMs doing adaptations of both DOS and Windows to their own machines, and anybody who uses a Microsoft systems product──especially to create Windows applications. We are in the process of making DIAL available to any person who does systems development using any Microsoft programming language or programming tool kit, with emphasis on our most strategic products: Windows and DOS. MSJ: Approximately how many users do you have? BAKER: We have about 1,200 users today; and we're getting about 120 new users a month. Certainly we would like to penetrate the community even deeper than that. Right now, we know we have about 6,000 Windows Toolkit users out there; our goal is to have at least 75 percent of the active program developers on-line. And there are thousands of C programmers out there who are another target market for us. As our user base expands, the Forums will get better, because there are more and more people exchanging information. MSJ: How much information does Microsoft post on the Bulletin Board? BAKER: We delete things that are obsolete, but otherwise the Bulletin Board is constantly expanding. We now post about 150 items a week. Our goal for next month is to post 1,000 items, and our goal over the next 6-month period is to get up to the point where we will be posting 7,000 items a month, because we're going to support Microsoft's complete product line. MSJ: The Bulletin Board contains information only from Microsoft. Is there any on-line interaction between users? BAKER: That's what the Forum is all about. We just started it in January, and it looks like we're going to have a good quorum. Our Forums are just like the forums on GENIE or CompuServe. Let's say that somebody is looking for a local area network card. That person can ask the community, "Hey, do any of you know a good local area network card?" DIAL people can respond to the Forum and say, "Hey, I know so-and-so." Their anonymity is maintained throughout the Forum. Forum items that we think are noteworthy are also translated into Bulletin Board items, so that everybody can have access to them. The whole point is to make DIAL a one-stop support system that combines everything a user basically needs as far as support from Microsoft, support from his own community, and support from our knowledge base──all integrated into one system. MSJ: When does a subscriber use TAR? BAKER: Any question that relates to the operation of a product or the building of an application with a Microsoft product can be asked over the TAR system. However, to support the complex questions that we get, we really need to see the customer's code. TAR offers an immediate mechanism for transferring that code back and forth. Coupled with our callback on priority questions, we think we've developed the optimum support system for our ISV customers. It lets them have direct contact over the telephone with Microsoft, and gives them the chance to explain the problem in detail in writing, which is usually the best way to handle it. Every TAR is summarized and rewritten to become a Bulletin Board item after it's answered. Of course, the anonymity of the person who sent in the TAR is always maintained. MSJ: How many people do you have on staff right now? BAKER: In the system support area in Microsoft, there are 35 people supporting languages and operating environments. We have just gotten approval to double our operations staff from 10 to 20. We've also introduced a very interesting job rotation plan that allows engineers to work on DIAL and then rotate back into engineering. MSJ: How long does it usually take for a question on TAR to be answered? BAKER: In the past, because of the limited staff, the typical time was 2 or 3 days. Right now, our goal is to get it to within 24 hours on all TAR questions. MSJ: How much does DIAL cost? BAKER: $450 a year for unlimited service covering all of the MS-DOS system's products; XENIX Support is $600 per year. MSJ: Are there any arrangements for long-distance callers? BAKER: We have negotiated an arrangement for low-cost service with Telenet so that DIAL users who call long distance can reduce that charge. MSJ: How many lines do you have now? BAKER: We have 8 Telenet lines and 8 long-distance lines. We also monitor the usage at any particular time and add lines as they are needed. MSJ: What is your own frank evaluation of DIAL 2.0? BAKER: It's a good, solid support vehicle that still needs work. In the past, there weren't enough senior people staffed on DIAL. Of course, we're meeting that need now by adding more people. The knowledge base is probably the most outstanding keyword search bulletin board system in the entire computer industry──and we've looked at the systems that DEC and IBM are using as well, so we really have a technological lead with our system. I think that in certain areas, such as actual support delivery, it does need some enhancement, but Microsoft management is being very responsive in giving us the resources to make it a really great world-class system. ████████████████████████████████████████████████████████████████████████████ Rich Text Format Standard Makes Transferring Text Easier ─────────────────────────────────────────────────────────────────────────── Also see the following related articles: An RTF Sample Preliminary Rich Text Format Specification ─────────────────────────────────────────────────────────────────────────── Nancy Andrews☼ Back in the bad old days of punch cards, formatted text was never an issue. There were no formats; you couldn't even use lowercase. But we've come a long way since then. With bit-mapped screens, state-of-the-art word processors can handle numerous character, paragraph, and section formats. Not only can you get lowercase, you can have 16-point Helvetica in a paragraph with a 1/2-inch hanging indent. However, problems arise when you transfer text and formats to other applications. Rich Text Format (RTF) provides a solution to these problems. RTF is a way to encode formatted text. Microsoft is proposing RTF as a standard to make moving text between applications easier. With RTF, users can transfer text from one application to another and not lose any formatting. And developers can save documents in one format, RTF, and know that other applications can read this information without worrying about translating to each application's unique format. Why RTF Microsoft is pushing RTF because of Microsoft Windows. Currently, if you use Window's clipboard to transfer text between applications, you lose all formatting. For example, if you were transferring formatted text from Windows Write into Micrografx Draw, you'd have to reformat the text within Draw. A way to retain the format from one application when it is transferred to another is needed. If both Write and Draw used RTF, the clipboard could transfer the formats along with the text. Microsoft recognizes the need to exchange documents easily among its own products. It sees RTF as a solution for existing and future products. Current format standards such as IBM's Document Content Architecture (DCA), although widely supported, are not adequate. DCA, for example, lacks a good font strategy. It works with pitch only, doesn't consider point sizes, and has only one code to specify proportional spacing. DCA is most efficient for transferring entire documents, as opposed to short strings. Microsoft wanted a standard that could handle all existing formats and both entire documents and the rapid exchange of short formatted strings. This kind of information is usually transferred via the Windows clipboard. When you transfer short strings, you don't want to include the document name, creator, and date of last modification. But when you transfer entire documents, this information is essential. RTF has the flexibility to handle both types of transfer. Also, the standard had to be what Charles Simonyi, author of RTF and Microsoft's chief architect of applications, calls "forward-looking," that is, one that can handle existing applications and leaves room for future enhancements as well. What RTF Is RTF provides a standard format for interchanging text regardless of the output device, operating system, or operating environment. Text and format instructions are saved in 7-bit ASCII format so that they are easy to read, and you can send them over virtually any communications link. RTF uses "control words" to encode the formats. Control words provide the space so that RTF can be expanded later to include macros or additional formats. Control words use this form: \lettersequence If the delimeter is a digit or a hyphen (-), another parameter follows. A nonletter or digit terminates the control word. In addition to control words, RTF uses braces. A left brace ({) marks the beginning of a group of text, and a right brace (}) marks the end. Braces can be used to delineate footnotes, headers, and titles. In RTF, the control words, the control symbol (\), and the braces constitute control information. All other characters in RTF are plain text. A bit of RTF code might look like this: \rtf0\mac{\fonttbl\f1\ fromanBookman;} {\stylesheet {\s0 Normal;} {\s1\i\qj\snext2\f1 Question;}{\s2\qj\f1 Answer;}} {\s0\f1\b\qc Questions and Answers\par } {\s1\i\qj 1. What is the left margin of this document?\par} {\s2\qj\li720\f1 Since no document parameters were specified, the default of 1800 twips (1.25") is used.\par}} Here's what this information means. \rtf0\ indicates that this is an RTF document and the version number is 0. It uses the Macintosh, rather than the PC or ANSI, character set. Next comes information about the current font, which is from font table 1, the roman family, the Times font. After that is style sheet information. Three different styles are assigned. Style 0 is the normal character and paragraph style. Style 1 is italic, the paragraph is justified, it is always followed by a style 2 paragraph, uses font 1, and is called Question. Style 2 is justified, uses font 1, and is called Answer. Next is information about the first paragraph, followed by the text of that paragraph. It is normal text (style 0), uses font 1, and is bold and centered. The second paragraph is a question, uses the question style, and is italic and justified. The last section is the answer, which is based on style 2, is justified, and uses a left indent of 720 twips or 0.5". (Twips are 1/20th of a point; there are 1,440 twips per inch.) To read a stream of RTF you first need to separate the control information from the plain text and then act on the control information. When you encounter a left brace ({), you have to stack the current state and then continue. When you encounter a control symbol (\), collect the control word and parameter, if any, look up the word in the symbol table (a constant table), and follow the directions outlined there. For example, change a formatting property such as adding the bold format. Then continue writing characters to the current destination by using the current formatting properties. When you come to the right brace (}), unstack the current state. Working with RTF is straightforward──much simpler than decoding proprietary file formats of individual applications. Who's Playing RTF, like any standard, is as valuable as the people who support it. Microsoft believes that most Microsoft Windows applications developers will support RTF, since it is advantageous for them to be able to easily exchange formatted text. George Grayson, president of Micrografx, one of the first Windows applications developers, says that its products, In*a*Vision, Draw, and Graph, will all support RTF by June. Also, Microsoft currently has several Windows applications in development that will definitely support RTF. Aldus, developer of PageMaker for the Mac and PC, has not yet committed to RTF. But Ted Johnson, an Aldus engineer, says that although RTF was too late to be included in Version 1.0 of Aldus's PC PageMaker, versions after 1.0 will definitely support RTF for clipboard transfers. PageMaker will also continue to accept formatted text from several word processors using the proprietary file format of each of these word processors. So it looks like RTF is well on its way to acceptance in the Microsoft Windows world. What Next Greg Slyngstad, program manager in Microsoft's word processing area and current keeper of the RTF standard, claims "this version is complete, but we expect to extend it in the future to meet the needs of Windows application developers (including Microsoft). Any future version will be superset of the current RTF." The goal of RTF is to retain all formatting in transfer. And Slyngstad's goal is to ensure that the RTF standard responds to the needs of developers. ─────────────────────────────────────────────────────────────────────────── An RTF Sample ─────────────────────────────────────────────────────────────────────────── {\rtf0\mac {\fonttbl{\f0\fswiss Chicago;}{\f3\fswiss Geneva;}{\f4\fmodern Monaco;}{\f13\fnil Zapf Dingbats;}{\f14\fnil Bookman;}{\f15\fnil N Helvetica Narrow;}{\f20\froman Times;}{\f21\fswiss Helvetica;}{\f22\fmodern Courier;}{\f23\ftech Symbol;}} {\stylesheet{\s243\tqc\tx4320\tqr\tx8640 \sbasedon0\snext243 footer;}{\sbasedon222\snext0 Normal;}}\margl1613\margr1080\margt720\widowctrl\ftnrestart {\headerl \pard\plain 1-4 see auditors notes{\|}\par \par }\sectd \linemod0\linex0\cols1\colsx0\endnhere {\footer \pard\plain {\i\f20\fs18 This document was prepared with Microsoft(R) Word}{\i\scaps\f20\fs18 }{\i\f20\fs18 Version }{\i\scaps\f20\fs18 3.0}{\i\f20\fs18 for Apple(R) Macintosh(TM) systems, \par and printed with an Apple LaserWriter(TM).}{\i\scaps\f20\fs18 \par }\pard\plain \s243\tqc\tx4320\tqr\tx8640 {\f150\fs18 \par }}\pard\plain \ri360 {{\pict\macpict\picw73\pich66 02b2012500af016700f81101a00082a0008c01000a0000000002d0024051013500b4016700f1 5809000000000000000071001e013500c5016200de016000dd013900c501 ∙ ∙ ∙ 00f3a0621670002a015300eb016000f7015400f3015700ee015c00eb016000ef015f00f60158 00f7015300f3015400f3a0008da00083ff}}{\b\f140\fs28 \par \par }\pard \ri187 {\b\f140\fs32 West Associates, Inc.\par }\pard \ri187\sb40\sl320\brdrt\tqr\tx9360 {\f139 Consolidated Balance Sheet}{\f21\fs20 \tab }{\i\f21\fs20 Current as of: December 31st, 1986\par }\pard \ri5947\sa40\brdrb {\f21\fs20 \par }\pard \ri187\brdrth\tx360\tx1080\tx6480\tqr\tx7560\tx8280\tqr\tx9360 {\b\f140\fs20 Assets}{\b\f21\fs20 \tab \tab \tab }{\b\i\f21\fs20 1986\tab }{\i\f21\fs20 \tab 1985\par }\pard \ri187\sb40\brdrt\brdrth\tx360\tx1080\tx8280 {\i\f21\fs20 \tab }{\f21\fs20 Current Assets:\par }\pard \ri187\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Cash and temporary cash investments}{\b\f21\fs20 \tab $114,888}{\f21\fs20 \tab $143,284\par \tab \tab Accounts receivable}{\b\f21\fs20 \tab 258,238}{\f21\fs20 \tab 136,420\par \tab \tab Inventories (at cost)}{\scaps\f21\fs16\up6 1}{\b\f21\fs20 \tab 264,619}{\f21\fs20 \tab 142,457\par }\pard \ri187\sb40\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Prepaid Income Taxes}{\b\f21\fs20 \tab 26,751}{\f21\fs20 \tab 27,949\par }\pard \ri187\brdrb\tx360\tx1080\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Other current assets}{\b\f21\fs20 \tab 23,055}{\f21\fs20 \tab 18,883\par }\pard \ri187\sb40\brdrb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab \tab }{\f138\fs20 Total Current Assets:}{\b\f21\fs20 \tab $687,551}{\f21\fs20 \tab $468,993\par }\pard \ri187\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab Property, plant and equipment\tab \par \tab \tab Land and Buildings}{\b\f21\fs20 \tab 24,892}{\f21\fs20 \tab 19,993\par \tab \tab Machinery and equipment}{\b\f21\fs20 \tab 68,099}{\f21\fs20 \tab 51,445\par \tab \tab Office furniture and equipment}{\b\f21\fs20 \tab 30,575}{\f21\fs20 \tab 22,628\par }\pard \ri187\sa60\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Leasehold improvements}{\scaps\f21\fs16\up6 2}{\b\f21\fs20 \tab 26,008}{\f21\fs20 \tab 15,894\par }\pard \ri187\brdrt\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab \tab }{\b\f21\fs20 \tab $149,574}{\f21\fs20 \tab $109,960\par }\pard \ri187\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab Accumulated depreciation and amortization}{\b\f21\fs20 \tab (73,706)}{\f21\fs20 \tab (42,910)\par ∙ ∙ ∙ \ri187\sb40\brdrb\brdrdb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \tab \tab \tab }{\b\f21\fs20 \tab $788,786}{\f21\fs20 \tab $556,579\par }\pard \ri187\brdrdb\tx360\tx1080\tx1620\tqr\tx7560\tqr\tx9360 {\f21\fs20 \par }{\f138\fs18 All amounts shown in thousands (000)\par }} ─────────────────────────────────────────────────────────────────────────── Preliminary Rich Text Format Specification ─────────────────────────────────────────────────────────────────────────── RTF text is a form of encoding for various text formatting properties, document structures, and document properties, using the printable ASCII character set. The main encoding mechanism of "control words" provides a naming convention to expand the realm of RTF to include macros, programming, and so on. Special characters can also be thus encoded, although RTF does not prevent the utilization of character codes outside the ASCII printable set. BASIC INGREDIENTS A file or stream of RTF consists of "plain text" interspersed with control symbols, control words, and braces. Control words follow the format: \ The delimiter can be a space or any other nonalphanumeric character. When a numeric parameter follows the control word, the first digit of the parameter (or the minus sign, in the case of a negative number) functions as the delimiter of the control word. The parameter is then in turn delimited by a space or any other nonalphanumeric character. Control symbols consist of a \ character followed by a single nonalphabetic character. They require no additional delimiting characters. Control symbols are compact, but there are not too many of them, whereas the number of possible control words is unlimited. The parameter is partially incorporated in control symbols, so a program that does not understand a control symbol can recognize and ignore the corresponding parameter as well. Each group begins with a control word that describes the contents of the group. { indicates the beginning of a text group and } indicates its end. The text grouping is used for formatting and to delineate structural elements of the document, such as the footnotes, headers, title, and so on. All other characters in RTF text constitute "plain text." Since the characters \, {, and } have specific uses in RTF, the control symbols \\, \{, and \} are provided to express the corresponding plain characters. WHAT RTF TEXT MEANS The reader of an RTF stream will be concerned with separating control information from plain text and acting on control information. This is designed to be a relatively simple process, as described below. Some control information just contributes special characters to the plain-text stream. Other control information changes the "program state," which includes properties of the document as a whole and also a stack of "group states" that apply to parts of the document. The file is structured as groups and subgroups. Each group state is defined by a text group (text enclosed in braces). The group state specifies the following: a. The "destination" or the part of the document that the plain text is building up. b. The character formatting properties, such as bold or italic. c. The paragraph formatting properties, such as justification. d. The section formatting properties, such as number of columns. GROUPS AND SUBGROUPS The overall grouping is the document file as a whole, since the entire document is enclosed in braces, beginning with the control word \rtf with the parameter being the version number of the writer. This control word must begin every RTF file, and the entire file must end with the corresponding closing brace. Before any text in the RTF file is entered, the character set for the entire destination may be declared using one of these control words: \ansi Text is the ansi character used by Microsoft Windows (default). \mac Text is the Macintosh character set. \pc Text is the IBM PC character set. COLOR TABLE The control word \colortbl is used to define the color table, with numeric indexes (starting at 0) for red, green, and blue. These indexes are the same as those used in Windows (the italicized portion represents the variable numeric parameter): \red000 Red index \green000 Green index \blue000 Blue index \cf000 Foreground color \cb000 Background color Each set of color definitions delimited by semicolons defines the next sequential color number. The following example defines color 0 and color 2: {\colortbl\red128\green0\blue64;;\red64\green128\blue0;} FONT TABLE The control word \fonttbl designates the group that is the font table, which assigns the font name and family to the font numbers used. The text is the font name delimited by semicolons. Default is used if no font was assigned and the recipient should use whatever font is considered the default for that particular output device. The font table, if it exists, must occur before the style sheet defintion and any text in the file. Possible font families are: \fnil Family unknown (default) \froman Roman family. Proportionally spaced, serif (Times Roman, Century, Schoolbook, etc.) \fswiss Swiss family. Proportionally spaced, sans serif (Helvetica, Swiss, etc.) \fmodern Fixed pitch, serif or sans serif (Pica, Elite, Courier, etc.) \fscript Script family (Cursive, etc.) \fdecor Decorative fonts (Old English, etc.) \ftech Technical or symbol fonts Example: {\fonttbl\f0\froman Tms Rmn;\f1\fswiss Helv;\f2\fnil default;} STYLE SHEETS The style sheet for the document is defined by a group beginning with the control word \stylesheet. More precisely, plain text in the group between semicolons is interpreted as style names, which will be defined to stand for formatting properties that are in effect. Example: {\stylesheet{\s0\f3\fs20\qj Normal;} {\s1\f3\fs24\b\qc Heading Level 3;}} This defines Style 0 (given the name "Normal") as 10-point justified type in font 3 (the font is defined in the font table). Style 1 (Heading Level 3), is 12-point font 3, boldface and centered. The following fields may be present if the destination is \stylesheet. \sbasedon000 This defines the style number which the current style is based on. If this control word is omitted, the style is not based on any style. \snext000 This defines the next style associated with the current style. If this control word is omitted, the next style is itself. PICTURES If the group begins with the control word \pict the plain text within the group is a hex dump of a picture. The following optional parameters may also exist if the group is a picture. If they are not present, the default frame size equals the picture size. A picture must be preceded by the \chpict special character that serves as the anchor point for the picture that follows. \pich000 Defines picture frame height in pixels. The picture frame is the area set aside for the image. The picture itself does not necessarily fill the frame (see \picscaled). \picw000 Picture frame width in pixels \picscaled Scales the picture up or down to fit within the specified size of the frame \wmetafile The picture is a Windows metafile \macpict The picture is in Mac Quick Draw format \bin000 Special field used to include binary information within the file (in lieu of hex as expected). The parameter defines the number of bytes of binary information that follow. This is useful for the "clipboard" data exchange where the file is not intended to be transferred over a communications line. FOOTNOTES If the group begins with the control word \footnote the group contains footnote text. The footnote is anchored to the character that immediately preceds the footnote group. The group may be preceded by the footnote reference characters if automatic footnote numbering is defined. A footnote group may also have the following complements: \ftnsep Text is a footnote separator. \ftnsepc Text is a separator for continued footnotes. \ftncn Text is continued footnote notice. HEADERS AND FOOTERS If the group begins with the control words\header or \footer the group is the header or footer for the current section. The group must precede the first plain text character in the section. The following variant forms may also be used: \headerl Header on left-hand pages only \headerr Header on right-hand pages only \headerf Header on first page only \footerl Footer on left-hand pages only \footerr Footer on right-hand pages only \footerf Footer on first page only INFORMATION The file may also contain an information group, specified by the control word \info. This group is used to store such information as the title. The name of the author, and the subject of the document. The appropriate information is entered as plain text following these control words: \title \subject \author \operator \keywords \comment Text will be ignored. \version \doccomm This last control word is for storing comments. It should not be confused with \comment which specifies that the text is to be ignored. Here is an example of an information block: {\info{\title Unified Field Theory Analysis}{\author A. Finestyne} Other control words allow values to be automatically entered into the information block: \vern000 Internal version number \creatim Creation time \yr000 Year \mo000 Month \dy000 Day \hr000 Hour \revtim Revision time \printim Last print time \buptim Backup time \edmins Minutes of editing \nofpages000 Number of pages \nofwords000 Number of words \nofchars000 Number of characters \id000 Internal id number TEXT PROPERTIES Note that a change of destination will reset all text properties to their default values and that changes are only legal at the beginning of a text group. DOCUMENT FORMATTING PROPERTIES The default value (given in square brackets) will occur if the control word is omitted. When the control words are used,000 is replaced by numeric parameters. All measurements are in twips which are twentieths of a point or 1/1440th of an inch. Examples: \paperw000 Paper width in twips [12240] \paperh000 Paper height [15840] \margl000 Left margin [1800] \margr000 Right margin [1800] \margt000 Top margin [1440] \margb000 Bottom margin [1440] \facingp Facing pages (enables gutters and odd/even headers) \gutter000 Gutter width (inside of facing pages) [0] \ogutter000 Outside gutter width [0] \deftab000 Default tab width [720] \widowctrl Enable widow control \headery000 Header y position from top of page [1080] \footery000 Footer y position from bottom of page [1080] \ftnbj Footnotes at bottom of page (default) \ftnsep Text is a footnote separator \ftnsepc Text is a separator for continued footnotes \endnotes Footnotes at end of section \ftntj Footnotes beneath text (top justified) \ftnstart000 Starting footnote number [1] \ftnrestart Restart footnote numbers each page \pgnstart000 Starting page number [1] \linstart000 Starting line number [1] \landscape000 Printed in landscape format SECTION FORMATTING PROPERTIES Examples: \sectd Reset to default section properties \sbknone Section break continuous (no break) \sbkcol Section break starts a new column \sbkpage Section break starts a new page (default) \sbkeven Section break starts an even page \sbkodd Section break starts odd page \pgnrestart Restart page numbers at 1 \pgndec Page number format decimal \pgnucrm Page number format upper case roman numeral \pgnlcrm Page number lower case roman numeral \pgnucltr Page number format upper case letter \pgnlcltr Page number format lower case letter \pgnx000 Page number x pos [720] \pgny000 Page number y pos [720] \linemod000 Line number modulus [1] \linex000 Line number text distance [360] \linerestart Line number restart at 1 (default) \lineppage Line number restart on each page \linecont Line number continued from previous section \vertalt Vertically align starting at top of page (default) \vertalc Vertically align in center of page \vertalj Vertically justify top and bottom \vertal Vertically align starting at the bottom \cols000 Number of columns (snaking) [1] \colsx000 Space between columns [720] \endhere Include endnotes in this section \titlepg Title page is special PARAGRAPH FORMATTING PROPERTIES Examples: \pard Reset to default paragraph properties \s000 Style; if a style is specified, the paragraph formatting implied by that style must still be specified with the paragraph \ql Quad left (default) \qr Quad right \qj Justified \qc Centered \fi000 First line indent [0] \li000 Left indent [0] \ri000 Right indent [0] \sb000 Space before [0] \sa000 Space after [0] \sl000 Space between lines (If no \sl is specified, default is 12 points. If \sl000 is specified, line spacing is automatically determined by the tallest font on the line). \keep Keep \keepn Keep with next paragraph \sbys Side by side \pagebb Page break before \noline No line numbering \tx000 Tab position (this places a vertical bar at the specified position for the height of the paragraph) \tqr Flush right tab (last specified position) \tqc Centered tab \tqdec Decimal aligned tab \brdrt Border top \brdrb Border bottom \brdrl Border left \brdrr Border right \box Border all around \brdrs Single thickness border \brdrth Thick border \brdrsh Shadow border \brdrdb Double border \tldot Leader dots \tlhyph Leader hyphens \tlul Leader underscore \tlth Leader thick line CHARACTER FORMATTING PROPERTIES Examples: \plain Reset to default text properties \b Boldface \i Italic \strike Strikethrough \outl Outline \shad Shadow \scaps Small caps \caps All caps \v Invisible text \f000 Font number \fs000 Font size in half points [24] \expnd000 Expansion or compression of space between characters in quarter points (negative value indicates compression). \ul Continuous Underline \ulw Word underline \uld Dotted underline \uldb Double underline \ulnone Cancels all underlining \up000 Superscript in half points [6] \dn000 Subscript in half points [6] A zero following the character formatting instruction turns off the format. For example: \b0 Turns off the boldface \i0 Turns off the italic SPECIAL CHARACTERS If a character name is not recognized by a reader, it will simply be ignored. Other characters may be added for interchange with other programs. These are examples of special characters: \chpgn Current page number (as in headers) \chftn Auto footnote references (footnote to follow in a group.) \chdate Current date (as in headers) \chtime Current time (as in headers) \| Formula character \~ Nonbreaking space \- Discretionary, or soft, hyphen \_ Nonbreaking hyphen \'hh Any hex value (can be used to identify 8-bit value) \page Required page break \line Required line break (no paragraph break) \par End of paragraph \sect End of section and end of paragraph \tab Same as ASCII 9 For simplicity of operation, ASCII 9 will be accepted as tab. The control code \<10> (backslash followed by ASCII 10) and \<13> will both be accepted as \par. ASCII 10 and ASCII 13 without the backslashes will be ignored. They may be used to include carriage returns for easier readability but will have no effect on the interpretation as long as they do not occur within a control word. It's a good idea to insert carriage returns every 255 characters or less to facilitate transmission via E-mail systems. ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob! DOS Path Dear Dr. Bob, I need to use the MS-DOS path to search for the overlays, initialization files, and data files my program needs. I can't figure out how to get this from MS-DOS. Can you give me a clue about where to get this information? ──Searching Dear Searching, It's true that MS-DOS has no usable support for path information. But you can use the C library getenv ("PATH") function. It returns a pointer to the current path string. At that point it's up to your program to decode and use that information, which is in the same format you see when you type "path" after the MS-DOS prompt. You need to parse the string and then scan individual directories for the files that you will need. Redundan-C Dear Dr. Bob, My programs (like most everyone's these days) use some C modules and some MASM modules. It seems so redundant to type the same declarations in C format and then in MASM format. Often, I make a change in one and then forget to incorporate it into the other. Is there a way to get around this? ──Forgetful Dear Forgetful, Actually, there is. The trick is to use C's preprocessor phase. What you can do is make a header file that produces the definitions required for both C and MASM. Then you write one declaration file using generic rather than C- or MASM-specific declarations. When you make a change, all you need to do is change the generic header file, and it will automatically change both the C and MASM declarations. Here's how it works. First you make a header file like the one in Figure 1, called C_OR_ASM.HDR. It takes information from a generic program declaration file and turns it into the format required by either the C compiler or MASM. Next, put the declarations for your program in a file like the one shown in Figure 2. In this case, we have called our file SHARED.HDR. As you can see, instead of using either C or MASM's declaration formats, what you use are generic formats. C_OR_ASM.HDR then takes this file and turns it into the required format. The last piece you need to finish the puzzle is a way of telling the compiler or assembler about these files. Here's where you use C's preprocessor. You use both the /D and /EP switches. The /D switch tells the preprocessor which set of definitions to use in C_OR_ASM. /EP tells the compiler to stop after this preprocessor stage. If you're assembling a MASM module, use this line: msc shared.hdr /DMASM_STYLE /EP; >shared.inc It takes SHARED.HDR and produces the declarations required in MASM format in a file called SHARED.INC. For C declarations use the following line: msc shared.hdr /DC_STYLE /EP; >shared.h It produces the declarations in C format in a file called SHARED.H. Now you have declaration files in both formats. All you need to do is include them with your .C or .ASM files when you compile. If you make any changes, the only file you need to change is SHARED.HDR. Taking Out Mouse Garbage Dear Dr. Bob, I am currently writing a QuickBASIC program that uses some of the new hi-res EGA modes──SCREEN 8, 9, and 10. But whenever I move the mouse around while my program is writing to the screen, I get garbage. Is there a fix for this? ──Mystified Dear Mystified, You're in trouble. Because the EGA has write-only registers, in order for two programs to write to the screen at the same time, they need to cooperate. Microsoft QuickBASIC doesn't cooperate with anyone. So you can just avoid using the mouse, or, if the mouse is critical, disable it while you're drawing on the screen. For additional information you can call Microsoft Technical Support at (206) 882-8089 and ask for "Writing EGA Software for the IBM PC." Hexing Windows' Calculator Dear Dr. Bob, I used to have a terminate-and-stay-resident calculator I used all the time. When I started using Microsoft Windows, I found I really needed all my memory for Windows and the applications I run with it. I've tried using the Windows calculator. Although it has the basics, what I need is the ability to display the hex equivalent of decimal numbers. Is there any way I can get the Windows calculator to do this?──Refiguring Dear Refiguring, You're in luck. It's quick and easy. Just hold down the H key and the number currently displayed will temporarily change to its hexidecimal equivalent. Standard Memory Error Dear Dr. Bob, I'm running Microsoft Windows and I'm using standard applications. When I quit one of the standard applications, Windows doesn't always seem to relinquish all of its memory. Occasionally I get a "Not enough memory..." error when I think I shouldn't.──Puzzled Dear Puzzled, This can happen because of the way Windows manages memory for standard applications. You need to follow a couple of basic guidelines when working with several standard applications and Windows. First, load the largest standard application. When Windows starts the second standard application, it swaps the first out and the second in. Loading the largest first ensures that there will be enough room. Then if you quit the largest application the second will still have as much memory as was allotted for the largest one. You can fix this by choosing the About... command in the MS-DOS Executive System menu. In addition to displaying the amount of memory, it also does garbage collection and will reclaim and reorganize the unused memory. BitBlt Limits Dear Dr. Bob, I'm working on a Windows app. I tried to use BitBlt to copy a large portion of the screen, but it didn't work. What's wrong?──Frustrated Dear Frustrated, An undocumented limitation of existing versions of Windows is that bitmaps can't be larger than 64K. One way you can get around this is to use "banding"──splitting the block that you want to move into rectangles and then calling BitBlt on each of the smaller rectangles. Stock Bitmaps Dear Dr. Bob, The Windows Software Development Kit documentation for CreateCompatible display context says that "GDI automatically selects a monochrome stock bitmap" for the new memory display context. What does this mean? Is there something special about a "stock bitmap"?──Wondering Dear Wondering, There is nothing special about a monochrome stock bitmap. It is simply a bitmap with one plane and one bit per pixel. By using the CreateCompatibleDC function a display context is set up whose "display surface" is simply a bitmap allocated from system memory──it is not actually displayed anywhere. Any GDI function may be used to draw into this bitmap. BitBlt can quickly move images between itself and another DC. When a memory DC is created, Windows allocates a monochrome bitmap for it as a default, even if your original had color. If you have all color information, you can switch to a color bitmap by using CreateBitmap and SelectObject. Scaling Text Dear Dr. Bob, I'm confused by Windows' mapping modes. My graphics get scaled because I specify anisotropic mode, but my text doesn't. Is there a way to scale text? ──Confused Dear Confused, Maybe. You can tell whether a given device supports character scaling with GetDeviceCaps (hDC,TEXTCAPS). Certain bits of the return value tell what sorts of text display tricks are supported. If the device does support some text scaling, be sure to use DrawText, instead of TextOut, because TextOut doesn't scale. Interrupt Routines Dear Dr. Bob, I'm writing a Windows app that uses a special interrupt-driven I/O board. Obviously, it's not supported by Windows. Ideally, I would like my interrupt service routine to send messages to the Windows app. It would be notified by GetMessage, just as when you press a key or move the mouse. Am I dreaming? ──Dreamer Dear Dreamer, Yes. First of all, you can't call any Windows functions from within your interrupt service routine because right now Windows is not re-entrant. You might have better luck when DOS and Windows are truly multitasking. Windows handles the keyboard and mouse in a special way and does not allow you to define your own messages that would work like the keyboard and mouse. The best you can do is use SetTimer to generate WM_TIMER messages for your Windows application periodically. You can have your interrupt routine set flags, which are polled in the WM_TIMER code. Obviously, this means that you can't count on a quick response since you'll be at the mercy of the Windows scheduler and the other applications to return your program control occasionally. Windows Color Support Dear Dr. Bob, The EGA hardware supports 16 colors. To my surprise, I discovered Windows displays only eight. Say it ain't so, Bob.──Surprised Dear Surprised, When Microsoft wrote the EGA driver, it traded colors for speed. The EGA card has four planes, allowing a possibility of 16 colors. Microsoft chose to use only three planes for color and reserved the fourth plane for the mouse cursor and the other cursor. If Microsoft had given this plane to color rather than the cursor, each time the mouse moved you would need to restore the screen in the old cursor position, save the screen in the new cursor position, and draw the cursor. All three operations would have to process all four planes. This would be too slow, especially on an 8088 PC. By dedicating one plane to the cursor, you don't have to save a copy of the screen under the cursor. Moving the cursor is simply erasing it from the old position and drawing it at the new position. This tradeoff seems reasonable. After all, Windows can dither the dots and get 64 shades for each of the eight colors that are supported. Using Windows and SYMDEB Together Dear Dr. Bob, I do a lot of traveling. I like to use my time on the airplane to work on my hobby──debugging Windows apps. How can I have my Windows app and SYMDEB running on the screen at the same time?──Jetsetter Dear Jetsetter, First, fasten your seatbelt, and then relax. It has been reported that you can run Windows and SYMDEB on the same screen some of the time. But this doesn't work reliably, and in fact, Dr. Bob hasn't been able to get it to work at all. In any event you'll most likely find it easier between landings and takeoffs to use a second monitor or a serial terminal. That way you can see your Windows app running on one screen and SYMDEB on the other. Getting the Same Image Dear Dr. Bob, I'm using the same series of steps for output to the screen and the printer, but the images are different. Is there a way to guarantee that the output will be the same on both devices?──Curious Dear Curious, You can't always guarantee the images will be the same because the images that are produced depend on the capability of the output device. For example, monitors differ in number of colors and resolution as well as support from the device driver. To get the same image on different devices, you must use SelectObject to give each device the same pen, brush, and font. Then it's up to Windows to match what you send to the capabilities of the device. Self-Modifying Code Dear Dr. Bob, Let's say that you have a program that manipulates its own code. How then can it function with only one code segment? Also, is there an objective reality?──Philosophical Dear Philosophical, Well, if you must write self-modifying code... be sure that the modified code can be relocated, and specify that it is not discardable. If Windows discards and reloads, then some can be lost. To prevent this, state that it isn't moveable in the definition file. Windows uses the same code segment when you load multiple instances of the same program. To force Windows to load a fresh copy of the code specify: CODE MULTIPLE MOVEABLE in the Module Definition File, passed to the linker. As for objective reality, Dr. Bob defers to his colleague Dr. Science of San Francisco, Calif. Figure 1: #ifdef MASM_STYLE #define CONSTANT(name,value) name = value #define INTVAR(name) name DW ? #define INTARRAY(name,size) name DW size dup(?) #define STRUCVAR(name,strc) name DB (size strc)dup(?) #define DEFSTRUC(name) name STRUC #define ENDSTRUC(name) name ENDS #endif #ifdef C_STYLE #define CONSTANT(name,value) #define name (value) #define INTVAR(name) int name; #define INTARRAY(name,size) int name[size]; #define STRUCVAR(name,strc) struct strc name; #define DEFSTRUC(name) struct name { #define ENDSTRUC(name) }; #endif Figure 2: #include "c_or_asm.hdr" CONSTANT (ScreenHt, 200) CONSTANT (ScreenWidth, 640) CONSTANT (MaxNumPts, 1024) DEFSTRUC (Point) INTVAR (x) INTVAR (y) ENDSTRUC (Point) STRUCVAR (startPt,Point) INTVAR (counter) INTARRAY (data,MaxNumPts) Figure 3: $cursor dd 0b8000000h ;for MONO adapter use 0b0000000h $hexdig db '0123456789ABCDEF' ;------ Display character in AL and advance cursor: $showc: push es,di les di,cs:$cursor ; Get current cursor ; (segment & offset) mov ah,07h ; Set attribute cld stosw ; Write char to screen mov word ptr cs:$cursor,di ; Save csr (offset only) pop di,es ret ;------ Display value in DX as a hex number: $showx: push f,ax,bx,cx mov cx,12 ; Shift by 12,8,4, and 0 bits $sx1: mov bx,dx ; Get number from DX shr bx,cl ; Get next nybble to display and bx,000fh mov al,cs:$hexdig[bx] ; Map nybble to hex digit call $showc ; Show hex digit sub cl,4 jns $sx1 ; Keep going until shift count ; is neg. pop cx,bx,ax,f ret ;------ Display string stored in code segment just after CALL to ;------ this routine: $shows: push f,ax,bx,bp mov bp,sp mov bx,[bp+8] ; Get return addr - ; it points to string $ss1: mov al,cs:[bx] ; Get next char from string inc bx ; Advance string pointer or al,al ; Reached 0 end of string? jz $ss2 ; Exit if so call $showc ; Display char jmp $ss1 ; Back for more $ss2: mov [bp+8],bx ; reset return address pop bp,bx,ax,f ret ;------ Macro to display a string. ;------ Example: SHOWS 'Booga booga!' SHOWS macro str call $shows db str,0 endm ;------ Macro to display a register. Example: SHOWR BX SHOWR macro reg SHOWS ' ®=' push dx mov dx,reg call $showx pop dx endm ████████████████████████████████████████████████████████████████████████████ Carl's Toolbox Carl Warren Starting a new column in a new magazine can be a tricky business. Often you aren't sure who the reader is or what the interest level might be. Luck and a great deal of experience seem to be with MSJ. We have a pretty good idea of who you are and what you want. Even though the column is called "Carl's Toolbox," the fact is, it's your toolbox. In amateur radio days we called this our junkbox. That was the old wooden or paperboard box under the bench that contained odds and ends, a few chassis and boards that we picked up at a junkyard or swap meet and from which we all vowed to create a masterpiece of ham radio design. One old friend of mine used this ploy to smuggle in his brand-new Collins S line──old-time hams know this was the good stuff. He had convinced his wife that all those old military chassis, tubes, and other collected junk would soon be transformed into a work of art──which, some six months and about $5,600 (equipment was much cheaper in 1959) later, it was. Times have changed and so has the equipment. Today, prices are a little better, and yes, you may decide that building is far better than buying, especially when the tool you need doesn't really exist. I'm going to tell you not only about the tools you can buy, but about those that you can build yourself. Since I'll be taking a hands-on approach, I'll tell you how well various pieces of equipment work, and perhaps some interesting ways to use them. Of course, not all the tools and equipment that I'll be talking about come from manufacturers. Every now and then a few friends and I sit down and try to noodle our way through a problem, and occasionally we develop something useful. For example, not too long ago my friend Keith dropped by my office for a chat. I was complaining about all the problems you run into when you add a new board to the system or try to get a new device driver to work. "It's like a continual series of reboots," I said. Keith, who spends most of his time writing specialized code for various vendors, asked, "Why don't you do it in software?" He explained that by using the "A" mini-assembler command in DEBUG, you can enter the following four instructions that will reboot the system: MOV AX,40 MOV DS,AX MOV WORD PTR [0072],1234 JMP FFFF:0000 The first two instructions (MOV AX,40 and MOV DS,AX) put the hex value 0040 into the 8086 DS register. This points the 8086 at the area of memory in which the IBM PC's ROM I/O, reset, and power-on routines are located. The next instruction (MOV WORD PTR [0072],1234) stores the hex value 1234 to the IBM PC ROM's Reset flag data field. The ROM's power-on routines use this flag to determine when a Ctrl-Alt-Del reset is in progress or whether a power-on reset is occurring. The 1234 value transfers control directly to the power-on routines, and the memory and I/O checks are skipped. If the Reset flag contains garbage, as it would during power-up, all the checks are performed. Of course, if you want the checks, change the 1234 to 0000. When the program is executed, a full I/O and memory check takes place. That's not a bad little piece of utility software for those of you who collect routines. I have a few others that will be published in future issues. Keeping Track of Changes IBM Personally Developed Software: SuperC Donald M. Ludlow and Randy Black, IBM Personally Developed Software; P.O. Box 3280; Wallingford, CT 06494; (800) 426-7279 Price: $29.95 Whenever I review a product from IBM, I'm always afraid that if I don't like it, someone will accuse me of being biased against IBM. So, when I was asked to test-drive IBM's Personally Developed Software package, SuperC, I heard an anxious voice in the back of my mind say, "What if it's junk?" After all, the software in this series has ranged from excellent to mediocre, so I wasn't exactly sure what to expect. First of all, SuperC is neither a C-language development package nor a tutor. It is, however, a powerful and useful utility that allows programmers, writers, editors, and anyone who deals with stored information that changes to keep track of those changes. In operation, you can find changes in source code, binary code, or just plain ASCII words. For example, suppose in updating a document you accidentally insert a large space or new word in a sentence, something that just shouldn't be there or that changes your meaning. SuperC uses its word- compare feature to notify you that a change has occurred. Of course, in order to compare changes, both an old and new file must exist. In the case of WordStar users, where a backup copy is kept, the new copy is compared with the old through the use of simple syntax: A>SuperC [d:][path]newfile [d:][path]oldfile In some cases, the only thing you would be interested in is whether or not there are changes in files. When a change is detected by using the file- compare feature, the program reports that there are indeed differences. At this time, you can determine whether to search for word, line, or byte differences. Within SuperC is the tagged-as-is delta-file (Tasdf) file format, which is intended for experienced programmers. It generates a special output file similar to a delta listing with a prefix tagged onto each line, including T) for title record, I) for inserted line, D) for deleted line, RN for reformatted line, S) for summary record, and E) for end record. Although it appears to be just a simple compare program, SuperC includes capabilities such as bypassing BASIC line numbers, processing assembler comments and blank lines, and even substitution for nondisplayable characters. Also, you can specify what line(s) should be ignored in the process. Don't be concerned about space, either; SuperC manages just about any length of file. Recently, I compared a long ASCII file in a standard document of over 70K in length with no difficulty. SuperC offers on-line help at the touch of a function key (F1). All documentation is on-line and can be printed out. Since the next several columns will address various programming methods, I'll be using the listing function in SuperC, which prints up to 132 columns wide. Getting All the Fax GammaLink GammaFAX; 2452 Embarcadero Way; Palo Alto, CA 94303; (415) 856-7421 Price: $995 Includes a synchronous modem board that fits in one slot on an IBM PC, XT, or AT, or a similar machine, and the GammaFAX software package. The GammaFAX board provides bidirectional facsimile transmission CCITT group III to and from PCs, as well as PC-to-PC communications with full data compression and error checking up to 9,600 bits per second. As more and more technology comes into the office, the desktop of the future is getting quite cluttered. The stacks of paper and in-and-out baskets have been pushed to one side to make room for IBM PCs, printers and facsimile machines, mice, and whatever else that manufacturers can dream up. At least one of these desktop items, the facsimile machine, can be moved off the desk and back to the store room. GammaLink's GammaFAX board and software can take over the chores once relegated to the standalone facsimile. Besides fitting into an open slot on the PC backplane, the GammaFAX board can also be plugged into any available phone jack with an RJ-11 receptacle. By doing this you can probably reclaim at least 2 square feet of your desk. The GammaFAX system is capable of taking existing text files or even saved picture files and shooting them out to a facsimile machine anywhere in the world. And since facsimile is a two-way street, so is GammaFAX: the machine can be set to work in an answer facsimile mode or to communicate with other PCs for very rapid file transfer. The GammaFAX software is flexible and smart. By entering the DEBUG option, you can have the program report the status of the operation, thus ensuring a great deal of confidence in the operation of the line as well as the modem. A series of menus facilitates use of the program. For example, on start-up you are greeted with a menu that asks what you want to do: send, receive, convert programs for facsimile transmission, or set up a string of files for sending or receiving. You make your choices with the function keys, which direct you to another level of menus. F10 always takes you back to the previous level. The GammaFAX requires the undivided attention of the machine, which is fine unless you have to get some other work done while sending. The store and forward function does allow you to set times for a transfer to begin, but does not let you use the machine. I set up a Windows definition file for GammaFAX, so it could be called from Microsoft Windows, and there were no ill effects on either application. It would be nice, though, if the GammaFAX people would do some Windows compatibility rewriting so that it can be called as a windowed task and work in the background. You can use GammaFAX with a PC, XT, or AT. I strongly recommend the AT because it lets you use all the speed that the board has. The manual, although poorly written and in need of a good layout job, contains lots of information──you just have to search hard for it. Clever programmers will be able to build a better software mousetrap for communications by using the board and the information contained in the manual. The GammaFAX board is essentially a facsimile machine, so software developers will find it ideal for creating desktop publishing images. GammaFAX makes it possible to obtain pictures from sources other than scanners. Scanners are becoming an important tool for desktop publishing, as they allow photographs or other complex pictures to be scanned in. There is a problem, though. GammaFAX stores pictures in CCITT group III format, which is good for facsimile, but not so good for most of the formats used with bit-image file formats. GammaFAX does provide the utilities necessary to scan images in with a Canon scanner and convert them for facsimile transmission. In the group III format, the least significant bit of the first byte in the file is the first bit transmitted during the group III phase of the in message of a facsimile call. The most significant bit of the last byte of the file is the last bit transmitted. A standard-resolution facsimile page has 1,728 dots across and 1,075 lines in the vertical dimension. A high- resolution picture, such as a half-tone, is 1,728 by 2,150. The FAX virtual page program, supplied with the board, lets you convert the binary image format into a bit-image format for use with such programs as Media Cybernetics's Dr. Halo II. Since the image does have specific characteristics, it can be manipulated into other forms. The Tag Image File Format (TIFF), a newly proposed format developed by Aldus Corp., Dest Corp., and Microsoft Corp., provides a structured format for digital data. TIFF is based on CCITT group III and IV formats, but it adds additional information for the digital data, including color and detail about individual pixels, so that a full picture can be saved. ════════════════════════════════════════════════════════════════════════════ Vol. 2 No. 2 Table of Contents Microsoft Operating System/2: A Foundation for the Next Generation Microsoft's joint effort with IBM to develop an operating system for Intel 80286- and 80386-based computers resulted in Operating System/2. Using the 286's protected mode, OS/2 not only breaks the 640K memory barrier, it also provides multitasking capability and inter-process communication. OS/2 Windows Presentation Manager: Microsoft Windows on the Future Included as a standard part of the new Operating System/2, the Windows presentation manager will provide OS/2 with the same advantages that Microsoft Windows brings to MS-DOS, such as a standard user interface and a rich independent graphic application environment. OS/2 DOS Environment: Compatibility And Transition for MS-DOS Programs An important OS/2 feature is its MS-DOS compatibility environment, a mode that emulates DOS 3.x and runs most existing programs. Discussed are the restrictions of the compatibility mode and the considerations in the construction of programs designed to run with both MS-DOS and OS/2. OS/2 Multitasking: Exploiting the Protected Mode of the 286 The Microsoft OS/2 scheduler, the dispatcher, and the protected mode of the 286 are the keys to OS/2's multitasking operation. This article considers the system developers view of the fundamental elements of multitasking: processes, threads, and screen groups. OS/2 Memory Management OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues Multitasking operations need fast and reliable communication between multiple processes and threads. To offer the system developer a wide range of facilities to choose from, OS/2 depends on shared memory as well as system semaphores, pipes, and queues for rapid exchange of information. The MS OS/2 LAN Manager A Complete Guide to Writing Your First OS/2 Program Discussions of program development for OS/2 introduce a new vocabulary. Dynamic linking or module definition files may at first seem forbidding, but they, like OS/2, are variants of familiar concepts. We offer a sample program complete with multitasking and inter-process communication. Turning Off the Car to Change Gears: An Interview with Gordon Letwin With more than 350,000 lines of code, OS/2 is unquestionably the most complex software that Microsoft has designed. Microsoft Systems Journal spoke with Gordon Letwin, the chief architect of OS/2, to learn more about the challenges of designing Operating System/2. A Simple Windows Application For Custom Color Mixing Our regular programming article presents COLORSCR, a deceptively simple program. On the surface, it provides three scroll bars to adjust the red, green, and blue components of GDI color specification. The elegance of the design is its use of child windows to manage the screen area. Ask Dr. Bob EDITOR'S NOTE The recently announced IBM Personal Series/2 computers and the first product of Microsoft's joint development agreement with IBM, Microsoft Operating System/2, advance personal computing to new levels of sophistication and improved performance. For the end user, this translates into greater ease of use. For the software developer, however, improvements in technology often mean increasingly complex procedures that bring new challenges and probably a few frustrations as well. To help the software developer unravel the intricacies of OS/2, this issue of Microsoft Systems Journal provides an overview of the new operating system as well as a detailed look at OS/2's basic features──multitasking, inter-process communication, and memory management. And MSJ doesn't stop there──the issue continues to explore OS/2 with articles on the MS-DOS compatibility environment, the Windows presentation manager, and an interview with Microsoft's chief architect of OS/2, Gordon Letwin. What will OS/2 mean to the PC world? Although no one disagrees that the impact will be deep, standards are not made overnight. Because MSJ is committed to the concerns and interests of the software developer, we are planning to bring you a great deal more on OS/2, but certainly not to the exclusion of MS-DOS. MS-DOS will continue to be important for a long time to come. A reminder to DIAL subscribers: Code samples in this and any previous issue are available on-line. Keep an eye on the DIAL Bulletin Board for more information on this. We've said it before, but it's worth repeating. We want MSJ to be your journal, your forum for exchanging ideas. Write to us with your suggestions for articles, your reactions to this and previous issues.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor BARRY OWEN Managing Editor CHRISTINA G. DYAR Associate Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor DIANA E. PERKEL Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher BETSY KAUFER Administrative Assistant Copyright(C) 1987 Microsoft Corporation. All rights reserved; reproduction in part or in whole without permission is prohibited. Microsoft Systems Journal is a publication of Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H. Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley, President and Chief Operating Officer; Francis J. Gaudette, Treasurer; William Neukom, Secretary. Microsoft Corporation assumes no liability for any damages resulting from the use of the information contained herein. Microsoft, the Microsoft logo, MS-DOS, and XENIX are registered trademarks and CodeView is a trademark of Microsoft Corporation. Macintosh is a trademark of Apple Computer, Inc. IBM is a registered trademark and Personal System/2 is a trademark of the International Business Machines Corporation. PageMaker is a registered trademark of Aldus Corporation. UNIX is a registered trademark of AT&T. Compaq is a registered trademark of Compaq Computer Corporation. Intel is a registered trademark of Intel Corporation. Paintbrush is a registered trademark of ZSoft Corporation. ████████████████████████████████████████████████████████████████████████████ Microsoft Operating System/2: A Foundation For the Next Generation ─────────────────────────────────────────────────────────────────────────── Also see the related articles: Evolution and History of MS-DOS CMD.EXE-OS/2 Command Processor System Configuration (CONFIG.SYS) ─────────────────────────────────────────────────────────────────────────── Tony Rizzo After much waiting, speculating, and hoping, the word from Microsoft and IBM is out, and that word is Operating System/2 (OS/2), their new single- user, multitasking operating environment. OS/2 is the successor to MS-DOS (and PC-DOS) and is the first product of Microsoft and IBM's Joint Development Agreement, which is especially relevant to MSJ's readers. At the same time that the software announcements were made, IBM launched its next generation of personal computers, the Personal System/2 series. On April 2, 1987, a host of new IBM hardware and software offerings were introduced, but the focus was decidedly on the new IBM machines: an entry- level 8086 PC and new 80286- and 80386-based machines, built mainly around in-house VLSI technology and offering substantial price/performance improvements over the old hardware. Very briefly, the new systems boast switchless planar (or self-configuring system) boards with on-board video and disk controllers, greatly expanded RAM capabilities, 3-1/2" floppies, and new IBM hard files with a 1-to-1 interleave factor. The new 286 and 386 machines also usher in IBM's 16/32- bit Micro Channel architecture, a new bus structure that greatly improves data throughput, and a new Video Graphics Array (VGA) display subsystem. VGA offers built-in, 640-by-480 resolution with 16 colors, which is necessary for advanced graphics─based software. For software developers, however, the big news is the new Microsoft OS/2 environment, which will run on the new 286 and 386 machines, as well as on the older PC AT and compatibles. ─────────────────────────────────────────────────────────────────────────── The Parallel Growth of Hardware and Software. As hardware continues to provide enhanced and more sophisticated services, the MS-DOS world will slowly give way to OS/2 and new multi-applications systems. ┌───────────┬─────────────┬────────────────┬───────────────┬──────────────┐ │ │ 8088 │ 8086 │ 80286 │ 80386 │ ├───────────┼─────────────┼────────────────┼───────────────┼──────────────┤ │MS-DOS 1.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│═══════════════│──────────────│ │ │ │ │ │ │ │ │ │ │ │ │ │MS-DOS 2.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│═══════════════│──────────────│ │ │ │ │ │ │ │ │ │ │ │ │MS-DOS 3.x │≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│════════REAL MODE ONLY═══════>│ │ │ │ │ │ │ │ │ │ │ │ │MS-OS/2 │ │ │≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡│≡≡≡≡≡≡≡≡≡≡≡≡≡>│ │ │ │ │ │ │ │ │ │ │ │ │ │MS-OS/2 386│ │ │ │≡≡≡≡≡≡≡≡≡≡≡≡≡>│ └───────────┴─────────────┴────────────────┴───────────────┴──────────────┘ ─────────────────────────────────────────────────────────────────────────── The Connection In contrast to the current situation in which IBM offers PC-DOS and other manufacturers offer the slightly different MS-DOS, what IBM calls "Operating System/2 Standard Edition" and what Microsoft calls "Operating System/2" are in fact one and the same operating system. Perhaps more important is that the Windows presentation manager, the MS OS/2 version of Microsoft Windows, is an integral part of the new operating system. Both MS OS/2 and the Windows presentation manager are in compliance with IBM's recently announced Systems Application Architecture (SAA), a set of common specifications created to promote "consistent" applications development across the entire line of IBM System/370, System/3x, and personal computer systems. To date, every version of MS-DOS, including the newly announced Version 3.3, has been a single-process operating system. That is, they can only execute one application at a time. This restriction is a consequence of the 8086/8088 architecture, which is limited to 1Mb of memory and offers no hardware support for protecting memory──any application can access at will any memory location at any time. Unrestricted memory and device access by software may very well be a boon to those creative programmers who look to squeeze every bit of performance out of the MS-DOS, 8086/8088 environment, but it poses serious problems for those trying to run more than one application at the same time. Furthermore, the memory map of the MS-DOS, 8086/8088 environment leaves only 640Kb of main memory available for both applications and the operating system. Though MS-DOS has been continually refined─especially in its file management, device support, background processing (e.g., print spooling), network support, and very rudimentary attempts at multitasking──it has remained fundamentally a single-user, single-application design. Almost the Promised Land Without hardware-based protection mechanisms, an operating system would have a very difficult time managing more than one application; preventing applications from accessing conflicting memory locations becomes almost impossible. Intel addressed this problem by including a "protected mode" of operation in the design of its 80286 processor. On account of this, expectations for an enhanced operating system were high when IBM introduced the 286-based PC. The 286 processor's protected mode of operation, its ability to address up to 16Mb of memory, and the fact that the 286 also provided a "real" mode fully compatible with the 8086/8088 suggested the potential for a more powerful operating system. Unfortunately, the 286 was designed before the importance of the IBM PC (8088) standard was established. As a result, the 286 cannot simultaneously support both real and protected modes─a major drawback. Adding to the problem, the 286 real mode has every deficiency of the 8086 environment: the 640Kb barrier, no memory protection, and, as a result, no multiple-application operation. Because of these 286 shortcomings, it was not feasible to build a protected-mode operating system with MS-DOS compatibility before the release of the PC AT. So, rather than take a half-step, Microsoft and IBM decided to invest in a new, extensive development effort that would focus on a protected-mode operating system (see "Turning Off the Car to Change Gears: An Interview with Gordon Letwin,"). The new operating system would take advantage of the hardware, break the 640Kb memory barrier, allow a multi-application, multitasking environment, and still permit total compatibility with MS-DOS applications. The result of this effort is MS OS/2. ─────────────────────────────────────────────────────────────────────────── Block Diagram of the MS OS/2 Environment. MS OS/2 is a complicated set of interrelated modules working together to provide a complete multitasking, virtual memory environment. Application Application (or Library Routine) │ │ │ Dynamic Link │ │ │ │ │ │ Function │ │ │ │ │ │ Call Interface │ │ │ │ │ ▼ ▼ │ │ │ │ ┌─System Session Manager─┐ │ │ │ │ │ ▼ │ │ │ │ │ │Console Device Interface│ │ │ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ╔══════════════════════════════════════════════╗ ║ Kernel ║ ║ ┌───────────┐ Process Management ║ ║ │File system│ Memory Management ║ ║ └───────────┘ Program Management ║ ║ Time Management ║ ║ I/O Management ║ ╚══════════════════════════════════════════════╝ ↕ ░░░░░░░░░░░░░░░░ Device Drivers ░░░░░░░░░░░░░░░░ ↕ ░░░░░░░░░░░░░░░░░░░ Hardware ░░░░░░░░░░░░░░░░░░░ ─────────────────────────────────────────────────────────────────────────── The Real Thing MS OS/2 is a priority-based, pre-emptive, multitasking environment which includes a new application interface that isolates applications from low- level hardware differences between machines running MS OS/2. OS/2 utilizes the protected mode of the 286 chip to allocate an exclusive address space to each application. OS/2 controls access to memory, so that different applications cannot corrupt each other's data and program areas. In protected mode, the 286 uses the contents of the segment register to access a local descriptor table (LDT) containing information about each application. The contents of the segment register serve as an index into the descriptor table. The operating system also accesses a global descriptor table (GDT), which contains information on code and data that is shared by all executing applications (see Figure 1). Both of these tables are exclusively managed by the operating system, and together they allow up to the entire 16Mb of real memory to be mapped to up to 1 gigabyte of virtual memory. Also, application programs are normally prevented from writing directly to the underlying hardware. The 286 architecture provides for discrete I/O privilege levels (IOPL) to be assigned to applications; this capability enables the operating system to control access to hardware. Although it is possible for an application to obtain hardware control if it can gain access to an appropriate privilege level, the operating system generally does not explicitly make that privilege available to an application. Protected mode and IOPL are the basic, 286 hardware──based underpinnings of MS OS/2 (for a detailed discussion of privilege levels and how OS/2 performs multitasking, see "OS/2 Multitasking: Exploiting the Protected Mode of the 286,"). Memory Management In protected mode OS/2 can simultaneously run many applications, any one of which may cause the amount of physical memory available to the system to be exceeded. When an application's memory requirements exceed real memory (referred to as "memory overcommit"), OS/2 will swap memory segments to disk as required (see Figure 3). OS/2 dynamically expands and contracts allocated memory and collects fragmented memory allocations (known as "memory compaction"), which provides efficient control of available memory. Microsoft has gone to great lengths to protect the enormous investment in pre-MS OS/2 software. To achieve this goal, OS/2 provides a compatibility mode in which real-mode MS-DOS applications can run (see Figure 3). The compatibility mode offers an 8086 environment in which most MS-DOS programs will run. However, a small number of programs, especially those that directly access hardware or with timing dependence, won't run in compatibility mode. Compatibility mode allows for one MS-DOS program to execute in the foreground; background, light protected-mode applications are suspended when in this mode. A program executing in the compatibility mode can only run in the foreground─such a program is suspended if one of the background protected-mode applications is selected for foreground operation. Command Processors Microsoft OS/2 comes with two command processors, COMMAND.COM and CMD.EXE. The former is the real-mode command processor, similar to the MS-DOS command processor, with its familiar initialization, resident, and transient parts. As in MS-DOS, COMMAND.COM is used to start programs when in compatibility mode. CMD.EXE is the MS OS/2 command processor, which offers an extended command syntax. Most important, however, is that CMD.EXE is both shareable and swappable. This means that multiple copies of CMD.EXE can be executed, but only one copy of CMD.EXE is actually kept in memory, and that copy is shared by all protected-mode applications. The only additional memory used is that which is unique to any given application. Presentation Manager OS/2 consists of two fundamental, interrelated parts. The first is the kernel, which provides for the major internal workings of the operating system. Integrated into the MS OS/2 system is the Windows presentation manager. Like Microsoft Windows for MS-DOS, it is a consistent, graphic user interface and a device-independent applications environment. The MS OS/2 Windows presentation manager is derived from the current Microsoft Windows, Version 1.03, software. The MS OS/2 Windows presentation manager, however, has been significantly enhanced; it has been designed to use multiple overlapping windows and to conform to the Common User Access component of IBM's SAA. The key word is consistency. Microsoft will soon release Windows, Version 2.0, for the MS-DOS environment with the same user interface as the Windows presentation manager. The visual interface will be consistent in both MS OS/2 and MS-DOS. Also, because the Windows presentation manager is consistent with SAA, this new standard is expected to eventually find its way into a large number of computer systems, from micros to mainframes. For the end user, the Windows presentation manager offers ■ the ability to run graphics-oriented programs ■ a simple and consistent user interface ■ access to the session manager ■ enhanced mouse and keyboard control And, for the software developer, the Windows presentation manager provides ■ access to the MS OS/2 multitasking and memory management kernel functions ■ access to intertask communication and data interchange facilities ■ a device-independent output architecture ■ a rich and sophisticated graphics library The Session Manager The new Windows environment gives the user full control over the operating environment. The windows environment accomplishes this by offering users a session manager, which allows users to control the status of concurrently running applications, as well as that of any application running in compatibility mode. The user requests access to the session manager with the SysReq key, and the session manager responds with a list of active applications (see Figure 4☼). The user can then bring any process to the foreground, start any number of new processes, or terminate any number of existing processes. There is a lot more to the session manager, however, than meets the eye. The New Frontier When the session manager is active, it switches between different screen groups. To understand screen groups we need to take a close, if somewhat brief, look at the activity underlying executing applications. MS OS/2 applications are made up of one or more processes, which are in turn made up of one or more threads. A thread is an "atomic unit," a unique entity that the MS OS/2 scheduler can allocate CPU cycles to, that is, it is actually executable by the CPU. A process can have any number of executable threads, each of which can be in one of three states: blocked, ready to execute, or executing. Processes are, in turn, assigned to screen groups. Screen groups are collections of processes that write to a common virtual display and receive input from a virtual keyboard. As an example, a screen group might be a collection of related windows, each of which is running its own process and each of which was started inside a parent window (screen). The screen group writes to a virtual display and reads from a virtual keyboard because it is possible to execute any number of screen groups, each with its own screen and keyboard needs. Since there is only one physical screen and keyboard, there must be some system to prevent applications from conflicting with each other. MS OS/2's solution maps each screen group to its own unique virtual screen, eliminating any possible conflicts. When a user requests access to an active application, the session manager maps that application's screen group to the physical screen─more commonly known as the foreground. MS OS/2 manages the mapping of virtual screens to the physical screen with the session manager (see Figure 5). The user has complete control over which applications MS OS/2 will bring to the foreground, and over initiation or termination of new applications (and their associated screen groups). There is a lot going on here. Obviously, OS/2 has provided the user with a great deal of flexibility. But what has the software developer gained? API and FAPI MS OS/2 offers a rich application program interface that allows software developers to fully exploit the resources of the operating system. For example, the Windows presentation manager provides a large set of sophisticated graphics functions, giving the software developer the building blocks necessary to create very sophisticated screen images. MS OS/2 provides a substantial set of CALL-based functions──189 of them to be exact──to assist in the development of programs that will run in a multitasking environment. These functions, which handle file and device management, memory management, and task management, are known as the kernel, or operating system Application Programming Interface (API). The API includes facilities to write device drivers, known as the Device Driver Interface. Device drivers handle device initialization, transfer of data to and from devices, and device error control, and they provide access to low-level BIOS functions. High-level languages such as C can call these functions directly. These callable functions completely eliminate the need to depend on interrupts (for example, Interrupt 21 function calls) and the need to write directly to hardware. API also provides functions to deal with the complexities of inter- process communication (IPC). Processes and threads communicate through semaphores, queues, pipes, and shared memory (see "OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues,"). API's functions make such complex programming significantly easier to write. Microsoft will support all of these functions as a standard. There will be a substantial improvement in the ability to port software to future releases of MS OS/2, as well as an enhanced ability for the products of different software developers to interact with each other. Microsoft suggests that applications conforming to the API standard will run within the MS OS/2 environment and future releases of OS/2. This creates an opportunity to standardize coding practices throughout the entire PC environment. Realistically, OS/2 won't become as pervasive as MS-DOS for some time yet. It will most likely be at least 3 years before 286- and 386-based machines take over the major share of the PC market. MS-DOS, Version 3.3, will remain with us for the foreseeable future, so developers will want the ability to write software that can move between MS-DOS and MS OS/2. This need, however, has already been anticipated and resolved; Microsoft guarantees to support a subset of the API function set that will execute in every available mode of the two operating environments. Dubbed the Family Application Programming Interface (FAPI), this set of 91 functions──25 of them are somewhat restricted──will permit programs to execute under MS-DOS 3.3, within the compatibility mode of MS OS/2, and as an actual, although limited, MS OS/2 application (see Figure 6). API/FAPI represents a unique opportunity for software developers to create applications that can deal with current limitations while incorporating future advances, thus providing users to run state-of-the-art applications on older machines. Software Assistance By August, Microsoft plans to release a Software Development Kit (SDK) that will contain the full specifications of MS OS/2─as well as a beta- release version of the operating system kernel. The toolkit will come with C Version 4.5, MASM Version 4.5, and the CodeView debugger. The new compiler and assembler will be able to generate code to be used with the new Microsoft Dynamic Linker. The linker will allow code to be linked with FAR calls, creating external references to various libraries. These external references eliminate the need to link library routines into each object (or load) module. The Dynamic Link Editor services program needs by loading library routines only when necessary. The LAN Manager The LAN Manager will play an important role in the MS OS/2 environment and can be fully integrated into MS OS/2. It promises an exciting new applications environment in which different applications, such as spreadsheets, word processors, and database systems, will be able to communicate with each other not only application-to-application (e.g., spreadsheet to word processor) within one processor, but also among processors connected to a network. The LAN API component allows developers to take full advantage of the capabilities of the LAN Manager so as to build truly distributed applications. It will also include site-to-site electronic-mail capabilities and resource sharing. (For more information on the LAN API, see "The MS OS/2 Lan Manager,"). Help from the 286 The 386 chip provides much faster speed, 1 gigabyte of addressable real memory, and a huge virtual address space─64 terabytes! It is also capable of running "virtual" 8086 machines, obviating the need for a compatibility mode. Users can run multiple MS-DOS sessions within the multitasking environment. Microsoft estimates that only 10 percent of the MS OS/2 code will have to be modified or rewritten, primarily in the memory management component, to utilize the 386. This code modification will be completely transparent to applications written under the MS OS/2 API on the 286 chip; MS OS/2 has been standardized in this respect. With the next release of MS OS/2, software developers may gain access to additional tools, but they won't have to rewrite any code because of changes made to the existing API. This means, of course, that software developers can begin now to create on the 286 applications that can easily be ported to the 386──a substantial development advantage. A New Philosophy MS OS/2 is obviously a greatly improved and sophisticated piece of software. Software development under this new environment will require a considerable effort to understand just how all of the pieces fit together. It will take some time before developers can claim mastery of all the tools currently available. There will also be a shift in emphasis from programming around the operating system to programming with it. Instead of hunting for undocumented interrupts that perform unsupported services and bypassing the operating system to get to the hardware, developers will learn to integrate all of the tools now at their disposal. With new hardware that virtually eliminates performance bottlenecks and a new operating system that takes full advantage of that hardware, developers can focus directly on their applications. Conclusion Although they are not exactly right around the corner, it won't be long before we will have applications "systems" that interact with each other, work together across networks, and share resources efficiently in environments spanning micros, minis, and mainframes. What will become of the "under-the-hood" programmer? What about all of those who still long to hack, to get their fingers dirty by directly driving hardware, manipulating code and data segments, and fooling with undocumented system calls? Well, MS OS/2 is undoubtedly full of mysterious code to decipher, but the real challenge will be fully utilizing the many tools and facilities that are now available to create state-of-the-art applications. For the serious microcomputer software developer the future is MS OS/2, and the future is now. ─────────────────────────────────────────────────────────────────────────── Evolution and History of MS-DOS ─────────────────────────────────────────────────────────────────────────── IN AUGUST 1981 Microsoft introduced MS-DOS, Version 1.0, a simple operating system providing support for a new IBM machine and a fledgling industry. Over the last six years the machine and the industry have grown up, and MS-DOS has evolved into MS OS/2, a very sophisticated, multitasking operating environment. Presented here is a brief but complete history of MS-DOS. It is hoped that the reader will find it entertaining, nostalgic, and perhaps revealing. How many readers know, for example, that the original design of the File Allocation Table (FAT) can be traced to Mr. Gates himself? 1974: Intel introduces the 8 bit 8080 processor. January 1975: MITS introduces the $400 Altair computer; it has no keyboard, no monitor, no disk, and no operating system. February 1975: Paul Allen and Bill Gates develop and sell their own version of BASIC to MITS for the Altair. February 1976: Paul Allen, now working for MITS, asks Bill Gates to write a disk-based version of BASIC for the Altair. Bill Gates creates a working model of his disk BASIC in 10 days. He designs a disk layout and file structure based on a centralized File Directory and File Allocation Table (FAT). He also includes a rudimentary set of file services in the disk BASIC he is developing. 1976-1978: Microsoft Disk BASIC is ported to all major 8 bit personal computers. An assembler and linker are developed for 8080- and Z80- based systems. April 1978: Intel announces the 8086, a 16 bit processor. January 1979: Tim Paterson of Seattle Computer Products begins work on a plug-in 8086 processor card to bring the power of the 8086 to the S-100 bus. June 1979: Microsoft and Tim Paterson show Microsoft's BASIC running on Paterson's 8086 card at the National Computer Conference in New York. April 1980: Delays hold up the delivery of CP/M 86. Tim Paterson decides to write his own "Quick and Dirty" OS, which becomes known as 86-DOS. He incorporates the FAT structure first designed by Bill Gates for Disk- BASIC, and some features and techniques underlying MS-DOS. August 1980: IBM takes its first step towards producing the IBM PC, planning to use readily available, off-the-shelf 8 bit hardware. IBM visits Microsoft, asking if Microsoft can write a ROM-based BASIC for the computer IBM is developing. Microsoft suggests that IBM consider the 16 bit architecture. IBM's "Project Chess" goes on to become the 8088 (8086-based) IBM PC. The first working version of 86-DOS runs on Tim Paterson's 8086 card. This is essentially the birth of what will become known as MS-DOS. September 1980: IBM asks Microsoft to provide COBOL, FORTRAN and Pascal for their personal computer. Microsoft suggests to IBM that an operating system would be necessary to develop the additional languages. October 1980: Microsoft submits a proposal to IBM that includes MS-DOS. November 1980: The proposal is accepted by IBM. A prototype machine arrives at Microsoft and a small DOS team begins a concentrated period of work. February 1981: 86-DOS runs on the prototype for the first time. Over the next half year the OS is refined and becomes MS-DOS, Version 1.0. August 1981: IBM introduces the IBM PC, and announces three operating systems: MS-DOS, CP/M 86, and the P System. For several months MS-DOS is the only OS available. It is also priced substantially lower than CP/M. MS-DOS incorporates a number of advanced features: ■ Greater device independence. ■ Greater data integrity. ■ Simplified handling of peripheral devices. ■ A replaceable COMMAND.COM processor. February-April 1982: Microsoft introduces for the MS-DOS environment: ■ The Macro Assembler. ■ The FORTRAN Compiler. ■ The COBOL Compiler. June 1982: MS-DOS, Version 1.1, is announced, providing support for double-sided, eight sector diskettes on the IBM PC. March 1983: IBM announces the PC XT. Microsoft announces the release of MS-DOS, Version 2.0, which includes: ■ Supports for hard disks including a XENIX(c)-like hierarchical directory structure. ■ File Handles, allowing programs to reference files anywhere they have been loaded. ■ The ability to redirect the I/O of a program to any file or device, and implements pipes and filters. ■ Support for nine-sectored diskettes, increasing storage to 360 Kb. ■ Provision for installable device drivers, promoting logical device independence. ■ Background printing through PRINT.COM. ■ ANSI.SYS device driver, providing support for serial "stream" processing to give cursor positioning and color control information to the monitor. March 1984: IBM introduces the PCjr with half-height disk drives, and MS- DOS, Version 2.11, is introduced, providing support for it. August 1984: IBM announces the 80286 based PC AT, with a 20 Mb hard disk and 1.2 megabyte high density diskette drive. Microsoft introduces MS-DOS, Version 3.0, which includes: ■ A rewritten MS-DOS kernel and a new standard set of I/O calls. ■ ISO Open System Interconnect (ISO) based model for networking. ■ Network redirector and file sharing support for the IBM PC Network Adapter Card. November 1984: Microsoft introduces MS-DOS, Version 3.1, and Microsoft Networks (MS-Net) including: ■ Redirector and file sharing services for non-IBM network cards. ■ Transport and server functions to all MS-DOS systems. ■ Network spooled printing. ■ The basic structure for Installable File Systems. June 1985: Microsoft, Intel, and Lotus establish the Lotus-Intel-Microsoft Extended Memory Specification, allowing programs to access memory beyond the MS-DOS limit of 640 Kb of RAM. August 1985: IBM and Microsoft enter into a Joint Development Agreement. January 1986: Microsoft introduces MS-DOS, Version 3.2, which includes support for 3.5" diskettes. April 1987: IBM announces the Personal System/2 series of computers. Microsoft announces MS OS/2, with an integrated Windows presentation manager, MS-DOS, Version 3.3, Windows 2.0, and a new LAN Manager. ─────────────────────────────────────────────────────────────────────────── CMD.EXE-OS/2 Command Processor ─────────────────────────────────────────────────────────────────────────── OS/2 provides the user with a new command processor that extends the functionality of the command language and provides enhanced batch file support. Command Line Operators The following shows both the old operators as well as the new, from highest to lowest precedence. command.com cmd.exe Function ^ Lexical Escape Charactor () Command Grouper < < I/O Redirector -+ Same > > I/O Redirector |->Precedence >> >> I/O Redirector -+ Level | | Pipe Operator && And Operator || Or Operator & Command Separator The operators common to both environments work the same way they previously did. The Lexical Escape Character allows the precedence operators to be used as regular charactors. Refer to the following, 1. echo go home > kid 2. echo go home ^> kid In the first case a file called kid is created with the line "go home" as its contents. In the second, "go home > kid" is echoed to the screen. The Command Grouper works in exactly the same way parentheses work in equations, to establish explicit precedence. Refer to the following, directory a: contains the files temp1, temp2 and temp3 temp1 contains "a" temp2 contains "b" temp3 contains "c" 1. dir a:*.* >> temp1 & (temp1 >> temp2 & temp2 >> temp3) 2. (dir a:*.* >> temp1 & temp1 >> temp2) & temp2 >> temp3 After execution of the first command line temp1, temp2 and temp3 will look as follows temp1 contains a,temp1,temp2,temp3 temp2 contains b,a temp3 contains c,b,a If the second line had been executed instead, temp1, temp2 and temp3 would look as follows temp1 contains a,temp1,temp2,temp3 temp2 contains b,a,temp1,temp2,temp3 temp3 contains c,b,a,temp1,temp2,temp3 The parentheses determine the order of execution. Additionally, lower precedence operators can be executed prior to those of higher precedence. The Pipe and Redirection operators work in the same way they previously did. The And operator performs the command to its right if and only if the command to its left successfully executes. For example, in dir exist.txt && copy exist.txt lpt1 if the DIR command is successful, then exist.txt is printed. If DIR was not successful the command on the right will not be executed. The Or operator performs one of two commands depending on the success or failure of the first command given. For example, in the following more < document.txt || more < document.doc more will either output the contents of document.txt and then exit, or, if document.txt doesn't exist, will output the contents of document.doc. The Command Separator allows more than one command to be typed on a command line. For example, dir a:\*.* > temp & dir b:\*.* >> temp places the directory listing of the root directory of a: in a file called temp (which is either overwritten if it exists or is created if it doesn't exist) and then immediately executes the next command, which appends the contents of the root directory of b: to temp. BATCH FILE ENHANCEMENTS OS/2 supports all of the old batch file commands and adds four new ones. They are CALL EXTPROC SETLOCAL ENDLOCAL The Call command finally provides a means for users to chain together batch files so that when a called batch file ends it returns control to the calling batch file. Extproc allows a user to run batch files designed for command processors other than CMD.EXE. The command is issued as the first line of the batch file and defines the alternate command processor. Setlocal allows the user to define local drive, directory and environment variables for the current batch file. When the command is issued it saves the existing drive, directory and environment information and replaces it with the user specified information. In conjunction with the setlocal command, the endlocal command restores the drive, directory and environment information previously saved through setlocal, and allows additional setlocal calls. If a setlocal call is issued and the batch file issuing the setlocal call terminates without issuing an endlocal call, the operating system restores the original drive, directory and environment information, insuring that the state previously known to CMD.EXE still exists. ─────────────────────────────────────────────────────────────────────────── System Configuration (CONFIG.SYS) ─────────────────────────────────────────────────────────────────────────── CONFIG.SYS is the file that contains the commands used to configure the system. Only a single CONFIG.SYS file is needed to configure the system for both real-mode and protected-mode operations. During system initialization, OS/2 opens and reads the CONFIG.SYS file in the root directory of the drive from which it was started and interprets the commands within the file. If the file is not found, OS/2 assigns default values for the configuration commands. The following list summarizes the configuration commands for OS/2: buffers Determine the number of buffers to allocate for disk I/O. country Select the format for country-dependent information. device Specify path and filename of a device driver to be installed. iopl Specify whether I/O privilege is to be granted or not. maxwait Set the time limit for calculating CPU starvation. memman Select memory management options. priority Disable dynamic priority variation in scheduling regular class processes. protectonly Select the modes of operation. protshell Specify path and filename of the OS/2 top-level command processor. run Start a system or daemon process during system initialization. swappath Specify the location of the swap file. timeslice Set the time slice values for process scheduling. threads Set the maximum number of threads in the system. trace Select system trace. tracebuf Set the system trace buffer size. The following commands are ignored: files lastdrive The following commands apply only to the configuration for real-mode and only if the MODE command specifies that old applications will be run. These real-mode-only commands are documented after the OS/2 configuration commands. break Check for Ctrl-Break. fcbs Determine file control block management information. rmsize Select the amount of memory for real-mode applications. shell Load and start the top-level command processor. Figure 1: Virtual/Real Memory Mapping. The global descriptor table (GDT) and every local descriptor table (LDT) can each map 8191 64K segments or .5 Gb (8191 x 64K bytes= .5 Gb). There is one GDT for the entire MS OS/2 system, shared by every application. Each specific application has its own private LDT. Together, the shared GDT and the private LDT provide a virtual address space of up to one Gb for each application. The virtual addresses are managed by the operating system, and are mapped to a real memory space of up to 16Mb. Application Address Spaces Application n-┌──────────┐ ┌─┴─────────┐│ ┌─┴──────────┐││ ┌┴──────────┐ │││ 1Gb Real Application 1-│ ─ ─ ─ ─ ─ │ │││ | Memory ┌───────── │_ _Code_ _ │ │││ | ┌───────┐16M Secondary │ │ Data │ │││ | ≈ ≈ Storage Unique ├ ─ ─ ─ ─ ─ ┤ │││ | ├ ─ ─ ─ ┤ ┌────────┐ to Each ≈ ≈ │││ LDTs │ Code │╔═══════╗│ .. │ Application │ │ │├┘ | ├ ─ ─ ─ ┤║Virtual║├─ ─ ─ ─ ┤ │ ├─ ─ ─ ─ ─ ─┤ ├┘ | │ Code │╣Memory ╠│ Code │ │ │ Data ├─┘ | ├ ─ ─ ─ ┤║ Mgt ║├ ─ ─ ─ ─┤ ╞════════ ├─ ─ ─ ─ ─ ─┤ ─ ─ ─ ─5Gb─ ─ │ Data │╚═══════╝│ Data │ │ ≈ ≈ | ├ ─ ─ ─ ┤ ├─ ─ ─ ─ ┤ Shared ├─ ─ ─ ─ ─ ─┤ | ├ ─ ─ ─ ┤ │ .. │ │System Code│ GDT │ System│ └────────┘ │ │ and Data │ | └───────┘ └──────────┴───────────┘ | Figure 2: Protected Mode Memory Map ╔═════════════╗ ║ Real Memory ║ ╚═════════════╝ 16M╔═══════════════════╗ ───┐ ║ New Application ║ │ Moveable ≈ code and data ≈ ├─Swappable or ║ segments ║ │ Non-swappable 1M╟───────────────────╢ ───┘ ║ ║ ║ BIOS and video ║ Fixed ║ buffers ║ 640K╟───────────────────╢ ───┐ ║ ║ │ ║ New Application ║ │ Moveable ≈ code and data ≈ ├─Swappable or ║ segments ║ │ Non-swappable ║ ║ │ ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┘ ║ OS/2 ║ Fixed ║ ║ 0╚═══════════════════╝ Figure 3: Compatibility Mode Memory Map ╔═════════════╗ ║ Real Memory ║ ╚═════════════╝ 16M╔═══════════════════╗ ───┐ ║ New Application ║ │ Moveable ≈ code and data ≈ ├─Swappable or ║ segments ║ │ Non-swappable ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┘ ║ OS/2 ║ Fixed 1M╟───────────────────╢ ─── ║ BIOS and video ║ Fixed ║ buffers ║ 640K╟───────────────────╢ ╟─ ─ ─ ─ ─ ─ ─ ─ ─ ─╢ ───┐ ║ Single ║ │ ≈ "old" Application ≈ │ ║ ║ ├─Variable size ╟───────────────────╢ │ ║ "MS-DOS 3.x" ║ │ ╟───────────────────╢ ───┘ ║ OS/2 ║ Fixed ║ ║ 0╚═══════════════════╝ Figure 5: OS/2 maps multiple virtual I/O to the physical screen and keyboard. Screen Groups ┌─────────┐ ┌─────────┐ ┌─────────┐ │ │ │▓▓▓ ┌──┐ │ │ │ │ C> │ │▓☻▓ │/\│ │ │ ? │ │ │ │ └──┘ │ │ │ └─────────┘ └─────────┘ └─────────┘ Character User Presentation Manager Special Purpose Interface Graphics Desktop Publishing │ Light WP │ │ Database ┌─────────────▼────────────────┐ │ Spreadsheet ──►│ Visual Console I/O Management│◄───────┘ └──────────────────────────────┘ ╔════════════════╗ ┌────────────────┐ ║ ░░░░░░░░░░░░░░ ║█ │ Physical Video │ ║ ░░░░░░░░░░░░░░ ║█ │ Manager │ ║ ░░░░░░░░░░░░░░ ║█ └────┬───────────┘ ║ ░░░░░░░░░░░░░░ ║█───────┘ ╔═╝────────────────╚═╗ ║ ‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼ ║█ ╚════════════════════╝█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Figure 6: FAPI assures the software developer that an application will run under both MS-DOS 3.x and MS OS/2. 16M ┌──────────────────────────┐ │ ┌────────────┐ │ │ │ OS/2 │ │ │ │ ┌──────────┴───┐ │ │ │ │ OS/2 │ │ │ │ │ ┌───────────┴────┐ │ │ │ │ │ OS/2 │ │ │ └─┤ │ Protect │ │ │ │ │ Mode │ │ │ └──┤ Full New │ │ │ │ API │ │ │ └────────────────┘ │ MS-DOS 3.n │ ┌────────────────┐ │ ┌──────────────────────┐ │ │ "Family API" │ │ │ Reserved │◄───┼────►│ Applications │ │ ├──────────────────────┤ 1M├ ─ ─ └────────────────┘ ─ ┤ │ DOS 3.n │ │ Reserved │ │ Compatibility │640K├ ─ ─ ┌────────────────┐ ─ ┤ │ Environment │────┼────►│ DOS 3.n │ │ └──────────────────────┘ │ │ Compatibility │ │ │ │ Environment │ │ │ └────────────────┘ │ 0 └──────────────────────────┘ ████████████████████████████████████████████████████████████████████████████ The OS/2 Windows Presentation Manager: Microsoft Windows On the Future Manny Vellon Last April, Microsoft announced a series of new products, including a new operating system, Microsoft Operating System/2 (OS/2). An important part of this announcement was the inclusion of Microsoft Windows as a standard part of the OS/2 operating system. Currently, Microsoft Windows must be purchased separately and installed under MS-DOS. The Windows presentation manager will be tightly integrated with OS/2 and will provide the same benefits that Microsoft Windows provides to MS-DOS: a windowed, graphical user interface and support for a variety of input and output devices. Through the presentation manager, OS/2 will replace the well-known A> prompt of MS-DOS with window-based screens. This union of Windows with OS/2 strengthens the role of Windows in future system and application products and also addresses the ease-of-use issues that have been associated with MS-DOS and IBM PC software. Windows provides a more intuitive interface that allows novice users to learn products more quickly. Also important is IBM's support of Microsoft Windows in its new products. This support ensures that Windows will become a standard part of the operating environment and encourages other hardware manufacturers to support it as well. Protected Mode The new IBM PS/2 series, the IBM PC AT, the Compaq 386, and other Intel 286- and 386-based computers are capable of running in real mode or protected mode. In real mode, they operate much like the Intel 8088-based IBM PC: they are limited to using 1 megabyte (Mb) of memory, they can usually run only one program at a time, and they are vulnerable to crashing when a program goes awry. In protected mode, the 286- and 386- based computers don't have those limits. Under OS/2's protected-mode operating system, programs are no longer limited to 640K; applications can take advantage of up to 16 Mb of real memory and up to 1 gigabyte of virtual memory. OS/2 is able to perform multitasking──it can run several programs simultaneously by quickly switching among them. OS/2 is more robust than real-mode operating systems; programs run independently of each other, and if one crashes it is less likely to affect the others. Currently, Microsoft Windows can also do many of these functions. By performing a variety of sophisticated functions, Windows can take advantage of extended memory if available and can provide rudimentary multitasking. With the OS/2 Windows presentation manager, however, these functions are much easier to implement since the underlying operating system kernel includes these capabilities. User Interface In addition to this symbiosis "under the hood," OS/2 Windows is integral to the operating system at the user level. The Windows presentation manager will be the standard user interface for the new operating system. Users won't have to learn about disk directories, filenames, and cryptic commands; executing programs and managing the OS/2 file system with the OS/2 Windows presentation manager will be intuitive and fast. Although the new OS/2 Windows bears a strong resemblance to today's Microsoft Windows, there are some substantial differences between the two. Most significant are the differences in user interfaces. To eliminate these differences so as to produce a common user interface, Microsoft has announced a new version of Microsoft Windows──Version 2.0 (see Figure 2). This version, while still a real-mode version for MS-DOS, employs the same user interface to be used by the OS/2 Windows presentation manager. When Microsoft Windows was first developed, it used tiled windows (see Figure 1☼). Overlapped windows were considered too slow and unusable with low-resolution displays. In order to respond to customer requests for overlapping windows, and because optimized graphics algorithms and improved processing speeds have eliminated performance bottlenecks that are found in older technology, the new Windows uses overlapped windows instead of tiled windows. New Windows products will also have improved keyboard interfaces. Although Windows is best used with a mouse, it is possible, and sometimes preferable, to use it solely through the keyboard. Changes in the user interface will make this easier. First, the keyboard interface has been improved in order to allow direct access to items in dialog boxes. In Figure 4☼, for example, the user can type Alt-F to quickly position the cursor in the filename field of the dialog box. Access to menus from the keyboard has also been enhanced by allowing the developer to select any letter from a menu command to execute the item rather than being limited to the first letter, as in the case of Windows Version 1.03 and Version 1.04. This helps to make it easier for software developers to provide meaningful command names while still providing fast keyboard access to commands. The second significant change affects mouse operation. Currently, Windows employs pull-down menus. You click down on the menu bar to make visible a pop-up menu; then, while holding down the mouse button, you drag down to the desired command in the pop-up menu and let go of the button. This technique is fast, but it is prone to accidental selections and requires considerable manual dexterity. The new interface allows you to click and let go on the menu bar to make the pop-up menu visible and then click down on the desired command within the given menu. Finally, the new interface employs some new terminology (for example, minimize and maximize, instead of icon and zoom) and some additional keyboard operations. Users who are accustomed to the old keyboard interface won't be forced to learn the new one. Microsoft Windows, Version 2.0, will work in both the old and new styles to help users get accustomed to the new interface. However, the OS/2 Windows presentation manager will use this new interface only. ─────────────────────────────────────────────────────────────────────────── Progression for Windows-based Applications Windows 1.03 ▼ Optional Changes • Edit Resource Files • Enhanced Printer Interfaces • Implement Mult Doc Facil • Error Condition Support ▼ Windows 2.0 • Overlapped Applications • EMS/EEMS Support • New User Interface • Display Performance • Multiple Document Facilities ▼ • Mechanical API Changes • Mode Rules • GDI-to-GPI • Access to OS/2 Facilities • Conform to Protected ▼ OS/2 Windows • Overlapped Applications • Preemptive Multitasking • New User Interface • Large Memory (16Mb) • GPI and New Device Drivers ─────────────────────────────────────────────────────────────────────────── API Although Microsoft Windows, Version 2.0, and the OS/2 Windows presentation manager appear to be similar and have many common functions, programs written for Microsoft Windows will have to be modified to work with the OS/2 Windows presentation manager. OS/2 restricts several operations in programs; for example, software interrupts are not allowed. Programs written for MS-DOS will also have to be modified to work in OS/2. Besides the changes required by the new operating system, such as replacing software interrupts with operating system calls, other changes have been made to the OS/2 Windows Application Program Interface (API) in order to standardize coding practices, improve error handling, and exploit new graphics capabilities. Fortunately, many of these changes are "mechanical" in nature. To follow the new coding practices, for example, all window manager calls are preceded with "Win": "WinCreateWindow", "WinShowWindow", etc. Other changes entail reordering parameters. Applications that make heavy use of graphics will require the most changes; the OS/2 Windows presentation manager contains a new graphics library that requires a variety of changes to be made to the API. The Future OS/2 is not intended to replace MS-DOS. Microsoft expects that real-mode (MS-DOS) and protected-mode (OS/2) products will continue to co-exist until Intel 286- and 386-based computers predominate. MS-DOS and Microsoft Windows will continue to be developed and marketed for low-end machines, while OS/2 addresses newer, more-powerful computers. The Windows user interface will serve as a bridge for users, allowing them to operate each of these classes of machines regardless of the underlying hardware. In time, Microsoft expects Intel 386-based machines to become the standard hardware environment for powerful new business and engineering applications. Microsoft will extend OS/2 to exploit the additional capabilities of the Intel 386 and will upgrade OS/2 in the future. Once more, the Windows presentation manager interface will help to keep these changes invisible to the user. Figure 2: Comparison of Current and Future Windows Versions ╓┌───────────────────────────────┌────────────────┌──────────────┌───────────╖ Microsoft Windows Microsoft MS-OS/2 1.03 & 1.04 Windows 2.0 Windows Presentation Spaces Tiled Overlapped Overlapped More Consistent User --- Yes Yes and Keyboard Interface Processor Environments 8088, 8086, 8088, 8086, 286, 386 286, 386 286, 386 Large Memory Support --- EMS/EEMS 16Mb Multitasking Non Non Fully Preemptive Preemptive Preemptive Enhanced Display Performance --- Yes Yes Microsoft Windows Microsoft MS-OS/2 1.03 & 1.04 Windows 2.0 Windows Enhanced Display Performance --- Yes Yes Runs Existing Windows (1.03) Yes Yes Yes Application Graphics API GDI GDI GDI Multiple Document Interface --- Yes Yes Device Drivers --- Enhanced New Model Old Application Support --- Improved Improved Integral Part of OS --- --- Yes Protected Mode Execution --- --- Yes Applications Execution Microsoft Windows Microsoft MS-OS/2 1.03 & 1.04 Windows 2.0 Windows New Development API --- --- Yes New User "Shell" --- --- Yes and Keyboard Interface Figure 3a: Microsoft Windows API. Programs written for MS-DOS will have to be modified to work in OS/2. Many of these changes are mechanical in nature──for example, all window manager calls are preceeded with "Win". Compare the above Windows 2.0 code with that shown in Figure 3b below. ∙ ∙ ∙ int PASCAL WinMain( hInstance, hPrevInstance, lpszCmdLine, cmdShow ) HANDLE hInstance, hPrevInstance; LPSTR lpszCmdLine; int cmdShow; { MSG msg; HWND hWnd; NPWNDCLASS pHelloClass; ∙ ∙ ∙ /* Allocate and initialize class data structure pHelloClass */ ∙ ∙ ∙ /* Create a window class */ if (!RegisterClass( (LPWNDCLASS)pHelloClass ) ) return FALSE; /* Create an application window */ hWnd = CreateWindow((LPSTR)"Class", (LPSTR) "Title", WS_TILEDWINDOW, 0, 0, 0, 0, (HWND)NULL, (HMENU)NULL, (HANDLE)hInstance, (LPSTR)NULL ); ∙ ∙ ∙ /* Process messages*/ while (GetMessage((LPMSG)&msg, NULL, 0, 0)) { TranslateMessage((LPMSG)&msg); DispatchMessage((LPMSG)&msg); } return (int)msg.wParam; } CAPTION: OS/2 Windows Presentation Manager Program ∙ ∙ ∙ Figure 3b: OS/2 Windows presentation manager API (see figure 3a) int cdecl main(argc, argv) int argc; char *argv[]; { QMSG qmsg; HAB hab; HMQ hmq; HWND hwnd, hwndFrame; /* get an anchor block */ hab = WinInitialize(); /* create a message queue for the application */ hmq = WinCreateMsgQueue(hab, 0); /* create a window class */ if (!WinRegisterClass( (LPCH) "Class", WndProc, WS_SYNCPAINT | WS_CLIPSIBLINGS | WS_SIZEREDRAW, 0, NULL)) return FALSE; /* create an application window */ hwndFrame = WinCreateStdWindow(NULL, FS_MENU | FS_TITLEBAR | FS_MINMAX | FS_SIZEBORDER | FS_SYSMENU, (LPSTR) "Class", (LPSTR) "Title", 0L, NULL, IDM_APPMENU, (HWND far *)&hwnd); ∙ ∙ ∙ /* process messages */ while (WinGetMsg(hab, (LPQMSG)&qmsg, NULL, 0, 0)) WinDispatchMsg(hab, (LPQMSG)&qmsg); /* destroy the resources used by the application */ WinDestroyWindow(hwndFrame); WinDestroyMsgQueue(hmq); WinFinalize(hab); } ████████████████████████████████████████████████████████████████████████████ The OS/2 MS-DOS Environment:Compatibility and Transition for MS_DOS Programs ─────────────────────────────────────────────────────────────────────────── Also see the related article: Some Programming Don'ts for OS/2 ─────────────────────────────────────────────────────────────────────────── Joel Gillman Microsoft's multitasking, virtual memory operating system has finally been unveiled. Now that MS OS/2 is a reality, programmers may be wondering with some trepidation whether they'll have to rewrite all their MS-DOS programs. The good news is that nearly all of your existing code, both commercial products and custom-made utilities, will run just fine in OS/2's MS-DOS compatibility environment, an operating mode that emulates MS-DOS 3.3. Programs that need low-level access will not run in the compatibility mode, but these are exceptions. Most spreadsheets, word processors, and many other applications programs will work just fine. However, once you start writing code to run in the multitasking protected mode of OS/2, you will have to do some things differently. There's a silver lining in this, because you will end up with better code once you've unlearned some of the bad habits you picked up writing in MS-DOS. Multitasking implies resource sharing, so you can't be overly greedy with system resources anymore. But what you may lose in resource access you gain in the power of multitasking. After booting OS/2, the user is greeted by the session manager, from which the user can start any number of protected-mode applications or enter compatibility mode. For each protected-mode program, the session manager creates a new protected mode execution environment, called a screen group. Each screen group has a command processor (CMD.EXE, which corresponds to the MS-DOS 3.x COMMAND.COM), a virtual screen buffer, a virtual keyboard, and a virtual mouse, along with a virtual memory space of up to 16 megabytes (Mb) depending on the total number of screen groups. Compatibility Mode The MS-DOS compatibility mode in OS/2 uses the 80286 chip's real mode, which is engaged by a technique called mode switching (see "OS/2: Turning Off the Car to Change Gears,"). Mode switching emulates a full system reset without disrupting operation, allowing the processor to switch from protected to real mode. The compatibility mode gives the system an 8086 interface with 1 Mb of address space and emulates MS-DOS 3.x with the MS-DOS SHARE utility installed. OS/2 needs up to 500 Kb of system memory in a typical configuration──substantially more than the 50 Kb required by MS-DOS 3.x. The compatibility mode recognizes all of the documented MS-DOS services. A number of undocumented interrupt 21H services also exist under MS-DOS 3.x, but since OS/2 does not recognize most of the undocumented MS-DOS services, programs that use them won't run in the compatibility mode. The compatibility mode supports a ROM data area and accepts service interrupts, but you cannot call ROM services directly by address. You must use the interrupt 10 through interrupt 1A services instead (see Figure 1). Applications can call any hardware interrupt except the CMOS clock/calendar interrupt or an interrupt already taken over by any OS/2 device driver other than the keyboard. The compatibility mode will issue interrupt 28h (spooler interrupt)──so you can still run SideKick(R)──and interrupt 24h (critical error handler). A program running in the MS-DOS compatibility mode freezes up if the system places it in the background by switching to the protected mode. Programs frozen in the background receive no CPU service or interrupts. A program that determines the time of day by counting clock ticks, for example, will generate inaccurate times if it goes into the background. Version-specific applications won't run in the compatibility mode, because the MS-DOS version number of OS/2 is 10. A way around this is to modify the code so that it calls DOSGETVERSION and, if it gets back 10, determines which mode it's executing by calling DOSGETMACHINEMODE. The program can then take the appropriate action. Handling Devices Most of your old device drivers will not run in compatibility mode. OS/2 doesn't support any of the MS-DOS block device drivers, such as those used with disk or tape drives. The only character device drivers it supports are VDI (video display interface) and CON (console) drivers, since character device drivers will work only if they are polled rather than interrupt-driven. The system supports all of the device driver command packets shown in Figure 2. When you run a driver, only programs running in the compatibility mode can use its device. The device isn't available to protected-mode applications. Device drivers cannot call user code, because they operate at a higher privilege level than the user program. Drivers are installed the same way as in MS-DOS(R), using the configuration command device = driver filename MS-DOS device drivers are loaded and initialized in compatibility mode. Initialization is in most respects the same as in MS-DOS, except that no interrupt 21h (hardware-independent) functions can be performed from the initialization code. Compatibility mode restricts which devices programs can manipulate. Sound- generating programs that need a higher frequency time base for more precise pitch control can remap the 8253 clock/timer chip, that is, assign different numbers to its system interrupts. Remapping the 8259 interrupt controller is not allowed. Such programs, which must remap the 8259 in order to trap keystrokes, will not run. Applications can still hook keystrokes after OS/2 gets them, however. Programs that need low-level disk I/O access for copy protection purposes cannot remap the disk controller. Programs do have direct access to the disk controller via interrupt 13h (floppy disk services), interrupt 25h (absolute disk read), and interrupt 26h (absolute disk write). Note that interrupt 13h and interrupt 26h are not allowed for fixed media such as hard disks. High-speed communications applications that must remap the DMA controller won't run because the operating system remaps the controller. Applications can remap the COM, AUX, and parallel ports, although using one of these ports in the compatibility mode makes it unavailable to protected mode programs and vice versa. 80286 Restrictions Programmers writing applications for MS-DOS developed some programming techniques and coding shortcuts to enhance performance, even though the books and manuals tended to discourage using them. Many of these techniques won't work in OS/2 because of differences between the 8086 or 8088 chip and the 80286. You'll have to observe several restrictions if you want to run your applications in the compatibility environment or in protected mode. First, don't use wrap-around segment offsets. The 8086 and 8088 microprocessors translate an out-of-range address value into something recognizable, but the 80286 doesn't. You cannot address beyond the allocated size of a segment; the system aborts the program if an offset larger than the segment descriptor's limit value is used to reference that segment. The 80286 doesn't allow writable code segments. One of the bits in the segment descriptor identifies the segment as either code or data. A code segment's descriptor doesn't have a read/write bit, so only valid code segments can be placed in the CS (code segment selector) register, and a program may not write into valid code segments. However, an alias can be used to make a data segment into a code segment to be executed. Since different machines use different timing speeds, don't count on the CPU clock as a timing constant. This is a typical problem in copy- protected programs. Don't allow a division-trap handler to resume execution in the original code stream unless it is able to detect and understand differences between the 8086 or 8088 and the 80286. After a division-error trap, the 80286 points to the division instruction (including prefixes) and doesn't change the register values. The 8086 and 8088 point to the instruction following the division instruction and may change the DX:AX or AH:AL register sets as well. The 80286 CL (low-order loop/shift/repeat count) registers won't permit shift counts greater than 31. On the 80286, the shift and rotate counts are masked to 5 bits. When executing the PUSH SP (push stack pointer onto stack) instruction, the 80286 pushes the SP value onto the stack before incrementing the value, while the 8086 and 8088 push the SP value after incrementing it. Few programs use this particular code sequence, but for those that do, Microsoft offers this way around the problem: MOV AX,SP PUSH AX these two lines of code can be implemented by a macro. The PUSHF instruction followed by a POPF may change the contents of the flag register in the 80286, since more flag bits are defined in the 80286 flag word than in those of the 8086 or 8088. Also, because of a bug in the 80286, the POPF may change the contents of the flag register in the 80286. You should use flag-specific instructions to set or test flag register values instead of setting or testing for explicit bit patterns. FAPI In order to permit you to write new programs guaranteed to run in MS-DOS and in OS/2's protected mode (both current and future versions), Microsoft has defined a set of system calls that are guaranteed to support both environments. This set of system calls, the Family Application Program Interface (FAPI), is a subset of the full OS/2 Application Program Interface (API). Five types of code will run on OS/2: old programs designed for MS-DOS 3.x that run on OS/2 in the compatibility mode; presentation manager programs, FAPI programs that run on OS/2 in the compatibility mode; FAPI programs that run on OS/2 in protected mode; and new programs that run on OS/2 only in the protected mode. Figure 3 summarizes the characteristics valid for each of these program types. FAPI allows a program to be linked to run in both modes and includes system calls in all categories except those specific to protected mode, such as multitasking, run-time linking, and device monitors. Some restrictions apply to using the FAPI calls in the compatibility mode, which are discussed in detail in the OS/2 Programmer's Guide. The protected mode does not give you direct access to the screen, as you would have with a MS-DOS 3.x BIOS call because of its memory protection. Instead, you have access to a virtual screen buffer within each screen group. The screen buffer paints the screen only when that screen group is active. For bimodal compatibility, FAPI provides a subset of the video input/output (VIO) calls for creating and writing to a virtual screen buffer. Just using FAPI calls, however, won't guarantee protected mode compatibility. Basically, you want a well-behaved program; it shouldn't sneak past the operating system to the hardware or get too clever with the segment registers. To write code that operates in protected mode, you give up sovereignty over the hardware. Multitasking requires that the operating system, rather than your program, allocate hardware and system resources. If you've done any programming in XENIX or UNIX, you're used to this. However, if you've only worked in MS-DOS, you'll want to adjust your thinking a little. OS/2 uses an indirect segment addressing scheme: a segment number points to a table entry, called a segment decriptor, which in turn points to the memory space. OS/2's memory management service will change the memory pointer in the descriptor as the total system memory allocation changes, so there is no way of knowing where a given segment number actually puts you in physical memory. Thus, a well-behaved program doesn't try to interpret segment numbers or calculate other segment locations from a given segment number. You can't assume that any given segments overlap or don't overlap, nor can you assume any particular relationship between segment:offset combinations and physical memory. The segment number is nothing more than a segment ID, with no particular significance apart from that. The segment registers are intended for the storage of valid segment numbers. If you store invalid numbers there──for example, by using the segment registers as scratch-pad memory (the 8086 doesn't seem to have enough registers to suit some people)──your program will crash. Your program cannot issue a CLI (clear interrupt) instruction in protected mode because this causes a protection trap. When in compatibility mode, IRET (interrupt return) restores the previous value of IFLG (interrupt flag), but IRET has no effect in protected mode. Protected mode programs can interact directly with hardware only by linking to a special I/O Privilege Level Segment. This allows access to the 80286 processor's Ring 2 security level. Bimodal Device Drivers OS/2 supports bimodal device drivers that run in either mode, obviating the need for the system to switch modes to process interrupts. Unlike a MS-DOS 3.x device driver, a bimodal OS/2 driver has to support multiple synchronous and asynchronous requests. However, the basic structure remains pretty much the same: the driver contains a strategy routine and an interrupt routine. In addition, some device drivers may have to include a routine to trap ROM BIOS software interrupts from compatibility mode. An application program uses a request packet to call the strategy routine, just as with a MS-DOS 3.x driver. The strategy routine determines whether a request is valid and places valid requests in a queue to the device, using the DevHlp functions to manage the queue. If the device is idle, the strategy routine starts it and returns to the operating system, which suspends the thread until the request has been executed. When the device completion interrupt occurs, the interrupt routine sets the return status in the request packet and removes that packet from the queue. It then calls a DevHlp routine, DevDone, to tell OS/2 that the request is complete. The strategy routine should disable interrupts when checking to see if the device is active and when examining the queue. This protects the interrupt routine from other driver request interrupts. When interrupts are reenabled, the interrupt routine will receive only ones of higher priority. This is only a sample of some design considerations in writing OS/2 bimodal device drivers. The subject is treated at greater length in the OS/2 Device Driver Guide. New Tools A new C compiler that runs in both modes is included in the OS/2 Software Development Kit. However, executable files written in Microsoft C Compiler, Version 4.0, should have no trouble running in the compatibility mode, as long as they meet the general criteria outlined earlier for operation in OS/2. You don't need to recompile or relink them with the new OS/2 C run-time library. The new C library is nearly identical to the Version 4.0 library, which is designed for single-thread execution only. Most of the functions are not re-entrant and therefore will not work in a multithread process. Figure 4 lists the re-entrant functions that may be used in multithread programs. All the routines in Figure 4 will work properly in programs that use a far data model (compact and large model). However, in near-data-model-memory (small- and medium-model) programs, only some of these routines are guaranteed to function properly. The others have model-dependent pointers in their interfaces and have the potential to allocate stack space outside the default segment (SS!=DS). The kit also contains a new macro assembler that will run in protected mode. Again, existing MASM executable files will run in the compatibility mode, subject to the general restrictions described earlier. The MS-DOS 3.2 network function calls aren't supported in the compatibility mode, but the new Microsoft OS/2 LAN Manager will allow networking in protected mode. Device Monitors One problem that has plagued MS-DOS programmers is that when several terminate-and-stay-resident (TSR) programs are loaded into memory, they tend to step on each other in order to catch a reactivate keystroke from the keyboard. When you put a TSR like Borland's SideKick, Rosesoft's ProKey, or North Edge Software's TimeSlips into the background, the program calls interrupt 21h function 31h (Terminate and Stay Resident) and remains in memory even though you return to the system prompt. The program is still watching the keyboard, trapping keystrokes before the system gets them. When you hit a hot key combination, such as Ctrl-Alt, the program pops back onto the screen. The problem is that when two or more pop-up programs reside in the background at the same time, each wants to trap the keyboard interrupt before any other program does (see "Moving Toward an Industry Standard for Developing TSRs," MSJ, Vol. 1, No. 2). OS/2 solves this problem in protected mode by giving each TSR its own keyboard device monitor for reading keystrokes. Suppose you want to have several TSRs written by different vendors available within a given screen group. Each one will have its own keyboard device monitor. The monitors receive keystrokes in the order of monitor registration, which is set when the programs are first run. Keystrokes are passed on to the first monitor registered, which can trap the keystroke and generate a response or pass the keystroke on the next monitor registered (see Figure 5). Obviously, in this scheme, no two TSRs can use the same reactivate-key sequence. You can't reactivate a pop-up program in one screen group from a different screen group. By definition, a screen group is made up of a virtual screen, virtual mouse, virtual keyboard, and virtual memory space, and screen groups can't talk to each other. The Tradeoff The constraints that OS/2 imposes on the programmer may seem at first rather strict. With MS-DOS, you are pretty much free to use the system services or to leave them alone and go directly to the CPU instead. But in a multitasking system, you just can't do that. OS/2 takes over the CPU and the hardware, granting access only in certain instances. On the other hand, a lot of old problems go away. Applications will no longer have to fight each other for system resources, because OS/2 allocates resources among them on a priority basis. Applications will have a common program interface (API) to work with, ensuring future compatibility. What you lose in resource access you gain in the new opportunities of multitasking, such as the potential for real-time multithread applications. Similarly, the memory access restrictions are compensated for in protected mode by the greatly increased size of the memory space that you can work with and the addition of memory management capabilities. The only limitation is that you must use system calls for access rather than stuffing numbers directly into the segment registers. And the 80286 chip is fast enough that efficiency issues aren't as critical as they were on the original PCs and XTs, so programming shortcuts such as segment games just aren't necessary anymore. Figure 1: Hardware/Operating Environment Compatibility Chart. Hardware compatibility and support is different for DOS 3.3 and OS/2. OS/2 seeks to insulate applications software from the hardware. ┌──────────OS/2─────────┐ DOS 3.3 Compatibility New Environment Programs Supported Hardware 8088 -- -- 8086 -- -- 286 286 386 386 Available Memory 640K 840K 16MB Can overcommit Memory -- -- YES True Multitasking -- -- YES Use Software Interrupts YES YES NO Use Hardware Interrupts YES YES NO Use undocumented DOS YES Some NO Interfaces Have direct access to Hardware YES YES YES Can run in the Background NO NO YES Obey 286 Segment Rules NO NO YES Figure 2: Device Driver Commands. Shown here are the device driver commands supported under OS/2. Code Command 0 Init 3 IOCtl Input 4 Input (Read) 5 Non-Destructive Input No Wait 6 Input Status 7 Input Flush 8 Output 9 Output with Verify 10 Output Status 11 Output Flush 12 IOCtl Output (Write) 13 Device Open 14 Device Close 15 Generic IOCtl Figure 3: Software Compatibility Chart. Programs may run in one or more modes depending on how they behave. ╓┌──────────────────────┌─────────────┌─────────────┌────────────┌───────────╖ ┌─ DOS ──┐ ┌────────────────── OS/2 ───────────┐ Old Programs ┌─────FAPI Programs─────┐ New Programs Start With COMMAND.COM COMMAND.COM COMMAND.EXE CMD.EXE Can Run in OS/2 Compatibility Box Yes Yes No No Can Run in Background No No Yes Yes ┌─ DOS ──┐ ┌────────────────── OS/2 ───────────┐ Old Programs ┌─────FAPI Programs─────┐ New Programs Permit Old-Style INT 21H Dos 3.x Interrupts Yes No No No Permit Undocumented Dos Interfaces No No No No Have IOPL Yes Via FAPI Via FAPI Via OS/2 Obey 286 Segment Rules No Yes Yes Yes Can Overcommit Memory No No Yes Yes Addressable Memory 640K 640K 16M 16M Permit Software ┌─ DOS ──┐ ┌────────────────── OS/2 ───────────┐ Old Programs ┌─────FAPI Programs─────┐ New Programs Permit Software Interrupts Yes Via FAPI No No Permit Hardware Interrupts Yes No No No Program Residence Below Below Above Above Boundary Boundary Boundary Boundary Permit Multitasking No Yes Yes Yes Figure 4: OS/2 API Calls Family API (FAPI) functions are highlighted * Indicates that FAPI support is limited (certain restrictions are imposed) ╓┌─────────────────────┌─────────────────────────────────────────────────────╖ API Function Name Description BadDynLink Bad Dynamic Link DosAllocHuge* Allocate Huge Memory DosAllocShrSeg Allocate Shared Segment DosBeep Generate Sound From Speaker DosBufReset Commit File Cache Buffers DosCaseMap* Perform Case Mapping on String of Binary Characters DosChdir Change Current Directory DosChgFilePtr Change File Read/ Write Pointer DosClose Close File Handle DosCloseQueue Close Queue DosCloseSem Close System Semaphore DosCreateCSAlias Create CS Alias DosCreateSem Create System Semaphore DosCreateThread Create another thread of execution DosCreateQueue Create Queue DosCWait* Wait for child termination DosDelete Delete File API Function Name Description DosDelete Delete File DosDevConfig Get Device Configuration DosDevIOCtl* I/O Control for Devices DosDupHandle Duplicate File Handle DosEnterCritSec Enter Critical Section of Execution DosError* Enable Hard Error Processing DosExecPgm* Execute Program DosExit* Exit Program DosExitCritSec Exit Critical Section of Execution DosExitList Routine List for Process Termination DosFileLocks* File Lock Manager DosFindClose* Close Find Handle DosFindFirst* Find First Matching File DosFindNext* Find Next Matching File DosFlagProcess Set Process External Event Flag DosFreeModule Free Dynamic-Link Module DosFreeSeg Free Segment DosGetCtryInfo* Get Country-Dependent Formatting Information DosGetDateTime Get Current Date and Time DosGetDBCSev Get DBCS Environmental Vector API Function Name Description DosGetDBCSev Get DBCS Environmental Vector DosGetEnv Get Address of Process DosGetHugeShift Get Shift Count DosGetInfoSeg Get Address of System VariablesSegment DosGetMachineMode Return Current Mode of Processor DosGetMessage Get System Message and Insert Text Strings DosGetModHandle Get Dynamic-Link Module Handle DosGetModName Get Dynamic-Link Module Name DosGetProcAddr Get Dynamic-Link Procedure Address DosGetPrtyGet Process Priority DosGetShrSeg Access Shared Segment DosGetVersion Get Dos Version Number DosGiveSeg Give Access to Segment DosHoldSignal* Disable/Enable Signal DosInsMessage Insert Variable Text Strings In Message DosIOAccess Request I/O Access to Device DosKillProcess Terminate Process DosLoadModule Load Dynamic-Link Module DosMakePipe Create Pipe DosMkdir Make Subdirectory API Function Name Description DosMkdir Make Subdirectory DosMonClose Close Connection to OS/2 Device Driver DosMonOpen Open Connection to OS/2 Device Driver DosMonRead Read Input from Monitor Structure DosMonReg Register Set of Buffers as Monitor DosMonWrite Write Output to Monitor Structure DosMove Move File or Subdirectory DosMuxSemWait Wait for one of n semaphores to be cleared DosNewSize Change File Size DosOpen* Open File DosOpenQueue Open Queue DosOpenSem Open Exisiting Semaphore DosPeekQueue Peek Queue DosPurgeQueue Purge Queue DosPutMessage Output Message Text To Handle DosQCurDir Query Current Directory DosQCurDisk Query Current Disk DosQFHandState Query File Handle State DosQFileInfo* Query File Information DosQFileMode Query File Mode API Function Name Description DosQFileMode Query File Mode DosQFSInfo Query File System Information DosQHandType Query Handle Type DosQueryQueue Query Queue DosQVerify Query Verify Setting DosRead Read from File DosReadAsync Asynchronous Read from File DosReadQueue Read from Queue DosReAllocHuge* Change Huge Memory Size DosReAllocateSeg* Change segment Size DosResumeThread Restart Thread DosRmdir Remove Subdirectory DosSelectDisk Select Default Drive DosSemClear Clear (release) Semaphore DosSemRequest Request Semaphore DosSemSet Set Semaphore Owned DosSemSetWait Set Semaphore and wait for next Clear DosSemWait Wait for Semaphore to be Cleared DosSetDateTime Set Current Date and Time DosSetFHandState* Set File Handle State API Function Name Description DosSetFHandState* Set File Handle State DosSetFileInfo Set File Information DosSetFileMode Set File Mode DosSetMaxFH Set Maximum File Handles DosSetPrty Set Process Priority DosSetSigHandler* Handle Signal DosSetVec Establish Handler For Exception Vector DosSetVerify Set/Reset Verify Switch Delay ProcessExecution DosSubAlloc Suballocate Memory within Segment DosSubFree Free Memory Suballocated within Space DosSubSet Initialize or Set Allocated Memory DosSuspendThread Suspend Thread Execution DosSystemService Dos System Process Services DosTimerAsync Start Asynchronous Timer DosTimerStart Start Periodic Interval Timer DosTimerStop Stop Asynchronous or Interval Timer DosPTrace Interface for Program Debugging DosWrite Synchronous Write to File DosWriteAsync Asynchronous Write to File DosWriteQueue Write to Queue API Function Name Description DosWriteQueue Write to Queue KbdCharIn Read Character Scan Code KbdFlushBuffer Flush Keyboard Buffer KbdGetStatus Get Keyboard Status KbdPeek* Peek at Character-Scan Code KbdRegister Register keyboard Subsystem KbdSetStatus Set Keyboard Status KbdStringIn Read Character String MouClose Close Mouse Device For Current Screen GroupMouDrawPtr Release Screen Area For Device Driver Use MouGetDevStatus Get Current Pointing Device Driver Status Flags MouGetEventMask Get Current Pointing Device One- Word Event Mask MouGetNumButtons Get Number of Buttons MouGetNumMickeys Get Number of Mickeys-Per-Centimeter MouGetNumQueel Get Current Status for Pointing Device Event Queue MouGetScaleFact Sets Scale Factors for Current Pointing Device MouOpen Open Mouse Device For Current Screen Group MouReadEventQue Read Pointing Device Event Queue MouRegister Register Mouse Subsystem MouRemovePtr Reserve Screen Area For Application Use API Function Name Description MouRemovePtr Reserve Screen Area For Application Use MouSetEventMask Assign New Event Mask To Current Pointing Device MouSetHotKey Set System Hot Key MouSetPtrShape Set Pointer Shape and Size MouSetScaleFact Set Scale Factors for Current Positioning Device VioEndPopUp Deallocate a Pop-up Display Screen VioGetAnsi Get ANSI State VioGetBuf Get Logical Video Buffer VioGetCurPos Get Cursor Position VioGetCurType Get Cursor Type VioGetPhysBuf Get Physical Video Buffer VioPopUp Allocate Pop-up DisplayScreen VioPrtScreen Print Screen VioReadCellStr Read Character-Attribute String VioReadCharStr Read Character String VioRegister Register Video Subsystem VioSavReDrawWait Screen Save RedrawWait VioScrLock* Lock Screen VioScrollDn Scroll Screen Down VioScrollLf Scroll Screen Left API Function Name Description VioScrollLf Scroll Screen Left VioScrollRt Scroll Screen Right VioScrollUp Scroll Screen Up VioScrUnLock Unlock Screen VioSetAnsi Set ANSI On or Off VioSetCurPos Set Cursor Position VioSetCurType Set Cursor Type VioSetMode Set Display Mode VioShowBuf Display Logical Buffer VioWrtCellStr Write Character-Attribute String VioWrtCharStr Write Character String VioWrtCharStrAttr Write Character String With Attribute VioWrtNAttr Write N Attributes VioWrtNCell Write N Character-Attributes VioWrtNChar Write N Characters VioWrtTty Write TTY String Figure 5: When TSR programs are run in protected mode, OS/2 assigns each a keyboard device monitor, which reads keystrokes from the keyboard. This keeps the TSRs from fighting each other to be the first to trap keystrokes. ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ╔═══════════╗ ║ 1 ║ ║ 2 ║ ║ 3 ║ ║ 4 ║ ║ 5 ║ ║ ║ ║ Pop-Up Applications ║ ║ ║ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ ╚═══════════╝ ┌─────╨─────┐ ┌─────╨─────┐ ┌─────╨─────┐ ┌─────╨─────┐ ┌─────╨─────┐ │ 1 │ │ 2 │ │ 3 │ │ 4 │ │ 5 │ │ │ │ Keyboard Device Monitors │ │ │ └────────╥──┘ └────────╥──┘ └────────╥──┘ └────────╥──┘ └───────────┘ ║ ║ ║ ║ ┌────────║────────║─────║─────────║────║─────────║────║────────║────────┐ │ ╚════════╝ ╚═════════╝ ╚═════════╝ ╚════════╝ │ ├ ─ ─ ─ ─ ┐ │ │Interrupt│ Keystroke Distribution │ └────────┴────────────────────────────────────────────────────────╥────┘ ╚═══════╗ ╔═══════╝ ┌───────╨─────────────────────────────────────────────▼───────┐ │ Keyboard Device Driver │ └────────────────────────────────────────────────────────────┘ ║ ─────────────────────────────────────────────────────────────────────────── Some Programming Don'ts for OS/2 ─────────────────────────────────────────────────────────────────────────── When writing programs for either the compatibility mode or protected mode in OS/2, you must observe a number of restraints that weren't necessary in writing for MS-DOS. Many of these restrictions arise from differences between the 8086 or 8088 and the 80286 microprocessors. ■ Don't depend on segment overlap or lack of it. ■ Don't depend on any relationship between segment:offset combinations and physical memory. ■ Don't use wrap-around address offsets. ■ Don't use the segment registers for anything but valid segment numbers. ■ Never address beyond the allocated size of a segment. ■ If you have to play with the I/O ports, use only the appropriate dynamic link routines (FAPI). ■ Don't mix code and data in the same segment. ■ Don't use undefined opcodes. ■ Don't use the PUSH SP instruction. ■ Don't use the POPF instruction. ■ Don't use shift counts greater than 31. ■ Don't use IDIV operands to produce a most-negative number. ■ Don't resume execution in the original code stream after a division trap. ■ Don't use redundant prefix bytes. ■ Don't use CLI instructions (in protected mode). ■ Don't use CPU speed as a timing constant. ■ Don't examine or set explicit flag register values. ■ Don't single-step interrupt instructions in debuggers. ■ Don't write self-modifying code. ████████████████████████████████████████████████████████████████████████████ OS/2 Multitasking: Exploiting the Protected Mode of the 80286 ─────────────────────────────────────────────────────────────────────────── Also see the related article: Configuring the OS/2 Multitasker ─────────────────────────────────────────────────────────────────────────── Ray Duncan☼ Multitasking is the technique of dividing CPU time between multiple tasks so that they appear to be running simultaneously. Of course, the microprocessor is only executing one sequence of machine instructions within one task at any given time, but the switching from one task to another is invisible to both the user and the programs themselves. The operating system is responsible for allocating system resources such as memory to the various executing tasks and for resolving contending requests to such peripherals as video displays and disk drives. The part of the operating system that allocates CPU time between tasks is called the scheduler or dispatcher, and the rotation from one task to another, or from a task to a module of the operating system, is called a context switch. When a context switch occurs, the dispatcher must save the current state of the task that was executing, including its registers and program counter, load the registers and program counter belonging to the next task to run, and transfer control to that task at the point where it was previously suspended. There are two basic strategies for task scheduling that are used by modern multitasking operating systems: event-driven and pre-emptive. Event-driven schedulers rely on each task to be "well-behaved" and yield control at frequent enough intervals so that every program has acceptable throughput and none will be "starved" for CPU cycles. This yielding may be explicit (the program calls a specific operating system function to give up control) or implicit (the program is suspended when it requests the operating system to perform I/O on its behalf and regains control only after the I/O is completed and other tasks have in turn yielded control). This strategy is quite efficient in transaction-oriented systems where there is a great deal of I/O and not much computation, but the system can be brought to its knees by a single compute-bound task, such as an in- memory sort. A pre-emptive scheduler relies on the presence of an external signal generated at regular intervals, typically a hardware interrupt triggered by a real-time clock. When the interrupt occurs, the operating system gains control from whatever task was executing, saves its context, evaluates the list of programs that are ready to run, and gives control to (dispatches) the next program. This approach to multitasking is often called "time-slicing"; this term is derived from the mental image of dividing the sweep of a second hand around a clock face into little wedges and doling out the pieces to all the eligible programs. Modern mainframe and minicomputers, and the more powerful microcomputers, such as the Intel 80286 and Motorola 68020, include hardware features specifically designed to make multitasking operating systems more efficient and robust. These include privilege levels and memory protection. In the simplest use of privilege levels, the CPU is either in kernel mode or user mode at any given time. In kernel mode, which is reserved for the operating system, any machine instruction can be executed, and any location in memory can be accessed. As part of the mechanism of transferring control from the operating system to an application program, the CPU is switched into user mode. In this CPU state, any attempt to execute certain reserved instructions, such as writing to an I/O port, causes a hardware interrupt and returns control to the operating system. (Although the Intel 80286 microprocessor actually supports four privilege levels, OS/2 ordinarily uses only the highest and lowest of these.) Similarly, the hardware memory protection mechanisms detect any attempt by an application program to access memory that does not belong to it, and generate a hardware interrupt that allows the operating system to abort the offending task. Microsoft OS/2 is designed around a pre-emptive, priority-based multitasking scheduler. Understanding OS/2 multitasking requires a grasp of three distinct, but related, concepts or system objects: processes, threads, and screen groups. ─────────────────────────────────────────────────────────────────────────── Three types of systems objects are involved in OS/2 multitasking: processes, threads, and screen groups. A process represents an application program accessing system resources such as files, memory, and inter-process communication facilities. A process can contain multiple concurrent points of execution called threads; each thread has its own priority and stack. Processes are collected into screen groups that read and write a virtual display and keyboard; the Session Manager is used to select a screen group. ┌──────────────────────────────────┐ │ │█ │ │█ │ │█ │ Physical Screen │█ │ │█ │ │█ │ │█ │ │█ └──────────────────────────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Session manager maps virtual screen for a group to the physical display. ┌ ─ ─ ─ ─ ─ ─ ─ ─ ┴──────────────────┐ │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ █ █ │ │█ │ │█ Virtual screen for █ Virtual screen for █ │ screen group 1 │█ │ screen group 2 │█ █ █ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘█ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ │ │ │ ┌───────┴───────┐ ┌──────┴───────────────┐ ┌───┴────┐ ┌──────┴────────┐ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ Thread Thread │ │ Thread Thread Thread │ │ Thread │ │ Thread Thread │ │ A1 A2 │ │ B1 B2 B3 │ │ C1 │ │ D1 D2 │ └───────────────┘ └──────────────────────┘ └────────┘ └───────────────┘ PROCESS A PROCESS B PROCESS C PROCESS D ─────────────────────────────────────────────────────────────────────────── Processes The simplest case of an OS/2 process is very similar to an application program loaded for execution under MS-DOS 2.x and 3.x. A process is started (whether by a shell or command processor or by another application) by a call to the OS/2 service DOSEXECPGM. OS/2 initiates a new process by allocating memory segments to hold that process's code, data, and stack, and then initializing the memory segments from the contents of the program's .EXE disk file. Once it is running, a process can obtain access to additional system resources such as files, pipes, semaphores, queues, and additional memory with various OS/2 function calls (see Figure 1). A process terminates itself with a call to the OS/2 function DOSEXIT and can also be aborted by its parent process, by an unrecoverable hardware error, or by a memory protection fault. In any case, OS/2 will automatically release all resources belonging to the process when it terminates. Ordinarily, processes are only aware of themselves, OS/2 services, and any child processes they start directly. They cannot directly access memory space or resources belonging to another process, including a child process, without the cooperation of that process. Threads The MS OS/2 scheduler, however, knows nothing about processes; it partitions the available CPU cycles among dispatchable entities known as threads. A thread has a point of execution, a priority, a stack pointer, and general register contents (see Figure 1). At any given time, a thread is either blocked (waiting for I/O or some other event), ready to execute, or actively executing (it has control of the CPU). Each process has a primary thread that receives control from OS/2 when the process is started; it begins executing at the program's designated entry point. However, that primary thread can start up additional threads within the same process. Multiple threads within a process execute asynchronously to one another, can have different priorities, and can manipulate one another's priorities. Although the threads within a process have separate stacks, they share the same near data segment (DGROUP) and thus the same local heap. Carefully designing code to use the stack for local variables allows procedures to be shared between threads──this happens naturally in C programs. Access to static variables or other data structures must be coordinated between threads through the use of RAM semaphores or similar mechanisms. The threads also share all the other system resources owned by the process as a whole──open files, system semaphores, queues, and pipes──but OS/2 generally provides automatic serialization of operations on these resources. When a thread within an application program is executing, the system is in user mode; the thread can only access the resources owned by the process that contains it and cannot execute certain machine instructions. A clock tick, another hardware interrupt, or a call by the application for an OS/2 function causes a transition back into kernel mode, so that OS/2 can service the interrupt or provide the requested operation. When OS/2 is ready to exit kernel mode, the scheduler receives control and examines its list of active threads. The thread with the highest priority that is ready to execute gains control of the machine. If the thread that was just interrupted is one of several eligible threads and has not used up its time-slice, it receives preference. If a thread becomes starved for CPU cycles because other threads with higher priorities are getting all the attention, OS/2 will temporarily bump that thread's priority to a higher value. Screen Groups Processes are in turn members of screen groups, which is what the average user perceives as being OS/2 multitasking. When the user presses the SysReq key, he exits from the current session, or screen group, to a menu displayed by the Session Manager. He can then select a command processor or other program already executing in another screen group or establish a new screen group by loading a new copy of the protected-mode command processor. OS/2 maintains a separate virtual screen for each screen group, which receives the output of all the processes in that group. The virtual screen is mapped to the physical display whenever that screen group is selected by the user with the Session Manager. New processes are added to a screen group by being "spawned" by the command processor or another process already executing within the group. By convention, only one of the processes within a screen group should be in the foreground at any given time, that is, writing to the screen and reading from the keyboard. The OS/2 DETACH command allows programs to be placed in the background from the command processor level. However, since OS/2 does not place any restrictions on access to the virtual display by the various processes within a screen group, DETACHing normal programs is not usually too useful since it just results in chaotic displays. Programs intended to be used as background tasks must be specially designed to use the OS/2 services for pop-up windows and keyboard monitors, so that they do not disrupt the displays or otherwise interfere with the proper operation of the foreground task within their group. A screen group, or session, is removed from the system by first terminating any active application programs within that group and returning to the command processor prompt. Then, entry of the EXIT command terminates the command processor itself. The Session Manager regains control and displays a menu of the remaining screen groups. OS/2 Programming OS/2 offers a diverse set of services to application programs that allow the creation of complex and powerful multitasking applications (see Figure 2). These services include ■ starting and stopping child processes ■ obtaining the return code of a child process ■ starting, suspending, and destroying threads ■ altering the priorities of threads ■ inter-process communication (see "OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues,") OS/2 DOSEXECPGM is used by the parent process to load and execute the child process. This function is analogous to, but considerably more powerful than, the EXEC function (Int 21h Function 4Bh) that was available in MS-DOS 2.x and 3.x. The child process can, in turn, load other processes and so on until system limits on threads or open files are exhausted or the system runs out of swap space on the disk. The OS/2 DOSEXECPGM function is called with ■ the address of the filename of the child process to be executed ■ a flag controlling whether the child process is synchronous or asynchronous ■ the addresses of an argument string block and an environment block to be passed to the child process (Each of these blocks consists of a series of null-terminated (ASCIIZ) strings; the block is terminated by an extra null byte. The environment block corresponds exactly to the environment block you are familiar with in MS-DOS 2.x and 3.x, while the simplest case of the argument block is simply a copy of the command line that invoked a process.) ■ addresses of buffers to receive the process ID of the child and other information When a child process executes synchronously, execution of the thread in the parent process that made the DOSEXECPGM call is suspended until the child process terminates, either intentionally or owing to an error condition. When the thread in the parent resumes execution, it is supplied with the return code of the child and a termination code that indicates whether the child terminated normally or was aborted by the operating system. If the child process is asynchronous, all threads in the parent process continue to execute while the child is running. Any thread in the parent process can later use the DOSCWAIT call to resynchronize with the child process by suspending itself until the child terminates and then obtaining its return code. As an alternative, the parent can use the process ID of the child, which is supplied by OS/2 on return from the original DOSEXECPGM call, to unilaterally abort execution of the child process with the DOSKILLPROCESS call at any time. (A special usage of the asynchronous option of DOSEXECPGM, together with the DOSPTRACE function, allows the child program to be traced, inspected, and modified under the parent program's control. This combination of OS/2 services allows the creation of program debuggers that are compatible with 80286 memory protection.) The child process automatically inherits access to certain resources of the parent process. These resources include the handles for any open files (unless the parent process explicitly opened the files with the noninheritance flag), handles to any open pipes, handles to any open system semaphores (but not ownership of the semaphores), and a copy of the parent's environment block (unless the parent goes to the trouble of creating a new block and passes a pointer to it). Figure 3 is a demonstration of the DOSEXECPGM function──a synchronous execution of CHKDSK as a child process. The various options for asynchronous execution, coupled with several options that are available with the DOSCWAIT function and the DOSKILLPROCESS function, allow for very flexible execution-time relationships between parent and child processes. Managing Threads OS/2 has a rich repertoire of function calls for control of multiple threads of execution within a process. The use of multiple threads is particularly appropriate for cases in which a process must manage several I/O devices with vastly different I/O rates (for example, the keyboard, disk, and video display) and remain rapidly responsive to the needs of each. Multiple threads are also appropriate for cases in which a process needs to run in multiple instances, but the instances do not require separate data segments or resources, since multiple threads are started much more quickly than multiple processes and have less system overhead. As mentioned previously, each process has a single primary thread that is known to the OS/2 dispatcher when it is started up, and this thread's initial point of execution is the program's designated entry point. The primary thread can use the MS OS/2 function DOSCREATETHREAD to start up additional threads within the process, and those new threads can also start up threads and so on. Each thread is initially entered through a far call from OS/2 and can terminate through a far return or by issuing the OS/2 function call DOSEXIT. (A process is terminated when the sole remaining active thread in a process issues DOSEXIT, or when any thread issues DOSEXIT with a special parameter that indicates that all threads should be immediately terminated.) A thread can use the function DOSSLEEP to suspend itself for a programmed period of time, or it can block on a semaphore to await reactivation by another thread or process triggering that same semaphore. Alternatively, a thread can use the functions DOSSUSPENDTHREAD or DOSRESUMETHREAD to suspend or reactivate other threads within the same process without their cooperation. Similarly, a thread can use the functions DOSGETPRTY or DOSSETPRTY to inspect or modify the execution priority of itself or other threads in accordance with execution-time requirements. Because a thread can be unilaterally suspended by another thread without its knowledge or cooperation, the functions DOSENTERCRITSEC and DOSEXITCRITSEC are provided in order to protect a thread from interruption while it is executing a critical section of its code. Figure 4 demonstrates the use of DOSCREATETHREAD by one thread to start up another thread that emits ten beeps at one-second intervals and then terminates. Although this is a trivial use of multiple threads, it gives an inkling of the enormous power of this concept and the ease with which asynchronous processing can be incorporated into an OS/2 application. Summary OS/2 uses a time-slicing, pre-emptive, priority-based multitasking strategy. The Intel 80286 microprocessor's support for privilege levels and memory protection are fully exploited by OS/2 in order to run multiple concurrent tasks and to protect those tasks from damaging each other or the operating system. A process represents the execution of an application and the ownership of any resources──files, memory, etc.──associated with that execution. Processes can spawn other processes and can exert certain control over those child processes, but sharing of data between two processes is possible only with the cooperation of both processes. OS/2 has a wealth of facilities for inter-process communication, which are discussed in "OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues." The thread is the dispatchable element used by OS/2 to track execution cycles of the processor. Threads can start, stop, and influence the execution of other threads within the same process. Sharing of data between threads is natural and, in fact, difficult to avoid, since all threads in a process share access to the same memory, files, pipes, queues, and semaphores. In essence, the OS/2 loader knows about processes, but the OS/2 scheduler knows about threads. Figure 1: Thread- and Process-specific information maintained by OS/2. Per-Process Information Process ID (PID) Disk-swapping information Local Descriptor Table (LDT) pointer System resources owned or opened: Files Pipes Queues System Semaphores Device monitors Memory Child processes Per-Thread Information Thread ID Thread priority Thread state: blocked, ready to execute, active Time-slice Instruction pointer Processor state (registers and flags) Stack pointer Figure 2: OS/2 multitasking services at a glance. Process Control DOSEXECPGM Load and execute a child process DOSCWAIT Wait for child process to terminate DOSKILLPROCESS Unconditionally terminate another process. DOSPTRACE Inspect/modify/trace a child process Threads Controlling Threads DOSCREATETHREAD Create another execution thread within the sameprocess DOSSUSPENDTHREAD Suspend the execution of a thread DOSRESUMETHREAD Reactivate a thread DOSEXIT Terminate current thread or all threads in process Read/Alter Thread Priorities DOSGETPRTY Get the priority of specified thread DOSSETPRTY Set the priority of specified thread Inter-Thread Protection DOSENTERCRITSEC Disable other threads in same process DOSEXITCRITSEC Re-enable other threads in same process Figure 3: This sample code fragment demonstrates the use of the OS/2 system DOSEXECPGM to run CHKDSK as a synchronous child process ∙ ∙ ∙ push ds ; address of object buffer push offset DGROUP:ObjName push ObjNameLen ; length of object buffer push 0 ; execute synchronously push ds ; address of argument block push offset DGROUP;ArgBlk push 0 ; address of envir.block push 0 ; (OL=inherit parent's) push ds ; address to receive return push offset DGROUP:RetCode ; and termination codes push ds ; address of name of child push offset DGROUP :PgmName call DOSEXECPGM ; transfer to 286DOS or ax,ax ; was EXEC successful? jnz error ; jump if EXEC failed... ∙ ∙ ∙ ObjName db 64 dup (0) ; receives name of dynamic link ObjNameLen equ $-ObjName ; causing EXEC failre ArgBlk db 'chkdsk *.*',0 ; block of argument strings for child... db 0 ; extra null byte terminates block ; receives return codes from child... RetCode dw 0 ; termination code for child dw 0 ; result code from child's DOSEXIT PgmName db 'chkdsk.eve',0 ; name of child program to run Figure 4: An example of the use of multiple threads for asynchronus execution of tasks within a single process. The main line of execution allocates memory for a new stack and then starts up another thread called beeper. The new thread uses the OS/2 service DOSBEEP to emit ten short tones at one second intervals and then terminates. stksiz equ 1024 ; size of new thread's stack ∙ ∙ ∙ Selector dw ? ; selector from DOSALLOCSEG BeeperlD dw ? ; Thread ID for new thread ; named 'beeper' ∙ ∙ ∙ push stksiz ; size of new segment push ds ; address of variable push offset DGROUP:Selector ; to receive new selector push 0 ; non-shared segment call DOSALLOCSEG ; TRANSFER TO 286DOS or ax,ax jnz error ; jump if alloc failed push cs ; execution address of push offset_TEXT:Beeper ; new thread push ds ; address to receive new push offsetDGROUP;BeeperID ; Thread ID push Selector ; address of base of push stksiz ; new thread's Stack call DOSCREATETHREAD ; transfer to 286DOS or ax,ax jnz error ; jump if create failed ∙ ∙ ∙ beeper proc far ; entry point for thread mov cx,10 ; emit ten beeps... beep1: push 440 ; sound a 440 Hz tone push 100 ; for 100 milliseconds call DOSBEEP ; transfer to 286DOS push 0 ; now pause for one sec. push 1000 call DOSSLEEP ; transfer to 286DOS loop beep1 ; loop ten times ret beeper endp ∙ ∙ ∙ ─────────────────────────────────────────────────────────────────────────── Configuring the OS/2 Multitasker ─────────────────────────────────────────────────────────────────────────── There are four different directives that can be placed in the system CONFIG.SYS file to influence the operation of OS/2 multitasking. These are THREADS=n MAXWAIT=seconds PRIORITY=ABSOLUTE|DYNAMIC MAXWAIT=x[,y] The THREADS directive controls how many threads can be simultaneously created in the system. The parameter n may be 16-255, with a default of 16, which is sufficient for OS/2 and a few simple processes. The upper limit of 255 cannot be expanded. When a thread is denied the CPU for the number of seconds specified by MAXWAIT because of other higher-priority threads using up all the time- slices, the starved thread receives a temporary increase in its priority for one time-slice. The PRIORITY directive activates OS/2's mechanisms for dynamically altering the priority of each thread based on the activity of other threads within the system. When PRIORITY=ABSOLUTE, the MAXWAIT directive has no effect whatsoever. The TIMESLICE directive controls the length of the time-slices used by the OS/2 scheduler. The x parameter represents the normal length of a thread's time-slice in milliseconds. If a thread uses up its entire time-slice and must be pre-empted, its next time-slice will be one tick longer, up to the limit given by the y parameter. This strategy is used by OS/2 to reduce the amount of context-switching overhead when several compute bound threads are running. ████████████████████████████████████████████████████████████████████████████ OS/2 Memory Management In a multitasking system, random access memory (RAM) must be administered as a critical, finite system resource akin to disk storage or a line printer. The well-bred application program uses the host operating system's facilities to request additional memory politely when necessary, releases unneeded memory, and abstains from meddling with memory that doesn't belong to it. In the MS-DOS environment, such genteel programs are few and far between. Most popular application programs place the goal of high performance above all else, and a disposition to grab all available memory, capture interrupt vectors, and directly access the video controller's refresh buffer for fast screen displays is the rule rather than the exception. This behavior causes many headaches for those system software vendors who seek to graft multitasking abilities onto MS-DOS with such new user interfaces as Windows, TopView, and DESQview. The 80286 microprocessor, which is the heart of the IBM PC AT and compatible computers, can execute in either of two modes: real mode and protected mode. When the 80286 is running in real mode, as it does under MS-DOS, it essentially functions as a fast 8086/88 processor with a few added machine instructions. The segment registers contain physical addresses, which can be manipulated directly, and the total amount of memory that can be addressed is 1 megabyte (Mb) (see Figure 1). On the 8086, and the 80286 running in real mode, there is no way for the operating system to intervene when a program does not use memory properly or to prevent one program from writing into another's memory space. Under OS/2, however, the rules are changed. OS/2 runs the 80286 processor in protected mode, providing hardware support for memory protection and memory access privilege levels, and presenting a very different hardware architecture to the programmer. The contents of segment registers are now logical selectors rather than physical addresses (see Figure 2). Selectors are indexes into descriptor tables that contain the actual physical address and length of the corresponding chunk of memory, and the descriptor tables themselves are typically not accessible to application programs at their privilege level. Each segment has an attribute that determines how it can be accessed: executable, read-only data, or read- write data. Memory protection between programs is enforced by each process's local descriptor table (LDT), a map of the memory segments that can be accessed by that process. If a task tries to load a segment register with a selector that is not valid for its own descriptor table or for the global descriptor table (GDT), a hardware interrupt is generated (see Figure 3). The operating system then recovers control, terminates the offending process, and displays diagnostic information on the system console. Thus, applications running under OS/2 must be civil to survive: there is no alternative to using the host system's services for managing memory. Only the operating system is entrusted with the privilege level and the knowledge of physical memory layout, which is necessary to create and destroy memory segments through manipulation of descriptor tables. Fortunately, OS/2 provides applications with a complete repertoire of memory management services, in three major groups: local heap management, conventional allocation and release of global memory segments, and "huge" global block management (logically contiguous memory spaces that are larger than 64Kb). In exchange for good behavior, the 80286's protected mode provides a reward──virtual memory (see Figure 4). A task can request, and own, more bytes of RAM memory than physically exist in the system. The combination of hardware support for paging and a specialized module of the operating system called the swapper present to the task the illusion that all of its segments are simultaneously present and accessible. When all of physical memory is in use and a process attempts to allocate additional memory, the least recently used segment is copied to a swap file on the disk, and the memory it occupied is freed up. A special bit in the descriptor table entry for the swapped-out segment is set to indicate that it is nonresident. When a selector for a swapped-out segment is used in a memory access, a hardware interrupt called a page fault occurs. The virtual memory manager (swapper) gains control, reads the needed segment from the disk into physical memory (possibly writing another segment out first to make room), updates the descriptor table entry for the selector to correspond to the new physical address, and restarts the process's instruction that was trying to access the segment. Thus, virtual memory means that the presence or absence in physical memory of any particular segment is completely transparent to the process that owns it. If an address that is not present is accessed, it will be invisibly loaded on demand. Correspondingly, the amount of memory that can be committed by all the processes in the system combined is limited only by the amount of swapping space on the disk (although limits on the number of available segment selectors do exist, they are of no practical significance owing to the sizes of today's fixed disks). The 80286's support for protected, virtual memory facilitates two other OS/2 features: shared text (code) segments and dynamic linking. Segments containing machine instructions are marked with the executable attribute and cannot be modified. Thus, when the same program is loaded for execution in two or more sessions, each instantiation can share the same memory-resident piece of code, instead of requiring separate, distinct copies. The protected-mode command processor called CMD.EXE is a good example: the overhead of multiple screen groups is small, since only one copy of the machine code in CMD.EXE is needed. Adding another screen group with its own command processor requires only the creation of another data segment and virtual screen buffer. Dynamic link libraries are simple extensions of the concepts of virtual memory and shared text segments. Under MS-DOS, when a program was linked to a run-time library, the actual machine code for the library routines became a permanent part of the application. Under OS/2, the linker and the .EXE file header have been extended to allow run-time binding (Figure 5) of library routines. When a program containing dynamic links is loaded for execution, OS/2 examines the file header and loads any modules from dynamic link libraries that are required, then updates the FAR CALL addresses within the application. Naturally, these dynamically linked routines can be shared between multiple processes and will be discarded and reloaded as needed by the memory manager. Much of OS/2 itself is implemented in the form of dynamic link libraries. Figure 1: Real Address Mode Segment Selector Interpretation. The selector is used to identify segments in real memory. ┌─────────────────────────────────────────────────────────────────┐ │ │█ │ ╔══════════════╤══════╗ ╔══════════╗ ───┐ │█ │ ║ Selector │ 0000 ║ ║ ║ │ │█ │ ╚══════════╤═══╧══════╝ ║ ║ │ │█ │ │ ┌─ ╟──────────╢ 1M │█ │ Segment and Base │ ║ ║ Physical │█ │ Address 64K ║ Seg 1 ║ Address │█ │ │ │ ║ ║ Space │█ │ └──────────────────────┴─►╟──────────╢ │ │█ │ ║ ║ │ │█ │ ║ ║ │ │█ │ ╚══════════╝ ───┘ │█ │ │█ └─────────────────────────────────────────────────────────────────┘█ ██████████████████████████████████████████████████████████████████ Figure 2: In protected mode the selector does not specify a segment's location in physical memory. Instead, it uniquely identifies (or names) any one of 16K possible segments──the selector uses 14 bits to address the segments in a given task's virtual address space. Because each segment can address 64K, the total virtual space available is Gb. ╔════════════╗ ──┐ ╠═ Seg 3fff ═╣ │ ╠═ Seg 3ffe ═╣ │ ╠═ Seg 3ffd ═╣ │ ┌──────►╠═ Seg 3ffc ═╣ │ │ ╠═ Seg 3ffb ═╣ │ ┌────┴───┐ ║ ║ │ │Selector│ < < 1 G Virtual └────────┘ > > Address Space ║ ║ │ ╠══ Seg 4 ══╣ │ 1 to 64K───╠══ Seg 3 ══╣ │ ╠══ Seg 2 ══╣ │ ╠══ Seg 1 ══╣ │ ╠══ Seg 0 ══╣ │ ╚════════════╝ ──┘ Figure 3: The GDT maintains system-wide information while the LDT maintains task-specific information. There is one GDT for any given system that is shared by every task or process. Each task's virtual local space, managed bu the LDT, is isolated from that of any other. OS/2 itself resolves virtual addresses to map tasks to real memory. CPU │ Memory │ ┌─────────────────────────┐ ├────────────┤──┐ │ │ ┌─┬──►├────────────┤ │ │ ┌15─────────0┐ │ │ ├────────────┤ │ │ │ GDT Limit ├─────────┘ │ │ ∙ │ │ GDT│ ┌23────┴────────────┤ │ │ ∙ │ GDT │ │ GDT Base ├─────────┐ │ │ ∙ │ │ │ └───────────────────┘ │ │ ├────────────┤ │ ├ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┤ │ │ ├────────────┤ │ │ ┌15─────────0┐ │ │ ├────────────┤ │ │ │LDT Selector│ │ └─┴──►├────────────┤──┘ │ └────────────┘ │ │ │ │ │ │ LDT{1} │ │ ┌ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┐ │ ┌─┬──►├────────────┤──┐ LDT│ ┌15─────────0┐ │ │ ├────────────┤ │ │ │ │ LDT Limit ├┼────────┘ │ ├────────────┤ │ │ ┌23────┴────────────┤ │ │ ∙ │ │ │ ││ LDT Base ├┼────────┐ │ │ ∙ │ Current │ └───────────────────┘ │ │ │ ∙ │ LDT │ │ │ │ │ │ ├────────────┤ │ │ Program Invisible │ │ ├────────────┤ │ │ └ ─ ─ ─ ─ ─ ─ ─ ─ ─ ─ ┘ │ │ │ ├────────────┤ │ └─────────────────────────┘ └────►├────────────┤──┘ │ │ TASK 1 │ LDT{n} │ VIRTUAL ADDRESS─┐ ├────────────┤ SPACE │ ├────────────┤ ┌──────────────┐ │ ├────────────┤ │ ╔══════════╗ │ │ │ ∙ │ │ ║TASK 1 ║ │ │ │ ∙ │ TASK 3 │ ║LOCAL ║ │ │ │ ∙ │ VIRTUAL ADDRESS │ ║ADDRESS ║ │ │ ├────────────┤ SPACE ┐ │ ║SPACE ║ │◄──┘ ├────────────┤ ▼ │ ╚══════════╝ │ ├────────────┤ ┌─────────────│──────────────│┐ ├────────────┤ │ ╔══════════╗│┌─────────────│───────────────┐ │ ║TASK 3 ║││ ╔════════╗ ││ ╔══════════╗ │ │ ║LOCAL ║││ ║GLOBAL ║ ││ ║TASK 2 ║ │ │ ║ADDRESS ║││ ║ADDRESS ║ ││ ║LOCAL ║ │ │ ║SPACE ║││ ║SPACE ║ ││ ║ADDRESS ║ │ │ ╚══════════╝││ ╚════════╝ ││ ║SPACE ║ │ └─────────────└──────────────┘┘ ╚══════════╝ │ └─────────────────────────────┘ └TASK 2 VIRTUAL ADDRESS SPACE Figure 4: 80826 Virtual Address Space. OS/2 provides one GDT that is shared by each task or process. Hardware support for paging and the OS/2 swapper together provide the illusion that all segments are simultaneously present and accessible. Task B Address Space Task A Private Address Space Task B Private Address Space ┌───────┐ 65535 ┌───────┐ 65535 │ │ │ │ │ SEG. │ Offset │ SEG. │ Offset │ │ │ │ │ │ ┌───────┬───────►└───────┘ 0 ▼ ┌───────┬───────►└───────┘ 0 ▼ │ │ ∙ │ │ ∙ │ LDT A │ ∙ │ LDT B │ ∙ │ │ ∙ │ │ ∙ └───────┴──┐ ┌───────┐ 65535 └───────┴──┐ ┌───────┐ 65535 │ │ │ │ │ │ │ │ SEG. │ Offset │ │ SEG. │ Offset │ │ │ │ │ │ │ │ └────►└───────┘ 0 ▼ └────►└───────┘ 0 ▼ Task C Private Address Space Shared Address Space ┌───────┐ 65535 ┌───────┐ 65535 │ │ │ │ │ SEG. │ Offset │ SEG. │ Offset │ │ │ │ │ │ ┌───────┬───────►└───────┘ 0 ▼ ┌───────┬───────►└───────┘ 0 ▼ │ │ ∙ │ │ ∙ │ LDT C │ ∙ │ GDT │ ∙ │ │ ∙ │ │ ∙ └───────┴──┐ ┌───────┐ 65535 └───────┴──┐ ┌───────┐ 65535 │ │ │ │ │ │ │ │ SEG. │ Offset │ │ SEG. │ Offset │ │ │ │ │ │ │ │ └────►└───────┘ 0 ▼ └────►└───────┘ 0 ▼ Figure 5: To invoke run-time dynamic linking, the application uses the following system calls. DOSLOADMODULE Load Dynamic Link Module DOSFREEMODULE Free Dynamic Link Module DOSGETPROCADDR Get Dynamic Link Procedure Address DOSGETMODHANDLE Get Dynamic Link Module Handle DOSGETMODNAME Get Dynamic Link Module Name ████████████████████████████████████████████████████████████████████████████ OS/2 Inter-Process Communication: Semaphores, Pipes, and Queues Ray Duncan☼ Operating System/2 multitasking services allow applications to create multiple concurrent threads within a process or to create child processes. However, as we have seen earlier (see "OS/2 Multitasking: Exploiting the Protected Mode of the 80286,"), the multitasking functions provide a parent process with only a very limited ability to influence and communicate with a child process. The parent process can pass information to the child at load time in the form of command strings and the environment block, it can obtain the child process's exit (return) code and a code describing the child process's method of termination, and unilaterally abort a child process. To fill the need for rapid exchange of all kinds of information between concurrent threads and related or unrelated processes, OS/2 includes a rich set of system services called inter-process communication (IPC) functions. Computer literature is teeming with different mechanisms for inter-process communication, and more are being added every day. Still, there is broad agreement on three basic methods of IPC, all of which are supported by OS/2: semaphores, pipes, and queues. In this article, we'll briefly discuss each of these three IPC mechanisms, summarize the relevant OS/2 functions, and supply a brief coding example. Since there are many options available in the IPC services, we can't provide a comprehensive description of their capabilities here; the program fragments are only intended to demonstrate the most common usage. More detailed information can be found in the OS/2 Programmer's Reference manual. While reading about the IPC functions, bear in mind the distinctions between processes and threads. When a process opens or creates a semaphore, pipe, or queue, a handle is returned that can be used by any thread within that process and is inherited by any child processes. On the other hand, when a thread issues an OS/2 function call that waits on a semaphore or performs a synchronous read or write to a pipe or queue, only the calling thread is suspended──other threads in the process continue to run as they did before. When multiple threads in the same process issue requests against the same IPC object, any necessary serialization of these requests is taken care of within the operating system. Semaphores A semaphore can be viewed as a simple object with two states: set (owned) or cleared (not-owned). OS/2 provides a complete battery of system services to test, set, and clear semaphores; a summary of these functions is found in Figure 1. Semaphores are a high-performance mechanism of inter-process communication, because they are always resident in memory and the OS/2 services that manipulate them are relatively fast. The classical use of semaphores, and a usage fully supported by OS/2, is to control access to a nonreentrant routine or a serially reusable resource (SRR). An SRR is a file, peripheral device, or a memory object that is damaged or produces unpredictable results if it is accessed by more than one thread or process at a time. With semaphores, mutual exclusion on such a resource is easy to arrange between cooperating processes. A semaphore is established that represents the resource, and a thread or process refrains from accessing the resource unless it "owns" the corresponding semaphore. An alternative use of semaphores in OS/2 is to provide for synchronization or signaling between threads or processes. In this usage, one or more threads can suspend themselves by using an OS/2 function to "block" or wait on a semaphore. When the semaphore is cleared by another thread in response to some event, all of the threads that were blocking on that semaphore will wake up and run. When a semaphore is used for signaling and no resource that can be corrupted is involved, any thread that knows about the semaphore can set, clear, or test the semaphore at any time. To provide for the slightly different requirements of inter-thread and inter-process communication, OS/2 supports two types of semaphores: RAM semaphores and system semaphores. RAM semaphores are used for signaling or resource control between multiple threads in a single process. Each RAM semaphore requires the allocation of a double word of data storage; the double word should be initialized to zero when the process is started. The handle for a RAM semaphore is simply its 32-bit address (selector and offset). System semaphores are named objects that are used for signaling or synchronization between processes; the name always takes the form: \SEM\name.ext The extension (.ext) is optional. The storage for a system semaphore is allocated by the operating system somewhere outside the creating processes' memory space. Access to a system semaphore is obtained by creating or opening it by name; OS/2 returns a 32-bit handle that can be used for subsequent access to the semaphore. When the process no longer requires access to the system semaphore, it can use the handle to close it as it would close a file. The three functions that control access to a system semaphore, DOSCREATESEM, DOSOPENSEM, and DOSCLOSESEM, are described in more detail below. Creating a system semaphore. The function DOSCREATESEM is called with the address of a null-terminated (ASCIIZ) semaphore name, the address of a double-word variable that will receive the semaphore handle, and a flag that indicates whether ownership of the semaphore will be "exclusive"──in which case the state of the semaphore cannot be altered by a process that does not own it (see Figure 2). If the create operation is successful, the initial state of the semaphore will be not-owned (cleared). Opening a system semaphore. The function DOSOPENSEM is called with the address of a null-terminated (ASCIIZ) semaphore name of an existing semaphore and the address of a double word of storage that will receive a handle. Opening a semaphore does not establish ownership, test, or change the value of the semaphore. When a process creates or opens system semaphores, the handles to those semaphores are inherited by any child processes that are started with DOSEXECPGM. However, the child process does not own the semaphores even if the parent owned them; only one process at a time can own a given semaphore. Closing a system semaphore. The function DOSCLOSESEM is called with a handle and terminates access to a system semaphore. If a process terminates with open semaphores, they are closed automatically. However, if any of these semaphores were owned by the process, any threads in other processes that were waiting on the semaphore are awakened and receive an error code that indicates that the owning process may have terminated abnormally and the resource controlled by the semaphore may be in an indeterminate state. The semaphore itself ceases to exist when processes that use the semaphore have terminated or called DOSCLOSESEM. ─────────────────────────────────────────────────────────────────────────── RAM Semaphores: A thread (Tx) issues the DosSemRequest call to claim access to the semaphore controlling a given SRR. If the semaphore is currently unused, DosSemRequest gives the calling thread ownership of the semaphore. All other threads are locked out until the owning thread clears the semaphore through the DosSemClear and relinquishes ownership of both the SRR and the semaphore. T1 ► D Tx enters and owns Multiple o the SRR (semaphore D Concurrent s not available) o Threads S │ s (T1...Tn) T2 ► e │ S Tx exits and desire m ▼ e relinquishes access to R Tx ► Tx ► Tx ► m Tx ►ownership of a Serially e C the SRR and Reusable ... ► q │ l the semaphore Resource u │ e (SRR) e Serially Reuseable a s Resource r Tn ► t ▼▼▼▼▼ SEMAPHORE AVAILABLE ─────────────────────────────────────────────────────────────────────────── Mutual Exclusion The two OS/2 functions that are associated with the classical use of a semaphore are DOSSEMREQUEST and DOSSEMCLEAR. Claiming a semaphore. The OS/2 function DOSSEMREQUEST is used by a thread to establish ownership of a semaphore and, by inference, ownership of the resource associated with the semaphore. DOSSEMREQUEST is called with the handle of a system or RAM semaphore and a time-out parameter. If the specified semaphore is currently unowned, the function sets it as owned and returns a success code. If the semaphore is already owned by another thread, the system either suspends the thread indefinitely until the semaphore becomes available, waits for a specified interval and then returns with an error code, or returns immediately with an error code, depending on the time-out parameter. Recursive requests for system semaphores are supported by means of a use count, maintained by OS/2, of the number of times the semaphore owner has issued DOSSEMREQUEST without a corresponding DOSSEMCLEAR. However, recursive use of RAM semaphores is not supported. Releasing a semaphore. The function DOSSEMCLEAR is called with a semaphore handle and releases the thread's ownership of that semaphore. An error code is returned if the handle is invalid or if the semaphore was created with the exclusive option and is not currently owned by the calling thread. If the semaphore is already cleared, then no error is returned. When multiple threads are waiting on the same semaphore with DOSSEMREQUEST, it can be difficult to predict which thread will be awakened and acquire the semaphore when it is released by the current owner. The thread selected depends on an interaction of the priorities of the waiting threads, the position of the threads in the scheduler's list, and the activity of other threads in the system. Signaling Semaphores are used as signals when only one thread is in a position to detect an event but other threads may wish to take action based on this event. For example, a thread in a detached process utility that is monitoring the keyboard data stream for a hot key might wish to trigger other threads in the same process to display a pop-up screen or write to a file. The MS OS/2 functions that support signaling include DOSSEMSET, DOSSEMCLEAR, DOSSEMWAIT, DOSSEMSETWAIT, and DOSMUXSEMWAIT. Setting a semaphore. The OS/2 function DOSSEMSET is called with the handle of a system or RAM semaphore and unconditionally sets that semaphore (see Figures 3 and 4). The function fails if the handle is invalid or if a system semaphore was created with the exclusive option and is currently owned by another process (a semaphore used for signaling would not ordinarily be created with the exclusive option). Clearing a semaphore. The function DOSSEMCLEAR is called with the handle of a RAM or system semaphore and unconditionally clears that semaphore. As with DOSSEMSET, the function fails if the semaphore handle is invalid or if the semaphore was created with the exclusive option and is currently owned by another process. If any threads were blocked on the semaphore, they are restarted. Waiting on a semaphore. OS/2 contains several functions that allow a thread to suspend itself until one or more semaphores are cleared. The function DOSSEMWAIT is called with a semaphore handle and a time-out value. The calling thread is suspended until the indicated semaphore is cleared by another thread or process with DOSSEMCLEAR; it regains control immediately if the semaphore is not set. The function returns an error if the handle is invalid, no-wait was specified, and the semaphore is currently set; if a finite wait was specified and timed out; or if the semaphore was created with the exclusive option and is currently owned by another process. The function DOSSEMSETWAIT works just like DOSSEMWAIT, except that it sets the semaphore if it was not already set and then suspends the current thread until the semaphore is cleared by another thread or process or the indicated time-out expires. DOSSEMWAIT and DOSSEMSETWAIT are level-triggered, not edge-triggered. This means that it is possible for a thread that is blocking on a semaphore to miss a quick clearing and resetting of the semaphore, depending on the thread's priority and position in the scheduler's list and the activity of the other threads in the system. The function DOSMUXSEMWAIT is called with a list of up to 16 semaphores and an optional time-out interval. The calling thread is suspended until any of the indicated semaphores are cleared or the indicated interval has elapsed. Unlike DOSSEMWAIT and DOSSEMSETWAIT, the function DOSMUXSEMWAIT is edge-triggered; the waiting thread is awakened when one of the semaphores in the list changes state even if that semaphore gets set (armed) again right away by another process or another thread. Pipes Pipes are a means of inter-process communication midway in power between semaphores and queues. Like semaphores, the level of performance of pipes is relatively high, because the information is always kept resident in memory; like queues, pipes can be used to pass any type of data between processes. Physically, a pipe is simply a chunk of memory that is used as a ring buffer, with In and Out pointers maintained by the system. From a process's point of view, reading and writing a pipe are equivalent to reading and writing a file, except that a pipe is much faster. Pipes are not named entities. A process creates a pipe by calling the OS/2 function DOSMAKEPIPE with the addresses of two variables to receive the read and write handles for the pipe, and a maximum pipe size (up to 65,504 bytes). The function fails if there is not enough memory to create the pipe or if no handles are available. The two handles for reading and writing the pipe are assigned out of the same numeric sequence as those used for access to files. Any child processes automatically inherit the handles and have access to the pipe. We have already encountered the two major restrictions on the use of pipes: they can only be used for communication between closely related processes, and the maximum amount of data that a pipe can contain at one time is relatively small. Threads read and write a pipe with the usual file DOSREAD and DOSWRITE calls, supplying a handle, buffer address, and record length (see Figure 5). If a thread attempts to write to a pipe and the pipe is full, that thread is suspended until another thread or process removes enough data from the pipe so that the write can be completed. If a thread requests a synchronous read from a pipe and the pipe is empty, that thread is suspended until some other thread writes enough data to the pipe to satisfy the read request. There are no control or permission mechanisms or checks performed on operations to pipes. A pipe vanishes from the system when all the processes using the pipe either close the pipe handles or terminate. If two processes are communicating with pipes and the process reading the pipe ends, the process writing the pipe receives an error code. The most common usage of pipes by a process──and the way that the system's command interpreter uses them──is to arrange for the pipe's handles to be substituted for the standard input and standard output handles. Any child processes that are started then automatically communicate with the parent process for their input and output instead of the keyboard and screen, without any special knowledge or action on the child's part. ─────────────────────────────────────────────────────────────────────────── Communicating with a Pipe: Pipes are a high performance mechanism for communication between closely related processes (such as a parent process and multiple child processes). Pipes are read and written like a file, and may contain variable-length messages with any content. ╔═════════╗ PIPES ╔══════════╗ ║ NEXT IN ║ ╔═══════════╗ ║ NEXT OUT ║ ╚═════════╝ ║ aaaaaa ║ ╚═════════╝ █ ║ aaaaaa ║ │ │ ╔═════════════╗ █ █ ╟───────────╢ │ │ ╔═════════════╗ ║ PROCESS A ╟─►◘ █ ║ eeeeee ║ │ └────►║ PROCESS X ║ ╚═════════════╝ █ █ ║ eeeeee ║ │ ╚═════════════╝ █ █ ╟───────────╢ │ ╔═════════════╗ █ █ ║ bbbbb ║─────┘ ║ PROCESS B ╟─►◘ █ ╟───────────╢ ╚═════════════╝ █ █ ║ ggggg ║ █ █ ║ ggggg ║ ╔═════════════╗ █ █ ║ ggggg ║ ║ PROCESS C ╟─►◘ █ ╟───────────╢ ╚═════════════╝ █ █ ║ ccccc ║ █ ╠═══════════╣ █─► ╠═══════════╣ █ ╠═══════════╣ ╚═══════════╝ ─────────────────────────────────────────────────────────────────────────── Queues Queues are the most powerful inter-process communication service provided by OS/2, and consequently the most complex to use. Queues are slower than pipes, and much slower than semaphores, but they are also far more flexible. ■ Queues are named objects and can be opened and written by any process. ■ The size of a queue is limited only by the amount of free memory plus swap space on the disk. ■ Each record in a queue is a separately allocated block of memory storage and can be as large as 65,536 bytes. ■ The records in a queue may be ordered by the first-in-first-out (FIFO), last-in-first-out (LIFO), or priority methods. ■ The queue owner can examine records in the queue and remove them selectively, in any order. ■ Data in the queue is not copied from place to place by the operating system; instead, the data is passed in shared memory segments. The name of a queue will always take the form of \QUEUES\name.ext where the extension (.ext) is optional. When a queue is created or opened, OS/2 returns a handle that is used for subsequent access to the queue. A summary of OS/2 queue services can be found in Figure 6. Creating a queue. The function DOSCREATEQUEUE is used to originate a new queue in the system. The function is called with a null-terminated (ASCIIZ) string that names the queue, a parameter that specifies the queue ordering (FIFO, LIFO, or priority), and the address of a variable to receive the queue handle (see Figure 7). An error code is returned if a queue already exists with the same name, the given name is invalid, or there is not enough free memory in the system to establish the queue's supporting data structure. Only the process that creates the queue, called the queue owner, can remove records from the queue, but all threads in the owning process can access the queue with equal authority. Opening a queue. Any process can open an existing queue by calling the OS/2 function DOSOPENQUEUE with the name of the queue and the address of two variables to receive the queue handle and the PID of the queue owner. The handle is used for subsequent writes of records to the queue. Writing a queue. Adding records to a queue is much more involved than writing a record to a pipe. The writer must first allocate a chunk of global shared memory of appropriate size with the function DOSALLOCSEG and copy the data for the queue record into it. Next, the writer must use the function DOSGIVESEG to obtain a new selector for the memory segment that can be passed to another process. Finally, the writer calls DOSWRITEQUEUE with a queue handle, a priority, the address (from DOSGIVESEG) and length of the data to be added to the queue (see Figure 8). The queue write proper fails if the queue handle is invalid or if there is insufficient system memory to expand the supporting structure of the queue. Obviously, the sequence can also fail at an earlier point (in DOSALLOCSEG or DOSGIVESEG) if the system runs out of memory (the physical memory plus swap space on the disk is exhausted) or selectors. Reading a queue. The function DOSREADQUEUE is used by the queue owner to remove a record from a queue; it is called with a queue handle and several other selection parameters and returns the address of a queue element (selector and offset) and its length. The owner can choose a record from the queue based on its position or priority or simply take the next record in line. The read operation can also be synchronous (the calling thread is suspended until a record is available) or asynchronous (the calling thread gets control back immediately, and a semaphore is set when a record is available). The function DOSPEEKQUEUE works similarly to DOSREADQUEUE, but the record is not removed from the queue when it is retrieved. This allows the queue owner to scan through the data waiting in the queue and decide on a processing strategy, without disturbing the data structure or being forced to copy the records to its own memory. Miscellaneous operations. The function DOSQUERYQUEUE allows any process that has a valid handle for a queue to obtain the current number of elements in that queue. DOSPURGEQUEUE discards all records that are currently in the queue; this function can only be called by the queue owner. Closing a queue. The function DOSCLOSEQUEUE is called with a queue handle and informs OS/2 that the calling process will not require further access to the queue. If the closing process is also the queue owner, the queue is purged and destroyed──any further attempts to write records to the queue by other processes will fail. ─────────────────────────────────────────────────────────────────────────── Communicating with a Queue: Queues are the most flexible and powerful mechanism of inter-process communication supported by OS/2. A queue is basically an ordered list of shared memory segments; each segment contains a separate message and may be as large as 64K. The messages may be ordered in the queue by First-In-First-Out, Last-In-Last-Out, or priority, and may be inspected or removed from the queue by the server process in any order whatsoever. ╔═════════════╗ ║ Queues Data ╠════════════╗ █ ► ║ Structure ║ ║ ╔═══════════╗ █ ╚═══╦═════════╝ ║ ╔═══════════╗ ║ Process D ║ ► ◘ ▼ ╚═══►║ Process Y ║ ╚═══════════╝ █ ╔════════╦════════════╗ ╚═══════════╝ ╔═══════════╗ █ ║ MSG 'H'║ ▼ ║ Process E ║ ► ◘ ╚════════╝ ╔════════╦═════════════╗ ╚═══════════╝ █ ║ MSG 'K'║ ▼ ╔═══════════╗ █ ╚════════╝ ╔════════╗ ║ Process F ║ ► ◘ ╔═════════════════╣ MSG 'J'║ ╚═══════════╝ █ ▼ ╚════════╝ █ ╔════════╗ ╔══════╣ MSG 'N'║ ▼ ╚════════╝ ╔════════╗ ║ MSG 'M'║ ╚════════╝ ─────────────────────────────────────────────────────────────────────────── Summary OS/2 supports three methods of inter-process communication that are used to pass messages between threads or processes: semaphores, pipes, and queues. Semaphores are used to symbolize ownership of a resource or signal occurrence of an event; they can be thought of as simple flags that can be set, cleared, or tested. Pipes are a high-performance means of passing variable-length, variable- content data between two closely related processes; however, pipes have the relative disadvantage that their maximum size is fixed at the time that they are created and they can never hold more than 64K. Queues allow a vast amount of prioritized data to be passed from multiple client processes to a server process; each message in the queue can be as large as 64K, and the total amount of data in the queue is limited only by the system's virtual memory. The selection of a particular IPC technique for a given application must be made carefully on the basis of performance requirements and the character of the messages to be exchanged between the interested threads or processes. Figure 1: A summary of OS/2 semaphore support functions. Semaphores can be used to coordinate access to a resource or for signaling between threads or processes. Access to system semaphores DOSCREATESEM Create a system semaphore DOSOPENSEM Open an existing system semaphore DOSCLOSESEM Close a system semaphore Semaphore functions for resource control DOSSEMREQUEST Establish ownership of a semaphore DOSSEMCLEAR Release ownership of a semaphore Semaphore functions for signaling DOSSEMSET Unconditionally set a semaphore DOSSEMCLEAR Unconditionally clear a semaphore DOSSEMWAIT Wait until semaphore is cleared DOSSEMSETWAIT Unconditionally set semaphore and wait until it is cleared by another thread DOSMUXSEMWAIT Wait for any of several semaphores to be cleared Figure 2: A code fragment that illustrates the creation or opening of a system semaphore. push 1 ; exclusive ownership not desired push ds ; variable to receive semaphore handle push offset DGROUP:shandle push ds ; address of semaphore name push offset DGROUP:sname call DOSCREATESEM ; transfer to OS/2 or ax,ax ; create successful? jz continue ; jump if it was... cmp ax,err_sem_exists ; semaphore already exists? jnz error ; jump if some other error ; semaphore exists, open it instead push ds ; variable to receive handle push offset DGROUP:shandle push ds ; address of semaphore name push offset DGROUP:sname call DOSOPENSEM ; transfer to OS/2 or ax,ax ; was open successful? jnz error ; if can't open and can't create, ; something is terribly wrong ∙ ∙ ∙ sname db '\SEM\RESOURCE.LCK',0 shandle dd 0 Figure 3: A code sample that demonstrates setting a system semaphore. System semaphores are best suited for "between" processes. ∙ ∙ ∙ ; open the semaphore push ds ; variable to receive handle push offset DGROUP:shandle push ds ; address of semaphore name push offset DGROUP:sname call DOSOPENSEM ; transfer to O/S2 or ax,ax ; open successful? jnz open_err ; jump if open failed ∙ ∙ ∙ ; set the semaphore: ; push semaphore handle... push word ptr shandle+2 push word ptr shandle call DOSSEMSET ; transfer to O/S2 or ax,ax ; was operation successful? jnz set_err ; jump if set failed ∙ ∙ ∙ sname db '\SEM\RESOURCE.LCK',0 shandle dd 0 ; handle for system semaphore Figure 4: A code fragment that demonstrates setting a RAM semaphore. RAM semaphores are best suited for use "within" processes. ∙ ∙ ∙ ; set the RAM semaphore push ds ; push its address push offset DGROUP:my_sem call DOSSETSEM ; transfer to O/S2 or ax,ax ; was set successful? jnz set_err ; jump if set failed ∙ ∙ ∙ mysem dd 0 ; storage for RAM semaphore Figure 5: Establishing Pipes ∙ ∙ ∙ push ds ; address to receive handle push offset DGROUP:prdh ; for reading pipe push ds ; address to receive handle push offset DGROUP:pwriteh ; for writing pipe push 0 ; max pipe size = default call DOSMAKEPIPE ; transfer to O/S2 or ax,ax ; was pipe created? jnz error ; jump if create failed. ∙ ∙ ∙ ; in here we spawn a "child" ; process which inherits handles... ; now send a message to child ; process through the pipe... push pwriteh ; pipe write handle push ds ; address of message push offset DGROUP:message push message_length ; length of message push ds ; address of variable to push offset DGROUP:status ; receive bytes written call DOSWRITE ; transfer to O/S2 or ax,ax ; did write succeed? jnz error ; jump if write failed. ∙ ∙ ∙ preadh dw ? : handle to read pipe pwriteh dw ? ; handle to write pipe status dw ? ; receives length from DOSWRITE Figure 6: Substantial control over queues is provided by the OS/2 queue management functions. Queue access DOSCREATEQUEUE Create a queue (process is owner, can read or write messages to queue) DOSOPENQUEUE Open a previously existing queue (can only write messages to queue) DOSCLOSEQUEUE Close queue (queue is destroyed when closed by owner) Queue input/output DOSWRITEQUEUE Write message to queue (either queue owner or any process that has opened queue) DOSREADQUEUE Read message from queue (queue owner only) DOSPEEKQUEUE Nondestructive read of queue message (queue owner only) Queue information DOSQUERYQUEUE Find number of messages currently in queue (queue owner or any process that has opened queue) DOSPURGEQUEUE Discard all messages currently in queue (queue owner only) Figure 7: Creating a queue, waiting for a message to be written into it, and then closing the queue (destroying it). ∙ ∙ ∙ ; first create queue... push ds ; address to receive handle push offset DGROUP:qhandle push 0 ; queue ordering = FIFO push ds ; address of queue name push offset DGROUP:qname call DOSCREATEQUEUE ; transfer to OS/2 or ax,ax ; was create successful? jnz error ; jump if create failed ; now read from queue push qhandle ; queue handle push ds ; address to receive PID push offset DGROUP:qident ; and event code push ds ; receives message length push offset DGROUP:qmsglen push ds ; receives message pointer push offset DGROUP:qmsgptr push 0 ; 0= read first element push 0 ; 0= synchronous read push ds ; receives message priority push offset DGROUP:qmsgpri push ds ; handle for RAM semaphore push offset DGROUP:qsem ; (not used for synch reads) call DOSREADQUEUE ; transfer to OS/2 or ax,ax ; was read successful? jnz error ; jump if read failed les bx,qmsgptr ; let ES:BX point to queue msg push es ; now release queue message's call DOSFREESEG ; shared memory segment or ax,ax ; was release successful? jnz error ; jump if released failed push qhandle ; now close the queue call DOSCLOSEQUEUE ; also destroying it or ax,ax ; was close successful? jnz error ; jump if close failed ∙ ∙ ∙ qhandle dw ? ; receives handle from DosCreateQueue qname db '\QUEUES\MYQUEUE',0 ; ASCIIZ queue name qident dw 0,0 ; writer's PID, event code qmsglen dw ? ; length of queue msg received qmsgptr dd 0 ; address of queue msg received qmsgpri dw ? ; priority of queue msg Figure 8: Opening a queue, writing a message into it, and then closing it. ∙ ∙ ∙ push ds ; address to receive PID of push offset DGROUP:qowner ; queue's owner push ds ; address to receive handle push offset DGROUP:qhandle push ds ; address of queue's name push offset DGROUP:qname call DOSOPENQUEUE ; transfer to OS/2 or ax,ax ; was open successful? jnz error ; jump if open failed push qmsglen ; length of shared memory segment push ds ; receives selector for segment push offset DGROUP:qselc push 1 ; 1 = segment will be shared call DOSALLOCSEG ; transfer to OS/2 or ax,ax ; was allocation successful? jnz error ; jump if memory allocate failed mov si,offset qmsg ; copy our queue message mov es,qselc ; to shared segment xor di,di mov cx,qmsglen cld rep movsb push qselc ; get a new selector for the push qowner ; shared segment that can push ds ; be used by receiver of push qselr ; the queue message call DOSGIVESEG ; transfer to OS/2 or ax,ax ; did function succeed? jnz error ; jump if can't give it away push qhandle ; handle for queue push 0 ; 0= event data push qmsglen ; length of queue message push qselr ; selector for queue data push 0 ; (offset) push 0 ; queue element priority call DOSWRITEQUEUE ; transfer to OS/2 or ax,ax ; did write succeed? jnz error ; jump if queue write failed push qselc ; release shared memory that call DOSFREESEG ; contains the queue message or ax,ax ; did release succeed? jnz error ; jump if release failed push qhandle ; now close the queue call DOSCLOSEQUEUE ; transfer to OS/2 or ax,ax ; did close succeed? jnz error ; jump if close failed ∙ ∙ ∙ qhandle dw ? ; receives handle from DosOpenQueue qowner dw ? ; receives PID of queue owner qname db '\QUEUES\MYQUEUE',0 ; ASCIIZ queue name qselc dw ? ; shared memory selector for caller qselr dw ? ; shared memory selector for receiver qmsg db 'This is my queue message' qmsglen equ $-qmsg ████████████████████████████████████████████████████████████████████████████ The MS OS/2 LAN Manager System connectivity has become an increasingly significant issue in the development of today's personal computing systems. As a result, it is becoming substantially more important for operating systems to provide expanded support for Local Area Networking. The design of Microsoft OS/2 recognizes this fact by providing substantial networking support in the form of the Microsoft OS/2 LAN Manager. Basically, the MS OS/2 LAN Manager is a protected mode version of Microsoft Networks, and it is compatible with existing versions of MS-Net, XENIX Net and the IBM PC Local Area Network Program. The LAN Manager is a separately "installable" component of MS OS/2; once installed, it works with the operating system to seamlessly solve networking requirements. The LAN Manager is designed to facilitate the traditional network file- and printer-sharing services. But it goes far beyond this by providing for a new class of communication facilities, which will allow software developers to increase the sophistication of network services substantially and will make distributed processing practical. The LAN Manager uses the protected mode, multitasking facilities of MS OS/2 to implement non-dedicated servers with extensive network administration and security services, as well as transparent file- and printer-sharing capabilities. The LAN Manager allows an OS/2 based system to be added to existing MS- Net-based networks. The new system can function within the network as a workstation or a server or both. Protected Mode NetBios The multitasking environment in which the LAN Manager must function requires a new, protected-mode version of the MS-Net NetBios. The NetBios is the equivalent of the transport layer of the OSI model, and provides for virtual circuit and datagram services. The standardized NetBios API allows network drivers to access low-level hardware (for such tasks as checking the status of a network card). The NetBios is designed not only to understand the hardware at one end (network hardware comes with a customized Hardware/NetBios layer), but also to understand API calls from network drivers at the other end. Applications access NetBios services via the network drivers. The LAN Manager must deal with several multitasking issues. First, applications will most likely need to share access to more than one network driver. Second, there must be a way for network drivers to share access to the NetBios services. Central to the LAN Manager is the Microsoft Net Kernel, which provides applications with the support necessary to simultaneously access one or more network drivers. Very briefly, the kernel serves as a dispatcher for applications' requests to access network drivers, and controls access to those network drivers by treating each driver as a named, installable device driver that can be opened or closed. To insure that network drivers can interface to the net kernel and therefore share access to the NetBios, developers must adhere to the system interface specifications (and the protected mode NetBios API) published as part of the Microsoft OS/2 LAN Manager. These specifications will ease the effort that is necessary for OEMs and third-party network software developers to design their hardware and software applications to support the new multitasking, distributed processing environment. Distributed Processing From a software developer's point of view, the LAN Manager can be seen as providing a set of extensions to the MS OS/2 Inter-process Communications API. Just as MS OS/2 provides semaphores, pipes, and queues for communication between different processes running on the same machine, the LAN Manager provides named pipes and mailslots for communication between processes running on different machines on the network. Named pipes and mailslots are the building blocks of distributed processing. These remote inter-process communication facilities provide the ability to establish communications channels between tasks, allowing for sharing of data──not only between a server machine and its clients, but also among peer machines residing on a given network. Data-base applications provide a clear example of the distinction between file service and distributed proessing. Using current LAN technology, it is typical to search a file resident on another machine by reading a virtual file that is mapped to the real file by the MS-Net redirector. In the distributed processing model, the requesting workstation passes a request to the server, which then conducts the search and passes back the results. The only information that travels on the network is the original request and the reply. This requires a level of inter-machine communication not possible with file- or record-oriented facilities. The LAN Manager API is analogous at the network level to the MS OS/2 Inter-process Communication API that allows thread-to-thread and process- to-process communication at the operating system level. At any given moment, LAN API functions can block, be blocked, or can be in a state of execution (i.e., reading, writing or querying either named pipes or mailslots). At the applications level, the LAN API provides CALL-based functions that can be incorporated into high-level language code. Programs written to the specifications of the API will be able to communicate across a network. Virtual Circuits The LAN API supports communications at the "Network and Transport Levels" of the Open System Interconnect (OSI) model. Stream-oriented and optionally, message-oriented information is communicated through a mechanism called the named pipe. Message-oriented information can also be communicated via the high-speed mechanism called a named mailslot. To understand the difference between named pipes and mailslots, one needs to understand two concepts basic to communications: those of virtual circuits and datagrams. Virtual circuits are network-level data channels that guarantee an error- free and continuous communication channel, where each packet of routed information is delivered in order. Regardless of how a channel is established between two points (it could, for example, be a very complex switching network in a phone system), the two communicating ends perceive the channel as a direct communications link. This is, in essence, how named pipes work. Datagrams are network-level channels that accept isolated messages (specific packets of information) from the transport layer and attempt to deliver them without regard for order or timeliness, much the same way as a postal system, in which the sender mails letters without knowing exactly when they will arrive, or in what order (message B, sent after message A, might arrive before message A). Named mailslots work in this fashion. Across the network, either of these mechanisms lets processes transparently establish remote or local communications through symbolic names that follow MS OS/2 file naming conventions. Additionally, it is possible to impose access restrictions on named pipes and mailslots, just as can be done with files. Named Pipes Every reader of MSJ should be familiar with MS-DOS pipes. These pipes function as a transparent holding area for the output of one program to become the input of another. For example, DIR | SORT routes the output from DIR to a temp file that is then read by SORT. Named pipes essentially extend this facility to provide direct communication between processes running on a network. Named pipes are two- way (or full duplex) channels that can be written at either end and read at the opposite end (that is, what is written at one end cannot be read from that same end). To create a named pipe a process executes the DosMakeNmPipe function call (see Figure 1), which then opens a pipe as either a byte stream or a message stream. DosMakeNmPipe returns an identifying handle for that end of the pipe, which is called the serving end of the pipe. DosOpen (a regular OS/2 API call) opens the other end of the pipe, known as the client end, and similarly returns a handle. Once a named pipe has been established it is controlled through the facilities provided by the LAN Manager. It can be read, written to and queried for activity or information. Named pipes can, at this point be utilized across a network, and can be serially closed and opened by other processes. Named pipes are therefore extremely flexible. Once created they can be accessed just like any sequential file, from beginning to end. Because rewinding of pipes is not possible (information comes and goes), the contents of a pipe can only be accessed once, unless the original information is again sent through the pipe. Local and remote procedure call dialogues between processes can be efficiently implemeted because named pipes support transaction I/O calls. Named pipes can also be accessed by applications running on MS DOS 3.x, machines serving as workstations on a given network, allowing those programs to communicate with server-based applications. Thus, DosMakeNmPipe can allow multiple instances of a pipe, each with the same name. This insures that multiple clients DosOpening to that name will obtain separate and distinct pipes to the serving process. Named Mailslots Named pipes must insure that a full duplex communication channel exists between processes. There is, of necessity, a considerable amount of system overhead necessary to maintain such a connection. Certain types of application-specific transactional messages to be exchanged between processes do not require a full duplex, error-free channel of communication. A named mailslot provides the optimal means for handling messages under these conditions. Note that byte streams cannot take advantage of named mailslots. Byte streams need the support mechanisms built into named pipes to ensure error-free transmission. Mailslots are inherently fast because they need not be opened or closed by processes. Once a named mailslot has been established through the DosMakeMailslot function call processes running on different machines across a network simply write to it by name. The function call used to write to a named mailslot, DosWriteMailslot, supports both "class" and "priority" settings for messages. Priority controls which messages DosReadMailslot reads first. Class is usually used with remote mailslots. Messages can be assigned either a first- or second-class status. First-class mail is reliable in that DosWriteMailslot blocks until a message is delivered or an error occurs, insuring that the state of the message will be known. Second-class mail is sent strictly without error checking. Although delivery will probably succeed, if it fails an error will not be reported. Between high-speed named mailslots and highly reliable named pipes, the LAN Manager provides the necessary communications facilities necessary to meet most sophisticated network communications needs. Software developers now have a base on which to develop complete, distributed applications. The opportunity to take advantage of a programming environment in which applications can access remote sites, and in which workstations can offload work to a server, will drive the next generation of software.──T.R. Figure 1: DosMakeNmPipe Function int FAR PASCAL DosMakeNmPipe(name, handle, omode, pmode, size1, size2, timeout) char far * name; asciz pipe name int far * handle; place for handle to be returned unsigned int omode; DOS open mode unsigned int pmode; pipe open mode unsigned int size1; hint of outgoing buffer size unsigned int size2; hint of incoming buffer size long timeout; time out for DosWaitNmPipe Name: Asciz name of pipe. Pipes are named \PIPE\NAME. Handle: Handle of the named pipe that is created. Omode: Open mode mask as for DosOpen call. The following bits are defined: Open Mode Bits 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 O W * * * * * * I O O O * A A A I Inheritance: 0 = Spawned processes inherit the pipe handle 1 = Spawned processes don't inherit the pipe W Write-through: 0 = Write-behind to remote pipes is allowed 1 = Write-behind to remote pipes is not allowed AAA Access mode: 000 = Inbound pipe (client to server) 001 = Outbound pipe (server to client) 010 = Full-duplex pipe (server to/from client) Other values invalid Pmode: Pipe-specific mode parameters 5 4 3 2 1 0 9 8 7 6 5 4 3 2 1 0 B * * * T T R R |... I count..| B Blocking: 0 = Reads/writes block if no data available 1 = Reads/writes return immediately if no data Reads normally block until at least partial data can be returned. Writes by default block until all bytes requested have been written. Nonblocking mode (B=1) changes this behavior as follows: 1) Reads will return immediately with BytesRead = 0 if no data is available. 2) Writes will return immediately with BytesWritten = 0 if the data transfer cannot be started. Otherwise, the entire data area will be transferred. TT Type of a pipe: 00 = Pipe is a byte stream pipe 01 = Pipe is a message stream pipe All writes to message stream pipes record the length of the write along with the written data. RR Read mode 00 = Read pipe as a byte stream pipe 01 = Read pipe as a message stream Message pipes can be read as byte or message streams, depending on the setting of RR. Byte pipes can only be read as pipe streams Icount: 8-bit count to control pipe instancing. When making the first instances of a named pipe, Icount specifies how many instances can be created: 1 means that this can be the only instance (pipe is unique) and -1 means the number of instances is unlimited; 0 is a reserved value. Subsequent attempts to make a pipe will fail if the maximum number of allowed instances already exists. The Icount parameter is ignored when making other than the first instance of a pipe. When multiple instances are allowed multiple clients can simultaneously DosOpen to the same pipe name and get handles to distinct pipe instances. Size1: Hint to system, number of bytes to allocate for outgoing buffer. Size2: Hint to system, number of bytes to allocate for incoming buffer. Timeout: Default value for timeout parameter to PipeWait. This value may be set only at the creation time of the first instance of the pipe name. If at that time the value is zero, a systemwide default value (50 ms) will be chosen. ████████████████████████████████████████████████████████████████████████████ A Complete Guide to Writing Your First OS/2 Program Charles Petzold☼ For the program developer, OS/2 opens up a whole new world of capabilities and challenges, and along with them, new concepts to learn. We'll explore some of these new concepts by walking through the creation of a typical OS/2 application that demonstrates inter-process communication (IPC) and multitasking. FINDDEMO and FINDER work in conjunction to locate and list all files on a disk that fit a particular file specification, for instance, *.EXE. You can enter up to nine different file specifications at any one time, and all nine run simultaneously. Figure 1☼ shows FINDDEMO in action. FINDDEMO.EXE is the program that you execute from the OS/2 command line. FINDER.EXE must be in the current subdirectory or a subdirectory in your PATH. FINDDEMO manages the screen display and keyboard input and loads as many as nine instances of FINDER.EXE as child processes. FINDER.EXE does the actual file searching and funnels data back to FINDDEMO via a queue. Separate threads within FINDDEMO read the queue and monitor termination of the FINDER instances. In other words, there's a real party going on here. I'll have more to say about the workings of this program after we look at what's involved in coding, compiling, and linking it. If you've already done some Windows programming, don't be surprised to see some familiar things along the way──you're several steps ahead of everybody else. Protected Mode Your first OS/2 programs will probably be existing MS-DOS programs that you will port to OS/2. Most programs written in straight C with standard library functions can simply be recompiled by using the new .LIB files. However, programs with assembly language routines that make software interrupt calls, programs that use the Microsoft C int86 or intdos library functions, and programs that access memory outside the program or directly manipulate I/O ports will have to be modified somewhat. OS/2 supports nearly 200 function calls that you'll use for communicating with the operating system and the PC's hardware. Obviously, these new function calls encompass virtually everything you can do now under MS-DOS 3.x using Int 21H. However, they also include a much faster and more versatile set of character-mode video I/O calls than the calls that have previously been available from the IBM PC BIOS services. You will no longer have to directly access video display memory in order to get good character- mode performance. OS/2 also includes a set of mouse function calls and many new calls that are related to tasking and inter-process communication. DOS Function Calls In C programs, OS/2 function calls look like ordinary function or subroutine calls. The names of all the functions are capitalized, and they begin with the letters DOS, KBD (keyboard), VIO (video), or MOU (mouse). Virtually all OS/2 function calls return a zero if the function is successful and an error code otherwise. If the function must return information back to your program, one or more of the arguments will be a far pointer to your program's data segment. OS/2 uses this pointer to store the result of the function. Some OS/2 function calls also require far pointers to structures for returning more extensive information. For instance, if you want to obtain the current cursor position, the OS/2 function that you must use is VIOGETCURPOS. The first argument is a far pointer to a variable where OS/2 stores the cursor row position, the second argument is a far pointer to a variable to store the column, and the third parameter is a "video I/O handle" that currently must be set to zero. Within a C program you would define two variables for the row and column cursor positions like this: unsigned row, col ; and then call VIOGETCURPOS by using pointers to these variables: VIOGETCURPOS(&row, &col, 0) ; The C source code files for FINDDEMO, shown in Figure 2, and FINDER, shown in Figure 3 begin with the statement #include This DOSCALL.H header file contains just two lines: #include #include In the DOSCALLS.H and SUBCALLS.H header files are the declarations for all OS/2 functions. The DOS functions are declared in DOSCALLS.H, and the KBD, VIO, and MOU functions are declared in SUBCALLS.H. For instance, the VIOGETCURPOS function is declared in SUBCALLS.H as extern unsigned far pascal VIOGETCURPOS(unsigned far *, unsigned far *, unsigned) ; All OS/2 functions are declared far, which directs the compiler to generate a far inter-segment call, and pascal. The pascal keyword indicates a Pascal calling sequence──the arguments are pushed onto the stack from left to right, and the called function then adjusts the stack before returning control to the caller. Normally, C compilers generate code that pushes arguments on the stack from right to left, and the caller fixes the stack. This calling sequence permits C to support functions with a variable number of arguments. Moreover, you can usually call a C function with an incorrect number of parameters, and while the function may not work correctly, in many cases the program will not crash. A function that uses the Pascal calling sequence, however, expects to receive a fixed number of arguments. Therefore, if you get the compiler warning message "too few actual parameters" for an OS/2 function call, you should fix the code before you try to run the program. Note that the function declaration shows the first two parameters to be far pointers to unsigned integers, but the VIOGETCURPOS call used simply &row and &col. The C compiler generates far addresses without casting based on the function declaration. The DOSCALLS.H and SUBCALLS.H header files also contain structure declarations used in some of the other OS/2 function calls. For instance, FINDER.EXE uses the FileFindBuf structure in conjunction with the DOSFINDFIRST and DOSFINDNEXT function calls. FINDDEMO.C uses the ResultCodes structure for returning values from the DOSEXECPGM and DOSCWAIT functions, and the KeyData structure for returning keyboard input from KBDCHARIN. Here's what the KeyData structure declaration looks like: struct KeyData {unsigned char char_code ; unsigned char scan_code ; unsigned char status ; unsigned char nls_shift ; unsigned shift_state ; unsigned long time} ; That's a little more information than you currently get from Int 16H, isn't it? Assembly Language If you are programming in assembly language, OS/2 function calls are made by pushing arguments on the stack and calling external far functions. Near the top of your source code you specify that the OS/2 function is in a different segment: extrn VIOGETCURPOS:far The two variables to receive the row and column positions would go in your data segment: row dw ? col dw ? When you call VIOGETCURPOS, you push the arguments on the stack and make the call: push ds push offset row push ds push offset col push 0 call VIOGETCURPOS When you return from the VIOGETCURPOS call, the AX register contains the return value of the function call, and the stack pointer is the value prior to pushing the arguments on the stack. This code sample pushes immediate values on the stack. Because these instructions are supported by the 80286 but not by the 8086, you have to include the assembler directive .286c in your source code file. Or you can simply transfer the values to registers before pushing them on the stack. Linking The new LINK generates a "new-format executable." The extension is still .EXE, but the format of the file is not the same as that used in MS-DOS versions through 3.2. However, it is the same as the new-format executable currently in use for Windows applications. Among other things, the new-format executable contains an "import table" that OS/2 uses to match the far calls in your program code to the appropriate OS/2 function calls. The actual entry address of the function call routine is inserted into the code when the program is loaded into memory. (This is dynamic linking, which has appeared previously in Windows programming.) LINK requires that two libraries be explicitly listed in the command line. These are SLIBC5.LIB, which contains normal C library functions that make OS/2 functions calls, and DOSCALLS.LIB. The DOSCALLS.LIB library is an "import library." It merely provides LINK with the module names and ordinal numbers associated with the OS/2 function calls. LINK uses this information to set up the import table contained in the new-format executable. DOSCALLS.LIB performs the same function in OS/2 programs that the SLIBW.LIB library performs in Windows applications. Module Definition File The new LINK also requires that a module definition file be specified as the fifth argument on the LINK command line. The module definition file (yet another concept that Windows programmers are already familiar with) provides information about your program's segments, stack size, and local heap size. The two module definition files that describe FINDDEMO.DEF and FINDER.DEF are shown in Figures 4 and 5, respectively. The NAME line indicates the name of the module, which is usually the same as that of the program. The DESCRIPTION is generally a copyright notice imbedded in the .EXE file. The keyword PROTMODE indicates that the program runs only in protected mode. The CODE and DATA lines specify characteristics of the code and data segments in your program. The options that are shown here are normal for protected mode. Both the code and data segments are MOVABLE. This is no sweat in protected mode because a segment can be physically moved but still retain the same selector address, previously known as the segment address. Specifying the CODE segment as PURE means that the same code segment loaded in memory can be used for multiple instances of the program. The automatic data segment of a program cannot be PURE because it contains the stack and most likely contains read/write data as well. However, data segments with read-only data without relocation information can also be flagged as PURE and can be shared among multiple instances of the program. For large applications, you might divide the program into many different segments and specify characteristics of each in the module definition file. These characteristics would include the keywords PRELOAD, which means the segment is loaded into memory when the program is run, and LOADONCALL, which means the segment is loaded into memory only when a routine within it is needed. Based on a least-recently-used algorithm, OS/2 can free up memory that is occupied by code segments and later reload them when needed from the .EXE file. This facility is essentially a built-in, hassle-free overlay manager. The automatic data segment of your program contains static variables, the stack, and a local heap organized as in Figure 6. The stack is fixed to the size specified in the STACKSIZE line of the module definition file. The HEAPSIZE value is a minimum local heap size, which is the size that OS/2 sets aside when your program is first loaded into memory. If you use C memory allocation functions such as malloc to allocate more memory from your local heap than is specified by HEAPSIZE, OS/2 can expand the local heap──by physically moving the data segment in memory if necessary. MAKEing the Program A make-file for the FINDDEMO application is shown in Figure 7. You create the FINDDEMO.EXE and FINDER.EXE executables by running MAKE FINDDEMO The compile step uses several compiler switches: cl -c -G2 -Zp finddemo.c The -c switch specifies that the program is to be compiled but not linked; this is necessary because the link step requires nondefault arguments. The -G2 switch compiles code by using 80286 instructions. This is optional, but it results in a slightly smaller .EXE size and faster execution. The -Zp switch indicates that structures are "packed." Normally the C compiler aligns each element of a structure on an even address. However, some of the structures declared in DOSCALLS.H and SUBCALLS.H, such as the KeyData structure shown earlier, contain consecutive char variables. New Executable The new-format executable created by LINK is really a superset of the old- format executable. It begins with an old-format header. This makes it appear to old DOS versions (and the OS/2 compatibility box) as a normal .EXE program. However, when you link the FINDDEMO and FINDER programs, the old- format header is constructed so that the program appears to require more memory than is available. So, if you attempt to run FINDDEMO.EXE under real mode (the OS/2 compatibility box or MS-DOS 2.x or 3.x), you get the message "Program too big to fit in memory." You have some alternatives to this message. The first alternative involves the STUB option in the module definition file, which Windows programmers already know about: STUB 'oldprog.EXE' The file oldprog.EXE is an old-format executable that runs under MS-DOS 2.x or 3.x. The STUB option directs LINK to insert this old-format executable in the top of the new-format executable. Now the .EXE file appears to MS-DOS 2.x or 3.x as a regular program. When MS-DOS 2.x or 3.x loads the program, this old program is executed instead. Windows programmers generally include the line STUB 'WINSTUB.EXE' in their module definition files. The standard WINSTUB.EXE program simply displays the message "This program requires Microsoft Windows." You might want to create a similar program that would display a message such as "This program must be run in OS/2 protected mode." The second alternative is to write a real-mode version and include that in the STUB line if your program is short. If you're writing a program using straight C without any explicit OS/2 function calls, you could simply compile and link it under a different name by using the old real-mode libraries. This process creates one .EXE file that actually contains two versions of the same program: the protected-mode version runs under protected mode, and the real-mode version runs under real mode. For longer programs, or programs that use explicit OS/2 function calls, you should consider the third alternative: going family style. Family Style The OS/2 developer's kit includes a library file called API.LIB that translates many OS/2 function calls into equivalent MS-DOS Int 21H function calls. Video I/O calls are translated into equivalent BIOS calls and direct screen accesses. When you run BIND program after linking, parts of this API.LIB library are tacked onto the end of the .EXE file, and the old-format header is modified to run a loader program that is also inserted into the .EXE file. When you run the resulting .EXE file under a real-mode DOS version, the loader patches addresses into your program that call the routines within this library rather than OS/2 functions. If you run the program under OS/2 protected mode, the loader and library routines are simply discarded. Thus, you have one executable file and one program that runs under protected-mode and real-mode MS-DOS. This is called the "family API" model. Not all OS/2 functions can be converted into old MS-DOS Int 21H calls. For instance, the FINDDEMO and FINDER programs shown here use several OS/2 facilities that have no old MS-DOS equivalents. Some options of other OS/2 functions are not fully supported. However, you can determine within your program whether it is running in real mode or protected mode and have separate logic that is appropriate for each. The Workings Now that we know how to create FINDDEMO.EXE and FINDER.EXE, let's look at how they work. FINDER is a child process run by FINDDEMO. FINDER does all the work while FINDDEMO sits back and waits for the messages from FINDER that report what it has found. FINDDEMO starts off by creating a queue named \QUEUES\FINDDEMO. A queue is a linked list maintained by OS/2 that you can use to transfer data from one program to another. One program creates the queue; another program opens the same queue. The program that creates the queue──in this case, FINDDEMO──has read and write privileges. The program that then opens the existing queue──in this case, FINDER──has write privileges. Queues may be first-in- first-out (FIFO), last-in-first-out (LIFO), or based on priority; the FINDDEMO queue is FIFO. FINDDEMO also creates two additional threads of execution. These appear in the program as functions called dispthread and waitthread. These two threads run simultaneously with the main thread, sometimes called the "parent" thread. I'll explain what they do a little later. FINDDEMO then prompts for a number and a file specification from the user. For each file specification you enter, FINDDEMO executes FINDER by using the OS/2 function call DOSEXECPGM. FINDER is executed in an asynchronous mode──the DOSEXECPGM call returns immediately, and FINDDEMO can prompt for the next file specification. As many as nine instances of FINDER can be executed. Although these multiple instances use different data segments, they share the same code segment in memory. When FINDDEMO executes FINDER it passes to it the file specification, the name of the queue, and an index number from 1 to 9 that indicates the display line on which the file specification appears. Now let's look at FINDER. FINDER opens the queue and searches for files meeting the file specification over the entire disk by using a recursive function called find. You'll note that FINDER changes the subdirectory during this search. Under OS/2, each process maintains its own current subdirectory. So each instance of FINDER can change the subdirectory and not affect other instances. Nice, huh? When FINDER finds a file that fits the file specification, it allocates a small shared segment of memory using DOSALLOCSEG and copies the pathname into it. The DOSGIVESEG function obtains a new selector (segment address) appropriate for its parent FINDDEMO. (FINDER obtained the process ID of its parent when it opened the queue.) FINDER can then write this segment to the queue. Now back to FINDDEMO. During initialization, FINDDEMO created a second execution thread, the subroutine called dispthread. This thread sits in an infinite loop that starts off with a call to DOSREADQUEUE. When DOSREADQUEUE returns control to the program, it reads the contents of the shared memory segment created by FINDER and writes it to the display. The other thread created during FINDDEMO initialization is waitthread. This is also an infinite loop that waits, using DOSCWAIT, for instances of FINDER to terminate. Because DOSCWAIT returns with an error if no child processes are running, FINDDEMO first sets nine semaphores and clears one only when executing FINDER. The waitthread thread is suspended, through the use of the function DOSMUXSEMWAIT, until one of these semaphores is cleared. Another set of semaphores, which are in the array runsem, are normally cleared. They are set when an instance of FINDER is executed and cleared when the instance terminates. These semaphores are used when you terminate FINDDEMO by using the Esc key. FINDDEMO uses DOSKILLPROCESS to terminate any instances of FINDER that are still running and then waits for all of the runsem semaphores to be cleared. The New Generation I claimed earlier that FINDDEMO's use of inter-process communication and multitasking represented a "typical" OS/2 application. Obviously, you can take advantage of the larger protected mode addressing space and more versatile OS/2 API without getting into the fancy stuff. Whether you do get fancy or not, I'm sure you'll find some interesting applications for these new OS/2 capabilities. Figure 2: FINDDEMO.C /* finddemo.c -- program to demonstrate IPC and multitasking */ #include #define WORD unsigned int #define DWORD unsigned long #define NUMPROC 9 /* number of process */ #define CHILDPROG "FINDER.EXE" #define QUEUENAME "\\QUEUES\\FINDDEMO" #define THREADSTACK 1024 void far dispthread (void) ; void far waitthread (void) ; WORD queuehandle, count [NUMPROC] ; DWORD runsem [NUMPROC], waitsem [NUMPROC] ; struct ResultCodes rc [NUMPROC] ; struct { WORD count ; struct { WORD reserved ; DWORD far *sem ; } index [NUMPROC] ; } semtab ; main () { static char prompt[] = "Line number or Esc to end --> \b" ; struct KeyData keydata ; WORD index, len, i, dispID, waitID, sigaction ; DWORD sigaddr ; char dirmask [15], dispstack [THREADSTACK], waitstack [THREADSTACK] ; /* Initialize: Set up "semtab" structure for DOSMUXSEMWAIT. ---------- Disable Ctrl-Break and Ctrl-C exits. Create queue for IPC with FINDER.EXE. Create threads for messages from FINDER and waiting for FINDER terminations. Display text. */ semtab.count = NUMPROC ; for (index = 0 ; index < NUMPROC ; index++) { DOSSEMSET ((DWORD) &waitsem[index]) ; semtab.index[index].sem = &waitsem[index] ; } DOSSETSIGHANDLER (0L, &sigaddr, &sigaction, 1, 1) ; DOSSETSIGHANDLER (0L, &sigaddr, &sigaction, 1, 4) ; if (DOSCREATEQUEUE (&queuehandle, 0, QUEUENAME)) { puts ("FINDDEMO: Cannot create new queue") ; DOSEXIT (1, 1) ; } if (DOSCREATETHREAD (dispthread, &dispID, dispstack + THREADSTACK) || DOSCREATETHREAD (waitthread, &waitID, waitstack + THREADSTACK)) { puts ("FINDDEMO: Cannot create threads") ; DOSEXIT (1, 1) ; } displayheadings () ; /* Main Loop: Display prompt and read keyboard. --------- Execute FINDER.EXE. */ do { VIOSETCURPOS (18, 0, 0) ; VIOWRTTTY (prompt, sizeof prompt - 1, 0) ; KBDCHARIN (&keydata, 0, 0) ; index = keydata.char_code - '1' ; if (index <= NUMPROC && rc[index].TermCode_PID == 0) { VIOWRTTTY (&keydata.char_code, 1, 0) ; VIOWRTNCHAR (" ", 77, 7 + index, 3, 0) ; do { VIOSETCURPOS (7 + index, 3, 0) ; len = 13 ; KBDSTRINGIN (dirmask, &len, 0, 0) ; } while (len == 0) ; dirmask [len] = '\0' ; executeprogram (index, dirmask) ; } } while (keydata.char_code != 27) ; /* Clean-up: Kill all existing FINDER.EXE processes. -------- Wait for processes to terminate. Close the queue and exit. */ for (index = 0 ; index < NUMPROC ; index++) if (rc[index].TermCode_PID) DOSKILLPROCESS (0, rc[index].TermCode_PID) ; for (index = 0 ; index < NUMPROC ; index++) DOSSEMWAIT ((DWORD) &runsem [index], -1L) ; DOSCLOSEQUEUE (queuehandle) ; DOSEXIT (1, 0) ; } displayheadings () { static char heading [] = "286DOS File Finder Demo Program", colheads [] = "Dir Mask Status Files", colunder [] = "-------- ------ -----" ; char buffer [5] ; WORD row, col, i, len ; VIOGETCURPOS (&row, &col, 0) ; /* get current attr */ VIOWRTTTY (" ", 1, 0) ; len = 2 ; VIOREADCELLSTR (buffer, &len, row, col, 0) ; VIOSCROLLUP (0, 0, -1, -1, -1, buffer, 0) ; /* clear screen */ len = sizeof heading - 1 ; col = (80 - len) / 2 ; VIOWRTCHARSTR (heading, len, 1, col, 0) ; /* heading */ VIOWRTNCHAR ("\xC6", 1, 2, col - 1, 0) ; /* underline */ VIOWRTNCHAR ("\xCD", len, 2, col, 0) ; VIOWRTNCHAR ("\xB5", 1, 2, col + len, 0) ; VIOWRTCHARSTR (colheads, sizeof colheads - 1, 5, 3, 0) ; VIOWRTCHARSTR (colunder, sizeof colunder - 1, 6, 3, 0) ; for (i = 0 ; i < NUMPROC ; i++) { /* numbers */ sprintf (buffer, "%d.", i + 1) ; VIOWRTCHARSTR (buffer, 2, 7 + i, 0, 0) ; } } executeprogram (index, dirmask) WORD index ; char *dirmask ; { char objbuf [32] ; char args [128] ; strcat (strcpy (args, CHILDPROG), " ") ; /* construct args */ strcat (strcat (args, dirmask), " ") ; strcat (strcat (args, QUEUENAME), " ") ; itoa (index, args + strlen (args), 10) ; count [index] = 0 ; /* initialize count */ if (DOSEXECPGM (objbuf, 32, 2, args, 0, &rc[index], CHILDPROG)) { puts ("FINDDEMO: Can't run FINDER.EXE") ; DOSEXIT (1, 1) ; } VIOWRTCHARSTR ("Running", 7, index + 7, 16, 0) ;/* now executing */ DOSSEMSET ((DWORD) &runsem [index]) ; DOSSEMCLEAR ((DWORD) &waitsem[index]) ; } void far dispthread () /* thread to read messages from FINDER */ { /* and display filenames. */ DWORD request ; WORD len, index, i ; char far *farptr ; char priority, pathname [80], buffer [64] ; while (1) { DOSREADQUEUE (queuehandle, &request, &len, &(DWORD)farptr, 0, 0, &priority, 0L) ; i = 0 ; while (pathname [i++] = *farptr++) ; index = (WORD) (request >> 16) ; count [index] += len > 0 ; sprintf (buffer, "%5d %-48.48s", count [index], pathname) ; VIOWRTCHARSTR (buffer, 56, 7 + index, 24, 0) ; DOSFREESEG ((WORD) ((DWORD) farptr >> 16)) ; } } void far waitthread () /* thread to wait for FINDER terminations */ { WORD index, PID ; struct ResultCodes rescode ; while (1) { DOSMUXSEMWAIT (&index, (WORD far *) &semtab, -1L) ; DOSCWAIT (0, 0, &rescode, &PID, 0) ; for (index = 0 ; index < NUMPROC ; index++) /* find index */ if (PID == rc[index].TermCode_PID) break ; VIOWRTCHARSTR (rescode.TermCode_PID ? "Halted " : "Done ", 7, index + 7, 16, 0) ; rc[index].TermCode_PID = 0 ; DOSSEMCLEAR ((DWORD) &runsem [index]) ; DOSSEMSET ((DWORD) &waitsem[index]) ; } } Figure 3: FINDER.C /* finder.c -- child process of finddemo */ #include unsigned index ; unsigned queuehandle ; unsigned parentPID ; main (argc, argv) int argc ; char *argv [] ; { if (argc < 4) { puts ("Run FINDDEMO rather than FINDER") ; DOSEXIT (1, 1) ; } if (DOSOPENQUEUE (&parentPID, &queuehandle, argv [2])) { puts ("FINDER: Cannot open queue") ; DOSEXIT (1, 1) ; } index = atoi (argv [3]) ; writequeue ("") ; chdir ("\\") ; find (argv [1]) ; writequeue ("") ; DOSCLOSEQUEUE (queuehandle) ; DOSEXIT (1, 0) ; } find (searchstr) char *searchstr ; { struct FileFindBuf ffb ; char cwd [81], pathname [100] ; unsigned handle = 0xFFFF, num = 1 ; if (cwd [strlen (getcwd (cwd, 80)) - 1] != '\\') strcat (cwd, "\\") ; DOSFINDFIRST (searchstr, &handle, 7, &ffb, sizeof ffb, &num, 0L) ; while (num) { writequeue (strcat (strcpy (pathname, cwd), ffb.file_name)) ; DOSFINDNEXT (handle, &ffb, sizeof ffb, &num) ; } DOSFINDCLOSE (handle) ; handle = 0xFFFF ; num = 1 ; DOSFINDFIRST ("*.*", &handle, 0x17, &ffb, sizeof ffb, &num, 0L) ; while (num) { if (ffb.attributes & 0x10 && ffb.file_name [0] != '.') { chdir (ffb.file_name) ; find (searchstr) ; chdir ("..") ; } DOSFINDNEXT (handle, &ffb, sizeof ffb, &num) ; } DOSFINDCLOSE (handle) ; } writequeue (str) char *str ; { unsigned selector, parentselector ; char far *farptr ; int len = strlen (str) ; DOSALLOCSEG (len + 1, &selector, 1) ; farptr = (char far *) (((unsigned long) selector) << 16) ; while (*farptr++ = *str++) ; DOSGIVESEG (selector, parentPID, &parentselector) ; DOSFREESEG (selector) ; farptr = (char far *) (((unsigned long) parentselector) << 16) ; DOSWRITEQUEUE (queuehandle, index, len, farptr, 0) ; } Figure 4: FINDDEMO.DEF NAME FINDDEMO DESCRIPTION 'File Finder Demonstration Program' PROTMODE DATA MOVABLE CODE MOVABLE PURE HEAPSIZE 2048 STACKSIZE 4096 Figure 5: FINDER.DEF NAME FINDER DESCRIPTION 'File Finder Module for FINDDEMO' PROTMODE DATA MOVABLE CODE MOVABLE PURE HEAPSIZE 2048 STACKSIZE 8192 Figure 6: Automatic data segment containing static variables, the stack, and a local heap. ┌───────────────────────────┐▄ │ ╔═══════════════╗ │ Local Heap ║ High Memory ║ │ ╚═══════════════╝ └───────────────────────────┘█ ████████████████████████████ ┌───────────────────────────┐ │ │█ │ Stack │█ │ │█ └───────────────────────────┘█ ████████████████████████████ ┌───────────────────────────┐▄ │ ╔═══════════════╗ │ Static Variables ║ Low Memory ║ │ ╚═══════════════╝ └───────────────────────────┘█ ████████████████████████████ Figure 7: Make-file for FINDDEMO.EXE and FINDER.EXE finddemo.obj : finddemo. cl -c -G2 -Zp finddemo.c finddemo.exe : finddemo.obj finddemo.def link finddemo, /align:16, /map, doscalls slibc5, finddemo.def finder.obj : findef.c cl -c -G2 -Zp finder.c finder.exe : finder.obj finder.def link finder, /align:16, /map, doscalls slibc5, finder.def ████████████████████████████████████████████████████████████████████████████ An Interview with Gordon Letwin: OS/2: Turning Off the Car to Change Gears ─────────────────────────────────────────────────────────────────────────── Also see the related article: Gordon Letwin: The Challenge of the 286 Speaks His Language ─────────────────────────────────────────────────────────────────────────── Lori Valigra☼ The development of Microsoft's multitasking operating system has taken many twists and turns since Microsoft began working on it three years ago. The many stages it has gone through so far have resulted from working with the schizophrenic Intel 80286 chip, which can run in either of two incompatible modes: protected (286) mode, and unprotected (8086 or "real") mode. The main problem has been to get an operating system which runs both protected- and real-mode programs. Microsoft solved the problem with mode switching──a feature which essentially works by mimicking the act of turning off the computer and restarting it, similar to turning off a car in order to switch gears. The new operating system began as an ambitious project to produce an operating system for Intel 80286-based computers running a mixture of office automation applications. This new operating system was to be capable of multitasking and compatible with the protected 286 mode. But as the project got too unwieldly, Microsoft split it up in early 1985, announcing a version called DOS 4. This version incorporates multitasking but does not include protected mode features. The DOS 5 project, as it was called, is intended to support the protected mode and add other features. OS/2, as the project has finally been named, was easily the most difficult and challenging program written at Microsoft to date, with more than 350,000 lines of code. While Microsoft generally feels that the most efficient projects are those staffed by one person, the complexity and time constraints of the OS/2 project required that the company assign more than 35 programmers to it. Managing the project──dividing the programmers into teams, dividing the project among them, and coordinating the various teams' modifications to the program's code──became an enormous challenge. Adding to the challenge was the joint development of the product with IBM, a company whose culture and methodology are quite different from Microsoft's. Microsoft Systems Journal talked with the project's chief architect, Gordon Letwin, to learn more about the planning, development, and cooperative effort that went into the development of OS/2. MSJ: What was the goal of the OS/2 project? Letwin: We tried to create the underpinnings for the ideal office automation operating system. In our opinion, that operating system would allow applications direct access to high-bandwidth peripherals, offer device- independent graphics drivers, provide multitasking without performance degradation (compared with a single-tasking system), run programs in both the protected 286 and real 8086 modes of the 286 processor, and provide a protected environment to assure system stability. I think we've done a pretty good job. MSJ: Microsoft has been developing multitasking operating systems for three years now, beginning with DOS 4. What was the purpose of that operating system, and where does it stand now that OS/2 is out? Letwin: DOS 4 was the first product to result from Microsoft's multitasking DOS effort. We began it even before IBM introduced the PC AT. It was an ambitious product that was originally to include a protected mode with mode switching abilities so it could run on the 8086 or the 286. A general-purpose multitasking system needs to run in both modes: the unprotected 8086 mode so that we can run existing DOS applications, and the protected 286 mode so that we can multitask arbitrary, unrelated applications. But the architecture of the 286 caused some delays. Although we knew the project would be difficult, it was only after we'd gotten deeply into it that we realized just how difficult it would be. As a result, DOS 4 became too complicated for our schedules. Because of the pressure of customer demand as well as that of previous commitments, we broke the project into two parts. DOS 4 runs only in real mode and provides multitasking only for specialized applications. DOS 5, which has now been released as OS/2, includes the protected mode and other features. DOS 4 was delivered in the last half of 1986 and is being sold in special application environments, primarily in Europe. It is a specialized product that can share the market with OS/2, because it runs on 8086 hardware, while OS/2 requires a 286. The move from DOS 4 to OS/2 was a gradual evolutionary process. OS/2 is by far the most complex and sophisticated operating system project we've worked on. To offer multitasking in a protected environment, we developed it jointly with IBM to get around the constraints of the 286. MSJ: How does OS/2 compare with earlier multitasking operating systems like Concurrent CP/M? Letwin: Unlike OS/2, Concurrent CP/M has a major problem. It serializes access to the file system. When multitasking you need to make the operating system features such as the file system accessible to multiple programs at a time. But Concurrent CP/M didn't do this. It only allowed one program to call the file system at a time. So in a real life situation with lots of I/O, Concurrent CP/M was too slow. MSJ: What are some of the features of OS/2 that you consider particularly neat? Letwin: One is the seamless system service interface, which means that system services don't need to be in the kernel. Some can be provided by subroutine libraries and others by system processes. Many services can be upgraded piecemeal, in the field, without changing the whole operating system. This is accomplished by a technique called "dynamic linking," which allows external procedure calls to be linked up at execution time, rather than link time. This same mechanism is used as the standard system interface between applications, utility packages, etc. Another exceptional feature is direct hardware access, which allows an application to write directly to the hardware. For example, it will do special graphics on a screen. MSJ: How did you get around the limitations of the 286 chip? Letwin: The 286 is designed to run in only one mode, either real mode or protected mode. Since we knew we needed to support programs written using protected mode, a group of us, including Bill Gates and some project engineers, brainstormed to try to figure out how to run 8086 mode applications from within protected 286 mode. One option was "real mode emulation," where we could emulate the operation of 8086 real mode by doing a lot of special things to the protected mode segment tables. IBM was examining this technique independently. We thought emulation would be too slow, since the 286 would run at one-third the speed of an 8086. IBM's research later showed this to be the case. Soon thereafter, Intel began shipping a chip with a bug in it that would stop the technique anyway, so we tried another approach: mode switching. The 286 chip switches from real mode to protected mode very easily, but it was not designed to switch back from protected mode to real mode at all. You could only switch back to real mode by causing a full system reset. The idea of doing a full system reset while running a multitasking system was very radical. IBM was aware of the mode switching technique itself and had the capability for it in the AT BIOS for use during power-up diagnostics and for special block copies from high memory. However, this feature was never intended for use during full system operation. What we were thinking of was similar to switching gears in a car by turning the motor off. The idea was to master-clear the processor and restart it. The question was, would this interfere with DMA transfers that might be happening at the same time, lose pending interrupts, or fail in some other way? The IBM ROM code was designed to "switch gears by turning off the car" with the car standing still. We wanted to switch gears with the car traveling at highway speeds, and without causing a jolt. Working together with IBM, we set up an experiment to see if it would work, and it did. However, it took us months to convince everyone involved that the technique was feasible. The first time we presented the idea to IBM Boca, they gave us a funny look and suggested that perhaps we'd had a bit too much sun. There is one difficulty with mode switching, however. Because the designers thought that it would be an infrequent operation, done primarily during the power-on diagnostics, they implemented it in a fashion that saved some components but produced a very slow mode switch: typically 1,000 microseconds, round trip. During this 1 millisecond interval the system cannot respond to interrupts. Any interrupts that arrive then are processed after the mode switch. This delay would normally be a problem only for high- speed communications at 96K bps or faster while you are simultaneously interacting with a real-mode program. You may experience data overruns while the real-mode screen group is on the display. Mode switching will affect any program that requires a very high-speed interrupt service rate faster than 500 Hz. If the communications protocols on your system can handle missing characters by asking for a retransmit, and most can, then communications will be okay. MSJ: Why is protection so important in OS/2? Letwin: We knew we needed a multitasking system to accomplish our goals, and we knew we needed to have it protected. If you run a multiprocessor system, software can accidentally read or change other programs or data if the memory is unprotected. You can't stop it and you can't tell it happened. The system can crash or you could corrupt data. Even in an unprotected DOS 3 system you can have problems with running programs simultaneously. SideKick, for example, loads into memory and terminates itself. It keeps memory in use, but the system doesn't know what is in that memory location. If you load several such programs at the same time and they conflict on how they use memory or interrupt vectors, the software will not work correctly, or the system will crash. So in an unprotected environment the system is always vulnerable to flaky software or conflicts between software products from different vendors. Of even greater importance than this is the fact that we needed to use protected mode to allow programs to access more than 640K bytes of memory. The 640K limit in earlier versions of DOS is a result of the addressing limitations of real mode. Protected mode programs are released from this restriction, and can use many megabytes of memory. As memory costs continue to drop and memory consumption by applications rises, it has become unacceptable to be restricted to the 640K that real mode provides. MSJ: How did you plan such an ambitious project, both conceptually and in getting the needed resources? Letwin: The first step was to decide what we wanted the system to do. Then, when we discovered how many of those capabilities we could implement, and by when, we could revise our goals. As a result, the initial planning process was iterative. We needed to have multitasking and to execute existing DOS 3 programs. While there were still only a few engineers on the project, we thought about techniques such as mode switching and when necessary, prototyped some code. As we got a better handle on what we could do, and how long it would take, we met again with Bill Gates and revised our goals. The tone of the first phase was as ambitious as possible, and we listed all the things we thought we could do. The second phase involved determining which of those features were feasible in the required time frame. As soon as the general scope of the project was determined, the engineers met in groups of two and three to lay out further details of the architecture of the system. After the initial meetings came the joint design work. Engineering then finalized the goals. Because of the scope of the project, this design phase took a few months. Next was the sign-up phase, a tradition in the computer industry, where we explained the excitement, the importance, and the tight schedule of the project, then let everyone "sign up" for a long stretch of hard, focused work. You can't force someone to excel; you've got to provide the opportunity and the encouragement and then let them excel. Then the actual work began. We decided on the key systems and the key characteristics of the product. Next came the design sessions for each of the key systems. About a half dozen people were involved, typically three from Microsoft and three from IBM. During the coding months, my job was to understand fully all of the parts of the system and to make sure that everthing worked toward achieving our overall system goals. In a project this large, with this many people working on it, it's easy for design elements to diverge. One project member might think that speed is more important than code size, and write his code that way. Another might think the opposite and write his code the other way. The result would be that neither goal is achieved. I participated in the design of each component to insure that the goals and design philosophies of the system's architecture were consistent throughout the code. On most projects, this role can be fulfilled on a "part time" basis, leaving time for the chief designer to also get to write some code. Unfortunately, on the OS/2 project this position required a full time effort, leaving me little opportunity to write any real code. MSJ: How did you divide up tasks on the project? Letwin: The most efficient programming is done by one person. It's a classic saying in the industry that "if one person can do it in one year, two people can do it in two years." But in a project like OS/2, although one person might be able to do it in 8 man years, you can't afford to wait 8 years, so you have to make it a multi-person project and have it take 30 man years. In order to reduce the inefficiency introduced by having multiple people on a project, it's important to break the work up properly, and to have each piece of work be a one man project. This project had about 35 people on it. The programmers worked on individual parts of the code, but they were organized into teams with a maximum of eight people each to facilitate coordinating their work. There was a test team, two teams of developers each headed by a senior engineer, a build team with four people, a test team with five people, and myself as the chief architect. This project was so complicated, especially since development work was simultaneously taking place at IBM in Boca Raton, Florida, that we needed a build team just to coordinate the sources, build the executable binaries, and otherwise bring organization to the physical aspects of software development. The test team, as its name implies, was responsible for the development of test cases, bug tracking, and performance measurement. MSJ: What was the most difficult part of managing the project? Letwin: This project's very large staff added to the complexity of integrating changes made at the IBM locations and timing the development between the two sites. We had to be especially careful that any changes made didn't affect other parts of the project. In the source code control system, for example, we split the source into about 200 modules, with modification tracking on each. We had a rule that you couldn't add a change to a module until you'd proven that the change wouldn't break anything. That way we retained a totally stable version. Without this rule, each engineer could have spent days looking for problems that were, in fact, introduced by another engineer. MSJ: IBM and Microsoft, to put it mildly, have very different corporate cultures and philosophies. How did you mesh the two environments during the development of OS/2? Letwin: First, whenever we arrived in Boca Raton, we rushed into the bathroom and changed into suits. But seriously, there were some times when differences of opinions were frustrating. Normally each company builds its own products and calls all the shots. But in a joint development, compromises were necessary. IBM engineers were slowed down by a set of rules which are intended to bring stability to large IBM projects. This naturally caused some conflicts. IBM-ers undoubtedly felt that Microsoft engineers were "wild and crazy guys." MSJ: What parts of OS/2 would you do differently if you had the choice? Letwin: A year ago it became clear we weren't going to meet our schedules, and we had to go through a simplification procedure. For example, we took out named pipes, a form of inter-process communication to send information to another program (now part of the OS/2 LAN Manager). We'll put that in another release of the product. Installable file systems were another victim. As a result, the hard disk file system still doesn't perform as well as we'd all like. These features will be added later. Also, we now know the parts of the system which turned out to be bigger or slower than they should, or which in some other way were a bit disappointing. Naturally we'd give them additional attention. At the end of any project, there are always a few things you'd like to improve. And we will, in the next release. MSJ: How does it feel to give up your product when it's ready to go to market? Letwin: At first, I felt relief when we got the product through testing and it went out the door. The last 20 percent is hard work. You just get tired of the product. The exciting stuff takes place in the earlier stages of the project. The last days are dealing with problem after problem──fixing bugs, making it smaller, making it faster, etc. You've worked on it so much there dosen't seem to be much change over the last few months. You're tired and all you do is deal with problems. In my opinion, it's the ability to keep working hard during the last part of a long project that separates the great people from the average. In any case, when you finally move on to something else, you feel a mixture of relief that it's completed, excitement about your new project, and anxiety about how well your efforts will be received. MSJ: What's your philosophy of programming? Letwin: Software is special at our level. Computer programming is 20 percent science, 60 percent engineering, and 20 percent art. The "art" element is the hardest to achieve, and it's the hardest to explain. When engineers talk about the "elegance" of their work, people usually think that such elegance is non-functional: the engineers are just indulging themselves. This might be the case in mechanical or civil engineering since an elegant bridge carrys no more load than an inelegant one. But it isn't so in advanced software development because mechanical engineering deals with physical objects and software engineering deals with information. Physical objects have properties independent of human perception, but information doesn't. In other words, although a bridge is equally functional whether or not it's elegant, information is much more useful when it's organized in a clear, flexible and useable form──a form a programmer would call "elegant." Bridge design is organized to meet just two goals──buildability and final function. Software is designed to meet those goals plus one more. It has to have minimal intellectual complexity and maximum flexibility so that it can be understood by the programmers who will maintain and upgrade it in the future. ─────────────────────────────────────────────────────────────────────────── GORDON LETWIN: THE CHALLENGE OF THE 286 SPEAKS HIS LANGUAGE ─────────────────────────────────────────────────────────────────────────── What attracted 34-year-old Gordon Letwin to computers in the late 1960s has also kept him in the forefront of programming today: an insatiable drive to overcome technical obstacles. "I'd read about computers in the late 1960s, that they were supposed to be super complicated. Computers held an abstract mystique. And I wanted to understand how that stuff worked," he says. That was a tough goal at a time when the cheapest computers cost $100,000 and were locked behind glass. Letwin taught himself FORTRAN from a manual, without access to a computer to try it on. While in high school he took some courses at a small private technical college to get free time on the school's computer. Letwin studied physics at Purdue, but found he was more interested in getting computer time than in studying physics. Whenever he could, he'd spend his day breaking through the computer's protective environments instead of studying physics. "Saying that there's something that I can't understand, or can't access because I'm not initiated into some mystery is like waving a flag in front of a bull," he says. He finally solved this conflict by changing his major to computer science. A large part of the enjoyment in computing for Letwin is getting immediate results. He likens computers to pinball machines. "You get positive and negative reinforcement right away." And, being "innately lazy," he likes the idea of computers doing work for him. Upon getting his Bachelor's and Master's degrees in computer science from Purdue, Letwin went to work for Wintek, a company run by one of his professors. He ended up bidding on a contract to supply Heathkit with a BASIC interpreter, assembler, and editor. He later moved to Michigan and went to work at Heath but found he didn't agree with its management's direction. "I wasn't happy with the direction of the company, which was run by `suits' who didn't know computers well but insisted on making the technical decisions anyway." When his concerns with Heath's approach at that time weren't improving, he signed on with Microsoft, then in Albuquerque, New Mexico. "I came here because of Bill Gates," says Letwin, who met Microsoft's chairman during sales meetings at Heath. Letwin became the first new employee when the firm relocated to Seattle about eight years ago. He started working on a BASIC compiler, then a Pascal compiler. When he became tired of being a "compiler compiler," he took advantage of his operating system experience to switch to the newly formed operating systems group, starting work on the IBM PC. "We thought the 8086 chip was the first processor powerful enough for general purpose desktop use. We had a vision of an office of the future. We knew that to realize this vision, we'd need a powerful operating system, much more sophisticated then those which were available at that time. Since the only way to insure its existence was to write it ourselves, we got into the operating system business." The widespread acceptance of MS-DOS makes that statement Microsoft's platform for its development of the single-user office automation operating system ████████████████████████████████████████████████████████████████████████████ A Simple Windows Application for Custom Color Mixing Charles Petzold☼ COLORSCR ("Color Scroll"), is a program for Microsoft Windows that displays three scroll bars in the left half of the client area, which are labeled "Red," "Green," and "Blue," as shown in Figure 1☼. As you scroll the scroll bars, the right half of the client area will change to the composite color that is produced by the mix of the three primary colors. The numeric values of the three primary colors are displayed under the three scroll bars. You can scroll the scroll bars by using either the mouse or the keyboard. COLORSCR works best when Windows is installed to use a color display, such as an Enhanced Graphics Adapter connected to a color monitor, but you can also run the program on a monochrome display. You can use COLORSCR as a development tool in experimenting with color and choosing attractive colors for your own Windows programs. But COLORSCR is most interesting for what it doesn't do. For instance, if you've done some Windows programming, you'll probably assume that the Red, Green, and Blue labels on top of the scroll bars and the color values at the bottom are displayed to the client area by using the TextOut function. However, COLORSCR does not use TextOut. Also, you'll probably assume that the colored rectangle at the right half of the client area is drawn by using the Rectangle or FillRect function. No, COLORSCR doesn't use these functions either, or any of the other GDI drawing functions. When COLORSCR is made into an icon, the entire surface of the icon is painted with the selected color. You might think that COLORSCR traps WM_SIZE messages to check if it's being iconed and then paints the client area differently if it is. Wrong again. COLORSCR does not know when it's an icon and doesn't have separate icon logic. In fact, COLORSCR doesn't process WM_PAINT messages and doesn't directly display anything to its client area. At some point the COLORSCR program does obtain a handle to a display context, but only for the purpose of determining the height of a character in the default system font. In short, COLORSCR is one of the laziest Windows programs ever. How can it afford to do so little? Because COLORSCR puts its children to work. Child Windows Child windows are separate windows that can be overlaid on the client area of a tiled or pop-up window. They usually have their own window functions separate from the parent window function. In their simplest form, child windows can simply divide the client area of a window into smaller rectangular windows, each with its own window function. For instance, the Windows MS-DOS Executive uses three child windows to divide its client area into sections for the drive icons, the current subdirectory path, and the file list. Three different window functions process messages from the three child windows. This simplifies painting and mouse processing in the MS-DOS Executive because each child window has its own client area coordinates. One type of child window is called a child window control, which usually sends messages back to its parent window (the tiled or pop-up window on which the child control appears) based on mouse or keyboard input to the control. Windows contains several predefined child window controls, which take the form of buttons, check boxes, edit boxes, list boxes, text strings, and scroll bars. Child window controls are most often used in dialog boxes; in such instances, the placement and size of the child window controls are defined in a dialog box template contained in the program's resource script. When you create a dialog box template, you can either calculate the position and sizes manually or you can use the DIALOG program included with the Windows Software Development Kit 1.03 (see "Latest Dialog Editor Speeds Windows Applications Development," MSJ, Vol. 1 No. 1). However, you can also use predefined child window controls on the surface of a tiled or pop-up window's client area. You create each child window with a CreateWindow call and adjust the position and size of the child windows with calls to MoveWindow. COLORSCR uses ten predefined child window controls. The color that appears on the right half of the window is actually the background color of the tiled window, as shown in Figure 2☼. On the left half of the client area there is a "static" child window called SS_WHITERECT (white rectangle) that blocks out half the client area (see Figure 3☼). The three scroll bars are child window controls with the style SBS_VERT (see Figure 4☼) placed on top of the SS_WHITERECT child. Six more static child windows of style SS_CENTER (centered text) provide the labels and the color values (see Figure 5☼). Raising the Children The COLORSCR.C source code, the COLORSCR.DEF module definition file, and the COLORSCR make-file are shown in Figures 6, 7, and 8, respectively. If you have the Microsoft C Compiler 4.0 and the Windows Software Development Kit installed on your hard disk, you will be able to create COLORSCR.EXE by executing: MAKE COLORSCR COLORSCR creates its normal tiled window and the ten child windows within the WinMain function through the use of the Windows CreateWindow call. The first parameter to the CreateWindow call indicates the window class of the window. The tiled window uses the window class called "ColorScr," which is defined and registered during WinMain initialization; this is perfectly normal. The child windows use classes called "static" (for the white rectangle and the text) and "scrollbar," which are the predefined child window classes. The window functions for these child windows are contained within Windows. For a tiled window, the second parameter to the CreateWindow call is the text that appears in the window's caption bar. For the six child windows that display text, this parameter is the text displayed in the child window. For the rectangle and scroll bars, the text is ignored. The window style parameter for all ten child windows includes the WS_CHILD indentifier. The window style for the three scroll bars includes the SBS_VERT identifier, indicating a vertical scroll bar. For the static child windows, the SS_CENTER identifier indicates centered text, and the SS_WHITERECT identifier indicates a white rectangle. The fourth through seventh parameters to the CreateWindow call normally indicate a position and size of the child window. The position is relative to the upper left-hand corner of the client area of the parent window. These values are all set to 0 initially because the placement and sizing depend on the size of the client area, which is not yet known. For child windows, the eighth parameter is the window handle of the parent, which is simply the hWnd value returned from the first CreateWindow call. The following parameter is a child ID that uniquely identifies the child window. We'll use this child ID later on. COLORSCR's window function (WndProc) resizes all ten child windows when it receives a WM_SIZE message. The formulas are based upon xClient, the width of the client area, yClient, the height of the client area, and yChar, the height of a character. Whenever you resize the COLORSCR window, the sizes of the child windows change proportionally. This is particularly interesting for the scroll bars. Depending on the dimensions of COLORSCR's window, the scroll bars can be long and thin or short and stubby (see Figure 9☼). They may look a little peculiar, but they work just fine. Scroll Bar Messages Child window controls that take the form of buttons, edit boxes, list boxes, and scroll bars pass messages back to the window function of the parent window. Buttons, edit boxes, and list boxes will post WM_COMMAND messages to the parent window. Scroll bar controls, however, will use the messages WM_VSCROLL (to scroll vertically) and WM_HSCROLL (for scrolling horizontally) to indicate that the scroll bar has been clicked with the mouse or the scroll bar thumb is being dragged. The first step in using the scroll bars is to set a range for the scroll bar, which is done in WinMain. Since the primary color values range from 0 to 255, the range is set accordingly: SetScrollRange (hChScrol[n],SB_CTL,0, 255, FALSE) ; When the WndProc window function receives a WM_VSCROLL message, the high word of the lParam parameter is the child window ID number, which is the number specified in the ninth parameter to the CreateWindow call. For the three scroll bars, we have conveniently set that ID to a 0, 1, and 2. That tells WndProc which scroll bar is generating the message: n = GetWindowWord(HIWORD(lParam),GWW_ID) ; Because the handles to the child windows had been saved in arrays when the windows were created, WndProc can set the new value of the appropriate scroll bar by using the SetScrollPos call: SetScrollPos(hChScrol[n],SB_CTL,color[n],TRUE) ; and it can change the text of the child window at the bottom of the scroll bar: SetWindowText(hChValue[n],itoa(color[n],szbuffer,10)) ; Keyboard Interface Scroll bar controls can also process keystrokes, but only if they have the input focus. (Because only one window can receive keyboard input at any time, the window that receives the keyboard input is referred to as the window with the input focus.) The keyboard cursor keys are translated into the scroll bar messages: Scroll Bar Message Cursor Key wParam Value Home SB_TOP End SB_BOTTOM PgUp SB_PAGEUP PgDown SB_PAGEDOWN Left-arrow SB_LINEUP Up-arrow SB_LINEUP Right-arrow SB_LINEDOWN Down-arrow SB_LINEDOWN In fact, the SB_TOP and SB_BOTTOM scroll messages are never generated when using the mouse on the scroll bar, only when using the keyboard. The Input Focus If you want a scroll bar control to obtain the input focus when the scroll bar is clicked with the mouse, you must include the WS_TABSTOP identifier in the window class parameter of the CreateWindow call. When a scroll bar has the input focus, a blinking gray block is displayed on the scroll bar thumb. However, to provide a full keyboard interface to the scroll bars, some more work is necessary. First, the WndProc window function must specifically give a scroll bar the input focus. It does this by processing the WM_FOCUS message, which the parent tiled window receives when the parent window obtains the input focus. WndProc simply sets the input focus to one of the scroll bars: SetFocus(hChScrol[nFocus]); But you also need some way to get from one scroll bar to another by using the keyboard, preferably with the Tab key. This is more difficult, because once a scroll bar has the input focus it processes all keystrokes. The scroll bar only cares about the cursor keys and ignores the Tab key. Window Subclassing Adding a facility to COLORSCR in order to jump from one scroll bar to another using the Tab key requires window subclassing. The window function for the scroll bar controls is somewhere inside Windows, but the address of this window function can be obtained by a call to GetWindowLong using the GWL_WNDPROC identifier as a parameter. Moreover, you can set a new window function for the scroll bars by calling SetWindowLong. This facility allows you to hook into the existing scroll bar window function, process some messages within your own program, and pass all the other messages to the old window function. The window function that does preliminary scroll bar message processing in COLORSCR is called ScrollProc and is found toward the end of the COLORSCR.C listing. Since ScrollProc is a function within COLORSCR that is called by Microsoft Windows, it must be defined as FAR PASCAL and it must be listed in the EXPORTS in the COLORSCR.DEF module definition file. First, to ensure that ScrollProc accesses the proper data segment, you must obtain a far address for the function with MakeProcInstance: lpfnScrollProc = MakeProcInstance((FARPROC)ScrollProc,hInstance); For each of the three scroll bars, COLORSCR uses GetWindowLong to obtain and save the address of the existing scroll bar window function: lpfnOldScr[n] =(FARPROC)GetWindowLong(hChScrol[n],GWL_WNDPROC) ; Next, it sets the new scroll bar window function: SetWindowLong(hChScrol[n],GWL_WNDPROC,(LONG)lpfnScrolProc) ; Now, the function ScrollProc gets first dibs on all messages that Windows sends to the scroll bar window function for the three scroll bars in COLORSCR (but not, of course, scroll bars in other programs). The ScrollProc window function simply changes the input focus to the next, or previous, scroll bar when it receives a Tab or Shift-Tab keystroke. It calls the old scroll bar window function with CallWindowProc. Use of Color The Windows functions that require (or return) a color value use an unsigned long (32-bit) integer, where the lowest three bytes specify red, green, and blue values ranging from 0 through 255 (see Figure 10). This results in a potential 224 (about 16 million) possible colors. This unsigned long is often referred to in the Windows documentation as rgbColor. The WINDOWS.H header file provides several macros for working with rgbColor values. The RGB macro in WINDOWS.H takes three arguments representing red, green, and blue values and then sticks the bytes together in order to form an unsigned long: #define RGB(r,g,b)(((DWORD)(b << 8 | g) = << 8)| r) Thus, the value RGB (255, 0, 255) is really 0x00FF00FF, an rgbColor value for magenta. When all three arguments are set to 0, the rgbColor is black; all three arguments set to 255 yields white. C mavens will justifiably recoil upon seeing this RGB macro. The b, g, and r identifiers within the definition should be surrounded by parentheses to avoid errors when one of the parameters is an expression involving operators of higher precedence than << and |. You should watch out when you use expressions for the b, g, and r values. The GetRValue, GetGValue, and GetBValue macros extract the unsigned character primary color values from an unsigned long rgbColor value. These macros are not used in COLORSCR, but they sometimes come in handy when you are using Windows functions that will return rgbColor values to your program. The Windows display driver for the Enhanced Graphics Adapter attached to a color display uses only three of the four color planes of the EGA board, so only eight pure colors are possible. (In COLORSCR, the eight pure colors result when all scroll bars are either at the top or bottom positions.) Windows can display additional colors on the EGA by dithering, the creation of a pixel pattern that combines pixels of different pure colors. You'll find from experimenting on an EGA that the dithering pattern remains the same for every increment of four units for the red, green, and blue values. So, for an EGA, you have 218 or 262,144 dithered colors. On a monochrome display──such as a CGA, an EGA connected to a monochrome display, or a Hercules board──Windows uses 64 different patterns of white and black. The density of white is approximately equal to red + green + blue ────────────────── 255 * 3 where the red, green, and blue values range from 0 to 255. Watch out for this──if you're developing using a color display, you might see nothing wrong with putting a pure red object against a pure blue back- ground. However, on a monochrome display the pure red and pure blue "colors" appear to be the same. Background Color When COLORSCR is first executed, it sets up a window class structure, called wndclass, that defines certain characteristics of its window. One member of this structure is a handle to a brush that Windows uses to color the background of the client area. This starts off as a solid black brush: wndclass.hbrBackground=CreateSolidBrush(0L); CreateSolidBrush requires an rgbValue and returns a handle to a brush. Although the hbrBackground member of the structure refers to a background, in more technical terms it is the brush that Windows uses to erase the window's client area in preparation for repainting by the program's window function. When you change the settings of COLORSCR's scroll bars, the program must create a new brush and put the new brush handle in the window class structure. Just as we were able to get and set the scroll bar window function by using GetWindowLong and SetWindowLong, we can get and set the handle to this brush by using the calls GetClassWord and SetClassWord. First, it's necessary to delete the existing brush: DeleteObject(GetClassWord(hWnd,GCW_HBRBACKGROUND)) ; Then the new brush can be created and the handle inserted in the window class structure: SetClassWord (hWnd,GCW_HBRBACKGROUND, CreateSolidBrush(RGB(color[0],color[1],color[2]))) ; Now, the next time Windows recolors the background of the window, Windows will use this new brush. To force Windows to erase the background, we invalidate the entire client area: InvalidateRect(hWnd, NULL, TRUE); The TRUE (nonzero) value as the third parameter indicates that we want the background erased before repainting. InvalidateRect causes Windows to put a WM_PAINT message in the message queue of the window function. Because WM_PAINT messages are low priority, this message will not be processed immediately if you are still moving the scroll bar with the mouse or the cursor keys. Alternatively, if you want the window to be updated immediately after the color is changed, you could add the statement UpdateWindow(hWnd) ; after the InvalidateRect call. However, this slows down keyboard and mouse processing. COLORSCR's WndProc function doesn't process the WM_PAINT message and just passes it on to DefWindowProc. Windows's default processing of WM_PAINT messages simply involves calling BeginPaint and EndPaint to validate the window. Because we specified in the InvalidateRect call that the background should be erased first, the BeginPaint call causes Windows to generate the message WM_ERASEBKGND (erase background). WndProc ignores this message also. Windows processes it by erasing the background of the client area with the brush specified in the window class. Normally Windows would erase the entire client area using the window class brush. This would cover the 10 child windows. Windows would then have to send WM_PAINT messages to all 10 child windows so that they could repaint themselves, which would be very annoying. However, you can avoid this problem by using the WS_CLIPCHILDREN style value when you are first creating the parent window with the CreateWindow call. The WS_CLIPCHILDREN style prevents the parent window from painting over its children. If you then take the WS_CLIPCHILDREN style out of CreateWindow, you'll see a big difference in how COLORSCR works. Like all GDI objects, the brushes created by a program that uses CreateSolidBrush are not automatically deleted by Windows when the program terminates. So far, we've been good about deleting each brush before creating a new one, but when the program is about to terminate, there is still one final brush in the window class that we should discard. Thus, during the processing of the WM_DESTROY message, DeleteObject is called once more: DeleteObject(GetClassWord(hWnd,GCW_HBRBACKGROUND)); Multiple Instances Normally, most Windows programs will reuse the same window class when you load multiple instances of the program. The window class is registered only if the previous instance is NULL: if (!hPrevInstance){wndclass.style = CS_HREDRAW | CS_VREDRAW; . . . } But COLORSCR must take a different approach because the background color is specified in the window class. If all instances of COLORSCR used the same window class, then each instance would alter and use the same background color. This problem is easy to solve: each instance simply registers its own window class. COLORSCR doesn't check if it's the first instance or the twentieth. Contrary to Windows documentation, a window class registered with the same name as a previously registered window class does not replace the earlier window class. A program simply has the option of using its own window class or a previously registered window class. COLORSCR takes the former approach. COLORSCR as an Icon One final mystery remains. When you make COLORSCR into an icon, the entire surface of the icon appears as the selected color rather than just the right half. Yet COLORSCR doesn't seem to have any separate icon logic. Notice that COLORSCR specifies a NULL icon in the window class: wndclass.hIcon=NULL; This indicates that COLORSCR is responsible for painting its icon. But why does the entire icon appear as the selected color? This is the simplest part of COLORSCR──Windows hides child windows when a program is iconed. The colored background is then completely uncovered. So, when the parent is sleeping, the children are neither seen nor heard. Figure 6: COLORSCR.C /* COLORSCR.C -- Color Scroll (using child window controls) */ #include #include long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ; long FAR PASCAL ScrollProc (HWND, unsigned, WORD, LONG) ; FARPROC lpfnOldScr[3] ; HWND hChScrol[3], hChLabel[3], hChValue[3], hChRect ; short color[3], nFocus = 0; int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow) HANDLE hInstance, hPrevInstance; LPSTR lpszCmdLine; int nCmdShow; { MSG msg; HWND hWnd ; WNDCLASS wndclass ; FARPROC lpfnScrollProc ; short n ; static char *szColorLabel[] = { "Red", "Green", "Blue" } ; static char szAppName[] = "ColorScr" ;- wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = CreateSolidBrush (0L) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) return FALSE; lpfnScrollProc = MakeProcInstance ((FARPROC) ScrollProc, hInstance) ; hWnd = CreateWindow (szAppName, " Color Scroll ", WS_TILEDWINDOW | WS_CLIPCHILDREN, 0, 0, 0, 0, NULL, NULL, hInstance, NULL) ; hChRect = CreateWindow ("static", NULL, WS_CHILD | WS_VISIBLE | SS_WHITERECT, 0, 0, 0, 0, hWnd, 9, hInstance, NULL) ; for (n = 0 ; n < 3 ; n++) { hChScrol[n] = CreateWindow ("scrollbar", NULL, WS_CHILD | WS_VISIBLE | WS_TABSTOP | SBS_VERT, 0, 0, 0, 0, hWnd, n, hInstance, NULL) ; hChLabel[n] = CreateWindow ("static", szColorLabel[n], WS_CHILD | WS_VISIBLE | SS_CENTER, 0, 0, 0, 0, hWnd, n + 3, hInstance, NULL) ; hChValue[n] = CreateWindow ("static", "0", WS_CHILD | WS_VISIBLE | SS_CENTER, 0, 0, 0, 0, hWnd, n + 6, hInstance, NULL) ; lpfnOldScr[n] = (FARPROC) GetWindowLong (hChScrol[n], GWL_WNDPROC) ; SetWindowLong (hChScrol[n], GWL_WNDPROC,(LONG) lpfnScrollProc) ; SetScrollRange (hChScrol[n], SB_CTL, 0, 255, FALSE) ; SetScrollPos (hChScrol[n], SB_CTL, 0, FALSE) ; } ShowWindow (hWnd, nCmdShow) ; UpdateWindow (hWnd); while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam) HWND hWnd; unsigned iMessage; WORD wParam; LONG lParam; { HDC hDC ; TEXTMETRIC tm ; char szbuffer[10] ; short n, xClient, yClient, yChar ; switch (iMessage) { case WM_SIZE : xClient = LOWORD (lParam) ; yClient = HIWORD (lParam) ; hDC = GetDC (hWnd) ; GetTextMetrics (hDC, &tm) ; yChar = tm.tmHeight ; ReleaseDC (hWnd, hDC) ; MoveWindow (hChRect, 0, 0, xClient / 2, yClient, TRUE); for (n = 0 ; n < 3 ; n++) { MoveWindow (hChScrol[n], (2 * n + 1) * xClient / 14, 2 * yChar, xClient / 14, yClient - 4 * yChar, TRUE) ; MoveWindow (hChLabel[n], (4 * n + 1) * xClient / 28, yChar / 2, xClient / 7, yChar, TRUE) ; MoveWindow (hChValue[n], (4 * n + 1) * xClient / 28, yClient - 3 * yChar / 2, xClient / 7, yChar, TRUE) ; } SetFocus (hWnd) ; break ; case WM_SETFOCUS: SetFocus (hChScrol[nFocus]) ; break ; case WM_VSCROLL : n = GetWindowWord (HIWORD (lParam), GWW_ID) ; switch (wParam) { case SB_PAGEDOWN : color[n] += 15 ; /* fall through */ case SB_LINEDOWN : color[n] = min (255, color[n] + 1) ; break ; case SB_PAGEUP : color[n] ╤= 15 ; /* fall through */ case SB_LINEUP : color[n] = max (0, color[n] - 1) ; break ; case SB_TOP: color[n] = 0 ; break ; case SB_BOTTOM : color[n] = 255 ; break ; case SB_THUMBPOSITION : case SB_THUMBTRACK : color[n] = LOWORD (lParam) ; break ; default : break ; } SetScrollPos (hChScrol[n], SB_CTL, color[n], TRUE) ; SetWindowText (hChValue[n], itoa (color[n], szbuffer, 10)) ; DeleteObject (GetClassWord (hWnd, GCW_HBRBACKGROUND)) ; SetClassWord (hWnd, GCW_HBRBACKGROUND, CreateSolidBrush (RGB (color[0], color[1], color[2]))) ; InvalidateRect (hWnd, NULL, TRUE) ; break ; case WM_DESTROY: DeleteObject (GetClassWord (hWnd, GCW_HBRBACKGROUND)) ; PostQuitMessage (0) ; break ; default : return DefWindowProc (hWnd, iMessage, wParam, lParam) ; } return 0L ; } long FAR PASCAL ScrollProc (hWnd, iMessage, wParam, lParam) HWND hWnd ; unsigned iMessage ; WORD wParam ; LONG lParam ; { short n = GetWindowWord (hWnd, GWW_ID) ; switch (iMessage) { case WM_KEYDOWN: if (wParam == VK_TAB) SetFocus (hChScrol[(n + (GetKeyState (VK_SHIFT) < 0 ? 2 : 1)) % 3]) ; break ; case WM_SETFOCUS: nFocus = n ; break ; } return CallWindowProc (lpfnOldScr[n], hWnd, iMessage, wParam, lParam) ; } Figure 7: COLORSCR.DEF NAME ColorScr STUB 'WINSTUB.EXE' CODE MOVABLE DATA MOVABLE MULTIPLE HEAPSIZE 1024 STACKSIZA 4096 EXPORTS WndProc ScrollProc Figure 8: Make-file for COLORSCR.EXE colorscr.obj : colorscr.c cl -c -d -Gsw -Os -W2 -Zpd colorscr.c colorscr.exe : colorscr.obj colorscr.def link4 colorscr, /align:16, /map /line, slibw, colorscr mapsym colorscr Figure 10: An unsigned long (32-bit) integer where the lowest three bytes specify red, green, and blue values. ╔═╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╤══╤═╤═╤═╤═╤═╤═╤══╗ ║0│ │ │ │ │ │ │7 │ 8│ │ │ │ │ │ │15│16│ │ │ │ │ │ │23│24│ │ │ │ │ │ │31║ ╚═╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╧══╧═╧═╧═╧═╧═╧═╧══╝ • • • • • • • • • • • • • • • • • • • • • • • • • • • • ╔═════╗ ╔═════╗ ╔═════╗ ╔═════╗ ║ 0 ║ ║BLUE ║ ║GREEN║ ║ RED ║ ╚═════╝ ╚═════╝ ╚═════╝ ╚═════╝ ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob The Driverless Printer Dear Dr. Bob, Could you tell me if it is possible in a Windows application to send text and control characters to a printer for which there is no driver?──Wondering Dear Wondering, If you're using a commercial Windows app, you're in trouble; you can't print without a driver. If you're writing your own app, you can't set up a DC (display context) and use standard GDI functions like Rectangle(), BitBlt(), and TextOut(). But you can use DOS file I/O to open, write, and close the DOS PRN: device. If you do this, it's up to you to understand the language your printer talks──its escape codes and special control characters. This, of course, ties your program to that specific printer and loses the device independence normally provided by Windows. Math Library Functions Dear Dr. Bob, How can I use Microsoft C math library functions in my assembler program?──Math Maven Dear Maven, Microsoft C, Version 4.0, has several floating-point options: in-line 8087 code, calls to an 8087 emulator, or calls to an alternative library. Before we tell you how to go about this task, we'll make a few disclaimers: C floating-point functions are meant for use by the compiler and handle arguments differently from standard functions, they're undocumented, and the fact that they work with Version 4.0 is no guarantee that they'll work with future versions. To figure out how the compiler uses floating point, we used the /Fc compiler switch to examine the compiler's assembly language output and /FPa or /FPc so that the compiler would produce calls to the floating-point functions instead of 8087 code. First, you have to initialize the library. If possible, let the regular C startup code handle this. If this won't work for you, look at the __CINIT function found in CRT0DAT.ASM from the C startup code disk. You should be able to pull the floating-point initialization code from this file. To find the names of the floating-point functions and the arguments that each function requires, write small C programs that demonstrate the floating-point functions you need and compile them with the /Fc switch. Study the assembler code and model yours after it. There are library functions for putting arguments onto a stack, doing math functions, and popping them off. Figure 1 is a sample C program and the assembler code it generates. You can explore other areas of compiler code this way, too. Another one you might want to look at is long int arithmetic. You don't have to worry about formatting your numbers because MASM's DD, DQ, and DT directives use the IEEE formats for the real number constants that the library expects. The floating-point functions documented in the C library manual, such as sin() and exp(), use standard C calling conventions, but before you use them, look at how the compiler converts all operands to double precision. Using COM2 Without COM1 Dear Dr. Bob, I just recently ran into a problem with QuickBASIC. I opened COM2 for I/O and kept getting a bad filename error. The problem turned out to be that I did not have anything addressed as COM1 in my system. Everything worked fine after I installed a modem or serial card addressed as COM1. All that Microsoft product support would say is that you should not use COM2 if you do not have a COM1 already installed. I have designed IBM- compatible serial interfaces, so I know how the hardware works. To me they are two completely separate entities with two unique I/O addresses. Why am I having this problem, and why doesn't Microsoft consider it a problem? They told me they would not try to fix it.──Constrained Dear Constrained, We're on your side. The hardware for COM2 can work without COM1. It seems that QuickBASIC looks in the BIOS data area at RS232_BASE. This area stores the addresses of up to four serial communications ports. When you start your system, the BIOS initialization code checks for serial ports, puts in addresses of any it finds, and sets the rest of the locations to zero. When both COM1 and COM2 are present, the address of COM1 (3F8h), is in the first word of RS232_BASE and the address of COM2 (2F8h) is in the second word. But when COM2 is the only port installed, its address, 2F8h, is in the first word, and the second word is zero. When QuickBASIC is loaded, it looks at RS232_BASE to see what serial ports are present, but it only recognizes COM2 when its address is in the second word. Here's a way to work around this. The program shown in Figure 2 will patch the second word of RS232_BASE with the address of COM2. You must run it before you run your QuickBASIC application that uses COM2. If you put this code at the beginning of your program, it will be executed too late to do any good because QuickBASIC checks for serial ports before it starts running your first BASIC statement. C Queries and Suggestions Dear Dr. Bob, I'd like to offer some suggestions and ask some questions regarding Version 4.0 of Microsoft C. In CodeView, in order to trace through a C source program, one must link with the object module. This is fine, but it would also be nice to have CodeView trace through the C source programs that have modules within a library. Is this possible? A related question: does the EXEPACK linker option remove the extra debugging information from specified libraries? If so, this would at least eliminate the need to compile a file twice──once with the CodeView options and once without. This way I would compile everything with the CodeView options before creating the libraries. Then if I needed to debug a particular program from the library, I could link in the appropriate object file. While using the watch option in CodeView, I tried to enter the following expression: strptr[ (idx==0) ? 0 : idx-1 ],s where strptr was declared as char *strptr[] and idx was an int. CodeView did not allow this and returned a message that ']' was missing. Am I missing something? Also, I often find myself setting several debugging options in CodeView (tracepoints, watchpoints, breakpoints, etc.), finding a bug, exiting CodeView, editing the source, compiling, linking, and then starting CodeView and having to reset the options I had before. It would be nice to have a save/reset option for CodeView that will save and later reset the tracepoints, watchpoints, and breakpoints. Here's what I suggest. I usually enter CodeView with the same options. There could be an environment variable (let's say CV) that would look something like "set CV= -w -f" so that these options will now be the default for entering CodeView. One more question, about malloc, free, and _memavl. If I have a program like the one shown in Figure 3, the last _memavl () does not reflect the amount of space now available from the prior free () call. This was done by using the small compiler model with the two options /Zi and /Od. Finally, I would like to share some tips about the MAKE facility. I now set up MAKE files that look like Figure 4. There might be standard include files in subdirectories \include,\gf\s, \gf\m, and gf\l, and standard libraries in subdirectories \lib, \gf\s, \gf\m, and \gf\l.──C Buff Dear C Buff, Yes, you are missing something: the CodeView manual, Section 4.4, which says CodeView supports a subset of C operators. The conditional operator ?: is not in the subset. That's the reason for the error. One thing we found interesting about the complex operations CodeView does support is that it reevaluates the entire expression each time it displays the watch window. This means that if you're watching an array such as strptr[idx], CodeView may not only watch a fixed location; when idx changes, you'll be watching a different element of the array. This is not what we expected, but we like what CodeView does. Your idea for the CodeView save/reset options is a good one. We'll pass it on. We, too, get tired of turning case sense off each time. Your malloc(), free(), and _memavl() question is interesting. By marking the block as no longer in use, free() releases the memory. But as you discovered, this free space does not show up immediately in _memavl(). According to the C manual, _memavl() returns "approximately" how much memory is available. Because it doesn't add up the free blocks until they're needed, what it in fact returns is the largest size you can be sure you can allocate. You may, however, be able to allocate a much larger block, because if malloc() gets a request for a block larger than _memavl(), it goes down the list of free blocks joining adjacent ones, trying to make a large enough block. There is no way to force garbage collection analogous to the BASIC FRE($) function. Figure 5 is a simple function, _maxavl(), that returns the size of the largest possible block. We suggest using it instead of _memavl(). Figure 1: A sample C program and the assembler code it generates. float a,b,c,d,e; main() {a = (b+c)*(d+e) ; } lea bx,WORD PTR _b ; put address of b into the bx register call __flds ; push b onto fp stack lea bx,WORD PTR _c call __fadds ; add c to top of stack lea bx,WORD PTR _d call __flds ; push d onto fp stack lea bx,WORD PTR _e call __fadds ; add e to top of stack call __fmul ; multiply top two elements of stack ; product replaces previous two values on stack lea bx,WORD PTR _a call __fstsp ; pop stack putting result in variable a Figure 2: This program demonstrates how to patch the second word of RS232_BASE with COM2. You must run it before you run your QuickBASIC application that uses COM2. def seg _ &h0040 if (peek(0) + 256*peek (1))= &h02f8 then _ poke 2,&hf8: poke 3,&h02:_ print "COM2 patch made"else_ print "COM2 patch NOT made" Figure 3: The last _memavl () does not reflect the amount of space now available from the prior free () call. This was done by using the small compiler model with the two options /Zi and /Od. printf ("starting available memory = %u\n",_memavl () ); if (NULL == (ptr=malloc ((unsigned) 5000)) ) printf("Out of memory."); printf("Available memory after malloc() = %u\n",_memavl () ); free(ptr); printf("Available memory after free() = %u\n",_memavl () ); Figure 4: MAKE file template # MAKE file template - ############################### # Assume the application program is called DOIT and the source # directory is \CUSTOMER\SOURCE. The target directory for all # obj and exe files is \CUSTOMER\S (which can easily be changed # to \CUSTOMER\M or CUSTOMER\L by setting M=specific model # type. The switches are: # # M= model type S, M, or L # # O= Compiler options (set for CodeView) # # L= LINT_ARGS switch (currently set on) # To set this off, use L= # # I= Switch to set environment variables # # To set this off, use I=null # # S= Switch to force a compile # # To force a compile, use S=null # ###################################################### M=S O=/Zi/Od L=/DLINT_ARGS D=\CUSTOMER\$(M) I=null S=\include\stdio.h COMP=\bin\msc $*,$@ /A$(M) $(L) $(0); LINK=\bin\link $(D)\doit.obj,$(D)\doit.exe,$(D)\doit.map,/M/E LINKCV=$(LINK)/CO # Make sure the include and library environ vars are correctly set $(I): \bin\inclib.bat \bin\inclib $(M) $(D)\doit.obj: doit.c $(S) $(COMP) $(D)\doitcv.exe: $(D)\doit.obj $(LINKCV) $(D)\doit.exe: $(D)\doitcv.exe $(LINK) dir $(D)\doit*.* # End of MAKE file##################################### The \BIN\INCLIB.BAT file looks something like this: if "" == "%1" goto :default set INCLUDE=\include;\gf\%1 set LIB=\gf\%1;\lib goto :end :default set INCLUDE=\include;\gf\s set LIB=\gf\s;\lib goto :end :end Figure 5: A simple function, _maxavl (), that returns the size of the largest block that malloc () can allocate. /* * _maxavl() - Return the size of the largest block that * malloc() can allocate */ int _maxavl() { register unsigned size; register unsigned incr; char *p; /* * Do binary search for largest block malloc() can return. * Start search with a size of 32K. Keep going until the * increment value is reduced to 0, with size homing in on * its largest possible value. */ for (size=incr=0x8000; incr; ) { incr >>= 1; /* cut increment in half */ if ( (p=malloc(size))==NULL ) size -= incr; /* malloc() failed, so decrease size */ else { free (p); /* malloc() succeeded, so free block */ size += incr; /* and increase size */ } } /* If last malloc() failed, return size-1 instead of size: */ return( (==NULL) ? size-1 : size ); } ════════════════════════════════════════════════════════════════════════════ Vol. 2 No. 3 Table of Contents PLEXUS Introduces Windows-based Tools for Building Image Databases The Plexus Extended Data Processing (XDP) system integrates a UNIX-based file server with Microsoft Windows-based workstation software. XDP provides a high-level DBMS environment for creating sophisticated, graphics-oriented database applications. Porting MS-DOS Assembly Language Programs to the OS/2 Environment OS/2's protected mode precludes programs and routines written in assembly language from taking certain liberties allowed under real mode. This article offers a guide that can minimize the conversion effort necessary to insure programs will work under both MS-DOS and MS OS/2. Microsoft Windows 2.0: Enhancements Offer Developers More Control Windows 2.0 gives developers over 60 new functions, a new user interface with overlapping windows, and a substantially improved development environment. Our author uses some of the new facilities to create TILER, which supplies one feature missing in Windows 2.0──tiled windows. Keeping Up With The Real World: Speedy Serial I/O Processing Serial I/O processing under MS-DOS benefits dramatically when the BIOS serial communications routines available through INT 14H are replaced. Strategies for handling the necessary interrupts, XON/XOFF processing, and reliable communications up to 38,400 baud are suggested. BLOWUP: A Windows Utility for Viewing and Manipulating Bitmaps Using bitmaps in the Windows environment is not trivial. The programmer must understand memory display contexts, how bitmaps are scaled, and how to transfer bitmaps via the clipboard. BLOWUP, a useful Windows tool, can capture and manipulate any part of the windows display. Increase the Performance of Your Programs with a Math Coprocessor The performance of calculations, especially those that make extensive use of floating-point operations, increases significantly when the CPU is supplemented with an 8087/80287/80387 coprocessor. Learn how your programs can take advantage of these coprocessors. TIFF: An Emerging Standard for Exchanging Digitized Graphics Images The growing popularity of desktop publishing has created a need for the different vendors' software to be able to exchange digitized graphics images. The Tag Image File Format (TIFF) is gaining momentum as the possible industry standard for scanner-created digital images. Ask Dr. Bob EDITOR'S NOTE The shipment of the Microsoft OS/2 Software Development Toolkit allows developers to begin programming for the new operating system. In this issue, and in those to come, MSJ focuses on specific OS/2 programming topics. Article 2, "Porting MS-DOS(R) Assembly Language Programs to the Microsoft OS/2 Environment", offers a step by step guide to converting assembler code so that it will work under OS/2. The importance of the Windows environment continues to grow. More and more companies, such as PLEXUS (spotlighted in article 1) rely on the Windows environment for their workstation software environment. Our article on the about-to-be-released Windows 2.0 highlights why the new and enhanced features are of particular interest to developers. And, Charles Petzold continues his Windows programming series with Blowup, another small but interesting Windows tool. One of the dangers a publication encounters when working with prerelease software is the inevitable changes products go through before they are released. We do our best to ensure the sample code works when the magazine is printed. You can find the most current versions of all our source code, as well as full code listings that we don't have room to print, on the DIAL system. We will soon be posting the complete code listings on other popular bulletin boards. Check in upcoming issues for more information. Some readers of our last issue commented that our discussion of the OS/2 DOS Environment implied that bypassing MS-DOS, writing directly to the hardware, and playing games with segment reisters should be punishable by death. Not at all! We recognize that MS-DOS has certain limitations. The article merely meant that OS/2 eliminates a good deal of those limitations and that it will no longer be necessary to bypass the operating system. If you have any comments, suggestions, or critiques, please write us and let us know.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor CHRISTINA G. DYAR Associate Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor DIANA E. PERKEL Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher BETSY KAUFER Administrative Assistant Copyright(C) 1987 Microsoft Corporation. All rights reserved; reproduction in part or in whole without permission is prohibited. Microsoft Systems Journal is a publication of Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H. Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley, President and Chief Operating Officer; Francis J. Gaudette, Treasurer; William Neukom, Secretary. Microsoft Corporation assumes no liability for any damages resulting from the use of the information contained herein. Microsoft, the Microsoft logo, CodeView, MS, MS-DOS, and XENIX are registered trademarks of Microsoft Corporation. IBM is a registered trademard of International Busness Machines Corporation. PageMaker is a registered trademark of Aldus Corporation. dBase is a registered trademark of Ashton-Tate. UNIX is a registered trademark of AT&T. Lotus and 1-2-3 are registered trademarks of Lotus Development Corporation. Intel is a registered trademark of Intel Corporation. Macintosh is a trademark of of Apple Computer, Inc. Paintbrush is a registered trademark of ZSoft Corporation. Motorola is a registered trademark of Motorola, Inc. ████████████████████████████████████████████████████████████████████████████ Plexus Introduces Windows-based Tools for Building Image Databases Kevin Strehlo☼ When personal computers first began to make inroads into the business environment, database managers such as dBASE(R) were fine for what was then a text-only world. They gave people who weren't crack programmers the ability to build custom applications with reasonable, if limited, interfaces for the management and manipulation of textual data. But Microsoft(R) Windows, scanners, and laser printers have taken us from that text-only world into one in which we can generate and manipulate images intermingled with text. OCR devices even allow us to translate images of documents into computer graphics and text. Where are the visual database managers with which we can easily build custom applications in order to manage huge volumes of this image data? Plexus(R) thinks it has the answer: its Extended Development Environment (XDE), which couples Windows with a fourth-generation database language. Still in its beta-test stage, XDE is designed to run on Plexus's Extended Data Processing (XDP) System, an elaborate configuration of LAN hardware and software for handling an Informix-based graphics database. XDE serves as a tool for creating custom applications that manage graphics data with a sophisticated visual interface. The real beauty of XDE is its simplicity for the programmer. Writing a picture perfect (or rather, a perfect picture) database with it requires no knowledge of the event-driven mysteries of Windows, the intricacies of C, or the problem of finding a bitmap in an optical disk stack. It lets the programmer create a Windows database application without worry about such things as WinMain functions or WINDOWS.H files. Hardware and Software When it comes to system requirements, XDE and standard database managers part ways, however. XDE will run only on a complete XDP system, which means a substantial investment in hardware and software. Complete systems range from $75,000 to more than $1 million. At the center of XDP is a 68020-based Plexus supermicro that acts as the database server for an Ethernet(TM) LAN. Hooked up to the LAN are a number of AT-compatible workstations running Windows on high-resolution, 1,664- by 1,200-pixel displays. The cabinets of the system can provide as much as 6.7Gb of magnetic disk storage and 8Gb of optical disk storage. If more is needed, a jukebox of optical laser disk drives can be attached to handle as much as 280Gb with an average access time of under 10 seconds. If you think that's an incredible amount of disk capacity, you're right, but let's face it──at 300Kb or more per compressed image, a database that replaces a roomful of microfiche or shelves full of technical manuals is going to require an incredible amount of storage. To record all of that visual information, you can attach microfiche and paper scanners to the workstations. To translate captured document images into ASCII text suitable for manipulation, a powerful OCR device handles a variety of fonts at a minimum speed of 60 characters per second with greater than 99 percent accuracy. For handling large images with dispatch, each AT workstation contains a board with enough memory to hold and manipulate an uncompressed 400-dpi image and a graphics coprocessor tailored for pixel manipulation. Having an entire 1.5Mb bitmap available in memory speeds up image manipulation. The coprocessor board also minimizes transmission times among the database server, the workstations, and the OCR subsystem by quickly compressing each image before transferring it across the Ethernet. The software at the heart of XDP is as much a mixed bag as the database that it supports. On the AT-compatible workstations, the system runs MS- DOS(R) 3.1 because of its networking hooks. The Plexus DataServer runs an enhanced version of the Informix relational database under UNIX(R) System V. Meanwhile, communication between server and workstations is handled according to TCP/IP protocols on an Excelan implementation of Ethernet. The Informix database running on the server has been extended with data types for handling images. A similarly extended version of the SQL standard for database queries (which is dubbed XESQL) allows mixed-mode (text and graphics) transactions. The custom applications created by XDE and running on the AT workstations generate the XESQL queries. How XDE Works The Plexus XDE consists of two components: an application called UI-Build (UI stands for user interface) and an extended version of the Informix 4GL language. UI-Build does for the overall user interface what the Microsoft Windows Dialog Editor does for dialog boxes (see "Latest Dialog Editor Speeds Windows Application Development," MSJ, Vol. 1 No. 1). It allows a sophisticated end-user or relatively inexperienced programmer to define the Windows front-end of the application interactively. After defining the elements of the Windows interface, the application developer uses the 4GL language to write the code behind the drop-down menus, buttons, image fields, and text fill-in boxes. The 4GL language is not strictly declarative. It is a high-level programming language with standard procedural capabilities, including IF/THEN/ELSE, CASE, FUNCTION, and VARIABLE. Upon compilation, XDE links that simple 4GL code to the appropriate elements of the Windows interface. XDE handles all of the tasks associated with building a full-blown Windows application, which are by no means trivial. "We try to cover up all the difficulties that Windows can bring in and present to the user," says Maurizio Gianola, software tools manager for Plexus. The visual and event-driven nature of Windows throws the unwary programmer up against new and strange concepts, such as client area and messages, he points out. "You have to know so many things to write a Windows application. What is the window handler? What are all those messages coming in? What do you do with those messages? Which ones go to the menu handler? Which ones go to the system?" Despite its high-level nature, the 4GL language offers rich functionality, including a number of graphics-oriented extensions to SQL, such as the capacity to adjust scanner parameters and a "magnifying glass" function that automatically rescales the on-screen presentation of an image from 100 dpi to 300 dpi. And to accommodate needs that the language's developers didn't foresee or thought too specialized to implement, specialized routines can be written in C and dropped into the 4GL program. Windows and Houses To illustrate the general form and function of XDE, Gianola whipped up a sample real estate application: the user specifies a range of house prices and asks to see some listings. The application in turn queries a database on the Plexus server and responds by allowing the user to view matching listings one at a time. The address, the number of bedrooms, and, optionally, a scanned image of each house are displayed (see Figure 1☼). When you invoke UI-Build to begin creating a new application, all you see above the Windows icon area are the two free-floating menu bars. Figure 2☼ shows the UI-Build environment a step into the process of creating the sample real estate application. Gianola has already named the title bar "Real Estate Database." Had he chosen to do so, he could have specified in the main UI-Build menu that the system box appear in the title bar, that the window be sizable and movable, and so on. Wishing to keep the application simple, Gianola merely chooses Menu_Bar from the Palette, adds the Options menu, and names the only command in that menu Show Picture. In Figure 3☼, Gianola has filled in the UI-Build dialog box that comes up when Menu is chosen from the Palette; Figure 4☼ shows the result after he has added two menu choices. Gianola next designs the forms that will pop up in the course of running the application. When users ask to see listings, they must use the form shown in Figure 5☼ to specify the minimum and maximum prices they wish to consider. The form is a Group_Box; it is labeled Price Range on screen and priceRange internally. Within this box Gianola has defined an Entry area, which he can stretch to the desired size by means of handles. He assigns Min as an ID for whatever text a user will input; this lets him reference it easily from within his 4GL program. Figure 6☼ shows the form that will appear when the user selects Show Listing from the Commands menu. Gianola has labeled the rectangle Available Houses, and because it is currently selected, it can be resized by stretching its handles. Note that he has defined two buttons in the lower right corner──Next and Done. To define a large area in which the scanned images of the houses can be displayed, he chose Image from the Palette. The figure shows Gianola in the process of defining fields in which to display the number of rooms, price, and address of the current house listing. Had he desired, Gianola could have included other means of interacting with the user. For example, he could have let the user specify the desired number of rooms by clicking on a toggle to enable a table of numbers drawn from the database and then vertically scrolling to his or her choice──much as point sizes are selected in the Type Specifications dialog box of PageMaker(R). Writing the Code After establishing the user interface with UI-Build, the programmer then uses a text editor to write the 4GL code that goes with it. Figure 7 shows the code that takes care of the preliminaries. The database is named "houses," and the predefined table in that database called "house" is used to define the records that will appear in windows on the AT's screen. Three variables are declared, one of which will serve as a flag. The first lines of the MAIN section name the help file and initialize the showpicture flag. Next, the program diplays the menu defined in Figures 2, 3, and 4☼ and names mainMenuHdl as the routine that will deal with user selections of items in that menu. The first instance of the modeless form availHouses is brought up as well. Figure 8 shows the menu-handling routine for the main menu. When the user selects Show Picture from the Options menu, that choice (the internal IDfor it is showHousePicture) is checked by calling the XDE standard Check function and the showpicture flag is changed if necessary. If the user chooses Show Listing from Commands, the form defined in Figure 5☼ pops up. The routine creates a cursor to allow user input and selects all records in the database that fall within the range specified by the user. It then calls the function showData, which we will discuss later. Finally, since the user is now looking at listings, the Show Listing choice in the menu is disable, and the End Listings choice changes from gray to black to indicate it is enabled. Figure 9 shows the form handling routine availHouseHdl, which uses the 4GL construct called Before Input to initialize the blank form. A message explaining how to view the picture appears along with the Windows note icon and an OK box for the user to check. If the user clicks in the Next button, the showData function is called, while a click in Print calls the printHouseInfo function. The line ON photo initiates a rather nifty feature of XDE: if the user clicks on the empty field in which houses are displayed in an effort to bring up an image, a message tells him or her to make the appropriate menu selection. Finally, when the user is done, the application asks if the current listing should be printed before it disappears. The routine in Figure 10 performs a simple check to ensure that the user has filled in minimum and maximum values that make sense. If this is not the case, a warning message will appear. Finally, the two functions called in the earlier routines appear in Figure 11. The first displays the proper image and values within their respective fields in the current instance (availH) of the form availHouses. The second prints the listing at the user's request. Unique Approach Several unique aspects of the Plexus XDE should appeal to anyone who wants to build Windows applications quickly. For one thing, the system automatically sets up everything required by Microsoft Windows. When the application is compiled, it produces not only a resource file that encodes everything done with UI-Build, but also a WINDOW.H file that includes the predefined XDE functions. For example, the functions available for creating rich menus include gray and ungray──that is, disable and enable menu choices──and check and uncheck. Gianola seems especially proud of another unique feature of XDE. "Usually, every time you change something in the user interface, you have to recompile a Windows application, but we tried to avoid that." Essentially, Plexus bound the user interface to the program at link time rather than during compilation. "In other words, when you go back and touch up your user interface after you've gotten the application running," Gianola says, "you will need to link but not to recompile." This feature is actually almost a necessity in XDE since compilation involves two levels of translation──first from 4GL to C, and then from C to object code. Recompilation would thus be a more cumbersome task than usual. Applications The Plexus XDP is already in use by some firms even though XDE is not yet available. Such was their need for a visual approach to managing large volumes of image data that these companies (with the help of Plexus) wrote ordinary Windows programs in C to run the system. The applications created thus far are fairly complex and demonstrate that there will often be a need to enhance an application roughed out with XDE by embedding at least a modicum of C code. For example, one customer is using a five-workstation system for producing CD ROM discs to replace microfiche. One feature of this system enables the user to exclude parts of a page from being recorded by the scanner. Such refinements are beyond the capabilities of XDE, although Plexus plans to enhance the environment in future releases. Another Plexus customer, the regional phone company US West, uses an XDP system to manage the placement of ads in its yellow pages publishing operations. When a customer calls, ad sales representatives can quickly find the order through a text-oriented database search that's facilitated by an easy-to-use Windows interface. They can view an image of the advertisement on-screen. Desktop publishing ventures involving a large number of technical diagrams or other images would seem to be another natural application for the system. Since an application built with XDE resides in the Windows environment, retrieved images could be transferred directly into a Windows desktop publishing program──and rich text could be moved just as easily. Altogether, XDP seems a big improvement over comma-delimited ASCII export files and pasted photocopies. Figure 7: DATABASE houses DEFINE {The record prospect is defined like the database table house } prospect RECORD LIKE house.* Min, Max INTEGER showpicture SMALLINT MAIN HELP FILE "houses.hlp" { F1 is request for help } LET showpicture = FALSE SHOW MENU mainMenu USING mainMenuHdl OPEN FORM availHouses USING availHousesHdl AS availH { modeless form } END MAIN Figure 8: mainMenuHdl MENUHANDLER mainMenuHdl ON SHOW MENU DISABLE IN MENU endListing MENU ON showHousePicture IF showhouse = FALSE THEN CHECK IN MENU showHousePicture LET showhouse = TRUE ELSE UNCHECK IN MENU showHousePicture LET showhouse = FALSE END IF ON showListing OPEN FORM priceRange USING priceRangeHdl { modal form } DECLARE listingCursor CURSOR FOR SELECT * INTO prospect.* FROM house WHERE house.price <= Max AND house.price > Min OPEN listingCursor CALL showData() DISABLE IN MENU showListing ENABLE IN MENU endListing ON endListing ENABLE IN MENU showListing DISABLE IN MENU endListing CLOSE listingCursor END MENUHANDLER Figure 9: availHouseHdl FORMHANDLER availHouseHdl() DEFINE print SMALLINT ON OPEN FORM DISPLAY 0, "", NULL, 0 TO FIELD price, address, photo, numOfRooms INPUT ON Done CLOSE FORM ON Next CALL showData() ON PrintAllInfo CALL printHouseInfo() ON photo MESSAGE "To see the picture of the house check the menu selection" NOTE_ICON OK_BUTTON END INPUT ON CLOSE FORM MESSAGE "Do you want to print this listing?" NOTE_ICON YES_NO_BUTTONS RETURNING print IF print = YES THEN CALL printHouseInfo(prospect.code) END IF END FORMHANDLER Figure 10: priceRangeHdl FORMHANDLER priceRangeHdl() INPUT BY NAME Min, Max ON idOk IF Min <= Max THEN CLOSE FORM ELSE MESSAGE "Incorrect price range!!" WARNING_ICON OK_BUTTON END IF END INPUT END FORMHANDLER Figure 11: Other Functions FUNCTION showData() FETCH listingCursor DISPLAY BY NAME prospect.* TO FORM availH IF showpicture = TRUE THEN DISPLAY prospect.photo TO FORM availH FIELD photo END IF END FUNCTION FUNCTION printHouseInfo() PRINT prospect.price, prospect.address, prospect numOfRooms IF showpicture = TRUE THEN PRINT prospect.photo END IF END FUNCTION ████████████████████████████████████████████████████████████████████████████ Porting MS-DOS Assembly Language Programs to the OS/2 Environment Ray Duncan☼ OS/2 the protected-mode operating system for the 80286 microprocessor, offers the applications programmer a rich variety of system services and is comparable in many ways to the powerful real-time operating systems used on minicomputers and superminicomputers. It supports preemptive multitasking, multiple screen groups, a broad spectrum of inter-process communication facilities, and virtual memory management. It includes high- performance device drivers for the video display, keyboard, mouse, and serial port. It even boasts a true print spooler. In short, OS/2 provides a sturdy foundation for an entirely new generation of business applications with vastly expanded features and capabilities. Yet at first programmers will be more concerned with porting their existing MS-DOS tools and applications to OS/2's protected-mode environment than with attempting to exploit the capabilities of the new system. Those who write code in high-level languages, such as Microsoft(R) C, can simply relink and run the programs when the appropriate protected- mode libraries are available. But those of us who still labor in the vineyards of bit-banging──writing primarily in assembly language or at least making extensive use of assembly language subroutines to obtain optimum performance──face a much more extensive revision process. Here I will outline a useful strategy for porting MS-DOS(R) assembly language programs to OS/2. Derived from my own experience in working with the alpha- and beta-test versions of OS/2, this conversion process should not be construed as an official recommendation from Microsoft, although I find that it works well. The procedure consists of five steps: segmentation, rationalization, encapsulation, conversion, and optimization. The first three should be performed and tested in the MS-DOS environment. It is only the last two that require OS/2 and the associated programming tools. Segmentation The Intel(R) 80286 microprocessor can function in either of two distinct modes: real mode or protected virtual address mode. In real mode, the 80286 has a 1Mb address space and behaves like an 8086 or 8088 processor, except that some machine instructions are more efficiently implemented and a few new instructions are available. MS-DOS runs on the 80286 in real mode. In protected mode, the personality of the 80286 is considerably different. Hardware features come into play letting an operating system manage 1Gb of virtual address space, isolate the memory used by one process from that used by another, perform fast context switching between processes and interrupt handlers, intercept certain privileged machine instructions, and detect the execution of invalid opcodes. OS/2, of course, runs in the 80286's preferred state: protected mode. Most of the 80286's protected-mode capabilities revolve around a change in the way memory is addressed. In real mode, the value in a segment register directly corresponds to a physical memory address. In protected mode, this correspondence is indirect: a segment register holds a selector, which is an index into a table of descriptors. A descriptor defines the physical address and length of a memory segment, its characteristics (executable, read-only data, or read/write data) and access rights, and whether the segment is currently resident in RAM or has been swapped to disk. Each time a program loads a segment register or accesses memory, the 80286 hardware checks the relevant descriptor table entry and the program's privilege level, generating a hardware interrupt (called a protection fault) if the selector or memory operation is not valid. Needless to say, manipulation of the descriptor table itself is reserved for the operating system's memory manager. This scheme of memory addressing in protected mode has two immediate consequences for applications programs. First, they can no longer perform arithmetic on the contents of segment registers (since selectors are only index numbers and have no direct relationship to physical memory addresses) or use segment registers for storing temporary values. A program must not load a segment register with anything but a legitimate selector that it received from the OS/2 loader or as a result of an OS/2 memory allocation function call. Second, machine code ("text") and data must be strictly segregated from each other by placing them in separate segments with distinct selectors, since an executable selector is not writable, and vice versa. Accordingly, the first step in converting a program for OS/2 is to impose upon it a segmented structure compatible with the protected-mode environment. The program must have at least one code and one data segment, and it should use the special name DGROUP to declare a group containing the "near data" segment, stack, and local heap (if there is one). Figure 1 shows this skeletal structure. It's best to follow the segment naming and ordering conventions used by the Microsoft compilers (see Figures 2 and 3). At this stage, you should also remove or rewrite any code that directly manipulates segment values. Now reassemble and link your program and make sure it still works as expected under MS-DOS. Changing or adding segmentation often uncovers hidden addressing assumptions in the code, so it is best to track these problems down before making any other substantive changes to the program. Rationalization Once you've successfully segmented your program so that it can be linked and executed as an .EXE file under MS-DOS, the next step is to rationalize your code. This means converting your program into a completely well- behaved MS-DOS application, ruthlessly eliminating any elements that manipulate the peripheral device controllers directly, alter interrupt priorities or edit the system interrupt vector table, depend on CPU speed or characteristics (such as timing loops), or are incompatible with MS- DOS's memory management or handle-based file and record management. When an MS-DOS application directly accesses the hardware, it is usually in connection with display routines and keyboard handling. Caught between the user's demand for snappy screen interaction and the regrettably poor performance of the MS-DOS and IBM(R) PC ROM BIOS video drivers, most programmers have felt it necessary to go around the operating system and give the applications program complete control of the video adapter. This allows the screen to be updated at the maximum speed possible under MS-DOS but poses obvious difficulties in a multitasking, protected memory environment such as OS/2. For porting purposes, all routines in your program that write text to the display, control character attributes, or affect cursor shape or position should be converted into Write Handle (Interrupt 21H Function 40H) calls with ANSI escape sequences or ROM BIOS Interrupt 10H calls. Similarly, convert all keyboard operations to Read Handle (Interrupt 21H Function 3FH) or ROM BIOS Interrupt 16H calls. After you've expunged all hardware dependence from your program, your next priority is to make sure it uses system memory properly. Whereas MS-DOS typically hands an application all of the unoccupied memory in the system, this is not true of OS/2. A process is initially allocated only enough memory to hold its code segment and stack and to meet its declared data storage needs. You can make the MS-DOS loader behave in the same way that the OS/2 loader does by linking your application with the /CPARMAXALLOC switch. Alternatively, your program can execute a Set Memory Block (Interrupt 21H Function 4AH) call early in its initialization routine to release any extra memory it may have been allotted by the loader. When your program has begun its main sequence of operation, it should dynamically obtain and release any additional memory it may require for buffers and tables with the MS-DOS Interrupt 21H Functions 48H (allocate memory block) and 49H (free memory block). The size of any single allocated block should not exceed 65,536 bytes, even though MS-DOS allows the allocation of larger blocks; this will ensure compatibility with protected mode. Finally, you must turn your attention to file handling. MS-DOS takes a rather schizoid approach to mass storage by supporting two completely different sets of file and record management calls. The first set relies on a data structure known as a file control block (FCB), while the second and far more powerful group uses null-terminated (ASCIIZ) filename strings and 16-bit file tokens called handles. The FCB calls became obsolete with the advent of the hierarchical directory structure in MS-DOS 2.0, but MS- DOS still supports them in the name of upward compatibility, and unfortunately there are still programs around that use them. If your program is one of these, you'll have to replace every FCB file or record function call with its handle-based equivalent, because OS/2's protected mode contains no support at all for FCBs. Encapsulation Now that you have a well-behaved, segmented MS-DOS application in hand, the most painful part of the porting process is behind you. To prepare your program for the actual conversion to protected-mode operation, you should next isolate all parts of it that are specific to the host operating system and encapsulate them inside individual subroutines. The objective here is to localize the program's knowledge of the environment into small procedures that you can subsequently modify without affecting the remainder of the program. As an example of a program component that needs encapsulation, consider a typical call by an MS-DOS application to write a string to the standard output device (ordinarily the video display). MS-DOS services are invoked with a software interrupt, which occupies less space than a subroutine call, so it is common practice to code MS-DOS calls in-line, as shown in Figure 4. To facilitate conversion to OS/2, you should replace every instance of such a write to a file or device with a call to a small subroutine that hides the mechanics of the actual operating system function call, as illustrated in Figure 5. Another candidate for encapsulation, which does not necessarily involve an operating system function call, is the application's code to gain access to command line parameters, environment block variables, and the name of the file it was loaded from. MS-DOS divides this information between the program segment prefix and the environment block. Nearly every assembly language programmer has evolved his or her own techniques for obtaining these parameters when they're needed. My own solution is to write three subroutines that return the same information as C's argc, argv, and getenv: the number of command line parameters, a pointer to a specific parameter, and the string associated with a specific environment block variable, respectively. When you've finished encapsulating the system services, subject your program to thorough testing once more under MS-DOS. Since you are still working in a familiar milieu and have access to your favorite debugging tools, this is the best time to catch any subtle errors you may have introduced during the three steps discussed thus far. You'll have more trouble trying to uncover them once you move to the OS/2 environment. Conversion Now each system-dependent procedure you created during the encapsulation stage must be rewritten so that your program can execute under OS/2. In contrast to MS-DOS functions, which are actuated through software interrupts and use registers for parameter passing, OS/2's Application Program Interface functions are requested through a "far call" to a named entry point. Parameters are passed on the stack, along with the addresses of variables or structures that lie within the calling program's data segment and will receive any results returned by the function. The status of an operation is returned in register AX (0 if the function succeeded, an error code otherwise). All other registers are preserved. Figures 6, 7, and 8 list OS/2 services that are equivalent to selected MS-DOS and ROM BIOS Interrupt 21H, Interrupt 10H, and Interrupt 16H calls. These tables don't include MS-DOS functions related to file control blocks or program segment prefixes because OS/2 does not support either of these structures. MS-DOS TSR functions are also omitted. Since OS/2 is a true multitasking system, there is no need for a process to terminate in order to stay resident while another process is running. As you examine each encapsulation subroutine, refer to the entry for the appropriate function in the MS(R) OS/2 Programmer's Reference manual. All you have to do is write code that takes the parameters passed into the procedure and pushes them onto the stack in the correct order, calls the appropriate OS/2 function, tests the returned status, and loads any returned values into the appropriate registers. While working your way through the program, you must remember to declare each OS/2 function used as a "far external." If the function requires any additional storage space in the application's data segment, you must also create this. For some OS/2 calls (such as DOSOPEN), it will also be necessary to push other parameters that had no counterpart under MS-DOS. These extra parameters can usually be given reasonable values that will make their existence temporarily invisible to the remainder of the application. Figure 9 illustrates the final form of our sample procedure after its conversion for OS/2. Note especially the addition of the EXTRN statement and the wlen variable, as well as the simulation of the MS-DOS function status. This code may not be elegant, but it is the result of only a minimum of changes in the source file. Once you've converted all the encapsulation subroutines and added the necessary variables and external references, you may delete the STACK segment declaration from the .ASM source file and change the ASSUME statement for the SS register to DGROUP. Next, assemble the source file into a relocatable .OBJ module file with the Microsoft Macro Assembler in the usual manner. In order to transform the .OBJ file into a protected- mode .EXE file, however, you will need to supply the linker with two additional files: DOSCALLS.LIB and a module definition (.DEF) file for your application. DOSCALLS.LIB is a special type of object module library, which contains stub records, called dynamic link reference records, for each OS/2 function call. Each time the linker finds a stub record that matches an EXTRN declaration within the program, it adds the name of the external routine to a table in the .EXE file header. When the program is ultimately loaded for execution, the table is examined, the necessary external procedures are brought into memory from dynamic link (.DLL) libraries (if they are not already resident), and the far calls within the program are fixed up appropriately. The .DEF file is just an ASCII text file that can be created with any line or screen editor. It contains directives that control various characteristics of a protected-mode executable, such as the module name, whether the file is an application or a dynamic link library, the attributes of each segment within the program, and the size of the stack (and for C programs, the size of the local heap). Figure 10 provides a very simple sample .DEF file. You will be pleasantly surprised to find that testing and debugging a newly converted protected-mode application is a relatively straightforward endeavor. MASM has been upgraded so that it can include line numbers in object modules, and the Microsoft OS/2 Software Development Kit includes a protected-mode version of CodeView(R), Microsoft's excellent symbolic debugger. In addition, the very nature of protected mode means that although your programs are just as prone to crashing as before, it's fairly difficult to crash the entire system──you won't need that big red switch nearly as often as you did with MS-DOS. Optimization Now that your program is running properly in protected mode, you will be anxious to smooth out some of the alterations you made for purposes of conversion and introduce various optimizations. There are three obvious categories of optimization to consider: taking advantage of the additional functionality of the OS/2 calls already being used, exploiting 80286- specific machine instructions where appropriate, and revamping the application in order to cash in on OS/2's multitasking, timer, virtual memory, and inter-process communication facilities. To make optimum use of the OS/2 function calls, you can quickly modify and enhance the subroutines that encapsulate them. For example, the OS/2 DOSOPEN function allows the programmer to determine separately what action will be taken if the named file already exists (open, fail, or truncate to zero length) or does not exist (create or fail). Similarly, the OS/2 video driver offers performance and a variety of services far superior to the screen support under MS-DOS. In the case of OS/2 functions that are called only once or twice by the application, it may be more efficient to throw away the encapsulating subroutine and place the function call in-line, thus eliminating the need to load parameters into registers outside the subroutine and push them on the stack inside. If you do not intend to use the Family Application Program Interface so that your program will also run on 8086/88-based machines, you can add the directive .286c to the beginning of the source file and take advantage of 80286-specific machine instructions. The most useful of these are the instructions that let you shift or rotate by an immediate count other than one, a three-operand multiply where one of the operands is an immediate (literal) value, and a push immediate value instruction. The last of these is particularly handy for setting up OS/2 function calls. For example, in Figure 9, the sequence mov ax,offset DGROUP:wlen push ax could be replaced by the single instruction push offset DGROUP:wlen If you wish to restructure your application to take full advantage of OS/2's programmable timers, multitasking, virtual memory, inter-process communication, dynamic linking, and device monitors you'll have to make a close examination of both your application and the OS/2 Application Programming Interface. Such study will often pay off in sizable benefits in performance, maintainability, and code sharing. For instance, in many cases, different elements of an application deal with I/O devices of vastly different speeds, such as the keyboard, disk, and video display. You can both simplify the application and increase its performance by separating these elements into threads (subprocesses) that execute asynchronously, communicating through shared data structures and synchronizing when necessary via semaphores. As another example, when several applications are closely related and contain many identical or highly similar procedures, it makes sense to extract those procedures from the individual applications and place them in a private dynamic link library. This reduces the size of each application's .EXE file, since the dynamic link library routines are brought into memory and bound to the application when it's loaded. It allows more efficient use of memory, since the code segments in the dynamic library can be shared among all of the related applications when they are running concurrently. Best of all, using private dynamic link libraries vastly simplifies code maintenance, since you can debug or improve the routines in the libraries at any time without relinking the calling applications, which will automatically benefit from the new code the next time they are executed. Figure 1: Skeleton of a properly segmented MS-DOS assembly language application being prepared for conversion to OS/2. name myprog page 55,132 title MYPROG - segmentation skeleton ∙ ; miscellaneous equates ∙ ; structures, and other ∙ ; declarations go here DGROUP group _DATA ; 'automatic data group' _TEXT segment byte public 'CODE' ; all executable code ; goes in this segment assume cs:_TEXT,ds:DGROUP,ss:STACK main proc far ; the routine that initially ∙ ; receives control can be ∙ ; called anything ... ∙ mov ax,4c00h ; main routine terminates, int 21h ; passing return code=0 main endp ∙ ; other routines needed by ∙ ; the program go here ∙ _TEXT ends _DATA segment word public 'DATA' ; all read/write or static ; data items in this segment ∙ ∙ ∙ _DATA ends STACK segment para stack 'STACK' db 256 dup (?) STACK ends end main ; declares end of module ; and initial entry point Figure 2: These memory models are commonly used in assembly language and C programs. Microsoft FORTRAN programs uniformly use the large model. Microsoft C also supports a huge model, which allows creation of data structures larger than 64Kb. Model Code Segments Data Segments Small One One Medium Multiple One Compact One Multiple Large Multiple Multiple Figure 3: Microsoft assembler programs use these naming conventions for the standard memory models. Microsoft C programs use a superset of these segments and classes. ╓┌──────────┌─────────────┌───────┌─────────┌──────────┌─────────────────────╖ Memory Segment Align Combine Class Model Name Type Classes Name Group Small _TEXT byte public CODE _DATA word public DATA DGROUP STACK para stack STACK DGROUP Medium module_TEXT byte public CODE ∙ ∙ _DATA word public DATA DGROUP STACK para stack STACK DGROUP Compact _TEXT byte public CODE data para private FAR_DATA Memory Segment Align Combine Class Model Name Type Classes Name Group data para private FAR_DATA ∙ ∙ _DATA word public DATA DGROUP STACK para stack STACK DGROUP Large module_TEXT byte public CODE ∙ ∙ data para private FAR_DATA ∙ ∙ _DATA word public DATA DGROUP STACK para stack STACK DGROUP Figure 4: This typical in-line code sequence for an MS-DOS service call writes a string to the standard output device. Since the standard output might be redirected to a file without the program's knowledge, it must also check that all of the requested characters were actually written. If the returned length is less than the requested length, this usually indicates that the standard output has been redirected to a disk file and the disk is full. stdin equ 0 ; handle for standard input stdout equ 1 ; handle for standard output stderr equ 2 ; handle for standard error ∙ ∙ ∙ msg db 'This is a sample message' msg_len equ $-msg ∙ ∙ ∙ mov dx,seg msg ; DS:DX = address of message mov ds,dx mov dx,offset DGROUP:msg mov cx,msg_len ; CX = length of message mov bx,stdout ; BX = file or device handle mov ah,40h ; AH = function 40h, write int 21h ; transfer to MS-DOS jc error ; on return, CY=true if error cmp ax,msg_len ; were all characters written? jne dev_full ; no, output device is full ∙ ∙ ∙ Figure 5: The code in Figure 4 has here been encapsulated. The portion of the code that is operating system-dependent has been isolated inside a subroutine that is called from other points within the application. stdin equ 0 ; handle for standard input stdout equ 1 ; handle for standard output stderr equ 2 ; handle for standard error ∙ ∙ ∙ msg db 'This is a sample message' msg_len equ $-msg ∙ ∙ ∙ mov dx,seg msg ; DS:DX = address of message mov ds,dx mov dx,offset DGROUP:msg mov cx,msg_len ; CX = length of message mov bx,stdout ; BX = file or device handle call write ; perform the write jc error ; on return, CY=true if error cmp ax,msg_len ; were all characters written? jne dev_full ; no, output device is full ∙ ∙ ∙ write proc near ; Write to file or device ; Call with: ; BX = handle ; CX = length of data ; DS:DX = address of data ; Returns: ; If successful, Carry clear ; and AX = bytes written ; If error, Carry set ; and AX = error code mov ah,40h ; function 40h = write int 21h ; transfer to MS-DOS ret ; return status in CY and AX write endp ∙ ∙ ∙ Figure 6: Selected MS-DOS function calls and their OS/2 counterparts. Note that OS/2 functions are generally much more powerful and flexible than their MS-DOS ancestors. ╓┌───────────┌────────────────────────────────────┌──────────────────────────╖ MS-DOS Interrupt 21H Function Description OS/2 Function 00H Terminate process DosExit 01H Keyboard input with exho KbdCharIn 02H Output character to screen VioWrtTTY 03H Auxiliary input DosRead 04H Auxiliary output DosWrite 05H Printer output DosWrite 06H Direct console I/O KbdCharIn, VioWrtTTY 07H Unfiltered input without echo KbdCharIn 08H Keyboard input without echo KbdCharIn 09H Output string to screen VioWrtTTY 0AH Buffered keyboard input KbdStringIn 0BH Get keyboard Status KbdPeek MS-DOS Interrupt 21H Function Description OS/2 Function 0BH Get keyboard Status KbdPeek 0CH Reset buffer and input KbdFlushBuffer, KbdCharIn 0DH Disk reset DosBufReset 0EH Select default disk drive DosSelectDisk 19H Get default disk drive DosQCurDisk 1BH Get information for default drive DosQFSInfo 1CH Get information for selected drive DosQFSInfo 2AH Get system date DosGetDateTime 2BH Set system date DosSetDateTime 2CH Get system time DosGetDateTime 2DH Set system time DosSetDateTime 2EH Set Verify Switch DosSetVerify 30H Get MS-DOS version DosGetVersion 36H Get free disk space DosQFSInfo 38H Get or select country DosGetCtryInfo 39H Create directory DosMkdir MS-DOS Interrupt 21H Function Description OS/2 Function 39H Create directory DosMkdir 3AH Delete directory DosRmdir 3BH Select directory DosChdir 3CH Create or truncate file DosOpen 3DH Open file DosOpen 3EH Close file DosClose 3FH Read file or device DosRead 40H Write file or device DosWrite 41H Delete file DosDelete 42H Move file pointer DosChgFilePtr 43H Get or set file attributes DosQFileMode 44H Device driver control (IOCTL) DocDevIOCtl 45H Duplicate handle DosDupHandle 46H Force duplicate of handle DosDupHandle 47H Get current subdirectory DosQCurDir 48H Allocate memory block DosAllocSeg 49H Release memory block DosFreeSeg MS-DOS Interrupt 21H Function Description OS/2 Function 49H Release memory block DosFreeSeg 4AH Resize memory block DosReAllocSeg 4BH Load and execute chils process DosExecPgm 4CH Terminate process with return code DosExit 4DH Get return code of child process DosCWait 4EH Search for first match DosFindFirst 4FH Search for next match DosFindNext 54H Get verify flag DosQVerify 56H Rename file DosMove 57H Get or set file date and time DosQFileInfo, DosSetFileInfo 59H Get extended error information DosErrClass 5BH Create unique file DosOpen 5CH Lock or unlock file region DosFileLock Figure 7: OS/2 video services include these equivalents to ROM BIOS Interrupt 10H video display driver functions used by MS-DOS applications. ROM BIOS Interrupt 10H Function Description OS/2 00H Select display mode VioSetMode 01H Set cursor type VioSetCurType 02H Set Cursor position VioSetCurPos 03H Get cursor position VioGetCurPos 06H Initialize or scroll window up VioScrollUp 07H Initialize or scroll window down VioScrollDn 08H Read character and attribute VioReadCellStr 09H Write character and attribute VioWrtNCell 0AH Write character VioWrtNChar 0EH Write character in teletype mode VioWrtTTY 0FH Get current display mode VioGetMode 13H Write string in teletype mode VioWrtTTY Figure 8: OS/2 keyboard services include these equivalents to ROM BIOS Interrupt 16H keyboard driver functions used by MS-DOS applications. ROM BIOS Interrupt 16H Function Description OS/2 Function 00H Read keyboard character KbdCharIn 01H Get keyboard status KbdPeek 02H Get keyboard flags KbdGetStatus Figure 9: The code in Figure 5 after conversion. The equivalent OS/2 function call has replaced the MS-DOS function call. Since dependence on the operating system has been confined to the subroutine by the previous encapsulation step, the surrounding program's requests for write operations should run unchanged. Note that the OS/2 function had to be declared as an external name with the "far" attribute and that a variable named when was added to the data segment of the application to receive the actual bytes written. stdin equ 0 ; handle for standard input stdout equ 1 ; handle for standard output stderr equ 2 ; handle for standard error extrn DOSWRITE:far ∙ ∙ ∙ msg db 'This is a sample message' msg_len equ $-msg wlen dw ? ∙ ∙ ∙ mov dx,seg msg ; DS:DX = address of message mov ds,dx mov dx,offset DGROUP:msg mov cx,msg_len ; CX = length of message mov bx,stdout ; BX = file or device handle call write ; perform the write jc error ; on return, CY=true if error cmp ax,msg_len ; were all characters written? jne dev_full ; no, output device is full ∙ ∙ ∙ write proc near ; Write to file or device ; Call with: ; BX = handle ; CX = length of data ; DS:DX = address of data ; Returns: ; If successful, Carry clear ; and AX = bytes written ; If error, Carry set ; and AX = error code push bx ; handle push ds ; long address of data push dx push cx ; length of data push ds ; address of variable to mov ax,offset DGROUP:wlen ; receive length written push ax call DOSWRITE ; transfer to OS/2 or ax,ax ; did write succeed? jnz writerr ; jump, error was returned mov ax,wlen ; no error, OR cleared CY ret ; and AX := bytes written writerr: stc ; write error, return CY set ret ; and AX = error number write endp ∙ ∙ ∙ Figure 10: This simple module definition (.DEF) file defines MYPROG.EXE as a protected-mode application rather than a dynamic link library and declares that the code segment is movable and discardable (a fresh copy can be loaded when needed, so swapping is not necessary), the data segment is movable and swappable but not discardable (it contains read/write data items), and the stack size is 4,096 bytes. NAME MYPROG PROTMODE DATA MOVEABLE CODE MOVEABLE PURE STACKSIZE 4096 ████████████████████████████████████████████████████████████████████████████ Microsoft Windows 2.0: Major Enhancements Offer More Control ─────────────────────────────────────────────────────────────────────────── Also see the related article: The Beta Version of Windows 2.0 Update ─────────────────────────────────────────────────────────────────────────── Michael Geary☼ Windows Version 2.0 is Microsoft(R)'s first major revision of its windowed operating environment. This updated version features the new overlapping- window user interface that will be used in the OS/2 presentation manager. Unlike the presentation manager, however, Windows 2.0 uses the same programming interface as Windows 1.x, albeit with a number of new functions and messages, and retains, for the most part, upward compatibility with existing Windows applications. Most .EXE files will run with no alterations. Yet source code will require some minor revisions to iron out several incompatibilities and bring it in line with changes made to the application style. Once you convert to Windows 2.0, you'll be rewarded with a fine assortment of new function calls and messages that make Windows a much more flexible, capable product for the applications developer. User Interface The most obvious change in Windows 2.0 is, of course, the new user interface. (For a closer look at the features of this interface, see "OS/2 Windows Presentation Manager: Microsoft Windows on the Future," MSJ Vol. 2, No. 2.) For the most part, this won't affect your programs. If an application ran in a tiled window, it will now simply use an overlapping window instead──and most likely will not even notice the difference. However, you should make several changes in the menu structure and keyboard accelerators so that your application will conform to the new user interface guidelines. Applications should no longer add menu items to the System menu. At the bottom of the File menu, add a separator line and the menu item Exit, which will allow the user to exit the application. Then move the "About..." item from the System menu to the end of the File menu, appending the name of your application here. Help menu items should now be flush right in the menu bar, and there is a new HELP dialog option to allow this. The Edit menu items remain unchanged, but the keyboard accelerators are different and are also shown in a different manner. Therefore, you should change your resource file and any special code you may have to handle the editing keys. Figure 1 gives the editing key assignments. You can now specify which letter of each menu item can be used in combination with the Alt key as a shortcut for making a menu selection. Simply insert an ampersand (&) in the item name preceding the shortcut letter, and Windows will underline it. For examples of this, see Figure 2, which shows a portion of an .RC file in the style recommended for the File and Edit menus. You can also add keyboard mnemonics to your dialog boxes. As with menu items, insert an ampersand at the appropriate point in the text of the dialog item. For push buttons, radio buttons, and check boxes, you can put a shortcut letter right in the name. You can't do this with an edit control, of course, but you can instead put a shortcut letter in an adjacent static text control. When the user presses the shortcut key, it's interpreted as a tab to the next tab stop after the static control. MDI A major new style recommendation for both Microsoft Windows 2.0 and the OS/2 presentation manager is the Multiple Document Interface (MDI). This is a method for using child windows in an application that works on multiple documents or multiple views of one document. Instead of creating a top-level overlapping window for each document or view, the application creates a single main window called the application workspace, which contains a child window for each document or view. These child windows can overlap and can be moved, sized, minimized, and maximized, just like top- level windows. The difference, of course, is that all the child windows are clipped at the edge of the workspace window. The active child window has its title bar highlighted along with the parent window's title bar. Each child window has a System menu, but none can have its own menu bar. The workspace window contains the menu for the entire application. If different menus are needed for the different child windows, the application should change or add items in the workspace window's menu as appropriate. Any child window can be maximized, that is, enlarged to fill up the workspace window, hiding all the other child windows. The user can still flip through the different child windows, but each one in turn becomes maximized. The workspace window has a pop-up Window menu, which lists all the child windows so that any one can be easily selected, whether it's visible or not. For example, a sequencer program for controlling musical instruments via the Musical Instrument Digital Interface (MIDI) might have these child windows in its application workspace: ■ conventional music (staff) notation ■ piano-roll notation ■ a piano keyboard ■ synthesizer voice editing ■ miscellaneous windows for tempo control, score formatting, key signature, MIDI options, and so on. If these were all independent top-level windows, it would be a real chore for the user to switch from the sequencer program to, say, Windows Write. There would be a good half-dozen windows cluttering up the screen. With the Multiple Document Interface, these windows would all be contained in a single application workspace window, and the user could get them all out of the way by simply minimizing the application window (that is, by changing it to an icon). Thus, MDI helps organize applications as independent entities, cleanly separating the windows of different programs. This will become more important as users really start to make use of the multitasking abilities of Windows and OS/2. Although Windows provides several new features that let developers create possible MDI applications, such applications require a fair amount of code to support MDI. The Windows 2.0 Application Style Guide details what's needed in an MDI application. Scheduling and Input Windows 2.0 has nearly 60 new functions, and several of the old functions have been extended with new options. Many of the changes were made to open up the Windows architecture, giving the application designer more control. Many things that just couldn't be done in earlier versions of Windows are now simple function calls. One change is relatively invisible to applications but quite welcome to the developer: pulling down a menu no longer stops scheduling, so applications continue to run when a menu is pulled down. But what of those cases in which an application really needs to turn off scheduling? For example, a forms-design program may want to let the user drag a selection rectangle across the entire screen when creating a pop-up window. The program can't yield to other applications, because it is temporarily writing into their screen space. In Windows 1.x, there was no way to turn off scheduling and still be able to determine when the mouse button was released. You couldn't use PeekMessage or GetMessage, because they would yield to other applications. Nor would GetKeyState work, because this gives you the key state at the time of the last message, not the real-time key state. Now, both of these methods are available. A new option in the PeekMessage function lets you choose not to yield to other applications. Complementing this is a new GetAsyncKeyState function, which resembles GetKeyState but gives you the real-time key state that is not synchronized with the message queue. A related function for keyboard control is GetKeyboardState, which gives you the current state of all 256 virtual keys in a buffer you provide. Its inverse, SetKeyboardState, lets you set the current state of the 256 virtual keys. Another useful function is GetInputState, which returns a Boolean value indicating whether any mouse, keyboard, or timer events are waiting in the system queue. And if the old limit of eight messages in your application's queue isn't enough, you can call SetMessageQueue to change your queue size. Rounding out the input functions are a couple of handy Get functions. GetCapture tells you which window (if any) has captured the mouse with SetCapture. GetCaretPos is the complement of SetCaretPos, returning the current position of the blinking caret. (Actually, GetCaretPos has always been available in Windows──it just wasn't documented until now.) Creating Windows The CreateWindow function has some new style bits. Since a window can now have Minimize and Maximize icons in its title bar, there are style bits for these. Also, some of the other style flags have acquired new names. If you feel silly using WS_TILEDWINDOW when your window is no longer tiled, you can use the more accurate but functionally identical WS_OVERLAPPEDWINDOW. More important, you can now specify the initial location and size of a top-level window. You're no longer at the mercy of the tiling algorithm. If you want to create two windows side by side, you can do it. You can still let Windows assign a default size and location, though. You just use the special value CW_USEDEFAULT (which happens to be 0x8000) for the position and size parameters to CreateWindow. Now we come to a slight anomaly. A Windows application certainly isn't passing 0x8000 for its position and size. More likely, it's passing zeros for all four values, thinking it's creating a tiled window. Windows assigns default values in this case, just as it used to. But, if you try passing zeros to CreateWindow in your Windows 2.0 application, you'll get a very tiny window instead. What's going on here? There certainly aren't two different CreateWindow functions. The key here is that the Resource Compiler sets a version number flag in your .EXE file, and Windows uses that to decide how to interpret some function parameters, such as the position and size in CreateWindow. If it encounters a Windows 1.x application, it ignores the position and size as before. But if it sees the 2.0 flag, it pays attention to those parameters. Window Management The window management capability of Microsoft Windows 2.0 really made me happy, since Windows 1.x is frustratingly limiting in this area at times. When using Windows 1.x, I knew it was maintaining a linked list of my windows, but I couldn't manipulate the list at all. For one of my own programs, this was a definite problem. I really needed a way to change the order of windows in the list and step through the list backwards and forwards as the dialog manager does. Previous versions of Windows just don't allow that. The only functions that come close are BringWindowToTop and EnumWindows. Windows 2.0 gives you all kinds of direct access to the window list. Let's start with GetWindow and GetNextWindow. These functions take a window handle and return your choice of next sibling, previous sibling, first sibling, last sibling, parent, or first child. And you can navigate in the same way that the dialog manager does. Next, we have SetWindowPos, an all- purpose window rearranger. It duplicates some of the functions of MoveWindow and ShowWindow but also has the capability to change the ordering of the window list. If you have Windows A and B, for example, you can place window A after (underneath) window B and even resize and move the window at the same time. If that's not enough, there's SetParent. This lets you change the parent of a child window──you can actually move a child window from one parent to another. Next, we have ShowOwnedPopups, which lets you show or hide all the pop-up windows associated with a window. Similarly, with ShowScrollBar you can show or hide a scroll bar (or both scroll bars) without having to fiddle with the scroll bar range. To round out window management, there are a few more functions. GetTopWindow returns the topmost child of a parent window, GetWindowTask returns the task handle for a window, and EnumTaskWindows resembles EnumWindows but enumerates only windows belonging to a specified task handle. Window Drawing Windows 2.0 contains several new GDI functions. Most important of these are EnumMetaFile and PlayMetaFileRecord, which together are the equivalent of PlayMetaFile. EnumMetaFile, however, calls a function you provide instead of just playing the file. It passes each metafile record to your function, where you can do whatever you want with it. If you just want to play it, call PlayMetaFileRecord. The new Microsoft Windows Software Development Kit documentation gives all the details of the metafile format, another case in which Microsoft has really opened up the Windows architecture. The TextOut function now lets you specify how the text will be aligned with the x-y starting point. Previously, that point could correspond only to the top left corner of the text, but now you have the choice of top left, top right, left center, top center, bottom center, or font baseline. You can also specify whether TextOut updates GDI's current position. These options form the text alignment flag and are part of the new SetTextAlign function. (The calling sequence for TextOut itself isn't changed.) There's also a GetTextAlign function to retrieve the current text alignment flag. DrawText has a couple of new options, too. There's a DT_CALCRECT option, which updates the formatting rectangle to show how much space DrawText actually used. Also, DrawText now interprets the ampersand as a flag to underline the next character; if you want to display an ampersand, you need to use two of them (&&). You can defeat this action with the DT_NOPREFIX option. For the font mapper, there's a new SetMapperFlags function. This lets you alter the font mapper's algorithm, determining how much priority it places on matching the aspect ratios of the font and the output device. There's one new graphics output function, Chord, which is the same as Arc except it draws the line segment as well as the ellipse segment, filling in the enclosed area with the current brush. For bitmaps, there's CreateDiscardableBitmap, which duplicates CreateCompatibleBitmap but makes the bitmap discardable. A new scrolling function, ScrollDC, is similar to ScrollWindow but allows more control over the scrolling. Instead of letting the uncovered area accumulate in the window's update region, it passes the uncovered region and its bounding rectangle back to the caller. Finally, there are two new region functions. ExcludeUpdateRgn is related to ExcludeClipRect, but it removes a window's update region from a clipping region. This could be useful when you're doing some drawing in advance of a WM_PAINT message and want to draw only in the region that will not be taken care of by the WM_PAINT. SetRectRgn is just like CreateRectRgn, but instead of allocating a new region, it takes an existing region and sets it to the specified rectangle. Dialog Boxes Have you ever wished you could create a dialog box on the fly at run time instead of having to use a dialog template from the resource file? Previously you had to give explicit CreateWindow calls for all the child windows in the dialog box. There was no way you could just set up a dialog template data structure and let it run. Now two new functions let you do just that. CreateDialogIndirect and DialogBoxIndirect correspond to CreateDialog and DialogBox, but instead of taking a resource from your .EXE file, they take a pointer to a dialog template in memory. (Actually, DialogBoxIndirect takes a global handle, and CreateDialogIndirect takes a far pointer.) You are no longer forced to choose between using a canned dialog box or using dozens of CreateWindow calls. Two other functions, GetNextDlgGroupItem and GetNextDlgTabItem, let you scan items the way the dialog manager does in response to the arrow and tab keys. In fact, I'd wager that these functions were there all along──because the dialog manager needed them──but were never made publicly available. This seems to be yet another example of the architecture being opened up to let you get at things previously hidden. Control Windows There aren't too many changes in the various types of control windows, but radio buttons have been given additional functionality. You can now create an automatic radio button. This is just like any other radio button, except that when clicked it automatically checks itself and unchecks all the other radio buttons in its group. There's always been a CheckRadioButton function, of course, but this was for dialog boxes only. If you wanted to add a group of radio buttons to some other kind of window, you had to write your own code to check and uncheck them. Now the buttons are smart enough to take care of themselves. An enhancement of both radio buttons and check boxes gives you the option of having label text appear on the left rather than the right side of the graphic. Menus Menus used to be write only, at least in part. For example, given a menu position, you could get the corresponding text string with GetMenuString, but there was no way to find out what the menu ID was. This made it difficult to write helper utilities that would send menu commands to other applications, a process that required a menu ID. Three new functions have overcome this limitation. GetMenuItemID takes a menu position and menu handle and returns the menu item ID. GetMenuItemCount tells you how many items are in either a top-level or a pop-up menu. GetMenuState allows you to determine what the menu flags (MF_ENABLED and so on) are for a menu item. The new LoadMenuIndirect function replicates LoadMenu, but instead of loading the menu resource from your .EXE file, it takes a pointer to a menu resource in memory. This makes it easy to build a menu on the fly without having to go through a whole series of CreateMenu and ChangeMenu calls. Like the new indirect dialog box calls, this function provides a nice middle ground between a static menu from the .EXE file and all the explicit code it would otherwise take to build a menu. Window Hooks Window hooks are now almost fully documented and functional. They are no longer among the most mysterious aspects of Microsoft Windows. The SetWindowsHook function can install any of the hook functions listed in Figure 3. Two new functions help support hook functions. DefHookProc lets a hook function chain to the previous hook function, so that there can be more than one hook of the same type. UnhookWindowsHook removes a hook function from a hook chain──something that before could only be done by exiting from Windows. They haven't taken all the mystery out of Windows, though. Along with the documented hooks, WINDOWS.H shows three new ones that are undocumented: WH_CBT, WH_SYSMSGFILTER, and WH_WINDOWMGR. Memory Management Windows now comes equipped with a kind of long-term equivalent of GlobalLock, called GlobalWire. When you allocate global memory and declare it FIXED, Windows allocates the space at the low end of memory to avoid fragmentation problems. If you allocate MOVEABLE memory, Windows doesn't worry about where it's located; then if you issue a GlobalLock, Windows just locks this memory in its current location for the sake of speed. This is fine for a short-term lock, but if you lock down memory for a long time, the memory space becomes fragmented, causing all sorts of memory management problems. GlobalWire prevents fragmentation by allocating a MOVEABLE memory block to low memory, as if it had been declared FIXED in the first place. GlobalUnwire unlocks memory that had been moved and locked with GlobalWire. SetSwapAreaSize lets you increase the amount of global memory dedicated to code rather than data. Normally, Windows allows nearly all global memory to be allocated for data with GlobalAlloc, reserving just enough space for the largest single code segment from any application or library. Although flexible, this arrangement will cause Windows to slow down severely if memory gets extremely tight, because it has to keep swapping code segments in. With SetSwapAreaSize, you can increase the amount of memory dedicated for code. This reduces the amount of global data you can allocate but prevents Windows from doing a great deal of low-memory thrashing. ValidateFreeSpaces is a new debugging function that helps you catch wild pointers. In the debugging version of Windows 2.0, the kernel fills all free memory with 0x0C. ValidateFreeSpaces scans all the free memory, and if it finds any byte that's not equal to 0x00C, it returns that address. If you run into a memory-clobbering problem, frequent calls to this function could help track it down. Miscellany Windows now has global atoms. These are like local atoms, but they are allocated from the global heap instead of your local heap and are shared among all applications. Four new functions serve as the global equivalents of the local atom calls: GlobalAddAtom, GlobalDeleteAtom, GlobalFindAtom, and GlobalGetAtomName. A few more miscellaneous new functions: EqualRect simply compares two rectangles and returns a Boolean value indicating whether they're equal. GetTickCount returns the number of milliseconds since the system was started. EnableHardwareInput enables or disables mouse and keyboard input. Figure 4 lists a number of new or enhanced messages. Upward Compatibility Most existing Windows applications will run under Microsoft Windows 2.0, but a change made to CreateWindow will affect nearly every application──fortunately, this should be the only real incompatibility for most programs. The position and size parameters of CreateWindow now have different meanings. The parameters (0,0,0,0) will still work, but the window will start out as being very tiny. You must either put in the CW_USEDEFAULT parameters or give an explicit position and size. If you do the latter, don't assume a particular screen size──use GetSystemMetrics. (0,0,0,0) on a Windows 1.x application gives the same result as CW_USEDEFAULT. WM_NCxxxxxx messages can cause problems. Most programs pass the nonclient messages through to DefWindowProc. If you process any of these messages to do any kind of special nonclient area handling, expect some trouble. You may also have problems if you have created a pop-up window that the user can iconize or zoom, since you probably had to fiddle with the WM_NCxxxxxx messages to get this to work. Since your application will be in an overlapping window anyway, you can eliminate the problem by just removing code (the easiest way to fix a bug). Any code that depended on the order of the System menu items or their exact functions will probably fail, since these have been changed. It's no problem if you just added an "About. . ." box to the System menu, although you will want to move it to the File menu in order to stay consistent with other applications. The last parameter to PeekMessage used to be called bRemoveMsg and was treated like most Booleans──zero meant false and nonzero meant true. If your code uses a value other than 0 or 1 for this parameter, you could run into trouble, because the parameter has been extended. It's now called wRemoveMsg, and the 0 and 1 values work as they always did, but any other values will have different meanings. For example, a value of 2 would be interpreted as PM_NOYIELD | PM_NOREMOVE instead of PM_REMOVE (TRUE). You're not likely to run into this problem, but if you do, it could be hard to track down. It's worth checking all your PeekMessage calls. (Do this at the same time you convert your CreateWindow calls.) Since DrawText now attaches a special meaning to the ampersand, this function won't do what you want if you have one in the string. You must either change it to a double ampersand or add the DT_NOPREFIX option to the call. A few of the things that Windows 1.x let you get away with aren't tolerated by Windows 2.0. Strictly speaking, they were errors all along──they just weren't caught before. I've run into a couple of these so far, but there are probably more. The one that gave me a little trouble converting to Windows 2.0 was GetParent(NULL). It's not legal to pass a NULL window handle to GetParent, but earlier versions of Windows accepted it and returned a NULL. Windows 2.0 (at least in the debugging version) will reject it. The other problem occurred in Microsoft's CUBE demo program. This program loads a binary resource into memory with LoadResource and then locks it with GlobalLock. That's not kosher at all, but somehow it worked before. The correct procedure is to use LockResource instead of GlobalLock. New Documentation The new programmer's documentation is one of the nicest features of Windows 2.0. The manuals have been extensively revised, with excellent tutorial discussions of such areas as window creation, painting, and scrolling. Unlike the old Windows manuals we've all struggled with, the new programmer's manual actually explains things. Newcomers to Windows programming will now have a much easier time learning the ropes. Besides containing new explanatory and overview material, the manual has been reorganized to list all the functions and messages in alphabetical order. No more scratching your head trying to figure out whether SetRect is a GDI or User function. TILER Despite all the enhancements made to Windows 2.0, I really miss one feature of the old version: tiled windows. As nice as the new overlapping windows are, tiled windows can be very handy too. So, which will it be, friends──New Windows or Windows Classic? Since we're talking about software, not soft drinks, why not have both? To this end, I wrote a program called TILER, which does just what its name implies──it moves and resizes your windows into a tiled arrangement. TILER runs as an icon and gives you two menu items to choose from: Tile Columns and Tile Rows (see Figure 5☼). When you select either of these menu options, TILER searches for windows that it can rearrange and then tiles them. According to which option you choose, TILER will first try to place your windows either in columns (side by side) or in rows (one above the other). Figures 6 and 7☼ show the different ways TILER will rearrange overlapping windows. This demonstrates the greater versatility of Windows 2.0. Windows 1.x allowed only a single tiling arrangement, with columns taking precedence. There was no way you could arrange your windows in rows (see Figure 8☼). A program such as TILER, which resizes the windows of other applications, could not even be written under the old Windows, which strictly prohibited an application from controlling the size of any tiled window, even its own. Now an application can resize its own windows or those of other applications, and no particular window arrangement is mandatory. A good tiling algorithm can be fairly complex. Because of space limitations, the version of TILER given in Figures 9, 10, 11, and 12 uses a very simplified algorithm that merely divides the screen into equally sized rows or columns. In fact, TILER will handle no more than four windows, simply leaving any additional windows where they were, hidden underneath the tiled windows. Even with this limitation, TILER is fairly useful. But you can download a more sophisticated version of TILER, which allows more-flexible window placement, from Microsoft's DIAL service. Initializing TILER TILER's WinMain resembles the main programs of most Windows applications. Yet, while most Windows applications allow several copies of themselves to be run concurrently, this wouldn't be useful for TILER, so it just beeps and exits if you try to run more than one instance of it. TILER calls its Initialize function to create and display its icon. Then it settles into a GetMessage/DispatchMessage loop, which pulls messages from the application queue and routes them to TILER's window function, TilerWndProc. As in most Windows applications, the real work consists of processing these messages. You may notice there's no TranslateMessage call in TILER's message loop. This function is needed only when your application wants to use WM_CHAR messages to get ASCII keyboard input. Since TILER does not require WM_CHAR, it doesn't have to call TranslateMessage. Similarly, there's no TranslateAccelerator call, since TILER doesn't use any keyboard accelerators. TILER's Initialize function registers its window class and creates its window using the RegisterClass and CreateWindow functions found in nearly every Windows application. It also adds Tile Columns and Tile Rows to TILER's System menu. Although it's generally not a good idea for applications to add items to their System menus, this is quite appropriate for a program like TILER, which has no other menu. (Indeed, since TILER runs as an icon, there would be no place to display another menu if it had one.) Initialize performs a little trick to determine how much of the screen the tiled windows should take up. We don't want to use the full screen, since we have to leave room at the bottom for icons, just as in the previous version of Windows. So TILER first creates a normal (noniconic) but invisible window and tells Windows to assign a default window size and position. The bottom edge of this default window happens to be just where we want the bottom of the tiled windows to be, so TILER saves that value as the bottom coordinate of TileRect. Finally, Initialize makes the window visible with the ShowWindow function call. The SW_SHOWMINIMIZED parameter to ShowWindow converts the window into an icon. (This parameter is the same as SHOW_ICONWINDOW in Windows 1.x.) Take a Message TILER's window function, TilerWndProc, handles only three messages specially. It passes all others along to DefWindowProc, which performs Windows' default message processing. This lets TilerWndProc concentrate on just the things it needs to do. The most important message for TILER is WM_SYSCOMMAND, since this is sent when the user selects an item from the System menu, where TILER's two menu items are located. TilerWndProc checks to see whether the message originates from a TILER menu item. If so, it processes the message by calling the TileWindows function. All other WM_SYSCOMMAND messages are passed on to DefWindowProc so Windows can handle the other System menu commands. Another message useful to TILER is WM_QUERYOPEN. Windows sends this message whenever the user tries to open the icon, that is, change it back into an open window. Since TILER runs only as an icon, it replies to this message by returning a 0, so Windows won't open it. Finally, the WM_DESTROY message tells TILER that the user has selected the Close item from the System menu. TilerWndProc then calls PostQuitMessage to tell the main application loop to terminate the next time through. Tiling Algorithm TILER really gets down to business with the TileWindows function. First, TileWindows calls CalcWindowRects, which loops through the active windows, calculating new sizes and positions for them. CalcWindowRects is the actual tiling algorithm. If there's just one window, it gets the entire tiling area, of course. Two windows split the area in half, either horizontally or vertically, depending on which menu item was selected. If there are three windows, the first one gets half the tiling area, and the second and third split the remaining half. Four windows get the four corners of the tiling area. With more than four windows, TILER just tiles the first four and leaves the remaining windows hidden underneath them. In the more sophisticated version of TILER available on DIAL, CalcWindowRects can handle more than four windows and is sensitive to how the user had already sized them. For example, if the user makes the topmost window larger, the enhanced TILER will shrink the other windows to fit instead of arbitrarily splitting the screen in half (see Figure 13☼). To keep things simple, TILER attempts to tile only top-level overlapping windows that are resizable and are not minimized (iconic) or maximized (zoomed). The IsTileable function determines whether a window fits these criteria, and CalcWindowRects ignores the window if IsTileable returns the value FALSE. CalcWindowRects is much simpler than it would have been in Windows 1.x, because it uses the slick new GetWindow function. Anyone who has suffered through having to use the clumsy EnumWindows will appreciate GetWindow, which lets you write an ordinary C loop to scan through the window list instead of forcing you to use a "call back" function. Figure 14 makes it clear just how much easier it is to use GetWindow instead of EnumWindows. Once the window rectangles are calculated, the TileWindows function moves the windows into place with the new SetWindowPos function. SetWindowPos also sends the WM_MOVE and WM_SIZE messages to the windows to notify them that they have been moved. The old MoveWindow function would have done the job here, but SetWindowPos does two things at once: besides setting the window positions, it reorders the window list, so the Alt-Tab and Alt-Esc key combinations will scan through the tiled windows in a natural order. TileWindows performs one other function here. It checks each window to see whether the CS_HREDRAW or CS_VREDRAW bits are set. If they are, it invalidates the window, causing it to be repainted. This is necessary because some Windows applications──notably CLOCK──do not properly respond to the WM_SIZE message but depend on those redraw flags to generate a WM_PAINT message. Programs like CLOCK that change their displays to fit the window size should really respond to the WM_SIZE message and recalculate their displays at that point. But CLOCK doesn't. If you feel like experimenting, remove the InvalidateRect call from TileWindows, and watch what happens when CLOCK is one of the windows you're tiling. You'll get some strange- looking results. Building TILER To compile and link TILER, you will need the Microsoft C Compiler, Version 4.0 or 5.0, and the Windows Software Development Kit, Version 2.0. Put the TILER.C source code, TILER.RC resource file, TILER.DEF module definition file, and TILER make-file (see Figures 9, 10, 11, and 12) in a directory, and make up a TILER.ICO icon file with the Icon Editor. Then issue the command MAKE TILER to build TILER.EXE. Figure 1: Editing Key Assignments Function Assignment Undo Alt+BkSp Cut Shift+Del Copy Ctrl+Ins Paste Shift+Ins Clear Del Figure 2: This .RC file establishes the File and Edit menus and their accelerators. MyApp MENU BEGIN POPUP "&File" BEGIN MENUITEM "&New" , CMD_NEW MENUITEM "&Open..." , CMD_OPEN MENUITEM SEPARATOR MENUITEM "&Save" , CMD_SAVE MENUITEM "Save &As..." , CMD_SAVEAS MENUITEM SEPARATOR MENUITEM "E&xit" , CMD_EXIT MENUITEM "A&bout My Application..." , CMD_ABOUT END POPUP "&Edit" BEGIN MENUITEM "&Undo\tAlt+BkSp" , CMD_UNDO MENUITEM SEPARATOR MENUITEM "Cu&t\tShift+Del" , CMD_CUT MENUITEM "&Copy\tCtrl+Ins" , CMD_COPY MENUITEM "&Paste\tShift+Ins" , CMD_PASTE MENUITEM "C&lear\tDel" , CMD_CLEAR END MENUITEM "\a&Help" , CMD_HELP, HELP END MyApp ACCELERATORS BEGIN VK_DELETE, CMD_CUT, VIRTKEY VK_DELETE, CMD_CLEAR, VIRTKEY, SHIFT VK_INSERT, CMD_COPY, VIRTKEY, CONTROL VK_INSERT, CMD_PASTE, VIRTKEY, SHIFT VK_BACK, CMD_UNDO, VIRTKEY, ALT END Figure 3: Hook Functions Installed by SetWindowsHook ╓┌───────────────────┌───────────────────────────────────────────────────────╖ Hook Function Description WH_MSGFILTER Message filter. Called when a dialog box, message box, or menu has received a message but before it has processed the message. WH_KEYBOARD Keyboard filter. Called when the application calls GetMessage or PeekMessage and there is any keyboard input pending. WH_GETMESSAGE GetMessage filter. Called from inside the GetMessage function before it returns the message to the application. (Debugging version of Windows only.) WH_CALLWNDPROC Window function filter. Called whenever any message is sent to a window function. (Debugging version of Windows only.) Hook Function Description WH_JOURNALRECORD Journal recording. Called on every event message (keyboard, mouse, or timer message) for any application. WH_JOURNALPLAYBACK Journal playback. Plays back messages recorded by WH_JOURNALRECORD. With these two hook functions, you could write a "ProMouseKey" program for Windows. Figure 4: New Messages in Windows 2.0 ╓┌───────────────────┌───────────────────────────────────────────────────────╖ New Messages Description BM_SETSTYLE Alters the style of a button. With this message, you can change a button control from its current style to any other style. The main purpose of the message is to allow the new user interface trick where any push button you tab to becomes the default push button. New Messages Description button you tab to becomes the default push button. BN_DOUBLECLICKED Notifies the parent window when the user double-clicks a button control. EM_LINEFROMCHAR Returns the line number for a given character position in an edit control. EM_SETWORDBREAK Lets you specify a word-break function to replace the default word-break function (which breaks on a blank). EN_UPDATE Just like EN_CHANGE, except that it is sent before the edit control displays the changed text instead of after. The text has already been formatted, so you can issue an EM_LINELENGTH or EM_GETLINECOUNT and resize the edit control accordingly. This makes it easy to create an edit control that expands and shrinks to fit its text. With this message, I was able to replace about 50 lines of very tricky code in one of my programs with three lines of utterly simple New Messages Description one of my programs with three lines of utterly simple code. WM_CHILDACTIVATE Sent to a child window's parent when the SetWindowPos function moves the child. WM_GETMINMAXINFO By default, when you Maximize a window, it fills the full screen. By replying to this message, you can specify a location and size that your window will take on when it's Maximized. You can also specify the minimum and maximum tracking size (for when the user sizes the window). WM_NCHITTEST Returns new values to indicate when the mouse is in one of the thick borders or in the Minimize or Maximize icons. WM_SHOWWINDOW Several new options to support the new Minimize, Maximize, and Restore commands. New Messages Description WM_SYSCOMMAND New option to support the Restore command. Figure 9: Make-File for TILER.EXE tiler.obj: tiler.c msc -AS -Gcsw -Ox -u -W3 -Zdp $*; tiler.res: tiler.rc tiler.ico rc -r tiler.rc tiler.exe: tiler.obj tiler.res tiler.def link4 tiler, tiler/align:16, tiler/map/line, slibw, tiler.def rc tiler.res mapsym tiler Figure 10: DEF File for TILER.EXE TILER.DEF NAME Tiler DESCRIPTION 'Windows Tiler by Michael Geary' STUB 'WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS Figure 11: RC File for TILER.EXE TILER.RC /* Tiler.RC - resource file for TILER.EXE */ #include Tiler! ICON tiler.ico Figure 12: Code for TILER.C TILER.C /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - *\ * Tiler.c * * Windows 2.0 Tiling Utility * * Written by Michael Geary * \* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #include /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ #define MAXINT 32767 /* Menu command definitions */ #define CMD_TILECOLS 1 #define CMD_TILEROWS 2 /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ typedef struct { HWND hWnd; RECT rect; } WINDOW; WINDOW Window[4]; /* Window info for each tiled window */ int nWindows; /* How many windows we will tile */ HANDLE hInstance; /* Our instance handle */ int hWndTiler; /* hWnd of our icon */ RECT TileRect; /* Overall tiling rectangle */ char szClass[] = "Tiler!"; /* Our window class name */ char szTitle[] = "Tiler"; /* Our window title */ char szTileCols[] = "&Tile Columns"; char szTileRows[] = "Tile &Rows"; /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Declare full templates for all our functions. This gives us * strong type checking on our functions. */ void CalcWindowRects( BOOL ); BOOL Initialize( void ); BOOL IsTileable( HWND ); long FAR PASCAL TilerWndProc( HWND, unsigned, WORD, LONG ); void TileWindows( BOOL ); int PASCAL WinMain( HANDLE, HANDLE, LPSTR, int ); /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Calculate window rectangles for the four topmost tileable windows * and set up the Window array for them. This is a simple-minded * tiling algorithm that simply divides the tiling area into equal * rows and columns. */ void CalcWindowRects( bColumns ) BOOL bColumns; { HWND hWnd; int n; n = 0; for( hWnd = GetWindow( hWndTiler, GW_HWNDFIRST ); hWnd; hWnd = GetWindow( hWnd, GW_HWNDNEXT ) ) { if( ! IsTileable( hWnd ) ) continue; Window[n].hWnd = hWnd; CopyRect( &Window[n].rect, &TileRect ); switch( n ) { case 0: break; case 1: if( bColumns ) { Window[0].rect.right = TileRect.right / 2; Window[1].rect.left = Window[0].rect.right - 1; } else { Window[0].rect.bottom = TileRect.bottom / 2; Window[1].rect.top = Window[0].rect.bottom - 1; } break; case 2: if( bColumns ) { Window[2].rect.left = Window[1].rect.left; Window[1].rect.bottom = TileRect.bottom / 2; Window[2].rect.top = Window[1].rect.bottom - 1; } else { Window[2].rect.top = Window[1].rect.top; Window[1].rect.right = TileRect.right / 2; Window[2].rect.left = Window[1].rect.right - 1; } break; case 3: if( bColumns ) { Window[3].rect.right = Window[0].rect.right; Window[0].rect.bottom = TileRect.bottom / 2; Window[3].rect.top = Window[0].rect.bottom - 1; } else { Window[3].rect.bottom = Window[0].rect.bottom; Window[0].rect.right = TileRect.right / 2; Window[3].rect.left = Window[0].rect.right - 1; } break; } if( ++n == 4 ) break; } nWindows = n; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Initialize TILER. Assumes a single instance. * Returns TRUE if initialization succeeded, FALSE if failed. */ BOOL Initialize() { WNDCLASS Class; /* Class structure for RegisterClass */ HMENU hMenu; /* Menu handle of system menu */ /* Register our window class */ Class.style = 0; Class.cbClsExtra = 0; Class.cbWndExtra = 0; Class.lpfnWndProc = TilerWndProc; Class.hInstance = hInstance; Class.hIcon = LoadIcon( hInstance, szClass ); Class.hCursor = LoadCursor( NULL, IDC_ARROW ); Class.hbrBackground = COLOR_WINDOW + 1; Class.lpszMenuName = NULL; Class.lpszClassName = szClass; if( ! RegisterClass( &Class ) ) return FALSE; /* Create our window but don't iconize it yet */ hWndTiler = CreateWindow( szClass, szTitle, WS_OVERLAPPED | WS_SYSMENU, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, NULL, NULL, hInstance, NULL ); if( ! hWndTiler ) return FALSE; /* Since we took the default size, the bottom of our window is * the base Y coordinate for tiling */ GetWindowRect( hWndTiler, &TileRect ); TileRect.top = TileRect.left = -1; TileRect.right = GetSystemMetrics( SM_CXSCREEN ) + 1; /* Add our menu items to the System (Control) menu */ hMenu = GetSystemMenu( hWndTiler, FALSE); ChangeMenu( hMenu, 0, NULL, MAXINT, MF_APPEND | MF_SEPARATOR ); ChangeMenu( hMenu, 0, szTileCols, CMD_TILECOLS, MF_APPEND | MF_STRING ); ChangeMenu( hMenu, 0, szTileRows, CMD_TILEROWS, MF_APPEND | MF_STRING ); /* Now display our window as an icon */ ShowWindow( hWndTiler, SW_SHOWMINIMIZED ); return TRUE; } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Tells whether a window can be tiled, returns TRUE if so. * We will tile only top level, resizable windows that are not * minimized and not maximized. */ BOOL IsTileable( hWnd ) HWND hWnd; { DWORD dwStyle; dwStyle = GetWindowLong( hWnd, GWL_STYLE ); return( ! ( dwStyle & ( WS_POPUP | WS_MINIMIZE | WS_MAXIMIZE ) ) && ( dwStyle & WS_SIZEBOX ) && ( dwStyle & WS_VISIBLE ) ); } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Tiler's window function. */ long FAR PASCAL TilerWndProc( hWnd, wMsg, wParam, lParam ) HWND hWnd; /* Window handle */ unsigned wMsg; /* Message number */ WORD wParam; /* Word parameter for the message */ LONG lParam; /* Long parameter for the message */ { RECT rect; /* A rectangle */ switch( wMsg ) { /* Destroy-window message - time to quit the application */ case WM_DESTROY: PostQuitMessage( 0 ); return 0L; /* Query open icon message - don't allow icon to be opened! */ case WM_QUERYOPEN: return 0L; /* System menu command message - process the command if ours */ case WM_SYSCOMMAND: switch( wParam ) { case CMD_TILECOLS: TileWindows( TRUE ); return 0L; case CMD_TILEROWS: TileWindows( FALSE ); return 0L; default: break; } break; /* For all other messages, we pass them on to DefWindowProc */ default: break; } return DefWindowProc( hWnd, wMsg, wParam, lParam ); } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* This function actually tiles the windows. First, it calls * CalcWindowRects to determine the window positions. Then, it * loops through the windows and moves them into place. */ void TileWindows( bColumns ) BOOL bColumns; /* TRUE = tile columns; FALSE = rows */ { int n; HWND hWnd = NULL; CalcWindowRects( bColumns ); /* Assign window rectangles */ if( nWindows == 0 ) { MessageBox( hWndTiler, "There are no windows that can be tiled.", szTitle, MB_OK | MB_ICONEXCLAMATION ); return; } /* Move, size, and reorder windows */ for( n = 0; n < nWindows; ++n ) { SetWindowPos( Window[n].hWnd, hWnd, Window[n].rect.left, Window[n].rect.top, Window[n].rect.right - Window[n].rect.left, Window[n].rect.bottom - Window[n].rect.top, SWP_NOACTIVATE ); hWnd = Window[n].hWnd; if( GetClassWord( hWnd, GCW_STYLE ) & ( CS_HREDRAW | CS_VREDRAW ) ) /* Make sure it's redrawn */ InvalidateRect( hWnd, NULL, TRUE ); } } /* - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - - */ /* Application main program. */ int PASCAL WinMain( hInst, hPrevInst, lpszCmdLine, nCmdShow ) /* Our instance handle */ HANDLE hInst; /* Previous instance of this application */ HANDLE hPrevInst; /* Pointer to any command line params */ LPSTR lpszCmdLine; /* Parameter to use for first ShowWindow */ int nCmdShow; { MSG msg; /* Message structure */ /* Allow only a single instance */ if( hPrevInst ) { MessageBeep( 0 ); return 0; } /* Save our instance handle in static variable */ hInstance = hInst; /* Initialize application, quit if any errors */ if( ! Initialize() ) return FALSE; /* Main message processing loop */ while( GetMessage( &msg, NULL, 0, 0 ) ) { DispatchMessage( &msg ); } return msg.wParam; } Figure 14: EnumWindows vs. GetWindow /* UsingEnumWindows to loop through the windows */ void LoopThroughWindows() { FARPROC lpEnumProc; lpEnumProc = MakeProcInstance( MyEnumProc, hInstance ); EnumWindows( lpEnumProc ); FreeProcInstance( MyEnumProc, 0L ); } BOOL MyEnumProc( hWnd, lParam ) HWND hWnd; LONG lParam; { /* do something with hWnd here */ } And don't forget to EXPORT MyEnumProc, or Windows will crash! Now, with GetWindow: void LoopThroughWindows() { HWND hWnd; for( hWnd = GetWindow( hWndMyWindow, GW_HWNDFIRST ); hWnd; hWnd = GetWindow( hWnd, GW_HWNDNEXT )) { /* do something with hWnd here */ } } ─────────────────────────────────────────────────────────────────────────── The Beta Version of Windows 2.0: An Update ─────────────────────────────────────────────────────────────────────────── Just at press time, a new release of Windows 2.0 has come along that forces me to (happily) eat a few of my words. One of the joys of working with alpha- and beta-test software is that things change right under your feet as you're writing about them. In this case, I don't mind, because Microsoft has put in the GlobalNotify function. I call it GlobalNifty, because it lets you implement swappable data for your application. You are able to mark global memory as discardable, but if you set the GMEM_GNOTIFY flag on it, Windows will call your notification function when it wants to discard a memory block. You can then write it to disk before it gets discarded, which is much cleaner than the kludges we had to use before. There's also a LocalShrink function, which compacts your application's local heap and shrinks down your data segment to match. Before, if your local heap grew past the initial allocation determined by the .DEF file, your data segment grew as needed, but it never shrank back again; LocalShrink allows you to do that. RegisterWindowDestroy is an interesting new function. It's easy to find out when one of your own windows is destroyed, since it receives a WM_DESTROY message. You may want to find out when another application's window gets destroyed, such as in an add-on program that works with an existing application and should exit when that application exits. You can do this with a RegisterWindowDestroy call, which sets──actually increments──a destroy-notification flag on a window. Then, when that window is destroyed, Windows sends a WM_DESTROYOTHERWINDOW to all top-level windows to let everyone know about the window being destroyed. For text output, the new ExtTextOut function is like a TextOut with two added parameters: a rectangle that can be used for background erasing, text clipping, or both, and an optional array of character horizontal spacing values. This array lets you set your own spacing, character by character. You can find out the default horizontal spacing for a font with the GetCharWidth function, which loads up an integer array of spacing values for a range of characters in a font. GetDCOrg gives you the offset from screen to client coordinates for a display context. GetUpdateRgn copies a window's update region into an actual hRgn that you can then manipulate. IsZoomed tells you if a window is zoomed. (Zoomed is still zoomed, even if they do call it Maximized.) Finally, GetNumTasks returns the number of tasks that are currently running. One of the undocumented SetWindowsHook options I mentioned is now documented: WH_SYSMSGFILTER. This is the same as WH_MSGFILTER, except that it gives you the DialogBox/MessageBox/menu messages for all windows, not just your own window. A warning to those who would use the hook functions: when Windows calls your hook function, your DS register is set up for your data segment, but you're running on the stack of whatever application triggered the hook. Therefore, you will run into the infamous DS != SS problem, which means that you will have to compile that part of your code with the -Aw switch and cannot call certain C library functions. The Windows documentation lists which C run-time functions will and won't work in the DS != SS environment. This affects all the Windows hooks except for WH_MSGFILTER and WH_KEYBOARD. Windows 2.0 directly supports LIM/EMS and EEMS memory. These bank-switched memory schemes provide one or more "windows" (not related to Windows' windows) within the 1Mb real-mode address space. Any area of memory on the bank-switched card can be mapped into these memory windows. Windows 2.0 supports this by assigning each application its own bank(s) of memory. A single application doesn't get any more memory than it used to, but you can run more applications concurrently. The larger the banked memory window(s), the better. Windows will keep application code and data in banked memory if possible, or else will put as much code there as will fit. EEMS memory can do a much better job here than straight LIM/EMS memory, because of its larger bank windows. For the most part, the memory banking is transparent to applications, but it's important to note that the banking is done on an application-by- application basis, and that code and data belonging to other applications can be banked out. This means, for example, that you can't give another application a global handle to your default data segment, expecting the other app to do a GlobalLock on it. Nor can you attempt to get a window function address from another app and call it directly, bypassing SendMessage. That never was a good idea, but now it's a sure way to crash. The only methods for intertask communication that will work under the EMS/EEMS environment are the clipboard, a shared library's data segment, and the Dynamic Data Exchange (DDE) protocol. Memory allocated with the GMEM_DDESHARE global memory flag is treated specially, so any app can get to it. You can also force global memory to be allocated from the shared area with GMEM_LOWER. Avoid this if possible, since the shared memory area is a scarce resource. There is one new Windows function for EMS support──LimitEMSPages. This simply lets you set a limit on the amount of EMS memory that Windows will assign to your application. At the cost of some extra disk access (because all your code won't fit in memory), you can reduce the total memory used by your application. You can still explicitly allocate more memory with GlobalAlloc calls, regardless of the limit set with LimitEMSPages. ████████████████████████████████████████████████████████████████████████████ Keeping Up With the Real World: Speedy Serial I/O Processing ─────────────────────────────────────────────────────────────────────────── Also see the following related articles: MS-DOS(R) Comm Port Syntax Problems Status of the Transmit Shift Register Versus the Transmitter Holding XON/XOFF RS-232C: A Standard Problem Sample Code Fragments for TSRCOMM.ASM Combining Interrupts and Polling: An Adventure in Programming ─────────────────────────────────────────────────────────────────────────── Ross M. Greenberg☼ At first glance, writing a serial communications program for an MS-DOS(R) system looks fairly straightforward. Most high-level languages can interface easily with the BIOS, and the BIOS, by definition, knows how to interface with the hardware. After you've designed your program with pop-up windows, pull-down help screens, fancy sounds, and flashy colors, you'll probably build a stub to test your ideas. You may be quite surprised to learn that you're limited to under 1200 baud. Turning to the back of the nearest BIOS Technical Reference manual, you'll soon discover how few of the services that you consider basic are provided: there is no XON/XOFF processing, little hardware handshaking is performed, the baud rates that are allowed seem arbitrarily low, and, most important of all, there is no true asynchronous interrupt processing. Whether you are writing a modem communications program or just attempting to get proper XON/XOFF processing of your serial printer in your favorite word processing program, some enhancement of the system services available under MS-DOS in the handling of serial communications is necessary. This article examines the writing of a replacement handler for Interrupt 0x14, the serial communications interrupt. My design goals arose from real-life needs I encountered when writing a terminate-and-stay-resident (TSR) communications program. These goals included: ■ asynchronous interrupt-driven I/O of baud rates up to 38,400 ■ the ability to handle COM ports 1 through 4 ■ XON/XOFF flow control, on both incoming and outgoing flow ■ proper error detection of parity, overrun, and other errors ■ transparency for existing software ■ extensibility as requirements changed Getting Started Due to the inherent limitations of the BIOS serial communications service routines, MS-DOS must be bypassed to address the hardware directly. The moment you interface directly with the hardware within your computer, you lose that most precious of commodities, transportability between machines. However, compatibility is less of an issue now than it was in the past, and these routines work on all IBM-compatible machines, as well as on many clones. Although the real-world applications for the BIOS routines soon outstrip their abilities, the programmers' interface to these routines should be preserved: they are more or less a standard, and without them existing programs would not work properly. They permit you to initialize the comm port, send or receive a byte to or from the comm port, and return the status of the comm port. These interfaces are through Interrupt 0x14. (For the rest of this article, I'll refer to software interrupts as "calls.") Maintaining compatibility with the old interface while expanding above and beyond it led to some interesting design decisions. Precursor Writing code to stay resident after termination can be tricky, and adding in asynchronous interrupt processing can make things even trickier. When your code is called, the only things you know for sure are the code segment (CS) and instruction pointer (IP) of the CPU. Registers, including the Data Segment (DS) register, are pointing to unknown values. They must all be restored to their original values when you return from the interrupt service routine, with the CS and IP, as well as the flags of the CPU, restored immediately upon execution of the final IRET instruction. There are a variety of programming practices to get around this inconvenience. One method is addressing all data off the code segment using what is called a segment override in the assembler code itself. This, however, makes the code a little bigger and more cumbersome and requires a couple of extra clock cycles for any instruction that uses segment overrides, causing the code to execute more slowly. The method used in this program differs from that approach: immediately upon the service of an interrupt, the data segment is saved on the stack, and the code segment itself is used for data addressing. It's faster in the long run, both in the design cycle and in the actual execution of the code. Although there are portions of the code that deal directly with the hardware attached to the bus, whenever possible I've used transportable features of MS-DOS, including the MS-DOS calls for intercepting and taking over various BIOS interrupts. Overall Structure The code is structured logically into three areas. The invocation code is run only once; it sets up the interrupt structure and initializes data areas. The second component handles the actual asynchronous interrupts themselves, properly handling the two different types of interrupts: receive and transmit interrupts. Error interrupts are not generated in this scheme. Also, if a hardware interrupt is generated and does not come from the serial device itself, the interrupt is handed off to the original interrupt service routine, permitting devices that use the same IRQ (Interrupt Request) to be processed properly. Finally, the third portion of the code is a replacement for the original BIOS Interrupt 0x14. It allows for a great deal of extensibility to be added, though at the price of having some additional programming overhead. Saving Parameters One of the requirements is the ability to remove this program from memory when desired. This implies that the original parameters of the hardware must be saved and then restored upon program termination. Therefore, a structure for each comm port has been designed to hold all relevant initial data. In addition, this structure contains pointers to the beginning and end of the transmit and receive interrupt buffers. All of this data must be initialized properly, with interrupts turned off, before the actual BIOS interrupt vector is taken over and made to point to our own interrupt service routines. Once all of the parameters are saved and interrupt services are enabled, the preamble code executes a DOS Terminate and Stay Resident call, reserving memory before exiting to COMMAND.COM. Replacing Services Interrupt 0x14 is the entry point into the BIOS service routines for serial communications. By setting register half AH equal to a given function number and register DX to the desired comm port (starting with COM1 equal to zero), comm software then generates a software interrupt to invoke the given service (see Figure 1). I've extended the BIOS services with a new call, AH=4, and used the BX register to allow for some of the special operations that the new comm driver permits (see Figure 2). Although the interface to the user program looks the same, there are substantial internal differences. When a call is made to initialize the comm port, character ready interrupts are enabled, buffer pointers are reset, and then the actual hardware is reset to the proper baud rates, parity, word length, etc. When a call is made to return the status of the comm port, the comm port status is read and processed to reflect the combined status of both the comm port and buffers. This status can be further monitored by earlier calls with AH=4. However, since multiple interrupts can be caused by errors for each "get status" call you make, you may lose some of the actual error information. The method that most software employs to deal with error status is preserved, however: when an error is apparent, all errors are set in the status word if possible. When reading a character from the comm port, whether the character you're receiving is directly from the communications port or from the interrupt buffer will be transparent. One particularly interesting aspect of the code is its ability to detect an error, store the apparent character, and return an error condition in the high bit of AH. The code is also able to set a variable to specify a looping constant. This permits you to wait in the "get a character" routine until a character appears in the receive buffer or until the specified number of loops have been used up. As a replacement to the original BIOS routines, this is far superior to the constant usually specified. Again, you use the AH=4 extension to the serial communications command set to set this constant. If you want, you can set this looping constant to 1/18 of a second and use the hardware timer tick interrupt as a countdown timer for error status. When transmitting a character, there are two options (set with AH=4): transmit the characters with the use of interrupts and the transmit buffer or attempt to output each character one at a time. The interrupt-driven transmit method is much faster, of course, but it has certain disadvantages. Processing Interrupts The most important feature of the code is its ability to properly handle interrupts that occur very frequently──as often as two interrupts for each character transmitted or received. Some background information on how interrupts are generated and how they are handled by the hardware should help you to understand how this code works. The chip at the heart of the serial communications board in your MS-DOS machine is an 8250. The 8250 requires eight contiguous I/O ports, which are used to read and write serial data and to read and set various parameters (see Figure 3). One of these parameters determines under what conditions the 8250 should generate an interrupt signal on the bus. Depending on how the parameter is set, an interrupt can be generated when a new character has been received, when the transmit buffer is empty and ready for a new character, or when an error condition has been determined. Initially, the code is set to generate an interrupt only on error conditions or when a new character is ready to be placed in the buffer. However, when a character is to be transmitted and a check of the status of the 8250 shows it is busy, the possibility of queuing characters up in a transmit buffer, which transmits the characters asynchronously to any other process, becomes very attractive. This is made possible through one of the settings of the BIOS call with AH=4, which allows you to determine if a character should be queued into the transmit buffer. Otherwise, the code will be set to continue testing for a clear transmit buffer or a time-out and will not return until either condition has been satisfied. When an interrupt is received, it is routed through one of two different interrupt vectors, namely the interrupt handler of COM1 or COM2. They merely set the correct header and then jump to the actual interrupt handler. The interrupt handler first determines which kind of interrupt was generated and then jumps to the appropriate routine. There is a priority to each interrupt, with error conditions having the highest priority, followed first by data becoming apparent at the serial port, then by the transmit buffer being empty, and finally by a change in other portions of the modem status. Also, if this routine determines that the interrupt was not generated by the 8250, it will revector the interrupt to the original interrupt service routine. If the interrupt is determined to be the result of a character being received, the next character is read from the physical hardware port and stored in a circular buffer. If the circular buffer is more than 80 percent full and XON/XOFF processing is turned on, then an XOFF character is sent──if one hasn't already been sent. Even at high baud rates, the remaining 20 percent of the buffer should be sufficient for the remote end to notice the XOFF and cease sending characters before the receive buffer runs out of room. If the remote end does not stop sending characters in response to the XOFF, or if XON/XOFF processing was not turned on, characters that were received while the buffer was full will be discarded, and a BELL character will be sent to the remote machine. If the flag is set to indicate that an XOFF character was sent due to a high level of data, then an XON is sent when the amount of space left in the buffer increases to some predetermined level (set as an equate, all XON/XOFF high- and low-water marks can be modifed easily). This should allow for processing of enough characters to prevent the remote end from timing out. Error conditions are generated by a fault in the incoming stream of characters, which is detected by the 8250, and include such errors as an overrun (the character in the transmit buffer was not read before the next character arrived), parity (even, off, mark, or space parity was set, and the parity of the incoming character did not match what was expected), or framing error (a stop bit was not detected by the hardware when one was expected──usually indicative of wrong baud-rate errors). For each character received, both the error flag and error status of the chip are read, if the appropriate flag is set with AH=4. If an error condition is determined to exist, a bit is set in the error array. Subsequent reads of the character will return the actual character read (which may not be accurate), along with an error condition status set in AH in the same way that the current BIOS calls will set an error condition. For space considerations, the actual cause of the error condition is not saved, and the return code is set to indicate that all errors were apparent──a brute-force method that saves as much as 4 bits for each byte in your receive buffer. Only one of these errors requires any special handling: that is the recognition of a Break having been generated from the remote end of a serial connection. By an appropriate setting of the call extensions with AH=4, this can either be considered an error condition or can safely be ignored. When a Transmit Buffer Empty interrupt is processed, a quick check is made to determine whether any outstanding characters to be transmitted are in the transmit buffer. If there are no outstanding transmit requests, transmit interrupts are turned off, the interrupt is reset, and control is returned to the interrupted program. If there are outstanding characters to be transmitted, the character at the head of the circular transmit buffer is sent out the appropriate port; pointers, counters, and interrupts are reset; and control is returned to the interrupted program by means of the IRET instruction. This approach generates an additional interrupt after the final character is transmitted, but this is an acceptable trade-off for the advantage that you get from transmit interrupt processing. Programming the 8259 When the 8250 generates an interrupt, it is, in fact, only raising a signal on the interrupt line of the bus. Attached directly to these lines is the interrupt controller on the motherboard, the 8259. This chip is the interface between all interrupt generators and the CPU and is a rather intelligent and programmable device. The 8259 distinguishes which interrupt has been raised and then does some internal processing to determine what actions it should take. The concept of interrupts is a powerful one, and the 8259 expands on this power, allowing the programmer to rearrange certain parameters so as to practically rewire the hardware in his or her computer. Unfortunately, much of the power of the interrupt system is lost in IBM-compatible machines in which the ROM BIOS programs the 8259 and causes it to be reset in a less- than-polite way. One of the rules of the interrupt system is that interrupts usually have to be reset. Upon receipt of an interrupt on the bus, the basic architecture of the 8259 sets bits on an internal register to correspond to each interrupt. Each bit is usually prioritized so that the highest priority interrupt (IRQ 0) is bit 0, the next highest is bit 1 (IRQ 1), and so on. This priority can be modified as desired and is one of the most powerful features of the Interrupt Controller Chip. Once an interrupt has been received and entered into this register, the controller determines if the CPU is already handling an interrupt of equal or higher priority. If it is not, and if the CPU has allowed interrupts at all with an STI or Enable Interrupts instruction, a CPU interrupt is generated, and the actual interrupt vector (0-7) is placed on the data bus. Immediately upon receipt of an interrupt, the CPU turns interrupts off (which disables the 8259 from raising further interrupts), saves the flags and the current instruction address, and jumps to the appropriate vectored address as set in the interrupt table. Until the interrupt service routine (ISR) executes an STI or an IRET, which resets the flags from the stack, interrupts are effectively disabled. Generally, the flag word on the stack will have interrupts enabled, since they had to be enabled in order for the hardware interrupt to be processed in the first place. Even when the IRET or the STI instruction clears the way for the CPU to process interrupts, the 8259 will not generate any interrupts of lower or equal priority until it is told to do so by an End of Interrupt (EOI) instruction. The 8259 allows for a number of different End of Interrupt instructions to be sent to it. Among these instruction types is the ability to reset a specific interrupt or to automatically allow the 8259 to reset the interrupt as soon as it is acknowledged by the CPU. The easiest one to implement is a nonspecific End of Interrupt; this tells the 8259 to reset the highest priority interrupt, which is usually the one that caused the last interrupt to be generated. However, since the concept of "highest priority level" can be reset from inside an ISR, generating a nonspecific EOI would cause the highest outstanding interrupt to be reset, which may or may not have been the interrupt currently being processed. If an interrupt in the 8259 is reset, it is as if that interrupt had never existed, and an important hardware interrupt could be missed. So, using nonspecific EOIs effectively disables many useful features of the 8259. Unfortunately, each and every EOI instruction in the IBM(R) ROM BIOS (and in most of the IBM-compatible machines as well) is a nonspecific one, making it difficult to use some of the power of the 8259. COM2 will almost always have priority over COM1, and many interesting features that you may wish to take advantage of in the 8259 are off-limits, unless you play a few programming tricks. Interrupts are reenabled with the STI instruction as soon as possible after interrupt processing has begun. And, due to the problems outlined above, nonspecific EOIs are generated at the end of each interrupt. Why was the EOI not generated earlier in the code? Once interrupts are reenabled with the STI instruction, only interrupts of a higher priority than the interrupt currently being processed will be acted upon. Typically, these interrupts are acted upon and returned from rapidly, and while they're being acted on, further interrupts of a lower priority will only be queued while waiting for an EOI. This is safe, in keeping with limited stack space. However, if an EOI is generated within the interrupt routine, and another interrupt routine takes over for some undetermined time, it is possible to lose the character ready or transmit buffer empty interrupt entirely. By using the belt-and- suspenders approach of reenabling interrupts immediately upon the start of interrupt processing, disabling interrupts before generating the EOI, and then executing the IRET instruction, you won't have to worry about the system crashing and the snake eating its own tail. Next Version What might the next version of this program do that this version doesn't? Perhaps being able to turn on and off an internal Xmodem protocol would be helpful, enabling the protocol layer of file transfers to be transparent. Once we get started with that, there are several protocols we can start playing with: Ymodem and Zmodem, X.25, UUCP, and maybe even some of the synchronous protocols, which usually require an external clock. Now, if we add in device driver I/O and a little bit of modem-dialing ability, the possibility of having the disk drive of a remote machine be considered local isn't all that farfetched, is it? Figure 1: The Original Interrupt 14 Calling Sequence Set AH to one of these values, and DX to 0 for COM1 or 1 for COM2. The requested value is either sent or received in AL. AH = 0 Port Initialization AL = Parameters to Set, AH will return status as in AH = 3 Bits 7 6 5 │ 4 3 │ 2 │ 1 0 Baud Rate │ Parity │ No. of Stopbits│ Word Length 000 - 110 │ 00 - None │ 0 - 1 Stopbit │ 00 - 5 Bits 001 - 150 │ 01 - Odd │ 1 - 2 Stopbits │ 01 - 6 Bits 010 - 300 │ 10 - None │ │ 10 - 7 Bits 011 - 600 │ 11 - Even │ │ 11 - 8 Bits 100 - 1200 │ │ │ 101 - 2400 │ │ │ 110 - 4800 │ │ │ 111 - 9600 │ │ │ AH = 1 Send Character AL = Character to Send, AH will return status as in AH = 3 (Bit 7 set indicates unable to send character) AH = 2 Get a Character AL returns character from comm port within time-out period (AH returns status as in AH = 3) AH = 3 Return Status of Comm Port AH = Bit 7 = Time-Out (Couldn't Send or Receive Character) Bit 6 = Transmitter Shift Register Empty Bit 5 = Transmitter Holding Register Empty Bit 4 = Break Detected Bit 3 = Framing Error Detected Bit 2 = Parity Error Detected Bit 1 = Overrun Error Detected Bit 0 = Data Ready AL = Bit 7 = Data Carrier Detect Bit 6 = Ring Indicator Bit 5 = Data Set Ready Bit 4 = Clear to Send Bit 3 = Delta (Change in) DCD Bit 2 = Delta RI Bit 1 = Delta DSR Bit 0 = Delta CTS Figure 2: Additions to the BIOS for Interrupt 14 All of these new functions require that AH = 4. The desired new function is loaded into AL. The comm port to operate on is loaded into DX as in the original interrupt. Statuses are returned in AX, if possible. AL = 0 Return 0ff0 in AX to determine load status AL = 1 Initialize ports, desired mode in CX, clear buffers AL = 2 Initialize comm port with baud rate and other parameters in CL Bits 7 6 5 │ 4 3 │ 2 │ 1 0 Baud Rate │ Parity │ No. of Stopbits │ Word Length 001 - 19.2K │ 00 - None │ 0 - 1 Stopbit │ 00 - 5 Bits 010 - 38.4K │ 01 - Odd │ 1 - 2 Stopbits │ 01 - 6 Bits │ 10 - None │ │ 10 - 7 Bits │ 11 - Even │ │ 11 - 8 Bits AL = 3 Set the time-out value to the number of 1/18ths of a second in CX AL = 4 Clear the Input Buffer (Reset its pointers) AL = 5 Return count in Input buffer in AX AL = 6 Clear the Transmit Buffer (Reset its pointers) AL = 7 Return the count in the Transmit Buffer in AX AL = 8 Uninstall the TSR driver, then release memory Figure 3: Register Usage in the 8250 UART All register usage is based on the offset from the base port address. For COM1 this is 3F8, for COM2 it is 2F8. BPA + 0 Data When read from, the current character will be returned from the Receive Buffer Register. When written to, the byte is transferred first to the Transmitter Holding Register, then to theTransmitter Shift Register, where it is actually transmitted. If the Divisor Latch Access Bit (DLAB and bit 7 of the Line Control Register) is set, then this port is the Least Significant byte for the Baud Rate Divisor. BPA + 1 Interrupt Enable Register Bits 7──4 NA Bit 3 Enable Modem Status Interrupts Bit 2 Enable Receive Line Status Interrupts Bit 1 Enable Xmit Holding Register Empty Interrupts Bit 0 Enable Data Available Interrupts If the DLAB is set, then this port is the Most Significant byte for the Baud Rate Divisor. BPA + 2 Interrupt Identification Register Bits 7──3 NA Bits 2──0 (in descending priority) 110 Receiver Line Status Interrupt 100 Data Available Interrupt 010 Transmitter Holding Buffer Empty Interrupt 000 Modem Status Interrupt 001 No Interrupt Pending (Not an Interrupt) BPA + 3 Line Control Register Bit 7 Divisor Latch Access Bit Bit 6 Set Break (1 turns break on, 0 off) Bit 5&4 Parity 0 0 Odd Parity 0 1 Even Parity 1 0 Space (Zero) Parity 1 1 Mark (Ones) Parity Bit 3 Parity Enable Bit 2 Number of Stopbits (0=1 Stopbit, 1=2) Bit 1&0 Word Length 0 0 5 Bits 0 1 6 Bits 1 0 7 Bits 1 1 8 Bits BPA + 4 Modem Control Register Bit 7──5 NA Bit 4 Loop Enable (Turns on Test Conditions) Bit 3 OUT2 (Hardware AND to Interrupts) Bit 2 External Connection, Not Used Bit 1 Request to Send Bit 0 Data Terminal Ready BPA + 5 Line Status Register Bit 7 NA Bit 6 Transmitter Shift Register Empty Bit 5 Transmitter Holding Register Empty Bit 4 Break Interrupt Received Bit 3 Framing Error Bit 2 Parity Error Bit 1 Overrun Error Bit 0 Data Ready BPA + 6 Modem Status Register Bit 7 Data Carrier Detect Bit 6 Ring Indicator Bit 5 Data Set Ready Bit 4 Clear to Send Bit 3 Delta (Change in) DCD Bit 2 Delta RI Bit 1 Delta DSR Bit 0 Delta CTS ─────────────────────────────────────────────────────────────────────────── MS-DOS(R) Comm Port Syntax Problems ─────────────────────────────────────────────────────────────────────────── There are certain problems when dealing with MS-DOS, which are caused by confusion in syntax. For example, the first communications port found by the POST code in the BIOS is loaded into a set memory location (40:0). What is actually stored is the base address of the communications port──each 8250 requires eight consecutive port addresses. Subsequent communications ports are loaded at 40:2, 40:3, and 40:4. (Yes, MS-DOS allows for more than two communications ports.) The problem arises when the first communications port found by the POST is really that of an 8250 set up as a physical COM2. Since this is the first communications port found, it will be stored at 40:0, which is the address reserved for COM1. However, COM2 generates an IRQ 3, whereas COM1 generates an IRQ 4. Programs that rely on the data found at 40:0 to determine which comm ports exist and what their physical interrupt characteristics are may have a problem unless the data stored at that address is examined and interpreted, which thereby prohibits device independence. ─────────────────────────────────────────────────────────────────────────── Status of the Transmit Shift Register Versus the Transmitter Holding ─────────────────────────────────────────────────────────────────────────── The 8250 chip has some inherent problems that you should be aware of. One of them can cause a routine that you might write to appear to transmit characters improperly. When you direct the 8250 to output a character by writing to the Transmitter Holding Buffer (a write instruction out to the Base Port of the 8250), it immediately transfers that byte to the Transmitter Shift Register (an internal register within the 8250) and starts transmitting it a serial bit at a time. However, the moment the Transmitter Holding Buffer is empty, an interrupt will be generated to indicate that the transmitter buffer is empty. If you have no bytes left to transmit, you might think this is a safe time to reset the 8250 as required. There is a good chance, however, that the last character is still being transmitted. If you were to reset the 8250, you might cause the last character to be transmitted improperly, if it is transmitted at all. It's a good idea to check the status of both the Holding Buffer and the Shift Register before you consider a transmission to be over. Fortunately, the folks at National Semiconductor have made this very easy: when you check the status of the Transmitter Shift Register, you're actually checking the status of both registers. The secret of getting the highest output rate in the safest manner is for you to check only the status of the Transmitter Holding Register when you are attempting to output characters, but to check the status of the Transmitter Shift Register to determine when the last character has been sent. ─────────────────────────────────────────────────────────────────────────── XON/XOFF ─────────────────────────────────────────────────────────────────────────── The XON/XOFF protocol, which is often used during non-error-checking transmissions of ASCII files, permits the receiving station in a communications session to indicate to the transmitting station to stop sending characters until it is told to continue. Even though the code allows you to define a buffer as large as you want (which will receive characters as long as there is space in the buffer and hardware interrupts), you'll eventually run across the problem of receiving characters more rapidly than you can handle them. An example of this is when the buffer fills up and you now must write the captured data to disk. This takes a finite amount of time, during which interrupts may be turned off for more than one character receive interval. If you don't use some protocol, you'll lose characters. The XON/XOFF protocol defines two characters, a Ctrl-S for XOFF and a Ctrl-Q for XON, that tell the transmitting side to cease transmission by sending an XOFF and to restart transmission by sending an XON. Life is seldom as easy as it might seem, and this protocol is no exception. First, it has been very loosely implemented in many current programs to determine that the XON character may be any character. If you design a protocol that waits only for the XON character, you might wait a long, long time. (The terminal driver in your MS-DOS machine has been loosely implemented with an XOFF protocol. Try hitting a Ctrl-S during a long listing and then any other character-generating key as if it were an XON character.) Second, this protocol is rather timing-sensitive, especially with the increasing popularity of some of the packet-switching networks. This is because when the buffer is 80 percent full, an XOFF character is sent to the transmitter. It may take a few seconds for the character to be received and acted on by the transmitter and for the transmitter to cease sending characters. Until the transmitter does receive the XOFF, it is merrily dumping characters out at character rates that can be as high as almost 1,000 cps. You might have to leave as much as 5 seconds of character space in your buffer to handle situations like this. A 5Kb high-water mark is probably too much in most situations, however, so the code lets you modify both the buffer size and the high-water mark as you require. You can only send one XOFF character, even if it looks as if the remote end is ignoring your first XOFF request. There is always the possibility that the packet-switch network has delayed your first XOFF and that subsequent XOFFs may actually be taken as XON characters. ─────────────────────────────────────────────────────────────────────────── RS-232C: A Standard Problem ─────────────────────────────────────────────────────────────────────────── RS-232C is the shorthand abbreviation for Recommended Standard 232, Revision C, from the engineering department of the Electronic Industries Association. Its full name is Interface between Data Terminal Equipment and Data Communication Equipment Employing Serial Binary Data Interchange. RS-232C defines the wiring at each end of a connection between two devices, such as a modem (the Data Communication Equipment, or DCE) and your computer (the Data Terminal Equipment, or DTE). It requires that the DCE have a female connection and the DTE a male connection. RS-232C defines the supposed purpose of 25 different wires, which pins they should terminate on and even what voltage levels and current drains and loads are allowed, but that's about it (see Figure A). So, if you have two computers that you want to hook up to one another, which one is the DTE and which is the DCE? That's not defined, but the standard seems to work only if there is one of each, since it defines the "direction" of the flow of information──whether a particular signal is generated from the DTE or the DCE. Therefore, you must define one or the other as DTE and fool the other side into thinking that it too is a DTE. You can do this with a null-modem cable; think of it as almost having two modems in between two machines. Of the 25 defined pins/wires, only 11 of them are used for asynchronous communications──the others are used for synchronous communications or reserved for future use (see Figure B). Usually, for each transmitted data or control function, there is a complementary receive data or control function. By properly wiring these two mutually exclusive sets of wires, you have designed a cable that can trick the other side into thinking it is a modem (see Figure C for a complete picture). Of course, when one of the serial cards mounted within your machine does not even follow what little defined standard there is, we've all got a problem. It shouldn't surprise you to find that many of the serial cards out there are following their own versions of RS-232C. Remember that the R stands for "Recommended." Figure A: Pin Usage Table ╓┌──────┌─────────┌──────────────────────────────────────────────────────────╖ Pin Circuit Name 1 AA Protective Ground 2 BA Transmitted Data 3 BB Received Data Pin Circuit Name 3 BB Received Data 4 CA Request to Send 5 CB Clear to Send 6 CC Data Set Ready 7 AB Signal Ground or Common 8 CF Received Line Signal Detect (Data Carrier Detect) 9 - Reserved/Unassigned 10 - Reserved/Unassigned 11 - Reserved/Unassigned 12 SCF Secondary Received Line Signal Detect 13 SCB Secondary Clear to Send 14 SBA Secondary Transmitted Data 15 DB Transmission Signal Element Timing 16 SBB Secondary Received Data 17 DD Receiver Element Timing 18 - Reserved/Unassigned 19 SCA Secondary Request to Send 20 CD Data Terminal Ready (DTR) 21 CG Signal Quality Detector 22 CE Ring Indicator Pin Circuit Name 22 CE Ring Indicator 23 CH/CI Data Signal Rate Detector 24 DA Transmit Signal Element Timing 25 - Reserved/Unassigned Figure B: Asynch Set of Pins Pin Abbrev. Name Direction Function 1 - Protective Ground None Ground 2 TD Transmitted Data to DCE Outbound Data 3 RD Received Data to DTE Incoming Data 4 RTS Request to Send to DCE DTE wants to send data 5 CTS Clear to Send to DTE DCE okays send request 6 DSR Data Set Ready to DTE DCE is ready to communicate 7 - Signal Common None Common Ground 8 DCD Data Carrier Detect to DTE Carrier (Linkup) between DTE/DCE 20 DTR Data Terminal Ready to DCE Enable DCE (say DTE is ready) 22 RI Ring Indicator to DTE Phone is ringing 23 DSRD Data Sig. Rate Undefined Complicated, but used to Detect negotiate data rates between DCE and DTE Figure C: │ │ ┌─┴┐ Pin No. Pin No. ┌┴─┐ │ └─────┐ ┌──┐ ┌─────┘ │ │TD 2╞═══──────────┘┌─│──────────═══╡2 TD│ │RD 3╞═══───────────┘ └──────────═══╡3 RD│ │RTS 4╞═══──┬─────────┐ ┌──═══╡4 RTS│ │CTS 5╞═══──┘ ┌──│────────┴──═══╡5 CTS│ │DSP 6╞═══────┐ │ │ ┌──═══╡6 DSP│ │COMMON 7╞═══─────────│───────────│──═══╡7 COMMON│ │DCD 8╞═══────│────┘ └────────│──═══╡8 DCD│ │DTR 20╞═══──┐ ├────────────────│──═══╡20 DTR│ │RI 22╞═══──│─┘ ├──═══╡22 RI│ │ ┌─────┘ └──────────────────┘ └─────┐ │ └─┬┘ └┬─┘ │ │ ─────────────────────────────────────────────────────────────────────────── Sample Code Fragments for TSRCOMM.ASM ─────────────────────────────────────────────────────────────────────────── ;; TSRCOMM.ASM - Written by Ross M. Greenberg ;; Sample Code Fragments Follow ∙ ∙ ∙ P1_INLEN equ 400h ; Define sizes of input and output P2_INLEN equ 400h ; buffers. High-water and low-water P1_OUTLEN equ 400h ; marks are a direct reflection of P2_OUTLEN equ 400h ; these values ;; Be careful with these settings if yours have different lengths for ;; each of the COM_INBUF's: these only play off COM1_INBUF HIGH_MARK equ (P1_INLEN/10 * 8) ; send XOFF when buffer is 80% full LOW_MARK equ (P1_INLEN/10 * 2) ; send XON when buffer is 20% full ;; Definitions of all 8250 Registers and individual bit meanings DATA equ 0h ; DATA I/O is from the base IER equ 1h ; Interrupt Enable Register IER_RDA equ 1h ; Received Data Available int bit IER_THRE equ 2h ; Transmitter Hold Reg. Empty int bit IER_RLS equ 4h ; Receive Line Status int bit IER_MS equ 8h ; Modem Status int bit IIR equ 2 ; Interrupt Identification Register IIR_RLS equ 5h ; *equal* to if Receiver Line Status int IIR_RDA equ 4h ; *equal* to if character ready IIR_THRE equ 2h ; *equal* to if TX Buffer empty IIR_PEND equ 1h ; set to zero if any interrupt pending IIR_MS equ 0h ; *equal* to if Modem Status int LCR equ 3h ; Line Control Register LCR_WLS0 equ 0h ; Word Length Select Bit 0 LCR_WLS1 equ 1h ; Word Length Select Bit 1 LCR_STOPBITS equ 4h ; number of stop bits LCR_PARITYEN equ 8h ; Enable Parity (see SPARITY & EPARITY) LCR_EPARITY equ 10h ; Even Parity Bit LCR_SPARITY equ 20h ; Stick Parity LCR_BREAK equ 40h ; set if break is desired LCR_DLAB equ 80h ; Divisor Latch Access Bit MCR equ 4h ; Modem Control Register MCR_DTR equ 1h ; Data Terminal Ready MCR_RTS equ 2h ; Request To Send MCR_OUT1 equ 4h ; Output 1 (nobody uses this!) MCR_OUT2 equ 8h ; Out 2 (Sneaky Int enable) MCR_LOOP equ 10h ; Loopback enable LSR equ 5 ; Line Status Register LSR_DATA equ 1h ; Data Ready Bit LSR_OVERRUN equ 2h ; Overrun Error Bit LSR_PARITY equ 4h ; Parity Error Bit LSR_FRAMING equ 8h ; Framing Error Bit LSR_BREAK equ 10h ; Break Detect (sometimes an error!) LSR_THRE equ 20h ; Transmit Holding Register Empty LSR_TSRE equ 40h ; Transmit Shift Register Empty MSR equ 6 ; Modem Status Register MSR_DEL_CTS equ 1h ; Delta Clear To Send MSR_DEL_DSR equ 2h ; Delta Data Set Ready MSR_EDGE_RI equ 4h ; Trailing Edge of Ring Indicator MSR_DEL_SIGD equ 8h ; Delta Receive Line Signal Detect MSR_CTS equ 10h ; Clear To Send MSR_DSR equ 20h ; Data Set Ready MSR_RI equ 40h ; Ring Indicator - during entire ring MSR_DCD equ 80h ; Data Carrier Detect - on-line CTRL_PORT equ 20h ; The 8259 lives here INT_EOI equ 20h ; The End of Interrupt reset value INT_MASK_PORT equ 21h ; The mask for the 8259 lives here COM1_MASK equ 0efh ; COM2_MASK equ 0f7h ; INTNO_COM1 equ 0ch ; The physical interrupt number for INTNO_COM2 equ 0bh ; Comm1 and for Comm2 PORT1 equ 3f8h ; Physical ports where Comm1 & Comm2 PORT2 equ 2f8h ; should be. See sidebar 1 for info. ∙ ∙ ∙ ;; COMM PORT BLOCK (CPB) ;; Comm Port Block defines information unique for each comm port ;; and includes information such as what the original interrupt ;; vector pointed to, which parameters are set, etc. CPB struc cpb_base dw ? ; base port of comm port (2F8, 3F8) cpb_nint_off dw ? ; new interrupt offset address cpb_pic_mask db ? ; mask for enabling ints from 8259 cpb_int_no db ? ; what interrupt we are cpb_mode dw ? ; whatever modes we have turned on cpb_timeout dw ? ; time-out value off timer tick cpb_in_xoff dw 0 ; true if we output an XOFF cpb_out_xoff dw 0 ; true if an XOFF was sent to us cpb_inbase dw ? ; start of input buffer cpb_inlen dw ? ; length of input buffer allocated cpb_inhead dw ? ; pointer to next input char location cpb_intail dw ? ; pointer to last input char location cpb_incnt dw 0 ; count of how many inp chars outstanding cpb_inerrors dw ? ; pointer to the error bits cpb_outbase dw ? ; start of output header cpb_outlen dw ? ; total length of output buffer allocated cpb_outhead dw ? ; pointer to next output char location cpb_outtail dw ? ; pointer to last output char location cpb_outcnt dw 0 ; count of how many outp chars outstanding cpb_outend dw ? ; ptr to the end of the output buffer cpb_tx_stat dw 0 ; set to no interrupts turned on cpb_oint_add dw ? ; original int offset:segment order dw ? CPB ends ∙ ∙ ∙ ;; HANDSHAKING OPTIONS BREAK_IS_ERROR_OPTION equ 01h ; Set these bits (OR combination) DSR_INPUT_OPTION equ 02h ; to set desired options. Then DCD_INPUT_OPTION equ 04h ; set CX to this value, DX to the CTS_OUTPUT_OPTION equ 08h ; comm port desired, and generate XOFF_INPUT_OPTION equ 10h ; an INT0x14 with AH=4 and AL=1 XOFF_OUTPUT_OPTION equ 20h DTR_OPTION equ 40h XON_IS_ANY_OPTION equ 80h TX_INTS_ON_OPTION equ 100h ;; Masm has a 256-byte static initialization limit. NC is shorter than ;; NO_CHARS.... NC equ 0 ;; WARNING! Do not move the error array away from its approriate ;; error array, or you'll probably crash at some point! com1_inbuf db P1_INLEN dup(0) ; Allocate space for com1_errs db (P1_INLEN/8) + 1 dup(0) ; both comm ports: com2_inbuf db P2_INLEN dup(0) ; input, error and com2_errs db (P2_INLEN/8) + 1 dup(0) ; output buffer com1_outbuf db P1_OUTLEN dup(0) com2_outbuf db P2_OUTLEN dup(0) ;; CPB1 and CPB2 ;; Allocate space and initialize COMM PORT BLOCKS for com1 and com2 cpb1 CPB cpb2 CPB ∙ ∙ ∙ xmit_int proc near cmp cpb_outcnt[si], 0 ; any work to do? jnz xmit1 ; yep! call till_clear ; wait for transmitter to clear call tx_off ; turn xmit interrupts off ret ; and return xmit1: mov bx, cpb_outtail[si] ; get next character to xmit inc bx ; now point right on it cmp bx, cpb_outend[si] ; cmp to the end jnz xmit2 ; if not past the end, jump mov bx, cpb_outbase[si] ; past end, reset to the head xmit2: cli ; don't get interrupted now dec cpb_outcnt[si] ; decr. count of chars to go mov cpb_outtail[si], bx ; save a pointer to next char mov al, [bx] ; get the character in al cmp cpb_outcnt[si], 0 ; any work left to do? jnz out_it ; yep! call till_clear ; wait for transmitter to clear call tx_off ; turn xmit interrupts off out_it: call out_char ; output the character ret xmit_int endp ∙ ∙ ∙ interrupt_table label word ; dw offset ms_int ; modem status int (ret only) dw offset xmit_int ; transmitter int dw offset rda_int ; character ready int dw offset err_int ; receiver line error int ∙ ∙ ∙ com1_isr proc far ; Entry point for comm1 interrupts push ax lea ax, cpb1 jmp short common_isr com1_isr endp com2_isr proc far ; Entry point for comm2 interrupts push ax lea ax, cpb2 jmp short common_isr com2_isr endp ∙ ∙ ∙ common_isr proc near ; IRQ's come to here. If not push bx ; ours jump to old int vector push cx push dx push si push di push ds push cs ; addressing off ds as cs pop ds ; makes it easier to think mov si, ax ; move in the cpb mov di, cpb_base[si] ; get the base port lea dx, IIR[di] ; and then the int ID Reg in al, dx ; get the interrupt type test al, IIR_PEND ; is there a pending int? jz is_mine ; interrupt on *this* chip! other_int: cli ; turn off ints since this ; is non-reentrant mov ax, cpb_oint_add[di] ; grab the old int out of mov old_int, ax ; the structure mov ax, cpb_oint_add[di][2] mov old_int[2], ax pop ds ; pop everything back pop di pop si pop dx pop cx pop bx pop ax jmp dword ptr cs:[old_int] ; jump to whatever was there polling_loop: ; this is required to be sure ; we haven't lost any ints lea dx, IIR[di] ; load the int ID Reg in al, dx ; test al, IIR_PEND ; is there a pending int? jnz clear ; no. time to return is_mine: and ax, 06h mov bx, ax mov bx, interrupt_table[bx] push di ; save di for polling loop call bx pop di jmp polling_loop ; time to check for more work clear: ; no further int processing pop ds ; pop everything back pop di pop si pop dx pop cx pop bx cli ; interrupts off, then reset mov al, INT_EOI ; interrupts on the 8259 out CTRL_PORT, al no_eoi: pop ax iret ; iret will turn ints back on common_isr endp ∙ ∙ ∙ int14_functions label word ; dw offset init14 ; initialize the port dw offset send14 ; send the character in al dw offset get14 ; return next char in al, ; status in ah dw offset stat14 ; get serial status,return in ax dw offset newfuncs14 ; all of the new functions get_baud proc near ; AX is the offset, shl ax, 1 ; divisor returned in AX push bx ; make the table offset mov bx, ax mov ax, baudrate_table[bx] ; and get the divisor pop bx ret get_baud endp baudrate_table label word dw 1047 ; 110 baud dw 768 ; 150 baud dw 384 ; 300 baud dw 192 ; 600 baud dw 96 ; 1200 baud dw 48 ; 2400 baud dw 24 ; 4800 baud dw 12 ; 9600 baud dw 6 ; 19200 baud dw 3 ; 38400 baud ∙ ∙ ∙ funcs_table label word ; dw offset new00 ; Each function corresponds to dw offset new01 ; AL value used for dw offset new02 ; subfunction dw offset new03 ; dw offset new04 ; dw offset new05 ; dw offset new06 ; dw offset new07 ; dw offset new08 ; newfuncs14 proc near cmp al, 08h ; out of bounds? jle dispatch ; no mov ax, 0ffffh ; yes, error code ret dispatch: call get_cpb ; get si to point to proper cpb mov di, cpb_base[si] ; point the ports! xor bx, bx mov bl, al shl bx, 1 mov bx, funcs_table[bx] call bx ret newfuncs14 endp new00 proc near mov ax, special_return_value ret new00 endp new01 proc near mov cpb_mode[si], cx ; move the new mode in call init_buffers ; and reset the pointers ret new01 endp new02 proc near lea dx, LCR[di] ; get the Latch in al, dx or al, LCR_DLAB ; turn on the divisor out dx, al ; in the chip push cx mov ax, cx and ax, 00e0h ; only the highest three bits mov cl, 5 shr ax, cl add ax, 7 ; let offset start at 8 (19200) call get_baud ; then get the correct divisor ; allows higher than 9600 pop cx lea dx, DATA[di] ; get the base address out dx, ax ; output the whole word lea dx, LCR[di] ; get the Latch mov al, cl ; get the other parameters and and al, 01fh ; mask only parity, stop bits, ; word length out dx, al ; set the params ret new02 endp new03 proc near mov cpb_timeout[si], cx ret new03 endp new04 proc near cli mov cpb_incnt[si], NO_CHARS mov ax, cpb_inbase[si] mov cpb_inhead[si], ax mov cpb_intail[si], ax sti ret new04 endp new05 proc near mov ax, cpb_incnt[si] ret new05 endp new06 proc near cli mov cpb_outcnt[si], NO_CHARS mov ax, cpb_outbase[si] mov cpb_outhead[si], ax mov cpb_outtail[si], ax sti ret new06 endp new07 proc near mov ax, cpb_outcnt[si] ret new07 endp new08 proc near mov si, offset cpb1 ; set up for port1 cmp cpb_oint_add[si], 0 ; com port installed? jz new0801 ; no call unset_up ; and kill this comm port new0801: mov si, offset cpb2 ; set up for port2 cmp cpb_oint_add[si], 0 ; com port installed? jz new0802 ; no call unset_up ; and kill this comm port new0802: cli mov dx, old_int14 mov al, 014h push ds mov ds, old_int14[2] DOSINT 25h ; reset the serial port int pop ds mov dx, orig_timer mov al, TIMER_TICK_INT_NO push ds mov ds, orig_timer[2] DOSINT 25h ; reset the timer_tick int pop ds push cs pop es ; free up our own memory DOSINT 49h ; the environment sti ret new08 endp ∙ ∙ ∙ ─────────────────────────────────────────────────────────────────────────── Combining Interrupts and Polling: An Adventure in Programming ─────────────────────────────────────────────────────────────────────────── One of the things that made writing a program such as TSRCOMM so interesting was the amount of time spent thinking, "But, that should have worked!" When initially contemplated, TSRCOMM was going to be fully interrupt driven; no polling was to be included at all. A good look at the hardware, confirmed by experimental programming, showed that it was possible for a "dual interrupt" system, one capable of handling transmit buffer empty and data interrupts, to actually lose an interrupt while processing another. Therefore, much to my chagrin, you'll find a polling loop right in the middle of the COMMON_ISR interrupt routine. The loop causes the code to continually cycle once an interrupt is generated until, by polling the hardware and reading the appropriate ports, it determines that there is no more work to be done during this cycle. Fortunately, once compiled, this ugliness is rarely noticed. ████████████████████████████████████████████████████████████████████████████ BLOWUP: A Windows Utility for Viewing and Manipulating Bitmaps Charles Petzold☼ When you first start programming for Microsoft(R) Windows, the concept of a bitmap seems rather easy to grasp. A bitmap is simply a collection of bits that correspond directly to the scan lines and pixels of a rectangular display image. However, working with bitmaps is not quite as easy as understanding them. Even the seemingly simple chore of displaying a bitmap on the client area of a window can be puzzling. If you search for a Windows function by the name of DrawBitmap, you do so in vain. The function does not exist, and by the time you learn that a "memory display context" is necessary for the job, you may be nostalgically recalling how easy and simple life was when you programmed strictly in character mode. BLOWUP is a program designed to dispel some of the mysteries of bitmaps and memory display contexts and provide a little fun as well. With BLOWUP you can use the mouse to transfer almost anything that appears on the screen to the Windows clipboard in a bitmap format. BLOWUP is also a "clipboard viewer" of bitmaps and will display any bitmap currently in the clipboard. As its name implies, BLOWUP will blow up or shrink down the bitmap to fit its own client area. Figure 1☼ shows BLOWUP's client area and the contents of the clipboard after BLOWUP has been used to capture part of the MS-DOS(R) Executive window. Once the bitmap is in the clipboard, you can transfer the image to any Windows program that can handle bitmaps. (Note that some programs, such as WRITE, PAINT, and CARDFILE, will convert a color bitmap to monochrome, while others, such as Aldus Corp.'s PageMaker(R), will not accept color bitmaps at all.) Although the image in the clipboard is the actual size of the image as it appeared on the screen, you can use BLOWUP a second time to transfer the expanded image in BLOWUP's client area to the clipboard. BLOWUP thus becomes a tool to manipulate bitmaps manually──you can crop them, turn them upside down or left to right, shrink them, and blow them up. The BLOWUP source code can also help you learn how to create bitmaps, display them on the screen, write a clipboard viewer, transfer bitmaps between your program and the clipboard, "capture" and track the mouse, and you can even draw outside the client area of your window. With an installed Microsoft(R) Windows Software Development Kit and Microsoft(R) C Compiler Version 4.0, you can create BLOWUP.EXE by executing: MAKE BLOWUP Capturing Images BLOWUP requires a mouse. The program is simple to use once you get the hang of it, but here are the precise instructions: 1. The first step is to click the mouse in BLOWUP's client area. The cursor will then be changed to a crosshair. 2. Move the mouse cursor to the upper left-hand corner of the screen area you want to capture. Press on the mouse button. 3. Drag the mouse (with the button depressed) to the lower right-hand corner of the screen area you want to capture. As you move the mouse, BLOWUP briefly displays the blocked-out image in reverse video. Release the mouse button. The image will be transferred to the clipboard and then to BLOWUP's client area. If you block out the image starting at the lower left-hand corner, the image will then be turned upside down, both in the clipboard and in BLOWUP's client area. Starting at the upper right-hand corner will flip it around the vertical axis; starting from the lower right-hand corner will flip it both horizontally and vertically. Capturing the Mouse The first problem BLOWUP has to solve is how to track the mouse when it is outside of BLOWUP's client area. Normally a Windows program receives mouse messages only when the mouse cursor is positioned over the program's window. In order to get around this restriction, BLOWUP uses a technique called "capturing the mouse," which requires one Windows call: SetCapture (hWnd) ; After this call, all mouse movement messages and all mouse button messages will be directed to the window function whose handle is hWnd. The mouse capture is ended by a call to ReleaseCapture. The lParam parameter that accompanies mouse messages contains the current position of the mouse relative to the upper left-hand corner of the window's client area. The x coordinate is in the low word of lParam, and the y coordinate is in the high word. After you capture the mouse, one or both of these coordinates will be negative if the mouse is to the left of or above your window's client area. BLOWUP maintains two Boolean variables, bCapturing and bBlocking, which are used to keep track of what mode it is in while it is processing the WM_LBUTTONDOWN, WM_LBUTTONUP, and WM_MOUSEMOVE messages. During the first button-down message, BLOWUP sets the bCapturing flag, captures the mouse, and displays the crosshair mouse cursor. On the second button-down message, BLOWUP sets the bBlocking flag and saves the position of the mouse in org ("origin"), a structure of type POINT. That's the first corner of the rectangle you'll be blocking out. When BLOWUP receives a WM_MOUSEMOVE message while bBlocking is set, it retrieves the new mouse position in another POINT structure called len ("length") and subtracts from that the origin. The result is the size of the image measured from the origin. (The values can be negative.) BLOWUP also calls the routine InvertBlock twice, once to invert the colors of the blocked-out image, and the second time in order to change it back to normal. Painting The inversion of the blocked-out rectangle may shock some Windows programmers because it requires that BLOWUP paint outside its client area, which is normally impossible. When a Windows program prepares for painting, it obtains a handle to a display context using the GetDC or BeginPaint function. This display context allows a program to paint only within its client area. But BLOWUP's InvertBlock routines use the less common CreateDC function to obtain a display context. This function is normally used for obtaining a display context for a printer, but it can also obtain a display context for the entire screen. The first parameter is the string "DISPLAY", and the other three parameters are set to NULL. The origin of this display context is the upper left-hand corner of the display. Because the coordinates of the org structure are relative to the upper left-hand corner of BLOWUP's client area, the point must be converted to screen coordinates by using the ClientToScreen function first. BLOWUP then calls PatBlt ("pattern block transfer") with a raster operation code of DSTINVERT to invert the rectangular area of the display. Painting outside a program's window is generally not a polite thing to do, which is why BLOWUP restores the area right away with another call to InvertBlock. This is a good compromise──it gives you a visual indication of the area you're blocking out with BLOWUP, but it doesn't permanently affect the windows of other programs. And because the MS-DOS version of Windows is nonpreemptive, there is positively no chance of anything changing the screen between the two consecutive calls to InvertBlock. Creating the Bitmap When BLOWUP receives a WM_LBUTTONDOWN message while the bBlocking flag is set, it must create a bitmap containing the blocked-out image and transfer it to the clipboard. This job requires us to approach that strange animal called the "memory display context" and attempt to make friends with it. A display context is a data structure that describes a physical display device, such as a screen or a printer. When a program obtains a handle to a display context, the program is also getting permission to paint on the device. A memory display context is very similar to a normal display context except that the display "surface" is a block of memory. When you first obtain a handle to a memory display context through the CreateCompatibleDC function, this display surface is very small──exactly one pixel wide, one pixel high, and monochrome, which is not very useful. What you must do before actually working with a memory display context is select a bitmap into it by using SelectObject. When you do this, the bitmap becomes the display surface of the memory display context. The upper left-hand corner of the bitmap corresponds to the display context coordinate of (0,0). Any image previously in the bitmap becomes part of the memory display context's display surface. Any drawing you do on that memory display context is actually performed on the bitmap. When you delete the memory display context, you are then left with a bitmap containing everything that you painted on it. There are a variety of methods for creating bitmaps; the one BLOWUP uses is the CreateCompatibleBitmap call. This function creates a bitmap with the same number of color planes and color bits per pixel as the display context specified in the first parameter of the function──which in this case is the video display context. In BLOWUP, the height and width of the bitmap are the absolute values of the two sizes in the len structure. The bitmap itself is uninitialized, which means that it contains random data. Once BLOWUP selects the bitmap into the memory display context, all it needs to do is call StretchBlt to transfer the blocked-out area of the screen display context to the memory display context. The image winds up in the bitmap. Because the bitmap is the same height and width as the blocked-out display area, you may be wondering why I use StretchBlt rather than BitBlt for this job. BitBlt would work fine if you only blocked out the image starting at the upper left-hand corner; otherwise BLOWUP must flip the image, and BitBlt cannot flip images whereas StretchBlt can. Copying the Bitmap Getting the bitmap into the clipboard is the easy part of the job. BLOWUP simply calls OpenClipboard, EmptyClipboard, SetClipboardData, and CloseClipboard, and it's done. Normally a program would delete a bitmap after it has finished using it. However, when a bitmap is transferred to the clipboard, the bitmap becomes the responsibility of Windows itself. Windows will delete the bitmap the next time a program transfers something else to the clipboard. Becoming a Viewer BLOWUP is also a clipboard viewer, which means that it is notified whenever the contents of the clipboard change and will display the clipboard contents. Unlike the indiscriminate CLIPBRD.EXE program that comes packaged with Microsoft Windows, BLOWUP is very selective──it will only display bitmaps and will ignore the other clipboard formats. Becoming a clipboard viewer requires very little overhead. BLOWUP first makes a call to SetClipboardViewer while it is processing the WM_CREATE message. SetClipboardViewer returns the window handle of the previous clipboard viewer. BLOWUP then saves this as a static variable that is called hWndNext. Saving this window handle is very important──Windows maintains only one window handle as a "current clipboard viewer," and it relies on other programs to participate in the "clipboard viewer chain." Here's how it works: when the contents of the clipboard change, Windows sends the current clipboard viewer (the most recent program to register itself as a clipboard viewer) a WM_DRAWCLIPBOARD message. The program that receives this message is responsible for sending the message to the next clipboard viewer, which is the window whose handle was returned from the SetClipboardViewer call. Every clipboard viewer will at that point see the message WM_DRAWCLIPBOARD as it ripples down the clipboard viewer chain. When a program wants to get out of the clipboard viewer chain, it must call ChangeClipboardChain. (BLOWUP does this during the processing of the WM_DESTROY message right before it terminates.) Notice that the two parameters to this function are the program's own window handle and hWndNext. Windows will then respond by sending to the current clipboard viewer a WM_CHANGECBCHAIN message with wParam equal to the window handle of the program removing itself from the chain and the low word of lParam equal to the window handle of the next clipboard viewer──the same two values that are passed along to ChangeClipboardChain. If BLOWUP finds that the clipboard viewer that is removing itself from the chain is the next clipboard viewer after BLOWUP──in which case hWndNext will be equal to wParam──then it must change its own stored value of hWndNext in order to effectively skip over the departing program in future WM_DRAWCLIPBOARD calls. That's the extent of the overhead required for being a clipboard viewer. Of course, a clipboard viewer will also do a little something while processing the WM_DRAWCLIPBOARD message to display the new clipboard contents. After sending the WM_DRAWCLIPBOARD message down the clipboard viewer chain, BLOWUP simply invalidates its own client area. This is what causes Windows to generate a WM_PAINT message for BLOWUP to recreate its client area. Processing the Message When BLOWUP receives a WM_PAINT message, it must get the bitmap out of the clipboard and display it in its client area. BLOWUP opens the clipboard with a call to OpenClipboard and then uses GetClipboardData to get a handle to the bitmap currently stored in the clipboard. It's possible that the clipboard will not contain a bitmap, in which case the function will return NULL. If that's the case, BLOWUP closes the clipboard and leaves its client area unpainted. If BLOWUP is successful in getting a bitmap from the clipboard, it creates another memory display context. Earlier, when it was processing the WM_LBUTTONUP message, BLOWUP used SelectObject to select an uninitialized bitmap into a memory display context in order to transfer an image from the screen to the bitmap. Now while it is processing WM_PAINT, BLOWUP selects the clipboard's bitmap into the memory display context in order to transfer the bitmap image to its own client area. BLOWUP gets the dimensions of the bitmap by a call to GetObject. This function copies information about the bitmap into the bm variable, which is a structure of type BITMAP. At this point, the only things that we are interested in are the bm.bmWidth (the width) and bm.bmHeight (the height) fields of the bitmap. It is the StretchBlt call again that does the transfer, this time from the memory display context to the screen display context. The source width and height are the dimensions of the bitmap; the destination width and height are the dimensions of BLOWUP's client area. StretchBlt then blows up or shrinks the image appropriately. StretchBlt takes a little time to execute, particularly when working with large display surfaces. It is for this reason that BLOWUP sets the cursor to the IDC_WAIT cursor, also known as the hourglass, during the transfer. The Stretching Mode You'll notice in BLOWUP.C that immediately before the call to StretchBlt, there is a call to SetStretchBltMode.The "stretching mode" is an attribute of the display context and governs what Windows does when it uses StretchBlt to reduce the size of an image. You might think that Windows simply throws away rows and columns of pixels, but that's not what happens in the default case. By default, Windows uses a stretching mode, which is called BLACKONWHITE. When an image is reduced, Windows combines rows or columns of pixels by performing a logical AND operation between adjacent bits. A particular pixel ends up as white (a 1-bit) only if all the pixels being combined into that pixel are also white. This will preserve a black image on a white background. The opposite of this is the stretching mode which is called WHITEONBLACK. Windows here performs a logical OR operation between adjacent pixels. The result is a black pixel (a 0-bit) only if all the adjacent pixels are also black. This preserves white images on a black background. These two stretching modes can have some strange effects. For instance, suppose you had a display context that was colored gray, which for most display adapters is accomplished by alternating black and white pixels. If you used StretchBlt to reduce the size of the image in half, then the default stretching mode, known as BLACKONWHITE, would cause the result to be entirely black. The WHITEONBLACK stretching mode would make it white. The third option is COLORONCOLOR. This is the stretching mode that causes Windows to do what you might have thought it was doing anyway──throw away rows and columns of pixels. A gray image is copied as gray. This is probably the best approach when a program has no knowledge of the type of image it will be dealing with. (Alternatively, you could add a menu option in BLOWUP to allow changing the stretching mode yourself.) A Few Restrictions I've been careful to state that BLOWUP can capture almost anything you see on the screen. If a pull-down menu is displayed, clicking the mouse in BLOWUP's client area to start the capture will shift the input focus to BLOWUP and the menu will go away. If a system modal dialog box (a dialog box that does not allow the user to switch to another application) is displayed, then the dialog box must be exited before you can use BLOWUP. Microsoft Windows Version 1.04 can't create bitmaps larger than 64Kb. If, however, you attempt to capture an entire 8-color high-resolution 84Kb EGA screen to the clipboard, the call CreateCompatibleBitmap will fail, and BLOWUP will beep to indicate the problem. You'll be pleased to know that Windows Version 2.0 doesn't have this limitation, and Figure 5☼ proves it. Figure 5 may look like a typical Windows 2.0 desktop, but it's actually BLOWUP maximized to use the full screen after capturing an 84Kb screen image. Figure 2: BLOWUP make-file blowup.obj : blowup.c cl -c -d -D LINT_ARGS -Gsw -Os -W2 -Zdp blowup.c blowup.exe : blowup.obj blowup.def link4 blowup, /align:16, /map, /line, slibw, blowup mapsym blowup Figure 3: BLOWUP.C source code file /* BLOWUP.C -- Capture Screen Image to Clipboard by Charles Petzold */ #include #include long FAR PASCAL WndProc (HWND, unsigned, WORD, LONG) ; int PASCAL WinMain (hInstance, hPrevInstance, lpszCmdLine, nCmdShow) HANDLE hInstance, hPrevInstance ; LPSTR lpszCmdLine ; int nCmdShow ; { static char szAppName [] = "Blowup" ; HWND hWnd ; MSG msg ; WNDCLASS wndclass ; if (!hPrevInstance) { wndclass.style = CS_HREDRAW | CS_VREDRAW ; wndclass.lpfnWndProc = WndProc ; wndclass.cbClsExtra = 0 ; wndclass.cbWndExtra = 0 ; wndclass.hInstance = hInstance ; wndclass.hIcon = NULL ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = GetStockObject (WHITE_BRUSH) ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) return FALSE ; } hWnd = CreateWindow (szAppName, szAppName, WS_TILEDWINDOW, 0, 0, 0, 0, NULL, NULL, hInstance, NULL) ; ShowWindow (hWnd, nCmdShow) ; UpdateWindow (hWnd) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void InvertBlock (hWnd, org, len) HWND hWnd ; POINT org, len ; { HDC hDC ; hDC = CreateDC ("DISPLAY", NULL, NULL, NULL) ; ClientToScreen (hWnd, &org) ; PatBlt (hDC, org.x, org.y, len.x, len.y, DSTINVERT) ; DeleteDC (hDC) ; } long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam) HWND hWnd ; unsigned iMessage ; WORD wParam ; LONG lParam ; { static BOOL bCapturing, bBlocking ; static HWND hWndNext ; static POINT org, len ; static short xClient, yClient ; BITMAP bm ; HDC hDC, hMemDC ; HBITMAP hBitmap ; PAINTSTRUCT ps ; switch (iMessage) { case WM_CREATE: hWndNext = SetClipboardViewer (hWnd) ; break ; case WM_SIZE: xClient = LOWORD (lParam) ; yClient = HIWORD (lParam) ; break ; case WM_LBUTTONDOWN: if (!bCapturing) { bCapturing = TRUE ; SetCapture (hWnd) ; SetCursor (LoadCursor (NULL, IDC_CROSS)) ; } else if (!bBlocking) { bBlocking = TRUE ; org = MAKEPOINT (lParam) ; } break ; case WM_MOUSEMOVE: if (bCapturing) SetCursor (LoadCursor (NULL, IDC_CROSS)) ; if (bBlocking) { len = MAKEPOINT (lParam) ; len.x -= org.x ; len.y -= org.y ; InvertBlock (hWnd, org, len) ; InvertBlock (hWnd, org, len) ; } break ; case WM_LBUTTONUP: if (!bBlocking) break ; bCapturing = bBlocking = FALSE ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; ReleaseCapture () ; if (len.x == 0 || len.y == 0) break ; hDC = GetDC (hWnd) ; hMemDC = CreateCompatibleDC (hDC) ; hBitmap = CreateCompatibleBitmap (hDC, abs (len.x), abs (len.y)) ; if (hBitmap) { SelectObject (hMemDC, hBitmap) ; StretchBlt (hMemDC, 0, 0, abs (len.x), abs (len.y), hDC, org.x, org.y, len.x, len.y, SRCCOPY) ; OpenClipboard (hWnd) ; EmptyClipboard () ; SetClipboardData (CF_BITMAP, hBitmap) ; CloseClipboard () ; } else MessageBeep (0) ; DeleteDC (hMemDC) ; ReleaseDC (hWnd, hDC) ; break ; case WM_PAINT: hDC = BeginPaint (hWnd, &ps) ; OpenClipboard (hWnd) ; if (hBitmap = GetClipboardData (CF_BITMAP)) { SetCursor (LoadCursor (NULL, IDC_WAIT)) ; hMemDC = CreateCompatibleDC (hDC) ; SelectObject (hMemDC, hBitmap) ; GetObject (hBitmap, sizeof (BITMAP), (LPSTR) &bm) ; SetStretchBltMode (hDC, COLORONCOLOR) ; StretchBlt (hDC, 0, 0, xClient, yClient, hMemDC, 0, 0, bm.bmWidth, bm.bmHeight, SRCCOPY) ; SetCursor (LoadCursor (NULL, IDC_ARROW)) ; DeleteDC (hMemDC) ; } CloseClipboard () ; EndPaint (hWnd, &ps) ; break ; case WM_DRAWCLIPBOARD : if (hWndNext) SendMessage (hWndNext, iMessage, wParam, lParam) ; InvalidateRect (hWnd, NULL, TRUE) ; break; case WM_CHANGECBCHAIN : if (wParam == hWndNext) hWndNext = LOWORD (lParam) ; else if (hWndNext) SendMessage (hWndNext, iMessage, wParam, lParam) ; break ; case WM_DESTROY: ChangeClipboardChain (hWnd, hWndNext) ; PostQuitMessage (0) ; break ; default: return DefWindowProc (hWnd, iMessage, wParam, lParam) ; } return 0L ; } MAKE File for BLOWUP blowup.obj : blowup.c cl -c -d -D LINT_ARGS -Gsw -Os -W2 -Zdp blowup.c blowup.exe : blowup.obj blowup.def link4 blowup, /align:16, /map /line, slibw, blowup mapsym blowup Figure 4: BLOWUP.DEF Module Definition File NAME BLOWUP DESCRIPTION 'Capture Screen Image to Clipboard by Charles Petzold' STUB 'WINSTUB.EXE' CODE MOVABLE DATA MOVABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS WndProc ████████████████████████████████████████████████████████████████████████████ Increase the Performance of Your Programs with a Math Coprocessor Marion Hansen and Lori Sargent☼ The microprocessor in your personal computer's CPU is powerful, but it wasn't designed to handle complex math operations rapidly. Whether it's an 8086, 8088, 80286, or 80386, your microprocessor will perform floating- point and transcendental calculations far more quickly and with greater precision if a math coprocessor is linked to it. Coprocessors also have capabilities useful for business computing. A coprocessor can process binary coded decimal numbers up to 18 digits long without round-off errors, perform arithmetic on integers from 2 x 10-9 through 2 x 109, and carry out math functions on real numbers as small as 3.4 x 10-4932 or as large as 1.1 x 104932. When software written to use a coprocessor performs certain types of math, it engages the coprocessor rather than the microprocessor. The coprocessor performs the calculation and returns the answer to the microprocessor. This entire process takes a fraction of the time required by the microprocessor executing alone. To give you an idea of how fast a coprocessor is, Figure 1 compares spreadsheet recalculation times with and without an 8087 math coprocessor. Besides performing certain kinds of math faster, coprocessors also save programming time. Because trigonometric, logarithmic, and exponential functions are built into the coprocessor's hardware, the programmer doesn't have to write these routines. And with the routines in the chip instead of in the code, programs are smaller. Coprocessors generate instructions for many numeric operations such as number conversions, arithmetic operations, and transcendental functions (tangents, exponents, and logarithms). A coprocessor is the most cost-effective way to increase number-crunching power. For a fraction of the cost of an accelerator board, a coprocessor can dramatically speed up floating-point calculations. And it won't consume a precious expansion slot (or two), since it fits in a socket already on the motherboard. Math coprocessors come in three varieties: the 8087 (for 8086- and 8088- based computers), the 80287 (for 80286-based computers), and the 80387 (for 80386-based machines). The 8087 and 80287 are both available in three different speeds. The speed you need depends on how fast your computer drives the coprocessor socket, not on the speed of your microprocessor. For example, some 10-MHz computers drive the socket at 8 MHz and thus need a coprocessor that runs at 8 MHz, not 10 MHz. If you aren't sure which speed is correct for your computer, contact the manufacturer. Hundreds of application programs have been written to take advantage of the coprocessor's speed and precision, including business, engineering, graphics, statistical, and econometric packages. Many compilers and assemblers can benefit from a coprocessor as well. Using a coprocessor with one of these programs couldn't be easier, because all interfacing between microprocessor and coprocessor is built in. The only difference you'll notice is the increased speed. Development Tools Most of today's compilers and assemblers can generate coprocessor code. This includes all recent versions of Microsoft C, Pascal, and FORTRAN compilers, as well as Borland's Turbo Pascal. No matter which of these languages you're writing in, incorporating complex math into programs is not difficult with a coprocessor. In a high-level language, using the coprocessor is quite painless. Coprocessor instructions such as sine, square root, hyperbolic tangent, and log are built into manufacturer-supplied library routines. Assembly language programmers using Microsoft's Macro Assembler Version 1.25 or later have the option of writing code that explicitly references coprocessor instructions or implicitly does so by linking in a math library such as those supplied with the Microsoft C, Pascal, or FORTRAN compilers. In addition, a number of other software vendors market specialized math libraries that perform many math functions and can be linked to programs written in a variety of languages. Although programs can usually call math library routines with or without a coprocessor, programs running on systems with a coprocessor will execute significantly faster. Figure 2 illustrates how much faster an 8-MHz computer performs floating-point instructions on typical spreadsheet data when a coprocessor is installed. Most high-level languages link an emulation library into any program that contains floating-point instructions or data. Code to check for the presence of the coprocessor is generated at run time. If a coprocessor is detected, it is used. If a coprocessor is not present, the emulation library is used. This way, programs written to take advantage of a coprocessor can run on systems without one. Debugging code that contains coprocessor instructions is not much different from debugging code written for the microprocessor alone. A good debugger, such as the CodeView facility included in Microsoft's C Compiler Version 4.0, lets you examine and change all the coprocessor registers, including status and control registers. CodeView displays data register contents in both their 80-bit internal hexadecimal form and their decimal equivalents. This makes debugging floating-point instructions no more difficult than debugging microprocessor instructions. Synergy The coprocessor is an extension of the microprocessor. (In fact, Intel calls the coprocessor the numeric processor extension, or NPX.) They share the same buses and memory. The microprocessor's status lines and queue status lines are directly connected to the coprocessor, so the coprocessor is able to track the microprocessor's instruction queue. The coprocessor monitors and decodes instructions without any overhead. It reads each instruction into its queue but executes only its own instructions, treating each microprocessor instruction as a no-operation (NOP) command. In turn, the microprocessor treats each coprocessor instruction as a NOP and executes only its own instructions. The microprocessor controls program execution, and the coprocessor controls numeric operations. Instead of the 8-bit registers in the 8088, the 16-bit registers in the 8086 and 80286, or the 32-bit registers in the 80386, the coprocessor has 80-bit data registers, which allow it to hold more information. The coprocessor's registers were designed to hold specific types of data and are significantly different from the microprocessor's general-purpose registers. Nonetheless, the two chips can still share data through common memory. Data Types Coprocessor registers were designed to store 80-bit floating-point numbers. This format, which Intel calls temporary real, is compatible with the proposed IEEE 754 floating-point standard. A temporary real number is composed of a sign bit, a 15-bit exponent, and a 64-bit significand. Although the coprocessor stores all data as temporary real numbers, it can also read and write data in six other formats: packed decimal, long real, long integer, short real, short integer, and word integer (see Figure 3). Coprocessor load and store instructions automatically convert the other six data types to temporary real format and back again. Microsoft's Macro Assembler allows these formats to be declared with the directives DW (word integer), DD (short integer and short real), DQ (long integer and long real), and DT (packed binary coded decimal and temporary real). The coprocessor stores numbers in normalized format (scientific notation). A number is normalized by shifting the 1 that's furthest to the left up or down until it occupies bit 63. The coprocessor assumes the number in the significand is a real number between 1 and 2. The exponent field specifies how far the digits must be shifted to get the correct number back. Because the exponent is stored as an unsigned value, an offset (bias) is added to it so negative numbers can be represented. This lets the coprocessor compare the magnitude of two numbers without first performing arithmetic on the exponents, and execution time is thus shortened. Registers Coprocessor computations occur in eight data registers. The registers can be accessed as a LIFO (last-in-first-out) stack, with instructions operating on the top one or two stack elements. Or the registers can act as a fixed register set, with instructions operating on explicitly designated registers. Unlike those of the microprocessor, the coprocessor's data registers don't have unique names. They're treated as indexed entries in a stack, with the top of the stack designated as register ST(0) and the others designated ST(1) and so on. Values are loaded into the coprocessor by pushing them onto the stack, and some (but not all) are retrieved by popping them off. Many coprocessor instructions operate only on the top of the stack. Most other instructions default to operating on the stack's top. All register addresses are relative to the top of the stack. A 3-bit top-of-stack pointer in another type of register──the status word register──identifies the current top-of-stack register. A push decrements the value in this pointer by 1 and loads a value into the new top register. A pop increments the value in the pointer by 1 and removes the value from the register currently at the top. The stack is circular and can be overwritten if not managed properly. All the coprocessor's numeric opcodes (as opposed to control opcodes) use the top of the stack as at least one operand. Some instructions operate only on the top of the stack, while others operate on both the top and the second stack register. Some take their second operand from another stack register, and others can take their second operand from memory. Besides the eight data registers, the 8087 has five other registers accessible to the programmer, each 16 bits in size: status word, control word, tag word, operand pointer, and instruction pointer. The status word can be thought of as a flag register (see Figure 4). It contains a busy indicator, a top-of-stack pointer, condition codes, and exception indicators. To read the status word from Microsoft C, call the built-in _status87 function. To read the status word from the coprocessor in assembler, execute an FSTSW instruction to write the status word to memory where the microprocessor can examine it. The control word defines how the coprocessor should react to different exception conditions (see Figure 5). It also defines the precision, how the results will be rounded, and whether signed or unsigned infinity will be used. The control word register has three control fields and six exception masks. Masking the exception bit tells the coprocessor to handle all occurrences of this exception; leaving it unmasked means that the programmer will have to handle the exceptions. In assembly language, control words are sent to the coprocessor by writing them to a memory location and having the coprocessor execute an instruction that reads in the control word from memory. Programmers using a high-level language can check their library reference guide to see how this is implemented in the library they are using. For programmers who do not care to set these fields, Intel provides a set of default control conditions. The default settings are: exceptions masked, interrupts masked, 64-bit precision, rounding to the nearest number, and projective infinity. The tag word contains information about the contents of each data register (see Figure 6). This information is used by the coprocessor primarily to optimize performance. The coprocessor stores 2 bits for each data register, for a total of four possible tag values. The coprocessor uses the tag word to keep track of the contents of each of its data registers and to report invalid results. The coprocessor also uses the tag word to maintain stack integrity information. For example, if a register tagged as empty (tag value = 11) is popped from the stack, the coprocessor detects stack underflow. Similarly, the coprocessor uses the tag word to detect stack overflow when new data is stored in a register that wasn't previously empty. Stack underflow and overflow trigger an invalid operation exception. Programmers can mask or unmask this exception (the default is masked). If either stack underflow or overflow occur and the invalid operation exception is masked, the coprocessor adjusts the stack pointer and returns a standard result to indicate that the value is not meaningful. The operand pointer and instruction pointer registers provide information about the instruction and data that caused an exception and are used with user-written error handlers. Most programmers do not employ these registers, however, preferring to let the coprocessor handle exceptions. Unlike the status word and control word, the tag word, operand pointer, and instruction pointer cannot be accessed directly. These registers are accessed indirectly by writing to memory either the coprocessor's environment (using FSTENV) or the coprocessor's state (using FSAVE). The 14-byte coprocessor environment consists of the status word, control word, tag word, instruction pointer, and operand pointer. The 94-byte coprocessor state includes everything in the environment plus the eight coprocessor data registers (see Figure 7). The format of the coprocessor state and environment depends on the coprocessor's operating mode. When an exception occurs while the coprocessor is in real mode, it supplies the 20-bit addresses of the offending instruction and its memory operand (if any), plus the 11 low-order bits of the opcode. In protected mode (with the 80287 and 80387 only), the coprocessor supplies the selectors and offsets of the offending instruction and its memory operand (if any). Although the 80287/80387 real-mode exception pointers have the same format as the 8087 exception pointers, the 80287/80387 instruction pointer indicates any prefixes preceding the opcode. In contrast, the 8087 instruction pointer indicates the escape (ESC) instruction opcode. Exceptions The coprocessor recognizes six exception conditions: invalid operation, denormalized operand, division by zero, numeric overflow, numeric underflow, and inexact result. The coprocessor's exception masks give programmers the choice of trapping exceptions themselves or having the coprocessor return a fixed value. When an exception occurs during execution of a coprocessor instruction, the coprocessor sets the appropriate bit in its status register. The coprocessor then checks its control register to determine whether or not that type of exception is masked. If the exception is masked, then the coprocessor uses its on-chip logic to return a result. The exception indicator bits in the status register will hold their value until they are explicitly cleared with either a FINIT or FCLEX instruction. Consequently, with exceptions masked, programmers do not have to check the status register after every instruction. Checking the exception indicator bits periodically ensures accurate results. The other method of handling exceptions is to unmask one or more of the exception bits and clear the coprocessor's interrupt enable mask. Under these conditions, an exception will trigger an interrupt request. It is up to the programmer to write the interrupt handler that will respond to such requests. The coprocessor contains a lot of built-in support for writing such routines. Instructions Coprocessor instructions fall into six categories: data transfer, loading constants, transcendental calculations, comparison, arithmetic, and processor control. A coprocessor instruction can be written in assembler in either of two ways: as a microprocessor ESC instruction followed by a number (for example, ESC 0BH) or as a specific coprocessor mnemonic (FSTP). All versions of Microsoft assemblers later than 1.25 accept coprocessor mnemonics; ESC instructions are needed only for older assemblers that do not, and are thus rarely used. Programmers writing in high-level languages needn't worry about the format of coprocessor instructions──the compiler will take care of everything. A coprocessor mnemonic takes the form of a sequence of letters beginning with an F. Figures 8 and 9 give examples of these instructions incorporated into assembly language programs. Figure 10 is a sample of code created by Microsoft's C Compiler Version 4.0. While the coprocessor instructions are not apparent in the source code, you will see them if you compile the program with the /Fc option and look at the .COD file. If an instruction starts with 11011 the microprocessor recognizes it as a coprocessor instruction and responds by generating any necessary operand addresses, putting them on the address bus, and ignoring the coprocessor opcode. The microprocessor then continues fetching and executing instructions unless it is instructed to wait for the coprocessor to complete its task. Since the microprocessor and the coprocessor can work on separate tasks simultaneously, they can overwrite each other's data or miss instructions unless they're synchronized. All high-level languages automatically synchronize the activity of the two chips, while assembly language programmers must do so explicitly. In exchange for the extra programming effort, however, assembly language programmers get more flexibility (carefully managed concurrency) and faster performance. The 80286 and 80386 have instruction synchronization built in, but this is not true of the 8088 and 8086 or any of the coprocessors. Consequently, programmers must on occasion insert an FWAIT after a coprocessor store instruction. When you are using escape sequences, specify the FWAIT instruction when the microprocessor must wait for data from the coprocessor. All floating- point mnemonics have an FWAIT as their first byte, so it isn't necessary to code one explicitly. (A few coprocessor instructions assume an FN form, which keeps the assembler from generating an FWAIT instruction.) In addition to synchronizing data, the 8086 and 8088 must also synchronize 8087 instructions. Because the coprocessor gets its instructions by monitoring them as they go into the microprocessor prefetch queue, the 8087 can miss an instruction if it is busy executing while the 8086/8088 is fetching and executing. Any program that uses a coprocessor should also be able to run without one. Before the software tries to use the coprocessor, it should check to see if there is one. It can easily do this by attempting to initialize the coprocessor and then attempting to read the coprocessor's control word after the initialization. (If a coprocessor is present, the control word will be set to the default value specified by Intel. Many software libraries have this checking function built in. If a coprocessor is not found, the program should call an emulation library to handle coprocessor instructions or should gracefully exit. Figure 11 provides an example of this type of program. Real Vs. Protected The 8087 operates only in real mode, while the 80287 and 80387 can operate in either real or protected mode. All programs written to use the 8087 are compatible with the 80287 and 80387 in real mode. Executing the privileged SETPM instruction will place the 80287 or 80387 in protected mode. They can then be returned to real mode only by a hardware reset. The microprocessor's operating mode affects coprocessor code in two areas: exception handling and memory accesses. The memory image of the instruction pointer and data pointer following an FSTENV or FSAVE instruction depends on the coprocessor's operating mode (see Figures 8 and 9). Any code that examines this information must consider the operating mode for accurate interpretation. In protected mode, Interrupt Vector 16 is dedicated to the numeric exception handling routine. Coprocessor instructions that result in exception conditions will trigger an Interrupt 16 if the exception is unmasked. Protected mode also has a built-in mechanism for handling coprocessor instructions when a coprocessor is not present (or if its absence is being emulated). Interrupt 7 is automatically triggered if a coprocessor ESC sequence is executed and the emulation bit (EM) of the microprocessor's machine status word is set. This built-in trapping can help programmers systematically include emulation code in their programs. MS(R) OS/2, Microsoft's new protected-mode version of MS-DOS(R), offers basic exception handling for coprocessors by supporting the exception handling capabilities of the 80287 and 80387. It doesn't supply a standard emulation library for coprocessors; this must be provided by the compiler. When in protected mode, the microprocessor checks all memory accesses (including coprocessor operands) for violations of protection rules. Coprocessor applications running in protected mode must comply with protected-mode memory management regulations. Any violations cause either dedicated Interrupt 13 (when the violation occurs on the first word of the numeric operand) or dedicated Interrupt 9 (when the violation occurs on subsequent words). If you want to port an 8087 program to a protected-mode system, consider reassembling the program on an 80286/80386 assembler. This removes the redundant FWAITs and usually gives you a more compact code image. In addition, make the following changes to the 8087 program: ■ Delete interrupt-controller-oriented instructions in numeric exception handlers. ■ Delete 8087 instructions FENI/FNENI (enable interrupts) and FDISI/FNDISI (disable interrupts). The 80287 and 80387 ignore these instructions, so none of the 80287/80387 internal states will be updated. ■ Be sure Interrupt Vector 16 points to the numeric exception handling routine. ■ Include a microprocessor exception handler for an Interrupt 7, which will occur during the execution of coprocessor instructions if the microprocessor's machine status word contains the settings TS=1 (task switched) or EM=1 (emulation). ■ Include a microprocessor exception handler for Interrupt 9 (which occurs when the second or later word of a floating-point operand falls outside a segment) and Interrupt 13 (caused by the starting address of a numeric operand falling outside a segment). Figure 1: Recalculating Lotus 1-2-3(tm) spreadsheets on an IBM(r) PC can usually be done much more quickly with an 8087 coprocessor. Standard│░░ Deviation│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ Exponents│░░░ │▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ Multiply/│░░░░░░░░░░░░░░░░░ ┌────────────────────┐ Divide│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ │ ░░ With an 8087 │ │ │ │ Add/│░░░░░░░░░░░░░░░░░░░ │ ▓▓ Without an 8087 │ Subtract│▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓▓ └────────────────────┘ ├──┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┬───┐ 0 10 20 30 40 50 60 70 80 90 100 110 120 130 Figure 2: Calculation times for floating-point instructions decrease dramatically when a coprocessor is added to an 8-MHz IBM PC. Instruction Approximate Execution Time (in microseconds) With an 8087 Without an 8087 Add/Subtract 10.6 1,000.0 Multiply (short real nos.) 11.9 1,000.0 Multiply (temporary real nos.) 16.9 1,312.5 Divide 24.4 2,000.0 Compare 5.6 812.5 Load (long real nos.) 6.3 1,062.5 Store (long real nos.) 13.1 750.0 Square Root 22.5 12,250.0 Tangent 56.3 8,125.0 Exponentiation 62.5 10,687.5 Figure 3: The coprocessor can recognize seven numeric formats, which make use of up to 80 bits. 79 78 64 63 0 ┌────┬────────┬───────────────────────────────────────────────────┐ │Sign│Biased │ Significand │Temporary │Bit │Exponent│ │Real └────┴(3FFFH)─┴───────────────────────────────────────────────────┘ 79 78 72 0 ┌────┬────┬───────────────────────────────────────────────────────┐ │Sign│Not │ d Packed Decimal Digits d │Packed │Bit │Used│ 17 │ • • • • • • • • • • • • • • │ 0 │BCD └────┴────┴─────┴────────────────────────────────────────────┴────┘ 63 62 52 51 0 ┌────┬────────┬──────────────────────────────────────┐ │Sign│Biased │ Significand │Long Real │Bit │Exponent│ │ └────┴─(3FFH)─┴──────────────────────────────────────┘ 63 0 ┌────────────────────────────────────────────────────┐ │ 2's │Long │ Compliment │Integer └────────────────────────────────────────────────────┘ 31 30 23 22 0 ┌────┬─────────┬─────────────────────┐ │Sign│ Biased │ Significand │Short │Bit │ Exponent│ │Real └────┴──(7FH)──┴─────────────────────┘ 31 0 ┌────────────────────────────────────┐ │ 2's │Short │ Compliment │Integer └────────────────────────────────────┘ 15 0 ┌────────────────┐ │ │Word │ │Integer └────────────────┘ Figure 4: The coprocessor's 16-bit status word register serves as a flag register. Reserved───────────────────────────┐ Condition │ Exception Flags (1=Exception occurred) Codes──────┬─────────┬──┬──┐ │ Stack-Top │ │ │ │ │ Pointer───────┬─┬──┐ │ │ │ │ Busy───────┐ │ │ │ │ │ │ │ │ 15 ╔╧╤╧═╤╧╤╧═╤╧╤╧═╤╧═╤╧═╤══╤╧╤══╤══╤══╤══╤══╤══╗ 0 ║B│C3│ │ST│ │C2│C1│CO│IR│X│PE│UE│OE│ZE│DE│IE║ ║ │ │ │ │ │ │ │ │ES│ │ │ │ │ │ │ ║ ╚═╧══╧═╧══╧═╧══╧══╧══╧═╤╧═╧═╤╧═╤╧═╤╧═╤╧═╤╧═╤╝ │ │ │ │ │ │ └─Invalid Operation │ │ │ │ │ └────Denormalized Operand │ │ │ │ └───────Divide By Zero │ │ │ └──────────Numeric Overflow │ │ └─────────────Numeric Underflow │ └────────────────Precision │ │ ┌─{Interrupt Request │ │ (8027) └───────────────────┤ {Error Summary Status └─ (80287) Figure 5: The control word register governs how the coprocessor reacts to exception conditions. Infinity Control───┐ Exception Masks (1=Exception masked) Rounding Control───────┐ │ │ Precision Control────────────┐ 15 │ │ │ 0 ╔═╤═╤═╤╧═╤═╪══╤══╪══╤═══╤═╤══╤══╤══╤══╤══╤══╗ ║X│X│X│IC│R│C │ P│C │IEM│X│PM│UM│OM│ZM│DM│IM║ ╚╤╧╤╧╤╧══╧═╧══╧══╧══╧╤═╤╧═╧═╤╧═╤╧═╤╧═╤╧═╤╧╤═╝ ├─┴─┴───────────────┘ │ │ │ │ │ │ │ │ │ │ │ │ │ │ └─Invalid Operation Reserved─────┘ │ │ │ │ │ └───Denormalized Operand │ │ │ │ └──────Divide By Zero │ │ │ └─────────Numeric Overflow │ │ └────────────Numeric Underflow │ └───────────────Precision │ │ ┌─{Interrupt Enable └──────────────────┤ (8087) └─{Reserved (80287) Figure 6: The tag word holds information about the contents of each data register. ┌───────────────────────────────────────────────────────────────────────┐ │ │█ │ 15 0 │█ │ ╔════════╤═══════╤═══════╤═══════╤═══════╤═══════╤═══════╤════════╗ │█ │ ║ Tag (7)│Tag (6)│Tag (5)│Tag (4)│Tag (3)│Tag (2)│Tag (1)│Tag (0) ║ │█ │ ╚════════╧═══════╧═══════╧═══════╧═══════╧═══════╧═══════╧════════╝ │█ │ │█ │ Tag Values: 00 = valid (i.e., any finite nonzero number) │█ │ 01 = zero │█ │ 10 = invalid (i.e., NaN or infinity) │█ │ 11 = empty │█ │ │█ └───────────────────────────────────────────────────────────────────────┘█ ████████████████████████████████████████████████████████████████████████ Figure 7: The coprocessor environment consists of the contents of all registers but the data registers. The coprocessor state includes all registers. Real Mode ┌─────────────────────────────────────┐ │ Control Word │ ├─────────────────────────────────────┤ │ Status Word │ ├─────────────────────────────────────┤ Coprocessor │ Tag Word │ ├─────────────────────────────────────┤ State │ Instruction Address (15-0) │ ├───────────────┬─────┬───────────────┤ │ Instruction │ 0 │ Instruction │ │Address (19-16)│ │ Opcode (10-0) │ ├───────────────┴─────┴───────────────┤ │ Operand Address (15-0) │ ├──────────────────┬──────────────────┤ │ Operand │ 0 │ │ Address (19-16) │ │ ┌──────────────┴15──────────────11┴12───────────────0┤ │ ST (0) │ ├────────────────────────────────────────────────────┤ │ ST (1) │ ├────────────────────────────────────────────────────┤ │ ST (2) │ ├────────────────────────────────────────────────────┤ │ ST (3) │ ├────────────────────────────────────────────────────┤ │ ST (4) │ ├────────────────────────────────────────────────────┤ │ ST (5) │ ├────────────────────────────────────────────────────┤ │ ST (6) │ ├────────────────────────────────────────────────────┤ │ ST (7) │ └79─────────────────────────────────────────────────0┘ Protected Mode ┌─────────────────────────────────────┐ │ Control Word │ ├─────────────────────────────────────┤ │ Status Word │ ├─────────────────────────────────────┤ Coprocessor │ Tag Word │ ├─────────────────────────────────────┤ Environment │ Instruction Offset │ ├─────────────────────────────────────┤ │ Instruction Selector │ ├─────────────────────────────────────┤ │ Operand Offset │ ├─────────────────────────────────────┤ │ Operand Selector │ ┌──────────────┴15──────────────────────────────────0┤ │ ST (0) │ ├────────────────────────────────────────────────────┤ │ ST (1) │ ├────────────────────────────────────────────────────┤ │ ST (2) │ ├────────────────────────────────────────────────────┤ │ ST (3) │ ├────────────────────────────────────────────────────┤ │ ST (4) │ ├────────────────────────────────────────────────────┤ │ ST (5) │ ├────────────────────────────────────────────────────┤ │ ST (6) │ ├────────────────────────────────────────────────────┤ │ ST (7) │ └79─────────────────────────────────────────────────0┘ Figure 8: This assembly language program uses coprocessor instructions to calculate the circumference of a circle with a given radius. Each of these instructions begins with an F. title circumference .287 ; Tell MASM there are coprocessor instructions ; in the program. data segment radius DD 2.468 circumference DD ? data ends code segment assume cs:code, ds:data start: mov ax, data ; Initialize data segment ; register mov ds, ax finit ; Initialize coprocessor fldpi ; ST = pi fadd st, st ; ST = 2pi fld radius ; ST = radius ; ST(1) = 2pi fmul st, st(1) ; ST = radius*2pi fstp circumference ; store result and pop fwait ; wait for store to complete mov ah, 4ch ; return to DOS int 21h code ends end start Figure 9: This program uses coprocessor instructions to calculate the root of each element in an array of binary coded decimal integers. The results are stored in another binary coded decimal array and can easily be converted to ASCII strings for output. .287 ; Indicate to MASM program contains npx code. bcd_data segment array_1 DT 1234567890, 82, 769823, 84165 DT 246809, 1526374859, 199, 41290 DT 98654210, 340126, 2400, 371849 array_2 DT 12 DUP (?); storage for results bcd_data ends code segment assume cs:code, ds:bcd_data start: mov ax, bcd_data mov ds, ax finit ; initialize coprocessor ; assume default control word mov cx, length array_2 ; initialize loop counter mov si, 0 ; initialize index process_array: fbld array_1[si] ; st(0) = array_1[index] fsqrt ; st(0) = sqrt (st(0)) frndint ; round st(0) to integer fbstp array_2[si] ; store bcd result in ; array_2[index] and ; pop coprocessor stack add si, 10 ; increment index to point ; to next DT array element loop process_array ; DO WHILE ; loop counter <= length array_2 exit: fwait ; make sure last store completed mov ah, 4ch ; exit to dos int 21h code ends end start Figure 10: Using Microsoft C library functions to calculate sine and cosine, this program in C draws a circle on the screen of a system equipped with a graphics adapter. The coprocessor instructions are apparent only after compilation. #include "stdio.h" #include "math.h" extern set_graph_mode(); extern set_text_mode(); extern plot_point(); #define VERTICAL_CENTER 99.5 #define HORIZONTAL_CENTER 319.5 #define PI 3.1415927 main() { char ch; float radians,radius,aspect_ratio; aspect_ratio=2.1; /* adjusts horizontal scaling to account */ /* for PC's "tall and skinny" pixels */ radius=90; set_graph_mode(); /* set screen to 640x200 graphics mode */ /* step around the circle in 1/100th radian increments */ for (radians=0; radians < 2*PI; radians=radians + 0.01) { long x,y; x=HORIZONTAL_CENTER+radius*aspect_ratio*cos(radians); y=VERTICAL_CENTER+radius*sin(radians); /* call routine to write a pixel on the screen */ plot_point((int)x,(int)y); } /* wait for user to hit a key before erasing screen */ ch=getchar(); /* restore user's screen to text mode */ set_text_mode(); } Figure 11: This routine performs the same multiplication function with or without a coprocessor. First the program checks for the presence of a coprocessor. If it finds one, it executes the imul_32 procedure. If not, it jumps to the emulation procedure, emulate_imul_32. title math_module .287 ; Tell MASM that there are coprocessor ; instructions here. public init_math public imul_32 present EQU 0 missing EQU 1 code segment public 'code' assume cs:code cp_flag DB 1 ; local flag ctrl_word DW 0 ; for storing '87 control word ;-------------------------------------------------------------; ; init_math: Detects math coprocessor and sets global flag ; ; which is used to determine whether or not to use ; ; coprocessor instructions or emulation code. ; ; ; ; This procedure must be called before the coprocessor can ; ; be used by math routines in this module. ; ;-------------------------------------------------------------; init_math PROC FAR fninit ; initialize coprocessor fnstcw cs:ctrl_word ; store '87 control word test byte ptr cs:[ctrl_word+2], 03 ; if bits 8 and 9 are set je yes_cp ; then coprocessor present mov cs:cp_flag, missing ; else no coprocessor jmp init_math_exit yes_cp: mov cs:cp_flag, present init_math_exit: ret init_math ENDP ;-------------------------------------------------------------; ; imul_32: Performs signed multiplication on two 32 bit ; ; integers. (Note: can also be used to perform ; ; fixed point 32 bit decimal multiplication) ; ; ; ; Input: Two 32 bit integers ; ; ds:si pointer to integer A ; ; ds:di pointer to integer B ; ; ; ; Output: 64-bit result returned at [es:bx] ; ; ; ;-------------------------------------------------------------; imul_32 PROC FAR cmp cs:cp_flag, missing ; IF coprocessor missing je emulate_imul_32 ; THEN emulate ; ELSE use coprocessor fild dword ptr [si] ; st(0)= A fimul dword ptr [di] ; st(0)=A*B fistp qword ptr es:[bx] ; store result and pop stack fwait ; wait for store to complete jmp imul_32_exit ; coprocessor done, exit emulate_imul_32: ;--------------------------------------------------------------------; ; ; ; The following code computes A x B where ; ; ; ; A is a 32 bit integer composed of ; ; a low word (A0) and a high word (A1) and ; ; B is a 32 bit integer composed of ; ; a low word (B0) and a high word (B1) ; ; ; ; The result is calculated by summing the partial products of ; ; individual 16 bit unsigned multiplies. The final result is ; ; sign adjusted. ; ; ; ;--------------------------------------------------------------------; ; push ax ; save caller's state push cx push dx push bp A0_x_B0: mov ax, [si] ; ax=A0 mul word ptr [di] ; dx=A0B0H, ax=A0B0L mov es:[bx], ax ; store A0B0L - 4th column sum mov cx, dx ; cx=A0B0H A1_x_B0: mov ax, [si+2] ; ax=A1 mul word ptr [di] ; dx=A1B0H, ax=A1B0L push bx ; running out of registers, ; reuse bx mov bx, ax ; bx=A1B0L mov bp, dx ; bp=A1B0H A0_x_B1: mov ax, [si] ; ax=A0 mul word ptr [di+2] ; dx=A0B1H, ax=A0B1L add cx, bx ; cx=A0B0H+A1B1L adc cx, ax ; cx=A0B0H+A1B1L+A0B1L+carry pop bx ; need pointer back mov es:[bx+2], cx ; store 3rd column sum push bx ; still short of register space xor bx, bx adc bx, 0 ; save carry information mov cx, dx ; cx=A0B1H A1_x_B1: mov ax, [si+2] ; ax=A1 mul word ptr [di+2] ; dx=A1B1H, ax=A1B1L add cx, bx ; cx=A0B1H+stored carry adc cx, bp ; cx=A0B1H+A1B0H adc cx, ax ; cx=A0B1H+A1B0H+A1B1L+carry pop bx ; restore pointer mov es:[bx+4], cx ; store 2nd column sum adc dx, 0 ; dx=A1B1+carry mov es:[bx+6], dx ; store 1st column sum ; now adjust for negative numbers test_A: mov ah, [si+2] ; ah=high byte of A or ah, ah ; IF A is negative js subtract_B ; THEN subtract B from ; high DD of result test_B: mov ah, [di+2] ; ah=high byte of B or ah, ah ; IF B is negative js subtract_A ; THEN subtract A from ; high DD of result jmp emulate_done subtract_B: mov ax, [di] ; ax=B0 mov cx, [di+2] ; cx=B1 sub es:[bx+4],ax ; adjust the two high words sbb es:[bx+6],cx jmp test_B subtract_A: mov ax, [si] ; ax=A0 mov cx, [si+2] ; cx=B1 sub es:[bx+4], ax ; adjust the two high words sbb es:[bx+6], cx emulate_done: pop bp ; restore caller's state pop dx pop cx pop ax imul_32_exit: ret imul_32 ENDP code ends end ████████████████████████████████████████████████████████████████████████████ TIFF: An Emerging Standard for Exchanging Digitized Graphic Images ─────────────────────────────────────────────────────────────────────────── Also see the related article: The CCITT/3 Scheme in TIFF ─────────────────────────────────────────────────────────────────────────── Nancy Andrews and Stan Fry☼ In the early days of microcomputers, PC users considered themselves lucky to have a paint program with basic drawing tools. Users could create simple drawings for documents, print them, and paste them in by hand. Unfortunately, the end results looked amateurish and, for many purposes, were barely acceptable. The difficulty was what if users wanted something more complex than what the available tools and their drawing skills allowed? What if they wanted to use existing line art or photographs? Users really needed to have the ability to digitize the professionally drawn art they had on paper and then electronically paste it into their documents. The arrival of scanner hardware solved this problem. Most scanners could scan an existing piece of art or a photograph at an amazing 300 dots per inch (dpi). This sounded like the answer to a document producer's dreams, but the puzzle had one missing piece. Users still needed a standard file format so that they could use any scanner to digitize an image, edit it with the paint or graphics program of their choice, and than electronically paste the image into their documents. Furthermore, having the whole process work quickly would be very helpful. Enter TIFF Aldus Corp., creator of PageMaker for the Macintosh and PC, recognized the need for a standard way to exchange digital data, took the initiative, and, with the assistance of several vendors, developed Tag Image File Format (TIFF). TIFF provides features that support most input devices, supports a variety of data compression techniques, and has the flexibility to add new features easily in a controlled fashion. TIFF's features can support scanners, point programs, and cameras (see Figure 1). It works just as well with a simple Windows Paint document as it does with a complex medical imaging scanner. TIFF files contain tags that describe not only the height and width of an image, but also provide resolution information, information about gray scale or color, and the type of compression scheme being used. Currently TIFF supports a modified form of the run-length compression, CCITT Group 3. CCITT/3 provides reasonable compression on black-and-white images that are 200 or 300 dpi by identifying continuous runs of all black or all white pixels. It then replaces these with a unique code word that can be used to reconstruct the original continuous run of data. You get the compression because the code words are typically shorter than the run of data they are replacing. CCITT/3's compression on dithered gray scale or color images isn't a good because redundancy isn't found in the same way as it is in standard black-and-white images. For this reason, TIFF also supports a simple packed scheme in which data is packed into bytes as tightly as possible with no unused bits, except at the end of a row. Specific compression schemes for gray and colored images will be added; Microsoft, the current keeper of the TIFF specification, will welcome you suggestions. TIFF makes it possible to add new features without having to rewrite supporting software each time a new feature is added. For example, if one application wants to add a "date-of-creation" tag to an image, it can be included as a tag when the application is written. Other applications that read TIFF files don't need to be rewritten unless they intend to make use of this new date-of-creation tag. The TIFF specification claims that "very high priority has been given to structuring the data in such a way as to minimize the pain of future additions." When additional capabilities, such as scanning color images, are available, TIFF should be able to accommodate them as well. How TIFF Works Traditional painting file formats, such as PC Paintbrush, use a fixed format organization (see Figure 2). The organization and location of each value is known prior to reading the file. For example, the x resolution information in this example is always stored in bytes 2 and 3 of the record. TIFF is not positionally based. Instead it uses tagged information, a style typically used in database design. Tags define the attributes of the image and how the actual image is stored. Each image has a header followed by tags that describe information contained elsewhere in the file. Tags consist of the name of the tag information, the size and length of the information described in the tag, and a pointer to the actual information (see Figure 3). Tags can expand to contain as much information as needed about the image, so that you can add as many tags as you need. You'll only need a few tags for simple images; you may need many tags for complex images. The header and tags for an image are called the Image File Directory. Because of its structure, you can store multiple versions of the same image in one file. This could be useful if, for example, you wanted to store a full-resolution version of an image for editing and printing and a subsampled version for better display performance, as shown in Figure 4. Each version has its own IFD with a unique set of attributes and pointers to the actual data. TIFF permits you to store the image data in variable-length strips or scan lines, giving you random access to any part of the image. If you just need one section of a very large image and you've stored the image in strips, you can decompress and load only the strips you need. And when you modify an image, you need to restore only the modified strips, not the entire image. Advantages For software and hardware developers, the advantages of a standardized format for exchanging digital data are obvious. Rather than support a proprietary file format for each scanner vendor, desktop publishing application, and paint or draw program, developers need to support just one file format. Even with a complex file format like TIFF, this is better than supporting a constantly growing number of different formats. TIFF is considered to be a "rich" file format. Rather than reduce all images to the lowest common denominator, TIFF is designed to handle many different kinds of images, line art, gray scale, and color, and different resolutions as well. TIFF supports different compression schemes and is flexible enough to adapt the compression algorithm to fit the type of data. It will expand to handle more complex images and compression schemes as the technology advances. Adding more features does not require rewriting existing applications if those applications don't want to take advantage of the expanded capability. Besides handling complex images well, TIFF also works with such simple images as those produced by paint programs, because the application can use only the parts of TIFF it requires. Another advantage of TIFF is its machine independence. You can use it with Intel or Motorola processors with UNIX, MS-DOS, or Macintosh operating systems, which encouraged Mitch Murphy, project manager at OptiGraphics Corp., to convert to TIFF. OptiGraphics, which makes engineering workstations and large-document scanners, were looking for a file format to use for all raster images for both its PC and UNIX machines. Murphy said it chose TIFF so it could "gracefully upgrade and maintain compatibility as products mature." Products from Murphy's group, such as View! and Markup!, are Microsoft Windows applications that allow you to view and make minor changes to large engineering and architectural drawings. The scanner outputs the drawings in TIFF, and View! and Markup! read and display the TIFF output. Later, if a drawing needs more extensive work, there are products that automatically vectorize the TIFF file so it can be used with a CAD system. Limitations TIFF's richness, which is one of its major advantages, is also the source of its limitations. Although it's not terribly difficult for a scanner to write a TIFF image file, it is reasonably complicated to write a TIFF reader. Marc Cantor, who is president of MacroMind, faults Aldus and Microsoft for not including a decoding algorithm with the TIFF specification. As it stands now, each company has to write its own parser; Cantor's company, whose product Graphic Works reads a scanned TIFF document of any size, wrote its own parser and is selling it to others. To remedy the problem, Microsoft will be making TIFF tools available. Another limitation is that there is not a standard TIFF file, nor a common subset of features that everyone using the name TIFF must support. Different vendors can support different Tag fields of TIFF. OptiGraphics' Murphy says that "TIFF is so rich that dialects will emerge." To prevent this, he and others suggest using common tools, which are becoming available. Murphy has a library of routines to access TIFF files in an orderly way; this helps to keep track of all the levels of indirection. If you contact him at OptiGraphics at (619) 292-6060, he'll send them to you for free. However, he cautions that you "use them at your own risk." The development engineers at Hewlett-Packard have made a significant contribution to the TIFF spec and have written a lucid Guide to the Tagged Image File Format. Hewlett-Packard also has some source code for reading, writing, compressing, decompressing, and debugging TIFF files. For information, call HP's ISV marketing department at (303) 350-4000. Dest Corp., a scanner hardware manufacturer, also has a library of routines it will license to desktop publishing vendors free of charge. The phone number is (408) 946-7100. And Tim Davenport at Aldus will send you sample TIFF files; for information, call (206) 622-5500. The New Standard? TIFF is rapidly becoming the standard for handling bit-mapped images. Companies like HP, Aldus, and OptiGraphics have put a significant amount of research and development into TIFF. Most echo Murphy's sentiments: now that they've committed to TIFF, they want others to commit as well, and they're willing to help. David Rosenbloom of Cygnet Technologies, a high-end telecommunications firm, chose TIFF rather than a proprietary format for its FAX product because it had what it needed, a generic descriptive header with an unlimited number of fields and :a shot at becoming some sort of accepted standard." Other firms that have publicly announced support for TIFF are Dest, DataCopy, Microsoft, Microtek, Media Cybernetics, New Image Technology, and Software Publishing. Support for TIFF is bridging the traditional rift between the PC and Macintosh camps. Although Steve Carlson at Aldus is the originator of the TIFF specification, Microsoft is now the keeper of TIFF. With Microsoft, a neutral player, managing TIFF rather than a hardware or desktop publishing vendor, companies are more likely to see TIFF as a standard instead of an aid to competitors. Microsoft is currently working with those companies whose products scan and archive images. It also wants to work with such page description vendors as Adobe, DDL, and Interpress to ensure that the functions TIFF supports are mapped closely to future functions of page description languages. If, for example, compression schemes are compatible, print time can be significantly improved. Manny Vellon of the Microsoft Windows marketing group is the new TIFF administrator; you can write to him for a copy of the spec and also feel free to send him your suggestions. Currently, the desktop publishing market is most interested in TIFF, but the market is expanding to include OCR, FAX, 3-dimensional CAD, and sophisticated medical imaging. Someday, instead of going to your bookshelf and reaching for a volume in your published art collection, you may go to your CD ROM disc and get high-quality typeset images stored in TIFF format. Soon you may be able to receive an engineering drawing as a FAX on your PC, use an application to vectorize the drawing, touch it up with your CAD program or the specs that accompany the drawing in another FAX, run an application to OCR the specs, edit them with your word processor, and then send them back in FAX form. These hardware and software applications will be able to talk and work together when there is a true standard for exchanging digital image data; TIFF is well on its way to becoming that Standard. Figure 1: TIFF's features can support scanners, paint programs, and cameras and treats simple paint programs just as it does complex medical documents. ╔══════════════╗ ┌░░░▒▒┐ ║ ░░░░░░░░░░░ ║█ Camera └──░──┘ __________ ║ ░░░≡ ≡≡░░░░ ║█ ┌───▀▀────────────▀▀▀─┐ ╓───/ /─╖ ║ ░░░≡≡≡≡≡░░░░ ║█ │╔════╗ ◘▒◘░░░░╔════╗ │█ ╔═╝──┴────────┴──╚═╗ ║ ░░░░░░░░░░↔↨ ║█ │║░░░▒║ ╔════╗ ║░▒▒▒║ │█ ║ ----‼‼‼‼‼‼‼‼‼‼‼ ║█ ╔═╝──────────────╚═╗ │║░░░▒║ ║ ░║ ║░░▒▒║ │█ ╚══════════════════╝█ ║ ----‼‼‼‼‼‼‼‼‼‼‼ ║█ │╚════╝ ╚════╝ ╚════╝ │█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ╚══════════════════╝█ └─────────────────────┘█ Scanner ┌──── ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ │ Desktop Publishing │ tiff │ ╔════════════╗ tiff │ ascii ║ ░░░░☻░░░░ ║█ │ ┌──▄▄▄▄▄▄▄▄──\ │ ╔══════════╗ ║ ░░░ █ ░░░░ ║█ └─────────│ ██████ █ │ │ ║ ▒▒▒▒▒▒▒▒▒║█ ║ ░░░ ░░░░ ║█ ───tiff──────│┌──────────┐│─────┤ ║ ▒▒▒▒▒▒▒▒▒║█ ║ ░░░░▌▐░░↔↨ ║█ ││ Disk ││ │ ╔════╩──────────╩══╗ ╔═╝────────────╚═╗ │└──────────┘│ │ ║ ----‼‼‼‼‼‼‼‼‼‼‼ ║█ ║ ----‼‼‼‼‼‼‼‼‼ ║█ └────────────┘ │ ╚══════════════════╝█ ╚════════════════╝█ │ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ Word Processsor Paint/Draw └──── Figure 2: Fixed Format Organization ╔════╗ ┌────────────────────────────┐ ║ 0 ║ │ Revision # │ ╟────╢ ├────────────────────────────┤ ║ 2 ║ │ X Resolution │ ╟────╢ ├────────────────────────────┤ ║ 4 ║ │ Y Resolution │ ╟────╢ ├────────────────────────────┤ ║ 6 ║ │ H Pixels │ ╟────╢ ├────────────────────────────┤ ║ 8 ║ │ V Pixels │ ╟────╢ ├────────────────────────────┤ ║ 10 ║ │ Colorplane │ ╟────╢ ├────────────────────────────┤ ║ ║ │ │ ║ ║ │ │ ║ ║ │ │ ║ 64 ║ ≈ Data ≈ ║ ║ │ │ ║ ║ │ │ ║ ║ │ │ ╚════╝ └────────────────────────────┘ Figure 3: TIFF uses tagged information, a style typically used in database design. Tags define the attributes of the image and how the actual image is stored. Each image has a header followed by tags that describe information conatined elsewhere in the file. ┌─────────────────────┐ ≈ Tiff Header •───≈──┐ ├─────────────────────┤ │ ┌──────────┐ ╔═══╗ ╔═══╗ │ Tag 1 ◄───────┼──┘ •│ Tag Type │█ ║ T ║ ║ D ║ ├─────────────────────┤ • ├──────────┤█ ║ A ║ ║ I ║ │ Tag 2 │• │ Size │█ ║ G ║ ║ R ║ ├─────────────────────┤ • ├──────────┤█ ║ ║ ║ E ║ │ Tag 3 │ •│ Length │█ ║ E ║ ║ C ║ ├─────────────────────┤ ├──────────┤█ ║ N ║ ║ T ║ │ Tag 4 •───┼──┐ │ Data or │█ ║ T ║ ║ O ║ ├─────────────────────┤ │ │ Pointer │█ ║ R ║ ║ R ║ │ Tag 5 │ │ └──────────┘█ ║ Y ║ ║ Y ║ ├─────────────────────┤ │ ███████████ ╚═══╝ ╚═══╝ │ Tag 6 •───┼──│─┐ └──────────:──────────┘ │ │ ┌──────────:──────────┐ │ │ │ Long Tag Data ◄────┼──┘ │ └──────────:──────────┘ │ ┌──────────:──────────┐ │ │ Bitmap Image Data ◄─┼────┘ └─────────────────────┘ Figure 4: The header and tags for an image are called the Image File Directory (IFD). It is structured so that you can store multiple versions of the same image in one file. ┌────────────┐ Image Data ┌────────────────────┐ ┌─►│ Pointer to ├─┐ ┌──────────┐ │ File Header │ │ │ Strip 1 •─────►│ Strip 1 │ ├────────────────────┤ │ ├────────────┤ ├─┐ ├──────────┤ ┌─•Pointer to first ifd│ │ ≈ ≈ │ │ │ │ │ └────────────────────┘ ┌──────────┐ │ ├────────────┤ │ │ ≈ ≈ │ Full Resolution ifd ┌─►│Pointer to│ │ │ Pointer to │ │ │ │ │ │ ┌─────────┬──────────┐ │ │ Plane 1 •─┘ │ Strip n •────┐ ├──────────┤ └►│ # of │ Subfile │ │ ├──────────┤ └┬───────────┘ │ │└►│ Strip n │ │ entries │ Type │ │ ≈ ≈ └┬────────────┘ │ ├──────────┤ ├─────────┴──────────┤ │ ├──────────┤ ┌───►│ Pointer to │ │ │ │ Tags •─┘ │Pointer to│ │ │ Strip n │ ≈ ≈ ├────────────────────┤ │ Plane m •─┘ └──────────────┘ │ │ ┌─•Pointer to next ifd │ └──────────┘ ┌────────────┐ ├──────────┤ │ └────────────────────┘ ┌─►│ Pointer to ├─┐ ┌►│ Strip 1 │ │ Subsample ifd ┌──────────┐ │ │ Strip 1 •────┘ ├──────────┤ │ ┌─────────┬──────────┐ ┌─►│Pointer to│ │ ├────────────┤ ├─┐ │ │ └►│ # of │ Subfile │ │ │ Plane 1 •─┘ ≈ ≈ │ │ ≈ ≈ │ entries │ Type │ │ ├──────────┤ ├────────────┤ │ │ │ │ ├─────────┴──────────┤ │ ≈ ≈ │ Pointer to│ │ │ ├──────────┤ │ Tags •─┘ ├──────────┤ │ Strip y •───┼─►│ Strip y │ ├────────────────────┤ │Pointer to│ └┬───────────┘ │ │ ├──────────┤ │Pointer to next ifd │ │ Plane x •───┐ └┬────────────┘ │ │ │ └────────────────────┘ └──────────┘ └─►│ Pointer to │ ≈ ≈ │ Strip y │ │ │ └──────────────┘ │ │ ─────────────────────────────────────────────────────────────────────────── The CCITT/3 Scheme in TIFF ─────────────────────────────────────────────────────────────────────────── The CCITT/3 scheme used in TIFF identifies continuous runs, which are called run-lengths, of either all white or all black pixels. Each of these run- lengths is then replaced with a code word that can later be used to reconstruct the continuous run of data. The compression scheme uses two tables. One table contains code words for different continuous runs of white pixels from 0 to 63 pixels in length. The other table contains code words for the same continuous runs of black pixels. The tables also contain values for some discrete run-lengths above 63. If the white- or black-pixel run-lengths exceed 63, combinations of codewords can be used to compress these longer run-lengths. Each row of the bitmap is compressed independently from any other row. In addition, each row must start with a white pattern and must end on a byte boundary. Starting with a white pattern provides synchronization and allows the decompression algorithm know whether to search in the black or white run-length table. If the row starts with one or more black pixels, then a white run-length of 0 pixels must start the row of compressed data code words. End-of-line (EOL) markers, common to CCITT/3 used for FAX, are not used. If EOL markers are used, readers of TIFF files would not be able to interpret the information. Also, the TIFF PhotoMetric interpretation tag doesn't apply when using CCITT compression. The values for white and black are predefined. You may need to invert all binary data before encoding it in CCITT format. Any padding bits added to the uncompressed image row must not be compressed. You should only compress the length of data that was specified in the width tag field. For example, if a bitmap consisted of 10 pixels across, it would be stored uncompressed as 2 bytes so that each row would begin on a byte boundary. In this case, six padding bits would have been added. When compressing this information, only the first 10 bits should be considered as the data to be compressed, not the six padding bits. ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob! Microsoft Pascal Inquiries Dear Dr. Bob, I received my first copy of MSJ last week, and from now on I will look forward to your column, since it gives me an inkling of what the rest of the world is doing. I have several questions about Microsoft(R) Pascal Version 3.31. In your March 1987 column (MSJ, Vol. 2 No.1), you refer to the C library function getenv(). Microsoft Pascal 3.31 supplies a part of the C library with the functions named SYSTEM and SPAWNLP, but no GETENV or any others. Is there a way to get and use them from Microsoft Pascal? In Microsoft Pascal, how can I make a single procedure definition file that can be included in all program modules, even the ones defining the procedures in the include file? Normally this gives some kind of "duplicate identifier" error. I use ALLMQQ extensively. Is there a problem with excessive use of ALLMQQ and FREMQQ, even if I am careful to free up what I don't use? When I allocate several large (25Kb) blocks and then free them up, sometimes I can't allocate them again. Granted, there are additional scores of ALLMQQs and FREMQQs going on at the same time. Where can I get information on resident programming techniques? I am totally ignorant, and the standard language manuals don't even hint at it. ──RHK If you have the Microsoft C Compiler, Version 3.0 or Version 4.0, you may use any of the large-memory model C libraries from a Microsoft Pascal 3.31 program. Actually, you can mix large model C code with Microsoft Pascal or FORTRAN code, if you like. See the appendix on mixed-language programming in your Pascal User's Guide. With regard to GETENV, you are particularly lucky. It is present in the subset of the C library included with Pascal 3.31 in the file CEXEC.LIB. Figure 1 is a small program that uses GETENV to print out the PATH. The Microsoft Pascal metacommands for conditional compilation ($if, $then, $else, $end) provide a way to make a single module definition file. First, create the definition file with the procedure and function definitions followed by "extern;", grouping the definitions together by module. Next, put an "$if moduleX $else ... $end" conditional around each module's group of definitions, where X is the module number, as shown in Figure 2. Finally, include this definition file in all your program's modules following a list of constant definitions. Make all the constants 0, except for the one for the current module. In the file for module number 1, for instance, you would place the following: CONST module1 = 1; module2 = 0; module3 = 0; dummy = 0; {$include:'define.inc'} The constant "dummy" is required because the compiler keeps one look-ahead symbol as described in section 18.3 of the Pascal 3.31 Reference Manual. Dr. Bob knows of no bugs in the ALLMQQ and FREMQQ memory management functions, but it is easy to make a sequence of calls to ALLMQQ and FREMQQ that will cause ALLMQQ to fail even when there seems to be lots of free space. This is because the memory heap can become fragmented, that is, broken into small pieces of alternating free and allocated blocks, so that there is no single chunk large enough for the ALLMQQ request. A more sophisticated memory management scheme would have a "garbage collector" to shuffle memory blocks around and put all the free space together to give ALLMQQ a chance to allocate a large block. However, adding a garbage collector opens its own can of worms and makes programming more complicated. There is no general-purpose method for working around this fragmentation problem. The best you can do is try to allocate large blocks early in your program, before the heap gets fragmented. Also, use ALLMQQ and FREMQQ less often by reusing a block that's already been allocated. Glad you asked about TSRs. The December 1986 issue of MSJ (Vol. 1 No. 2) has an article on TSRs, "Moving Toward an Industry Standard for Developing TSRs." Also, we've seen articles on writing TSRs in recent issues of Byte and Dr. Dobb's Journal. Environment Size in DOS Dear Dr. Bob, It appears that with MS-DOS Version 3.2 the only way to increase the environment size is with COMMAND /E:xxxx. I have tried putting this command into my AUTOEXEC.BAT with an additional /P switch to make it permanent. This results in an infinite regression of COMMAND invoking AUTOEXEC, which runs COMMAND and invokes AUTOEXEC , which ... you get the idea. How can I do this without getting thrown for a loop?──EG When started with the /P switch, COMMAND.COM looks for an AUTOEXEC.BAT in the root directory of the current disk. You can take advantage of this by making a small AUTOEXEC.BAT on your boot disk that copies the "real" AUTOEXEC.BAT to a RAMdisk and runs the secondary version of COMMAND from there. Figure 3 shows what such an AUTOEXEC might look like, where C: is the boot disk and D: is a RAMdisk. In the example, drive C: is the boot disk, drive D: is the RAMdisk, and AUTOEXEC.2ND contains the rest of the boot sequence normally found in AUTOEXEC.BAT. An Alternative Suggestion Dear Dr. Bob, Your delightfully convoluted response to Forgetful in the March 1987 issue of MSJ (Vol. 2 No. 1) prompts me to suggest this alternative. Instead of twice compiling an IFDEFed C file and piping it into both an .INC and an .H file for inclusion in both assembly and compilation processes, try the trick shown in Figure 4, which depends on a basic difference between C and MASM comments: C comments can cross line boundaries, while MASM comments go to the end of the line. This file can be directly included in both the assembly and the C source code. Perhaps there is an even more compact and bizarre method of achieving the same goal, but I can't think of it.──BN You have a cute idea there, but we find it deficient in two respects. First, there are still two distinct copies of each piece of information, and it would be all too easy to change one without changing the other. This defeats the purpose of the system, which is to make a single collection of definitions that will automatically serve for both assembler and C programming. Second, none of our C compilers will accept your code, as they all require the #define directives either to start in the first column or to be the first nonwhite space on a line. Comments and Corrections Dear Dr. Bob, In Dr. Bob's "Self-Modifying Code" in the March 1987 issue of MSJ (Vol.2 No.1), the answer is incorrect. LINK4 does not accept the MULTIPLE option for the CODE statement. I have struggled with the problem of forcing Windows to use different code segments for multiple instances when I was writing about Windows memory management. To my mind, Windows should load different code segments for multiple instances when you specify that a CODE segment is IMPURE or NONSHARED in the module definition file. This is valid syntax, but Windows seems to ignore these options. The only way I found to force Windows to load multiple code segments for different instances is by making the .EXE name different from the NAME of the module in the .DEF file. I discovered this bug by accident, and it certainly doesn't make any sense to me. I should also note that even if you get Windows to load multiple code segments, it will still discard code segments if you specify only MOVEABLE without DISCARDABLE. This is implied by the text of your answer, but not by the example. You have to make a code segment FIXED to prevent Windows from discarding it. For the stock bitmap question, I think the answer should have included the fact that the stock bitmap has a width and height of only one pixel, so it is useless for drawing until you select another bitmap onto it.──CP MULTIPLE (or IMPURE or NONSHARED) code segments are not supported. All code segments are by definition shared. It is unfortunate that the documentation is not clearer on this point. The documentation is also not clear about saying that all CODE can either be FIXED or DISCARDABLE and that MOVEABLE implies DISCARDABLE for code segments. As for forcing Windows to load multiple code segments for different instances by making the .EXE name different from the NAME of the module in the .DEF file, I suspect that what was happening was not what you think it was. It definitely does not load multiple instances of the same code segment. Dear Dr. Bob, In MSJ, Vol. 2 No. 1, Dr. Bob tells a very good story about why Windows uses only three of the four planes on the EGA, but unfortunately it's totally wrong. As the programmer who first wrote the Windows EGA driver, I know what I'm talking about. The mouse cursor is stored in off- screen memory in planes 0-2. Windows also uses planes 0-2 for display. Plane 3 is not used at all by Windows. Plane 3 by itself wouldn't have been enough for the cursor──I wish it had been, as it would have made my job a lot easier──since the Windows cursor requires two planes: it supports white, black, transparent, and inverted pixels. Why is the fourth plane unused? When I did the driver originally, it was because I had to get the thing working in a matter of days and I didn't have time to fool with the fourth plane. Since the fourth plane is most logically used as some kind of intensity bit, and this is not independent of the R, G, and B bits (that is, you can't make R intensified without simultaneously making G and B intensified), this fourth bit doesn't really fit into the GDI virtual RGB model very well, so logical <-> physical mapping is hard to do. Since then, the code has been maintained by someone else, and support for the fourth plane was never added, probably because of the awkward logical <-> physical mapping mentioned above.──BC Figure 1: program EnvTest(input,output); type chrptr = ^array[0..1000] of char; var path : chrptr; i : integer; function getenv : chrptr [c,varying]; extern; begin write('Dr Bob''s PATH = '); path := getenv(ads('PATH'*chr(0))); i := 0; while (path^[i] <> chr(0)) do begin write(path^[i]); i := i+1; end; end. Figure 2: {$if module1 $else} {definitions for module1:} procedure f1; extern; {$end} {$if module2 $else} {definitions for module2:} procedure f2(p2:integer); extern; {$end} Figure 3: COPY AUTOEXEC.2ND D:AUTOEXEC.BAT D: C:COMMAND /p Figure 4: ;/* ScreenHt=200 ; */ #define ScreenHt (200) /* comment ScreenWidth=640 ; */ #define ScreenWidth (640) /* comment Point STRUC ; */ struct Point { /* comment x DW ; */ int x; /* comment y DW ; */ int y; /* comment Point ENDS ; */ }; ════════════════════════════════════════════════════════════════════════════ Vol. 2 No. 4 Table of Contents Microsoft(R) Windows/386: Creating a Virtual Machine Environment Windows/386 exploits the 80386's virtual machine capabilities to offer an environment that allows the user to simultaneously run and switch between existing MS-DOS and Windows applications. The product fully implements preemptive multitasking and data exchange between applications. Programming in C the Fast and Easy Way with Microsoft(R) QuickC(TM) Making the process of C programming faster and easier has long been a goal for developers of Microsoft C programming products. The Microsoft QuickC compiler offers an integrated environment that provides programmers with high-speed compilation and an easy debugger. Character-Oriented Display Services Using OS/2's VIO Subsystem Microsoft OS/2 provides application programs with a wide variety of display functions. The OS/2 video capabilities detailed in this article are the VIO services──the subsystem that lets you get your applications up and running quickly without the Windows Presentation Manager. Dynamic Allocation Techniques for Memory Management in C Programs How well memory is managed can make a real difference in the performance of a program, particularly those programs that process unpredictable amounts of data. Dynamically allocating memory to hold array and linked list data provides your programs with extra flexibility and utility. CD ROM Technology Opens the Doors on a New Software Market Thanks in part to the success of the compact audio disc, CD ROM is viable and growing. Developers with applications requiring large amounts of data space, such as parts catalogs, can benefit from CD ROM's huge data capacity and low cost duplication. MS-DOS(R) CD ROM Extensions: A Standard PC Access Method Using the Microsoft MS-DOS CD ROM Extensions frees developers to concentrate solely on their applications, eliminating dependence on any particular drive technology. This article discusses CD ROM device drivers and the MSCDEX.EXE program that interfaces with MS-DOS. Microsoft(R) QuickBASIC: Everyone's First PC Language Gets Better You can create elegant applications in BASIC by combining the powerful Microsoft QuickBASIC 3.0 control structures and statements, the INT860 interface to MS-DOS and BIOS services, and custom assembly-language routines of your own design. Here's how. Ask Dr. Bob EDITOR'S NOTE Microsoft(R) Windows/386 provides users of 80386-based computers with a new operating environment. Its principal benefit is the flexibility to simultaneously run nearly any combination of MS-DOS applications, each in its own virtual machine. While it does not offer developers a new programming interface, we think you will appreciate a behind-the-scenes look at the product's design. Meanwhile, many software developers are working hard to unravel the details and intricacies of MS(R) OS/2. The first OS/2 applications will take advantage of the rich set of character-based display functions that are part of the OS/2 API. In this issue, Ray Duncan explores the use of these functions. However, operating environments aren't all that's new. QuickC gives both the professional and beginning C programmer a new tool──the integrated working environment first seen in QuickBASIC. CodeView(R)-compatible debugging aids and superfast compilation speeds helped create TOUCH.C, a useful file date-stamp utility included here. We also have two articles about a new kid on the PC block──CD ROM, which finally seems ready for general acceptance. All our source code listings can now be found on DIAL, CompuServe, BIX, and two public access bulletin boards. On the East Coast, users can call (212) 889-6438 to join the RamNet Bulletin board. On the West Coast, call (415) 284-9151 for the ComOne bulletin board. In either case, look for the MSJ directory. A reminder──we read all of our mail, hardcopy and electronic (our MCI mailbox is MSJ). Write and let us hear from you. If you're interested in submitting a manuscript drop Tony Rizzo, our technical editor, a note. He would be more than happy to hear from you.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor CHRISTINA G.DYAR Associate Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor KIM HOROWITZ Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher BETSY KAUFER Administrative Assistant Copyright(C) 1987 Microsoft Corporation. All rights reserved; reproduction in part or in whole without permission is prohibited. Microsoft Systems Journal is a publication of Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H. Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley, President and Chief Operating Officer; Francis J. Gaudette, Treasurer; William Neukom, Secretary. Microsoft Corporation assumes no liability for any damages resulting from the use of the information contained herein. Microsoft, the Microsoft logo, CodeView, MS, MS-DOS, and XENIX are registered trademarks of Microsoft Corporation. QuickC and Bookshelf are trademarks of Microsoft Corporation. IBM and PC AT are registered trademarks of International Business Machines Corporation. PS/2 is a trademark of International Business Machines Corporation. dBASE II is a registered trademark of Ashton-Tate Corporation. AT&T and UNIX are registered trademarks of AT&T. Lotus and 1-2-3 are registered trademarks of Lotus Development Corporation. Intel is a registered trademark of Intel Corporation. COMPAQ is a registered trademark of COMPAQ Computer Corporation. DESKPRO 386 is a trademark of COMPAQ Computer Corporation. CP/M is a registered trademark of Digital Research, Inc. Hercules is a registered trademark of Hercules Computer Technology. WordStar is a registered trademark of MicroPro International Corporation. Zenith is a registered trademark of Zenith Radio Corporation. ████████████████████████████████████████████████████████████████████████████ Microsoft Windows/386: Creating a Virtual Machine Environment ─────────────────────────────────────────────────────────────────────────── Also see: A Comparison of Current and Future Windows Versions ─────────────────────────────────────────────────────────────────────────── Ray Duncan☼ The past year has seen the emergence of a new class of personal computers, based on the IBM(R) PC AT(R) architecture but incorporating an Intel 80386 microprocessor with a 32-bit memory path for increased performance. The pioneer in this category was the COMPAQ(R) DESKPRO 386(TM), which has since been joined by the IBM PS/2(TM) Model 80, as well as a score of machines from other clone vendors. Until now, the primary benefit associated with these machines has been their formidable speed. However, 32-bit operating systems and programming tools for the 80386 are still in the development stage, while 32-bit applications software waits in the wings until the tools stabilize. Microsoft Corp.'s new product, Windows/386, unmasks the larger potential of 80386 machines while protecting the user's investment in MS-DOS(R)- compatible hardware and software. The key features of Windows/386 are: ■ a graphical user interface compatible with Windows 2.0 and the OS/2 Presentation Manager ■ true preemptive multitasking of MS-DOS applications, each in a private 640Kb memory space ■ applications that run under Windows/386 receive much more memory than they would under Windows 2.0 ■ the ability to run MS-DOS applications in overlapping windows, even so-called "ill-behaved" applications that do not rely on MS-DOS or the ROM BIOS for screen output ■ exchange of screen data between both standard MS-DOS and Windows applications ■ emulation of the Lotus/Intel/Microsoft Expanded Memory Specification Version 4.0 Windows/386 requires either an 80386-based PC AT-compatible or an AT&T(R) 6300 computer, an EGA, VGA, or CGA monitor, at least 1Mb of RAM (2Mb are recommended), and a fixed disk. The 80287 or 80387 numeric coprocessors are also supported. An Intel Retrospective Nearly all of the capabilities of Windows/386 depend on a feature of the 80386 called virtual 86 mode. To fully appreciate this particular facet of the 80386, it is necessary to digress for a moment and review the characteristics of the 80386's ancestors, the Intel 8086/88 and 80286 microprocessors. Each successive generation of Intel processors has supported the software that was written for its predecessors by means of "execution modes," and virtual 86 mode is the logical culmination of this approach to software compatibility. The first Intel 16-bit processors, the 8086 and 8088 (announced in 1978 and 1979 respectively), can address a maximum of 1Mb of memory. When memory is referenced, the contents of one of the four segment registers is shifted left four bits and combined with a 16-bit offset to form a 20-bit physical address; the segment registers simply act as base pointers. The 8086 and 8088 have no provision for memory protection, virtual memory, or privilege levels; any program can access any location in memory or any I/O device without restriction. The Intel 80286 (first shipped in 1982) represents a major increase in speed and capabilities over the 8086/88. It can run in either of two modes: real or protected. In real mode, the 80286 acts like a fast 8086 with a few additional machine instructions. It can run virtually all 8086/88 software and is limited to 1Mb of memory. In protected mode, the 80286's mapping of addresses is altered to add a level of indirection. The value in a segment register is a selector, which is an index to an entry in a descriptor table that contains the base address and length of a physical memory segment, segment attributes (executable, read-only, or read-write), and privilege information. Each time a program references memory, the hardware accesses the associated descriptor to generate the physical address and simultaneously checks to make sure that the memory access is valid. This method of protected-mode address generation allows the 80286 to support memory protection and virtual memory management within a physical address space of 16Mb and a virtual address space of 1Gb. Four levels of privilege are also provided, allowing the operating system to be protected from applications programs and those programs from each other. When the Intel 80286 was designed, the dominant software base consisted of CP/M(R) applications such as WordStar(R) and dBASE II(R). At the time the 802 was released, the IBM PC was just a few months old. Hence Intel engineers had no way of foreseeing the incredible success of the IBM PC and its compatibles, or the eventual need of an enormous user base to make a smooth transition from real-mode (8086/88) to protected-mode (80286) environments. Although the 80286 was designed to start up in real mode for compatibility with 8086 software, and although machine instructions were included to switch from real mode to protected mode, no mechanism was built into the chip to allow a return from protected mode to real mode under operating system control without halting the processor. This single omission proved to be a hideous technical problem during the development of the Microsoft OS/2, since one of the overriding design goals for the new operating system was to allow "old" (real-mode) applications to run alongside "new" (protected-mode) applications. Although experimentation and painstaking optimization eventually led to an acceptable solution for the necessary mode switching, another drawback remains: the protected-mode operating system cannot be shielded against bad behavior on the part of a real-mode program. By the very nature of real mode, such a program has a free hand with the hardware and can easily crash the machine. The Four Modes The Intel 80386 (introduced in 1985) is a true 32-bit processor that supports a 4Gb physical address space and a 64Tb virtual address space. To ensure compatibility with previous processors and to solve the problems of support for real-mode applications that were encountered with the 80286, the 80386 has no less than four different execution modes. The first is the familiar real mode, wherein the 80386 functions as a fast 8086/88- compatible processor with some bonus opcodes. Like the 80286, the 80386 always powers up in real mode and can therefore run any existing 8086 operating systems and software. In protected mode, the 80386 can take on two different personalities. It can execute a logical superset of the 80286 protected-mode instructions and run 16-bit programs, or it can run in its native mode, which offers 32-bit instructions, registers, and stacks, and allows individual memory segments as large as 4Gb. In either case, the 80386 translates selectors and offsets to linear addresses using descriptor tables in much the same manner as the 80286. However, an additional level of address translation──supported in hardware by page tables──allows much greater flexibility in mapping the linear addresses onto physical memory. Unlike the 80286, the 80386 allows the operating system to switch smoothly and quickly back from protected mode to real mode when necessary. But it is unlikely that this capability will find much use because of the 80386's fourth operating mode: virtual 86 mode. Four Modes of the Intel 80386 Microprocessor Real Mode Functions as a very fast 8086/88-compatible processor. Protected Mode Functions in protected mode as an enhanced 286 (16-bit) processor. Protected Mode Functions in protected mode using full 32-bit (32-bit, native instructions, registers, and stacks. mode) Virtual 86 Mode Runs multiple, protected-mode, virtual 8086 machines, each with its own 1Mb of memory space. Device Virtualization A protected-mode 80386 operating system can create special memory segments──up to 1Mb in size──that have a remarkable characteristic: programs that run within these segments execute as though they were running on an 8086/88 in real mode. Each such segment is called a virtual 8086 machine, and each has its own address space, I/O port space, and interrupt vector table. Multiple virtual machines can be running simultaneously, each under the illusion that it is in complete control of the computer. The crucial difference between real mode and virtual 86 mode is that memory protection, virtual memory, and privilege-checking mechanisms are still in effect when a virtual machine is running. Thus, a program executing in a virtual 8086 machine cannot interfere with the operating system or damage other processes. If the program reads or writes memory addresses that have not been mapped into its virtual machine, or if it manipulates I/O ports to which it has not been allowed access, an exception (hardware interrupt) is generated, and the operating system regains control. The operating system's exception handler can choose to carry out the operation on behalf of the program running in the virtual machine, possibly substituting other port or memory addresses; it is also able to arbitrate requests from multiple virtual machines directed at the same I/O port or memory address. This process of intercepting I/O operations, where, for example, the operating system creates the illusion of a separate disk controller or video controller for each virtual machine while only one physical device is present in the system, is called device virtualization. A program that runs in the 80386's native 32-bit protected mode and oversees a set of virtual machines is called a 386 control program, or virtual machine monitor. Windows/386 is a such a virtual machine monitor; it also provides complete device virtualization for the PC's disk controller, video adapter, keyboard, mouse, timer chip, and 8259 programmable interrupt controllers. The User Interface At first glance, the user interface of Windows/386 appears identical to that of Windows Version 2.0: it has the same MS-DOS Executive window, pull-down menus, "hot keys," and utilities (Cardfile, Terminal, and the like). A program is launched by selecting its executable file, its PIF file, or one of its data files with the arrow keys or by double-clicking with the mouse, just like under Windows. An open window can be brought to the foreground with Alt-Tab or Alt-Esc or by clicking on the window with the mouse; also, a window may be resized or moved by clicking and dragging its borders (see Figure 1☼). The first inkling that something different is going on comes when a standard application (that is, an MS-DOS application that is not written specifically for Windows) is started. Under Windows 2.0, only those relatively rare, "well-behaved" MS-DOS programs that perform all of their input and output through standard MS-DOS calls, strictly observe the system's memory management conventions, and avoid all direct access to hardware can run in a window. "Ill-behaved" programs that access the hardware directly for performance reasons──a category that includes nearly every popular MS-DOS application──must run full-screen: Windows 2.0 simply gets out of the way until such programs terminate, and its multitasking comes to a screeching halt. A standard application under Windows/386 initially comes up full-screen just as it does under Windows 2.0. But if the user presses Alt-Enter, the program is suddenly running in an overlapping graphics-mode window right alongside the true Windows application (see Figure 2☼). The user can use the mouse or the cursor keys to select and copy data from a window containing a standard application to any other window and can resize, move, or even "iconize" such a window. Furthermore, the user can toggle between windowed and full-screen mode with Alt-Enter at any time. When an application is full-screen, Windows/386 can place it into a text-display mode to allow faster displays, and the application's intrinsic mouse support works as though it were running under MS-DOS alone. Windows/386 associates a drop-down control menu, activated with the keycode Alt-Space, with each standard application that can be used to affect its window size, position, and behavior (as shown in Figure 2☼). The Settings dialog box (see Figure 3☼), which is reached through the control menu, allows the user to suspend or resume a standard application, specify whether it should run full-screen or in a window, and control its multitasking behavior. The initial settings for a program can also be determined by creating a PIF file for it. There are three multitasking options available for a standard application: foreground, background, and exclusive. When foreground is chosen, the application runs only when it is displayed full-screen or when its window has been selected, but other applications are allowed to run in the background at the same time. The background option means that the application will continue to run even when some other application is selected (of course, this is not useful for word processors and other programs that have nothing to do unless they are receiving keyboard input). When the exclusive option is picked, the application runs only when it is in the foreground, but while it is active it receives all of the CPU time and no other applications are allowed to run at all. Hence the exclusive option permits the application to perform as if it were in a single-tasking MS-DOS environment. Figures 4 and 5☼ show the process of defining an area of the standard application for copying, bringing up another application, and transferring the defined area. A little experimentation leads to an additional pleasant surprise: there seems to be much more RAM left for a standard application than is usual under Windows. In fact, if COMMAND.COM and then CHKDSK are run in a window, approximately 580Kb is seen to be available──as though Windows weren't present at all. And when multiple standard applications are launched, each runs in a separate memory space as large as 640Kb, until the physical memory of the system is exhausted. As a fringe benefit, devotees of RAM-resident applications (TSRs) can find relief from the phenomenon of "RAM cram" by starting multiple copies of COMMAND.COM to establish several MS-DOS sessions, and loading a different selection of TSRs into each session. This is the magic of the 80386's virtual 86 mode in action. How Windows/386 Works The Windows/386 system actually consists of three separate, mutually interdependent elements. The first is a regulation copy of MS-DOS or PC- DOS, Version 3.0 or later. The second is a copy of Windows 2.0, which is included in the Windows/386 product. The third is the core of the Windows/386 product, the Virtual DOS Machine Manager (VDMM). The MS-DOS component supplies the routine file, time, and date, as well as memory allocations services to the application programs running in each virtual machine; the MS-DOS SHARE.EXE module can optionally be loaded for networklike file locking and sharing support if multiple applications will be accessing the same files. Windows 2.0 provides the graphical user interface and pointer device support. The VDMM runs in the 80386's native 32-bit protected mode and oversees the multitasking and protected-memory aspects of the system's operation. The VDMM also takes advantage of the 80386's paging capability to supply expanded (Lotus/Intel/Microsoft EMS) memory to applications that require it. The EMS services furnished by Windows/386 are compatible with the recently announced EMS 4.0 specification and do not require loading of any special driver. Windows/386 is started on a system that has already been booted up under MS-DOS or PC-DOS in the usual manner. When Windows/386 gains control, it loads the Virtual DOS Machine Manager into extended memory (the memory above the 1Mb boundary), creates an initial virtual machine (referred to as VM1), maps the existing copy of MS-DOS into it using the 80386's page tables, and then loads Windows 2.0 on top of MS-DOS in the first virtual machine. Control is then passed to Windows 2.0, which reads its configuration file (WIN.INI) and presents the familiar MS-DOS Executive to the user. True Windows applications, such as Cardfile and Terminal, run together in the virtual machine containing Windows itself (VM1). Windows presents such programs with exactly the same applications program interface as in real mode, and multiple Windows applications running concurrently are scheduled for execution by the internal Windows multitasker──that is, nonpreemptively. Just as in real mode, the Windows kernel can use expanded memory pages to swap the code and data segments belonging to Windows applications, so that the number of Windows applications that can be loaded simultaneously is not limited by the 640Kb address space. When a standard application is started, the VDMM creates a new virtual machine and maps the image of MS-DOS, the ROM BIOS data area, and other vital structures into it using the 80386 page tables, and then loads the application into the virtual machine (see Figures 6 and 7). If the application does not have a PIF file, the size of the new virtual machine is under the control of the windowmemsize= entry in the WIN.INI file and defaults to 640Kb. If a PIF file exists for the program, the size of the new virtual machine depends on the Kb required and Kb desired fields in the PIF, and on the amount of physical memory actually available (there is no demand paging in Windows/386 as in OS/2). While initializing a new virtual machine, the VDMM also creates a corresponding instance of a special application called VMDOSAP in VM1 (multiple instances share the same code segment, so the overhead in the Windows VM for each additional standard application VM is minimal). VMDOSAP is analogous to the WINOLDAP module of real-mode Windows; when the standard application is running in a window instead of full-screen, VMDOSAP is responsible for any necessary translation and clipping of its output for the window shape, size, and graphics-display mode. It also translates input on behalf of the application when necessary (for example, when data is being pasted from another window). The operation of VMDOSAP is completely transparent to the application, and VMDOSAP does not occupy any memory space within the application's virtual machine. The final step in initializing the new virtual machine is to allocate a "shadow" video buffer for the application and select the application's initial display and multitasking status based on the contents of its PIF file. The shadow video buffer lies outside the VM's address space and holds a copy of the application's virtual screen; it is used to restore the display when the application is brought to the foreground and allows scrolling when the application is running in a window. If no PIF file exists for the program, it begins execution with a full-screen display and with its multitasking option set to "foreground"──that is, the application is suspended when it is in the background. The complete isolation of VMDOSAP from the application, and of standard applications from each other, is made possible by Windows/386's virtualization of the system's input and output devices, as well as by its control over each virtual machine's interrupt vector table and I/O port address space. When an application executes a software interrupt (to invoke an MS-DOS or ROM BIOS service), reads from or writes to an I/O port, or accesses a memory address that lies outside its virtual machine space, an exception is generated that is fielded by the VDMM. The VDMM disposes of the exception by examining the machine instruction that provoked it and the contents of the CPU registers. For example, requests for input and output can be funneled to VMDOSAP, while calls for file system services revert to the virtual machine's image of MS-DOS for processing. An application that accesses the hardware to select a video mode not supported by Windows/386 is not terminated, but is forced to run full-screen. Windows/386 Multitasking The Windows/386 multitasking scheduler uses the standard PC AT 18.2-Hz timer tick. Its allocation of CPU cycles across multiple virtual machines is determined by the multitasking setting on each application's control menu or from its PIF file. In the simplest case, where each active program has the "background" option selected, the currently selected application gets two-thirds of the CPU cycles, and the remaining one-third are divided among the other virtual machines. Applications that hook the ROM BIOS timer tick interrupt vector (Interrupt 1CH) receive a proportionate number of timer interrupts. There are also two extreme cases of multitasking worth mentioning. The first is when a standard application that runs under the exclusive option is selected. Such an application receives all the CPU cycles, and all other virtual machines are suspended. The other is encountered when no standard applications are running and only the Windows VM is active. In this case the internal Windows scheduler apportions the CPU cycles between various Windows applications, and the VDMM plays no multitasking role at all. Many Machines, One DOS One of the most interesting aspects of Windows/386 is its relationship to MS-DOS. The 80386's paging tables allow a single physical instance of MS- DOS to be mapped into each virtual machine's address space at the same apparent address. Windows/386 uses internal knowledge of MS-DOS and Windows to maintain the integrity of the file system and to prevent different applications from executing concurrently within MS-DOS's critical regions. It also maintains a copy of the Windows and MS-DOS control structures (such as the MS-DOS system file table and the ROM BIOS video driver data area) for each virtual machine, and swaps these in and out of the MS-DOS image to supply the proper context for the application that is running. These tables and structures are referred to as the MS-DOS kernel instance data. Since simply copying the kernel instance data back and forth on every timer tick would burn up a significant number of CPU cycles, and since applications may execute for prolonged periods (several context switches) without referencing MS-DOS or the ROM BIOS, the VDMM uses an interesting trick to reduce the multitasking overhead. Before a program is given control at the beginning of its time slice, the memory pages in its virtual machine that contain MS-DOS instance data, the interrupt vector table, and the ROM BIOS data area are marked "not present" in the CPU's page table. Only a few pages within the MS-DOS that contain instance data are marked "not present" and cause page faults. Each page is demanded independently of the others. This way many accesses to DOS cause no context switching overboard at all, and when an instance switch is required, its overhead is as small as possible. If the program attempts to access these pages (for example, by inspecting the current cursor location in the ROM BIOS data area or executing an Interrupt 21H), a page fault is generated that suspends the program and transfers control to the VDMM's interrupt handler. The VDMM handles the fault for that page by moving in the MS-DOS kernel instance data, marking the MS-DOS memory page as "present" in the page table to prevent further faults during the same time slice, and restarting the instruction that caused the fault. On the other hand, if the program runs to the end of its time slice without referencing instance memory, no harm has occurred, and the overhead of moving the data needlessly has been avoided. Communications The techniques used in Windows/386 to virtualize the video controller and asynchronous communications controller are also particularly interesting. Since direct hardware access to these devices by standard applications for performance reasons is common, Windows/386 must stay out of the way as much as possible. This ensures that the application's throughput will not not be impaired, and that other programs will not be disrupted while still providing for preemptive multitasking. As mentioned earlier, each virtual machine is allocated a "shadow" video buffer that is used to save a copy of its complete screen image when the application is running in a window or in the background. The buffer is located in extended memory, outside the virtual machine's 1Mb address space, and may range in size from 16Kb to 256Kb depending on the display mode selected by the program. When an application is running in the background or in a window, the VDMM uses the 80386's page tables to map the virtual machine's video refresh buffer addresses (segment A000H, B000H, or B800H) onto the shadow buffer. The application can modify what it perceives to be the video buffer at will (see Figure 8), but the physical screen is not directly affected. At intervals, the VDMM checks the "dirty bits" in the page table──which are set by the hardware when an address within the page is written to──to determine if the application has modified its video buffer. If a write is detected, the buffer is compared against an earlier copy. The changes are sent to VMDOSAP, which renders them into an appropriate pattern of pixels and displays them in the visible portion of the application's window. When an application is running full-screen, the virtual addresses of the video refresh buffer within its virtual machine are mapped onto the physical memory belonging to the video controller, and the shadow buffer is not used as an intermediary. Thus, the program's control of the video display is direct, just as if it were running in real mode and there is no speed degradation. When the user switches away from the full screen application, the VDMM simply copies the video controller's physical buffer to the shadow buffer (the 80386's double-word string move instruction, which transfers 32 bits at a time, is used to advantage here) before restoring the screen image of the Windows/386 desktop or the next full- screen application to be selected. Applications that run in certain EGA graphics modes receive special treatment. The virtual machine's video buffer addresses are mapped onto the second 128Kb of the EGA controller's physical refresh buffer. This lets the application use all of the EGA features without the overhead of emulation, conserves system memory, and allows a rapid switch to full- screen operation. (Instead of copying the shadow buffer to the physical buffer, the VDMM selects the second graphics page as the active display.) In contrast to the video controller, the asynchronous communications controller, serial port, is fully virtualized at all times. The VDMM always handles all communications interrupts and input or output operations, and maintains an internal queue of serial port data and status information. Even the initialization of the serial port (baud rate, word length, parity, and number of stop bits) is virtualized, and this information is maintained separately for each virtual machine. An application signals its intention to use the communications controller by attempting to read or write one of the controller's data ports, change its interrupt mask bit on the 8259 Programmable Interrupt Controller, or capture its interrupt vector. Any of these operations will generate an exception that is processed by the VDMM. If the serial port is not in use by another program, the VDMM assigns "ownership" of the virtualized controller to the program that caused the exception; otherwise, a dialog box is displayed and the user is allowed to decide which program will retain access to the device. Once a program establishes ownership of the communications controller, it can perform input and output operations, and service communications interrupts in a normal manner from its point of view. In actuality, the interrupts and I/O operations are simulated by the VDMM, which transfers data between the virtual machine and its internal queue. In this fashion, the operation of the physical device, which is asynchronous and can generate an interrupt at any time, is decoupled from the virtual 86 machine──which can only execute during its time slice. Portents for the Future In summary, Windows/386 exploits the 80386's unique capabilities to furnish preemptive multitasking, a windowing user interface, data exchange between any two applications, and efficient use of large amounts of RAM in conjunction with existing MS-DOS and Windows applications. In doing so, it overcomes the two most common objections to real-mode Windows: the latter's inability to coexist with popular ill-behaved programs such as Lotus(R) 1-2-3(R) or Microsoft Word, and its hunger for memory. In addition, although Microsoft bills Windows/386 as an interim product, it has some interesting implications──both short-term and long-term. In the near future, until protected-mode applications appear for OS/2 that fully exploit its virtual memory management, multitasking, and interprocess communications facilities, Windows/386 offers more to 80386- based PC owners than does OS/2. Windows/386 allows multiple MS-DOS applications to run concurrently, while OS/2 supports only one real-mode application at a time. In addition, the Windows/386 memory overhead in each virtual machine is typically 80 to100Kb less than the OS/2 overhead in the DOS 3.x Box. Finally, since Windows/386 uses the "real" MS-DOS as a substrate──unlike OS/2, which in effect emulates MS-DOS──Windows/386 is compatible with a broader range of existing applications than is OS/2. Taking the longer view, the availability of a true 32-bit version of OS/2 and the Presentation Manager for the 80386 will probably threaten Windows/386's market niche. However, Windows/386 has an important role to play here too. It serves as a technology testbed for memory management, multitasking, device virtualization, and MS-DOS virtual-machine techniques that can eventually be absorbed into its successor──a system that will run multiple MS-DOS applications, 16-bit 80286 protected mode applications, and 32-bit 80386 protected-mode applications simultaneously. ─────────────────────────────────────────────────────────────────────────── A Comparison of Current and Future Windows Versions ─────────────────────────────────────────────────────────────────────────── ╓┌──────────────────────┌──────────────┌──────────────┌───────────┌──────────► Microsoft Microsoft MS OS.2 Windows Windows Windows Presentation 1.03 & 1.04 2.0 386 Manager Presentation Spaces Tiled Overlapped Overlapped Overlapped More Consistent User and Keyboard Interfaces ── Yes Yes Yes Microsoft Microsoft MS OS.2 Windows Windows Windows Presentation 1.03 & 1.04 2.0 386 Manager Processor 8088 8088 ── ── Environments 8086 8086 ── ── 286 286 ── 286 386 386 386 386 Large Memory Support ── EMS/EEMS EMS/EEMS 16Mb Multitasking Nonpreemptive Nonpreemptive Fully Fully Preemptive Preemptive Enhanced Display Performance ── Yes Yes Yes Runs Exisiting Windows (1.03) Applications Yes Yes Yes No Microsoft Microsoft MS OS.2 Windows Windows Windows Presentation 1.03 & 1.04 2.0 386 Manager Applications Yes Yes Yes No Graphics API GDI GDI GDI GPI Multiple Document Interface ── Yes Yes Yes Device Drivers ── Enhanced Enhanced New Model Old Application Support ── Improved Improved Improved Integral Part of OS ── ── ── Yes Protected Mode Applications Execution ── ── Yes Yes Microsoft Microsoft MS OS.2 Windows Windows Windows Presentation 1.03 & 1.04 2.0 386 Manager New Development API ── ── ── Yes New User "Shell" and Keyboard Interface ── ── ── Yes Virtual 86 Mode ── ── Yes ── Figure 6: This diagram shows the relationship between VDMM, MS-DOS, Windows, the VMDOSAP module, and standard applications. ┌───────────────────────────┐ ╔═════════════╗ ╔═════════════╗ │The Windows/386 Environment│ ║ MS-DOS 3 ║ ║ MS-DOS 3 ║ └───────────────────────────┘ ║ Application ║ ║ Application ║ ╚═════════════╝ ╚═════════════╝ ╔═════════════╗ ╔═════════════╗ ▼ ▼ ║ WIN ║ ║ WIN ║ ╔═════════════╗ ╔═════════════╗ ║ Application ║ ║ Application ║ ║ VMDSAP DOS ║ ║ VMDOSAP DOS ║ ║ ║ ║ ║ ║ Interface ║ ║ Interface ║ ╚═════╤═══════╝ ╚═══════╤═════╝ ╚══╤════════╤═╝ ╚╤╤═══════════╝ │ │ │ ┌────│────┘│ ▼ ▼ ▼ ▼ ▼ ▼ ┌──────────────────────────────────────────────────┬──────────┐ │ │ │ │ Windows │ │ │ │ │ ├──────────────────────────────────────────────────┤ │ │ │ │ │ DOS 3.x │ │ │ │ │ ├──────────────────────────────────────────────────┘ │ │ │ │ Virtual DOS Machine Monitor (VDMM) │ │ │ └─────────────────────────────────────────────────────────────┘ Figure 7: Memory occupied by the DOS and other fixed items, such as the ROM BIOS, is shared between all virtual machines. Thus the system consumes physical memory sufficient for only one copy of each of these items and shares this memory between all the virtual machines using the memory management capabilities of the 386. Components Within the Address Space of a VDM ╔═══════════════════════╗ FFFFFH ║ ROM ║ ║ ║ ╠═══════════════════════╣ ║ EGA, etc. ║ ╠═══════════════════════╣ 9FFFFH ║ ║ ║ Application ║ ║ Area ║ ║ ║ ╠═══════════════════════╣ ║ ║ ║ DOS ║ ║ ║ ╠═══════════════════════╣ ║ Int Vect ║ ║ BIOS Data ║ 00000H ╚═══════════════════════╝ Figure 8: This diagram shows how a display adapter is virtualized. ┌────────────────────┐ │Virtual Video Buffer│ ╔═════════════════╗ └────────────────────┘ ║ ║ ║ ROM BIOS ║ ╔════════════════╗ ║ ║ ╔═══════════════╗ ║ VDMM ║ ╚═════════════════╩═══►║ ╟──────────────┐ ║ & Virtual ║ ║ ║ Gets Changes╞═►║ Display Driver ║ ┌─────────────────────┐║ Virtual Video ╟──────────────┘ ╚═══════╦════════╝ │Writes into Video RAM│║ RAM ║ ║ └─────────────────────┘║ ║ ║ ╔═════════════════╦═══►║ ║ ┌───────╨────────┐ ║ ║ ╚═══════════════╝ │Sends Changes to│ ║ ║ │ Windows/VMDOSAP│ ║ DOS ║ └───────╥────────┘ ║ Application ║ ║ ║ ║ ╔══════════════════╗ ║ ║ ║ ║ ░░░░░░░░░░░░░ ≡≡║ ╔═══════▼════════╗ ║ ║ ║ ░░░░░░┌──────────╨────────────────╢ ║ ╚═════════════════╝ ║ ░░ ◄══╡Renders Changes into Window║ VMDOSAP ║ ║ ░░░░░░└──────────╥────────────────╢ ║ ║ ░░░░░░░░░░░░░ ║ ╚════════════════╝ ╚══════════════════╝ ████████████████████████████████████████████████████████████████████████████ Programming in C the Fast and Easy Way with Microsoft QuickC Augie Hansen☼ The widespread use and popularity of the C language, which began in the early 1970s with researchers at Bell Laboratories, is likely to endure. C has matured in the marketplace and is becoming standardized, just as traditional languages like FORTRAN and COBOL did. The original purpose of C was to provide a malleable high-level assembler for reprogramming the UNIX(R) operating system for portability. Programmers have found the language and its support tools to be an inviting and highly compliant medium for system-level work and applications programming. The typical environment for C programmers, whether working on large multiuser systems or alone on PCs, has been to use a set of separate programs for each step in the process of translating source code into executable programs. The sequence is edit, compile (and sometimes assemble), link-edit object files and library modules, and then test. Depending on the results, the sequence might need to be repeated a number of times. Development can be tedious, even with a reasonable set of support tools, and is usually time-consuming. Making C programming faster and easier than is possible with traditional methods has become an important objective for vendors of C programming products. In the past few years, we have seen the introduction of C interpreters as front-end training, testing, and development systems. Interpreters typically give quick feedback to the user by avoiding the edit-compile- link-test cycle. However, such systems usually lack the flexibility for handling large programming projects. When an interpreter is used in program development, the source code produced is subsequently used as input to more traditional C compiler systems, which offer speed and size optimizations, the ability to handle multiple modules, provisions for custom libraries, and other convenient features. C compiler environments, the next stage in the evolution of C programming systems, are now the rage, and for a good reason: they provide the desired quickness on both ends of the development process. Creating a program is easier and faster than ever, and the final program runs at warp one. Introducing QuickC QuickC is an integrated C programming environment that provides all of the features you would expect in a mature program-development system while offering you the benefit of quick feedback and appropriate hand-holding when needed. The QuickC compiler can be obtained as a separate product or packaged with the Microsoft C Compiler Version 5.00. QC.EXE is the QuickC integrated environment program. It is great for learning C, useful in prototyping, and completely compatible with Microsoft C 5.00. The integrated environment consists of a tightly coupled full-screen editor, compiler, linker, and debugger. QuickC also has a built-in "make" feature that automates most of the process of compiling and linking programs. QCL.EXE, which is the QuickC equivalent of the C 5.00 CL control program, controls standalone operation. You can use the QCL program together with MAKE and the special version of the linker, LINK, to compile and link programs in the traditional manner. The use of separate programs is slower than working with the integrated environment because the number of disk accesses is significantly greater, but the approach lends itself well to automation via batch files and make files. Using a separate MAKE program gives you more latitude in preparing a program. It also provides greater access to other programs and features of the operating system than the built-in program-building feature of the integrated environment. QuickC is compatible with Microsoft C 5.00 in several ways. The compilers accept the same source code, use the same object file formats, and link with compatible libraries. Four memory models are supported: small, compact, medium, and large. Unless in-structed otherwise, the medium model is the default model used by QuickC. QuickC has no huge keyword to objects that exceed 64Kb in size, nor does it support the huge memory model. The near and far addressing keywords, however, are sup-ported. They are not part of the formal definition of C, but they are useful when you are pro-gramming the segmented archi-tecture of the Intel(R) 8086/80286 microprocessor family. The few differences between QuickC and C 5.00 will affect those programmers who have exacting requirements for optimization of execution speed, for program size, or who need greater addressing flexibility than QuickC offers. Using QuickC for fast prototyping and then moving to the Microsoft Optimizing C Compiler to fine-tune program performance creates a comfortable working environment. The optimizing compiler has features for improving code size and execution speed. Integrated Environment The QuickC integrated environment, contained in QC.EXE, is shown schematically in Figure 1. The integrated environment is a collection of essential components that work together to increase the speed of creating and testing C programs. The built-in full-screen editor accepts input from both a keyboard and a mouse. It is a complete WordStar(R) compatible programmer's editor that accepts WordStar commands (a Ctrl-key combination) and IBM-compatible keyboard commands such as those found in Microsoft Word and many other visual text editors. Also, it is integrated with the compiler so that you can find errors and fix them quickly. Compiling at 7000 lines per minute, the QuickC in-memory compiler can capture information about errors (up to 26 of them) in a single compilation and feed the information back to the editor. For each error found, the editor places the cursor on the location in the source file where the error occurred. The compiler can produce a memory-based program or a disk-based executable; the choice is yours. Usually you will run entirely in memory to get the greatest speed advantage during development and then save the program to disk for later use as a standalone program. To help you find program bugs, QuickC incorporates a source-level debugger that is a comprehensive subset of the well-known CodeView(R) debugger. The debugger lets you single-step program execution, establish watch expressions and display their values, set multiple breakpoints, and search for functions by name. In addition, you can switch between displays of the program and its output by using a simple keypress command. Multimodule programs necessitate a significant amount of bookkeeping work. Such work is best left to computer programs, and QuickC obliges. A built- in program maintainer patterned after MAKE automatically prepares a make file from a program file list that you provide. As you modify your program, QuickC will keep each object module up to date with respect to its source file and controls linking if the executable file is to be preserved as an EXE file on disk. Core Library QuickC has a set of run-time library routines built right into the compiler. If a program uses only core routines, all external references can be resolved in memory, resulting in very high-speed program preparation. Figure 2 lists the core routines. If the program needs any routines that are not in the core set, they must be read from a library file on disk. QuickC uses the medium-memory model by default, so it normally looks for the library file MLIBCE.LIB or MLIBC7.LIB in the default library directory. The library to be chosen depends on whether floating-point emulation or 8087/80287 routines are used. Alternatively, QuickC looks for the component medium-model files if you choose not to create a combined library during installation. The component libraries are MLIBC.LIB, MLIBFP.LIB, EM.LIB or 87.LIB, and LIBH.LIB. Because searching a large file like MLIBCE.LIB takes a lot of time, you can speed up the process by creating a small library that contains only the routines your program needs. QuickC searches for a file named QUICK.LIB, or one you name on the QC invocation command line, if it cannot resolve external references by using the core routines. The QuickC system interacts with you via a menu bar and pull-down menus that you can control from the keyboard and a mouse, if one is installed. All frequently used selections can also be made by a shortcut method, such as pressing a function key, a Ctrl-key or Alt-key combination, or some other special key. Some of the available menu windows can be seen in Figures 3, 4, 5, and 6☼. Figure 3, which shows the editing window, adorned with scroll bars, and the File menu, indicates how the QuickC drop-down menus appear. Once a menu is selected, either from the keyboard by typing the first letter of its name while holding down the Alt key or by clicking a mouse button on the menu name, you can use either the arrow keys plus Enter or a mouse click to select an option. The figure shows the highlight on the "Set Program List" option. When you select it, this option leads to a dialog box (signified by "...") on which you select the names of files that comprise the program you want to compile. The dialog box lists all source files in the current or specified directory. You must indicate which of them comprise the sources of your program by using a "point and shoot" selection process. The selected files make up the program list that QuickC uses to build the program. In-Memory Compilation The advantage of in-memory compilation is that all source files, temporary files, and most library routines are held in high-speed, primary memory. This dramatically cuts the time it takes to produce an executable program compared with the traditional approach, which is usually very disk- intensive. The integrated environment facilitates debugging with close editor- debugger interaction. By placing the cursor on an error in your source code, you are spared the time and effort it takes to reload the editor, read in the source file, and move the cursor to the location of the error. In the integrated environment, errors are described in sufficient detail so that reference to manuals is usually unnecessary. For example, Figure 4☼ shows an error message in the pop-up window at the bottom of the screen. The message is detailed enough to be helpful, and the cursor points to the line containing the error. Raw compilation speed on a given machine is about three-to-one in favor of QuickC compared with a standard C compiler. The programmer perceives the improvement in speed to be even greater because there is no need for continually loading and exiting a series of programs and suffering the concomitant disk-access times. Library Support The standard run-time library provided with QuickC includes all of the routines that accompany the Microsoft Optimizing C Compiler. The library, compatible with XENIX(R), provides extensive DOS support (bdos and intdos, for example), and includes routines that conform to the draft-proposed ANSI standard for C. A new graphics library is included in the QuickC package. It contains more than forty graphics routines that handle configuration, palette and attribute settings, outputting text and images, and a variety of other color and monochrome graphics tasks. A new set of DOS and BIOS interface routines add considerably to the support for PC hardware interactions. While not portable to other hardware environments, these routines allow you to get the most from a PC and its peripheral devices. Standalone Operation For programmers who prefer the traditional approach, QuickC provides separate programs for compiling and linking. QCL.EXE reads a list of optional control switches and filenames from the DOS command line and produces standard Microsoft format object files that you can put into libraries (use LIB.EXE) or link with other objects and libraries in order to produce executable programs. MAKE can control the entire process; it reads a list of instructions from a make file, and uses the time stamps on files to determine which files need to be recompiled and linked. MAKE is more flexible than the program maintenance feature built into the QuickC integrated environment. The library maintainer, LIB.EXE, permits you to add modules to a library, delete modules, copy modules to disk files, and combine libraries. It allows you to create your own custom libraries and modify existing libraries to meet special needs. For instance, to create a custom QUICK.LIB file of routines for a given program, you can use LIB to extract copies of the needed routines and then use LIB again to prepare the QUICK.LIB file. Sample Session Figures 7 and 8 show the two source files from which the TOUCH program, a useful adjunct to the MAKE program provided with the compiler, is built. TOUCH updates the date and time stamps associated with a file to the current date and time. This gives you a convenient way to force all or selected files for a program to be remade, even if the files were not modified since the last successful compile. TOUCH accepts command-line options -c and -v. TOUCH normally creates a named file if the file does not exist. The -c option tells TOUCH not to create any files. The -v option tells TOUCH to be verbose, giving the user a play-by-play description of what it is doing. This option caters to those programmers who like chatty programs. Figure 9 lists the make file and linker response files produced by the automatic program maintainer feature of QuickC. The make file is compatible with and can be used by the standalone MAKE command. The linker response file provides information needed by the LINK program when it combines object modules and libraries to produce an executable program. The source code is presented in its final form. However, to demonstrate the debugging capabilities of QuickC, we will introduce several deliberate errors into TOUCH. Program errors fall into two general categories: syntactic and semantic. Syntax errors result from how a program is put together. The rules for writing programs in C, as is the case for nearly all computer languages, are precise and rigid. Compilers have to be unforgiving in insisting on correct syntax. A missing semicolon, which effectively concatenates two perfectly good statements into one illegal one, is one of the more common syntax errors. Semantic errors result from the contextual meaning of a program's statements. Does the program run to completion and produce correct results? If not, it probably contains semantic errors, which can be further categorized into execution errors or logic errors. Execution errors are requests written in a perfectly acceptable way that are impossible to fulfill, for example, dividing by zero. A program containing such an error aborts with a run-time error. Logic errors also get by the compiler because the syntax is correct, and the program containing them runs fine, but it produces incorrect results. Such a program is not likely to become a best-seller. QuickC contains the tools to detect and correct both syntax errors and semantic errors. Syntax errors are caught during compilation and reported one at a time in a pop-up window. Up to 26 compile-time errors are maintained in a list that can be scanned in either direction. The "Warning Level" setting in the compile dialog box controls the level of checking and reporting to be done by QuickC (see Figure 6☼). The higher the warning level (they range from 0 to 4), the more detailed the checking and reporting. The resulting list of warnings and error messages will guide you in wringing out all compile-time errors. Figure 4☼ shows an error caught by the compiler. It is the common error of using assignment where the programmer really intended a test for equality. The statement if (fclose(fp) = EOF) { ... } should use "==" instead of "=". The error is detected because a function is not an lvalue, so nothing can be assigned into it. If the left-hand side of the assignment were an 1value, the compiler would be happy and compile without comment. Your program would execute, but probably incorrectly. Another common error is a missing parameter in an fprintf statement. Try leaving the stderr parameter out of an error message line. The compiler will detect the problem because it uses function prototypes to determine how many parameters a function takes and what data types the parameters have. In this case, QuickC will report incorrect parameter types. Because fprintf takes a variable number of arguments, the compiler cannot determine that the number of parameters is incorrect. You could still have semantic errors lurking about in your program, which the compiler cannot detect. Traditionally, C programmers have resorted to inserting printf statements in their programs to check values at critical locations during test runs. This is tedious, time-consuming, and unnecessary because of the built-in debugger provided in the QuickC integrated environment. To use the debugger, which contains an extensive subset of CodeView features, you must first compile your program with the Debug option set so that symbolic information needed by the debugger is preserved in the executable program (see Figure 6☼). Select the Run item (Alt-R) from the menu bar and then the Compile(C) dialog box. Use the Tab key to select the Debug option and the Spacebar to toggle it on. Press Enter to compile the program. After successful compilation, select the Debug item (Alt-D) from the menu bar and set watch expressions and breakpoints. A watch expression can be as simple as a variable name or as complex an expression as you need to check program operation. You can set watch expressions one at a time or several at once by separating them from each other with a semicolon. Watch expressions are displayed in a separate watch window at the top of the screen during program execution (see Figure 5☼). A breakpoint is a line in the program at which execution should stop to let you observe the values of watch expressions or just to observe the displayed output using the screen-switching feature of QuickC. It is simple, effective, and a lot easier and faster than placing printfs all over your program. You can set multiple watch expressions and breakpoints. If the ones you set initially don't do the job, it is a simple matter to clear them and set new expressions and breakpoints. There is no need to recompile as there would be with embedded printf statements. Introducing deliberate errors into TOUCH.C will help you to learn how to use the debugger. Try changing the sense of some logical tests (use "!=" instead of "==") and observe the effect on the output. Deleting the two lines in TOUCH.C just before the first while loop produces an interesting result. The program will not skip the program name argument (argv[0]), and all following arguments will be treated as filenames even if they are preceded by a dash, which is the specified option flag. To fix the problem, you could set watch expressions on the command-line argument parameters, argc and *argv, by typing "argc; *argv" in the watch dialog box and put breakpoints on statements where these values should have just changed, such as the first statement inside each loop. You can provide a command line by selecting the Set Runtime Options (O) from the Run (Alt-r) menu before you run the program. Type only the command-line arguments; do not type the program name──it is already known to QuickC. Use something like -c one two three as a command line. After running the program, you can use the DOS Shell (D) selection of the File menu (Alt-F) to get to a temporary DOS command line. Run a DIR command and check the list of files. Don't be surprised to find that TOUCH created files named "-c", "one", "two", and "three", even though we instructed it not to create any files. The thorough syntax-checking and integrated debugging features provided by QuickC are as important to a programmer as the swiftness of the compiler. Become familiar with these easy-to-use tools and you will never want to be without them. Summary For easy learning and use of a C compiler, raw compilation speed, and overall flexibility, it's hard to beat QuickC. Compiling in the integrated environment makes trying out an idea exhilarating. Modifying the program until it does exactly what you want is as simple as playing "what if" with a spreadsheet. It's about time programmers received the same kind of help that financial analysts have had for years. When we took the big step from batch processing to interactive processing on mainframes back in the 1960s, programmer productivity received a great boost. The move to swift and capable integrated programming environments on personal computers provides the opportunity to make similar, perhaps even greater, productivity gains in the 1980s. Figure 1: You can use separate libraries insteaad of the combined library. The default memory model is medium. You can also select small, compact, or large. ┌──────────────────────────────────────────────────┐ │ The QuickC Integrated Environment and Libraries. │ └──────────────────────────────────────────────────┘ ╔═══════════════════════════╗ ╔════════════════════════════╗ ║ QC.EXE ║█ ║ MLIBCE.LIB* ║█ ╠═══════════════════════════╣█ ╠════════════════════════════╣█ ║ Full-screen Editor ║█ ║Combined Library File Merges║█ ╟───────────────────────────╢█ ║ the following libraries: ║█ ║ Compiler ║█ ║ ║█ ╟───────────────────────────╢█ ║ • MLIBC.LIB ║█ ║ Source-level Debugger ║█ ║ • MLIBFP.LIB ║█ ╟───────────────────────────╢█ ║ • LIBH.LIB ║█ ║ Program List Maintainer ║█ ║ • EM.LIB (or 87.LIB) ║█ ╟───────────────────────────╢█ ╚════════════════════════════╝█ ║ ║█ █████████████████████████████ ║ ║█ ╔════════════════════════════╗ ║ "Core Subroutine Library" ║█ ║ QUICK.LIB ║█ ║ ║█ ╠════════════════════════════╣█ ║ ║█ ║ Quick Library ║█ ╚═══════════════════════════╝█ ╚════════════════════════════╝█ ████████████████████████████ █████████████████████████████ Figure 2: The core library routines are defined in QC.EXE and speed up linking time by avoiding unnecessary disk accesses. Core Library Routines abort _fmalloc isatty puts strlen access _fmsize itoa read strlwr atof fopen kbhit realloc strncat atoi fprintf longjump remove strncmp atol fputc lseek rewind strncpy bdos fputs ltoa rmdir strnset brk fread malloc rmtmp strpbrk calloc free _memavl sbrk strrchr chdir _freect memccpy scanf strrev chmod fscanf memchr segread strset clearerr fseek memcmp setbuf strspn close fstat memcpy setjmp strtok creat ftell _memmax setmode strupr dosexterr fwrite _memset setvbuf system eof agetch mkdir signal tell _exit getche movedata spawnl time exit getcwd msize spawnv tmpfile _expand _getdate _nfree sprintf tmpnam fclose _gettime _nheapchk sscanf tolower fflush getenv _nheapset strcat toupper _ffree gets _nheapwalk strchr tzset _fheapchk halloc _nmalloc strcmp ultoa _fheapset hfree _nmsize strcmpi unlink _fheapwalk int86 onexit strcpy vfprintf fgetc int86x open strcspn vprintf fgets intdos printf strdup vsprintf filelength intdosx putch stricmp write flushall Figure 7: TOUCH.C /* * PROGRAM: TOUCH * * DESCRIPTION: Update the last modification time of a file or a * group of files to the current time. * * ENTRY: A list of files to "touch". The filenames can be preceded * by one or both of the following options: * * -c don't create any files * -v operate in verbose mode (report activity) * * SYNTAX: * touch [-cv] filename ... */ #include #include #include #include #include #include #include #include #include #define ERR 0x7FFF #define MAXNAME 8 typedef enum { FALSE, TRUE } BOOLEAN; extern void usage(char *); int main(argc, argv) int argc; char *argv[]; { int ch; /* character buffer */ char *cp; /* character pointer */ int badcount; /* number of files that cannot */ /* be successfully updated */ struct stat statbuf; /* buffer for stat results */ BOOLEAN errflag; /* error flag */ BOOLEAN cflag; /* creation control flag */ BOOLEAN vflag; /* verbose control flag */ FILE *fp; /* file pointer */ static char pgm[MAXNAME + 1] = { "touch" }; /* * Initialize flags and variables */ errflag = cflag = vflag = FALSE; badcount = 0; /* * Move past the command name argument and look for * optional arguments (signaled by a leading dash) */ ++argv; --argc; while (argc > 1) { cp = *argv; if (*cp != '-') /* not an option flag */ break; /* * Having seen an option flag ('-'), look at * any option letters that follow it in the * current argument string */ for (++cp; *cp != '\0'; ++cp) switch (*cp) { case 'c': /* don't create files */ cflag = TRUE; puts("CREATION flag set"); break; case 'v': /* verbose -- report activity */ vflag = TRUE; break; default: fprintf(stderr, "%s: unknown option %c\n",pgm, *cp); usage(pgm); exit(ERR); } ++argv; --argc; } /* * Update modification times of files */ for (; argc-- > 0; ++argv) { if (stat(*argv, &statbuf) == -1) { /* file doesn't exist */ if (cflag == TRUE) { /* don't create it */ ++badcount; continue; } else if ((fp = fopen(*argv, "w")) == NULL) { fprintf(stderr, "%s: Cannot create %s\n", pgm, *argv); ++badcount; continue; } else { if (fclose(fp) == EOF) { perror("Error closing file"); exit(ERR); } if (stat(*argv, &statbuf) == -1) { fprintf(stderr, "%s: Can't stat %s\n", pgm, *argv); ++badcount; continue; } } } if (utime(*argv, NULL) == -1) { ++badcount; perror("Error updating date/time stamp"); continue; } if (vflag == TRUE) fprintf(stderr, "Touched file %s\n", *argv); } return (badcount); } Figure 8: USAGE.C /* * usage() * * DESCRIPTION: Display an informative usage message using the * actual program name, which may have been changed by the user. * * ENTRY: A pointer to the program name. */ #include void usage(pname) char *pname; { fprintf(stderr, "Usage: %s [-c] [-v] file ...\n", pname); fprintf(stderr, "\t-c Do not create any files\n"); fprintf(stderr, "\t-v Verbose mode -- report activities\n"); } Figure 9: Make File # # Program: Touch # .c.obj: qcl -c -Iusage.c -W0 -Ze -Zid -AM $*.c usage.obj : usage.c touch.obj : touch.c Touch.exe : usage.obj touch.obj del Touch.lnk echo usage.obj+ >>Touch.lnk echo touch.obj >>Touch.lnk echo Touch.exe >>Touch.lnk echo Touch.map >>Touch.lnk link @Touch.lnk; Link Information - Touch.lnk ============================ touch.obj+ usage.obj Touch.exe Touch.map ████████████████████████████████████████████████████████████████████████████ Character-Oriented Display Services Using OS/2's VIO Subsystem ─────────────────────────────────────────────────────────────────────────── Also see the related article: A Survey of OS/2 Display Services ─────────────────────────────────────────────────────────────────────────── Ray Duncan☼ OS/2 provides the programmer with a fast and powerful set of video I/O functions. This particular group of API calls is important because the average user bases his assessment of the quality of application software largely on the ease and speed of his interaction with the program. He has neither the opportunity nor the experience necessary to peek into the source code and appreciate the elegance of its structure or the appropriateness of its algorithms, but the rapidity with which the application builds and updates its screen displays speaks for itself. Naturally, control of the video display has become one of the major areas of concern for the PC programmer. Unfortunately, the built-in video drivers available in MS-DOS(R) have done little to aid the programmer in his quest for competitive performance. The speed with which MS-DOS transfers text to the screen might be adequate for EDLIN, but it certainly doesn't suffice for sophisticated word processors, while the degree of control MS-DOS provides over character colors and attributes, display mode, and cursor shape and position is rudimentary at best. Consequently, the authors of most popular MS-DOS applications have felt justified in abandoning portability, bypassing the operating system altogether, and directly manipulating the video controller to achieve the desired throughput. In the real mode, single-tasking world of MS-DOS, such hardware-dependent display techniques rarely cause any significant problems, but the same cannot be said for the multitasking, protected-mode OS/2 environment. A program that tries to commandeer the video controller will either be terminated with a protection fault, or, if it manages to circumvent the memory-protection mechanisms, wreak havoc among the other processes that are active in the system. The designers of MS(R) OS/2 correctly perceived that they could head off such anarchy only by removing the motivation for programmers to go around the operating system and placed a high priority on the creation of a battery of video services rich enough and efficient enough to satisfy the needs of any reasonable application program. And that is just what they did. MS OS/2 provides application programs with a huge assortment of display functions that cover the full range of hardware independence, from the sophisticated graphical and rich text capabilities of the Microsoft(R) OS/2 Presentation Manager to the ability of an application to temporarily lock onto the logical or physical display buffer and modify its contents. (See "A Survey of OS/2 Display Services.") This article will focus specifically on the VIO subsystem, which occupies the middle ground (both in complexity and power) of OS/2's video cap- abilities. The other techniques for driving the display under OS/2 will be discussed in future articles. The VIO Subsystem OS/2's VIO subsystem provides character-oriented display services suitable for program editors, simple pop-up utilities, compilers and interpreters, and the like. The VIO function calls can be regarded as a superset of those available from MS-DOS and the IBM(R) PC ROM BIOS in real mode, and their actions can be grouped into the following general categories: ■ transferring strings of text to the screen at any position ■ controlling such text attributes as blink, underline, and color ■ controlling the cursor shape and position ■ scrolling and clearing the screen ■ setting the display mode ■ support for partial or full-screen pop-up windows The VIO subsystem is implemented as a dynamic link library (VIOCALLS.DLL) that resides on the system disk. Programs are bound to the VIO routines when they are loaded for execution, rather than at link time. Thus, the VIO library can be replaced at any time without recompiling or even relinking the client applications. Although the VIO subsystem does not support graphics operations aside from providing a means to get in and out of graphics display modes, programs that confine themselves to the use of VIO calls and do not access the logical or physical screen buffer will run properly in a window under the Presentation Manager without any changes. How is this possible? Under the Presentation Manager, the normal VIO dynalink library is replaced by a new "Windows-aware" library that maps each character and its attributes into an appropriate pattern of pixels and clips the output appropriately for the application's current window size. Figure 1 shows a summary of the VIO function calls. We'll take a close look at some of the most commonly used VIO services, together with sample C and MASM source code. Note that the C code shown assumes use of the large model, and the parameter called a "VIO handle" in the function descriptions is always zero in the Software Development Kit version of MS OS/2. Displaying Text There are a number of different VIO functions that can be used to place text on the screen. The simplest is VIOWRTTTY, which is called with the address and length of a string and a VIO handle and returns a status code of zero if the write succeeded or an error code if it failed. The control characters line feed, carriage return, backspace, and bell code are properly handled, automatic line wrap and screen scrolling are provided (unless they are intentionally disabled), and the cursor position is updated to lie at the end of the newly displayed text. Figure 2 is an example of the use of VIOWRTTTY. The VIOWRTTTY call is functionally similar to a call to DOSWRITE with the stdout handle (the preassigned handle for the standard output device), but is much faster and immune to redirection. As with DOSWRITE, and unlike the other VIO calls, ANSI escape sequences can be included in the text stream to control cursor position and character attributes. Ordinarily, the only error code returned by VIOWRTTTY is an invalid VIO handle. Providing the function with an invalid string address or length does not result in an error code but may well cause a General Protection Fault (Trap D), terminating the process. Three additional VIO calls are available for displaying text: VIOWRTCHARSTR (write character string), VIOWRTCHARSTRATT (write character string with attribute), and VIOWRTCELLSTR (write string of character/attribute pairs). These services are faster than VIOWRTTTY and offer direct control over screen placement. They are not sensitive to the presence of such control characters as carriage return or line feed; any control characters embedded in the string are simply displayed as their graphics character equivalents. These functions do not affect the current cursor position and only support limited line wrapping: if a string is too long for the current line, it will wrap onto the next line. However, if the end of the screen is reached, any remaining characters are discarded, the screen is not scrolled, and an error code is not returned. VIOWRTCHARSTR is the simplest of the three functions listed above. It accepts the address and length of a string, the screen position at which to begin writing the string, and a VIO handle. The new text assumes the attributes of the characters that were previously displayed at the same screen positions. VIOWRTCHARSTRATT is similar to the VIOWRTCHARSTR function, except for one additional parameter: the address of an attribute byte that is applied to every character in the string (see Figure 3). On monochrome adapters, the attribute byte specifies normal or reverse video, blink, underline, and intensity (see Figure 4). On color adapters in text modes, the attribute byte contains the background color in the upper four bits and the foreground color in the lower four bits (see Figure 5). VIOWRTCELLSTR displays a string that consists of alternating character and attribute bytes.VIOWRTCELLSTR is designed to be used in combination with VIOREADCELLSTR to restore an area of the display that was previously saved into a buffer. A programmer would not ordinarily select this function for normal text output, because generation of initialized strings with embedded attribute bytes is awkward in most languages, such strings are relatively bulky, and it is rare that an application needs to display a string where the attribute byte of each successive character is different. The VIO functions VIOWRTNCHAR (write string of identical characters), VIOWRTNATTR (write string of identical attribute bytes), and VIOWRTNCELL (write string of identical characters and attributes) offer some special capabilities that supplement the previously discussed text display functions and require similar parameters. VIOWRTNATTR replicates an attribute byte across a selected area without changing the text at that position and can rapidly alter the appearance of a display field. VIOWRTNCHAR and VIOWRTNCELL replicate a character or a character/attribute pair respectively and allow extremely efficient drawing of borders and similar screen objects. Scrolling and Clearing The four kernel services VIOSCROLLUP, VIOSCROLLDN, VIOSCROLLLF, and VIOSCROLLRT can clear a window of arbitrary size and position or scroll it up, down, left, or right any number of columns or rows. Any desired character and attribute byte can be specified to fill the newly blanked or scrolled lines. All four functions have the following parameters: the screen coordinates of the upper-left corner and lower-right corner of a window, the number of lines to be scrolled or blanked, the address of a character and attribute pair to be used to fill the blanked lines, and a VIO handle. Clearing the entire display in any mode, without first determining the screen dimensions, can be accomplished as a special case call of any of the four scroll functions, using an upper-left coordinate of (0,0), a lower-right coordinate of (-1,-1), and the value -1 as the number of lines to scroll. Figure 6 demonstrates clearing the screen to ASCII blanks with a normal video attribute. Scrolling a selected portion of the screen is also easy, as shown in Figure 7. Cursor Control The function VIOSETCURPOS positions the cursor, while the parallel function VIOGETCURPOS allows a program to obtain the current cursor location. Both VIOSETCURPOS and VIOGETCURPOS use text coordinates and assume a home position of (0,0) at the upper-left corner of the screen; neither function can be called in graphics modes. Figures 8 and 9 show examples of typical VIOSETCURPOS and VIOGETCURPOS calls. The functions VIOGETCURTYPE and VIOSETCURTYPE are used to get or alter the cursor height, width, and hidden/visible attributes. Both functions use a four-word data structure with the same format to communicate the attribute values──quite convenient when the cursor must be altered or hidden temporarily (see Figure 10). Display Mode Control The functions VIOGETMODE and VIOSETMODE allow programs to query or select the video display mode. Both calls use a data structure that contains flags for the adapter type, color burst enable, and text versus graphics mode; the number of displayable colors; the number of alphanumeric (text) columns and rows; and the number of pixel columns and rows. Another function, VIOGETCONFIG, returns the type of monitor and video adapter installed in the system and the amount of memory present on the adapter. This approach is open-ended: it allows the application to deal with the adapter on the basis of its capabilities and doesn't require the programmer to remember an ever-expanding list of "mode numbers" that are less and less related to the display modes they represent. For example, the code in Figure 11 selects 80 X 25, 16-color text mode with color burst enabled. Pop-up Support The functions VIOPOPUP and VIOENDPOPUP allow background processes to temporarily assert control of the screen and interact with the user. These processes, launched with the DETACH command, are placed in a special "black box" screen group; ordinarily the programs in this group cannot perform KBD or MOV input calls, and thus their output to the screen is discarded. In order to gain access to the screen, the background process first calls VIOPOPUP. This function has a wait/no-wait option: if wait is specified, the calling process is suspended until the screen is available; if no-wait is specified, the function returns immediately with an error code if the screen cannot be preempted, for example, if another background process has already called VIOPOPUP. If the VIOPOPUP call is successful, the current contents of the screen, which belong to the foreground process, are saved away, the screen is blanked and placed into an 80 X 25 text mode, and all further VIO calls by the background process that issued VIOPOPUP are directed to the active display. At this point, the background process can interact freely with the user. Other processes continue to run normally with their output going to the usual virtual screen buffer, until they require input or call VIOPOPUP, at which point they can be suspended. When the background process is finished with its pop-up activities, it must call VIOENDPOPUP to release control of the screen. The display belonging to the foreground process is then automatically restored. Figure 12 shows an example of this procedure. Since screen group switching is disabled during a VIOPOPUP...VIOENDPOPUP sequence and the abrupt transition from a normal display to the largely blank display of a pop-up (with a possible concomitant change in display mode) can be startling and disruptive to the user, use of VIOPOPUP should be kept to a minimum and reserved for true background processes. An ordinary application that wishes to use pop-up windows as part of its interaction can achieve more pleasant results by using the VIOREADCELLSTR and VIOWRTCELLSTR functions to save and restore small portions of the display. Summary MS OS/2's VIO subsystem provides a set of character-oriented display functions for use by so-called "kernel apps," programs that use only OS/2 kernel services and do not rely on the presence of the Presentation Manager. Most of the programs initially ported to OS/2 from MS-DOS will likely be based on VIO services exclusively. This is because the VIO calls do not require any drastic changes to the converted program's internal logic; they are sufficiently powerful to make direct hardware access unnecessary and can be invoked directly from high-level languages. Also, the Presentation Manager itself will not be available during the introductory period. ─────────────────────────────────────────────────────────────────────────── A Survey of OS/2 Display Services ─────────────────────────────────────────────────────────────────────────── The MS OS/2 Presentation Manager is based on Microsoft(R) Windows and provides applications with a uniform graphical user interface. It supports text fonts, windowing and clipping, pull-down menus, pop-up dialog boxes, pointing devices, and a broad range of high-performance graphic drawing and painting operations. Applications written to take full advantage of the Presentation Manager's services must have a special structure and must abide by an intricate set of conventions. However, the programmer's payoff for making this effort is complete portability between machines and output devices that support the Presentation Manager, transparent adjustment of the program's output to compensate for the display's resolution and aspect ratio, the ability to exploit available fonts, and a shortened learning curve for the user. Character-oriented applications can avoid the complexity of the Presentation Manager graphical interface, while retaining device independence and the ability to run in a window, by using the function DOSWRITE together with the preassigned handles for the standard output (1) and standard error (2) devices to send their output to the screen. ANSI escape sequences embedded in the output allow for control of the video mode, foreground and background colors, and cursor position. This method is analogous to the use of Interrupt 21H Function 40H (Write to File or Device) under MS-DOS Versions 2.x and 3.x and is most appropriate for filters and other utility programs where redirectability of the output is an important consideration. For more flexibility and higher performance, character-oriented applications can employ the OS/2 kernel's VIO family of services. The VIO functions allow scrolling in all four directions, control over the cursor shape, more versatile assignment of character attributes and colors, and reading strings back from the screen buffer, among other things. The VIO calls are roughly analogous to the ROM BIOS video driver (Interrupt 10H) calls available under MS-DOS 2.x and 3.x in that they are immune to redirection of the standard output and mostly ignore control codes embedded in the text. Applications that use VIO calls and avoid other hardware dependence will still run in a window under the Presentation Manager. Finally, we come to two hardware-dependent display methods that are allowed by MS OS/2. Applications that need to present a graphics display without the aid of the Presentation Manager, drive the controller in a mode or resolution not supported by OS/2's built-in screen driver, or have other special requirements might use these methods. The first hardware-dependent display technique is to obtain a selector from OS/2 that gives the application direct access to the screen group's Logical Video Buffer. After making modifications to the contents of the buffer, the program issues an additional command to refresh the actual video display──this call has no effect if the application's screen group is not currently in the foreground. Such applications will obviously not run in a window under the Presentation Manager and may also have unexpected effects on other programs running in the same screen group, but they will not conflict with the operation of programs in other screen groups. The second, and potentially more destructive, hardware-dependent display technique is to obtain the actual physical address of the video refresh buffer from OS/2. After "locking" the screen with a function call, the application can write to the refresh buffer and modify the state of the video controller directly, "unlocking" the screen with another function call when it is finished. While the lock is in effect, the user is prevented from switching to another screen group, and background tasks are unable to obtain control of the screen so as to attract the user's attention. Figure 1: VIO Subsystem Services at a Glance Text Display Functions VioWrtTty Write text string to display, then reposition cursor at end of string. Line feeds, carriage returns, tabs, and backspaces are interpreted properly. Line-wrap and screen scrolling are provided. VioWrtCellStr Write string of alternating characters and attribute bytes to screen at specified position. Cursor position is not affected. VioWrtCharStr Write character string to screen at specified position. Each character takes on the attribute of the previous characters at the same position. Cursor position is not affected. VioWrtCharStrAtt Write character string to screen at specified position, applying same attribute byte to each character. Cursor position is not affected. VioReadCellStr Read string of characters and attributes from specified position in display buffer to local buffer. VioReadCharStr Read string of characters from specified position in display buffer to local buffer. Replication Functions VioWrtNAttr Replicate attribute byte on screen n times, starting at specified position. Cursor position is not affected. VioWrtNCell Replicate character and attribute byte on screen n times, starting at specified position. Cursor position is not affected. VioWrtNChar Replicate character on screen n times, starting at specified position. Cursor position is not affected. Cursor Size and Position VioGetCurPos Get cursor position. VioSetCurPos Set cursor position. VioGetCurType Get cursor shape and size. VioSetCurType Set cursor shape and size. Scroll or Clear Screen VioScrollDn Scroll entire screen or portion of screen down by 1 to n lines, filling new lines with specified character and attribute, or erase part or all of screen. VioScrollLf Scroll left or clear screen as above. VioScrollRt Scroll right or clear screen as above. VioScrollUp Scroll up or clear screen as above. Display Mode Control VioSetANSI Turn interpretation of ANSI escape sequences on or off. VioSetCP Select code page used to display text data. VioSetFont☼ Downloads a display font into the video adapter and defines the dimensions of a character cell. VioSetMode Select current display mode. VioSetState☼ Set palette registers, border color, or blink/intensity toggle. Mode Information VioGetANSI Get state of ANSI driver (on or off). VioGetBuf Get selector for logical video buffer of current screen group and buffer length. VioGetConfig Get information about adapter type, display type, and amount of memory on adapter board. VioGetCP Get identifier for current code page in use for text display. VioGetFont☼ Get character cell dimensions and address of bit table for current or specified font. VioGetMode Get current display mode. Information returned includes adapter type, number of colors, vertical and horizontal resolution in both characters and pixels. VioGetPhysBuf☼ Get selector for physical video buffer. VioGetState☼ Get current settings of palette registers, border color, and blink/intensity toggle. Miscellaneous Functions VioPrtSc Print screen. VioPrtScToggle Turn echo of display to print device on or off. VioScrLock☼ Disable screen switching (used by a process that is updating the physical video display buffer directly). VioScrUnLock☼ Enable screen switching (used when a process is finished with direct access to physical display buffer). VioShowBuf Force update of physical display buffer from logical buffer. Pop-up Support VioPopUp Allocate full-screen text-mode pop-up display (used by background process, for example, a TSR program, or to notify user of errors while process's screen group is not selected). VioEndPopUp Deallocate pop-up display screen. VioSavReDrawWait☼ Allows a process to be notified when its screen should be saved or redrawn. Used by graphics mode program to recover from screen group switch or a pop-up by another program. VioSavReDrawUndo☼ Cancels a VioSavReDrawWait call by another thread within the same process. VioModeWait☼ Allows a process to be notified when it should restore the video display mode. Used by graphics mode program to recover from screen group switch or a pop-up by another program. VioModeUndo☼ Cancels a VioModeWait call by another thread within the same process. VIO Function Replacement VioRegister☼ Register video subsystem. Allows replacement of system's default routine for any or all VIO functions with new driver routines. VioDeRegister☼ Deregister video subsystem. Figure 2: Using VIOWRTTTY to write the string "Hello World" to the screen at the current cursor position, then move to a new line (scrolling if necessary). The cursor position is updated appropriately after the write. Microsoft Macro Assembler: ∙ ∙ ∙ push ds ; address of string to display push offset DGROUP:msg push msg_length ; length of string push 0 ; VIO handle (reserved) call VIOWRTTTY ; transfer to OS/2 or ax,ax ; did write succeed? jnz error ; jump if write failed ∙ ∙ ∙ msg db 'Hello World',0dh,0ah msg_length equ $-msg Microsoft C: extern unsigned far pascal VIOWRTTTY(char far *, unsigned, unsigned); ∙ ∙ ∙ int status; static char msg[]="Hello World\r\n"; ∙ ∙ ∙ status=VIOWRTTTY(msg, sizeof(msg)-1, 0); ∙ ∙ ∙ Figure 3: Using the VIOWRTCHARSTRATT function to write to the screen. This code displays the string "Hello World" in reverse video at cursor location (10,5), column 10, row 5. The VIOWRTxxx calls support high-speed output of strings of any length (up to the size of the screen), with simultaneous control over character position and attributes. Microsoft Macro Assembler: ∙ ∙ ∙ push ds ; address of string push offset DGROUP:msg push msg_length ; length of string push 5 ; Y position push 10 ; X position push ds ; address of reverse video attrib push offset DGROUP:rev_attr push 0 ; VIO handle (reserved) call VIOWRTCHARSTRATT ; transfer to OS/2 or ax,ax ; did write succeed? jnz error ; jump if write failed ∙ ∙ ∙ msg db 'Hello World' msg_length equ $-msg rev_attr db 70h ; reverse video attribute Microsoft C: extern unsigned far pascal VIOWRTCHARSTRATT(char far *, unsigned, unsigned, unsigned, char far *, unsigned); ∙ ∙ ∙ int status; static char msg[]="Hello World"; char rev_attr=0x70; ∙ ∙ ∙ status=VIOWRTCHARSTRATT(msg,sizeof(msg)-1,5,10,&rev_attr,0); ∙ ∙ ∙ Figure 4: Attribute byte for monochrome display adapter (MDA), EGA in monochrome text mode, or Hercules(R) Graphics Card in text mode. 7 │ 6 5 4 │ 3 │ 2 1 0 BL │background │I │forground BL = blink bit I = intensity (highlight or bold) Background Foreground Display 000 000 no display 000 001 underline 000 111 normal video 111 000 reverse video Figure 5: Attribute byte for graphics adapter in text mode is divided into two four-bit fields that control the foreground and background colors. The color assignments show are immutable for the original Color/Graphics Adapter and are the defaults for the Enhanced Graphics Adapter (other color assignments can be obtained by programming the EGA palette with VIOSETSTATE). 7 6 5 4 │ 3 2 1 0 background │foreground Color Assignments 0 black 1 blue 2 green 3 cyan 4 red 5 magenta 6 brown 7 white 8 dark gray 9 light gray 10 light green 11 light cyan 12 light red 13 light magenta 14 yellow 15 intensified white Figure 6: Clearing the screen to ASCII blanks with a normal video attribute, using one of the four VIO scroll functions. This special case call with an upper-left window coordinate of (0,0), lower-right coordinate of (-1,-1), and -1 as the number of lines to scroll does not require that the programmer first determine the dimensions of the active display. Microsoft Macro Assembler: ∙ ∙ ∙ push 0 ; Y upper-left push 0 ; X upper-left push -1 ; Y lower-right push -1 ; X lower-right push -1 ; number of blank lines push ds ; address of character/attrib push offset DGROUP:init_cell push 0 ; VIO handle (reserved) call VIOSCROLLUP ; transfer to OS/2 or ax,ax ; did scroll call succeed? jnz error ; jump if call failed ∙ ∙ ∙ init_cell db 20h,07h ; ASCII blank, normal video Microsoft C: extern unsigned far pascal VIOSCROLLUP(unsigned, unsigned, unsigned, unsigned, unsigned, char far *, unsigned); ∙ ∙ ∙ int status; static char init_cell[2]; /* initialization parameter */ init_cell[0]=0x20; /* character = ASCII blank */ init_cell[1]=0x07; /* attribute = normal video */ ∙ ∙ ∙ status=VIOSCROLLUP(0, 0, -1, -1, -1, init_cell, 0); ∙ ∙ ∙ Figure 7: Scrolling selected portions of the screen in any direction can be accomplished very easily with the VIOSCROLLxx functions──a useful building block for windowing packages. For example, this code scrolls the bottom half of the screen up one line, filling the newly blanked line with ASCII blanks having a normal video attribute, and leaving the top half of the screen untouched. Microsoft Macro Assembler: ∙ ∙ ∙ push 12 ; Y upper-left push 0 ; X upper-left push 24 ; Y lower-right push 79 ; X lower-right push 1 ; lines to scroll push ds ; address of character/attrib push offset DGROUP:init_cell ; for new line push 0 ; VIO handle call VIOSCROLLUP ; transfer to OS/2 or ax,ax ; did scroll succeed? jnz error ; jump if error ∙ ∙ ∙ init_cell db 20h,07h ; ASCII blank, normal video Microsoft C: extern unsigned far pascal VIOSCROLLUP(unsigned, unsigned, unsigned, unsigned, unsigned, char far *, unsigned); ∙ ∙ ∙ int status; static char init_cell[2]; /* initialization parameter */ init_cell[0]=0x20; /* character = ASCII blank */ init_cell[1]=0x07; /* attribute = normal video */ ∙ ∙ ∙ status=VIOSCROLLUP(12, 0, 24, 79, 1, init_cell, 0); ∙ ∙ ∙ Figure 8: Cursor positioning with the VIO calls. This code locates the cursor on the first column of the last line of the screen. Microsoft Macro Assembler: ∙ ∙ ∙ push 24 ; Y coordinate, row push 0 ; X coordinate, column push 0 ; VIO handle (reserved) call VIOSETCURPOS ; transfer to OS/2 or ax,ax ; did function succeed? jnz error ; jump if function failed ∙ ∙ ∙ Microsoft C: extern unsigned far pascal VIOSETCURPOS(unsigned, unsigned, unsigned); ∙ ∙ ∙ int status; ∙ ∙ ∙ status=VIOSETCURPOS(24,0,0); ∙ ∙ ∙ Figure 9: Reading the cursor position with VIOGETCURPOS. The cursor location is returned in text coordinates that assume a home position of (0,0). Microsoft Macro Assembler: ∙ ∙ ∙ push ds ; address to receive row (Y) push offset DGROUP:CurY push ds ; address to receive column (X) push offset DGROUP:CurX push 0 ; VIO handle call VIOGETCURPOS ; transfer to OS/2 or ax,ax ; was cursor position obtained? jnz error ; jump if function failed ∙ ∙ ∙ CurY dw ? CurX dw ? Microsoft C: extern unsigned far pascal VIOGETCURPOS(unsigned far *, unsigned far *, unsigned); ∙ ∙ ∙ int curx,cury,status; ∙ ∙ ∙ status=VIOGETCURPOS(&cury,&curx,0); ∙ ∙ ∙ Figure 10: Example of altering the cursor to a block and then back to its original shape. Microsoft Macro Assembler: ∙ ∙ ∙ push ds ; address of structure to push offset DGROUP:CurData ; receive cursor information push 0 ; VIO handle call VIOGETCURTYPE ; transfer to OS/2 or ax,ax ; did we get cursor info? jnz error ; jump if call failed mov ax,CurStart ; save cursor starting line mov CurPrev,ax mov CurStart,0 ; force starting line to ; zero to get block cursor push ds ; address of cursor data block push offset DGROUP:CurData push 0 ; VIO handle call VIOSETCURTYPE ; transfer to OS/2 or ax,ax ; did we change cursor? jnz error ; jump if call failed ∙ ∙ ∙ mov ax,CurPrev ; restore original cursor shape mov CurStart,ax push ds ; address of cursor data block push offset DGROUP:CurData push 0 ; VIO handle call VIOSETCURTYPE ; transfer to OS/2 or ax,ax ; did we change cursor? jnz error ; jump if call failed ∙ ∙ ∙ CurData label byte ; cursor data structure CurStart dw ? ; starting scan line CurEnd dw ? ; ending scan line CurWidth dw ? ; width (0=default) CurAttr dw ? ; attribute (0=visible, -1=hidden) CurPrev dw ? ; previous starting line for cursor, ; used to restore shape later Microsoft C: struct CursorData { unsigned cur_start; unsigned cur_end; unsigned cur_width; unsigned cur_attribute; }; extern unsigned far pascal VIOGETCURTYPE(struct CursorData far *, unsigned); extern unsigned far pascal VIOSETCURTYPE(struct CursorData far *, unsigned); ∙ ∙ ∙ struct CursorData CurData; int status,CurPrev; ∙ ∙ ∙ status=VIOGETCURTYPE(&CurData,0); /* get current cursor data */ CurPrev=CurData.cur_start; /* save cursor start line */ CurData.cur_start=0; /* set start line to 0 */ status=VIOSETCURTYPE(&CurData,0); /* force block cursor */ ∙ ∙ ∙ CurData.cur_start=CurPrev; /* previous cursor start line */ status=VIOSETCURTYPE(&CurData,0); /* restore cursor size */ ∙ ∙ ∙ Figure 11: Using VIOWRTTTY to write the string "Hello World" to the screen at the current cursor position, then move to a new line (scrolling if necessary). The cursor position is updated appropriately after the write. Microsoft Macro Assembler: ∙ ∙ ∙ push ds ; address of mode data structure push offset DGROUP:TextMode push 0 ; VIO handle (reserved) call VIOSETMODE ; transfer to OS/2 or ax,ax ; did mode set succeed? jnz error ; jump if mode set failed ∙ ∙ ∙ TextMode dw 8 ; length of data passed db 1 ; non-MDA, text mode, color enabled db 4 ; 16 colors dw 80 ; 80 text columns dw 25 ; 25 text rows Microsoft C: struct ModeData { unsigned length; unsigned char type; unsigned char color; unsigned col; unsigned row; unsigned hres; unsigned vres; }; extern unsigned far pascal VIOSETMODE(struct ModeData far *, unsigned); ∙ ∙ ∙ int status; struct ModeData TextMode; /* init mode params */ ∙ ∙ ∙ TextMode.length=8; /* amount of data passed */ TextMode.type=1; /* non-MDA, text mode, color */ TextMode.color=4; /* 16-color mode */ TextMode.col=80; /* 80 columns */ TextMode.row=25; /* 25 rows */ status=VIOSETMODE(&TextMode,0); /* force mode change */ ∙ ∙ ∙ Figure 12: Example of the use of VIOPOPUP and VIOENDPOPUP by a background process to interact with the user. Note that if a successful VIOPOPUP call is made, the program must be careful not to take any branch away from the main line of execution toward VIOENDPOPUP. Microsoft Macro Assembler: ∙ ∙ ∙ ; First put up pop-up Window... push ds ; address of Wait flags push offset DGROUP:WaitFlag push 0 ; VIO handle call VIOPOPUP ; transfer to OS/2 or ax,ax ; did we capture display? jnz error ; no, jump to error handler ; now display pop-up message... push ds ; address of message push offset DGROUP:msg push msg_len ; length of message push 12 ; Y push (80-msg_len)/2 ; X (center it) push 0 ; VIO handle call VIOWRTCHARSTR ; transfer to OS/2 push 0 ; pause for 5 seconds push 5000 call DOSSLEEP ; transfer to OS/2 ; Take down pop-up window... push 0 ; VIO handle call VIOENDPOPUP ; (should never fail) ∙ ∙ ∙ msg db 'Hello World' msg_len equ $-msg WaitFlag dw 1 ; bit 0=1 if Wait until pop-up ; is available, =0 if no wait Microsoft C: extern unsigned far pascal DOSSLEEP(unsigned long); extern unsigned far pascal VIOWRTCHARSTR(char far *, unsigned, unsigned, unsigned, unsigned); extern unsigned far pascal VIOPOPUP(unsigned far *, unsigned); extern unsigned far pascal VIOENDPOPUP(unsigned); ∙ ∙ ∙ static char msg[]="Hello World"; ∙ status=VIOPOPUP(&WaitFlag, 0); /* pop-up screen */ /* display msg at center screen */ status=VIOWRTCHARSTR(msg,sizeof(msg)-1,12,(80-sizeof(msg))/2,0); status=DOSSLEEP(5000L); /* pause for 5 seconds */ status=VIOENDPOPUP(0); /* release pop-up screen */ ∙ ∙ ∙ ████████████████████████████████████████████████████████████████████████████ Dynamic Allocation Techniques for Memory Management in C Programs Steve Schustack☼ Most end-user PCs have between 64Kb and 640Kb of memory, and it is safe to assume that many of these machines will, at some point, have more RAM added on. Though we know what the lower and upper bounds of installed memory are, we can never be certain about how much memory any given machine might have. Programs can be written to run in specific, fixed-size environments, or they can be designed to dynamically adjust to any given amount of RAM. Programs that support such flexible memory management capability certainly offer an extra measure of utility. I began to consider this when my favorite word processor did not recognize the 128Kb of additional memory I had just installed in my own machine; instead it completely ignored the additional RAM and merrily continued to access the PC's disk. Had the programmers who wrote my word processing program utilized dynamic memory allocation techniques, the program would have been able to use the additional memory I had installed to hold more of my text, and perhaps to hold more of the application as well. Dynamic allocation allows programs to adapt to their environments, taking advantage of any memory available on an as-needed basis. Such programs dynamically allocate memory to hold array and linked list data rather than using fixed array dimensions, which are set by the programmer in array declarations. The only limitation on the array sizes becomes the amount of memory available on the user's system. This article will focus on using C's standard library functions to write C programs that employ data structures that grow and shrink dynamically during program execution. We will implement these functions through variable-sized arrays and linked lists, which are applications of dynamic memory allocation. Standard library functions such as malloc and free are among the programmer's tools explored here. The function malloc, for example, accepts as its argument the number of bytes of memory needing to be dynamically allocated, and returns a pointer to a block of free memory at least that large, provided enough memory is available. A call to the function free returns dynamically allocated memory to the pool of memory available to a program. The Problem Like most programming languages, C requires that when you declare an array you must specify its size. This often becomes a limitation of the program, however, because it sets an upper bound on the amount of data the program can process. Why not simply make the arrays so large that they are sure to satisfy even the largest uses? Regrettably, memory is not limitless. The more memory a program needs, the larger the system required to run it. As terminate-and-stay-resident (TSR) programs become more popular and operating system memory requirements grow, it makes a great deal of sense for programs to conserve memory by using only what is needed. A PC's memory has, in fact, become a shared resource. What are a programmer's options for reserving memory to hold a program's data? Memory Organization A program's memory is organized into areas known as heap, stack, data, and code. The heap is at the top of a program's memory space and is the source of dynamically allocated memory. The size of the heap depends on the sizes of the other areas, because the heap consists of all available memory not assigned to one of the other areas. The stack is just below the heap in memory; its size is set by LINK, either to 2048 bytes by default, or to the size given to LINK with the /STACK option. The data area contains static and external variable data. Executable machine instructions fill the code area. Allocation of Memory When a program obtains memory for its exclusive use, the memory is said to be allocated to that program. C programs have two options for memory allocation. The most common way memory is allocated is by simple declaration of variables. The alternative is to dynamically allocate memory by calling certain standard library functions, such as malloc, calloc, and realloc. Memory for static and external variables is allocated and initialized only once, prior to execution of the program, at the time the program is loaded from disk into RAM. It remains allocated until the program terminates its execution. All other variables, namely those of auto or register storage class, are allocated each time the function (or block) in which they are declared is entered. The memory for auto and register variables is released (deallocated) when the flow of control leaves the declaring function (or block). Because memory for statics and externals is allocated throughout execution, limiting the use of static and external variables tends to conserve RAM. Auto and register variables, on the other hand, need only be allocated when a given function is active (on the calling stack). A function is active when it is called by main or any other function. Lifetime, scope (visibility), and initialization are other factors to consider when selecting storage classes for your variables. In order to dynamically allocate memory, the amount of memory needed must first be known. The C keyword operator sizeof is used to find the number of bytes allocated to any variable that has been declared. The sizeof operator may also be used on data types that are enclosed in parentheses. This means that a program can expect the value of the expression "sizeof (int)" to be two or four, depending on whether it was compiled and whether it is executing on a 16-bit or on a 32-bit CPU (see Figure 1). Variable-length Arrays Dynamic allocation of a block of memory takes place during execution. The size of the dynamic block is specified using arguments to standard library functions such as malloc, calloc, and realloc. This is in contrast to the dimension of an array, which is fixed by a constant or by a constant expression in a declaration. The elementary program SUM_NUM, listed in Figure 2, introduces the standard library function calloc. Within SUM_NUM, calloc returns the address of a block of memory whose size is specified by passing the number of cells (elements) to be allocated, along with the size in bytes of each cell. Simply declare a pointer to the type of data to be stored, and assign the return value from calloc to that pointer variable, as follows: long *nums; ∙ ∙ ∙ nums = (long *) calloc(how_many,sizeof(long)); These two statements create a dynamically allocated array of "how_many" long integers, and cause the pointer "nums" to point to that array. The "(long *)" before calloc, which is a cast operator indicating "pointer to type long," is optional and specifies a conversion that will avoid compiler warnings such as "different levels of indirection." If a function is not declared prior to being called, or if it is not explicitly declared as being of a particular type (like "long"), the compiler assumes that "int" is the type of value returned by that function. Assigning "nums" a value from calloc without the "(long *)" cast would therefore result in trying to assign an int value to a variable declared as long, resulting in a mismatch of levels of indirection and causing a compiler warning message. If the amount of memory requested is not available, then calloc returns the value of NULL, which happens to be defined in stdio.h as zero. If you don't test for this special null pointer value and store data at address 0, you will get a "null pointer assignment" error when your program terminates. Clobbering data in memory not dynamically allocated and not allocated to any variable, such as data at address 0, can easily go unnoticed and create hard-to-find errors, hard-to-find because the program may only misbehave while handling some rare and unusual case not tested during program development. Pointers and Arrays An understanding of C's use of pointers and arrays is essential, especially in relation to memory management schemes. The pointer and array data types are very closely related──perhaps more than you realized. Whenever you refer to an array in C by using that array in an expression, such as passing it to a function or getting the value of one of its elements, the value of the name of the array is taken to be the starting address of that array. So if you have a pointer variable that has been made to point to an array of the same data type, you can use that pointer in expressions as if it were the array (see Figure 3). The fact that pointers behave very much like arrays means that you can operate on elements of the array of data pointed to by the variable nums in SUM_NUM.C (in Figure 2), as if nums were an ordinary array of long integers. The address of an element pointed to by nums in the expression "&nums[inum]" is passed to the function scanf. The value of the element is added to sum in the statement "sum += nums[inum];". Even if you are a newcomer to C's pointer operations, you have been applying this concept every time you've written a function that operated on elements of an array that was passed to it. The parameters of a function that receive data from an array are declared using empty brackets, as in the expression get_param[ ]. What C actually passes to the function is the array's starting address. The function treats the parameter as an array, even though the parameter is in fact a pointer. One way to tell the array itself apart from a pointer to it is by using the sizeof operator. In Figure 3, the sizeof operator returns different values for p_longs and long_array, as indicated. A consequence of this type of argument-passing is that a called function cannot directly tell the dimension of an array that was passed to it as an argument. This is because the array itself is not available to it, so its size cannot be tested. Instead, the value passed to the function is a pointer to the array's first element, and as a pointer variable, it is either 2 or 4 bytes in size (an int or a long in a 16-bit machine). Free Memory When a program terminates, all of the memory allocated to it is released. This includes memory allocated dynamically by calling C library functions, like calloc. When allocated memory is released, it is deallocated and becomes available for future allocations. If your program requires repeated dynamic allocations, then you can call the function free to avoid running out of memory. Call free to release blocks of memory when they are no longer needed. The free function expects as its argument the same address that was returned by a previous call to calloc, malloc, or realloc, for example: nums = (long *) calloc(how_many,sizeof(long)); free(nums); /* deallocates memory nums points to */ Never pass an address to free that was not first given to you by calloc, malloc, or realloc; otherwise things will get nasty. Each block of dynamically allocated memory is preceded by internal system maintenance information, which includes the size of that block, as well as a pointer to the next block. If that maintenance information has been clobbered by storing data off the end of another block or the beginning of this block, or the information is simply not there, then the pool of available memory will become "polluted" by that call to free. Library dynamic allocation functions will erroneously determine that free memory is already allocated, or even worse, that already allocated memory is free to be reallocated. To illustrate the concept of dynamically allocating data we will look at a particular application, namely sorting a directory. Directory information is an unknown and unpredictable quantity. A directory sorting program would therefore have to do one of two things: either some specific size would have to be predetermined, or the sorting routine could be designed to dynamically allocate whatever memory it might need. Though the program may seem simple, it provides an excellent example of dynamic memory allocation and lends itself well to the memory management technique called linked list processing. Problem Definition The number of files in a directory can vary and change in unpredictable ways. Directories can vary in size from as little as two files ("." and ".."──an empty directory) to whatever upper bound the most current release of MS-DOS(R) allows. Because this upper bound may change with the next release of the operating system, a utility to sort an MS-DOS directory should be able to deal with this change in a manner that will not necessitate rewriting the program. The DIR command outputs information about files in a directory. Unfortunately, the order in which files are listed is not very useful. A solution to both problems will be discussed. The MS-DOS filter program SORT can be used to reorder the listing of files, to be sorted by file name and extension, for instance. Just tell SORT what column to begin sorting on, and it does the job. Using a pipe with the SORT and DIR command provides a somewhat useful but rather limited directory listing and sorting capability. Furthermore, a number of things are poorly handled by SORT. For example, a file modified in the early afternoon will be listed ahead of a file modified late in the morning of the same day, because MS-DOS uses "a" and "p" to represent A.M. and P.M., rather than a 24-hour clock. Combining DIR and SORT can be useful, but something better is needed. Linked Lists Linked lists are a very powerful and flexible way of using dynamically allocated memory. They are suited to applications in which data of different types (integer, character, and floating point) can be combined in chainlike structures of unpredictable lengths. Each element of a linked list is like a record in a memory-resident file of records. A record holds keys for other records, which it points to. These records link together to form structures ranging from a simple single chain to exotic networks of chains. Linked lists come in a variety of sizes and shapes, with some common characteristics and terminology. They can become quite complex in structure. We will be looking at the simplest type, the singly linked list, which consists of a series of elements, called nodes, that are joined to form a chain. It is only possible to move along this type of list in one direction, from the starting node of the list, its head, to the end node, its tail. The ease of inserting a new link between two links in a list makes linked lists great for storing data in a variety of different orderings. If each new link added to a list is inserted at the head of the list, then the list behaves like a push-down stack (also known as a last-in-first-out (LIFO) queue). If new links are stored only at the tail of the list, a first-in-first-out (FIFO) queue is the result. Any ordering is possible if new links can be inserted anywhere in the list. Sorting text data alphabetically is easily accomplished. The data in Figure 4 was output by the DIR command and will be used to construct two kinds of linked lists: first a push-down stack, then a sorted list. The evolution of a push-down stack from an empty list is demonstrated in Figures 5, 6, 7, 8, 9, and 10. The list itself is displayed in Figure 11. A memory diagram showing the state of the list after each code segment is executed follows each segment in Figures 5, 6, 7, 8, 9, and 10. The link structure tag, s_dir_node, is declared in Figure 5, along with p_head and p_new, which are both declared to be pointers to that type of structure. The member "next", the first member of an s_dir_node structure, is also a pointer to an s_dir_node structure. This type of structure is called self-referencing because it contains a pointer to its own data type. The member "next" is the means for joining structure links to form a linked list. The pointer to the head of the list, p_head, is assigned the null pointer NULL (from stdio.h) to create an empty list. Enough memory to hold a single s_dir_node structure is dynamically allocated by calling the standard library function malloc in Figure 6. The argument passed to malloc is the number of bytes required to be allocated. The pointer returned by malloc is the address of the newly allocated block, but it is type pointer to char, so it must be converted to type pointer to an s_dir_node structure using the "(struct s_dir_node *)" cast operator. The type-converted address is assigned to p_new as the final action of this first statement. The memory diagram indicates that the new link is at address 3800. The remaining two statements in Figure 6 assign values to the members of the allocated link by using the arrow "->" operator. The expression "p_new->dir_line" refers to the member dir_line in the s_dir_node structure pointed to by p_new. NULL is assigned to "p_new->next" to make it the tail of the list. This link is also the head of the list. The variable p_head is made to point at the same link that p_new points to via the assignment in Figure 7. In Figure 8, another new link is allocated at address 3900. The combined actions in Figures 9 and 10 place this second new link at the head of the list, in front of the previous head, in push-down stack fashion. The "for" loop in Figure 11 lists each link in the list, starting at the head and moving, one link per iteration, to the tail. One interesting aspect of the loop is that, rather than counting up or down, it moves through a chain of pointers. The expression "p_new = p_new->next" accomplishes that movement by assigning to p_new the value of the member "next" in the link p_new points to. Notice that in the format string passed to printf, inside the loop, the %u (unsigned integer) format specifier is used to display addresses, since addresses are never negative. Now that you've had a taste of building linked lists and operating on them, take a moment to examine the comparison between linked lists and arrays in Figure 12. Linked lists facilitate handling unpredictable amounts of data. It is easy to change the order of the data, by inserting a new link or chain of links between two existing links on the list; it is just as simple to remove a single link, or chain of links. Accessing the different elements of an ordinary array is quite fast and provides random direct access. The elements in a linked list must be accessed in order, from the head, moving link-by-link, to the tail. Different paths through (or orderings of) a single list can be achieved by adding additional next-link pointers to the link structure, however, which speeds up the process somewhat. The term "doubly linked list" refers to a linked list that contains a pointer to the previous link as well as a pointer to the next one. The overhead of storing link pointers in each link is another price paid for linked lists, but not for arrays. Keeping those facts in mind, let's proceed to a more real-world application of linked lists. BY_TIME Program The BY_TIME program may be used to sort DIR command output for all files in a directory, or only those selected by a wildcard file specification. The file information is placed in order with the most recently created or modified first. The sort is accomplished by inserting a link for each file's information, read from standard input, into its correct position in the list so that the list is always in order. A DOS command to invoke BY_TIME to list the *.EXE files in date and time sequence appears in Figure 13, along with some sample output. The organization and structure of the program BY_TIME demonstrates a coding style that is appropriate for developing larger applications. The main function hides the details of the data manipulations that take place in the lower-level functions, but it does give a clear overall picture of the processing to be performed. Each function is short and concise, doing a single job. No external data is used; instead the paths through which the data flows are made more clear by being passed as arguments. The BY_TIME program is built by separately compiling each of the C source files and then linking the resulting object files (see Figures 14, 15, 16, 17, 18, 19, and 20). Each node in BY_TIME's linked list is a familiar structure with two members, a pointer to another node and a string of information, output by DIR, about each file. The structure of the nodes is defined in the header file, BY_TIME.H in Figure 14, so that it can be #include'd in each source file that needs it. The first action taken by the main function in Figure 15 is a call to the function bgn_list, passing the address of a dummy head node that was declared as an s_dir_node structure. The function bgn_list in Figure 16 assigns dummy data (not for a file from DIR) into that node, with a date value that is high enough to ensure that that node will always be the head, given the date and time ordering requirements for this list. This special head helps keep the code clean, because it prevents the head node from being treated as a special case when inserting new links. The head of the list is not dynamically allocated──it is an ordinary auto structure type variable. The initialized list is now ready to grow with the user's file information. The "while" loop in main alternates between calls to get_dir and sav_dir, until get_dir returns zero. Each call to get_dir (listed in Figure 17) places one line of file information in the buffer that was passed to it. It returns 1 if successful; otherwise it returns 0, indicating that the end of input has been reached. Once DIR information for the first file has been input by the function get_dir, the first block of dynamically allocated memory is requested and put to use in sav_dir, as shown in Figure 18. The function sav_dir is responsible for inserting new links in such a way that the list of files is always in most-recently-created-or-modified-first sequence. The insertion process begins with a loop that searches the list for the correct insertion point. The function time_b4 in Figure 19 is called in the test expression of the sav_dir "for" loop. The time_b4 function compares the date and time in the input buffer with the date and time in a link in the list and returns 1 if the buffer time is earlier; otherwise it returns 0. Memory for the new node is dynamically allocated by a call to malloc in sav_dir. Notice that a test for NULL is made to verify that enough memory is available for the new node. The last two statements in sav_dir break the connection between the two appropriate links and splice in the new link, thereby creating a list that is one node longer, with all nodes in the correct order. Each DIR output line is displayed by the function sho_list in Figure 20, as it moves from the head to the tail of the list. This takes place after the end of input has been reached and the list is built. The code in sho_list loops through the list, displaying the data portion of each link, just as the loop in Figure 11 did. Beware of Dangling Pointers As with the free function previously mentioned, care must be taken when dealing with memory management C code, especially when referencing memory through pointers. The program DANGLE.C in Figure 21, for example, is guilty of memory mismanagement, because it refers to values in unallocated memory. The results don't make sense at first glance, until you realize that a nasty bug is at work. The output consists of three different lines of garbage. Try it and see for yourself. The memory pointed to by p_text was not allocated in the three cases where its address was passed to puts and printf for output. It was allocated while str_dang was executing, but after str_dang returned the address of its variable auto_string, the memory for auto_string became deallocated. Automatic variables within a function become deallocated when that function returns to its caller. Next, the auto_string memory became reallocated for a new use, so the string pointed to by p_text changed, as if by magic. The new use was for printf, or one of its subfunctions. Memory for all of a program's static and external variables remains allocated for the entire time the program is executing. If the declaration of auto_string were preceded by the word "static," then instead of three different lines of junk, you would see three identical lines of 123456789. Summary and Beyond Programs that process unpredictable amounts of data residing in memory can benefit from dynamically allocated arrays. These arrays may be made up of simple single-element data, or they may be arrays of complex structures. Once allocated, these dynamic arrays may be operated on by using the square brackets "[ ]" operator to access individual elements such as ordinary arrays. If changes in the ordering of the data are part of the data's processing, then linked lists should be considered to hold the data. The trade-offs in this decision involve how the data will be accessed. Will random accesses be frequent, or will the list only be processed in sequence? Will the overhead of node pointers for each link be acceptable? Are the people who will maintain this code fluent C programmers who understand pointers, dynamic allocation, and linked list theory? Only the singly linked list was examined here in detail. The doubly linked list, which can be traversed forward or backward, received a mention. A tree is another, more complex, type of linked list. The head of a tree may have two or more links it points to, which are sometimes called children, as in a family tree. Each of the child nodes may also have one or more children. Nodes without children are called leaves. No loops within a tree are permitted, just as a person cannot be his own parent or grandparent. Applications of trees include sorting, parsing expressions in compilers and translators, and processing directories of files and their subdirectories. For more information about managing memory, dynamic allocation, and linked lists, I encourage you to read the chapter "Efficient Use of Memory" in my book Variations in C (Microsoft Press: 1985). How well memory is managed can make a real difference in the value of a program to those who use it. May your pointers not dangle and your lists stay linked. Figure 1: Using sizeof to Determine Memory Needs main() { short counter; /* 2 bytes of RAM allocated. */ static char name_list[1000][25] = '\0'; /* 25000 bytes */ int status_vector[10]; /* 20 bytes on 16-bit CPU's, */ /* 40 bytes on 32-bit CPU's. */ printf("sizeof counter %d, name_list %d, status_vector %d\n", sizeof counter, sizeof name_list, sizeof status_vector); printf("int's are %d bytes, I'm a %d-bit CPU\n", sizeof (int), 8 * sizeof (int)); } This output will result from the program above: sizeof counter 2, name_list 25000, status_vector 20 int's are 2 bytes, I'm a 16-bit CPU Figure 2: SUM_NUM.C /**************************************************************** * SUM_NUM.C: Input and total a variable number of integers. * Show list of numbers and cumulative subtotals in output. */ main() { long *nums; /* Pointer to array of numbers to sum */ short how_many; /* Number of numbers to input and sum */ short inum; /* Counter to index into nums array */ long sum; /* Sum of numbers in nums array */ printf("How many numbers to sum? "); if (1 != scanf("%hd", &how_many) || how_many < 2) exit(1); /* Terminate sum_num program */ /* Dynamically allocate memory for how_many long's */ nums = (long *) calloc(how_many, sizeof (long)); for (inum = 0; inum < how_many; ++inum) /* Input nums */ { printf("Enter #%d: ", inum + 1); if (1 != scanf("%ld", &nums[inum])) how_many = inum; /* Quit early */ } for (sum = inum = 0; inum < how_many; ++inum) { sum += nums[inum]; /* Add number to sum */ printf("%3d: %10ld %10ld\n", /* Show results */ inum + 1, nums[inum], sum); } printf("\nThe sum of the %d numbers entered is %ld\n", how_many, sum); } Figure 3: Use of Pointers with Arrays main() { long long_array[100]; /* Ordinary array of long's */ long *p_longs; /* Pointer to type long */ p_longs = &long_array[0]; /* point at long_array */ p_longs[8] = 32; printf("%d equals %d\n", p_longs[8], long_array[8]); printf("sizeof p_longs %d, sizeof long_array %d\n", sizeof p_longs, sizeof long_array); } Which outputs: 32 equals 32 sizeof p_longs 2, sizeof long_array 400 Figure 4: Sample Data from Original DIR Command MEMO TXT 46 11-18-86 6:26p DIROUT TXT 1206 1-17-87 7:25p Figure 5: Declaring the Link Structure Tag #include /* For #define of NULL pointer */ #define DIR_LINE_LEN 39 /* DIR output line length */ struct s_dir_node /* Linked list node structure */ { struct s_dir_node *next; /* Next node pointer */ /* or NULL (0) */ char dir_line[DIR_LINE_LEN + 1]; /* DIR output line */ } struct s_dir_node *p_head; /* Pointer to head of list */ struct s_dir_node *p_new; /* Pointer to new link */ p_head = NULL; /* List is empty now */ ┌──────┐ p_head │ 0000 │ └──────┘ ┌──────┐ p_new │ ???? │ └──────┘ Figure 6: Allocating for a New Link /* Dynamically allocate a new link and assign data to it */ p_new = (struct s_dir_node *) malloc (sizeof (struct s_dir_node)); strcpy (p_new->dir)line, "DIROUT TXT 1206 1-17-87 7:25p"); p_new->next = NULL; /* No next node - this is the tail */ ┌──────┐ p_head │ 0000 │ └──────┘ ┌──────┐ p_new │ 3800 │ └───┬──┘ ┌──────┐ └─────┤ 0000 │ DIROUT TXT 1206 1-17-87 7:25p └──────┘ Figure 7: First Link p_head = p_new; /* Empty list grows to a single link */ ┌──────┐ p_head │ 3800 ├─────┐ └──────┘ │ ┌──────┐ │ p_new │ 3800 │ │ └───┬──┘ ┌──┴───┐ └─────┤ 0000 │ DIROUT TXT 1206 1-17-87 7:25p └──────┘ Figure 8: Allocating for Additional Links /* Allocate a second link and assign data to it */ p_new = (struct s_dir_node *) malloc(sizeof (struct s_dir_node)); strcpy(p_new->dir_line, "MEMO TXT 46 11-18-86 6:26p"); p_new->next = NULL; /* No next node yet */ ┌──────┐ p_head │ 0000 │ └───┬──┘ ┌──────┐ └─────┤ 0000 │ DIROUT TXT 1206 1-17-87 7:25p └──────┘ ┌──────┐ p_new │ 3900 │ └───┬──┘ ┌──────┐ └─────┤ 0000 │ MEMO TXT 46 11-18-86 6:26p └──────┘ Figure 9: Second Link p_new->next = p_head; /* New link points to list's head link */ ┌──────┐ p_head │ 0000 │ └───┬──┘ ┌──────┐ └─────┤ 0000 │ DIROUT TXT 1206 1-17-87 7:25p └───┬──┘ ┌──────┐ │ p_new │ 3900 │ │ └───┬──┘ ┌───┴──┐ └─────┤ 3800 │ MEMO TXT 46 11-18-86 6:26p └──────┘ Figure 10: Completed Links p_head = p_new; /* List grows again, now 2 links long */ ┌──────┐ p_head │ 0000 ├─────┐ └──────┘ ┌──┴───┐ │ 3800 │ MEMO TXT 46 11-18-86 6:26p └──┬───┘ ┌──────┐ │ p_new │ 3900 │ │ └───┬──┘ │ │ ┌──┴───┐ └─────┤ 0000 │ DIROUT TXT 1206 1-17-87 7:25p └──────┘ Figure 11: Output Result from the Linked List /* Now that the linked list is built, display it in a for loop */ for (p_new = p_head; p_new != NULL; p_new = p_new->next) printf("At addr: %4u, next: %4u, dir_line: %-.12s\n", p_new, p_new->next, p_new->dir_line); * * * * * * * * * * * * * * * * * * * * * * Output: At addr: 3900, next: 3800, dir_line: MEMO TXT At addr: 3800, next: 0, dir_line: DIROUT TXT Figure 12: Linked Lists vs Arrays Tradeoffs exist between storing data in linked lists and in arrays. Linked lists Arrays Variable length Fixed dimension with maximum size Will use memory added Ignores memory added by user by user Fast to insert or remove Slow to insert or a link remove an element No random access, sequential Random direct access to lists only accesses are fast Memory overhead for node No pointer memory pointers overhead Code to manipulate lists Code to manipulate is harder to read arrays is easier to read Figure 13: Sample Output of BY_TIME Program DIR *.EXE | BY_TIME BY_TIME EXE 5654 1-26-87 8:44p VWC EXE 16886 1-17-87 8:22p GREP EXE 9336 4-06-87 6:43p PR EXE 11604 3-16-86 11:07p CFIND EXE 8146 6-20-85 5:30p CHARCNT EXE 8146 4-07-87 9:53p LISTING EXE 6266 4-07-85 9:27p VISIBLE EXE 6402 4-07-85 9:24p Figure 14: BY_TIME.H /**************************************************************** * BY_TIME.H: Header file for BY_TIME program, define linked * list structure as next node pointer and line of DIR text */ #define DIR_LINE_LEN 39 /* DIR output line length */ struct s_dir_node /* Linked list node structure */ { struct s_dir_node *next; /* Next node pointer */ /* or NULL (0) */ char dir_line[DIR_LINE_LEN + 1]; /* DIR output line */ }; Figure 15: BY_TIME.C /**************************************************************** * BY_TIME.C: Filter sorts DIR output in linked list, most recent * first. This is the main function of the program. */ #include /* For BUFSIZ symbol */ #include "by_time.h" /* Linked list struct */ main() { struct s_dir_node head; /* First node in list */ char in_buf[BUFSIZ]; /* Input buffer for */ /* DIR output */ bgn_list(&head); /* Initialize list */ while (get_dir(in_buf)) /* Input DIR info for */ /* next file */ sav_dir(in_buf, &head); /* Save file info in */ /* sorted linked list */ sho_list(&head); /* Show sorted list */ } Figure 16 BGN_LIST.C /**************************************************************** * BGN_LIST.C: Initialize head of list to dummy highest value */ #include /* For NULL (zero) pointer */ #include "by_time.h" /* For linked list struct */ bgn_list(p_head) struct s_dir_node *p_head; /* Pointer to head of list */ { /* Date in head is greater than DIR date for any file */ strcpy(p_head->dir_line, "ZZZZZZZZ ZZZ 99999 99-99-99 99:99p"); p_head->next = NULL; /* No next node - empty list */ } Figure 17: GET_DIR.C /**************************************************************** * GET_DIR.C: Input DIR info for next file from stdin */ #include /* For NULL (zero) pointer */ int get_dir(buf) char *buf; /* Buffer for read and pass line back */ { char *rtn; /* save gets() return */ /* Loop: Input lines until no more input or got line */ /* starting with an uppercase letter (has file data) */ while ((rtn = gets(buf)) && /* Input a DIR line */ (buf[0] < 'A' || buf[0] > 'Z')) /* For file? */ ; return (rtn != NULL); /* Return 1 if got data, else 0 */ } Figure 18: SAV_DIR.C /**************************************************************** * SAV_DIR.C: Allocate new node in list, save DIR info in it */ #include /* For NULL (zero) pointer */ #include "by_time.h" /* For linked list struct */ sav_dir(buf, p_head) char *buf; /* Line of DIR output to save */ struct s_dir_node *p_head; /* Pointer to head of list */ { struct s_dir_node *p_next; /* Pointer to next */ /* node in list */ struct s_dir_node *old_p_next; /* Pointer to previous */ /* next node, parent of current next node */ struct s_dir_node *p_new; /* Pointer to new node */ /* Loop: for each node in list until end of list or */ /* insert point that will keep list sorted is found */ for (p_next = p_head->next, old_p_next = p_head; p_next && time_b4(buf, p_next); old_p_next = p_next, p_next = p_next->next) ; /* Dynamically allocate memory for new node - DIR output * line. Note use of the cast (struct s_dir_node *) * operator in the assignment to avoid this message: * warning 47: '=' : different levels of * indirection */ p_new = (struct s_dir_node *) malloc(sizeof (struct s_dir_node)); if (p_new == NULL) /* malloc() failed, out of RAM */ { puts("Out of memory!!!"); return; } strcpy(p_new->dir_line, buf); /* Save DIR line in */ /* newly alloc'd node */ p_new->next = old_p_next->next; /* New node points to */ /* rest of list */ old_p_next->next = p_new; /* Insert new node in */ /* list */ } Figure 19: TIME_B4.C /**************************************************************** * TIME_B4.C: Return 1 if date and time in buf is before date * and time in node p_next points to, otherwise return 0. */ #include "by_time.h" /* For linked list struct */ int time_b4(buf, p_next) char *buf; /* Line of DIR output to find */ /* insert point in list for */ struct s_dir_node *p_next; /* Pointer to node in list to */ { /* compare time in buf with */ int rtn; /* Return value from strncmp() */ /* compare year, month, day, am/pm, hour, and minute */ if (rtn = strncmp(&buf[29], &(p_next->dir_line)[29], 2)) return (rtn < 0); /* Years differ */ if (rtn = strncmp(&buf[23], &(p_next->dir_line)[23], 2)) return (rtn < 0); /* Months differ */ if (rtn = strncmp(&buf[26], &(p_next->dir_line)[26], 2)) return (rtn < 0); /* Days differ */ if (buf[38] != (p_next->dir_line)[38]) /* am/pm's */ return (buf[38] == 'a'); /* differ */ if (rtn = strncmp(&buf[33], &(p_next->dir_line)[33], 2)) return (rtn < 0); /* Hours differ */ if (rtn = strncmp(&buf[36], &(p_next->dir_line)[36], 2)) return (rtn < 0); /* Minutes differ */ return (0); /* Dates and times are equal */ } Figure 20: SHO_LIST.C /**************************************************************** * SHO_LIST.C: Show sorted linked list - output it to stdout */ #include /* For NULL (zero) pointer */ #include "by_time.h" /* Linked list struct */ sho_list(p_head) struct s_dir_node *p_head; /* Pointer to head of list */ { struct s_dir_node *p_next; /* Pointer to next */ /* node in list */ for (p_next = p_head->next; /* Start at first node */ p_next != NULL; /* Still more list? */ p_next = p_next->next) /* Move down a node */ puts(p_next->dir_line); /* Output a DIR line */ } Figure 21: DANGLE.C──Beware of Dangling Pointers /**************************************************************** * DANGLE.C: Danger of referencing a dangling pointer. That is a * pointer to deallocated memory. */ main() { char *p_text; p_text = str_dang(); puts(p_text); printf("%s\n", p_text); printf("%s\n", p_text); } str_dang() { char auto_string[10]; strcpy(auto_string, "123456789"); return (&auto_string[0]); } ████████████████████████████████████████████████████████████████████████████ CD ROM Technology Opens the Doors on a New Software Market Tony Rizzo Along with the introduction of major new CD ROM applications from Microsoft(R) and other software publishers, the confident glow of success that usually follows a "good year" can be seen on the faces of all the participants in the CD ROM industry these days. After a somewhat shaky entry into the personal computing environment, a new stability has settled on the CD ROM world that is finally fostering the growth of a new PC software market──and expectations for dramatic growth are running high. Two factors in particular contribute to this new stability. First, the commercial success of compact audio discs give CD ROM a strong, low-cost manufacturing capability, prompting considerable interest from potential users of the medium. Second, the creation of a standardized logical disc format and new systems software from Microsoft allows CD ROM drives to easily interface with MS-DOS(R)-based personal computers so as to provide a high level of software and hardware compatibility. CD ROM applications extend the use of PCs from information processing to include information accessing. It has proved to be a perfect medium for PC applications requiring: ■ a substantial amount of data space ■ wide and inexpensive distribution of medium-volatile data, such as a parts catalog──it is considerably less expensive to press and ship new discs in quantity than it is to print new copies of reference works or update looseleaf references ■ random searching and accessing of large databases ■ distribution of large standardized reference works, such as dictionaries and specification catalogs, to be integrated into a PC user's environment Additionally, state-of-the-art hardware now gives developers the ability to integrate text with video and audio information, which will ultimately give rise to multimedia PC applications. The Audio Connection CD ROM evolved from Sony and Philips's development of the audio compact disc. Their new optical format offered the ability to store very large quantities of digital information on a relatively small and damage- resistant disc. More importantly, their combined work led to a definition for the physical format of CD ROM that has become a de facto industry standard. As with compact audio discs, a major advantage of CD ROM is the fact that once a master disc is made, any number of discs can be quickly and inexpensively pressed from it. The success of compact audio discs led to the creation of a growing number of pressing facilities, which, in turn, has considerably reduced the cost of mass-producing both compact audio and CD ROM discs. The production process involves the following: ■ all data relevant to a given application is assembled ■ the data is indexed and structured for retrieval ■ a CD ROM image in High Sierra format is created from the data files── this step can be done by a service bureau or by using special systems such as CD Publisher ■ a High Sierra data image is then mixed with control information such as error correction code and recorded on a master tape (premastering) ■ the master tape is shipped to a pressing plant, where the master disc is then created (mastering) ■ the desired number of CD ROM discs is pressed from the master disc After production of the master disc, which can cost from $3,000 to $10,000, distribution discs can be pressed for as little as $3 per disc in quantities of 100 or more. If one considers that a single CD ROM disc can hold 660 Mb of data──more than 1500 floppy disks──it is a very cost effective and efficient way to distribute information. The CD ROM drive market also benefits from the success of compact audio discs. Technological gains made in audio hardware lead directly to improvements in CD ROM drive technology. More than ten manufacturers currently make CD ROM drives. Prices, which now average less than $1,000, will continue to drop as volume picks up. The CD ROM Disc The specification Sony and Philips created for CD ROM discs was included in a document called "CD ROM," informally known as the Yellow Book. Though not an officially recognized standard, its universal acceptance by hardware developers has created a stable and well-defined physical environment. The specification states that a CD ROM disc contains a stream of micron- size pits that are read by a laser. The pits, laid out in a spiral, define bit patterns that are grouped into bytes. Bytes are then grouped to form sectors that are each 2352 bytes long: 304 bytes are dedicated to providing system and hardware information (for example, error correction code), and the remaining 2048 bytes contain user data (see Figure 1). CD ROM Drives Most magnetic disks store information on concentric tracks of information. Each track is divided into an equal number of sectors, each of which holds the same amount of information; that is, the largest outer sectors hold the same amount of information as the smallest inner sectors. As sectors get smaller, information is more densely packed. Because the drives spin at a constant rate, because the number of sectors is the same on each track, and because the outer tracks move at a faster rate than the inner tracks, information on the largest sectors must be spaced out far enough so that it takes the same amount of time to read them as it does to read the smallest sectors (see Figure 2☼). This "spacing out" results in the loss of a significant amount of possible data space, but because the drive spins at a constant rate, and track and sector location is specific and constant, the seek times of the read/write heads can be very fast. CD ROM works differently. There is only one continuous spiral track. Each sector is the same physical size, allowing for substantially more data to be stored──there is no wasted sector space (see Figure 3☼). To read data stored in this format, however, the drive must spin at a variable rate. As the head moves towards the inner layers of the spiral the rotational speed increases, and as the head move towards the outer layer the rotational speed decreases. This change in rotational speed is required to maintain a constant linear velocity (CLV), which ensures that the speed with which the outer and inner layers of the spiral travel, relative to the read head, is the same. The drive first seeks to the approximate location of a sector within the spiral, and then must read through the sectors in order to find the exact sector it is looking for. This mechanical characteristic of CD ROM results in relatively slow seek times, generally ranging from .2 to 1 second (200 to 1000 ms). The benefit of the design is a much denser and substantially greater data space (see Figure 4). Good CD ROM layout and design means placing related data close together──this ensures that a good deal of the time an application will seek information that is nearby. Seeks that are close to each other can have a minimum seek time of as little as 10 ms. The Pioneers Most of the pioneer CD ROM projects involved the distribution of text- based information. Aside from building the applications themselves, early CD ROM developers had to deal with a number of issues over which they did not have much control. The major problem was compatibility. Though a physical disc format existed, there was no equivalent standard for logical disc formats. Worse, there was no standard hardware or software PC interface for CD ROM drives, and each project required the writing of system software to access the CD ROM drive from a PC. This resulted in a number of proprietary logical disc formats and interfacing techniques. Users found themselves in the position of having to invest in expensive hardware that was incompatible with any software not specifically designed for it. Developers had to face the probability that proprietary hardware and software would not be widely accepted. The Road to Success It became clear that, if CD ROM was to succeed, the industry would require hardware and software compatibility, as well as the ability to easily interface CD ROM drives to MS-DOS-based PCs. An industry committee of dedicated CD ROM participants that became known as the High Sierra Group (HSG) was formed to iron out the particulars of creating a standard logical disc format. At the same time, Microsoft undertook development of the MS-DOS CD ROM Extensions, software so as to interface CD ROM drives to MS-DOS. The HSG Proposal The High Sierra Group work is based on the Sony/Philips physical format specification. The HSG logical format provides two levels of CD ROM disc definitions. The first level deals with the volume as a whole, and the second deals with the actual files and directories that make up a volume's information base. The full proposal also provides for several levels of operating system support. The lowest level (level 1) supports MS-DOS and similar operating systems, while the higher levels support operating systems like XENIX(R) and VMS. The HSG logical format limits itself to using only the 2048-byte user data segment of the physical sector, which it calls a logical sector (see Figure 5). The proposal anticipates larger sector sizes on future media. The underlying hardware, device driver and system software will accommodate such changes, maintaining compatibility with older formats while taking advantage of newer formats. The HSG format defines specifications for file identifiers, file directories, and subdirectories. File layout is defined in terms of logical blocks, extents, and optional XARs (eXtended Attribute Records). Files containing actual information can be grouped within hierarchical directories, just as they are in XENIX or MS-DOS. Directories, which are themselves files, contain entries that provide the information necessary for locating the entire contents of every file. Directories can contain subdirectories nested to a maximum of eight levels. The HSG specification takes into account the seek-performance limits of CD ROM drives. For example, the proposal does not define an absolute location for directories. This means directories can be placed close to the most commonly used files, which would help keep seek time to a minimum, thereby optimizing read/seek performance. Another optimization in the HSG file structure is the Path Table. Because of the design of CD ROM drives, finding and tracking subdirectories can be a painfully slow process, especially when information is located deep within a hierarchy──for example, within the sixth, seventh or eighth level of subdirectory──or at distant parts of the spiral track itself. The Path Table is an index into the hierarchical directories (see Figure 6). It provides a way of quickly determining the starting sector of any directory at any level in the hierarchy without having to traverse the entire directory path, thus keeping the number of seeks necessary to look up a directory or file to a minimum. The sectors that contain the Path Table will be cached in memory by the MS-DOS CD ROM Extensions, significantly improving seek performance. From there, the system can search the directory to find actual files that it needs. Because searching for directories should be very fast, while searching for files can be very slow, the designer is cautioned against using directories with lots of files in them. Otherwise, an application might need to scan many sectors of a given directory to find a particular file, resulting in seek time penalties. A single CD ROM directory sector can hold about 40 directory entries. The specification also defines Volume Descriptors, which provide information for the entire contents of a CD ROM volume. Volume Descriptors are the only "fixed-location" entities defined by the HSG specification, from which any other information can be traced. Each CD ROM volume must have at least one Volume Descriptor that provides directory and Path Table information. Types of information that can be kept in the Volume Descriptors include the standard ASCII character set (Standard File Structure Volume Descriptor), author names, and creation dates. A specification is also provided for handling multidisc applications, where data exceeds the 660Mb capacity of one volume. The HSG completed its work on May 28, 1986. Since that time, most CD ROM vendors have embraced the group's set of specifications. The High Sierra Groups Initial Members Apple Computer Digital Equipment Corp. Hitachi LaserData Microsoft Corp. Phillips Reference Technologies Inc. Sony Corp. 3M TMS Inc. VideoTools XEBEC The MS-DOS CD ROM Extensions The MS-DOS CD ROM Extensions are designed to allow a user to purchase any CD ROM drive from any manufacturer, to install the drive on any MS-DOS- based personal computer, and to read any publisher's discs that use the HSG format in that drive or in any other CD ROM drive. Microsoft has implemented the Extensions through a terminate-and-stay- resident (TSR) program known as MSCDEX.EXE, which is installed on top of MS-DOS and handles the interfacing of CD ROM Device Drivers to MS-DOS itself. The set of existing device driver commands was enhanced so that they would support the unique characteristics of the CD ROM drives. Microsoft has opted to distribute the MS-DOS CD ROM Extensions through CD ROM drive manufacturers. Each drive manufacturer provides its own device driver(s) and a copy of Microsoft's MSCDEX.EXE program with the CD ROM drive itself, both supplied on a setup diskette. CD ROM application developers need not supply any systems software with their applications. This approach is analagous to the floppy and hard disk market. Software developers can simply make the same assumptions about CD ROM drives that they make about magnetic drives and disks. Once the hardware is in place, the setup program is run by the user and makes the necessary modifications to CONFIG.SYS (to install the device driver) and AUTOEXEC.BAT (to include the call to MSCDEX.EXE). Once the system is rebooted the PC will recognize the newly installed CD ROM drive, and users and application programs can access the information on any CD ROM disc adhering to the HSG specifications. For a technical discussion of the MS-DOS CD ROM Extensions, see the accompanying article MS-DOS CD ROM Extensions: A Standard PC Access Method. Development Issues A number of features unique to the CD ROM environment must be taken into consideration during application design. The first and foremost is that CD ROM is read-only. Most software assumes, for example, that the current disk is always writable; this will create obvious problems if the current drive is a CD ROM drive. And of course, index and search strategies, file layouts and judicious use of directories must be considered. One must also keep in mind that MS-DOS is, in High Sierra format terms, a level 1 operating system──for example, filenames are limited to eight characters plus a three-character extension. The MS-DOS CD ROM Extensions will eventually support most features of the High Sierra format, such as multivolume sets. The HSG specification and the MS-DOS CD ROM Extensions have made the development process substantially easier. Many of the obstacles faced by the pioneer CD ROM developers and marketers have been cleared: The MS-DOS CD ROM Extensions have eliminated most of the systems-level programming that was previously required, and the HSG specification ensures logical disc compatibility. The developer need only concentrate on the application itself. Applications Typical CD ROM applications include parts catalogs, information databases such as those offered by on-line database services, specialized applications like medical diagnoses, and applications requiring the distribution of very large texts, such as an encyclopedia. According to Carl Stork, Microsoft's director of marketing for CD ROM, 150 vertical applications using CD ROM are currently shipping, and the use of the High Sierra format is booming. Microsoft itself has developed what can be called the first general- purpose CD ROM product available to the PC user. Microsoft(R) Bookshelf(TM) offers ten reference works on a single CD ROM disc. Bookshelf is designed to work as either a standalone program or interactively with programs such as word processors, allowing immediate on-line access to the ten reference works while creating or editing a document. Applications will soon include video and audio components. Digital Video Interactive (DVI) is a new technology that is ideally suited for CD ROM; it allows for full-motion video and graphics at the same data rates as those of a CD ROM. Optical media technology has finally hit its stride, and CD ROM is leading the way through its vastly improved drive technology, the High Sierra format specification, and the MS-DOS CD ROM Extensions. Developers with existing applications that lend themselves to CD ROM technology or with applications that would benefit from the CD ROM format now have the entire PC market available to them. And those exciting new applications that CD ROM is ideally suited for can now be brought to this market, offering personal computer users new and added functionality, as well as increased productivity. The combination of desktop personal computers and CD ROM is proving to be a winning hand. Disc compatibility has paved the way for mass market acceptance of CD ROM, which is bound to encourage the creation of a new software market and provide this new technology with a large degree of success. Figure 1: The physical sector is 2352 bytes long; 2048 bytes can contain user data. Type of Information Allocated Space Synchronization Data 12 bytes Header Data 4 bytes User Data 2048 bytes Error Detection Code (EDC) 4 bytes Unused Space 8 bytes Error Correction Code (ECC) 276 bytes Figure 4: Performance and storage characteristics of several types of media. ╓┌─────────────┌────────────┌─────────┌────────┌──────────┌────────────┌─────╖ │ Small │ Large │ │ │ Large │ ▌ │ Winchester │ Optical │ Floppy │ Magnetic │ Winchester │ CD ▌ Media │ Disk │ ROM │ Disk │ Tape │ Disk │ ROM ▌ ─────────────┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀┤ Media ▌ │ │ │ │ │ │ Cost ▌ │ │ │ │ │ 10- │ (in U.S. $) ▌ N/A │ 15-30 │ 1-5 │ 10-20 │ N/A │ 20 │ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Drive ▌ │ │ │ │ │ │ Cost ▌ │ 7,000- │ 200- │ 3,000- │ 10,000- │ 500-│ (in U.S. $) ▌ 500-3,000 │ 100,000 │ 1,500 │ 15,000 │ 150,000 │ 2500│ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Capacity ▌ │ 1,000- │ 0.36- │ │ │ 550 │ │ Small │ Large │ │ │ Large │ ▌ │ Winchester │ Optical │ Floppy │ Magnetic │ Winchester │ CD ▌ Media │ Disk │ ROM │ Disk │ Tape │ Disk │ ROM ▌ ─────────────┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀▀▀▀▀▀▀▀┼▀▀▀▀▀┤ Capacity ▌ │ 1,000- │ 0.36- │ │ │ 550 │ (in Mb) ▌ 5-50 │ 4,000 │ 1.20 │ 30-300 │ 50-4000 │ 680 │ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Media ▌ │ │ │ │ │ │ Size (in) ▌ 5.25 │ 12.0 │ 5.25 │ 10.50 │ 14.0 │ 4.72│ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Access Time ▌ │ 0.03- │ 0.03- │ │ │ 0.40│ (in sec.) ▌ 0.03-0.30 │ 0.40 │ 0.05 │ 1-40 │ 0.01-0.08 │ -1 │ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Density ▌ │ │ │ │ │ 35, │ (bits/in.) ▌ 15,000 │ 35,000 │ 10,000 │ 6,250 │ 15,000 │ 000│ ─────────────┼────────────┼─────────┼────────┼──────────┼────────────┼─────┤ Data Rate ▌ │ │ │ │ │ │ (Kb/sec.) ▌ 625 │ 300 │ 31 │ 500 │ 2,500 │ 150 │ ▀▀▀▀▀▀▀▀▀▀▀▀▀┴────────────┴─────────┴────────┴──────────┴────────────┴─────┘ Figure 5: The HSG proposal focuses only on the 20448-byte user data space of the 2352-byte physical sector defined by Sony and Philips. The logical sector can then be further divided into logical blocks as per the HSG proposed definition. ╔═══════════════════╗ ║ Logical Sector ║ ╚═════════╤═════════╝ │ ╔═══════════════╧═════════════════╗ │ │ ┌────────┬──────────┬─────────────────────────────────┬───────╥───────┐ │ Sync │ Header │ 2048 bytes user data │ EDC ║ ECC │ └────────┴──────────┴─────────────────────────────────┴───────╨───────┘ │ │ ╚═══════════════════════════════════╤═════════════════════════════════╝ │ ╔═════════╧═════════╗ ║ Physical Sector ║ ╚═══════════════════╝ Figure 6: The Path Table provides the means to move directly to the location of the first logical sector of any directory and subdirectory. VOLUME DESCRIPTOR ├──────────────────────────────────────────┐ ▼ ▼ ROOT ╔═══╗ ┌─────┬─────┬──┴──┬─────┬─────┐ ║ ║ │ ┌─────┬─────┬─────┬─────┬─────┬───────────────────────║ P ║ │ │ │ │ │ │ │ │ │ │ │ │ ║ A ║ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ▼ ║ T ║ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ║ H ║ │ A │ │ B │ │ C │ │ D │ │ E │ │ F │ ║ ║ └┬─┬┤ └───┘ └───┘ └───┘ └┬─┬┤ └───┘ ║ T ║ │ │└──────┐ │ │└──────┐ ║ A ║ ▼ ▼ ▼ ▼ ▼ ▼ ║ B ║ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ┌───┐ ║ L ║ │ G │ │ H │ │ I │ │ J │ │ K │ │ L │ ║ E ║ └───┘ └───┘ └───┘ └───┘ └───┘ └───┘ ║ ║ ║ ║ │ │ │ └─────┴─────┴─────────────────────║ ║ └─────┴─────┴─────────────────────────────────────────────╚═══╝ ████████████████████████████████████████████████████████████████████████████ MS-DOS CD ROM Extensions: A Standard PC Access Method Tony Rizzo Developers of CD ROM applications need not concern themselves with the details of interfacing CD ROM drives to the MS-DOS environment. Microsoft(R), working closely with CD ROM drive manufacturers, has developed the software necessary to accomplish this. The MS-DOS CD ROM Extensions, in conjunction with the High Sierra logical disc format, free developers to concentrate solely on their applications and eliminates dependence on any particular manufacturer's drive technology. The MS-DOS CD ROM Extensions consist of a device driver for a CD ROM and a RAM-resident program called MSCDEX.EXE that interfaces with MS-DOS. Together they provide an interesting solution to some rather thorny MS-DOS/CD ROM interfacing problems. The Dilemma CD ROM device drivers are of concern primarily to manufacturers of CD ROM drives, who write and package them with their products in accordance with Microsoft's "CD ROM Device Driver Specification." However, an understanding of how these drivers work will benefit CD ROM application developers. Unlike conventional hard and floppy disks, CD ROM discs are read-only, with file structures that can occupy up to 660Mb of data space, part of which might be audio or video information. There is no File Allocation Table (FAT) as there is on a normal MS-DOS file system, and there is no need to dynamically track disc space for allocating data. Furthermore, CD ROM drives cannot be accessed via the standard MS-DOS Interrupt 25h/Interrupt 26h (read/write disk sector) mechanism, and they cannot be looked at by the operating system at a physical level because they are not in MS-DOS file format. Files and directories are not organized as MS-DOS expects them, which is why CHKDSK, FORMAT, and other MS-DOS utilities will not work on CD ROMs. MS-DOS interfaces to standard magnetic media through block device drivers that deal with disk drives at the hardware level and transfer blocks of data in multiples of a given sector size. Also, block drivers are able to support multiple devices (for example, two or more hard disks at once), as well as removable media. MS-DOS makes some assumptions about block device drivers. It assumes there is a FAT, and it will attempt to read one when the device driver is initialized. MS-DOS also assumes that it will need to assign drive letters to each unit supported by a particular device driver. Neither of these assumptions is relevant to CD ROM drives, nor can MS-DOS deal with CD ROM's 660Mb data space, since MS-DOS can support a maximum disk space of only 32Mb. MS-DOS also accepts a character device driver, which deals with information only one byte at a time, rather than in blocks. Character device drivers are usually associated with serial I/O devices such as modems, printers, the keyboard, and the video monitor. Character device drivers are assigned a unique name instead of drive letters and do not support multiple units or removable media. Since MS-DOS assumes nothing about them, character device drivers are useful models for CD ROM drivers. What CD ROM required was a hybrid device driver closely modeled on character device drivers but supporting, in block driver fashion, multiple units and removable media. In fact, CD ROM device drivers are character device drivers that have additional fields to support drive letters and multiple units. The standard MS-DOS character device driver command set consists of commands to control input and output and the device itself, as well as to query the status of the device. The CD ROM device driver command set consists of a subset of these commands, plus a group of new commands that pertain only to CD ROM drivers (see Figure 1). CD ROM Device Drivers Standard device drivers are simply EXE files that do not contain Program Segment Prefixes. Because they have no PSPs, they originate at location 0h rather than 100h. A device driver contains a header (see Figure 2), which identifies the file as a device driver, defines the device's particular attributes, and provides information that MS-DOS will need when the driver is initialized. There is also a name/unit field that will hold a character device name or, if the device is a block device, the number of units to be supported. Commands are passed to standard device drivers from MS-DOS via request headers (see Figure 3), which contain system information such as the length of the request header, the command code itself (from Figure 1) and any data necessary for the device's operation, as well as a field in which the driver can return a status code. If the device is a block device, there will also be a subunit code to tell the device driver which of its supported units is being requested. When a CD ROM device driver is first initialized by MS-DOS, some interesting things happen to maintain its character device driver disguise while it is operating as a block device driver. This makes it necessary for CD ROM device headers to have three additional fields that are not found in standard character or block device headers (see Figure 4). When MS-DOS installs device drivers, it gets its information from the DEVICE=xxx entries in the CONFIG.SYS field. A typical line in a CD ROM device entry in CONFIG.SYS might be: DEVICE=C:\DEV.SYS\HITACHI.SYS /D:CDROM1 /N:3 When MS-DOS reads this line, it issues an INIT command, via a request header, to HITACHI.SYS (in subdirectory C:\DEV.SYS). The CD ROM device driver responds in familiar character driver fashion by supplying its strategy and interrupt routine offsets and its attributes (see Figure 5), obtaining its unique device name, in this case CDROM1, by returning a 0 to the INIT's request for number of units (which MS-DOS expects since it is supposed to be a character device driver). The CD ROM device driver then returns a status code (see Figure 6) that prevents MS-DOS from wanting to read the FAT or trying to supply drive letters. Having accomplished this, the CD ROM driver next determines from the CONFIG.SYS entry the number of units it will support (3 in this case) and fills the Number of Units field in its device header, which MS-DOS knows nothing about. Next, the driver returns control to MS-DOS, which goes on to read whatever else might be in CONFIG.SYS and then executes AUTOEXEC.BAT, if it exists. At this point, the CD ROM device driver has successfully fooled MS-DOS into thinking it's a character device driver, and it has almost set itself up as a block driver. Once initialized, MS-DOS itself will never again deal directly with the CD ROM driver. MSCDEX.EXE Along with the CD ROM drive itself, drive manufacturers are planning to include the CD ROM device driver and MSCDEX.EXE, a special terminate-and- stay-resident (TSR) program that is really the key to the CD ROM interfacing. MSCDEX.EXE is typically loaded from AUTOEXEC.BAT with a command line such as: C:\MSCDEX.EXE /D:CDROM1 /D:CDROM2 /M:20 /V The command line must contain the name of any CD ROM device driver that is listed in CONFIG.SYS. MSCDEX.EXE goes through the following procedure for each of the identified device drivers: ■ issues a DEVICE OPEN call to MS-DOS using the device driver's name and receives from MS-DOS a handle for the device driver ■ executes a DOS IOCTL call to obtain the device header address of the CD ROM device driver ■ determines the number of subunits that are to be supported by the driver and assigns legal drive letters for each subunit ■ after completing this process for every listed driver, terminates but remains resident in background mode The MSCDEX.EXE command line shown above also contains a number of other switches. /M: tells MSCDEX.EXE how many sector buffers it should allocate for its own use (the default value is four sectors per drive). MSCDEX.EXE uses the sector buffers to cache sectors from the Path Table, directory sectors, and file data sectors. Allowing it to cache more directory information reduces its need to reread directories from the disc and speeds things up. /V (for Verbose) provides additional start-up information. Finally, there is an /L switch that allows manual specification of a drive letter assignment. The CD ROM Interface At this point, the CD ROM drivers and drives are set up, but we still have another problem. MS-DOS no longer knows anything about them, so how is it able to access them? Again, MSCDEX.EXE fools the operating system, this time using several components of MS-DOS in a unique way. MS-DOS Version 3.1 and later has a number of built-in networking features: when MS-DOS sees a request for network drives, it sends the request to the network redirector, which passes it on to the networking software. MSCDEX.EXE designates the drive letters it assigns to its device drivers as network drives. Whenever MS-DOS receives a CD ROM drive request, it thinks it's a request for a network drive and forwards the request to the redirector. MSCDEX.EXE remains active in the background intercepting these requests, processing those that are really requests for a CD ROM drive, and passing on the legitimate network requests to the redirector. MS-DOS issues requests to the network at a virtual file level. MSCDEX.EXE converts these virtual file level requests into physical sector requests that can be understood by the CD ROM device driver. For example, a virtual file level CD ROM request going to the network from MS-DOS might read: OPEN "\DIRECT1\SUBDIR2\FILEA" READ x bytes at FILEA offset y MSCDEX.EXE intercepts the request, analyzes it, determines that it's a CD ROM request, translates it to READ sector z and sends the appropriate command codes to the CD ROM driver. Operating at a file level, rather than a physical level, allows certain limits of MS- DOS──for example the 32Mb file limit──to be circumvented. Figure 7 illustrates the entire process. The end result: MS-DOS thinks that it has available to it a very large network drive, which it can easily deal with. The MS-DOS CD ROM Extensions (MSCDEX.EXE plus the CD ROM device drivers) thus enable a developer to create a CD ROM-based application that will run on any CD ROM drive attached to any PC. The Commands Now let's take a look at several of the commands supported by CD ROM device drivers and see how they compare to various standard device driver commands. Figure 8 shows the standard READ/WRITE command control block. Note that this particular control block is used for command codes 3, 4, 8, 9, 12, and 16 (refer to Figure 1). For CD ROM, only command codes 3 and 12 (IOCTL INPUT and IOCTL OUTPUT) are valid. IOCTL calls will permit MSCDEX.EXE and application programs to send control strings to the device driver that consist of CD ROM-specific commands or requests for information about the status of the device drivers and the CD ROM drive. Figure 9 lists the CD ROM IOCTL codes for command codes 3 and 12. The transfer address for an IOCTL call points to a control block that is used for communication with the driver. The first byte of the control block serves as the command code for the IOCTL call. IOCTL INPUT calls request and receive information from the device driver about the device. IOCTL OUTPUT calls instruct the device driver to open or close the device door, eject a disc, and so on; in this case, the device responds to the instruction but returns no information. Figure 10 shows the CD ROM READ LONG command (command code 128). This differs in a number of ways from the standard READ command control block (see Figure 8). First, the media descriptor byte, which is set to 0 for IOCTL calls and ignored, becomes the addressing mode byte. This byte will almost always be set to 0, or High Sierra Group (HSG) addressing mode, that is, long address values are read as logical block numbers, as per the HSG format. All CD ROM drives will support HSG addressing mode, which can be considered the standard or default mode of operation. Interleaving is not yet supported, though it will play a role in file design in later releases of the software. The standard READ command pointer to a requested volume ID is replaced with a data read mode byte in the READ LONG command. This will usually be set to 0, for cooked mode, in which 2048 bytes are read using error detection and correction. With the other option──raw mode, or Red Book addressing mode (mode byte set to 1)──the entire 2352 bytes are read, including the error detection and correction code. Any drives and drivers that support raw mode can invoke it by setting this byte to 1. Perhaps the major difference between READ and READ LONG is that the byte/sector count field becomes the number of sectors to read, and the starting sector number is expanded to double-word length. This means it can now reference 4 gigasectors (or more than 8 terabytes). This is how the CD ROM's larger capacity is handled. At the very least, CD ROM device drivers will be able to return proper values for IOCTL calls and will be able to read cooked mode 1 data sectors using HSG addressing mode. READ LONG PREFETCH attempts to anticipate where the next sector reads will take place. It is a hint from MSCDEX.EXE to the device driver that the sectors requested by this command will most likely be needed; it will attempt to cache the requested sectors or at least position the read head within the area they occupy. This minimizes the time that is required to seek the next location when reading data. SEEK positions the read head at specific locations on a disc. All CD ROM device drivers are required to support DEVICE OPEN and DEVICE CLOSE and are included for DOS compatibility. For CD ROM drives with audio capability, PLAY would be used to play the audio section of a disc starting at a given location, and STOP PLAY would end audio playing. Conclusion In the final analysis, CD ROM device drivers and their more familiar siblings have more similarities than differences between them. They are initialized in the same manner, they are accessed in basically the same manner (at least to all appearances), and most of the systems issues are handled the same way for each. For example, the ES:BX registers are used by both MS-DOS and MSCDEX.EXE to pass the far address of the request header when the driver's strategy routine is called. Once the request header has been passed to the strategy routine, it is dispatched to the appropriate subroutine. See Figure 11 for sample CD ROM driver dispatch code. The combination of the front-end MSCDEX.EXE program and the low-level CD ROM device drivers has the potential to make CD ROM technology an integral part of the familiar MS-DOS personal computing world. All that remains is for developers to begin building applications to take advantage of this new environment. Figure 1: Device Driver Command Codes ╓┌─────────┌──────────────────────────────┌──────────────────────────────────╖ Supported Command CD ROM Code Name Command 0 INIT Yes 1 MEDIA CHECK (block devices) No 2 BUILD BPB (block devices) No 3 IOCTL INPUT Yes 4 INPUT (read) No 5 NONDESTRUCTIVE INPUT NO WAIT No 6 INPUT STATUS No 7 INPUT FLUSH Yes 8 OUTPUT (write) No Supported Command CD ROM Code Name Command 8 OUTPUT (write) No 9 OUTPUT WITH VERIFY No 10 OUTPUT STATUS No 11 OUTPUT FLUSH No 12 IOCTL OUTPUT Yes 13 DEVICE OPEN Yes 14 DEVICE CLOSE Yes 15 REMOVABLE MEDIA No 16 OUTPUT UNTIL BUSY No 128 READ LONG Yes 129 Reserved 130 READ LONG PREFETCH Yes 131 SEEK Yes 132 PLAY Yes 133 STOP PLAY Yes Commands 0, 3, 7, 12, 13, 14, 128, 130, and 131 are the standard codes necessary to write CD ROM device drivers. Commands 132 and 133 are used to write extended CD ROM device dreivers that support audio. Commands 1, 2, 4, 5, 6, 8, 9, 10, 11, 15, 16, and 129 are not supported CD ROM commands and will return an error code for unknown command. Figure 2: Standard Device Header Format DevHdr DD -1 ; Ptr to next driver in file ; or -1 if last driver DW ? ; Device attributes DW ? ; Device strategy entry point DW ? ; Device interrupt entry point DB dup 8 (?) ; Character device name field ; or drive letter Figure 3: Request Header Format ReqHdr DB ? ; Length in bytes of request header DB ? ; Subunit code for minor devices DB ? ; Command code field DW ? ; Status DB dup 8 (?) ; Reserved Figure 4: CD ROM Device Header Format DevHdr DD -1 ; Ptr to next driver in file ; or -1 if last driver DW ? ; Device attributes DW ? ; Device strategy entry point DW ? ; Device interrupt entry point DB dup 8 (?) ; 8-byte character device name ; field or drive letter DW 0 ; Reserved DB 0 ; Drive Letter DB ? ; Number of Units Figure 5: Sample Device Driver Including Attributes Field Definitions DevHdr DD -1 ; Ptr to next driver in file ; or -1 if last driver DW 0c800h ; Device attributes DW STRATEGY ; Device strategy entry point DW DEVINT ; Device interrupt entry pint DB `HSG-CD1' ; 8-byte character device ; name field DW 0 ; Reserved (must be zero) DB 0 ; Drive Letter (must be zero) DB 1 ; Number of Units (one or more) Device Attributes For CD ROM device drivers, the device attributes field is 0c800h. Bit 15 1 Character Device Bit 14 1 IOCTL supported Bit 13 0 Output until busy Bit 12 0 Reserved Bit 11 1 OPEN/CLOSE/RM supported Bits 10-4 0 Reserved Bit 3 0 Dev is CLOCK Bit 2 0 Dev is NUL Bit 1 0 Dev is STO (standard output) Bit 0 0 Dev is STI (standard input) Figure 6: Status Word ╔══════════════════════════════════════════════════════════════════════════╗ ║┌──────┬────┬────────────────────────┬───┬───┬───────────────────────────┐║ ║│ Bits │ 15 │ 14 13 12 11 10 │ 9 │ 8 │ 7 6 5 4 3 2 1 0 │║ ║└──────┼────┼────────────────────────┼───┼───┼───────────────────────────┘║ ║ │ │ │ B │ D │ ║ ║ │ E │ Reserved │ U │ O │ Error Code (if bit 15 on) ║ ║ │ R │ │ S │ N │ ║ ║ │ R │ │ Y │ E │ ║ ╚══════════════════════════════════════════════════════════════════════════╝ The status word is zero on entry. Bit 15 Error bit Set by the device driver if an error is detected or if an invalid request is made to the driver. The low 8 bits indicate the error code. Bits 14-10 Reserved Bit 9 Busy bit Set by the device driver when the device is in play mode. All requsets of the physical device when this bit is set will fail unless play mode is interrputed (using the STOP PLAY function) and a request is then made. Monitoring this bit will tell when play mode is complete. Bit 8 Done bit Set by the device when an operation is complete. Bits 7-0 Error code 0 - Write-protect violation 1 - Unknown unit 2 - Device not ready 3 - Unknown command 4 - CRC error 5 - Bad drive request stricture length 6 - Seek error 7 - Unknown media 8 - Sector not found 9 - Printer out of paper A - Write fault B - Read fault C - General failure D - Reserved E - Reserved F - Invalid disk change Figure 7: Summary of CD ROM/MSCDEX.EXE Initialized Events MS-DOS MSCDEX │ │ ┌────────────┐ └────Init─────►│ Device │█ │ Driver │█ └────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ─────────────────────────────────────────────────────────────────────────── MS-DOS ◄────────────Open─────────────── MSCDEX ┌────────────┐ │ Device │█ │ Driver │█ └────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ─────────────────────────────────────────────────────────────────────────── ◄─────────────────────────────── MS-DOS MSCDEX ───────────────────────────────► │ Ioctl ┌────────────┐ │ └────────────►│ Device │█ └───────────────┤ Driver │█ └────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ─────────────────────────────────────────────────────────────────────────── Network Non-CD ROM MS-DOS ──────► FILE LEVEL ──────► MSCDEX ──────► Calls Passed Request On │ All CD ROM Calls ┌──────────────┐ │ │ │ CD ROM │█ │ │ │ Device │█◄┘ │ │ Driver │█ ───┘ └──────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Figure 8: Standard Read/Write Command Format Read or Write Command code = 3, 4, 8, 9, 12, 16 CmdName DB (dup 13 0) ; Request Header DB ? ; Media descriptor byte from BPB DD ? ; Transfer address DW ? ; Byte/Sector Count DW ? ; Starting sector number ; (ignored on character devices) DD ? ; ptr to requested volume ID if ; error 0Fh Figure 9: CD ROM IOCTL IMPUT and IOCTL OUTPUT, Command Code = 3 or 12 Note: CmdNAme below could be IOCTLI or IOCTLO, for example. CmdName DB (dup 13 0) ; Request Header DB 0 ; Media descriptor byte from ; BPB is set to zero DD ? ; Transfer address DW ? ; Byte/Sector Count DW 0 ; Starting sector number ; is set to zero DD 0 ; ptr to requested volume ID ; if error 0Fh-set to zero The transfer address points to a control block that is used to communicate with the device driver. The first byte of the control determines the request that is being made. ╓┌─────────────────────┌─────────┌───────────────────────────────────────────╖ Code Function For IOCTL INPUT 0 Return address of Device Header (Command code = 3) 1 Location of Head 2 Reserved 3 Error Statistics 4 Audio Channel Info 5 Read Drive Bytes 6 Device Status 7 Return Sector Size 8 Return Volume Size Code Function 8 Return Volume Size 9 Media Changed 10 Audio Disc Info 11 Audio Track Info 12 Audio Q-Channel Info 13 Audio Sub-Channel Info 14-255 Reserved For IOCTL OUTPUT 0 Eject Disc (Command code = 12) 1 Lock/Unlock Door 2 Reset Drive 3 Audio Channel Control 4 Write Device Control String 5-255 Reserved Figure 10: READ LONG, Command Code = 128 ReadL DB (dup 13 0) ; Request Header DB ? ; Addressing mode DD ? ; Transfer address DW ? ; Number of sectors to read DD ? ; Starting sector number DB ? ; Data read mode DB ? ; Interleave size DB ? ; Interleave skip factor Figure 11: Sample Dispatch Code for a CD ROM Device Driver ; ***Offsets for cmd buffer *** public drvtbl drvtbl label word ; Standard DOS device driver ; functions dw error ; *INIT (init. handled through ; far jump dw error ; MEDIA CHECK dw error ; GET BPB dw ioctl$i ; *IOCTL INPUT dw error ; INPUT dw error ; NON-DESTRUCTIVE INPUT dw error ; INPUT STATUS dw error ; *INPUT FLUSH (Nothing for this ; controller to do) dw error ; OUTPUT dw error ; OUTPUT WITH VERIFY dw error ; OUPTUT STATUS dw error ; OUTPUT FLUSH dw ioctl$o ; *IOCTL OUTPUT dw devopen ; *DEVICE OPEN dw devclose ; *DEVICE CLOSE dw error ; REMOVABLE MEDIA dw error ; OUTPUT UNITL BUSY public drvtbl2 drvtbl2 label word ; Extended CD ROM device driver ; functions dw read$ ; *READ LONG dw error ; reserved dw pfetch$ ; *READ LONG PREFETCH dw seek$ ; *SEEK dw play$ ; *PLAY (for extended drivers) dw stop$ ; *STOP (for extended drivers) dw error ; WRITE LONG dw error ; WRITE LONG NON-BLOCKING dw error ; WRITE LONG VERIFY public ioi_tbl ioi_tbl label word ; IOCTL INPUT subfunctions dw ret_addr ; IOI_ret_addr dw loc_head ; IOI_loc_head dw error ; IOI_ioquery dw error ; IOI_err_status dw error ; IOI_audio_info dw rd_drv ; IOI_rd_drv_bytes dw dev_stat ; IOI_dev_status dw sect_size ; IOI_ret_sectsize dw vol_size ; IOI_ret_volsize dw media_changed ; IOI_media_changed dw audio_diskinfo ; IOI_audio_diskinfo dw audio_trackinfo ; IOI_audio_trackinfo dw audio_qchaninfo ; IOI_audio_qchaninfo dw error ; IOI_audio_subinfo dw upc_code ; IOI_upc_code public ioo_tbl ioo_tbl label word ; IOCTL OUTPUT sub-functions dw eject ; IOO_eject_disc dw lock ; IOO_lock_door dw reset_drv ; IOO_reset_drv dw error ; IOO_set_audio_param dw wr_drv ; IOO_wr_drv_bytes PAGE ; *** Device strategy routine *** public strat strat proc far mov word ptr cs:[reqhdr],bx mov word ptr cs:[reqhdr+2],es ret strat endp ; *** Device interrupt handler *** public devint devint proc far ; save registers push ax ; (1) push bx ; (2) push cx ; (3) push dx ; (4) push si ; (5) push di ; (6) push ds ; (7) push es ; (8) lds bx,cs:[reqhdr] ; dx:bx -> req header mov al,ds:rqh_unit[bx] ; (al)= drive number mov cs:drive_num,al ; Save drive number mov si,offset drvtbl ; Assume it will be ; normal mov al,ds:rqh_cmd[bx] ; Get command in al cmp al,DVRQ_NCMD_MAX ; Check if normal ; command jbe normal cmp al,DVRQ_ECMD_MIN ; Extended command? jae ext ; Yes jmp error ; Maybe ext: cmp al,DVRQ__EMCD_MAX ; Extended command? jbe ext2 ; Yes jmp error ; No ext2: sub al,DVRQ_ECMD_MIN ; Convert to offset ; into table mov si,offset drvtbl2 ; get address of ; table normal: cbw shl ax,1 ; Change to index ; into command table add si,ax cmp ax,0 ; Init. command? jz init_this_dvd ; Yes ; No init_already_done: jmp word ptr cs:[si] ; dispatch to command init_this_dvd: cmp cs:init_dvd,0 ; Device driver init. ; yet? mov cs:init_dvt,1 ; (Set the flag) jnz init_already_done ; Yes ; No jmp far ptr doint : Perform the ; initialization ; *** IOCTL OUTPUT command handler *** public ioctl$o ioctl$o: mov si,offset ioo_tbl mov dl,IOO_cmd_max jmp short ioctl ; *** IOCTL INPUT command handler *** public ioctl$i ioctl$i: mov si,offset ioi_tbl mov dl,IOI_cmd_max jmp short ioctl ioctl: les di,ds:ioctl_xfer[bx] mov al,es:[di] inc di ; Skip past command ; code cmp al,dl jbe iolegal jmp error iolegal: cbw shl ax,1 add si,ax jmp word ptr cs:[si] ; Dispatch to command ∙ ∙ ∙ ████████████████████████████████████████████████████████████████████████████ Microsoft QuickBASIC: Everyone's First PC Language Gets Better Dan Mick☼ Microsoft's implementation of nearly everyone's first language──BASIC──has matured into quite a productive, flexible tool for professional program development with Version 3.00 of Microsoft QuickBASIC (hereafter referred to as QuickBASIC). Modular coding structures such as SUB...END SUB, DO WHILE...LOOP and DO...WHILE, SELECT CASE, and others have moved venerable old BASIC closer to traditionally more structured languages such as C and Pascal/Modula-2. Still, it seems that no matter how powerful the programming language, when programming on microcomputers, the programmer has a real need for low- level routines to access such things as MS-DOS(R) interrupts (service calls) or to access the keyboard or CRT directly. This article will explore two methods of dealing with custom, low-level routines that use QuickBASIC's INT86 function as well as a custom assembly language function called DSEARCH. Why Low-Level Routines? Why do we need to bypass QuickBASIC's normal set of functions? The DSEARCH routine addresses a common problem I've encountered when using various versions of Microsoft(R) BASIC──that of getting a disk directory and providing it to the user for file choice. There is no good way in Microsoft BASIC to accomplish this task. A common trick is to use the rather inflexible FILES statement to provide a directory of a given disk. This will at least let the user see which files are present and will let the user retype the filename as input to the program. But there has never been a good way to get the filenames into BASIC variables, so that a more user-friendly interface could be created by presenting a menu or a window. As with any limitation in a language implementation, clever programmers have found ways around this one. Some have read the filenames character- by-character directly from the screen after a FILES statement. Another clever trick is to spawn a DOS DIR command with the output redirected to a temporary file. Both these solutions are rather inelegant and time- consuming. The former displays the FILES output momentarily on screen before the program's custom file-choice interface is presented. Clever programmers have however avoided this by setting foreground color to background color. Using DIR output redirection involves such problems as obtaining a truly unique filename and space on the disk for the redirected output, which complicates the writing of really "bulletproof" code that will run on many different hardware configurations. Clearly, a better solution is preferable. There is a better solution in the form of DSEARCH, which is an assembly language subroutine I've written in Microsoft Macro Assembler. This allows a QuickBASIC program to specify a search path and an MS-DOS file attribute to search for and then either return a count of the files matching the specifications or fill a preallocated string array with the actual filenames (see Figure 1). I've tried to make the routine as flexible as possible while striking a balance between doing too little in assembler (making the support code too complex) and doing too much (making the assembler code lengthy and labyrinthine). Figures 2 and 3 provide two QuickBASIC programs that use DSEARCH: DSEARCHT.BAS is a test driver that lets you play with DSEARCH's features, while BROWSE.BAS is a hard-disk file management application that allows the user to traverse the directory to browse and delete files. Interface Interfacing assembly language to QuickBASIC is much like interfacing to other Microsoft languages. QuickBASIC uses the Intel-recommended method of procedure calling: Parameters are passed on the stack, and the called procedure expects a fixed number of parameters, which it cleans from the stack before returning to the calling procedure. This is somewhat different from the normal C calling procedure in which parameters are removed by the calling program, allowing a variable number of parameters to be easily passed. Rather, it is much like the pascal sequence available in Microsoft Pascal or like using the pascal keyword in Microsoft C. Preserving registers is another issue. The program must preserve the values in DS, SS, and BP, although all other registers may be cleared, including ES (the QuickBASIC code always does a PUSH DS/POP ES after the CALL). Usually, you won't need to set up your own stack, but if you do, keep in mind that you must obtain the parameters from the stack passed by QuickBASIC, and you probably should do so before setting up a local stack. Local data may be held in the code segment in typical COM-file fashion. However, it's nice to be able to put it in BASIC's data segment, especially if you will need to access things in BASIC's data segment anyway (as we will in the DSEARCH routine). To do this, the recommended procedure is to allocate a segment named DATA with combine type DATA, use a GROUP directive to put it in a group named DGROUP (QuickBASIC's reserved group name for all its data segments), and then use an ASSUME DS:DGROUP directive so that data references are assembled with the correct offset. This is accomplished as shown in DSEARCH.ASM (see Figure 1). A caveat here: If you want to obtain the address of a parameter in your data area, it's best to use "LEA reg, item" rather than "MOV reg, OFFSET item". Usually these two are equivalent, but when using groups, the offset operator returns the offset from the beginning of the current segment, rather than the offset from the beginning of the group, which is your ticket to disaster here. The quick fix is to use LEA. You may also be able to use "MOV reg, OFFSET DGROUP:item" instead. It's easy to forget the group prefix, however, and you may find it easier to simply not use MOV with an OFFSET. The parameters are set up by BASIC on the stack, with the parameter furthest to the left pushed first and the one furthest to the right pushed last. A FAR CALL is then issued to the user subroutine. Typically, the user subroutine saves the BP (base pointer) register on the stack, loads BP with the current SP (stack pointer) value, and uses BP to access the parameters on the stack. When the routine finishes, it executes a far RET instruction to return to the caller. The specifies the number of bytes to clean off the stack after obtaining the return address from the stack. This enables the subroutine to leave the stack as it was just before the push/call sequence. Parameters are most often passed by reference from QuickBASIC, though later versions of the language allow several new calling methods, including "pass by value" and "pass by long pointer reference." Not much is said about these in the QuickBASIC documentation though, and we won't discuss them here. The normal CALL (parameter1, parameter2, ..., parametern) statement is the one used for DSEARCH. Data Types The CALL statement pushes the address of each parameter, starting with the one furthest to the left, as a 16-bit offset in QuickBASIC's data segment. Since the SS, ES, and DS registers are set up to this segment by the call, 16 bits is enough of an address for most items. Large numeric array support was added to QuickBASIC 2.0 so that numeric arrays dimensioned dynamically would reside outside the default data segment. To use large numeric arrays, some method of passing a double word address would be required. We're going to use only integers, a string, and a string array. The passing of the integers is the most straightforward. The word address is simply the address containing the parameter, so that you may access the variable by simply getting its address from the stack. Strings are a bit more complex. The address passed is the address of a string descriptor, which consists of two words. The first word is the length of the string, while the second is the address (in QuickBASIC's data segment) of the actual string data. Because of the dynamic string space management BASIC uses, a subroutine should not change the length of a string. This may seem quite restrictive at first if you need to pass back a variable-length item. You can, however, get around this by setting the string to a fixed, maximum length and then replacing the characters as needed. This leaves fixed-length strings upon return, but if you've padded them with blanks to start, you can just trim off the trailing blanks in QuickBASIC, which knows how to deal with changing string lengths better than a programmer can in assembler. This leaves arrays. The normal syntax for passing arrays to QuickBASIC subroutines is to put the array name in the CALL statement. This is more flexible than most calling sequences, since the array may be of any dimensionality and size. However, the array descriptor that is pushed onto the stack is quite large and complex and is not documented in the QuickBASIC manual, so it's better to pass the base address of the array. Also, since QuickBASIC stores the arrays contiguously in a known order and since the called routine must know the size and type of the array anyway, it's much easier on the assembler code and uses less of the stack to pass arrays by base address. This is how C always passes arrays, so the procedure should be quite familiar to C programmers. The STRUC Directive MASM is much more structured than many microcomputer assemblers. It performs type checking on operands in sometimes surprising ways, and its data structure definition statements go far beyond the typical define storage directives in many assemblers. One that is especially useful for our purposes here is the STRUC directive, which allows you easy access to the stack structure. STRUC is similar to the RECORD statement in Pascal or the struct definition in C. The statements put inside a STRUC/END block are the same as normal data definition statements (those that simply reserve space, such as DB, DW, and DD──not those that initialize that space) with labels for each structure field. Those labels can then be used later in the assembly to access areas of memory in record style──that is, you can load a register with the base address of an area of memory that is laid out like the structure definition and access it with the symbolic names in the structure. As you would expect, this makes the code much more readable, and in assembly language, every little bit of readability is crucial. You can use a STRUC directive to describe the stack on entry to the routine, enabling you to refer to the parameters by name instead of by offsets from the BP register. This makes the actual assembler code much clearer. Also, this will help with the return statement, which must clean up the stack, requiring you to code a number based on how many and what type of parameters were passed. Here's a simple STRUC as an example: small struc firstword dw ? secondword dw ? small ends When this structure is assembled, you will see offsets listed by the labels in the structure, offset from 0. The member firstword will have an offset of 0, and secondword will have an offset of 2. Now, if you want to refer to the members of an area of memory consisting of two words, as in this example, you can use an instruction like the following (assuming that BX contains the address of the base of that area): mov si,secondword[bx] mov si,secondword.bx mov si,[bx+secondword] mov si,bx[secondword] Note that the expression evaluator is fairly flexible. I prefer the first syntax, since it keeps the components in their most natural order: "Move into si from secondword (an area addressed by bx)." In DSEARCH, a structure just like this one describes the string descriptor structure, which is of the same form as in the example above. The structure is called strdes and has two fields, len and strptr (these refer to the string length and the address of the string in QuickBASIC's data segment). In the move_filename routine, these are used with the statement mov di,es:strptr[di] which loads di with the word contained at es:di+2, but does it in a way that makes it much more obvious why this is done. Similarly, a structure called stk describes the stack on entry. This is even more convenient, because it lets you access the parameters without having to worry about the stack structure. This time, I used the BP register, which tells the processor I want to get at the stack segment automatically (which is why this is the logical choice for the structure base register). Note the places reserved for the return address that QuickBASIC pushes in the CALL process, as well as the place reserved for the BP register, which is saved just before being loaded from SP. After the MOV BP,SP instruction, I can do whatever else locally with the stack I want, because I've established the base of that area of the stack described in the structure stk in BP. Now I can PUSH registers at my leisure and still get at the path$ parameter simply by referencing path$[bp]. Note also the equate following the structure: popval=(offset path$) + (size path$) - (offset arrofs) This calculates the parameter that will be required for the RET statement at the end of the routine, so I don't have to worry about it. I'm of the school that believes the tools you use should work as hard for you as possible, correcting as many of your silly mistakes as they can. For instance, I'm awful at calculating offsets, but if stack manipulation isn't just so, the machine will crash horribly, possibly losing some work. (Of course, it's good to take a very disaster-minded approach when doing custom assembly language routines──saving everything before you test and making sure your RAMdisk is backed up──but that's another story.) Using structures and address calculations can prevent some of the really disastrous careless errors in your coding. I use a version of this same stack structure every time I write an assembler routine. It means that all I have to do is change the number and names of the parameters, and the structure and popval will take care of the rest. Parameters to DSEARCH Let's examine the particular syntax of the DSEARCH call and take a closer look at parameter passing. DSEARCH will take the following parameters: a search path string, a DOS file attribute (see MS-DOS documentation or the comments in DSEARCH.ASM for a full description of the file attribute settings), a parameter to hold the number of files found, one to indicate the method of file searching based on the attribute, and one to carry the address of the string array in which the filenames will be stored. Why all these parameters? I began by imagining a simple routine just to get the filenames and pass them back in an array. However, string arrays use up a fair amount of space: there's the 4-byte overhead for each element, plus the length of the string, plus another word of overhead in the string space called the back pointer, and finally 1 byte per string element. Since you can't change the length of a string, I needed to allocate 12 spaces (8 filename characters, a period, and 3 extension characters) to each string. This made a total of at least 18 bytes (12 + 4 + 2) for each filename. Also, since I imagined using DSEARCH only once in a program, that space might well be wasted once the filename procedure was finished. Clearly, a dynamic allocation would be better. QuickBASIC came to the rescue here. Starting with QuickBASIC 2.0, dynamic arrays were added to the language. With a dynamic string array, you can redimension the array whenever you need it (starting from zero and using as many names as required) and then discard the names once the user has used the directory information. But how do you determine the number of array positions you need? I'd thought about using a find first/find next loop in DSEARCH from the start. It occurred to me that I would have to have a similar routine to count the filenames (so that the QuickBASIC code could allocate the array) and then call a routine that actually filled the array. I decided that because the code for the first routine would be similar to the code for the second, I could make the first serve double duty by setting a flag based on a parameter to indicate whether or not I was actually filling the array. Since I needed an output parameter for the count of filenames, I saved myself a bit of coding and used that same parameter for the fill/don't fill indicator. This has an added advantage: since I'm making one DSEARCH call to get the count, I can initially set the count parameter to 0 and make the call, and then arrange DSEARCH so that this means "only count filenames satisfying the pattern." For the next call, after the QuickBASIC code has allocated a string array of the proper size, I can simply leave the count as nonzero, eliminating some complexity from the BASIC code as well. At this point I had in mind a DSEARCH that would take a search path, a count parameter (one that, on input, meant "only count or fill" depending on whether it was zero or nonzero), and a string array for the result. When I looked up the definition of the find first/find next calls in my MS-DOS documentation, I saw that there was a file attribute input parameter for the DOS call that would allow me to find subdirectories, volume labels, and other such files. So I added a parameter indicating the file attribute to search for. At first, I added this as another subfield of the count parameter, but the manipulations were getting a little sticky──I had the most significant bit of count indicating count/fill and the low byte indicating the search attribute──and it was a little convoluted. I decided that separating them wouldn't waste much space and would make for much clearer code. (Such are the internal wars between an assembly language hacker and a fan of structured program design.) Finally, in the process of writing and testing DSEARCH, I found a problem in the find first/find next functions. If, for example, you wanted to search for subdirectories, you'd think you would set the subdirectory bit in the search attribute and call find first/find next until no more files were found. Unfortunately, the way MS-DOS interprets this search is "find all files with the subdirectory bit set or with no bits set" so that it returns all normal files as well as all subdirectory files. This makes DSEARCH a little less flexible, especially in searching for volume labels, for example. I fixed this behavior in the DSEARCH code simply by checking each file found to see if it matched the search attribute exactly, refusing to count or return it if not. However, I could envision applications that would want to find all normal files, and since other bits exist in the attribute search (archive, system, hidden, and read-only, for example), it might sometimes be necessary to find files the way MS-DOS does. So, I added one final parameter that controls the search strategy. If the selective search parameter is zero, the search is done just the way MS-DOS would do it; if it's nonzero, the search excludes all files that don't match the search attribute exactly. In its final form, DSEARCH is called using this syntax: CALL DSEARCH(path$, attr, count, selective, array ofs) All parameters but the first are integers. They must be integers since the routine has to know the exact format of the call in order to behave properly. This should come as no surprise. Testing DSEARCH So now we've designed DSEARCH and have written it absolutely bug-free the first time, right? Well, maybe you're a different kind of programmer than I am. I always make mistakes. So what's a good strategy for testing assembler routines? Whenever I write assembly language I like to count on using a symbolic debugger such as SYMDEB, CodeView(R), or Periscope (each has its own individual charm). With subroutines, though, this can be a little more difficult than with standalone code. You must interrupt the execution just at the point of your routine. Luckily, the symbolic debuggers allow you to execute up until a label, such as "g dsearch," and do that painlessly. This is the way I usually end up debugging QuickBASIC subroutines. It's a little more involved than QuickBASIC development normally is, because the module has to be incorporated into the code at link time, meaning the editor environment is a little less convenient than usual. Of course, you can use the /l switch when invoking QuickBASIC and load a user library that you've constructed with BUILDLIB, but you still have the problem of popping out to a debugger when your routine is executed. There are ways to do this──such as imbedding an INT 3 in the code and then loading QuickBASIC under the debugger──but, by and large, I prefer to use the standalone compile/link option. When using this option, be sure to specify the /MAP parameter to the linker, so that it writes all public symbols to a MAP file. SYMDEB will use this file to create a SYM file (with the MAPSYM utility). Periscope will use the MAP file directly or create its own file, but the symbolic debug is nearly crucial in my mind. Also, I usually use the /O option on the QuickBASIC compile. Either way will work, and omitting /O makes the LINK go much more quickly, but the resulting executable is much easier to follow when /O is used. The run- time version uses software interrupts for BASIC calls, whereas the /O compile uses far calls to named routines and fewer in-line data switches (in which a routine looks at memory at its return address and performs different functions based on that data). This can make it difficult to set breakpoints in the code. So compile (while testing) with /O, link with /M, and then use a symbolic debugger. This has the added advantage of saving all your work to disk each time you try out the assembler code. It's so easy to hang the machine, especially if you're new at assembly language interfacing, that I can't stress this benefit enough. Dangerous things happen with great frequency when you're designing routines like DSEARCH; be sure you're prepared. There are several things to be on the lookout for. Be sure the stack is cleaned up properly on exit and that DS and BP are reset to their initial values. I've made this mistake at least a hundred times, and I expect to do it again. The STRUC tricks discussed earlier will save you a lot of grief in such matters. Also, be sure you know what you're accessing and where it is in reference to the parameters. Does the location contain the address of the parameter or the value itself? Check that you've got the right offset for the parameter. In particular, watch for mistakes associated with the GROUP and the OFFSET operator, as described earlier. General Design Issues Let's summarize the considerations behind designing the DSEARCH subroutine. First, it's important to select a task that's difficult to do directly in QuickBASIC──otherwise, why spend the time and effort on developing an assembly language version, which is much harder than writing BASIC code. Of course, speed is always an issue, too. Some screen operations can be done much more quickly in assembler code, as we'll see later. Another big consideration is the syntax and setup of the BASIC call. You want to have a clean division of effort between the assembler routine and the BASIC driver code. Carefully choosing what to let the assembler do and what to let BASIC do can make for much cleaner, more readable code. For example, the code using the combined count/attribute parameter described earlier looked like this: attr% = &H10 'find all files and subdirectories count% = &H8000 OR attr% 'set hi bit to fill array call dsearch (path$, count%, arrofs%) It's clearer in the BASIC code simply to add a separate attribute parameter: attr% = &H10 'find all files and subdirectories count% = 1 'fill array, don't just count call dsearch (path$, count%, attr%, arrofs%) The change in this example is intentionally minor. It doesn't make that much difference in the way the calling code looks, and it does add slightly to the complexity of the DSEARCH routine. However, any extra clarity helps the matter, in my opinion. Much more of an issue is whether to provide a full directory search routine or simply some sort of interface to the find first/find next calls that MS-DOS already has. I decided that I could do nearly the same thing with INT86 already, and the object was to make a routine that was not too complex in assembly language (because I prefer to write in BASIC), but that saved some time writing in BASIC and provided a tool that I could use. I think that putting the looping/selection code in assembler accomplishes this. I can now issue a call/allocate/call and get a whole list of files rather painlessly. Also, I've managed to provide a little more flexibility with the selective search parameter. The thing to keep in mind is that often the task can be accomplished in many different ways. Performing part of it in BASIC and part in assembler affords you the option of writing the way you write most clearly. Some extra thought in the design and modification stage will pay off in the final version and as you later reuse your toolbox of assembler routines. Demonstration In order to demonstrate what DSEARCH can do for a BASIC application and, not coincidentally, to help with the design and modification of the routine, I decided I'd write a user interface similar to those used for entering filenames in many popular PC products. I wanted to provide a directory and allow cursor key selection of the filename desired (in a manner similar to the editor in QuickBASIC). Similarly, I thought I'd provide subdirectories as a possible selection, allowing the user there to change directories and see a new filename list. I decided to make all this happen in relatively flexibly sized and colored windows as well, and perhaps to provide the whole routine as a prewired, drop-it-in subroutine that readers could use in their own code. As I started writing, though, things got out of hand. I kept adding little features, tweaking this and that, thinking, "It would sure be neat if you could do this," and so on. So now I have a pretty useful file- and directory-browsing and maintenance utility that I call BROWSE. The program starts by displaying a directory of the currently logged path and drive, courtesy of two DSEARCH calls, and then presents a window of definable size and color containing as many filenames and subdirectory names as will fit. The cursor keys move a reverse-video bar cursor around the filenames, and when one is selected with the return key, BROWSE determines whether the name is that of a subdirectory or a file. If the former, the directory is changed and a new window of filenames presented. If the latter, the file is sent to one of two routines──TypeIt or ListIt──that use QuickBASIC's SHELL statement either to TYPE file | MORE for one-way browsing or to invoke the popular shareware file browser LIST for browsing the file bidirectionally. Also, I've assigned F1 to change logged disk drives and Alt-D to delete a file (if you decide that the file you just browsed isn't worth keeping). Alt-X terminates the program. The code is fairly well commented, so each routine doesn't need a lot of discussion. I do want to mention, though, that the SaveScreen and RestoreScreen routines are never called. They turned out to be relatively slow, and I've ended up with a sort of standalone demonstration, anyway. They certainly can be used, though, with the dummy ScreenData array, to implement a clean "save the old screen, make a new window, and then put the old screen back" sort of application. Actually, one solution I really like is to write a pair of short assembly language routines to perform the "save screen to array" and "restore screen from array" functions. Routines such as these really do benefit from assembly language's terseness and direct access to machine hardware, and ones I've written for the IBM(R) PC and for the Zenith(R) Z-100 are very nice to use. Snappy window response is one thing that increases a user's confidence in an application, and for raw speed, this is a perfect application. Another feature I'd like to point out is the INT86 routine. More and more, MS-DOS implementations of languages include some nice facility for calling DOS interrupts directly to perform system services that might be a bit difficult to accomplish in other ways. Before QuickBASIC, if you wanted to call the DOS Change Directory function from BASIC, you would have had to write a short assembly language routine to allow the interface. Well, that's exactly what INT86 provides: a way to interface directly to software interrupts without all the fuss and bother of writing your own assembler routine. For a function such as Change Directory, which really does only one thing, INT86 is the perfect solution. I've used INT86 to implement the get/set current path/disk functions in BROWSE, in subroutines, and I think the result is elegant and handy. Room for Improvement Of course, BROWSE isn't perfect, nor is the set of routines included in BROWSE going to be perfect for your own directory interface. You'll undoubtedly be able to enhance the utility to meet your own needs──for example, by adding a pair of better save/restore routines to help give QuickBASIC that extra punch that makes a professional application stand out from the crowd. Combining the powerful new QuickBASIC control structures and statements, the INT86 interface to MS-DOS and BIOS services, and custom assembly language routines of your own design, it's easy and worthwhile to create sophisticated applications with QuickBASIC, a language that's no longer just for beginners. Figure 1: Code Listing for DSEARCH.ASM ;; DSEARCH.ASM ;; Written by Dan Mick page 62,132 data segment public 'data' dta_ofs dw ? ;save area for BASIC's DTA dta_seg dw ? fill db ? ;"fill array" flag selflg db ? ;"selective search" flag search_attr db ? ;files' search attribute filcnt dw ? ;count of files found path db 64 dup (0) ;search path, passed from BASIC ;DTA structure for findfirst, findnext calls (see DOS documentation) dta db 21 dup (0) ;reserved area (used for findnext) attrib db ? ;attribute of file found time dw ? ;time of last write date dw ? ;date fsize dd ? ;filesize (32 bit integer) fname db 13 dup (0) ;ASCIIZ filename: name.ext,00 data ends dgroup group data code segment byte public 'code' assume cs:code, ds:dgroup .xlist ; ;CALL DSEARCH(path$, attr, count, selective, array ofs) ; path$ is a normal string variable with the path for the ; directory search. It be a full pathname, including filename ; and extension fields. To get all the files in the default ; directory on C:, for instance, path$="C:*.*", or all the files ; in subdir on the current drive, path$="subdir\*.*". ; attr is the MS-DOS file attribute for the search. See below for ; a description. Each bit represents a file attribute. ; count is an input and an output parameter. On input, if it is ; zero, the files will not be put in the array, only counted. ; This is provided so that a dynamic array may be allocated after ; the size is determined by dsearch(). If count is non-zero on ; input, the filenames will be placed in the array. ; In either case, the count of filenames found is returned. ; selective is a flag indicating how to do the counting and ; searching process. If the file attribute 10 is specified, for ; example, DOS will return all those filenames that have the ; subdir attribute as well as those that don't. If selective is ; set nonzero, the non-subdir files will not be returned. What ; this does, basically, is allow a "filter normal files" selection ; so that subdirs and volume labels may easily be found. ; array offset is a pointer (offset only for strings) to a string ; array big enough to hold all files found in path$. The size ; required may be determined as described below. ; ; The attribute byte: ; ; ------------------------------------- ; | X | X | X | A | D | V | S | H | R | ; ------------------------------------- ; ; X is don't care, A = archive, D = subdirectory, V = volume label, ; S = system, H = hidden, R = read-only. ; .list stk struc bbp dw ? ;BASIC's bp retadd dd ? ;return address arrofs dw ? ;param: array offset select dw ? ;param: filter normal files flag count dw ? ;param: count flag/count return attr dw ? ;param: search attr path$ dw ? ;param: pathname to search stk ends ;calculate value to pop with RET n at end of proc (length of params) popval = (offset path$) + (size path$) - (offset arrofs) strdes struc ;BASIC string descriptor structure len dw ? ;word length of string strptr dw ? ;pointer to string space strdes ends dsearch proc far ;it's a far proc to BASIC public dsearch ;let LINK know about this one push bp ;save BASIC's bp mov bp,sp ;address stack as it exists now mov [filcnt],0 ;initialize file count mov si,path$[bp] ;get pointer to string descriptor mov cx,len[si] ;cx = path length mov si,strptr[si] ;si -> path string data lea di,path ;di -> place to fill rep movsb ;move pathname to our data area mov byte ptr es:[di],0 ;make sure it's an ASCIIZ string mov si,count[bp] ;get pointer to fill flag in di mov cx,[si] ;cx = fill flag mov byte ptr [fill],0 ;set flag to "no" first or cx,cx ;nonzero? jz nofill ;nope mov byte ptr [fill],0ffh ;yes, set flag nofill: mov di,select[bp] ;get pointer to selective flag in di mov cx,[di] ;cx = selective flag mov byte ptr [selflg],0 ;set flag to "no" first or cx,cx ;nonzero? jz nosel ;nope mov byte ptr [selflg],0ffh ;yes, set flag nosel: mov di,attr[bp] ;point at search attribute param mov cx,[di] ;and get it mov ah,2fh ;get BASIC's DTA int 21h mov [dta_ofs],bx mov [dta_seg],es ;and save it lea dx,dta ;set DTA to our area here mov ah,1ah int 21h mov di,arrofs[bp] ;di -> first string descriptor mov di,[di] ;di = first string descriptor offset mov bx,ds ;set up es to string array segment mov es,bx ; (BASIC's data segment) ;now es:di -> first string descriptor mov ah,4eh ;find first matching file xor ch,ch ;clear hi part of attribute lea dx,path ;ds:dx points to search path int 21h jnc ok ;if error, cy set cmp ax,18 ;no files found? jz exit ;yes, don't report error mov [filcnt],0ffffh ;set count to -1 to report path error jmp short exit ;and leave ok: call countit ;rets cy if should save, incs filcnt jz findnext ;no fill if zr set call move_filename ;do the move findnext: mov ah,4fh ;find next function int 21h ;do it jc exit ;if error, must be no more files call countit ;count, return cy if should save jz findnext ;if zr set, don't save name call move_filename ;move it jmp short findnext ;and keep hunting exit: push ds lds dx,dword ptr [dta_ofs] ;get BASIC's DTA mov ah,1ah ;set DTA fn. int 21h pop ds mov di,count[bp] ;di -> fattr for count return mov ax,[filcnt] ;get file count mov [di],ax ;put file count in fattr pop bp ;restore BASIC's BP ret popval ;return and pop parameter pointers dsearch endp countit proc ; ;Check file attribute if selective, and update count or not based on ;result. Return flag saying whether or not to move filename, too, ;based on updated count and the "fill" flag. ; cmp [selflg],0ffh ;are we selective? jnz bump ;nope, count it cmp [attrib],cl ;is the attr just what we want? je bump ;yes, count this file cmp cl,cl ;nope, set ZF jmp short dontfill ;and skip to exit bump: inc [filcnt] ;update counter cmp [fill],0 ;now, are we filling? dontfill: ;get here from jne, so NZ ret ; return with NZ if filling countit endp move_filename proc near ; ;es:di points to string descriptor to fill with filename from DTA ; push di ;save pointer to descr. length part mov di,es:strptr[di] ;and point instead to string data lea si,fname ;si -> filename (ASCIIZ) moveloop: lodsb ;get filename character or al,al ;is it 0? (end of string?) jz done ;yes, quit moving data stosb ;no, store jmp short moveloop ;and continue done: test byte ptr [attrib],10h ;is this a subdir? jz notsub ;no mov al,'\' ;yes, store a trailing backslash stosb notsub: pop di ;di -> length in s.d. again add di,4 ;move to next s.d. pointer ret move_filename endp code ends end dsearch Figure 2: DSEARCHT.BAS is a "Test Program" for DSEARCH. 'dsearcht.bas 'simple program to check dsearch routine and make sure it's 'working. The idea is to call it just enough to check out 'its capabilities, and perhaps vary its input by small 'amounts, so that you get a controlled test where there are 'no likely bugs in the BASIC code. rem $dynamic DIM A$(0) start: input "Path for directory? (wildcards allowed): ",path$ if instr("\:",right$(path$,1)) then path$=path$+"*.*" 'get number of filenames in path attr%=&H10:arrofs%=0:count%=0:selective%=0 call dsearch(path$,attr%,count%,selective%,arrofs%) 'now count% has actual filename count for redim, or -1 if error '(other than file not found, that is.) if count%=-1 then print "Bad pathname":goto start redim a$(count%) print "There are "count%"filenames in path "path$"." 'set up a$ so that dsearch doesn't have to change length for i=0 to count%-1:a$(i)=space$(12):next i arrptr=varptr(a$(0)) arrofs%=int(arrptr) 'explicit conversion call dsearch(path$,attr%,count%,selective%,arrofs%) for i= 0 to count%-1:print i;":";a$(i):next i goto start Figure 3: Code Listing for BROWSE.BAS 'BROWSE.BAS - a file browser/directory maintenance routine 'To compile in the editor environment, make a user library by 'assembling DSEARCH.ASM, using BUILDLIB to add it and USERLIB.OBJ 'to a user library (call it MYLIB.EXE, for instance), and then 'use the /L switch to load QB, as in "QB BROWSE /L MYLIB.EXE". 'To compile standalone, use "LINK BROWSE+DSEARCH+USERLIB...." defint a-z dim winrec(8) 'holds window dimensions, colors for text and borders 'the following arrays and constants are used for int86() dim inary(7), outary(7) const ax = 0, bx = 1, cx = 2, dx = 3 const bp = 4, si = 5, di = 6, flag = 7 'actions returned by PageAndSelect const disk.change = 1, delete.file = 2, type.file = 3, quit = 4 action.flag = 0 'set by PageAndSelect to one of the above rem $dynamic dim filename$(0) dim shared ScreenData (0,0) def fn.min(x,y) if x < y then fn.min = x else fn.min = y end def def fn.max(x,y) if x > y then fn.max = x else fn.max = y end def '************************ MAIN PROGRAM ************************** call GetCurrDisk(origdisk$) call GetCurrPath(origdisk$,origpath$) disk$ = origdisk$ 'get a count of all normal and subdir files Do call GetCurrPath(disk$,CurrPath$) searchpath$ = disk$ + ":" + CurrPath$ + "*.*" attr = &H10 : count = 0 : selective = 0 arrofs = int(varptr(filename$(0))) call dsearch (searchpath$,attr,count,selective,arrofs) ' This should never happen!... if count = -1 then locate 25,1:print "Invalid path: "searchpath$ print "Strike a key...":a$ = input$(1):end end if redim filename$(count-1) for i = 0 to count-1:filename$(i) = space$(12):next i arrofs = int(varptr(filename$(0))) call dsearch (searchpath$,attr,count,selective,arrofs) 'now filename$(0 to count-1) have all the files and subdirs winrec(1) = 1 : winrec(2) = 1 'upper left of window (row,col) winrec(3) = 10 : winrec(4) = 80 'lower right of window winrec(5) = 0 : winrec(6) = 11 'fg/bg color winrec(7) = 10 : winrec(8) = 0 'border colors, fg/bg call makewindow (winrec()) 'do window/cursor/path manipulation. call PageAndSelect(winrec(),filename$(),action.flag,f$) 'f$ comes back as fixed-length (12) string ' ...strip off trailing blanks i = instr(f$," ") : if i > 0 then f$ = left$(f$,i-1) select case action.flag case quit call SetCurrDisk(OrigDisk$) call SetCurrPath(OrigPath$) cls : end case disk.change cls : print "Enter new disk drive letter: "; disk$ = input$(1) : print disk$; if disk$ > "Z" then disk$ = chr$(asc(disk$) and &HDF) call SetCurrDisk(disk$) action.flag = 0 case type.file if right$(f$,1) = "\" then CurrPath$ = CurrPath$ + f$ call SetCurrPath(CurrPath$) else call ListIt (disk$+":"+CurrPath$+f$) end if case delete.file kill (disk$+":"+CurrPath$+f$) end select action.flag = 0 loop while (1) '**************************************************************** sub makewindow (winrec(1)) static y1 = winrec(1):x1 = winrec(2):y2 = winrec(3):x2 = winrec(4) fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8) wid = x2-x1+1 : height = y2-y1+1 const vert = 186, upright = 187, lowright = 188 const lowleft = 200, upleft = 201, horiz = 205 color bfc,bbc locate y1,x1:print chr$(upleft);string$(wid-2,horiz);chr$(upright); for i = 2 to height-1 locate y1+i-1, x1:print chr$(vert); locate y1+i-1, x2:print chr$(vert); next i locate y2,x1:print chr$(lowleft);string$(wid-2,horiz);chr$(lowright); call clearwindow(winrec()) end sub '**************************************************************** sub clearwindow (winrec(1)) static y1 = winrec(1):x1 = winrec(2):y2 = winrec(3):x2 = winrec(4) fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8) wid = x2-x1+1 : height = y2-y1+1 color fc,bc for i = 2 to height-1 locate y1+i-1,x1+1:print string$(wid-2," "); next i end sub '**************************************************************** sub savewindow (winrec(1)) static y1 = winrec(1):x1 = winrec(2):y2 = winrec(3):x2 = winrec(4) fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8) wid = x2-x1+1 : height = y2-y1+1 for i = x1 to x2 for j = y1 to y2 ScreenData(j-y1,i-x1) = screen(j,i,1) * 256 + screen (j,i,0) next j next i end sub '**************************************************************** sub restorewindow (winrec(1)) static y1 = winrec(1):x1 = winrec(2):y2 = winrec(3):x2 = winrec(4) fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8) for j = y1 to y2 locate j,x1 for i = x1 to x2 d = ScreenData(j-y1,i-x1) bc = (d\256)\8 : fc = (d\256) mod 8 : color fc,bc print chr$(d and &hff); next i next j end sub '**************************************************************** sub PageAndSelect_ (winrec(1),file$(1),action.flag,FileSelected$) static 'This routine does all the work here. It assumes MakeWindow 'has been called before entering, and does all key processing and 'subsequent window updates. action.flag is returned as the action 'for the main program to take (see the constants in the main program 'for a list of possible actions). shared disk$,CurrPath$,count 'second codes from extended keys used in PageAndSelect const up = 72, down = 80, left = 75, right = 77 const f1 = 59, AltD = 32, AltX = 45 y1 = winrec(1):x1 = winrec(2):y2 = winrec(3):x2 = winrec(4) fc = winrec(5):bc = winrec(6):bfc = winrec(7):bbc = winrec(8) wid = x2-x1+1 : height = y2-y1+1 locate y1,x1+1:color bfc,bbc:print " ";disk$;":";CurrPath$;" " locate y2,x1+1 print "RET-Type file AltD - delete file F1 - chg disk AltX - quit" locate y1,x2-1-9:print count;"files" NamesPerLine = (wid-2)\15 'how many filenames on each line in window lines = height-2 'how many usable lines inside window FilesPerWindow = lines * NamesPerLine StartIndex = 0 'first file$() to be displayed in window CurrIndex = 0 'current file$() being highlighted NewIndex = 0 'updated file$() index after moving cursor do call clearwindow(winrec()) limit = fn.min (ubound(file$) - StartIndex, FilesPerWindow - 1) color fc,bc : locate y1+1 for i = 0 to limit step NamesPerLine for j = 0 to NamesPerLine-1 locate ,x1 + 1 + j*15 if StartIndex + i + j <= ubound(file$) then print file$(StartIndex+i+j); else print space$(15); end if next j print next i 'initialize highlight bar position BarRow = y1+1 : BarCol = x1 + 1 do color fc,bc : locate BarRow,BarCol : print file$(CurrIndex); CurrIndex = NewIndex BarRow = (CurrIndex-StartIndex)\NamesPerLine + y1 + 1 BarCol = (CurrIndex mod NamesPerLine)*15 + x1 + 1 color bc,fc : locate BarRow,BarCol : print file$(CurrIndex); 'wait for extended character (look for cursor keys) or RETURN GetKey: a$ = "" : while a$ = "" : a$ = inkey$ : wend if a$ = chr$(13) then action.flag = type.file FileSelected$ = file$(CurrIndex) exit do end if if len(a$)<>2 then goto GetKey redraw = 0 'flag to indicate when window must be redrawn 'and in which direction the cursor was moving select case asc(right$(a$,1)) case up NewIndex = fn.max (0,CurrIndex-NamesPerLine) if NewIndex < StartIndex then redraw = -1 case down NewIndex = fn.min (ubound(file$), CurrIndex+NamesPerLine) if NewIndex > StartIndex + FilesPerWindow - 1 _ then redraw = 1 case left NewIndex = fn.max (0,CurrIndex-1) if NewIndex < StartIndex then redraw = -1 case right NewIndex = fn.min (ubound(file$), CurrIndex + 1) if NewIndex > StartIndex + FilesPerWindow - 1 _ then redraw = 1 case f1 action.flag = disk.change : file.selected = -1 : exit do case AltX action.flag = quit : file.selected = -1 : exit do case AltD action.flag = delete.file FileSelected$ = file$(CurrIndex) : exit do case else sound 440,2:sound 220,2 end select loop while redraw = 0 if action.flag then exit do 'otherwise, fall through and redo the entire "do" loop select case sgn(redraw) case -1 StartIndex = fn.max (StartIndex - FilesPerWindow , 0) case 1 StartIndex = fn.min (StartIndex + FilesPerWindow , _ ubound(file$)) end select CurrIndex = StartIndex loop while not action.flag end sub '**************************************************************** sub GetCurrPath(disk$,CurrPath$) static shared inary(),outary() 'first set up 64-byte work area for Int 21h Function 47h ' (get current directory) CurrPath$ = space$(64) : pathptr = sadd(CurrPath$) inary(dx)=asc(disk$)-65+1 inary(si)=pathptr 'pointer to area to fill inary(ax)=&H4700 'function number call int86(&H21,varptr(inary(0)),varptr(outary(0))) 'strip off trailing NUL, rest of blanks CurrPath$=left$(CurrPath$,instr(CurrPath$,chr$(0))-1) CurrPath$="\"+CurrPath$ if CurrPath$ <> "\" then CurrPath$=CurrPath$+"\" end sub '**************************************************************** sub GetCurrDisk(disk$) static shared inary(),outary() inary(ax) = &H1900 call int86(&H21,varptr(inary(0)),varptr(outary(0))) disk$ = chr$(65 + (outary(ax) and 255)) end sub '**************************************************************** sub SetCurrDisk(disk$) static shared inary(),outary() inary(ax) = &H0E00 inary(dx) = asc(disk$) - 65 call int86(&H21,varptr(inary(0)),varptr(outary(0))) end sub '**************************************************************** sub SetCurrPath(path$) static shared inary(),outary() newpath$=path$ if newpath$<>"\" then newpath$=left$(newpath$,len(newpath$)-1) newpath$=newpath$+chr$(0) inary(ax) = &H3B00 inary(dx) = sadd(newpath$) call int86(&H21,varptr(inary(0)),varptr(outary(0))) end sub '**************************************************************** sub TypeIt (path$) static cls shell ("type "+path$+" | more") locate 25,1:print "Any key to continue..."; a$=input$(1):cls end sub '**************************************************************** sub ListIt (path$) static shell ("list "+path$) cls end sub ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob! Undocumented 34H Call Dear Dr. Bob, Can you shed any light on the undocumented MS-DOS(R) call 34H that was briefly mentioned in the article on TSR standards in the December 1986 issue ("Moving Toward an Industry Standard for Developing TSRs," MSJ, Vol. 1 No. 2)? It's not clear how it can be used. Can I check this flag to determine if it's safe to pop up my terminal program?──MF Setting AH to 34H and generating an Interrupt 21H returns a pointer to some interesting information. Since this particular function is undocumented, all of the information we've discovered about it has come from people who specialize in DOS spelunking. DOS will return a pointer in the register pair ES:BX, which points to the location in DOS where the first byte of the Critical Section Flag exists. If your TSR program checks this flag and makes sure it is set to zero before attempting to issue a call to DOS, you'll be pretty safe. Depending on the DOS version, you should check the byte after (DOS 2.1), before (DOS 3.0 and 3.1, if not Compaq-DOS), or 01AAH bytes before (Compaq-DOS 3.0). You should also check to make sure you're not in the middle of a BIOS Interrupt 13H (Disk I/O) or 16H (Keyboard Service Request). Hooking into the 28H interrupt chain allows you to call DOS at times when the Critical Section Flag would normally indicate that DOS was busy. But be warned: you cannot generate any calls to DOS with AH <= Ch from the Interrupt 28 chain. Remember, of course, that since this is undocumented and unsupported by Microsoft, there is no guarantee that it will work or that it will not disappear in future versions of DOS. Dynamic Dialog Boxes Dear Dr. Bob, I am building a Windows application that requires my dialog boxes to be dynamic, that is, I need to be able to add or remove controls under "program control." Do I need to write my own version of the dialog box routines in order to accomplish this? Or are there existing Windows functions to handle this situation? What does Windows 2.0 provide?──DBL Yes, you can add and remove dialog box controls at any time by using the normal CreateWindow and DestroyWindow functions. Dialog box controls are ordinary child windows, and when you call CreateDialog or DialogBox, it simply does a series of CreateWindow calls to create the child controls. You can create additional child windows at any time, even after the dialog box is already visible, by simply doing additional CreateWindow calls. The only problem under Windows 1.x was that there was no easy way to control the ordering of the child windows, that is, the Tab key sequence. Under Windows 2.0, the new SetWindowPos function takes care of this. There are also new CreateDialogIndirect and DialogBoxIndirect functions, which take a dialog template in memory instead of from a resource, providing another way to build a dialog box on the fly. See Figure 1 for a Windows 2.0 coding example. Pascal vs. C Calling Conventions Dear Dr. Bob, Why do OS/2 functions use the Pascal calling convention? What are the pros and cons of the C and Pascal calling conventions?──KT In the Pascal calling convention the caller pushes the arguments onto the stack and the function that is called removes the arguments from the stack. Since there are multiple calls to the function, this replaces multiple stack cleanups with a single stack cleanup. Consider the pseudo code example shown in Figure 2. You get the idea. It's marginally faster on the 8086 because you can embed the pop into the ret──something like ret(1)──however, the speed improvement is not noticeable. The Pascal calling convention reverses the order of parameters; rather than pushing right to left (as in C), you push left to right. The benefit of the C calling convention is that you can use a variable number of parameters. The benefit of the Pascal calling convention is that it produces smaller code. In general you want to use the Pascal calling convention for all functions that don't have variable-length parameter lists so that you get smaller code. Microsoft uses the Pascal calling convention extensively in its own products for this reason. It's simple to declare which calling convention you are using with Microsoft(R) C because the header files that contain the prototypes for the libraries declare the functions CDECL so that you can use the compilation switch that says "assume Pascal calling convention for all functions" and the libraries will still work. Your program will contain a mixture of Pascal calls for all the functions you wrote and C calls to the library routines. Pragmas and Function Prototypes Dear Dr. Bob, I am a relative newcomer to C programming. Although I have a fairly good understanding of the language, I've been confused recently by several new terms introduced by the ANSI C committee. First, what is a pragma? I remember reading about pragmas in a book on ADA, but now I keep seeing references to them in relation to C. Second, what are function prototypes and why are they needed?──RJW Welcome to the wonderful world of standards, where things we're used to always change. A pragma, as defined in ADA, is simply a compiler directive. In C, pragmas selectively enable or disable certain features of the compiler. The programmer can specify, within the code listing itself, where to turn compiler options on and off. For example, in Microsoft C Version 5.0 there is the loop_opt pragma, which is used to control loop optimization locally. The programmer might globally invoke loop optimization at the compiler command line (cl /Ol in this case) and then locally control which loops should be optimized (see Figure 3). Figure 4 is a list of the pragmas that are available in Microsoft C 5.0. Function prototypes are function declarations that include a list of the names and types of formal parameters associated with the function. A function prototype establishes the name, return type, and storage class of a function. A function prototype declaration precedes the actual definition of a function and is used by the compiler for argument type checking, to establish the return type of a function that is of a type other than int, to initialize pointers to functions before those functions are defined, and to reflect that a variable number of arguments, or no arguments, will be passed. Figure 5 illustrates the use of function prototypes. The prototype for printf is predefined. The use of the ellipse indicates a variable number of arguments. The prototype for addreals indicates that there is a return value of double, rather than a default value of int. The prototype declaration for addreals informs the compiler of this, which allows addreals to be called before it is defined. The forward declaration also establishes the function argument types, allowing for type checking. It is highly recommended that all new code use function prototyping. Figure 1: Windows 2.0 Coding Example /* Add an edit control to hwndDlg, giving it a dialog ID of D_NEWITEM. * Place it right after the existing item whose ID is ID_OTHERITEM. */ hwndChild = CreateWindow("Edit", "",WS_CHILD | WS_TABSTOP | WS_BORDER,X, Y, nWidth, nHeight, hwndDlg, ID_NEWITEM, hInstance, NULL); if( ! hwndChild ) { /* couldn't create it - out of memory! */ } SetWindowPos(hwndChild, GetDlgItem( hwndDlg, ID_OTHERITEM ), 0, 0, 0, 0,SWP_NOMOVE | SWP_NOSIZE | SWP_SHOWWINDOW); Figure 2 foo: push ax call goo pop ax ∙ ∙ ∙ push ax call goo pop ax ∙ ∙ ∙ push ax call goo pop ax ∙ ∙ ∙ ret goo: ret versus foo: push ax call goo ∙ ∙ ∙ push ax call goo ∙ ∙ ∙ ; no pop - saved a byte push ax call goo ∙ ∙ ∙ ret goo: pop ax ret Figure 3: Example of Pragma Usage Command line invokes global loop optimization: cl \Oal optimum.c /* optimum.c */ ∙ ∙ ∙ main() { ∙ ∙ ∙ #pragma loop_opt(off) /* turn loop optimization off */ ∙ ∙ ∙ #pragma loop_opt(on) /* turn loop optimization back on */ ∙ ∙ ∙ } Figure 4: Pragmas Available in Microsoft C 5.0 Pragma Local Effect loop_opt Turns loop optimization on and off. pack Specifies packing alignment for structures. intrinsic Specifies which functions are compiled as intrinsic functions. function Specifies which functions are compiled as standard function calls. same_seg Tells the compiler to assume that specified variables are allocated in the same far data segment. alloc_text Specifies modules to be grouped into a specified far code segment. check_stack Turns stack checking on or off. Figure 5: Example of Function Prototyping /* Function prototyping */ #include /* stdio.h contains a function prototype for printf: * int printf(const char *format[,argument]...); */ main() { float a; float b,c.d; double addreals(double x, double y, double z); /* function prototype declaration for addreals */ ∙ ∙ ∙ a = addreals(b,c,d); printf("\t%f\n \t%f\n \t%f\n \t%f\n ",a,b,c,d); } double addreals(double x, double y, double z) /* function definition for addreals */ { ∙ ∙ ∙ return(x+y+z); } ════════════════════════════════════════════════════════════════════════════ Vol. 2 No. 5 Table of Contents Microsoft(R) Excel for Windows: Meeting the Demands of a New Generation Designed to take full advantage of the latest generation in personal computers, Microsoft Excel for Windows offers a host of advanced spreadsheet functions, Lotus(R) 1-2-3(R) compatibility, and perhaps most interesting to developers, a powerful macro language for custom applications. Interprogram Communication Using Windows' Dynamic Data Exchange Microsoft(R) Excel supports Windows Dynamic Data Exchange (DDE), a public message protocol. DDE allows concurrently executing Windows applications to exchange information in real time. The article, and accompanying sample program Maze, illustrate this process. Designing for Windows: An Interview with the Microsoft(R) Excel Developers Microsoft Excel for Windows is the most significant program to be developed in the Windows environment to date. MSJ talks with the design team for a behind-the-scenes look at the challenges involved in bringing the successful Macintosh(TM) spreadsheet to Windows. A Strategy for Building and Debugging Your First MS-DOS(R) Device Driver At first glance, writing device drivers might seem to be a tricky and confusing venture. Using the prototype communications program MDM_DRV as a model, this article explains how to write a character device driver and suggests some useful debugging tips and techniques. Microsoft(R) C Optimizing Compiler 5.0 Offers Improved Speed and Code Size The latest version of Microsoft's professional C compiler generates faster code and includes improved development tools, an enhanced version of the CodeViewer(R) debugger, a full library of graphic routines, new libraries of routines for BIOS/DOS access, and comes with QuickC(TM). Ask Dr. Bob EDITOR'S NOTE Past issues of MSJ have provided an inside look at some of the advanced applications currently being developed by such companies as Reuters, Aldus, and Plexus. The recent introduction of Microsoft(R) Excel offers PC users one of the most important general purpose applications now available──one that we believe is going to have a significant impact on applications development in the 80286 and 80386 world. Microsoft Excel is also the first full-scale Windows application program, making the Windows presentation manager graphical interface a serious development environment. With that in mind, we have opted to present a detailed view of Microsoft Excel, one that explores what its introduction means to the applications developer. Since it is a Microsoft product, we risk blowing our own horn──but it also means we can give our readers a behind-the-scenes look at the software development process that led to the PC version. Sophisticated, windows-based applications such as Microsoft Excel have the ability to communicate with each other in real time by taking advantage of DDE-Dynamic Data Exchange──the communications protocol that is part of Windows. Kevin Welch, another of our Windows experts, takes you on a tour of DDE with his MAZE program. MSJ presents the full documented source code for MAZE, which will provide significant insights into the workings of DDE. Though OS/2 and the Windows graphical interface are clearly trends for the future, there is, of course, a major MS-DOS and XENIX(R) world out there. This issue tackles the somewhat esoteric world of MS-DOS(R) device drivers. Future issues will continue to provide thorough explorations into these environments. The next issue of MSJ continues its coverage of OS/2 with an in-depth study of the Presentation Manager. In addition, Ray Duncan will begin his two-part discussion of OS/2 bimodal device drivers──perhaps the most difficult piece of the OS/2 puzzle for software developers to understand. Stay tuned. All our source code listings can now be found on DIAL, CompuServe(R), BIX, and two public access bulletin boards. On the East Coast, users can call (212) 889-6438 to join the RamNet Bulletin board. On the West Coast, call (415) 284-9151 for the ComOne bulletin board. In either case, look for the MSJ directory.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor CHRISTINA G. DYAR Associate Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor KIM HOROWITZ Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher DONNA PUIZINA Administrative Assistant Copyright(C) 1987 Microsoft Corporation. All rights reserved; reproduction in part or in whole without permission is prohibited. Microsoft Systems Journal is a publication of Microsoft Corporation, 16011 NE 36th Way, Box 97017, Redmond, WA 98073-9717. Officers: William H. Gates, III, Chairman of the Board and Chief Executive Officer; Jon Shirley, President and Chief Operating Officer; Francis J. Gaudette, Treasurer; William Neukom, Secretary. Microsoft Corporation assumes no liability for any damages resulting from the use of the information contained herein. Microsoft, MS-DOS, XENIX, Microsoft Press, Multiplan, CodeView, and the Microsoft logo are registered trademarks of Microsoft Corporation. QuickC is a trademark of Microsoft Corporation. IBM is a registered trademark of International Business Machines Corporation. Apple is a registered trademark of Apple Computer, Inc. Macintosh is a trademark of Apple Computer, Inc. Hayes is a registered trademark of Hayes Microcomputer Products, Inc. Lotus and 1-2-3 are registered trademarks of Lotus Development Corporation. AT&T and UNIX are registered trademarks of American Telephone and Telegraph Company. CompuServe is a registered trademark of CompuServe Incorporated. ████████████████████████████████████████████████████████████████████████████ Microsoft Excel for Windows: Meeting the Demands of a New Generation Jared Taylor☼ Microsoft(R) Excel for Windows is a completely rewritten version of the best-selling spreadsheet program for the Apple(R) Macintosh(TM). A crucial consideration in developing the PC version was to abide by an important standard in the MS-DOS world: open architecture. In hardware, open architecture has given the IBM(R) PC and compatibles great flexibility and has enabled third parties to expand the basic machine in ways never imagined by IBM. Likewise, open architecture in software should provide Microsoft Excel with the same kind of flexibility. Many of the features are specifically designed to make the program easy to customize. The file format, for example, is fully documented and intended not to be machine-specific. The macro language allows custom design of menus, dialog boxes, and commands that are just as powerful as those built into the program. Macros are the gateway to interprogram communications and even permit access to user-written programs and Windows libraries. This external access extends the Microsoft Excel environment far beyond its own system of rows and columns. Users will gain from the advantages of open architecture, but independent applications developers will benefit most from it. This article provides a look at features and explores a few of the development possibilities of the Microsoft Excel macro language, with an emphasis on custom routines, menus, and dialog boxes. Appearance Microsoft Excel's most obvious departure from spreadsheet convention is its appearance. Although it is designed to run under Windows, it comes with a run-time Windows module that lets you use it as a standalone program. In either case, Microsoft Excel adds the rich, graphical features of Windows to the usual spreadsheet environment of rows and columns. Not only can you display different worksheets on different parts of the screen, you can also control how a spreadsheet looks. For example, you can vary the height of rows as well as the width of columns, so as to display characters of any size. You can have up to four different font styles, such as boldface and italic, in a single spreadsheet, and display them in any color you choose. You can use color informatively as well as decoratively, by highlighting negative numbers, say, in red. You can add shading and borders to cells or entire ranges for dramatic emphasis. The visual result is as different from conventional spreadsheets as the output of desktop publishing programs is from that of word processors. Spreadsheet Functions Microsoft Excel lets you build analytical models of unequaled power and flexibility within a very large matrix of 256 columns and 16,384 rows. It has 131 built-in functions, 42 more than Lotus(R) 1-2-3(R), which is the industry standard, and functions don't require a special leading character like the at sign (@). Unusual functions found in Microsoft Excel include PRODUCT(), which is analogous to SUM() except that it multiplies the values referenced within the parentheses; FACT(), which returns the factorial value of a reference; and GROWTH() and LOGEST(), which give values and parameters of an exponential trend. All functions can be quickly called up in alphabetical order in a function menu. From there, it's easy to cut and paste them into formulas, eliminating typos. You can custom design your own functions by using the program's macro language. If you wanted to calculate the volume of a sphere, you could design a custom function that returned volume as a function of radius, and it would appear in the cut-and-paste function menu along with the built-in functions. Microsoft Excel gives 21 standard number and date formats for display purposes. If these aren't enough, you can design your own to include special symbols and display conventions. For financial reporting you can eliminate the last three, six, or nine digits so as to display numbers in thousands, millions, or billions. Versatility of display extends to printing, as Microsoft Excel makes full use of color and laser printers. With proper graphics and laser printers, you can reproduce all the fonts, shading, and colors that the program supports on screen. A tax preparation worksheet, for instance, could produce reports that look identical to a 1040 form. Formatting commands permit you to include automatic headers, footers, date and time stamps, titles, and page numbers. When necessary, Microsoft Excel can print wide spreadsheets in landscape orientation. Its preview feature takes the guesswork out of printing complex documents, letting you look at screen images of a worksheet page-by-page before you send it to the printer. Microsoft Excel has built-in database commands similar to current popular spreadsheets. Data records are made up of rows with single cells as fields. You can have as many different databases in one sheet as you like, but databases must fit into memory. You can sort by three simultaneous keys, as well as find and extract records according to mathematical and Boolean criteria. An innovative database command lets you display records as "forms": fields appear stacked on top of each other in a window rather than strung out in a row across the screen, which makes it easier to examine and enter records. Taming Recalcs Several pioneering steps have been taken to reduce the wait for recalculation. First of all, an intelligent minimal recalc is used. Instead of painstakingly recalculating every formula each time you make even a minor change, the program recalculates only those formulas that are affected by a change. If you do have a recalculation that takes time, this won't stop you from continuing your work. As soon as you hit a key, calculation will suspend and won't start up again until you pause. Eliminating Errors Microsoft Excel offers several auditing tools to help keep complex models free of errors. For example, you can annotate individual cells with explanations of spreadsheet logic. Annotations can be of any length and are entered and retrieved through windows that can be sized, moved, or hidden. There is a tool to show both the dependent and antecedent cells for any formula, which is essential to tracking spreadsheet references. You can either list these in an information window or highlight them on the screen. Even if you aren't thinking about maintenance or auditability while you build a model, you can document the relationships in formulas after the model is built. First, you would name the cells that contain the crucial values in your model. Then, with the Apply Names command, you could substitute names for any cell reference in any formula in the worksheet. F15* B29 becomes a much more understandable Profits* Tax_Rate in any formula that references those cells. Moreover, the program's naming conventions make it particularly easy to give cells useful names. For example, in a spreadsheet that shows monthly sales by region, the data will be bounded by a top row of month labels and a left column of region names. If you highlight this entire area and use the Create Names command, every data cell automatically receives a unique name. The cell at the intersection of the April column and the Southwest row will be known by the name April, Southwest, and formulas that reference April and Southwest alone will operate on the entire column or row. If you need to analyze models on paper, you can print out a formula display rather than current values. There are seven different formula error values to help keep worksheets honest. For example, #DIV/0! obviously means you have tried to divide by zero, and #NAME? means you have used an incorrect name or one that Microsoft Excel doesn't recognize. More specific error messages let you fix problems more easily. A search-and-replace feature also aids troubleshooting. You can locate strings in formulas or labels and replace them selectively or en masse. And, if you should happen to make a mistake, you can snatch it back with the Undo command. Graphs Microsoft Excel has 44 different predesigned charts based on seven basic types. All can be customized at will, and you can store any chart formats you design for future use. Since graphs are displayed in their own windows, you can examine them while you are working with the underlying data. For more sophisticated graphic jobs, you can overlay one chart onto another, so that you can put a lot of data into a single chart and even have two vertical indexes. You could combine a high-low-close stock chart that has a vertical scale from $30 to $50 with a volume chart for the same stock that has a vertical scale from 2,000 to 30,000. Vertical scales can be linear, logarithmic, or semilogarithmic, and you can print even the most elaborate chart without leaving the program. If necessary, charts can refer to data from more than one spreadsheet. Series formulas that reference labels and data ranges link the spreadsheets to the underlying data. For increased flexibility, you can edit series formulas just as freely as any other formula. If you find that you often work with the same combination of active spreadsheets or graphs, you can save the whole thing in a bundle as a "workspace." The names, relations, and settings of these different documents are saved together in a small file. When you reopen the workspace, the entire package is redisplayed as you left it. Linking Spreadsheet linking, one of the most powerful features of Microsoft Excel, meets the needs of those users frustrated by the current generation of two- dimensional spreadsheets. Links let you write formulas in a spreadsheet that refer to values in other spreadsheets. The obvious use for external references of this kind is in consolidations; the "roll-up" spreadsheet would contain many references to the component, or subsidiary, spreadsheets. However links do serve other purposes. Since subsidiary worksheets are all standalone models, different users can work with them simultaneously──which is much handier than stuffing subsidiary models into a huge consolidation model that can be used by only one person at a time. Another use for links is to display the same data in different ways. Often, a single basic set of data must be put into reports that vary in detail or layout. Instead of reconstructing the same data set for each report, links can extract from the common data set only the necessary portions and display them in a variety of formats. Command Macros Unlike 1-2-3 and most other spreadsheets now available, Microsoft Excel stores macros separately from worksheets. Macro sheets have their own characteristics but otherwise behave exactly like any other document created by Microsoft Excel. By effectively storing "programs" separately from "data," you can use the same library of useful routines on any number of different worksheets. This also facilitates running routines that work on a sequence of different worksheets──printing, formatting, or consolidation, for example. The Microsoft Excel macro language has 355 commands and statements. The majority of these are command equivalents, which automate normal spreadsheet functions. The SEARCH() and REPLACE() commands in the macro language, for example, activate the spreadsheet commands of the same name. All of the display features, such as mixed fonts, variable row height, color, and multiple windows, can be controlled with a series of eight different format commands, included in which are FORMAT.TEXT() and FORMAT.SIZE(). MAIN.CHART() specifies the elements from predefined chart types. In all cases, the syntax of the macro command is as close as possible to that of the spreadsheet command. You'll hardly ever have to write these commands, though, because the macro recorder does it for you. Turn on the recorder via the Macro menu, enter commands, and Microsoft Excel records them automatically as macro script. Even if you make typing mistakes, the recorder writes script to produce your final result rather than recording canceled menu choices or characters typed and then deleted. The recorder runs in two different modes. In relative mode, cursor movements are recorded from the original cursor position; the macro will play back differently if you start it with the cursor in different positions. When running in absolute mode, cursor movements are recorded by cell address, and the macro runs identically no matter how you start it. You can switch between modes any time during the recording. If you need to, you can stop recording entirely, while you do other spreadsheet work. Then, when you start again, you can choose either to add keystrokes to the first macro or start a new one. Keystroke recording is the easiest method for building the structure of a macro, since you can always edit it later. There are two ways to run a macro from the keyboard. You can choose the Macro Run command from a menu and then pick the macro's name from a list. Even if the macro has no name, if you know the cell in the macro sheet where it begins, you can enter an address at the Run command. A quicker way is to start a macro with a Ctrl-key combination by assigning macros to any alphanumeric key. Micosoft Excel distinguishes between plain keys and shifted keys, so it is possible to have different macros running on, say, Ctrl-P and Ctrl-Shift-P. Advanced Macros There are three kinds of macro functions that do more than just duplicate commands: control functions, customizing functions, and value-returning functions. Control functions, such as FOR(), NEXT(), GOTO(), and WHILE() govern macro execution. Customizing functions let you write menus and dialog boxes. Value-returning functions, which return information based on arguments, are the most similar to regular spreadsheet functions. Some are macro equivalents of normal spreadsheet functions like AVERAGE(), AREAS(), SUM(), or ASIN(). Others are exclusive to macros. OFFSET(), for example, tells you the address of a cell or cells that are so many columns across and so many rows up (or down) from another cell. LINKS() returns the names of all documents that the current sheet is linked to. The nature of value- returning functions is similar enough to normal spreadsheet functions to allow you to cross over. You can write value-returning functions of your own that work just like spreadsheet functions. Figure 1 shows a number of macros that all conform to standard Microsoft Excel notation. The first line, which contains the name of the macro and the Ctrl-key combination that runs it, is optional. All statements and commands begin with the equal sign, which indicates a formula, and all of the macros end with =RETURN(). This returns control to whatever process actually started the macro, be it keyboard input or a sub-routine call. Macro sheets display formulas rather than values, which is the opposite of normal Microsoft Excel worksheets. However, just as a worksheet cell might display the value 5 but contain the formula =sum(2,3), cells in macro sheets contain underlying values for their formulas. Formulas that return a value contain that value. Others, like command-equivalent macros that wouldn't normally return a value, have the value FALSE before they execute and TRUE after they have executed successfully. If they have been run but failed to execute, they have the value FALSE or an error value. This value-displaying character of macro script has two important functions: the value can be very useful in debugging macros because it gives you clues to why a macro failed, and macros store user input and returned values in the cells that contain the script, not someplace else. The first macro in Figure 1 is an example of this. The INPUT statement produces a simple dialog box (see Figure 2☼) with the quoted string as a prompt. The second argument, 1, specifies that the prompt will accept only numbers, but you can specify many different acceptable data types. When INPUT executes and gets a response from the user, the value of the response becomes the underlying value of the cell containing INPUT. This explains why the COLUMN.WIDTH command in the line below takes, as its argument, the cell address B2, which is where the value is. The second macro sends an alert message (see Figure 3☼). Since this type of alert doesn't give the user any chance to reply, you'd use it for purely informational messages that don't have to be acted on. The ALERT() statement takes a message string as its argument, as well as an indicator of the type of alert. (There is no second argument in this macro, but there is in macro 14.) Here the quoted text is combined by means of the ampersand with a name (name_3), which is a constant. The constant happens to be 10, but it could be a value, a cell address, or something else. The exclamation mark before the name means that the macro looks for the name in the currently active sheet. Without it, the macro would look for name_3 in the macro sheet by default. Macro 3 uses the powerful DISPLAY command to change the display in the active sheet. It displays formulas instead of values, turns off grid lines, leaves headings on, displays zero values, and paints the row numbers and column letters red. (Red is the third of eight different color choices.) Macro 4 returns the display to normal. Macros 5 and 6 enter formulas directly into a worksheet cell. In macro 5, for example, the formula goes into cell B6 in the current sheet (because of the exclamation point) and sums a range. The first argument must be quoted, and in this case uses the alternate-cell addressing convention of Microsoft Excel, which is compatible with Multiplan(R). In conventional notation, the summed range is B2:B4. The next macro uses the same notation to enter a formula one column over and one row down from the cursor. Macro 7 returns information about the currently open document. Its argument can be a number from 1 to 26, and each number returns different information about the document: document type, path of file, numbers of the first or last rows used, fonts used, protection status, type of chart, and so on. In this case, the value returned is TRUE if changes have been made to the worksheet since it was last saved and FALSE otherwise. This type of information is invaluable for applications developers. Macro 8 is unlike the others in Figure 1 in that it is a function macro. Once it's written, you can use it in a worksheet as you would any other built-in function. You would distinguish it from the other macros by indicating that it is a function macro when you named it. Function macros have a simple structure. They state the arguments required for the function and then return a value based on those arguments. In macro 8, there is only one argument, the radius of a sphere. The number 1 means the argument must be a number. RETURN cubes this value and multiplies by pi to give the volume of the sphere. As long as the sheet containing this macro is open, you can use the Sphere function in any worksheet. Macros 9 and 10 work together: macro 9 uses the ON.TIME command to execute macro 10 at a certain time. In this case, the time of execution is one ten- thousandth of 24 hours from NOW(). When that time is reached, the routine named Time (macro 10) in the macro sheet named SJT.XLM (the sheet containing all these macros) executes. It pops up an alert box that tells you your toast is done, but it could say anything. The ON.TIME command has an optional argument that lets you specify how long the subordinate macro should wait before executing, in case the program is running a different macro, displaying an error message, or doing anything else that would keep it from running a macro. If you leave that argument out, the subsidiary macro then waits as long as it has to before it takes control. The next pair of macros also governs conditional execution. Macro 11 remaps the Home key so as to run the macro named Home (macro 12) in SJT.XLM. The Home routine displays an alert box and then remaps the Home key to its normal function by using the ON.KEY command without an argument. An ON.CLICK macro that could redirect mouse clicks would be handy, but there is no such thing, nor is there a way to use more than one mouse button. The last three macros can run only if there are other applications running under Windows Version 2.0. Macros 13 and 14 run a routine depending on whether data has changed in another worksheet. The ON.DATA command causes the macro named Data (macro 14), in macro sheet SJT.XLM, to run whenever new information is received via a Dynamic Data Exchange (DDE) link in a different sheet, named STOCK.XLS. When Data runs, it puts up an alert box that refers to the cell in STOCK.XLS that holds the value of IBM stock. The last argument in the alert, the number 1, indicates that the user has a choice of OK or Cancel. These responses govern the TRUE/FALSE value of the cell containing the ALERT command. If it is FALSE, the new ON.DATA command deactivates the previous one by calling it without a macro argument. A routine like this could take some kind of action if the user chose OK and gave the ALERT cell the value TRUE. The routine could, for instance, run a macro like macro 15, which uses the EXEC command to start up a communications program under Windows 2.0 and the SEND.KEYS command to start commands in that application as if they were typed at the keyboard. EXEC's arguments are the name of the app and the type of window it is to run in; the number 1 specifies a normal, full-sized window. The macro waits three seconds for the terminal app to load and then sends a series of keystrokes, which follow a certain syntax: the percent sign means the next key, or all keys in parentheses, are Alt-shifted, nonalpha keys are in curly braces, and so on. The final argument is a wait log which, when TRUE, suspends macro operation until the sent keys are processed in the terminal app. When FALSE, the macro just keeps running. The EXEC/SEND.KEYS combination is obviously a powerful way for applications developers to link programs. Microsoft Excel can load, run, close, and read the output from any program that runs under Windows 2.0. However, compared with DDE, these are simpleminded routines that do no more than mimic operator keystrokes. DDE is a much more elegant means of interprogram communication, as it allows Windows 2.0 apps to run both as servers and clients and to automatically establish two-way links. One program can have more than one active DDE link at a time, and all programs have access to the command primitives of the others and can share memory objects. Two unusually powerful macro commands are REGISTER and CALL, which run Windows library routines or your own C or FORTRAN routines from within Microsoft Excel. REGISTER defines the procedure you want to run and specifies the number and data types of the procedure's arguments; it returns a text value. CALL uses this value as one of its arguments, along with the arguments you want to pass to the procedure that is to run. You cannot use CALL without also having returned a value with REGISTER. The most likely use of REGISTER/CALL would be to run a custom-designed spreadsheet function that was too involved or complex to be efficiently written as a function macro. However, REGISTER and CALL actually push and pop the command stack, so if you use them incorrectly they can blow up your application. Menus and Dialog Boxes The macro language has a series of commands that let you design custom command menus and dialog boxes. These work just like the ones designed into Microsoft Excel, so users will be perfectly comfortable with them. Figure 4 shows the macro script for a fairly detailed custom menu, and Figure 5☼ shows how this menu would appear on the screen. Menu building follows a set macro pattern as in the initialization macro in cells D53 to D60. The first procedure defines the new menu bar; then the text and macro references for the different menu choices are defined. This information, indicated by cell range addresses like D8:G16, is all referenced to the ADD.BAR command in cell D53. The new menu bar is not displayed until the macro executes the SHOW.BAR command in the last line, and the reference to D53 activates the entire menu. The structure of the menu references themselves is simple. Each ADD.MENU command specifies a menu item on the menu bar, and the cells within the specified range are the choices available in the drop-down box under that menu item. An ampersand in the name of the choice means that the letter that follows is underlined in the menu, and you can select that choice by hitting the letter on the keyboard. A single dash in a menu just draws a line to separate different menu choices. Column E contains references to macros that are executed when the user makes a choice. Column G is information about each menu choice that appears. The full gamut of menu control is available to applications developers. For example, you can make a menu item gray to show that it isn't currently available. When a menu item displays choices that the user can toggle on or off, you can display the choice with or without a check mark beside it, depending on its status. You can even make changes to standard menu bars. If, for example, you were designing an application that might jump the tracks if the user had access to the windowing commands, you could remove them from the program menu. In this application, one of the menu options lets the user sort data. Since there are many different ways to sort the data, there must be a way for the user to make choices. The solution is to use the macro script in Figure 6 to produce the custom dialog box in Figure 7☼. The DIALOG.BOX command calls the box in much the same way as the menu commands call a menu, but it does so in a single step. In Figure 6☼, the macro in I26:I28 specifies the dialog box reference in B3 to H24 and displays the box immediately. The numbers in column B identify the type of item in the dialog: list box, text box, option button, and so on. The next four columns specify the vertical and horizontal location and height and width of the item. Column H is essential; user choices are stored here. The text strings in I10 to I18 are referenced in two of the choices and are options for the user (see Figure 7☼). In designing a dialog box, it's a bother to have to come up with all the numbers in columns C through F for location and size. If you leave them out, Microsoft Excel tries to arrange the contents of the dialog box for you, but the results are not ideal. Microsoft is working on a developer's utility to let you design dialog boxes by drawing them directly on the screen. All by itself, the script in Figure 6 will display the dialog box in Figure 7☼ and record user choices in column H. However, in order for these choices to mean anything, they have to be referenced by macros that evaluate them. Security No matter how sophisticated an application is, it can fall on its face if users can tamper too easily with the underlying scripts. Microsoft Excel offers several ways to secure your applications. First, although a macro sheet has to be in memory to operate, it can be hidden or reduced to an icon. Most inexperienced users won't think to look for hidden windows, and if you remove the windows command from the menu bar, it will make tampering even harder. Macros can normally be aborted by hitting Escape, but you can remap the Escape key to keep routines secure. Your application will always be vulnerable when you hand the screen over to the user for data entry. The simplest corrective here is to protect worksheets and allow data entry only in unprotected cells. If necessary, you can remove the protection commands from the menu. If you need even greater protection, you can avoid turning the screen over to the user even for data entry. For virtually bulletproof apps, you can funnel all user input through dialog boxes. If your program files are password-protected and are called from a hidden macro file containing a camouflaged password, you'll be able to keep most users out. Even so, any security measures that you design can probably be defeated by anyone who tries hard enough. Turning Point The release of the PC version of Microsoft Excel is a turning point for Microsoft and the world of MS-DOS(R). Microsoft Excel is the first full- scale Windows application program, making the Windows graphical interface a serious development environment. This article has touched on only a few of the commands and options available to applications developers. The menu/dialog box combination in Figures 4 through 7 is typical of the manner in which sophisticated commands can be made available to inexperienced users through custom programming. Combined with the inherent power of Microsoft Excel, the flexibility of the macro language, and the means to communicate with other programs, this open software architecture should make Microsoft Excel a very solid platform for applications development. Figure 1: Sample Macros ╓┌────┌─────┌────────────────────────────────────────────────┌───────────────╖ A B C 1 1 Column_Width (b) 10 2 "=INPUT(""Column Width?"",1)" 3 =COLUMN.WIDTH(B2) 4 =RETURN() A B C 4 =RETURN() 5 6 2 Alert_Names (e) 7 "=ALERT(""Total is ""&!name_3)" 8 =RETURN() 9 10 3 Display Macro (c) 11 "=DISPLAY(TRUE,FALSE,TRUE,TRUE,3)" 12 =RETURN() 13 14 4 Display Macro (d) 15 "=DISPLAY(TRUE,TRUE,TRUE,TRUE,1)" 16 =RETURN() 17 18 5 Formula (h) 19 "=FORMULA(""=sum(r2c2:r4c2)"",!$B$6)" 20 =RETURN() 21 22 6 Formula (l) 23 "=FORMULA(""=test"",""R[1]C[2]"")" A B C 23 "=FORMULA(""=test"",""R[1]C[2]"")" 24 =RETURN() 25 26 7 Getdoc (G) 27 =GET.DOCUMENT(4) 28 =RETURN() 29 30 8 Sphere 31 "=ARGUMENT(""radius"",1)" 32 =RETURN(radius^3*PI()) 33 34 9 On_Time (s) 35 "=ON.TIME(NOW() +0.0001,""sjt.xlm!Time"")" 36 =RETURN() 37 38 10 Time 39 "=ALERT(""Toast is done"")" 40 =RETURN() 41 42 11 On_Key (r) A B C 42 11 On_Key (r) 43 "=ON.KEY(""{HOME}"",""sjt.xlm!Home"")" 44 =RETURN() 45 46 12 Home 47 "=ALERT(""you pressed the Home key"")" 48 "=ON.KEY(""{HOME}"")" 49 =RETURN() 50 51 13 On_Data (t) 52 "=ON.DATA(""Stock.xls"",""sjt.xlm!Data"")" 53 =RETURN() 54 55 14 Data 56 "=ALERT(""IBM is now at ""&'A:\STOCK.XLS'!$C$2,1)" 57 "=IF(B56=FALSE,ON.DATA(""Stock.xls""))" 58 =RETURN() 59 60 15 Terminal (v) 61 "=EXEC(""terminal"",1)" A B C 61 "=EXEC(""terminal"",1)" 62 =WAIT(NOW)() +3/86400) 63 "=SEND.KEYS(""%{f10}"",TRUE)" 64 "=SEND.KEYS(""%(fo)c:*.*~"")" 65 "=SEND.KEYS(""%(ft){ESC}"",TRUE)" 66 "=SEND.KEYS(""%(cc)9600%c{right}~"",TRUE)" 67 "=SEND.KEYS(""%(fb)"",TRUE)" 68 =RETURN() Figure 4: Macro Script for a Sample Custom Menu ╓┌───┌───────────────────────────┌─────────────────────────┌─────────────────► D E G 7 Custom menu Bar ref 8 &Employee Records "Add, delete, save 9 &New Employees Payroll.XLM!Add Add a new employee 10 &Retire Employee Payroll.XLM!Delete Remove an employee 11 &Save Records Payroll.XLM!Save Save database D E G 11 &Save Records Payroll.XLM!Save Save database 12 13 Page &Setup Payroll.XLM!PageSetup Adjust output layou 14 &Print Records Payroll.XLM!Print Print database 15 16 &Quit . . . Payroll.XLM!Quit Quit to Microsoft E 17 18 &Employee Data Edit and find data 19 Change &Address Payroll.XLM!ChangeAddress Change address of s 20 Change &Rate Payroll.XLM!ChangeRate Change pay rate of 21 Change &Hours Worked Payroll.XLM!ChangeHours Change number of ho 22 &Find employee . . . Payroll.XLM!FindEmp Find an employee 23 24 &Organize "Edit layout, organ 25 &Hide field Payroll.XLM!Hide Hide a data field 26 &Un-hide field Payroll.XLM!UnHide Unhide a data field 27 28 &Sort Payroll.XLM!Sort Sort employee 29 30 Re&port Choose pre-defined D E G 30 Re&port Choose pre-defined 31 &Employee List Payroll.XLM!EmpList List with all data 32 Pe&rsonal Data List Payroll.XLM!PerList "List with only nam 33 &Wage List Payroll.XLM!WageList "List with only nam 34 &Phone List Payroll.XLM!PhoneList List with only name 35 36 37 &Format Edit display option 38 &Dollar signs Payroll.XLM!Numbers Dollar signs on mon 39 &Borders around employees Payroll.XLM!dbBorders Outline of employee 40 Borders around &summary bar Payroll.XLM!SumBorders Outline of summary 41 &Gridlines Payroll.XLM!Gridlines Database gridlines 42 43 &Alignment . . . Payroll.XLM!Alignment Data alignment 44 &Font . . . Payroll.XLM!Font Change font 45 46 &Info . . . 47 &Introduction . . . Payroll.XLM!Intro Introduction to Cus 48 &Original Bar Payroll.XLM!Revert Revert to original 49 &About . . . Payroll.XLM!About Display Program inf D E G 49 &About . . . Payroll.XLM!About Display Program inf 50 51 52 Initialize 53 =ADD.BAR() 54 "=ADD.MENU(D53,D8:G16)" 55 "=ADD.MENU(D53,D18:G22)" 56 "=ADD.MENU(D53,D24:G28)" 57 "=ADD.MENU(D53,D30:G34)" 58 "=ADD.MENU(D53,D37:G44)" 59 "=ADD.MENU(D53,D46:G49)" 60 =SHOW.BAR(D53) Figure 6: Macro Script for a Sample Custom Dialog Box ╓┌───┌───┌────┌────┌───┌──────┌───────────────┌───┌──────────────────────────╖ B C D E F G H I 2 │ SORT DIALOG BOX B C D E F G H I 2 │ SORT DIALOG BOX 3 │ 4 5 5 Sort Records │ 5 14 5 15 70 95 First Key │ 6 15 10 30 60 35 I10:I18 4 │ 7 14 10 70 60 35 Sorting Order │ 8 11 1 │ Sort Dialog 9 12 15 82 50 10 Ascending │ List Box ref 10 12 15 50 10 Descending │ Last Name 11 14 80 15 70 95 Second Key │ First Name 12 15 85 30 60 35 I10:I18 5 │ No. and Street 13 14 85 70 60 35 Sorting Order │ "City, State" 14 11 2 │ Zip Code 15 12 90 82 50 10 Ascending │ Phone Number 16 12 90 50 10 Descending │ Hourly Rate 17 14 155 15 70 95 Third Key │ Hours Worked 18 15 160 30 60 35 I10:I18 9 │ Amount Due 19 14 160 70 60 35 Sorting Order │ 20 11 2 │ 21 12 165 82 50 10 Ascending │ B C D E F G H I 21 12 165 82 50 10 Ascending │ 22 12 165 50 10 Descending │ 23 1 170 115 OK │ 24 2 170 130 Cancel │ 25 │ 26 │ Sort 27 │ =DIALOG.BOX(B3:H24) 28 │ =RETURN() ████████████████████████████████████████████████████████████████████████████ Interprogram Communications Using Windows' Dynamic Data Exchange Kevin P. Welch☼ Late in 1985, Microsoft released Windows to the world. Since that time programmers have been exploring the many capabilities that this new environment offers. New concepts like module definitions, resource scripts, and dynamically linked libraries are becoming part of the everyday vernacular of the Windows programmer. One of Microsoft Windows' more exciting capabilities is that of interprocess communication. In the past, MS-DOS(R) applications existed more or less as islands unto themselves, seldom sharing information with one another in a seamless and integrated fashion. With Windows, programs can now be written to function as part of a much larger interconnected system that includes both local and remote applications. The data from the PC down the hall and from the mainframe at the corporate office can now be part of your application. Interprocess Communication Windows accomplishes this level of interconnectivity using three distinct mechanisms: the clipboard, shared memory, and messages. The clipboard lets a user share a selected data object with all interested applications in the system. In most cases the user manually copies the data object into the clipboard (usually with a select-and-copy operation), followed by one or more paste operations into participating programs. Certain applications that are especially interested in the current clipboard contents can, at their request, become part of the clipboard viewer chain. These programs are then notified whenever the user makes any subsequent changes to the clipboard. In general the clipboard is a temporary repository of information and is suited only to transactions that require the manual involvement of the user. Shared memory is another mechanism through which cooperating applications can communicate. This is typically accomplished using either a dynamically linked library (that normally has only one data segment) or commonly held handles to global memory. Changes to the shared memory are available instantly with very little overhead to all participating applications. Unfortunately, most applications that use only shared memory for interprocess communication are limited to data exchange within a group of programs that were designed to work together. The third, and perhaps most natural, interprocess communication mechanism utilizes standard Windows messages: two or more applications send messages notifying each other that data is available and provide some means for obtaining it. In most cases the actual data is passed through the clipboard or by means of a shared global memory handle. As one can imagine, cooperating applications must recognize the messages and process each of them according to a predefined protocol. One refinement of message-based interprocess communication is DDE (Dynamic Data Exchange). DDE is a published message protocol for the exchange of data between Windows programs; it consists of a small set of messages and data structures designed to support this interaction. Any Windows application that has a window and a message-processing loop can be modified to accept these messages, allowing it to participate in a DDE conversation. Because DDE is a published protocol, software developers wishing to use it can do so in their applications. Applications written to support DDE will then be able to participate in an integrated desktop, sharing data with other applications in the Windows environment. Uses of DDE DDE is best suited for situations that require user-independent interprocess communication. In most cases the user would be expected to establish links manually, but from then on each application would share data independently either as it became available or on specific request. Figure 1 lists some of the many possibilities that exist for using DDE. DDE Overview First and foremost, DDE is a message protocol. No changes to the Windows environment are necessary in order to use this standard. Each application is responsible for implementing the message protocol and for managing the conversations in which it participates. The communications model used by DDE follows the client-server paradigm. In this model the client application initiates a conversation with one or more servers. When the server application decides to participate, it acknowledges the client, and the conversation is established. Note that data is not automatically transmitted once the server acknowledges the client. For this to happen, the client must request a one-time transfer of data, or advise the server that it wishes to be provided with a specific item of information on a periodic basis. You will note that data flows from the server to the client and continues until either application decides to terminate the conversation. A DDE conversation is established by utilizing a special set of predefined Windows messages. These messages are transferred between applications using the standard Windows SendMessage and PostMessage commands. The general syntax used (described here for the PostMessage case) is shown in Figure 2. The lParam portion of the message usually consists of an atom combined with a data handle or a status word. The exact composition of this parameter depends on the type of message being sent. For those of you unfamiliar with atoms, an atom is an integer number that uniquely identifies a character string. The character strings are stored in an atom table either on a local or global basis. Locally stored character strings result in atoms that can be used only within one instance of an application. On the other hand, globally stored character strings produce atoms that can be used throughout the entire Windows environment. This enables two cooperating applications to share the same character string in a consistent fashion. Note that in Windows Version 2.0x, global atom management services are supplied by Windows itself; in Version 1.0x, these services are made available through calls to the dynamically linked library DDE.EXE. The functions listed in Figure 3 are present in Windows 2.0 and facilitate the creation, inspection, and deletion of global atoms. The set of messages shown in Figure 4, each with its associated lParam definition, comprises those used in the DDE protocol. The low and high words are combined into a double word with the MAKELONG macro defined in WINDOWS.H. The various atoms (normally indicated by a lowercase "a" before the variable name) are used to reference the type of data supplied/wanted or the type of conversation desired. The hData, hCommands, and hOptions words are handles to global data structures (created using calls to GlobalAlloc); they contain the physical data being passed. In cases where data is being requested or acknowledged, the cfFormat word is used. One of the predefined or registered clipboard data formats, cfFormat enables the client application to request information in a specific rendering. The atom portion of each message is used to implement a three-level communications hierarchy: application, topic, and item. The application atom is used when a conversation is being initiated and specifies the name of the program that is to respond. If the application atom is NULL, any interested application can respond. The topic refers to a logical data context. Topics can consist of filenames (for those applications that are file-based) or any other program-specific character strings that represent related groups of data. Finally, an item refers to a single data object that can be transmitted using a DDE exchange. An item could refer to a single integer, a character string, or even to a more complex data structure such as a bitmap or metafile. DDE conversations are established using the application and topic atoms. It is within this context that the client asks the server to furnish real-time data for a particular data item. To illustrate this communications hierarchy, consider a Microsoft Excel spreadsheet user who wishes to track the price of several computer stocks on the New York Stock Exchange. The user has access to a Windows application called QUOTE that in turn has access to NYSE data (NYSE). The DDE conversation between the spreadsheet and the stock quotation program would proceed according to the following discussion. The user initiates the conversation by supplying the name of the server application and the particular topic of interest, then uses the resulting communication channel to request quotes on specific stocks. In the example given, the following application and topic would be broadcast to all interested applications: application|topic QUOTE|NYSE Upon receiving notification of this request, the stock quotation application sends a positive acknowledgment back to the spreadsheet. By means of a spreadsheet formula, the user can then request to be informed whenever a particular stock quotation changes. For example, to be notified whenever a change in the selling price of IBM, Digital Equipment, and Hewlett-Packard stock occurs, the user need simply define three separate data items and request the QUOTE application to periodically advise the spreadsheet. These data items may be given as: item IBM DEC HP In actual practice, the spreadsheet user would probably initiate the conversation and define the real-time remote references for three separate cells using the following formulas: = QUOTE | NYSE ! IBM = QUOTE | NYSE ! DEC = QUOTE | NYSE ! HP Once some of the building blocks that make up DDE are understood, it becomes easier to explore the actual message protocol and investigate how to implement this scheme in a Windows application. Initiating a Conversation As mentioned earlier, two participating applications must engage in a DDE conversation prior to the real-time exchange of data. The program initiating the conversation is regarded as the client application, while the program responding to the client and furnishing the data is known as the server application. Both the client and server applications must possess a message queue and must contain a separate window for each conversation they engage in. The reasons for these requirements are that in order to receive a message, an application needs a message-processing loop, and also, that DDE messages are sent between two cooperating client and server windows. Applications that need to engage in more than one conversation normally define several hidden windows whose express purpose is to process DDE messages. The resulting window handle then uniquely identifies the conversation. A DDE conversation is initiated by the client application using a SendMessage in the following format. Note that the "──1" window handle will cause the initiate message to be sent to all active windows (including the client window itself): SendMessage( (HWND)-1, WM_DDE_INITIATE,hMyWnd,MAKELONG(aApp,aTopic)); The SendMessage will not return until all the receiving applications in the sequence either process or ignore the initiate message. These applications, after examining the aApp and aTopic atoms, can respond with a positive acknowledgment if interested in participating in a conversation. The pseudocode fragment in Figure 5 illustrates the kind of code present in a server application that responds to an initiate message. Note that both the client and server windows use the Windows SendMessage command when part of an initiate sequence. This is the only case in the DDE specification where SendMessage is used. In all other situations a PostMessage command is issued. The reason why SendMessage is used in this case is so that the client application can guarantee that all interested servers have responded before it proceeds with subsequent initiation steps. If a PostMessage command was sent, the client would have no way of knowing when to expect a response to its initiate request. Permanent Data Links Once a DDE conversation has been established, the client application can advise the server that it is interested in periodic updates on a particular item of data. This "link" constitutes a permanent data stream (or "hot link") and continues either until it is explicitly halted or until either application terminates. Note that the initiation of a conversation does not automatically create this data link: the client must explicitly ask the server to send information as it becomes available. To start this data stream the client application advises the server that it wants periodic updates on a particular data item. Besides providing an atom that represents the data item, the client defines a special global memory data structure that specifies how the data is to be transmitted and in what format it is to be sent. The code fragments in Figure 6 demonstrate how the client can advise a server. The Options data structure (defined as part of the DDE specification) determines if the server expects an acknowledgment to each data item, how the data is formatted, and so on. The server is obliged to respond with either a positive or negative acknowledgment to each advise message. The response enables the client to verify whether the server is truly acting on the advise notification. In situations where the server cannot supply the data in the requested format, the server responds with a negative acknowledgment. After receiving this acknowledgment, the client can then ask the server to furnish the same data item in a different format. This process may be repeated until a format is found that both applications can accept. Once the data link is established, the server notifies the client whenever the data changes. If the fNoData flag (part of the options data structure) is true, the server simply informs the client but does not render the information. The client application, at its discretion, can then request the latest version of the data by performing a regular request transaction. In the normal case the server allocates a block of global memory, formats the information, then informs the client through a data transaction. The code fragment in Figure 7 illustrates this process. Upon receiving the data transaction, the client is expected to extract the desired information and respond to the server according to the defined option flags──a process that may result in releasing the global memory and removing the atom that referenced the data item. This removal prevents the accumulation of unused portions of memory and incorrect atom reference counts. In addition, depending on the kind of transaction, the client may be expected to acknowledge the server. The data link between the two continues until either the server or the client application terminates or until the client requests the data flow to be stopped. To stop the data flow, the client performs an unadvise transaction with the server for a particular data item, a process that is normally accomplished using a code fragment such as the following: /* define data atom */ aItem = GlobalAddAtom((LPSTR)"DataItem" ); /* turn off data flow */ PostMessage(hClientWnd,WM_DDE_UNADVISE,hMyWnd,MAKELONG(NULL,aItem)); One-Time Data Transfer The DDE specification includes two separate mechanisms through which one- time data transfer can be accomplished. The first of these is the request transaction, in which the client asks the server to supply a specific data item. This is done through the following code fragment: PostMessage(hServerWnd,WM_DDE_REQUEST,hMyWnd,MAKELONG(cfFormat,aItem)); If the server has access to the specified item and can render it in the requested format, it supplies the information using a normal data transaction. If the server is unable to satisfy the request (for any reason), it responds with a negative acknowledgment. The second mechanism, called a poke transaction, is a one-time transaction in which the client sends data to the server. In this case the client renders the data in a format of its choice and notifies the server. The code that performs this transaction is very similar to that which transmits real- time data using the WM_DDE_DATA message. The only exception is that in this transaction a WM_DDE_POKE message is used. The server is expected to reply with a positive acknowledgment if it is able to accept the data. In cases when the server is unable to process the data due to format or some other reason, it responds with a negative acknowledgment. These acknowledgments take the following form: PostMessage(hClientWnd,WM_DDE_ACK,hServerWnd,MAKELONG(wResponse,aItem)); Remote Command Execution One of the more interesting DDE transactions is the remote execution of a series of commands by another application. Remote command execution is accomplished using the execute message. The client formats a null-terminated command string and transmits it to the server. The command string syntax proceeds according to the rules listed in Figure 8. The examples in Figure 9 demonstrate how this syntax can be used to generate meaningful command sequences. The client application is responsible for allocating and defining the block of global memory that contains the text command sequences. The resulting data handle is then passed to the server using the following code fragment: PostMessage(hServerWnd,WM_DDE_EXECUTE,hClientWnd,MAKELONG(NULL,hCommands)); The server, upon receipt of the command string, attempts to perform the requested actions. If successful, it responds to the client with a positive acknowledgment; if unsuccessful, a negative acknowledgment is sent. The net result of this transaction is that the client is able to use the server as a "black box" to perform certain operations that the client may not be able to accomplish itself. Ending a Conversation As with most other things, good times often need to come to an end. For a DDE conversation, this happens when either application terminates or when the data link is no longer meaningful. Note that both the server and the client application can end the conversation by performing a terminate transaction. The receiving application is obligated to terminate the conversation and respond with a matching terminate message. The transaction is accomplished using the following code fragment: PostMessage(hServerWnd,WM_DDE_TERMINATE,hClientWnd,MAKELONG(NULL,NULL)); The application initiating the terminate transaction is obligated to wait until all matching terminate responses have been received before closing. During this period, it is expected that the terminating application will not send any additional messages and will accept without processing all incoming messages. (This includes the deletion of all atoms and the release of allocated global memory.) In addition, the originator should be careful not to respond to the matching terminate responses so as to eliminate the possible cyclid condition in which both applications keep responding to the terminate message ad infinitum. DDE Maze Perhaps the best way to understand the DDE protocol is to see how it is used in an actual application. The DDE Maze program is just such an application; besides demonstrating several kinds of DDE transactions, it also functions as a general-purpose programming model that can be used for real-time animations. Overview Conceptually, the DDE Maze application involves the real-time animation of a ball that "bounces" around the client region of a window. The client region contains a number of randomly positioned holes, or rectangles, into which the ball can disappear. Each hole is numbered, and represents a conversation with another instance of the Maze application. When the ball falls into a hole, a DDE transaction occurs in which the ball is transferred to the corresponding Maze instance. The ball then continues to bounce in the new instance until it falls into another hole and is transferred yet again. This process continues indefinitely until the last Maze instance is terminated or the Windows session is closed. The Maze application is a particularly interesting demonstration of DDE in that it simultaneously supports multiple client and server conversations. Although the source code is somewhat long, it is surprisingly small when you consider the complexity of operations performed. Two kinds of holes can exist in each Maze: black ones and white ones. (Refer to Figure 10☼ for the following discussion.) The black ones represent conversations in which the client application has asked to be notified whenever the ball falls into a hole. In this situation the ball is transferred to the client using a standard DDE data transaction. The white holes represent active conversations in which the client application does not want to be notified. In cases such as this the animation continues as if the hole were not present. When you bring up the first instance of the Maze application, it will be positioned in the upper left-hand corner of your display and will contain the caption "DDE Maze - #1." Subsequent instances of Maze will be serially numbered and will result in a row-oriented tiling of the window until the screen is completely covered. Each new instance of Maze will attempt to start a conversation with each of the existing instances. The result of these processes is the display of a randomly arranged set of holes representing each successful conversation. Each of these holes will be either black or white, depending on the notification status of the instance they represent. The system menu of each Maze instance contains two special menu options. The first of these is the "Grab the ball" option. Selecting this option toggles the ball advisory status for the current Maze instance. When this option is checked, other Maze instances are notified that the current window wishes to be informed whenever the ball can be transferred. The second menu option, "Grab the focus," controls how the system focus is handled. If this option is checked, the window will automatically capture the system focus whenever the ball is transferred. When activated it results in an interesting display as the focus shifts from window to window, especially when you have several overlapping Maze applications passing the ball among one another. Communications Model As previously mentioned, from a DDE perspective each instance of Maze is simultaneously a client and a server. Although this might seem a little confusing at first, it helps to separate the Maze functionality into two alter egos: a client side and a corresponding server side. From a client perspective, Maze starts life by initiating a conversation with each of the servers using the application atom of Maze and the topic atom of Ball. Each of the servers, given that they are able to participate in another conversation, respond with a positive acknowledgment to the client. In doing so each of the servers appends the client window handle to its respective client list and displays a white hole in its window. At this point the server does not know the client's window number, and indicates this by displaying a question mark in the hole. After receiving an acknowledgment from each server, the client informs each server of its window number and current advisory status. Each server in turn updates its display by changing the question mark to a number and indicating the client's active advisory status by using a black or white rectangle. To capture the bouncing ball, the user must select the "Grab the ball" option from the client system menu. When this happens the client advises each of its servers that it now wants to grab the ball (done by using the Grab atom). Each of the servers in turn receives and acknowledges this data advisory and responds by changing the client's hole from white to black. When the "Grab the ball" option is disabled, the client unadvises each of its servers, and a corresponding reversal of the initial advisory occurs. During this time the ball continues to be animated and is passed from window to window in a visibly random manner──a process that continues until the client window closes. When this happens the client unadvises and terminates the conversation with each of its servers. If the client has the ball at the time the client window closes, it automatically transfers the ball to the first interested server. From a server perspective the ball is a commodity that is animated for a short period of time and then transferred to a client (upon receipt of which, the client becomes the server for a while). Once a new Maze instance has initiated a conversation, the server is responsible for animating the ball, checking with each iteration to see if it has entered an active hole. When the ball enters an active hole, the server disables animation and notifies the respective client of the situation. The client then responds by continuing the animation of the ball in its window until another hole is encountered. Besides maintaining a conversation by means of the bouncing ball, the server is also capable of reporting vital statistics on the current state of animation. Such reporting enables other interested applications to peek into Maze and be advised of the current operating status. For example Microsoft Excel, a spreadsheet (with perhaps a corresponding chart) could be defined as an application that uses this information in a real-time fashion. General Structure The Maze application is created using six separate files (see Figures 12 through 17). The MAZE file contains the various directives used by the Microsoft Make utility. The MAZE.LNK and MAZE.DEF files contain standard directives used by the linker when building the Maze. MAZE.H is a header file, referenced by the Maze source code, that contains general application definitions and data structures. The MAZE.D file is like MAZE.H, but contains only function definitions. In practice it is commonly useful to separate these definitions from the header file so as to avoid recompiling the entire application when new functions are created or the parameters of old ones change. Finally, MAZE.C contains all the C source code for the application. Note that the application does not have an associated resource (.RES) file. This is because Maze does not have a menu, but uses only static resources and uses its client area as an icon. MAZE.C consists of nine functions that can be grouped into five distinct categories. MazeWndFn is by far the largest and most complicated function and is responsible for processing all the messages dispatched to it by the message retrieval loop. This function, like most other message-processing routines, is basically a very large switch statement that handles every message on a case-by-case basis. Figure 11 summarizes the major functions in the source code. WinMain is the entry point for the Maze application and is responsible for creating the Maze and for retrieving/dispatching all related system messages. However, unlike most other Windows applications, the message- retrieval loop is designed to incorporate the animation requirements of the bouncing ball. Ball.bIsBouncing, a global flag, controls whether animation of the ball is being performed. When this flag is logically true, a PeekMessage operation is performed on the application message queue. If any message is present, it is translated and dispatched in the normal fashion. Note that the PeekMessage function returns true even if a WM_QUIT message is encountered. In a normal message-retrieval loop, this message is not dispatched to the window; hence the check for this special condition. In cases when the ball is not bouncing, a normal GetMessage operation is performed, and the message is translated and dispatched to the Maze window. The message-retrieval process continues until either GetMessage or PeekMessage encounter a WM_QUIT message. When this happens, the loop is broken and Maze is destroyed. The design of the message-processing loop is such that the animation of the maze proceeds only when there are no messages to be processed. This enables the maze to coexist with other applications without consuming undue microprocessor time. MazeWndFn is by far the most complicated portion of the Maze source code. It is responsible for handling both the standard Windows messages and those relating to the various DDE conversations in progress. Throughout this function three main data structures are used; Maze, containing maze-related variables; Ball, containing ball-related variables; and Link[#], a list of active DDE communications links. Note that MazeWndFn is responsible for handling both the client and server sides of a DDE conversation. This combination results in a function that supports more DDE messages than either a dedicated client or server would individually. Furthermore, this function incorporates several safeguards that prevent the possibility of having the window become a server to itself. The following discussion lists each of the messages processed by MazeWndFn and describes in algorithmic terms what is being accomplished. WM_CREATE is the Maze window created using the WS_OVERLAPPEDWINDOW style option defined in WINDOWS.H. The first 15 instances of (MAZE_ROWS * MAZE_COLS) in the application are tiled into the display by using the MoveWindow command. Additional instances use the default sizing supplied by Windows. WM_SYSCOMMAND is generated whenever the user selects one of the system menu options (or moves or resizes the window). Two distinct actions are performed in response to this message. When the user toggles the "Grab the ball" menu option, Maze advises or unadvises each server respectively. Note that the Link data structure is a list of client conversations; however, in this case each of these clients is also a server. When the user toggles the "Grab the focus" menu option, an internal data flag is changed and the menu updated. WM_GETMINMAXINFO is a new Windows 2.0 message generated in conjunction with the CreateWindow function call and enables the window to adjust the default minimum and maximum tracking sizes supplied by the system. In this case we adjust the minimum tracking height to prevent the user from defining a window height less than the current hole height. WM_SIZE involves the randomization of the Maze display, which consists of the random repositioning of each of the client holes using the Link data structure. In addition, the ball is randomly positioned and assigned a new direction, with +1 meaning down or right and ──1 left or up. Note that in several places the Maze sends a WM_SIZE message to itself when the display needs randomizing. In many ways this is equivalent to defining a separate function to handle the WM_SIZE message and calling it whenever necessary. WM_PAINT causes the application to draw all the holes using their currently defined positions. Of particular interest is how the text and background colors are manipulated in order to achieve black on white and white on black text for the numbers inside the client holes. WM_CLOSE is received when the user first attempts to close the Maze window. In normal situations the default window procedure destroys the window and returns. However, when the ball is bouncing, Maze must transfer the ball to the first interested client, thus eliminating the possibility of losing the ball when the user closes the window. Also included under this message is the termination of all active DDE conversations. An unadvise operation for each active data item is required, followed by a terminate transaction. Note that Maze is not allowed to be destroyed until each of the clients has responded to the terminate transaction. This precaution eliminates a situation in which an application attempts to respond with an invalid window handle. WM_DESTROY, as is the case with all other Windows applications, is the second-to-last message received by the window (the last one being WM_NCDESTROY). This message causes the WM_QUIT message to be posted and brings about the subsequent ending of the message retrieval loop. WM_DDE_INITIATE is received whenever a client wishes to initiate a DDE conversation. The proposed conversation is examined and a response made only if the initiate message is deemed acceptable. The conversation is established by appending the caller to the client list and sending an appropriate acknowledgment. Note how the client is asked to reciprocate and also act as a server. The Maze.bInitiate flag prevents recursive reciprocation of this process and enables the correct establishment of the conversation. WM_DDE_ADVISE is received whenever a client wishes to be advised on a particular data item. The application searches to see if a communications link has been established and checks the conditions under which the client wishes to be notified. Note that the application is obligated to respond to the caller and should send a positive acknowledgment only when the advisory conditions are satisfactory. This enables the caller to continue polling the application using different clipboard formats until a mutually agreeable one is found. WM_DDE_UNADVISE is received whenever a client wishes to unadvise the server on a particular data item. As is the case with the advise message, the application searches to verify the communications link and posts an appropriate acknowledgment. Note how the display is invalidated whenever the advisory status changes. This enables the user to verify the current communications state of each data link visually. WM_DDE_REQUEST is received whenever a client desires a one-time notification on a particular data item. The Maze application supports only requests for the Statistics data item in the CF_TEXT format. If the request is acceptable, an immediate data transaction is generated. Note that the transaction is specially flagged to indicate that it is in response to a request. The DDE request transaction is normally encountered immediately after a data link is made and an advisory established, enabling the client to immediately obtain the data in question without waiting for the next notification. WM_DDE_POKE and WM_DDE_DATA The poke message is received whenever a client wishes to inform a server of its window number. On the other hand, the data message is received when the server passes the ball to a client. This section of the application can be particularly confusing, since the same code is used for both the client and server sides of the conversation. Data transmitted by the Maze application is in the CF_TEXT format and consists of two numbers separated by tab characters. In the poke case, the first value is the window number followed by the client window focus state. This focus state is logically true if the client currently has possession of the system focus. In the data case, the first value represents the window number of the hole into which the ball fell followed by the server focus state. If Maze.bGrabFocus is true and the server currently has the focus, the client manually sets the focus to itself. WM_DDE_ACK in normal situations is received in response to an initiate or advisory operation. This is perhaps one area in which Maze takes a rather cavalier attitude to acknowledgements. The application currently takes any action only when a response to an initiate operation is received. When this occurs, the global atoms are deleted, and the server is informed of the client's window number and current advisory status. A sophisticated DDE application would probably want to handle situations in which negative acknowledgements were received. In such cases, any dangling portions of global memory would need to be released and associated atoms deleted. WM_DDE_TERMINATE is received when a client or server wishes to end a conversation. If the link is still active, the caller is unadvised on all active data items and a matching terminate is sent. In cases where the application is waiting to be destroyed, the terminate messages are counted until the last response is received, upon which the window is destroyed. CreateMaze is responsible for the creation and initialization of the Maze application, including the definition of all global atoms. If no previous instance of the program exists, the first instance is assigned a window number of one, and the window class is registered. If a previous instance is present, a search process is initiated to find the lowest unused window number. The resulting value then becomes the assigned window number. Once the window is created, an initiate message is sent to all windows in the environment. This is accomplished by performing a SendMessage to a window handle of -1 with the generic maze topic and the aBall atom. Only when each interested application responds to the initiate message is the Maze window made visible. DestroyMaze is called only after the main window message-processing loop is terminated. This function is responsible for removing all defined global atoms and returning the final exit code to the system. Animation of the bouncing ball is accomplished using the BounceBall function. At each iteration, this function checks to see if the ball has entered one of the client area holes. If the ball has not fallen into a hole, a new position is calculated from its current position. This calculation considers the ball's current direction of motion and its reflections off the client area boundaries. The new ball position is indicated by retrieving the window display context and inverting the ball's location. One interesting variant of this code is to insert an InvertRect function call prior to calculating the new ball position. This eliminates the trail the ball leaves behind as it bounces around the window. If the ball has fallen into a hole, it is passed to the respective client using a DDE data transaction and removed from the current window. In addition, the ball's elapsed time in Maze is calculated, and all interested clients informed. The first data value furnished represents the number of milliseconds the ball was bouncing in Maze. The second one represents the current area (in pixels) of the Maze client area. One would expect that, on average, the time spent in a particular Maze instance would be proportional to its window area. The Advise function is one that could be part of almost any generic DDE application. The sole purpose of this function is to advise a server that a client is interested in a particular data item. In this case the advisory process is limited to asking for data without acknowledgment in the CF_TEXT format. Note that Advise does not make any assumptions regarding the success or failure of the GlobalAlloc, GlobalLock, or PostMessage functions. One would expect GlobalAlloc to fail in situations where memory is scarce, but few realize that GlobalLock can also fail when one is using expanded memory. Although Maze does not attempt to recover from this situation, a serious implementation of the DDE specification should be able to handle this case. Data is transferred between two Maze instances using the Transmit function. As with the Advise routine, this function involves the allocation and locking of a portion of global memory. However, unlike the Advise routine, the Transmit function also formats the supplied data as parameters into the global memory block. After unlocking the memory, the handle is transmitted to the client as part of a data or poke transaction. The FindLink function is a utility used by the Maze application to find a particular communications link. Given the window handle of the caller, it searches the client list and returns the communications channel used. Using this function, the Maze can determine the exact characteristics of the link in a robust and link-independent fashion. Lastly, the BumpGlobalAtom function increments the reference count for a specified global atom. This is useful when sending information as part of a data or poke transaction. The DDE specification requires that the recipient of such a transaction delete the atoms sent. BumpGlobalAtom allows the server application to increment this count without having access to the original character string. Building the Maze As previously mentioned, the Maze application is created from six separate files. To build the program, you will need to have a Microsoft C Compiler (Version 4.0 or greater) and have access to a copy of the Windows 2.0 Software Development Kit (SDK). The SDK will contain the WINDOWS.H include file as well as the WINSTUB.EXE program specified in the STUB line of MAZE.DEF. Using the Make utility that comes with the SDK, you can compile and link the Maze program by using the following command: MAKE MAZE Note that the MAZE.DEF file makes the assumption that the WINSTUB.EXE program is present in your \BIN directory. If this is not the case, you will have to change this to reflect your current hard disk directory structure. Using Maze The DDE Maze, unlike many other Windows applications, is best viewed in large numbers. After creating the application, it is suggested that you start up at least three instances by selecting the program in the MS-DOS executive and starting it in an iconic form. When you have these icons displayed at the bottom of your screen, you turn on the "Grab the Ball" option and restore each to its suggested size. The end result will be a tiled arrangement of the Maze instances with the ball visible and bouncing from Maze to Maze. Using Maze, you can experiment by bringing up additional Maze instances and observing how new conversations are displayed in the client area of each existing instance. You can turn on the "Grab the focus" option and also watch the input focus being passed along with the bouncing ball. This can be used to create an interesting visual effect when various overlapping windows are present in the system. Microsoft(R) Excel One of the more interesting experiments you can conduct with the Maze application is with Microsoft Excel. Using Microsoft Excel, you can query Maze and create a spreadsheet that incorporates the latest bouncing ball statistics. To do this you would bring up the spreadsheet program and create an array formula containing the following text: =Maze|Ball!Statistics To create this array formula, you would select two adjacent cells (by dragging the mouse) and type in the formula. After typing the text, you would make it part of the spreadsheet by using the Ctrl-Shift-Enter keychord, which creates an array formula in which the two data items supplied by Maze are displayed in adjacent columns. After entering the keychord, you will be asked to choose from the list of available instances. This is because Microsoft Excel supports only one communications link per remote link (supporting more wouldn't really make much sense in a spreadsheet anyway). Once you select the instance you wish to communicate with, the values in the two adjacent cells should change as the ball bounces between Maze instances. The first data value represents the number of milliseconds the ball spent in the referenced Maze before "falling" into a hole. The second data value represents the area (in pixels) of the Maze client area. By repeating this same operation, you can establish remote links to each of the Maze instances currently executing. The end result is a table of data values that change in response to the bouncing ball. If you wish you can create a chart that uses this data and view the results as they change in real time. If you wish to establish an external reference to a particular Maze instance (rather than having to manually select which one you want to talk to), you can enter the following formula, in which the number sign (#) represents the Maze instance number: =Maze#|Ball!Statistics Inside DDE Supporting DDE in an application can present some formidable challenges, especially when multiple conversations are being simultaneously maintained in a general fashion. Unfortunately the previous code fragments (and even the Maze application to a lesser extent) are relatively simplistic in their implementation of the DDE specification. When designing a commercial application that supports DDE, you are best advised to refer to the original DDE specification (available on DIAL or directly from Microsoft). In most respects this document is a comprehensive and authoritative guide to the protocol; however, you will probably have to refer to various supplemental programs (such as Maze) before the scheme of things becomes apparent. Despite its completeness, the DDE specification is rather weak in some areas. Perhaps one of its most significant shortcomings lies in the area of message processing and synchronization. As mentioned, DDE is based on the orderly processing of all system messages. However, in situations where multiple conversations are being held, it is very difficult to keep track of the exact state of each transaction. It is deceptively simple to create a situation where two applications engage each other in an endless DDE slugging match. This might best be illustrated by the manner in which Maze uses two simultaneous client-server conversations to emulate peer-to-peer interaction. Since Maze responds to each interesting initiate message with both an acknowledgment and another initiate transaction, one can see how two instances could become incessantly locked. Further complicating the situation is the fact that the first initiate transaction is also received by the Maze instance that originated it. Another situation, perhaps even more insidious, concerns the way in which an application responds to negative acknowledgments (or even the lack of any acknowledgment if one is expected). The Maze application blithely ignores such situations, opening the way for conditions in which unreferenced portions of global memory are accumulated and global atoms left needlessly bumped. A sophisticated DDE application (Microsoft Excel, for example) is forced to compensate for this situation by tracking the exact state of each transaction. Although such tracking yields some measure of security when communicating with unruly or uncooperative applications, it dramatically increases the complexity of the DDE handling portion of the application. A second underemphasized area of great importance is that of shared memory and global atoms. Great care must be taken to allocate and release such resources correctly. Each of the DDE transactions has specific synchronization rules that must be followed to ensure that resources remain available. One situation in which this is particularly important is the handling of global atoms in a data transaction. In this case the server is expected to bump the atom reference count prior to initiating the transaction. The client, on the other hand, is expected to delete the atom and re-add it if an acknowledgment is expected. Failure to bump or re-add the atom results in a condition where the atom mysteriously disappears after several DDE transactions. Similarly, failure to correctly delete the atom results in abnormally high reference counts that could potentially mask other communications problems. A third area barely covered in the DDE specification concerns the system topic. This is a special topic (using the name "System") that furnishes a context for items of general information available to partners in a DDE conversation. Supporting this topic involves the creation of a new (probably hidden) window and the definition of several data items for broadcast to interested clients. Although not mentioned in the specification, it is highly recommended that DDE applications support the system topic. When this topic is available, potential conversants can conduct enquiries regarding the current application status or obtain a list of available topics and data renderings supported by the server. One extension to the current DDE specification is that of paste links. A paste link is a manually defined data link where the server provides the conversation parameters. Upon user request, the server application would supply the clipboard with a specially formatted "Link" data object. This object consists of three null-terminated strings which contain the name of the server, available topic, and data item. Any potential client, including Microsoft Excel, can recognize this format and establish the conversation using the parameters present on the clipboard. One final area not covered by the DDE specification is that of debugging. Unfortunately no commercial tool is currently available that facilitates the interception and interpretation of DDE messages on a global basis. In the interim, the code fragment in Figure 18 may be of some use as you try to instrument at least your side of the conversation. This routine is called in much the same way as the standard printf function, with the difference being that the resulting string is displayed using a system message box. The examples in Figure 19 illustrate how it could be used. Note that although the debugging monitor will look good in most situations, problems arise when it is used in conjunction with a message transmitted by a SendMessage function call. This is because it effectively blocks the application that is waiting for the SendMessage call to return. Because of this, the monitor function should not be used to trap WM_DDE_INITIATE messages. Conclusion By now, hopefully, you have a much better feel for the way in which Dynamic Data Exchange operates and are ready to try experimenting with the protocol in your application. To be fair, a consistent and well-structured implementation of DDE is a considerable development challenge; however, the benefits obtained by the seamless integration of data between applications more than outweigh the additional effort. Of all the capabilities offered by Windows, DDE represents one of the most exciting new prospects, paving the way to a world of consistent and easy-to- use applications that cooperate together as never before. Despite its many shortcomings and simplistic approach, the DDE Maze further demonstrates the potential of this message protocol. With a little effort you can easily generalize the concepts presented here into a robust and efficient formulation upon which the DDE modules of your application can be based. Figure 1: Uses of DDE Hot links to real-time data, such as to stock market updates, scientific instruments, or process control instrumentation. Mission-critical transactions (often using automated data links), such as airline reservations or commodity price reporting systems. Creation of compound documents, such as a word processing document that includes a chart produced by a graphics program. Using DDE, the chart will change inside the document when the underlying data is modified. Interapplication data queries, such as a spreadsheet querying a database application for accounts past due. Intercomputer data queries, such as a Windows application requesting data from the PC down the hall or the corporate mainframe. Figure 2: General Syntax Used to Invoke DDE PostMessage( hWndDest, wMessage, wParam, lParam ) hWndDest = destination window handle wMessage = DDE message number wParam = sender's window handle lParam = assorted message arguments Figure 3: Global Atom Functions aAtom = GlobalAddAtom( lpString ) This function adds the character string referenced by lpString to the global atom table and returns the atom number. If the string is already present in the table the function returns the existing atom value and increments the string's reference count by 1. nLength = GlobalGetAtomName( aAtom, lpString, nSize ) This function retrieves a copy of the character string associated with the atom and places it in the buffer provided. The length returned is 0 if the specified atom is invalid. aAtom = GlobalFindAtom( lpString ) This function searches the atom table for the character string referenced by lpString and returns the atom associated with it. A NULL atom is returned if the string is not present in the table. aOldAtom = GlobalDeleteAtom( aAtom ) This function deletes the atom and, if the atom's reference count is zero, removes the associated string from the global atom table. Figure 4: DDE Messages Summary ╓┌──────────────────┌─────────────────────────────┌──────────────┌───────────► ┌─Arguments in IParem─┐ DDE Message Purpose Low Word High Word WM_DDE_INITIATE Request initiation of a aApplication aTopic conversation WM_DDE_TERMINATE Terminate a current (reserved) (reserved) conversation WM_DDE_ACK Acknowledge a DDE message In reply to INITIATE aApplication aTopic ┌─Arguments in IParem─┐ DDE Message Purpose Low Word High Word In reply to INITIATE aApplication aTopic In reply to EXECUTE wStatus hCommands In reply to all others wStatus aItems WM_DDE_REQUEST Request to server cfFormat aItem to provide a data item WM_DDE_DATA Notify a client application hData aItem of the availability of data WM_DDE_ADVISE Request to a server to hOptions aItem supply an update for a data item whenever it changes WM_DDE_UNADVISE Request to a server that a (reserved) aItem specified data item should no longer be updated WM_DDE_POKE Request to an application hData aItem ┌─Arguments in IParem─┐ DDE Message Purpose Low Word High Word WM_DDE_POKE Request to an application hData aItem to accept an unsolicited data item WM_DDE_EXECUTE Sends a string to a server (reserved) hCommands application to be processed as a series of commands Abbreviations Used Above: aName: An atom of word length (16 bits) hName: A handle of word length to a global memory object cfFormat: A registered clipboard format number of word length wName: Any other word-length parameter Figure 5: Responding to WM_DDE_INITIATE aApp = application atom provided by client aTopic = topic atom client aClientWnd = handle to client application (who initiated conversation) if ( (aMyApp == aApp) || (aApp == NULL) ) { if ( aTopic ) { if ( aMyTopic == aTopic ) { SendMessage( hClientWnd, WM_DDE_ACK, hMyWnd, lParam ); } else { for ( Each supported topic ) SendMessage( hClientWnd, WM_DDE_ACK, hMyWnd, MAKELONG(aApp,aMyTopic) ); } } } Figure 6: Client Advising a Server /* create global data block */ hOptions = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(OPTIONS) ); if ( hOptions ) { /* lock options data structure */ lpOptions = GlobalLock( hOptions ); if ( lpOptions ) { /* define advise options data structure */ lpOptions->fAck = TRUE; lpOptions->fNoData = FALSE; lpOptions->cfFormat = CF_TEXT; GlobalUnlock( hOptions ); /* advise server to send data */ PostMessage( hServerWnd, WM_DDE_ADVISE, hMyWnd, MAKELONG(hOptions,aItem) ); } } Figure 7: Posting Data to a Client /* allocation global block of memory & define item atom */ hMemory = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(DATA) ); if ( hMemory ) { /* lock allocated data block - may fail with expanded memory! */ lpMemory = GlobalLock( hMemory ); if ( lpMemory ) { /* define data flags */ lpMemory->fAck = TRUE; lpMemory->fNoData = FALSE; lpMemory->cfFormat = CF_TEXT; /* DEFINE GLOBAL MEMORY HERE */ lstrcpy( lpMemory->lpData, lpDataString ); /* unlock data & define item atom */ GlobalUnlock( hMemory ); aItem = GlobalAddAtom( "DataItem" ); /* post data to client */ PostMessage( hClientWnd, WM_DDE_DATA, hMyWnd, MAKELONG(hMemory,aItem) ); } } Figure 8: Rules for Building Command Strings := ... := [ ] := | := application defined string token := ( ,,... ) := application defined string token Figure 9: Command String Examples [file][open("sample.txt")][print] [dial("1-800-123-4567")][connect][transmit("sample.txt")][disconnect] [datafile][find("money","devaluation")][report] Figure 11: Major Sections of MAZE.C ╓┌───────────────┌───────────────────┌───────────────────────────────────────╖ Function Category General Description WinMain mainline system mainline and vessage-retrieval loop ─────────────────────────────────────────────────────────────────────────── MazeWndFn Window related maze window function and message processing CreateMaze initialization of maze and initial communications DestroyMaze termination of maze and atom removal ─────────────────────────────────────────────────────────────────────────── BounceBall animation animates bouncing ball inside maze ─────────────────────────────────────────────────────────────────────────── Advise communications advise server on particular data item Function Category General Description Transmit transmit data using POKE or DATA ─────────────────────────────────────────────────────────────────────────── FindLink utility functions find a communications link using window handle BumpGlobalAtom increment global atom reference count Figure 12: MAKE is the MAZE Make file CFLAGS=-c -u -AS -FPa -Gsw -Os -Zep maze.obj: maze.h maze.c cl $(CFLAGS) maze.c maze.exe: maze.obj maze.def maze.lnk link4 @maze.lnk Figure 13: MAKE.DEF is the MAZE Definitions File NAME Maze DESCRIPTION 'Dynamic Data Exchange Maze' STUB '\BIN\WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 2048 STACKSIZE 2048 EXPORTS MazeWndFn @1 Figure 14: MAZE.LNK is the MAZE Link file maze /align:16 maze maze slibw maze.def Figure 15: MAZE.H is the Maze header file /* * DDE MAZE - HEADER FILE * * LANGUAGE : Microsoft C * MODEL : Small * STATUS : Operational * * 09/22/87 - Kevin P. Welch - initial creation. */ #define MAZE_COLS 3 #define MAZE_ROWS 5 #define SC_GRAB_BALL 0x0100 #define SC_GRAB_FOCUS 0x0101 #define HI HIWORD(lPrm) #define LO LOWORD(lPrm) #define RGB_BLACK RGB(0x00,0x00,0x00) #define RGB_WHITE RGB(0xFF,0xFF,0xFF) /* * BALL DATA STRUCTURE DEFINITIONS * */ #define BALL_WIDTH 6 #define BALL_HEIGHT 6 #define RANDOM_MOTION (((rand()%2)*2)-1) #define H_BOUNCE(a,b) ((a<=0)?1:((a>=b)?-1:Ball.iHorzMotion)) #define V_BOUNCE(a,b) ((a<=0)?1:((a>=b)?-1:Ball.iVertMotion)) #define ADVISE_OFF (!Link[i].bAdviseBall) #define OUTSIDE_LEFT (Ball.rPosn.left<=Link[i].rHole.left) #define OUTSIDE_RIGHT (Link[i].rHole.right<=Ball.rPosn.right) #define OUTSIDE_TOP (Ball.rPosn.top<=Link[i].rHole.top) #define OUTSIDE_BOTTOM (Link[i].rHole.bottom<=Ball.rPosn.bottom) #define OUTSIDE_WIDTH ((OUTSIDE_LEFT)||(OUTSIDE_RIGHT)) #define OUTSIDE_HEIGHT ((OUTSIDE_TOP)||(OUTSIDE_BOTTOM)) #define OUTSIDE_HOLE ((ADVISE_OFF)||(OUTSIDE_WIDTH)||(OUTSIDE_HEIGHT)) typedef struct { RECT rPosn; /* current ball position */ long lTimeIn; /* time ball entered maze */ int iHorzMotion; /* vertical ball motion offset */ int iVertMotion; /* horizontal ball motion offset */ BOOL bIsBouncing; /* ball bouncing flag */ } BALL; /* * MAZE DATA STRUCTURE DEFINITIONS * */ #define DISPLAY_WIDTH (Maze.wWidth+BALL_WIDTH) #define DISPLAY_HEIGHT (Maze.wHeight+BALL_HEIGHT) typedef struct { WORD wNum; /* maze window number */ WORD wLinks; /* number of maze links */ WORD wWidth; /* width of maze client area */ WORD wHeight; /* height of maze client area */ BOOL bInitiate; /* in initiate flag */ BOOL bGrabBall; /* grab the ball flag */ BOOL bGrabFocus; /* grab the focus flag */ WORD wGoingAway; /* maze going away counter */ } MAZE; /* * COMMUNICATION LINK DATA STRUCTURE DEFINITIONS * */ #define MAX_LINK 32 #define HOLE_WIDTH 20 #define HOLE_HEIGHT 14 typedef struct { HWND hWnd; /* client window handle */ WORD wNum; /* client window number */ RECT rHole; /* client hole position */ BOOL bAdviseBall; /* advise client of ball flag */ BOOL bAdviseStat; /* advise client of stats flag */ } LINK; /* * DYNAMIC DATA EXCHANGE DEFINITIONS * */ #define ACCEPTED 0x8000 #define REJECTED 0x0000 #define WM_DDE_INITIATE 0x03e0 #define WM_DDE_TERMINATE 0x03e1 #define WM_DDE_ADVISE 0x03e2 #define WM_DDE_UNADVISE 0x03e3 #define WM_DDE_ACK 0x03e4 #define WM_DDE_DATA 0x03e5 #define WM_DDE_REQUEST 0x03e6 #define WM_DDE_POKE 0x03e7 #define WM_DDE_EXECUTE 0x03e8 #define SEND(a,b,c) SendMessage(a,b,hWnd,c) #define POST(a,b,c) PostMessage(a,b,hWnd,c) #define DDE_INITIATE(a,b,c) SEND(a,WM_DDE_INITIATE,MAKELONG(b,c)) #define DDE_TERMINATE(a) POST(a,WM_DDE_TERMINATE,0L) #define DDE_ADVISE(a,b) Advise(a,hWnd,b) #define DDE_UNADVISE(a,b) POST(a,WM_DDE_UNADVISE,MAKELONG(0,b)) #define DDE_ACK(a,b,c) POST(a,WM_DDE_ACK,MAKELONG(b,c)) #define DDE_DATA(a,b,c,d,e) Transmit(a,hWnd,WM_DDE_DATA,b,c,d,e) #define DDE_POKE(a,b,c,d,e) Transmit(a,hWnd,WM_DDE_POKE,b,c,d,e) typedef struct { WORD fEmpty:12; /* reserved for future use */ WORD fResponse:1; /* in response to request */ WORD fRelease:1; /* release data */ WORD fNoData:1; /* null data handle ok */ WORD fAck:1; /* Ack expected */ WORD cfFormat; /* clipboard data format */ BYTE info[30]; /* data buffer */ } DATA; typedef DATA * PDATA; typedef DATA FAR * LPDATA; Figure 16: MAZE.D is the MAZE Function Definitions File /* * DDE MAZE - FUNCTION DEFINITIONS * * LANGUAGE : Microsoft C * MODEL : Small * STATUS : Operational * * 09/22/87 - Kevin P. Welch - initial creation. * */ VOID BounceBall ( HWND ); WORD DestroyMaze ( WORD ); ATOM BumpGlobalAtom( ATOM ); BOOL FindLink ( WORD *, HWND ); BOOL Advise ( HWND, HWND, ATOM ); HWND CreateMaze ( HANDLE, HANDLE, WORD ); BOOL Transmit ( HWND, HWND, WORD, ATOM, BOOL, LONG, LONG ); LPSTR FAR PASCAL lstrcpy( LPSTR, LPSTR ); LONG FAR PASCAL MazeWndFn( HWND, WORD, WORD, LONG ); Figure 17: MAKE.C is the Source Code Listiong for MAZE /* * DDE MAZE - SOURCE CODE * * LANGUAGE : Microsoft C * MODEL : Small * STATUS : Operational * * 09/22/87 - Kevin P. Welch - initial creation. * * The DDE Maze demonstrates how Dynamic Data Exchange (DDE) can be * used in multiple server, multiple client conversations. The maze * supports two distinct data items - one used in passing the animated * ball between instances of the Maze, the other for reporting vital * Maze statistics to an interested listener. * * Note that the DDE techniques demonstrated in this program are * a simplification of the general specification. In ALL cases * where you observe differences between this application and the * specification, please follow the published protocol. * * Special thanks to Ed Fries, Geoff Nichols, and David West as * they helped me see the forest from the trees! * */ #include #include "maze.h" #include "maze.d" ATOM aWnd; /* window number item */ ATOM aBall; /* bouncing ball topic */ ATOM aGrab; /* grab the ball item */ ATOM aStat; /* statistics item */ ATOM aAnyMaze; /* generic maze atom */ ATOM aThisMaze; /* this maze atom */ MAZE Maze; /* maze data structure */ BALL Ball; /* ball data structure */ LINK Link[MAX_LINK]; /* maze communications link */ /* * MAZE MAINLINE & MESSAGE PROCESSING LOOP * * hInst current instance handle * hPrevInst previous instance handle * lpsCmd execution command line string * wCmdShow initial show-window option * * This mainline is responsible for calling the initialization and * termination routines in addition to processing and distributing * all incoming messages. Note the revised message processing * loop used to animate the bouncing ball. * */ WORD PASCAL WinMain( hInst, hPrevInst, lpsCmd, wCmdShow ) HANDLE hInst; HANDLE hPrevInst; LPSTR lpsCmd; WORD wCmdShow; { MSG Msg; /* current system message */ HWND hWnd; /* maze window handle */ /* create & initialize maze */ hWnd = CreateMaze( hInst, hPrevInst, wCmdShow ); /* message processing loop */ do { /* retrieve next message */ if ( Ball.bIsBouncing ) { if ( PeekMessage(&Msg,NULL,0,0,TRUE) ) { if ( Msg.message != WM_QUIT ) { TranslateMessage( &Msg ); DispatchMessage( &Msg ); } } else BounceBall( hWnd ); } else if ( GetMessage(&Msg,NULL,0,0) ) { TranslateMessage( &Msg ); DispatchMessage( &Msg ); } } while ( Msg.message != WM_QUIT ); /* destroy maze & exit */ exit( DestroyMaze(Msg.wParam) ); } /* * MAZE WINDOW MESSAGE PROCESSING FUNCTION * * hWnd window handle * wMsg window message number * wPrm additional message info * lPrm additional message info * * This function processes all the messages related to the maze * window, including all the DDE messages required in order to * participate in one or more conversations. Note that this * window function handles DDE messages for both the client and * the server sides of the conversation! * */ LONG FAR PASCAL MazeWndFn( hWnd, wMsg, wPrm, lPrm ) HWND hWnd; WORD wMsg; WORD wPrm; LONG lPrm; { WORD i; /* channel number */ LONG lAck; /* acknowledgement of message */ /* initialization */ lAck = FALSE; /* process message */ switch( wMsg ) { case WM_CREATE : /* create window */ /* adjust window position if tileable */ if ( Maze.wNum <= MAZE_ROWS*MAZE_COLS ) MoveWindow( hWnd, (Maze.wWidth*((Maze.wNum-1)%MAZE_COLS)), (Maze.wHeight*((Maze.wNum-1)/MAZE_COLS)), Maze.wWidth, Maze.wHeight, TRUE ); break; case WM_SYSCOMMAND : /* system command */ /* process sub-message */ switch( wPrm ) { case SC_GRAB_BALL : /* inform all clients */ if ( Maze.bGrabBall ) { Maze.bGrabBall = FALSE; for (i=0; icfFormat == CF_TEXT ) { wAnswer = ACCEPTED; Link[i].bAdviseBall |= (HI==aGrab); Link[i].bAdviseStat |= (HI==aStat); GlobalUnlock( (HANDLE)LO ); GlobalFree( (HANDLE)LO ); InvalidateRect( hWnd, NULL, TRUE ); } else GlobalUnlock( LO ); } } /* respond to message */ DDE_ACK( wPrm, wAnswer, HI ); } break; case WM_DDE_UNADVISE : /* stop advising client on DDE item */ { WORD wAnswer; /* answer to advise message */ LPDATA lpData; /* temporary advise structure */ /* initialization */ wAnswer = REJECTED; /* search for link */ if ( FindLink(&i,wPrm)&&((HI==aGrab)||(HI==aStat)) ) { wAnswer = ACCEPTED; if (HI==aGrab) Link[i].bAdviseBall=FALSE; if (HI==aStat) Link[i].bAdviseStat=FALSE; InvalidateRect( hWnd, NULL, TRUE ); } /* respond to message */ DDE_ACK( wPrm, wAnswer, HI ); } break; case WM_DDE_REQUEST : /* client requestion data item */ { WORD wAnswer; /* answer to advise message */ LPDATA lpData; /* temporary advise structure */ if ( FindLink(&i,wPrm)&&(HI==aStat)&&(LO==CF_TEXT) ) DDE_DATA( Link[i].hWnd, aStat, TRUE, (Ball.bIsBouncing)?GetCurrentTime()-Ball.lTimeIn:0L, DISPLAY_WIDTH*(LONG)DISPLAY_HEIGHT ); else DDE_ACK( wPrm, REJECTED, HI ); } break; case WM_DDE_POKE : /* client providing unsolicited data item */ case WM_DDE_DATA : /* server providing data item */ { WORD wData1; /* window number */ WORD wData2; /* focus state */ LPDATA lpData; /* temporary advise structure */ BOOL bRespond; /* boolean respond flag */ WORD wResponse; /* response to message */ char sString[30]; /* temporary data string */ /* initialization */ bRespond = TRUE; wResponse = REJECTED; /* search for link & check data item */ if ( FindLink(&i,wPrm)&&((HI==aGrab)||(HI==aWnd)) ) { /* retrieve data - lock may not succeed */ lpData = (LPDATA)GlobalLock( LO ); if ( lpData ) { /* check format */ if ( lpData->cfFormat == CF_TEXT ) { /* extract data */ lstrcpy( (LPSTR)sString, lpData->info ); sscanf( sString, "%u\t%u", &wData1, &wData2 ); /* process data */ if ( HI == aGrab ) { /* grab ball */ Ball.bIsBouncing = TRUE; Ball.lTimeIn = GetCurrentTime(); if (Maze.bGrabFocus && wData2) SetFocus(hWnd); /* randomize display */ SEND( hWnd, WM_SIZE, MAKELONG(DISPLAY_WIDTH,DISPLAY_HEIGHT) ); } else Link[i].wNum = wData1; /* Determine if acknowledgement required */ if ( !lpData->fAck ) { bRespond = FALSE; GlobalDeleteAtom( HI ); } else wResponse = ACCEPTED; /* unlock memory & free if required */ if ( lpData->fRelease ) { GlobalUnlock( LO ); GlobalFree( LO ); } else GlobalUnlock( LO ); /* force system to repaint display */ InvalidateRect( hWnd, NULL, TRUE ); } else GlobalUnlock( LO ); } } /* respond to caller */ if (bRespond) DDE_ACK(wPrm,wResponse,HI); } break; case WM_DDE_ACK : /* DDE acknowledgement */ if ( Maze.bInitiate ) { /* delete atoms - since bumped */ GlobalDeleteAtom( LO ); GlobalDeleteAtom( HI ); /* inform server of window number */ DDE_POKE( wPrm, aWnd, FALSE, (LONG)Maze.wNum, (LONG)((GetFocus()==hWnd)?TRUE:FALSE) ); /* inform server of current advise status */ if ( Maze.bGrabBall ) DDE_ADVISE( wPrm, aGrab ); } break; case WM_DDE_TERMINATE : /* end a DDE conversation */ if ( FindLink(&i,wPrm) ) { /* respond with an unadvise on all items */ if (Link[i].bAdviseBall) DDE_UNADVISE(wPrm,aBall); if (Link[i].bAdviseStat) DDE_UNADVISE(wPrm,aStat); /* remove caller from list */ memcpy(&Link[i],&Link[i+1],(Maze.wLinks-i-1)*sizeof(LINK)); Maze.wLinks--; /* respond with a matching terminate & update display */ DDE_TERMINATE( wPrm ); InvalidateRect( hWnd, NULL, TRUE ); } else if ( (Maze.wGoingAway)&&(Maze.wGoingAway-- == 1) ) DestroyWindow( hWnd ); break; default : /* pass on to default */ lAck = DefWindowProc( hWnd, wMsg, wPrm, lPrm ); break; } /* return result */ return( lAck ); } /* * CREATE DDE MAZE * * hInst current instance handle * hPrevInst previous instance handle * wCmdShow initial show window command * * This function creates and initializes the Maze, including the * definition of all global atoms. A handle to the maze window is * returned if the entire process is successful. * */ static HWND CreateMaze( hInst, hPrevInst, wCmdShow ) HANDLE hInst; HANDLE hPrevInst; WORD wCmdShow; { WORD i; /* temporary loop variable */ HWND hWnd; /* new maze window handle */ HMENU hMenu; /* system menu handle */ WORD wQueue; /* queue length counter */ BOOL bSearch; /* boolean search flag */ BOOL bPresent; /* number present flag */ WNDCLASS WndClass; /* window class structure */ BYTE sCaption[64]; /* current window caption */ /* perform instance specific initialization */ if ( !hPrevInst ) { Maze.wNum = 1; Ball.bIsBouncing = TRUE; Ball.lTimeIn = GetCurrentTime(); memset( &WndClass, 0, sizeof(WNDCLASS) ); WndClass.lpszClassName = (LPSTR)"MazeWindow"; WndClass.hCursor = LoadCursor(NULL,IDC_ARROW); WndClass.lpszMenuName = (LPSTR)NULL; WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = MazeWndFn; WndClass.hInstance = hInst; WndClass.hIcon = NULL; WndClass.hbrBackground = (HBRUSH)(COLOR_MENU + 1); } else { GetInstanceData( hPrevInst, (NPSTR)&Maze, sizeof(MAZE) ); GetInstanceData( hPrevInst, (NPSTR)Link, MAX_LINK*sizeof(LINK) ); Link[Maze.wLinks++].wNum = Maze.wNum; bSearch = TRUE; Maze.wNum = 1; while ( bSearch ) { bPresent = FALSE; for (i=0; ifAck = FALSE; lpData->fNoData = FALSE; lpData->cfFormat = CF_TEXT; /* unlock prior to sending */ GlobalUnlock( hMem ); /* notify server to send data */ bResult = PostMessage( hToWnd, WM_DDE_ADVISE, hFromWnd, MAKELONG(hMem,aItem) ); } else GlobalFree( hMem ); } /* return result */ return( bResult ); } /* * TRANSMIT DDE DATA TO CLIENT * * hToWnd destination window handle * hFromWnd server window handle * wMsg message number to use * aItem atom representing data item * bResp data in response to a request * l1 first portion data item to send * l2 second portion data item to send * * This function enables the calling routine to transmit data to a * client window using either a DDE_DATA or DDE_POKE messgae. It is * assumed that the information is sent in CF_TEXT format and does not * require the client to respond. A value of TRUE is returned if the * entire process is successful. * */ static BOOL Transmit( hToWnd, hFromWnd, wMsg, aItem, bResp, l1, l2 ) HWND hToWnd; HWND hFromWnd; WORD wMsg; ATOM aItem; BOOL bResp; LONG l1; LONG l2; { /* local variables */ HANDLE hMem; /* temporary memory handle */ LPDATA lpData; /* pointer to data structure */ BOOL bResult; /* boolean result value */ char sString[32]; /* local string variable */ bResult = FALSE; /* allocate memory & check if succeeded */ hMem = GlobalAlloc( GHND|GMEM_DDESHARE, (DWORD)sizeof(DATA) ); if ( hMem ) { /* lock the data - may not suceed with expanded memory */ lpData = (LPDATA)GlobalLock( hMem ); if ( lpData ) { /* define data structure constants */ lpData->fAck = FALSE; lpData->fRelease = TRUE; lpData->fResponse = bResp; lpData->cfFormat = CF_TEXT; BumpGlobalAtom( aItem ); /* format the data */ sprintf( sString, "%ld\t%ld", l1, l2 ); lstrcpy( lpData->info, (LPSTR)sString ); /* unlock prior to sending */ GlobalUnlock( hMem ); bResult = PostMessage( hToWnd, wMsg, hFromWnd, MAKELONG(hMem,aItem) ); } else GlobalFree( hMem ); } /* return result */ return( bResult ); } /* * DESTROY DDE MAZE * * wQuit application exit code * * This function destroys all resources consumed by the Maze during * execution. Included is the removal of all global atoms. A final * exit code is returned by the function. * */ static WORD DestroyMaze( wQuit ) WORD wQuit; { /* remove global atoms */ GlobalDeleteAtom( aAnyMaze ); GlobalDeleteAtom( aThisMaze ); GlobalDeleteAtom( aBall ); GlobalDeleteAtom( aWnd ); GlobalDeleteAtom( aGrab ); GlobalDeleteAtom( aStat ); /* return final exit code */ return( wQuit ); } /* * FIND DDE LINK * * pwNdx index to link * hWnd window handle of caller * * This function finds the link references by the window handle * provided. The resulting link is returned to the caller. The * function returns TRUE if the window handle was found. * */ static BOOL FindLink( pwNdx, hWnd ) WORD * pwNdx; HWND hWnd; { WORD i; for (i=0; ifAck ); Monitor( hWnd, "WM_DDE_DATA: aItem = %u", aItem ); The monitor Function in Figure 18 can serve as a rudimentary debugging aid for DDE applications. Figure 19 shows some examples of its use. ████████████████████████████████████████████████████████████████████████████ Designing for Windows: An Interview with the Microsoft Excel Developers Bringing Microsoft Excel to the PC environment took more than simply porting the program from the Macintosh version. The project team wanted to design a spreadsheet that included a fully device-independent, graphics- based product that would have not only the look and feel of the Macintosh version, but would also provide substantial improvements in terms of speed and functionality. To accomplish these objectives, the spreadsheet program was totally rewritten using the Windows environment. Microsoft Systems Journal spoke to the design team to learn more about the planning, development, and cooperative effort that went into the complex job of creating Microsoft Excel. MSJ: What were the original goals of the Microsoft Excel project? How were they formulated, and how were they unique to this project? The original goal was to port the Macintosh(TM) version of Microsoft Excel onto Windows. We realized that we had a great product running on the Macintosh and wanted to take the opportunity to run it under the same kind of graphical user interface on PCs. As part of formulating the project goal, we tried to understand the unique needs of PC users. We paid more attention to the features that were available in existing spreadsheet products like Lotus(R) 1-2-3(R) and made sure that our software was at least compatible or consistent in those features. Then we added the extra features that made the Mac version of Microsoft Excel such a success. Microsoft Excel was developed to run under Microsoft Windows. In what areas did Windows help the project and in what ways did it hurt? Clearly Windows helped the project because it served as the graphical environment that Microsoft Excel needs to run under. Windows is a full, bit-mapped, fully supported graphical environment that includes device- independent drivers for all the various devices that exist in the PC world. From that standpoint, we couldn't have developed Microsoft Excel without a product like Windows: the amount of work required to provide the rich environment that you get with Windows would have been prohibitive. Windows hindered us in the sense that it turned out to be a very complex environment. Part of that is because the PC world itself is so complex──the number of devices you have to support and the problems you have in being device-independent makes it more difficult than the Apple world. Microsoft Excel runs under Windows Version 2.0 and Windows/386, and it will run under OS/2 shortly. What were the considerations in planning for the longevity and portability of the program, and did this make the task more difficult? The point is that the Macintosh environment is different than the Windows environment, and the Windows environment is different than the Presentation Manager environment. Often you don't understand those differences until you actually have to resolve them as we did on this project. We've learned a great deal by moving between Macintosh and Windows as far as understanding Microsoft Excel and how to make it more environment-independent. We learned things we never would have guessed at the time we started to port to Windows. We use the concept of core code in porting from one environment to another──that is, you separate out as much as possible that is not environment-specific. You then take the code that is environment-specific and try to isolate what is different about the environments and how the code can be adapted to move between them. Microsoft Excel will soon be modified to run under the Presentation Manager in the OS/2 environment. The Presentation Manager is somewhat different from Windows. Microsoft Excel will have to be changed to support those differences──it will certainly run under the Presentation Manager when that's ready. We feel that, after putting Microsoft Excel through two environments, the third will be much easier. Some of you were involved in the development of Microsoft Excel for the Macintosh at Microsoft. What was most different about the PC project and what did the Mac experience teach you that helped you on this project? The aspect that was the most different on the PC project was how we changed our memory management. There were several reasons to do that. One reason was to get better addressability than Microsoft Excel for the Macintosh had. Even more important was how you get more addressability working in the PC world──currently, the usual way of doing this is by using EMS boards. There's a requirement that typical boards have a 64Kb window, and they can have four pages in a window. For efficiency reasons, you want to keep your pages at less than 64Kb. Often you keep them around 16Kb so you can have as many pages as possible. The entire design of our data structure had to change to support that type of chunkiness. How was the Microsoft Excel project divided into tasks? It was divided into tasks by doing a careful examination of Microsoft Excel for the Macintosh and creating a plan for moving the code that existed in Microsoft Excel for the Macintosh over onto Windows. That was assigned to various people by expertise. We used a large number of very experienced people──people who had worked on the Macintosh product and understood graphical user interfaces and environments. Essentially, we assigned various areas according to expertise that people had. For instance, an area like charting was assigned to one person who understood how it worked in the Mac environment and became even more expert in that area as it was modified to work under Windows. What is your ideal size for a project team? The team varied between eight and ten people. Somewhere after ten people the project starts to get large: you start to develop a problem with communication and your bandwidth starts slowing down. The more people that are involved, the more time you have to spend informing them of changes and making sure everyone understands what everyone else is doing. We prefer to keep project teams under the ten-person limit if possible. If we can't, we create separate subprojects and make very good, well-defined interfaces between the subgroups so that they aren't required to interact as much. Tell us about some of the secrets and strategies that went into ensuring that the job got done. Our strategy is to give people a lot of responsibility so that they feel personally involved in what they're doing and so they can take some real pride in what they've created. We don't legislate around here, telling people how to resolve their problems or force-feeding designs that they must implement. It's more a situation where people become experts in the area they are working in and they take the initiative. They also take the responsibility for creating the solution and for getting it done on time. Part of that is that the team members participate in the planning as well as the schedule of the project. They make commitments as to what they will do and how long it will take them; it's a bottom-up design. Somebody is doing a very careful analysis of the work involved and then making a commitment to finish that work. Obviously, you made a major commitment to guarantee that users of 1-2-3 would have a smooth transition to Microsoft Excel. Did you sacrifice some other features in order to accommodate Lotus users? We made a careful analysis of the features that were available in Lotus 1- 2-3 Version 2. We made sure that we were functionally complete so that current Lotus users wouldn't feel they had to give up some feature that was useful to them. Adapting the Macro Translator was a very large task. The macro language in Microsoft Excel works in a fundamentally different way than 1-2-3 macros because we are not keystroke-based. We also didn't map the Lotus interface directly like some other products do. We had to have an intelligent program that could go through and figure out what the intent of a Lotus-type macro was and then convert it into the same kind of macro running under Microsoft Excel. If there hadn't been a 1-2-3 standard, we probably would've done certain things in different ways──ways we felt were more efficient and better designed. But because we believe that Lotus 1-2-3 is a standard and that a large group of people understand 1-2-3 and are comfortable with it, we needed to work to that standard. So we couldn't put in all the features we would have liked. How important was the "make it faster" element of the project? How did you determine when the program was as fast as you felt it was going to get? Speed is one of the most critical issues in a spreadsheet. This is something we learned with Microsoft Excel for the Macintosh; it's one of the reasons it was received so well. We concentrated on speed, and the users felt the program was what we call "crisp." It had a quick response to what you wanted to do with it. Because of that, we always set speed as one of our number one priorities with Microsoft Excel. The result was that we did very serious benchmarks of existing programs and of Microsoft Excel for MS-DOS(R) as we built it. Every time we did a testing release, we would run all the benchmarks. If anything had gotten slower we would find out why. Using the benchmarks let us set some goals; using them consistently allowed us to meet those goals. We did as much as we could using recurrent design, without throwing away everything to squeeze all the speed out. The way you do that is to run benchmarks, find areas you believe are slow, and then go in and do a detailed analysis using profiling tools. From the analysis you can discover what is slow about it. You can then decide whether to do local redesign if necessary or to write better code for that area. In a graphical interface the areas that get slow are not those that do the recalculation. What does get slow is the display──just moving those bits on the screen. We spent a great deal of time understanding where all the time was going in redisplaying to the screen. As it turns out, a high percentage of the time spent in scrolling is spent moving the bits on the screen from one place to another. The reason for this is that an Enhanced Graphics Adapter (or EGH), which is one of our target devices, has a lot of wait states. Just sliding over one column and moving those bits takes a high percentage of time because the EGA card is slow. We understand it very well and have done a great deal to make it as fast as possible. Among other "firsts," Microsoft Excel was the first to implement Windows' DDE (Dynamic Data Exchange) protocol. How difficult was this part of the project, and what did you learn about implementing DDE? Conceptually DDE was very simple. We developed a design, described what we needed it to do, and were able to come up with a very small subset of nine commands that would accomplish everything we needed. The implementation, on the other hand, was quite difficult. Because DDE is an asynchronous communication protocol, the product has to be able to handle messages that can be sent to it at any time, no matter what the product is doing. This is where the complexity came into it. Microsoft Excel wasn't originally designed to be able to service messages at any time. We needed to be able to go through and understand how we could build in that capability. We needed to be able to understand changes in design that would allow the program to queue a message or service it, to handle it while inside the macro language, or to handle it during recalcs and other time-intensive tasks. Implementation took far longer than we expected but, in hindsight, it was because we didn't at first consider all the issues involved in supporting this type of protocol. The Microsoft Excel project took three years. How important was group morale during that time? Incredibly important. You can't do large projects that are this complex without people being motivated and feeling a sense of ownership. We have always had very good morale on all of our projects because people feel it is their work that's being done. Part of keeping the morale up is the fact that the people who are writing the code are also involved in understanding what the features are. Features are not legislated by someone else who writes a spec and says, "Well we're just gonna do these features." The development group is actively involved in doing an analysis of those features. They try to understand what we're trying to accomplish by including a given feature and make suggestions about why it's good or bad. Someone who does the wrong job usually knows it. It's very seldom the situation where somebody has been going in the wrong direction for six months and you say to the person, "This is terrible." Instead, a guiding hand is offered along the way. It helps to clear the smoke when it gets to be too much: point out what is important, perhaps make note of a better solution, or suggest stepping back and thinking about what's been done before proceeding forward. Managing the project is an interesting job from the standpoint that it is interrupt-driven. With the amount of experience and expertise on our project teams we don't need to tell them exactly where the project is going. We do need to watch what's happening and be involved in seeing their decisions. We can then go about making sure that if something is being missed or, more likely, if another group has discovered something, that it is transmitted between groups. It must be understood that if a group has a problem with resources, then we can get the resource for them. If we need to change a schedule, we understand why and it's transmitted to the project team. Did one phase in this project stand out as being especially difficult to manage? The most difficult phase is trying to set expectations. There's a tendency when you start a new project to want to do as much as you can. You've got to balance that out against the time frame, when you'd like to deliver a product, and what your windows of opportunity are. Microsoft Excel is not done. We've got hundreds of other features we'd like to put into the product. Part of the job was to be involved in the tough decisions of looking at a large list of possible features and saying, "This isn't important today . . . but this is." That is by far the toughest part of the job: setting the expectations, making sure we're doing the right thing and then going ahead and focusing the team on realistic goals. Was there a feature of the project that nearly got carried away? There was nothing that really got carried away because we had the advantage of having experienced developers working on this project. They've been through this and know what it's like to say "Oh, that's pretty easy" and then spend eight times longer on it than they expected. There's a tendency to do more of an analysis and understand what's involved before making a commitment. How was the development of Microsoft Excel aided because of the close proximity of the other groups, particularly the Windows and OS/2 groups at Microsoft? Having the Windows group close by probably helped for several reasons. First of all, it allowed us to actually discuss interface issues. Some of the changes that have been done for Windows 2.0 are the result of necessary changes needed in the interface. The multiple document interface (MDI) is a result of ours and some other ISDs. We felt it was a necessary feature to maximize screen space for the user. We were able to help influence the design of Windows in the same way we were able to influence the Macintosh interface. We were actively building complex products and had many suggestions about how to make it better. The fact that they were "across the pond" didn't help that much──we could have done it on the phone or with visits, as we did with Apple. Proximity did help in fixing bugs. We were able to help the Windows group. If we found a bug and thought it was in their code, we could isolate it and have a Windows expert come over, look at it and see the problem. That paid off more than anything else──really being able to get into the code and provide assistance and communications between the two groups. What role did Bill Gates play in the development of Microsoft Excel? Bill was the visionary. He was one of the people who was able to say "We've got to have a feature like this, it's very important." He helped guide the formulation of what the product became by having those beliefs and having reasons for those beliefs, and for being able to communicate them. We've been working with Bill for so long that we can now ask many of the same questions of ourselves that Bill might ask. We can do the right design by being more critical of ourselves. Bill, over time, has showed us that we shouldn't be complacent and that we should think about what we are doing. When we went into a meeting he used to ask questions we felt foolish about because we had no answer. We've been through enough of those situations that we're now able to ask ourselves those same questions and answer them before we go into those meetings. In retrospect, is there anything you would have done differently? It would have been nice if some things had proceeded more smoothly, but there is really nothing we would have done differently. This project was organized using some new philosophies and new approaches to development. While the implementation didn't always go as smoothly as we'd hoped (just because nothing ever does), it did go exactly as we'd hoped as far as the results we got, the way the project ran and the management. We're going to continue to refine this same approach. How does the group feel now that another flavor of Microsoft Excel is out the door? This is the start of Microsoft Excel. We've seen what it looks like on a Mac and we've seen what it looks like on Windows. More importantly, we have so many ideas and so many new things we'd like to implement that this seems like just the start of a long history of great spreadsheets. ████████████████████████████████████████████████████████████████████████████ A Strategy for Building And Debugging Your First MS-DOS Device Driver ─────────────────────────────────────────────────────────────────────────── Also see the related articles: The Xmodem Protocol: Error-Free Transmission a Block at a Time Sample Code Fragments for MDM_DRV.ASM ─────────────────────────────────────────────────────────────────────────── Ross M. Greenberg☼ Writing device drivers is a difficult, tedious task that only operating system gurus should ever consider. Those who attempt this feat are eligible for extra hazard pay, must have their parents' or guardians' permission if under age, sign certain documents relinquishing their right to sue in the event of injury, and have at least $1 million in liability insurance. That may be the prevailing thought, but writing device drivers really isn't that different from writing other programs, nor is it substantially more difficult. This article will examine the inner workings of device drivers, and offer some useful debugging tips and techniques. The device driver built in this article, MDM_DRV, serves as a rudimentary communications program, including Xmodem/checksum protocol transfers. Although it expects to see another copy of itself on the other end of a transfer, through a modem or hard-wire connection, it can download files from a variety of bulletin boards and conferencing systems. A device driver is the logical interface between the operating system I/O subsystem and the underlying physical hardware. In some cases, the device driver replaces or extends certain aspects of the BIOS, and thus is responsible for administering the underlying hardware characteristics. In other device drivers, the actual physical I/O is done solely through the BIOS. Early versions of MS-DOS(R) did not allow for easy installation of device drivers. Starting with Version 2.0, however, the independent programmer could easily integrate additions into the operating system. Because the operating system interface for device drivers is rigidly defined, as opposed to the lack of standards for terminate-and-stay- resident (TSR) programs, most device drivers will work with other device drivers installed. Device drivers come in two flavors: character and block oriented. Device drivers that allow storage and/or retrieval of data on disks or other types of random-access devices, are usually block-oriented device drivers. Block device drivers transfer data in fixed amounts──hence their name. This article will concentrate on character device drivers. Several topics that are covered here are related to the TSR serial─interrupt program, TSRCOMM.ASM, discussed in "Keeping Up With the Real World: Speedy Serial I/O Processing," MSJ, Vol. 2 No. 3. Character device drivers must be able to operate in either of two modes: cooked or raw. Cooked mode makes simple single-character-at-a-time requests to the device drivers and processes certain characters, such as carriage returns, line feeds (which may or may not be translated), Ctrl- Z's, and Ctrl-C's in a special way. A status check is made on each I/O request to determine if a Ctrl-C has been struck on the keyboard, and a device driver in cooked mode allows MS-DOS to terminate the driver call if it sees one. Since it is not required that the calling program know whether the device is in cooked or raw mode, MS-DOS must be able to adequately and properly translate a typical multiple byte I/O request from the higher-level application program into the correct number of single byte requests. MS-DOS does this by using internal buffers and finally transferring the data into the user buffer when the request is complete. In raw mode, no translation or other special service is expected of the device driver or provided by MS-DOS. When in raw mode, there is no granularity in the number of bytes requested for I/O: the device driver must be able to properly handle a request for the specified number of bytes. Usually, these requests do not require any internal MS-DOS buffers and are processed in the application program's own buffer, making raw mode considerably more efficient than cooked mode. The user program can change the mode from raw to cooked or vice versa by using the MS-DOS function call for IOCTL, Interrupt 21 with AH = 44H (see the MS-DOS Technical Reference manual for more information). Device Driver Structure The three major parts of a device driver are the header, the strategy routine, and the interrupt routine. The header routine describes the capabilities and attributes of the driver, gives the character device driver a name, and has NEAR (offset only, single word) pointers to the strategy and interrupt routines as well as a FAR (offset and segment, double word) pointer to the next device driver in the chain of device drivers (the pointer chain only points one way, causing problems in finding its beginning. The pointer to the next device driver is set by MS-DOS immediately after the initialization routine is completed and should be initially set to ──1 (FFFFFFFFH) in the device driver itself. A device driver is limited to at most 64Kb, since the strategy and interrupt routines contain only the offsets within whatever segment MS-DOS assigns to the device driver. A graphical representation of the header can be found in Figure 1, while a description of the attribute word bit definitions is seen in Figure 2. The Strategy Routine The strategy routine is called when your device driver is first installed on boot and for each I/O request generated by the operating system. A single I/O request from the application program can generate multiple I/O requests to the device driver. The purpose of the routine is to save an address somewhere for future processing and then return. This address, passed in the ES:BX register pair, points to a structure called the request header (see Figure 3), which contains information that tells the device driver what operation it should perform. It is important that no actual input or output operation be performed in the strategy routine and that the address of the request header be saved for future processing by the interrupt routine. In a true multitasking system, this address would probably be saved in some array, and possibly sorted using a method for optimal device usage when the interrupt routine is later called. Under MS-DOS, a call to the interrupt routine immediately follows the strategy routine. A point to consider is that interrupts are enabled between the strategy call and the interrupt call, which can cause problems if your device driver is built under the assumption that there is "zero-time" between the two calls. The Interrupt Routine The interrupt routine is where all of the actual work of a device driver is done and is, therefore, the most complicated part of the device driver code. When this routine is called, the command byte (the third byte) of the previously saved RH is examined, and the appropriate action is taken based upon this value. See Figure 4 for a list of these command bytes and their implied actions. The interrupt routine normally uses the command byte as an index into some dispatch table and then calls the appropriate routine for each command. Obviously, however, you can introduce a jump table if you wish. The request header contains all the necessary information for proper processing of each command and informs the calling program (MS-DOS, in most cases) of the status of the request when the routine finishes. The status word itself is broken into a number of fields (see Figure 5). It has an error bit to indicate that other parts of the status word contain the particular error condition, a done bit to show that the indicated operation was completed, and a busy bit that is primarily used to indicate the current status of the device. It is generally not sufficient to return only the status of the operation. The count of the number of characters processed, where appropriate, such as in a read or write operation, should be returned as well. Even though the interrupt routine is not actually called as an interrupt routine──it is called with a FAR CALL and therefore expects a FAR RET──it should act as if it were a true interrupt service routine, saving all registers and flags and restoring them when finished. The Commands The device driver commands listed in Figure 4 are built into MDM_DRV as callable routines. The dispatch table, just below the request header in the assembler code listing, points to each of the routines the device driver needs. The table is accessed by the interrupt routine, which uses the command byte in the request header as an index into the table to find the correct routine to call. Once the strategy and interrupt routines are in place, the commands need to be implemented. This is basically a straightforward procedure, as can be seen in the code listing. Each command detailed below is found in the same order in the code. This structure can serve as a basic model for almost any device driver, no matter how complicated. For further information on writing device drivers, see either the MS-DOS Technical Reference manual or Advanced MS-DOS by Ray Duncan (Microsoft Press(R), 1986). Not all of the available commands are utilized by MDM_DRV, though they are all referenced in the code for the sake of completeness. Those that are not used are simply stubs in the code that do nothing and return. Each command that is used follows the same general logic: it is called from the interrupt routine with a near call, it performs its operation, and returns a status in AX. This status is "or'd" with the done bit, and then stored into the status word in the original request header at RH+3. Character counts, if required for a given operation, are updated in the individual routines. If a routine is not used for this device driver, it returns a status indicating that it is finished with the particular operation. INITIALIZE DRIVER (Command 00H) is called only once, when the driver is first installed. It should initialize the device as required for future operation and can, if desired, print a greeting message indicating that it has been successfully installed. For debugging purposes, you might also want to have it output its code segment address while developing your device driver. You may only call MS-DOS services via Interrupt 21H with AH less than or equal to 0CH, or with AH set to 30H to get the MS-DOS version number. Since some of the routines are version-specific, you may want to check the version number and set some flags or take appropriate action if the wrong version number is found. There are specific operations required for block devices, not covered in this article, which involve setting up certain static tables so MS-DOS will later know the particular attributes of the block device associated with the driver. When the INITIALIZE DRIVER routine is called, the double word pointer at RH+18 points to whatever text followed after the equal sign in the CONFIG.SYS line that caused the device driver to be invoked; this allows you to process options if you wish. In the case of MDM_DRV, the screen displays this string. The highest memory address required by the device driver is returned in the double word at RH+14, which indicates to MS-DOS such information as where it can load the next device driver. Since the initialization routine is only called once, its own address can be returned in the request header for efficient utilization of memory. Remember that anything in memory after the address you return will most likely be overwritten. MDM_DRV uses two services available only in MS-DOS 3.x and higher: the device open and device close routines. Earlier versions of MS-DOS did not know that these routines exist: since they are required for operation of the driver, the wrong operating system version causes an exit. MDM_DRV uses the initialization routine to assure that it is running on an MS-DOS 3.x system, then prints an error message and exits if this is not the case. It initializes the modem port to 1200 baud, no parity with eight data bits and one stop bit. There are alternatives to exiting if the wrong MS-DOS version is found. With a little more coding, the first call to a read or write routine could call the open routine, and the last call, or a suitable pause after a return from a read or write call, could trigger a device close call. I've opted not to code this driver for the older 2.x version of MS-DOS; it's time for everyone to update anyway. Finally, a greeting message is printed out, and the initialization routine ends. DEVICE OPEN (Command 0DH) has no defined purpose under MS-DOS, but you can be sure it will be called in MS-DOS 3.x before any other routine, provided the appropriate bit in the header attribute block is set (Bit 11). The MDM_DRV code uses it for several purposes. When first called it re-initializes the communication port to the default setting of 1200,n,8,1 (actually, this setting can be changed later via appropriate calls to the IOCTL function). The data carrier status is then checked to determine if the port is active and already on-line to another machine. If on-line, the following dialing and/or answering session is skipped. If not on-line, a message is output to the screen requesting either a phone number to dial or a simple return. Depending on whether a number or other input was entered, the modem sends out a dial or answer string and awaits carrier. A Ctrl-C causes an immediate exit. Once carrier is detected, a very simple terminal communications routine is entered that echoes characters entered by the keyboard to both the screen and the comm port, and characters found on the comm port are echoed directly to the screen. When either side of the communications session enters an escape, both sides leave the loop and continue. Once an escape is hit, or if there was carrier originally, the timer tick is taken over, since timing is a critical part of the Xmodem protocol that subsequent read or write calls will be using. Finally, control is returned to the originating caller. READ (Command 04H) is, by far, the most conceptually difficult routine in MDM_DRV, because of the dual nature that the read routine requires. The calling routine (in most cases MS-DOS) makes a request to read a certain number of characters. The actual character count is found in RH+18. These characters are returned to the calling program by storing them in the sequential address found at the double word pointer at RH+14. Xmodem, however, transfers data in fixed blocks of 128 data bytes per block with some additional protocol information (see the sidebar on Xmodem). It expects to see an ACK (acknowledgement) or a NAK (negative acknowledgement) character transmitted by the receiver within a certain specified time after sending a block to indicate whether that block was received properly. Consider a situation in which requests are made for one character at a time. The first call does not have any data awaiting it, so it must wait for a full block to arrive. If that block does not check out, meaning that the block number, its complement, and the checksum byte are not what was expected or that a time-out occurred before there was an entire block to process, then an immediate NAK is sent and the wait-for-a- block loop is entered again. If the block does check out, the ACK cannot be sent until the device is ready to receive the next full block. Now, if the TSRCOMM program has been implemented, the ACK can be sent. (TSRCOMM allows for asynchronous interrupts, that is, incoming characters, to be processed properly and is available on both DIAL and the author's own bulletin board system at (212) 889-6438.) If an asynchronous interrupt handler is not present, however, characters will be lost if an immediate ACK is sent out. Therefore, it is not possible to send out the ACK immediately; it is not sent until the last character in the block is properly processed and stored at its appropriate location in the caller's transfer buffer. This means that subsequent calls for the next byte must be handled with the knowledge that they are already in the middle of a block. This causes a problem, of course, if it takes longer to process 128 characters with driver I/O requests than is allowed between the time the last character is sent and the ACK or NAK is expected. What about a case in which a large number of characters are requested? Again, the read routine must wait for a full block to arrive, then process as many characters as allowed by the character count in the request header before returning to the caller. If the read count requires more characters than exist in the buffer, the routine stays in the get-a-block, process- character loop until the count is depleted. The routine also keeps track of how many characters are processed by incrementing the character count as it stores each character. When the read request is complete, control is returned to the caller. If there are too many NAKs and the transfer has to be aborted, control is returned with an error status set. Control is returned without an error code, but with a "short count" when an EOT (end-of-transmission) is received indicating that the transfer has been completed, or when read requests issued after an EOT have been received. INPUT STATUS (Command 06H) is not especially valuable for this device driver, but is included nonetheless. If there is a character available (a block has been received, but not fully processed), a status of 0 is returned in the busy bit of the return status word at RH+3. Unless the read count occurs in 128-byte increments, this routine will normally return a character ready status after the first read call. There is some cheating here: if the variable in_block is set, then obviously there are characters outstanding. NON-DESTRUCTIVE READ (Command 05H) merely returns what the next character to be returned would be. Return the character in RH+13, but do not remove it as that would affect the current input buffer. What to do if the input buffer is empty when this call is made is not defined. Therefore, an error code of BH (read error) is returned in the status word at RH+3, a count of 1 is returned at RH+18, and the actual character is returned as a question mark. FLUSH INPUT BUFFERS (Command 07H), if called, causes the input buffer to be flushed. Since the next call may be a read request and the buffer was flushed, the read will eventually time out, causing a NAK to be sent. The NAK triggers the last block sent to be sent again, and everything should continue working well. WRITE (Command 08H) and the write subsystem are very simple when compared with the rigors of the read subsystem. When a write request is made, the number of bytes to be transferred is found at RH+18, as expected. The starting transfer address is found with the double word pointer at RH+14. Characters are transferred, one by one, into the output buffer. When the output buffer has 128 characters in it, the send_block routine is called. The block is sent until it is either successfully received by the other end, indicated by the other end sending an ACK, or until enough NAKs or time-outs have been received to indicate a transmission error. When there are no characters left to be sent, the processed number of characters, which may be different from the transmitted count, are returned in RH+18, and control is returned to the calling program. WRITE WITH VERIFY (Command 09H) makes little sense for a character device, especially for one that uses Xmodem. Generally, it should call the write routine and then attempt to read the data just written, compare them, and return an error if they do not match. In the case of MDM_DRV, a simple jump to the WRITE routine suffices. OUTPUT STATUS (Command 0AH) should set the busy bit if the output device is busy. Since MDM_DRV is neither buffered nor interrupt driven, this is impossible: the write routine never returns with any outstanding work to be done. Therefore, the busy bit is not set when control is returned to the calling program. FLUSH OUTPUT BUFFERS (Command 0BH) does nothing in MDM_DRV since emptying the current output buffer would probably have an adverse effect on the transfer underway. DEVICE CLOSE (Command 0EH), like the DEVICE OPEN command, is only called if the device driver is operating under MS-DOS 3.x and the appropriate bit is set in the header attribute word. This routine determines if there are any outstanding bytes from a write and forces the partial block to be output via the send_block routine if required. There will probably be outstanding bytes, since the write routine only sends out bytes in blocks of 128. When an ACK on the final block is received, an EOT is sent to indicate that the transaction is complete to the remote side. This routine is also called in the event of an aborted transfer or after a Ctrl-C is entered on the console during a transfer. Therefore, the abort flag is checked; if set, a CAN character is sent over the line, which indicates to the remote that the transfer has been canceled. Obviously, if this is the case, any partial outstanding block is not sent. If this was a dialed call, then the Hayes(R)-compatible hangup string is sent with the required 1-second guardband around the attention-getting triple plus signs (+++). Finally, DTR is dropped on the modem port, the interrupt vectors that were stolen are returned to the state they were in, and control is returned to the calling program. I/O CONTROL READ (Command 03H) along with I/O CONTROL WRITE, allows information to be passed to the calling program regarding device status. It is not fully defined and can therefore be used for just about any purpose. In MDM_DRV, it passes a copy of the structure that contains the information shown in Figure 6. This information allows an application program to define which modem port, for example, MDM_DRV will use. This routine allows an application program to determine what these parameters are currently set to. Note that the count in RH+18 indicates how many bytes you can return on this call. Since a buffer of a limited length is usually used for this call, passing more memory to the address specified in the double word pointer at RH+14 could cause a write into an area that is not properly allocated. Often, this will be a direct write into the application program buffer, where a call may only want to return the first word, which indicates the current communications port, for example, instead of the entire structure. I/O CONTROL WRITE (Command 0CH), like its cousin above, permits you to pass information to and from the device driver. This particular command sets internal parameters within the device driver. Its basic use would be to do an I/O CONTROL READ request into a local buffer first, modify the bytes you want to alter, and then issue an I/O CONTROL WRITE using the local buffer address and the count of the characters you wish to update. CHECK MEDIA (Command 01H), BUILD BIOS PARAMETER BLOCK (Command 02H), REMOVABLE MEDIA (Command 0FH), and OUTPUT UNTIL BUSY (Command 10H) are not used by the MDM_DRV driver; however, for completeness, they are individually included in the device driver. Usually, the address of a common "null" routine would be used in the dispatch table. Design Considerations There are problems inherent in the design and writing of a device driver unlike those of other programs. In many ways, designing and implementing a device driver can be likened to a Windows application: your main routines are called by an outside task, perhaps multiple times, the system will not continue processing until you give up control, and the interface is rather rigidly specified. The first problem is that there is no guarantee that, simply because a dev_open call was made that there will be a corresponding dev_close call. Just to make the matter a bit more confusing, the simple MS-DOS COPY command makes a set of dev_open/dev_close calls before it actually opens the device for the appropriate I/O instruction. (The actual sequence might be: dev_open / dev_close / dev_open/ [read|write] / dev_close.) Another problem inherent in the device driver is the retry count. Somewhere in the depths of MS-DOS, there is a number that is supposed to be able to indicate how many retries you wish a device driver to attempt before it gives up in disgust and MS-DOS outputs the ominous "Abort, Retry, Ignore?" message (caused by Interrupt 24H, the critical error interrupt). There does not appear to be a documented method to force only one attempt on a device open before generating a critical error. Therefore, there is no graceful way to abort from a "dead" line. Since MS-DOS may not be called for any purpose within the non-INIT part of the device driver, only the BIOS may be called for character I/O (direct keyboard reads and direct screen writes would also work, but make the job needlessly complex). However, this means that a control-C or control-break will not function as you might wish, allowing you to exit from the device open call back to the calling program, MS-DOS prompt, or even the critical error interrupt, and is only considered another character with no unusual attributes. This is, actually, an extraordinarily difficult problem to deal with since the basic nature of MS-DOS device drivers did not intend for them to be of an interactive nature. It becomes a challenge which I have not yet had the opportunity to address. Therefore, once you enter this device driver, there is no real way out! That really is the least of the problems created when one tries to do things with MS-DOS that it really isn't intended to do. Here's a problem which had to be addresed as an external program: MS-DOS always opens character device drivers in cooked mode. If you're transferring a file which happens to have a control-Z in it, the control-Z will be processed as an end-of-file character and the file you are transferring will be closed prematurely. Therefore you must operate the MDM_DRV in raw mode. Yet, unless you specifically do the programming yourself, you can't guarantee that there will be an IOCTL call after each open to reset the device driver to raw mode. Setting raw mode lasts only for the given file handle's lifetime; subsequent opens will still be cooked mode, unless you run the small TSR program called SET_MDM. This program simply intercepts the MS-DOS interrupt (Interrupt 21H), checks to determine if this is an "open-with- file-handle" call and, if it is and if the filename pointed to by the DS:DX register pair matches 'MDM' (followed by a NULL), sets the file handle (after a successful open) to raw mode. This is done transparently to the calling program and is quite legal, even if exceedingly ugly. Every programmer should have a reasonable kludge to compare their worst kludge to, and I would like to submit SET_MDM as the new standard for your comparisons. But──as in any kludge──if it works, it is acceptable. Room for Improvement Since MDM_DRV is an example of how a device driver functions, the code itself is not polished, and there are a few areas ripe for enhancement. For example, the get_num routine does not concern itself with whether a character entered on the keyboard is a legal ASCII character; it just sits and waits for either a return or for up to twenty characters to be entered. Having a nice way to exit from that routine (or from any other routine in the device driver), as discussed above, would be high on my list of enhancements. Further, the driver assumes that a Hayes-compatible modem is attached to the comm port, and that assumption is hard-wired into the dev_open routine and the dev_close routine. If MDM_DRV is used for other modems, or with other devices attached to the serial port(s), then some additional code will be required, perhaps through the IOCTL functions. The protocol used in this driver is simple Xmodem checksum. An additional improvement would be to use an enhanced version of the protocol called Xmodem CRC. Implementing the required enhancements requires only a little bit more coding. An errant abort or EOT character is not treated according to specification, since it is accepted and acted upon immediately. The specification calls for the first CAN or EOT to be NAKed to assure that it is not line noise and only subsequent characters are acted upon. Debugging Developing every program requires patience and detective work once a problem shows up. However, it is difficult to debug and enhance device drivers without proper debugging tools. Unfortunately, there aren't any tools readily available for debugging device drivers; you have to create your own. The first thing to do before even attempting to debug a device driver is to have full and adequate backup and to ensure that you have some means of rebooting your system without having the device driver automatically installed. My method was to have a CONFIG.SYS on a bootable floppy disk and boot with that floppy when testing out the device driver. The following are some hints and ideas used in the creation of MDM_DRV: ■ Build the device driver as a normal COM program, and then have a simulator that calls each of the routines and lets you examine the registers and the request header with almost any debugger. I consider this somewhat tedious, and it doesn't allow you to test the device driver under "real" situations. But this is a decent starting approach, and allows you to move on to the next method. ■ Create a skeleton program that has no initialization routine at all. Have the interrupt routine simply consist of setting a register pair to the address of the request header you saved in the skeleton strategy routine. Then call an interrupt that you've reserved for yourself. Here is an example of a simple interrupt routine: interrupt proc far push es push bx mov es, cs:[old_segment] mov bx, cs:[old_offset] int 60h pop bx pop es ret interrupt endp ■ This implies, of course, that you have some sort of interrupt service routine set up later for the interrupt you choose. I used interrupt 60H, which is defined as a "user" interrupt. Something like this may suffice: int60 proc far call real_int_routine iret int60 endp This lets you generate your real_int_routine as a far routine and with a FAR return. The point of this particular exercise is to permit you to build TSR code that simply takes over the specified interrupt and from that point operates as if it were a device driver. After you finish debugging it as a TSR (which most debuggers, including CodeView(R), make pretty easy), you can then swap the code into the actual device driver and you're finished, except for the initialization routine. ■ The hardware-assisted Periscope debugger makes it easy for the offsets of variables declared public to be associated with any code segment desired. If your initialization routine displays the code segment address upon boot, it is easy to create a dummy program that gets loaded into the debugger with the associated symbol map and then change the segment of the symbols to point to the original code segment of the driver. From this point, it is as if you are debugging a "normal" program. ■ Remember that there is a very limited amount of stack space available to the device driver when initially called. There is really no way to determine how much memory is available to the device driver. Your best bet is to save the original stack pointer and segment, then use a private local stack, and restore the original stack when you're finished. MDM_DRV does this in the interrupt routine. ■ Treat the device driver as if it were a hardware ISR: every register (including the flag register) must be restored upon exit. Critical areas of the device driver should operate with interrupts turned off if required. ■ Be sure that character transfer counts indicate the actual number of bytes transferred. Incorrect counts will cause many strange and less- than-wonderful things to happen. ■ When in doubt, make sure the device driver is not accessing any memory beyond the initial address you passed back in the initialization routine. Figure 1: Standard Device Driver Header 00H ╔═══════════════════════════════════════════════╗ ║ Link to next driver, offset ║ 02H ╠═══════════════════════════════════════════════╣ ║ Link to next driver, segment ║ 04H ╠═══════════════════════════════════════════════╣ ║ Device attribute word ║ 06H ╠═══════════════════════════════════════════════╣ ║ Strategy entry point, offset ║ 08H ╠═══════════════════════════════════════════════╣ ║ Interrupt entry point, offset ║ 0AH ╠═══════════════════════════════════════════════╣ ║ ║ ║ Logical name (8 bytes) ║ ║ if character device. ║ ║ ║ ║ Number of units (i byte) if block ║ ║ device, followed by 7 bytes of ║ ║ reserved space. ║ ║ ║ ╚═══════════════════════════════════════════════╝ Figure 2: Device Driver Attribute Word ╓┌──────┌────────────────────────────────────────────────────────────────────╖ Bit Description 15 0 - if block device 1 - if character device 14 0 - IOCTL is not supported 1 - IOCTL is supported 13 0 - (if Bit 15 is 0) IBM format 1 - Not IBM format 0 - (if Bit 15 is 1) Output-Till-Busy is not supported 1 - Output-Till-Busy is supported 12 0 - Undefined 11 0 - Use DOS 2.x calls only 1 - DOS 3.x, calls to Open/Close Device and removable media supported, ignored if DOS is 2.x Bit Description media supported, ignored if DOS is 2.x 10 0 - Undefined 9 0 - Undefined 8 0 - Undefined 7 0 - Undefined 6 0 - Undefined 5 0 - Undefined 4 0 - Undefined 3 0 - Normal device 1 - Special Clock Device 2 0 - Normal Device 1 - Null device driver 1 0 - Normal Device 1 - Standard Output device driver 0 0 - Normal Device 1 - Standard Input device driver Figure 3: Request Header Record Layout rlength db 0 ; 0 - length of pertinent data in header unit db 0 ; 1 - unit number (not used in MDM_DRV) command db 0 ; 2 - the actual command status dw 0 ; 3 - return status reserve db 8 dup (0) ; 5 - reserved for DOS media db 0 ; 13 - media desciptor (not used in MDM_DRV) address dd 0 ; 14 - doubleword pointer for I/O count dw 0 ; 18 - unsigned int count for character I/O sector dw 0 ; 20 - starting sector (not used in MDM_DRV) Figure 4: Request Header Command Bytes and Their Meanings ╓┌────────┌──────────────────────────────────────────────────────────────────╖ Command Byte Meaning 00H Initialize the Device Driver. Called only on installation of the device driver at bootup time. 01H Media Check. Returns a status indicating if the current Command Byte Meaning 01H Media Check. Returns a status indicating if the current media is changed (not used in MDM_DRV). 02H Build the BIOS Parameter Block. Called when new media or media changed, this call should build the BPB and return its address (not used by MDM_DRV). 03H Read IOCTL. Copy certain device driver information into a local buffer. 04H Read. Retrieve a certain number of characters from the device driver tables. 05H Non-destructive character read. Return the next character from the device, without removing it from the input buffer. 06H Input Status. Return a status indicating if there is a character waiting in the input buffer. Command Byte Meaning 07H Input Flush. Empty the input buffer of the device. 08H Write. Output a certain number of characters to the device. 09H Write Verify. Output the characters, then reread to check output was correct. 0AH Output Status. Return a status indicating if device is busy or free to output. 0BH Flush Output Buffers. Empty the output buffers associated with the device. 0CH Write IOCTL. Copy certain device driver information from a local buffer into the device driver tables. 0DH Device Open (DOS 3.x only). Initialize the device. Command Byte Meaning 0EH Device Close (DOS 3.x only). Called preparatory to closing the file handle. 0FH Removable Media Routine (DOS 3.x only). Block devices only. 10H Output Until Busy (DOS 3.x only). A weird hybrid of the normal write function, it allows an "incomplete" output call. Figure 5: Device Driver Status Word 15 0 No error 1 Error 14 Reserved 13 Reserved 12 Reserved 11 Reserved 10 Reserved 9 1 Device BUSY 8 1 Operation Completed (May still have an error condition, in which case this bit is ignored) 0──7 Specific Error Codes: 0H Write Protect Violation 1H Unknown Unit Requested 2H Drive Not Ready 3H Unknown Command Encountered 4H Data Error on Read 5H Illegal I/O Request Structure 6H Error on Seek Request 7H Unknown Media Encountered 8H Requested Sector Not Found 9H Printer Error (Paper Error) AH Write Fault on Device BH Read Fault on Device CH General Failure on Device DH Reserved EH Reserved FH Media Change Invalid Figure 6: IOCTL Structure for MDM_DRV com_port dw 0 ; The comm port we are ; communicating with. ; COM1 = 0, COM2 = 1, etc. init_data db 10000011b ; The initialization byte. See ; the BIOS Technical Reference ; manual for details. block_num dw 0 ; Current block being ; transferred. abort_xfer dw 0 ; Whether the current transfer ; is to be aborted. ; (TRUE = YES) nak_cnt dw 0 ; How many NAKs have been ; sent or received. cancel_flag dw 0 ; Was a Ctrl-C hit? inrequest dw 0 ; The number of bytes not yet ; processed. in_block dw 0 ; Has a block been received? was_dialed dw 0 ; Modem was dialed. ─────────────────────────────────────────────────────────────────────────── The Xmodem Protocol: Error-free Transmission a Block at a Time ─────────────────────────────────────────────────────────────────────────── Xmodem transfers data in blocks of 128 bytes, one block at a time. Each block has certain information added to it to ensure that the transmission of data is error free. Often called a "checksum," this additional information is a calculation of the data contained within the block. The initial calculation is done by transmitting half of the conversation. The receiving half does a similar calculation of the data when it gets the block. It then compares its calculated result with the original calculation, which is sent by the transmitter as part of the information added to the block. If the two calculated values differ, the receiver will reject the block, since it knows it received different data than what was sent. To reject the block, the receiver merely tells the transmitter it was received improperly, and the transmitter will then transmit the block again. If there are too many errors, then either side may tell the other to cancel the transfer. The Xmodem protocol is a receiver-driven protocol; the transmitter waits for the receiver to request that a block be sent. The request indicates whether to send the next block or the last block sent. At the head of the block is an SOH (ASCII 01H), followed by a single byte that represents the block number. The block number is followed by the one's complement (a logical NOT) of the block number, then the 128 bytes of data. Finally, the trailing byte is a one-byte additive checksum of the 128 data bytes. Other flavors of the Xmodem protocol include a more complicated two-byte checksum, which is actually a cyclical redundancy check (CRC) calculation on the 128 data bytes. When the receiver is first invoked, it sends out a NAK (ASCII 15H), which the transmitter uses as a synchronization byte in Xmodem checksum transfers. If the character "C" is sent instead of the NAK, the transmitter will use Xmodem CRC. In any case, the transmitter sends the first block of the file, which starts at block number 1, when it receives the NAK or the "C", or it issues an appropriate message if a reasonable amount of time has elapsed without either being received (a time-out). After each block is sent, the receiver performs the same calculation on the data bytes and checks its internal checksum or CRC against the block's version. If they differ, there was an error in transmission. Most receivers ignore all incoming characters until an SOH is seen, so the SOH is certainly part of the checking scheme, allowing certain line noise to be ignored. The block number is checked against its complement to ensure that the block arrived intact. If the block is the one expected, an ACK (ASCII 06H) is sent, and the data is processed, usually by saving it in a local buffer, which is eventually written to disk. Since many disk controllers turn off interrupt processing, the disk write is often performed before the ACK is sent so that no characters are missed. If the block number indicates this is the last block successfully received, then an ACK is also sent, but the block is not processed or saved. This happens if the remote transmitter never received an ACK for the indicated block. A NAK is sent if a full block is not received within the time-out period, the next character within a block is not received within a shorter time- out period, or any of the above checks indicate that there was an error in transmission. When the transmitter receives a NAK instead of an ACK, it bumps up a NAK count, which is reset upon receipt of an ACK. When this count exceeds some number, the transmitter will send a CAN character (ASCII 18H) to the receiver and abort the transfer. In a protocol that is not robust, the receiver will immediately abort the transfer, usually throwing away whatever data has been transmitted. In the robust protocol, the CAN is NAKed, the transmitter resends the CAN, and then both sides of the communications session will abort the transfer. If the NAK count was not exceeded, however, the last block sent is simply sent again. If neither an ACK nor a NAK was received, a time-out will eventually occur, which is handled exactly as if a NAK had been received. Eventually, either the transfer is aborted or the end-of-file (EOF) on the file being transmitted is reached. When EOF is reached, an end of transmission (EOT ASCII 04H) is sent. Again, if a nonrobust version of Xmodem is used, the EOT is immediately acted upon. In a robust Xmodem implementation, a NAK will be sent and the next EOT received will be acted upon. Xmodem is the single most popular protocol used in communications and file transfers in the PC market for several reasons: it was developed early (back in the seventies, the formative days of PCs), it is simple to implement and design, it is effective, and most important of all, it works. ─────────────────────────────────────────────────────────────────────────── Sample Code Fragments for MDM_DRV.ASM ─────────────────────────────────────────────────────────────────────────── ;; MDM_DRV.ASM An XMODEM Device Driver ;; Written by Ross M. Greenberg ;; Sample Code Fragments Follow ∙ ∙ ∙ code segment public 'CODE' ;; The driver itself is one large procedure, called 'driver' driver proc far assume cs:code, ds:code, es:code; segments should ; point to code seg org 0 ; drivers must start ; at offset of zero ;; DEVICE DRIVER HEADER header1 dd -1 ; Must be set to -1. Filled in by dw 0e800h ; DOS character device, ; IOCTL supported, ; Output till busy, ; open/close/rm supported dw strat ; point to the strategy routine dw ints ; point to the interrupt routine db 'MDM ' ; The name of device. Left ; justified, space filled to ; eight characters ;; THE REQUEST HEADER request struc rlength db 0 ; 0 - length of data in header unit db 0 ; 1 - unit number (not used) command db 0 ; 2 - the actual command status dw 0 ; 3 - return status reserve db 8 dup (0) ; 5 - Reserved for DOS media db 0 ; 13 - Media desciptor.(Not used) address dd 0 ; 14 - Doubleword pointer for I/O count dw 0 ; 18 - unsigned int cnt for char I/O sector dw 0 ; 20 - starting sector. (Not used) request ends ;; The dispatch table dispatch: dw init ; 0x00 - init driver dw media_chk ; 0x01 - media check (Not used) dw bld_bpb ; 0x02 - Build the BPB (Not used) dw rd_ioctl ; 0x03 - Read IOCTL dw read ; 0x04 - Read 'count' characters dw nd_read ; 0x05 - Non-destructive char read dw inp_stat ; 0x06 - Input Status dw inp_flush ; 0x07 - Input Flush dw write ; 0x08 - Write count chars to device dw write_vfy ; 0x09 - Write Verify dw out_stat ; 0x0A - Output Status dw out_flush ; 0x0B - Flush Output Buffers dw wrt_ioctl ; OxOC - Write IOCTL dw dev_open ; OxOD - Device Open (DOS 3.x) dw dev_close ; 0x0E - Device Close (DOS 3.x) dw rem_media ; 0x0F - Removable Media Routine ; (DOS 3.x) dw out_busy ; 0x10 - Output Until Busy (DOS 3.x) ;; STRATEGY ROUTINE strat proc far mov word ptr cs:[rh_ptr], bx mov word ptr cs:[rh_ptr + 2], es ret strat endp ∙ ∙ ∙ ;; INTERRUPT ROUTINE ints proc far cli mov cs:[old_stack], sp mov cs:[old_stack + 2], ss mov sp, cs mov ss, sp mov sp, offset cs:new_stack_end sti PUSHALL push cs pop ds les di, cs:[rh_ptr] mov bl, es:[di.command] xor bh, bh cmp bx, MAX_CMD jle ints1 mov ax, ERROR + UNK_COMMAND jmp ints2 ints1: shl bx, 1 call word ptr dispatch[bx] les di, cs:[rh_ptr] ; reset just in case ints2: or ax, DONE ; Set the done flag mov es:[di.status], ax ; status in structure POPALL cli mov ss, cs:[old_stack + 2] mov sp, cs:[old_stack] sti ret ints endp ;; READ ROUTINE read proc near DO_PRINT msg_read mov ax, es:[di.count] mov cs:[inrequest], ax ; save request cnt xor ax, ax ; 0 request hdr cnt mov es:[di.count], ax ; and zero rh count lds bx, es:[di.address] ; ds:bx pnts to data top_read: cmp cs:[in_block], TRUE ; are we in a block? jnz lp_it jmp read_loop ; yes lp_it: mov cs:[_incnt], FALSE mov si, offset cs:[_soh] ; pointer block begin mov cs:[timer], TEN_SECONDS ; set up timer mov cs:[nak_cnt], FALSE ; set to no NAKS cmp cs:[block_num], 1 ; first time? jnz rd_blk ; no call send_nak ; send the first NAK STAT LEFT_BRACKET ; show a '[' rd_blk call get_char ; any characters? jc rd_blk2 ; no cmp cs:[_incnt], FALSE ; first character? jnz nxt_char ; no cmp al, SOH ; first char an SOH? jz nxt_char ; yes. store it cmp al, ABORT ; Abort xfer? jz abort_it ; yep. Abort it! cmp al, EOT ; are we done? jnz rd_blk ; no, throw char away call send_ack ; process EOF STAT RIGHT_BRACKET ; show a ']' STAT CR ; and a clean line STAT LF xor ax, ax ret nxt_char: mov cs:[si], al ; store it inc cs:[_incnt] ; bump the char count inc si ; and the pointer rd_blk2: cmp cs:[_incnt], FULLBLKSIZE ; enough bytes jge chkblk ; to check? call eat_char ; for control-C cmp cs:[timer], FALSE ; out of time? jnz rd_blk ; nope. try again STAT DOT ; show a dot jmp bad_blk2 bad_blk: STAT QUESTION ; show question mark bad_blk2: call send_nak ; yep. send a NAK cmp word ptr cs:[abort],TRUE ; abort? jnz rd_blk3 ; no, so reset timer, ; try again abort_it: call send_abort ; send an abort call send_abort ; twice STAT EXCLAIM ; finish with a bang! mov ax, 800ch ; mark an error ret rd_blk3: mov cs:[timer], TEN_SECONDS ; reset the timer jmp rd_blk ; and try again ret chkblk: mov ax, cs:[block_num] push ax dec al ; temporary cmp al, cs:[_blk1] ; duplicate block? jnz real_blk ; no STAT DUP_BLK ; yes xor ax,ax mov cs:[_incnt], ax jmp ack_blk real_blk: pop ax ; back up to correct block cmp cs:[_blk1], al ; is block count ok? jnz bad_blk ; no not al cmp cs:[_blk2], al ; block complement? jnz bad_blk ; no mov cx, BLKSIZE mov si, offset _buf call do_chksum cmp cs:[_chksum], al jnz bad_blk ; checksum bad mov cs:[in_block], TRUE ; looks good! mov cs:[_inptr], offset cs:_buf ; set up pointer sub cs:[_incnt], FULLBLKSIZE - BLKSIZE read_loop: mov si, cs:[_inptr] ; reset the pointer rd_loop: mov al, cs:[si] ; get the character mov ds:[bx], al ; and stuff it inc bx ; up each pointer inc si inc word ptr es:[di.count] ; inc the counter dec cs:[_incnt] ; drop the chars left jnz stf_nxt_char ; still more! inc cs:[block_num] STAT STAR ; send a '*' ack_blk: call send_ack ; send the ack mov cs:[in_block], FALSE ; out of characters jmp top_read stf_nxt_char: dec cs:[inrequest] ; anymore? jnz rd_loop xor ax,ax ; no more request, ret ; no errors read endp ;; WRITE ROUTINE write proc near PUSHALL_AX DO_PRINT msg_write mov cx, es:[di.count] ; how many bytes? xor dx, dx ; zero our char cnt lds bx, es:[di.address] ; ds:bx pnts to data mov si, cs:[_outptr] ; where to stick ; each char wr_lp: mov al, ds:[bx] ; get the character mov cs:[si], al ; and save in buffer inc bx ; move pointers inc cs:[_outcnt] ; total char count inc dx ; current char cnt cmp cs:[_outcnt], BLKSIZE ; send a block? jnz wr_ok ; not yet call send_block ; send the block cmp cs:[abort_xfer], FALSE ; should we abort? jz blk_ok ; no, block sent okay call send_abort ; yes, call send_abort ; send it twice! mov es:[di.count], FALSE ; mark no chars sent mov ax, ERROR + WRITE_ERROR ; mark as write error ret blk_ok: mov cs:[_outptr], offset _buf; reset the pointer, mov si, offset _buf ; the register, mov cs:[_outcnt], FALSE ; and number of chars wr_ok: inc si ; pnt next char pos loop wr_lp mov es:[di.count], dx ; how many went okay mov cs:[_outptr], si ; now save it xor ax, ax ; no errors POPALL_AX ret write endp ;; DEVICE OPEN ROUTINE dev_open proc near PUSHALL_AX DO_PRINT msg_dev_open mov cs:[cancel_flag], FALSE mov cs:[_inptr], offset cs:_soh mov cs:[_incnt], FALSE mov cs:[_outptr], offset cs:_buf mov cs:[_outcnt], FALSE mov cs:[block_num], 1 call set_timer mov dx, cs:[com_port] ; init the comm port mov al, cs:[init_data] mov ah, 0 int 14h eat_loop: mov dx, ds:[com_port] mov ah, 3 ; get status int 14h test ah, 1 ; any data ready? jz continue ; no mov ah, 2 int 14h ; eat the character and jmp eat_loop ; try again continue: test al, 080h ; DCD on? jz off_line jmp on_line ; yes off_line: mov dx, offset cs:offline ; output the message mov ah, 9 int 21h mov si, offset cs:numbuf mov cx, 19 ; maximum length call get_num cmp cs:[kb_len], 0 ; only a return? jnz dial_it mov dx, offset cs:await mov ah, 9 int 21h mov si, offset cs:answerstring call out_string mov cs:[timer], FOREVER jmp offline_lp ; now wait for carrier dial_it: mov dx, offset cs:dialing mov ah, 9 int 21h mov si, offset cs:dialstring call out_string mov si, offset cs:numbuf call out_string mov si, offset cs:return call out_string mov cs:[timer], ONE_MINUTE mov cs:[was_dialed], TRUE ; mark as a dialed call ; for ATH later offline_lp: mov ah, 3 ; are we on-line? mov dx, cs:[com_port] int 14h test al, 080h ; DCD on? jnz made_con ; yes call eat_char ; for control-C cmp cs:[timer], 0 ; out of time? jnz offline_lp ; nope abort_call: mov cs:[was_dialed], FALSE mov dx, offset cs:no_con mov ah, 9 int 21h xor ax,ax mov es:[di.count], ax ; set count to zero mov ax, ERROR + GEN_FAILURE ; mark as an error POPALL_AX ret made_con: mov dx, offset cs:con mov ah, 9 int 21h term_em_lp: call get_char jc get_term_char cmp al, ESCAPE jz term_exit ; escape. Get out! mov ah, 02h mov dl, al int 21h get_term_char: mov ah, 0bh int 21h ; any characters? or al, al jz term_em_lp ; nope mov ah, 1 ; yep. get the character int 21h mov ah, 1 mov dx, ds:[com_port] int 14h cmp al, ESCAPE jz term_exit ; escape. Get out! mov ah, 02h mov dl, al int 21h jmp get_term_char term_exit: mov dx, offset cs:xfer ; status message mov ah, 9 int 21h on_line: xor ax, ax POPALL_AX ret dev_open endp ;; DEVICE CLOSE ROUTINE dev_close proc near DO_PRINT msg_dev_close cmp cs:[cancel_flag], TRUE ; were we canceled? jnz no_cancel ; nope mov ah, 1 ; send the ABORT mov dx, cs:[com_port] mov al, ABORT int 14h jmp no_send ; and exit normally no_cancel: cmp cs:[_outcnt], FALSE ; any characters left? jz no_send ; no call send_block ; yes. send remainder STAT RIGHT_BRACKET STAT CR STAT LF mov cs:[_outptr], offset _buf; reset the pointer, mov si, offset _buf ; the register, mov cs:[_outcnt], FALSE ; and number of chars inc cs:[block_num] ; just in case.... no_send: cmp cs:[block_num], 1 ; minim 1 block sent? jz no_blk_sent ; no mov ah, 1 ; send the EOT mov dx, cs:[com_port] mov al, EOT int 14h no_blk_sent: cmp cs:[was_dialed], TRUE ; were we the dialer? jnz no_hang mov cs:[timer], ONE_SECOND ; one sec of silence sil_lp1: call eat_char ; for control-C cmp cs:[timer], FALSE ; expired? jnz sil_lp1 mov si, offset cs:pluses ; get the modems att call out_string mov cs:[timer], ONE_SECOND ; one sec of silence sil_lp2: call eat_char ; for control-C cmp cs:[timer], FALSE ; expired? jnz sil_lp2 mov si, offset cs:hangup ; now hangup call out_string no_hang: mov cs:[was_dialed], FALSE mov cs:[cancel_flag], FALSE call reset_timer ret dev_close endp ;; NON-DESTRUCTIVE READ ROUTINE ◄──┐ ;; INPUT STATUS ROUTINE │ ;; FLUSH INPUT BUFFER ROUTINE │ These routines are ;; OUTPUT STATUS ROUTINE ├─► located here in the ;; FLUSH OUTPUT BUFFERS ROUTINE │ full program listing ;; READ I/O CONTROL ROUTINE │ ;; Write I/O CONTROL ROUTINE │ ;; SEND_BLOCK ROUTINE ◄──┘ ∙ ∙ ∙ ;; INITIALIZE DRIVER ROUTINE init proc near DO_PRINT msg_init mov ah, 030h ; get the DOS version int 21h cmp ah, 3 ; version 3.x? jge okay_dos ; yes. At least. mov dx, offset cs:wrong_dos ; output the message mov ah, 9 int 21h endless_loop: cli jmp endless_loop ; very ugly, ; but effective! okay_dos: mov dx, offset cs:greetings ; output the message mov ah, 9 int 21h push ds mov ds, es:[di.count + 2] ; get the segment of ; CONFIG.SYS line mov si, es:[di.count] ; get the offset of ; CONFIG.SYS line call output_chars pop ds mov dx, offset cs:end_greetings ; output the message mov ah, 9 int 21h mov word ptr es:[di.address], offset init mov word ptr es:[di.address + 2], cs xor ax,ax ret init endp wrong_dos db '??Must run this driver under DOS 3.0 or higher', db CR, LF, ' System Halted! $' greetings db CR, LF, LF, 'MDM_DRV being installed...', CR, LF db 'CONFIG.SYS Line is: $' end_greetings db CR, LF, LF, LF, '$' output_chars proc near output_loop: mov dl, ds:[si] ; get the character cmp dl, LF ; is it NULL? jnz outit ; no ret outit: mov ah, 2 ; output character int 21h inc si jmp output_loop output_chars endp driver endp code ends end ████████████████████████████████████████████████████████████████████████████ Microsoft C Optimizing Compiler 5.0 Offers Improved Speed and Code Size Augie Hansen☼ Microsoft(R) Corporation has released the latest version of its professional C compiler, Microsoft C Optimizing Compiler Version 5.0. The new compiler produces highly optimized object code that runs about 25-30 percent faster than code generated by the Version 4.0 C compiler. Improved development tools, an enhanced version of the CodeView(R) debugger, expanded documentation, new libraries of routines for BIOS/DOS access, and a full set of graphics routines all support the new version. The professional development environment provided by Microsoft C 5.0 is bolstered by the addition of the highly interactive, integrated development environment offered by QuickC(TM), which is being packaged with the optimizing compiler. Optimizing C A compiler is a type of code translator, specifically a program, or a set of programs, designed to translate instructions from a readable and writeable form (source code) into one that can be understood and executed by a computer (object code). A compiler converts high-level instructions into machine code once, preserving the results in a program file that the operating system can load and run without any additional translation. Assemblers and interpreters are also code translators. An assembler takes low-level mnemonic statements (like MOV AX,0) and converts them to their equivalent machine instructions, which are expressed as sequences of binary numbers, 0s and 1s. The programmer therefore avoids having to program in 0s and 1s, but the programmer is still "programming the bare metal" when writing assembler code. An interpreter, like a compiler, accepts high-level instructions as input, but instead of performing one translation to executable machine instructions, it reads the input and translates it each time a program is run. Interpreters provide an interactive development environment that assists the programmer, but at a cost of slower execution speed of the program. Microsoft C Optimizing Compiler Version 5.0 combines the QuickC compiler (see "Programming in C the Fast and Easy Way with Microsoft QuickC,") with a multipass optimizing C compiler to serve the needs of a wide range of programmers. QuickC, a fast, in-memory compiler offers an interpreter like development environment featuring ease of use and instant feedback. QuickC sacrifices execution speed of the object code to obtain a fast 7000- lines-per-minute compilation speed on a typical 6-MHz 80286-class machine. The compiler will take the source developed in the QuickC environment, or with a favorable text editor, and will optimize it for either speed or code size. Optimizing for Code Size and Execution Speed A translator is free to convert source code into object code in any way it chooses, but the quality of the resulting code varies considerably from one translation to another. A simple translator should produce code that runs and generates the correct results for all inputs, but the executable code may not be the smallest or the fastest that can be written. One measure of the quality of code produced by a compiler is its execution speed; another measure is the size of the object code. Usually a tradeoff must be made between speed and size to arrive at the best code for a given operating condition; rarely does code satisy speed and size requirements simultaneously. Optimization is the process of improving code over that which a simple translation produces. Optimizing compilers attempt to produce the best possible object code resulting in the fastest running or smallest object. The Microsoft C compiler employs state-of-the-art techniques for generating code as well as advanced optimization methods to produce object code that rivals that produced by hand assembly. One added benefit of Microsoft C is that the XENIX(R) C compiler and the equivalent MS-DOS version are essentially the same product. This facilitates the development of programs that are portable from MS-DOS to XENIX and vice versa. Also, the XENIX CMERGE compiler offers a full cross- development environment in which programs targeted for MS-DOS can be developed and managed under XENIX. The XENIX environment is much better suited to large development projects because it provides a well- coordinated set of software engineering tools, multiuser access, and multitasking features that are not available under MS-DOS. Standards The proposed ANSI C standard is still being reviewed and commented on, so no C compiler vendor can claim full ANSI compatibility. Microsoft is participating in the standardization effort, and its compilers follow the proposed standard as closely as possible. In the absence of an official standard, Microsoft C provides the next best thing──full compatibility with the UNIX System V C compiler, which, along with its standard libraries, is the current de facto standard of the industry. The C Programming Language, Brian Kernighan and Dennis Ritchie (Prentice Hall, 1978), served as the de facto standard but was out of date shortly after it was printed because features such as the void function return type (typeless), enum (the enumeration data type), and structure assignment were soon added to the language. Memory Models The standard memory models for Intel segmented-memory processors are supported: small, compact, medium, and large. The default memory model is small. The difference between the models is the maximum allocation of memory for code and data that each permits (see Figure 1). The maximum size of any data item under any of these memory models is 64Kb. The compiler also provides a huge memory model, which is identical to the large memory model except that data items can exceed 64Kb. Mixed-model programming permits code and data items to be referenced as near or far items. The keywords near and far can be applied to variables and functions according to how addressing is done. A near address is a 16- bit offset with a default segment. A far address uses both segment and offset values, 16 bits each, to address memory items. Items addressed by near pointers are assumed to lie completely within a single 64Kb segment, so pointer arithmetic is done only on the 16-bit offset component of an address. To permit direct access to large data items, such as a large array, the huge keyword specifies full 32-bit addressing. Individual array elements are still limited to 64Kb, and while they cannot span segment boundaries, the array can be larger than 64Kb. Also, casts to long are required on huge pointer differences and sizeof values. The huge keyword applies only to data; it does not apply to code items. Floating-point Support Microsoft C performs floating-point arithmetic in several ways. If a numeric data processor (NDP, commonly called a coprocessor) is present, in-line code, which avoids unnecessary function-call overhead, is generated for calculations to obtain the greatest possible execution speed. In the absence of an NDP, run-time modules produced by Microsoft C automatically use software emulation routines for math calculations. Routines that calculate to the same 80-bit accuracy as the NDP are used by default. Microsoft binary math routines that calculate with 64-bit accuracy can be selected if reduced accuracy can be reasonably traded for faster program execution. Interlanguage Calling The C language uses a different convention for passing parameters than such programming languages as FORTRAN and Pascal. FORTRAN and Pascal push parameters onto the stack in left-to-right order, which is the order in which they appear in procedure and function declarations. The C language convention is just the reverse──the leftmost parameter of a function parameter list is the last one pushed onto the stack. The different convention results from a C language provision for passing a variable number of parameters to a function. Since the first parameter is always the last one pushed, its offset from the start of the stack frame is at a known address. The first parameter, or a subsequent one, can contain information about how many other parameters were passed. This is how printf-family functions are implemented. The C language convention (the default) requires that the calling function contain the instructions that tell how to restore the stack to the conditions that applied before the call. The clean-up code is added after each and every call to the function. The alternative FORTRAN and Pascal convention is to place the stack clean-up code in the called procedure or function. Thus the clean-up code appears only once for each procedure regardless of how many times the procedure is called. The net effect is that the FORTRAN and Pascal calling conventions produce slightly smaller and faster executable code at the expense of some programming flexibility. In small programs, the difference is negligible, but as programs grow in size and complexity, the advantages of the fixed- parameter convention become more apparent. Microsoft C offers the fortran, pascal, and cdecl extended keywords to tell the compiler which calling convention to use on a function-by- function basis in programs that involve interlanguage calling. Also, a compiler command-line switch, /Gc, can be used to compile an entire module by using the alternative calling convention. Installation An automated procedure greatly simplifies compiler installation. The SETUP program takes a set of parameters that specify the memory-model support desired, optional names of directories for commands, headers, and libraries, and an optional C 4.0 compatibility flag. Use the SETUP program to ensure that all necessary files are placed in the correct subdirectories. A frequent problem associated with the installation of earlier versions of the Microsoft C compiler is a failure on the part of the programmer to create a SYS directory under include and to copy in the system header files, such as stat.h. The SETUP program also has the option of creating a single combined library for each memory model specified, which results in fewer disk accesses and, therefore, faster linking times. Standard C Library The standard library contains a comprehensive set of system calls and standard subroutines. Taken together, the roughly 250 standard routines provide the essential time, input/output, string-handling, memory- management, process-control, and math operations needed to construct a wide range of programs. The system calls for MS-DOS to follow the UNIX/XENIX pattern for buffered and unbuffered access to the file system and files. Owing to differences in the operating systems, however, some of the functions behave differently or require different parameters or numbers of parameters. A few UNIX/XENIX functions, such as fork, getlogin, and sleep, have no MS- DOS equivalents. Still, the standard subroutines are, for the most part, equivalent to their UNIX/XENIX alter egos. Graphics Library In addition to standard UNIX System V──compatible run-time libraries, Microsoft C includes about 50 new graphics and DOS/BIOS functions, which are contained in separate libraries, to help programmers with tasks that have previously required custom programming. The GRAPHICS.LIB library file contains graphics functions that take advantage of highly compliant CGA and EGA display hardware. Support for selected VGA modes is also included. Figure 2 is a summary of the graphics routines. The graphics routines differentiate between visual and active screen pages, memory regions that hold images that are being displayed (visual page) and written to (active page). If the pages (memory areas) are identical, then anything written is seen as it is being drawn. To create animation effects or "instant" screen updates, use separate active and visual pages. Write to the active page while the visual page is being viewed, and then swap the pages. All graphics writing is relative to a logical coordinate system, which is, in turn, expressed relative to the screen's physical coordinate system. The origin of the physical coordinate system is (0, 0) at the upper-left corner of the screen, while the origin of the logical coordinate system can be set to any valid physical coordinate. The number of pixels in each screen dimension can be obtained from the configuration data. The x-axis increases its values as it moves to the right, but the y-axis is inverted, with values increasing as it progresses downward. You can write macros to change signs in order to provide axis translation if it is needed in an application. Text is managed on a grid of rows and columns with row 0 and column 0 at the upper-left corner of the screen. Text can be output in either text or graphics modes. Each text item is simply a string, so any numbers must be converted to their string representations before being written. All images written by functions in the graphics library, whether graphics or text, are clipped to specified window boundaries (clipping regions). The graphics library completely controls display attributes, line styles, and area-fill operations. The built-in graphics support provided by Microsoft C considerably eases the burden of graphics programming by encapsulating the most commonly used graphics operations into a convenient, memory model-independent library of routines. The routines relieve the programmer of a tremendous amount of detailed programming work in preparing and controlling graphics images on the screen. A sampling of the compiler's graphics functions is used in GDEMO to show how easy it is to use the functions, and how capable and flexible the interface is. Although GDEMO is a very simple graphics program, it demonstrates how programs set a graphics mode, either upon entry or during normal operation, and then restore the user's original video mode upon exit. Examining the GDEMO.C source file reveals that a call to _setvideomode is all that is necessary to switch to a specific video mode. Modes are represented by symbolic names defined in the GRAPH.H header file. In this program, you set the medium-resolution 16-color graphics mode (_MRES16COLOR), which requires an enhanced graphics adapter (EGA) and a suitable monitor. Before the program terminates, restore the video mode that was in effect before the program started running by sending a call to _setvideomode with the mode parameter set to _DEFAULTMODE. The demonstration program is purposely unfettered by error-checking code to make the graphics calls visible, but the return value of the function call should be checked before graphics operations are performed to be sure that the mode-setting operation is successful. A return value of 0 flags an error, which is usually caused by a program trying to set an unsupported video mode. A check such as if (_setvideomode (_MRES16COLOR) > 0) { fprintf (stderr, "This program requires an EGA\n") ; exit (1) ; } will do the job. An alternative design approach is to attempt a switch to one of the CGA modes if the EGA test fails. The program can run, albeit with fewer colors, on a CGA-compatible video system. After setting the video mode, it is necessary to gather video configuration information to discover the screen limits in pixels and other important information about the video system. The information is saved in a structured variable that is defined by the struct videoconfig template in the GRAPH.H header file. In GDEMO, the screen dimensions are used to obtain the value of the screen center and make that coordinate the logical origin by the call to _setlogorg. Drawing lines and rectangles is as easy as writing calls to _rectangle, _moveto, and _lineto. All points are expressed relative to the previously defined logical origin. The graphics functions handle all calculations needed to place points at the correct physical screen locations. Plotting arbitrary points requires only slightly more work. The _setpixel function plots points and sets the pixel at the specified logical coordinate to the current color. The opposite operation, retrieving the value of a specified pixel, is done by _getpixel. As noted earlier, the sense of the y-axis needs to be inverted if curves are to be drawn correctly from the user's point of view. The hardware notion of screen coordinates is based on the fact that the physical origin (0, 0) is at the upper-left corner of the screen. The PLOT macro was written to simplify the plotting of points: #define PLOT(x_, y_) _setpixel ((x_), -y)) This macro inverts the value of the y parameter and calls _setpixel to display the point. In graphics modes, text is actually drawn by routines that plot a pattern of points. The graphics-mode text origin is at the upper-left corner of the screen and text-writing functions use the familiar text-mode conventions of screen rows and columns. Placing text into an image requires a call to _settextposition to move to the desired row and column, followed by a call to _outtext with a pointer to a NUL-terminated string as the sole parameter. A number of other functions are provided in the graphics library to control access to video screen pages; set colors, control cursor visibility, set clipping regions and viewports, and manage coordinate systems. This highly functional graphics interface is an important addition to the Microsoft C compiler package. Library Functions Another new set of functions in the Microsoft C compiler package provide operating system and PC hardware interfaces. Figure 3 is a summary of new MS-DOS and BIOS functions that greatly simplify programming in an MS-DOS environment. Note that these functions are not portable to XENIX or other environments; they are intended solely to give full access to the MS-DOS environment. The BIOS interface functions offer easy access to basic input/output interrupts. Besides housekeeping and configuration information (system clock, equipment list, and memory size), the BIOS library functions include peripheral-access functions for disks, printers, and serial ports. The MS-DOS interface functions access the operating system through system calls. Several of the functions in the set were available in earlier versions of the C compiler: bdos, int86, and its variants. The new MS-DOS functions provide C language functions that make calls on the Interrupt 21H "umbrella" interrupt. Previously, it was necessary to write the functions in terms of int86 calls. Now each of the major MS-DOS services can simply be called by name, such as _dos_getdate to obtain the current system date. Heap Debugging Dynamic storage problems can be the bane of any programmer's existence. The heap is the unallocated area of memory in which dynamic simple variables and data structures are allocated, moved, and freed during the running of a program. It is often difficult to find errors in heap management, so three new functions have been added to the C library to assist programmers in testing and debugging dynamic memory. The _heapchk function runs consistency checks, _heapset fills the heap with known values, and _heapwalk "walks" through the heap gathering information about each entry. When called in small data models (small and medium), the functions map to near-heap versions (_nheapchk, _nheapset, and _nheapwalk). In large data models (compact and large), they map to far-heap versions (_fheapchk, _fheapset, _fheapwalk). Controlling Compilations Pragmas are implementation-defined preprocessor directives that are ignored by systems that don't recognize them. A pragma directive, added to C by the proposed ANSI standard, is introduced by the #pragma token. Microsoft C uses pragmas to control compilation. A loop optimization pragma allows local control over loop optimizations. Local control overrides any global optimization controls established on the compiler command line. For example, #pragma loop_opt(on) enables loop optimizations, and #pragma loop_opt(off) disables loop optimizations. Stack-checking can be turned on and off in a similar fashion by using the check_stack pragma. Other pragmas offer control of intrinsics, structure alignment and packing, and segment allocations. Run-time Library Source The source code for the entire run-time library is now available as a separate product. Having the library source code gives programmers a base for customization to meet special requirements. The source also has many examples of tightly written C and assembler code to help beginning and expert programmers. Enhanced CodeView CodeView is a full-screen symbolic source-level debugger. Multiple windows can display source and object code simultaneously, set and observe watchpoints, set breakpoints, watch CPU register and flag values, and trace the stack. CodeView provides complete execution control over the program being debugged, including single-stepping, screen-swapping to alternately view code and resulting screen displays, and many other features (see "A Guide to Debugging with CodeView," MSJ, Vol. 2 No. 1). Since its introduction, the debugger has been improved and enhanced with new features. Expanded memory support gives CodeView access to large amounts of memory, thus very large applications, including those that use overlays. A new mixed-language debugging capability allows programmers to debug programs with modules written in Microsoft C, FORTRAN, Macro assembler, Pascal, and BASIC. The QuickC integrated environment includes a subset of CodeView debugging features. Using MAKE Simple programs, particularly those that consist of only a single source file, are easily translated into executable programs. You simply use the compiler driver program, CL.EXE, which takes a source filename and possibly some optional parameters. It controls the entire preprocessing, compiling, and linking phases of the translation. CL calls the individual compiler passes, C1, C2, and C3, to compile the source statements and then it calls the LINK program to combine the object file with the standard C library routines. Programs that involve more than one source file or a mixture of assembler and C sources and those that require linking with libraries other than the default LIBC.LIB (for the selected memory model) require a greater project management effort that is best controlled by the MAKE program builder/maintainer utility. Figure 4 depicts one method of creating a two- module graphics demonstration program. The primary purpose of MAKE is to automate program building and maintenance tasks. You build a description of how the various elements of the program are created and put together to form the final product. MAKE interprets the instructions in the description file and does the least amount of work possible to keep the product current with its component parts. MAKE thus speeds up the development by reducing the amount of typing you have to do and eliminating unnecessary compilation steps. A makefile is a user-supplied set of instructions that specifies the names of targets (items to be created) and the sources from which the targets are to be built; MAKE interprets the relationships among the sources and targets. The makefile for the graphics demonstration example program, for instance, is contained in the file GDEMO.MAK. In order to create GDEMO.EXE for the first time or to update it after changes have been made to any of the source files or libraries, you simply type MAKE GDEMO.MAK and MAKE will compile any sources that are newer than their respective object files. The temporal relationships among files are the basis of MAKE's operation. MAKE uses the MS-DOS directory date and time stamps to determine whether to remake a target file. A given target can have one or more explicit dependencies expressed in the makefile. If any one or more of the files upon which a target depends is newer than the target, the target is then remade. An implicit dependency, such as that which exists between any object file (.OBJ) and its source file (.C, for example) can be given in rules specified in a TOOLS.INI file, which MAKE can find either in the current directory or in one of the directories specified by the PATH environment variable. Explicit rules can be placed in the makefile to override implicit rules. Sample Project To illustrate the building and maintenance of a program, let's construct a graphics demonstration program. GDEMO switches to medium-resolution graphics mode and draws an x-axis, a y-axis, plots two equations, and labels them. The program uses a macro to translate plotted y values of the equations. It then prompts the user and waits for a keypress before restoring the original video mode and returning to DOS. The program shows how text and graphics can be easily combined in a screen image. The C source is split into two files. GDEMO.C (see Figure 5) contains the main program source, which illustrates the use of several new graphics library routines. MESSAGE.C (see Figure 6) contains the source for a function that writes a text string at a specified location and waits for a continuation signal, which is the user pressing a key on the keyboard. In creating the program, I used QuickC to develop and test the code. QuickC automatically created the file GDEMO.MAK (see Figure 7), which controls the compiling and linking within the QuickC integrated environment. To use the in-memory MAKE utility, all you have to do is identify the source files from which the program is built, and QuickC does the rest. In switching to the full Microsoft C 5.0 environment to get the benefit of greater optimization for program speed, I edited the makefile to use the CL compiler driver program instead of QCL and to build in the link-control statements rather than have them placed in a separate linker-response file. The revised makefile is the GDEMO.MAK shown in Figure 8. The graphics screen display produced by GDEMO appears on the first page of this article. Each of the labels is positioned and written by the Message function. For the equation labels, the wait parameter to the Message function call is M_NOWAIT, so the function just draws the text and moves on. The prompt message at the bottom of the screen is drawn by a call to Message with wait set to M_WAIT, which tells the function to wait for the user to press a key. The Best of Both Worlds Before Microsoft introduced its C compiler several years ago, I used a number of other C compilers in a quest to find the best one for MS-DOS programming tasks. Since the introduction of Microsoft C Version 3.0, many other C programming products have been introduced into a market that is hungry for programming products. The fact that so many vendors have survived in an intensely competitive environment is a testimonial to both the enormous size of the market and its explosive growth. My recent book about C programming, Proficient C (Microsoft Press, 1987), is devoted to programming in an MS-DOS environment and is based on the Microsoft C Version 4.0 compiler. Microsoft C compilers are noted for the fast-running code they produce, as well as for their full UNIX-compatible library support. Significant portions of the book deal with low-level access to MS-DOS and PC system services, and in spite of the need for speed and tight coupling to the hardware and operating system, it was not necessary to write even a single line in assembler. Having used the new Microsoft C Optimizing Compiler Version 5.0 for many months during its development and testing phases, I am impressed by its overall quality, speed, and breadth of coverage. I consider it to be the C programming tool of choice. Proficient C contains more than 6500 lines of C source code. After recompiling the entire source using the Version 5.0 compiler, I observed an object module space saving of about 15 percent and an increase in program execution speed averaging better than 25 percent compared to the Version 4.0 compiler. Microsoft C Optimizing Compiler Version 5.0 is a very sophisticated compiler system. By combining QuickC, a speedy in-memory development environment, with a new optimizing C compiler, Microsoft has developed a compiler package that serves the needs of beginner and expert alike. Speed and code size improvements, great new libraries, and an enhanced CodeView all combine to produce the best C programming environment available today. Figure 1: Available Memory Models Maximum Maximum Model Data Size Code Size small 64Kb 64Kb compact 64Kb available medium available 64Kb large available available Figure 2: Graphics Library Routines Configure _displaycursor Cursor ON/OFF upon graphics exit _getvideoconfig Get current graphics environment _setactivepage Set memory area for writing images _setvideomode Set screen display mode _setvisualpag Set memory area for displaying images Set Coordinates _getlogcoord Convert physical to logical coordinates _getphyscoord Convert logical to physical coordinates _setcliprgn Set clipping region _setlogorg Position the logical origin _setviewport Limit output region; set origin Set Palette _remapallpalette Assign colors to all pixel values _remappalette Assign colors to selected pixel values _selectpalette Select a predefined palette Set Attributes _getbkcolor Obtain current background color _getcolor Obtain current color _getfillmask Obtain current fill mask _getlinestyle Obtain current line style _setbkcolor Set background color _setcolor Set color _setfillmask Set fill mask _setlinestyle Set line style Output Text _gettextcolor Obtain current text color _gettextposition Obtain current text output position _outtext Display text at current position _settextposition Set the text position _settextcolor Set the text color _settextwindow Establish a text window _wrapon Enable/disable line wrap Output Images _arc Draw an arc _clearscreen Clear screen to background color _ellipse Draw an ellipse _floodfill Fill an area with the current color _getcurrentposition Obtain current graphic output position _getpixel Obtain current pixel value _lineto Draw a line _moveto Move graphic output position _pie Draw a pie-slice shape _rectangle Draw a rectangle _setpixel Set a pixel value Transfer Images _getimage Store a screen image in memory _imagesize Return image size (in bytes) _putimage Retrieve an image and display it Figure 3: BIOS and MS-DOS Library Routines BIOS Interface _bios_disk Issue disk requests _bios_equiplist Perform equipment check _bios_keybrd Access keyboard services _bios_memsize Get available memory size _bios_printer Access printer output services _bios_serialcom Access serial comm services _bios_timeofday Access system clock DOS Interface bdos Invoke DOS system call _chain_intr Chain interrupt handlers _disable Disable interrupts _dos_allocmem Allocate a block of memory _dos_close Close a file _dos_creat Create a file _dos_creatnew Create a new file _dos_findfirst Find the first occurrence of a file _dos_findnext Find subsequent occurrences of a file _dos_freemem Free a block of memory _dos_getdate Get the system date _dos_getdiskfree Get disk drive information _dos_getdrive Obtain ID of current disk drive _dos_getfileattr Get file or directory attributes _dos_getftime Get date and time of last file write _dos_gettime Get the system time _dos_getvect Get value of an interrupt vector _dos_keep Install as TSR program _dos_open Open an existing file _dos_read Read a file _dos_setblock Change the size of an existing block _dos_setdate Set the system date _dos_setdrive Set the default disk drive _dos_setfileattr Set file or directory attributes _dos_setftime Set file date and time _dos_settime Set the system time _dos_setvect Set an interrupt vector value _dos_write Send output to a file dosexterr Obtain register values _enable Enable interrupts FP_OFF Return offset portion of far pointer FP_SEG Return segment portion of far pointer _harderr Establish a hardware error handler _hardresume Return to DOS after a hardware error _hardretn Return to the application after a hardware error int86 Invoke a DOS interrupt int86x Invoke a DOS interrupt with segment register values intdos Invoke general DOS system call intdosx Invoke general DOS system call with segment register values segread Returns values of segment registers Figure 4: Software Project Management with MAKE ┌─────────────┐ ┌─────────────┐ │ GDEMO.MAK │►│ │ └─────────────┘ │ MAKE.EXE ├──────────────────────┐ │ │ │ └──────┬──────┘ │ ┌─────────────┐ ┌──────▼──────┐ ┌─────────────┐ ┌───▼──────┐ │ GDEMO.C │►│ │►│ GDEMO.OBJ │►│ │ └─────────────┘ │ │ └─────────────┘ │ │ │ │ │ │ │ CL.EXE │ │ │ ┌─────────────┐ │ │ │ │►│ GDEMO.EXE │ ┌─────────────┐ │ │ ┌─────────────┐ │ │ └─────────────┘ │ MESSAGE.C │►│ │►│ MESSAGE.OBJ │►│ │ └─────────────┘ └──────■──────┘ └─────────────┘ │ LINK.EXE │ ■ ┌─────────────┐ │ │ ┌──────■──────┐ │GRAPHICS.LIB │►│ │ ┌─────────────┐ │ │ └─────────────┘ │ │►│ GDEMO.MAP │ GDEMO │ C1.EXE │ │ │ └─────────────┘ Graphics │ C2.EXE │ │ │ Demo │ C3.EXE │ ┌─────────────┐ │ │ Program │ │ │ ?LIBC.LIB │►│ │ └─────────────┘ └────────────┘ └──────────┘ The ? is replaced by S,C,M, or L to match the selected memory model. Figure 5: GDEMO.C /******************************************************************** * NAME: gdemo * * DESCRIPTION: This is a simple graphics demonstration program. Its * purpose it to show how to select a graphics mode, do some simple * graphics work, and then return to DOS after restoring the user's * previous video environment. ********************************************************************/ #include #include #define RHEIGHT 160 #define RWIDTH 300 #define MSG_ROW 25 #define MSG_COL 8 #define M_NOWAIT 0 #define M_WAIT 1 /* * Macro to plot points with the normal sense of the y axis. The * graphics library default is inverted (y increases in value moving * down the screen). */ #define PLOT(x_, y_) _setpixel((x_), -(y_)) extern void Message(short, short, char *, short); int main() { struct videoconfig config; /* video configuration data */ short x_org, y_org; /* coordinates of the origin */ short x_ul, y_ul; /* upper-left corner */ short x_lr, y_lr; /* lower-right corner */ short x, y; static char prompt[] = { "Press a key to continue..." }; /* * Set up the medium-resloution graphics screen and set * the logical origin to the center of the screen. */ _setvideomode(_MRES16COLOR); _getvideoconfig(&config); x_org = config.numxpixels / 2 - 1; y_org = config.numypixels / 2 - 1; _setlogorg(x_org, y_org); /* * Draw a border rectangle and coordinate system. */ x_ul = -RWIDTH / 2; y_ul = RHEIGHT / 2; x_lr = RWIDTH / 2; y_lr = -RHEIGHT / 2; _rectangle(_GBORDER, x_ul, y_ul, x_lr, y_lr); _moveto(x_ul, 0); _lineto(x_lr, 0); _moveto(0, y_ul); _lineto(0, y_lr); /* * Plot some curves. */ for (x = -100; x <= 100; ++x) { y = x / 2; PLOT(x, y); } Message(11, 27, "y = x / 2", M_NOWAIT); for (x = -20; x <= 20; ++x) { y = (x * x / 3) - 75; PLOT(x, y); } Message(22, 3, "y = x**2 / 3 - 75", M_NOWAIT); /* * Wait for the user's command to continue. */ Message(MSG_ROW, MSG_COL, prompt, M_WAIT); /* * Restore the original video mode. */ _setvideomode(_DEFAULTMODE); return (0); } Figure 6: Message.C /*************************************************************** * Message * * DESCRIPTION: Display the message text at the specified * screen location (row, col). If wait has a non-zero value, * wait for the user to press a key. When the user complies, * grab the character from the keyboard buffer so it won't * interfere with the calling program following the return. ***************************************************************/ #include #include void Message(row, col, text, wait) short row, col; /* text position */ char *text; /* text pointer */ short wait; /* wait flag */ /* (wait != 0 means wait for a keypress) */ { int k; /* key code */ /* * Write the prompt text at the specified location. */ _settextposition(row, col); _outtext(text); /* * If the wait flag is set, wait for a key to be pressed, * then remove the code from the keyboard buffer. Handle * extended codes by grabbing two bytes if the first is NUL. */ if (wait) { while (!kbhit()) ; k = getch(); /* read the character */ if (k == '\0') /* extended code -- get next byte */ getch(); } } Figure 7: GDEMO.MAK as Created by QuickC. # # Program: Gdemo # .c.obj: qcl -c -W2 -Ze -AM $*.c gdemo.obj: gdemo.c message.obj: message.c Gdemo.exe : gdemo.obj message.obj del Gdemo.lnk echo gdemo.obj+ >>Gdemo.lnk echo message.obj >>Gdemo.lnk echo Gdemo.exe >>Gdemo.lnk echo Gdemo.map >>Gdemo.lnk link @Gdemo.lnk $(LDFLAGS); Figure 8: GDEMO.MAK Edited to work with the Full Optimizing Complier. # makefile for the GDEMO program .c.obj: cl -c -W2 -Ze -AL $*.c gdemo.obj: gdemo.c message.obj: message.c gdemo.exe: gdemo.obj message.obj link gdemo message, gdemo, nul, graphics; ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob! Keeping the Faith Dear Dr. Bob, Charles Petzold's article, "A Simple Windows Application For Custom Color Mixing," MSJ Vol. 2 No. 2, must contain an error. I tried running the program under the latest beta version of Windows and it will not run. However, it runs fine under an earlier version. What gives?──FH Dear FH, The COLORSCR program uses wndclass.hbrBackground to paint its background color, which is fine for most applications. However, it is a program in which different instances of the application will have different color backgrounds. Since the window class is normally common to all instances of an application, using the wndclass.hbrBackground is a bit of a problem. In each instance, COLORSCR "solves" this problem by reregistering the window class, so that each one will have its own wndclass.hbrBackground. However, that's not the right way to do it. It works in Windows 1.0x and the early beta releases of Windows 2.0, but if you try running COLORSCR under the final Windows 2.0 release, you'll discover that it will only run once. Subsequent instances refuse to start. This is because Windows 2.0 no longer lets you reregister. The second RegisterClass will fail if you try to register the same window class twice. You would have been right to think that the technique of registering the class in each instance sounded questionable. The correct way to have different background colors in different application instances is to set the value of wndclass.hbrBackground to NULL, and process the WM_ERASEBKGND message explicitly in your window function. This works properly in all versions of Windows. To ensure that COLORSCR uses the correct background erasing method, make the changes listed in Figure 1 to the program. Charles Petzold replies: Thanks for the correction. As many Windows programmers know, the Windows technical documentation and the actual workings of Windows are frequently inconsistent. Often it is necessary for programmers to assume that the documentation is wrong and to take a more empirical approach to Windows programming. Of course, the danger in this is that later revisions to Windows may work differently, which is what happened in the case of COLORSCR. The program was originally developed under Windows 1.03 and only briefly tested under an early version of Windows 2.0. It is interesting to note that the Windows 2.0 documentation──at least the most recent copy I've seen──still indicates that "If two classes having the same name are registered, the most recently registered class is recognized; the other is ignored." So it appears as if Windows is still not working in accordance with the documentation. This raises a far more serious question, and that is whether a system with as complex and detailed an API as Windows can be documented adequately enough to discourage developers from empirical programming. I have not yet seen any evidence that it can, but I'm keeping the faith. Languages Interfacing Dear Dr. Bob, I have developed a number of routines using Microsoft C 4.0 that I would like to be able to utilize in the Microsoft QuickBASIC environment. How can I accomplish this?──GY Dear GY, Microsoft QuickBASIC Version 4.0 will let you interface to your C routines quite easily. There are a number of conventions to observe, but you can interface between the two using one of two methods. You can compile your Microsoft QuickBASIC program modules from the MS-DOS command line using BC (the Microsoft QuickBASIC command line compiler) and then LINK the resulting object file to any C routine object file(s) or library. In fact, most of the functions available in the C standard library can be called. Alternatively, you can compile your C routines, link them into a Quick library and then load the library into the Microsoft QuickBASIC environment. The routines would then be available to your program from within the environment itself. Without listing all the details, there are a number of requirements to observe. First, any C routine called through Microsoft QuickBASIC must be DECLARE'd as either a SUB or FUNCTION (see Figure 2). A FUNCTION procedure can return a value whereas a SUB procedure cannot. Second, a common calling convention must be established. For example, the CDECL keyword in the DECLARE statement would insure that arguments passed by the calling routine use the standard C convention for passing arguments, in the reverse order from which they are listed, which is what the C routine would expect. This is the most common case, and will not require modification of your C code. It is also possible to change the calling convention of the called function instead. For example, the called C routine can be made consistent with BASIC by use of the fortran keyword, as shown in Figure 3. Third, a common naming convention must be observed. For example, the C compiler recognizes 31 characters and does not change upper and lowercase letters. Further, it always inserts an underscore before each routine name. The function called_proc in Figure 3 becomes _called_proc. BASIC recognizes up to 40 characters, drops the BASIC type declaration characters, converts all letters to uppercase, and allows the use of a period within variable names. The CDECL keyword insures that the C naming convention is followed, i.e. periods are converted to underscores and a leading underscore is added to the name. Since the linker will ignore case (assuming /NOI is not specified), Called.Proc will result in _Called_Proc, and the linker will be able to resolve the external reference. This means that C routines should not rely on case (e.g. _called_proc and _CALLED_proc) to distinguish between different functions. Also note that CDECL does not enable C to recognize more than 31 characters. There are two ways around this limitation. The first is to simply avoid names longer than 31 characters. The second is to use the Microsoft QuickBASIC keyword ALIAS, which allows a substitute name to be passed. Fourth, a convention must be established for passing parameters (the data itself). Microsoft compilers pass parameters in one of three ways: by value, by near (offset), or by far (segment + offset) reference. The calling and called routines must agree on how a given parameter will be passed. A variable passed by reference will give a called routine direct access to that variable, allowing a called routine to change a variable's value. Any such change would then have to be taken into account by the calling procedure. Passing by value means the called routine has no actual access to the variable. The variable's value is known and can be used, but the variable itself cannot be changed. As it happens, the default in BASIC is to pass parameters by near reference. In C, the default (except for arrays) is to pass parameters by value, unless pointers are used (see Figure 4). The C function declares its three parameters as near pointers, meaning the parameters are expected to be passed as references. Since this is the default method in BASIC no other keywords are necessary. Figure 5 shows a C function that expects a parameter passed as a far reference, and a parameter passed as a value. Note the DECLARE statement in the Microsoft QuickBASIC code. The SEG keyword causes a segmented (far) reference to be passed. The BYVAL keyword causes a true value to be passed. This is different than the standard method of passing a value in BASIC, which is to use two sets of parens. CALL CALC((Q)) still passes a reference, but the reference is to a temporarily created variable and not to the actual variable, therefore preserving the value of the original. BYVAL actually allows a true value to be passed, rather than a reference to a temporary variable. Arrays should always be passed by far reference, using the SEG keyword. Arrays of dynamic strings, as well as $STATIC arrays are near references, but all other arrays are far references. Unless you are absolutely confident that a near reference will work with an array, use the SEG keyword, which will always work. Finally, memory models must be taken into account. Microsoft QuickBASIC uses far code addresses exclusively. This means that any C routines that are going to be used in a Microsoft QuickBASIC program must be compiled using the medium, large, or huge models. This is the simplest approach. Small and compact models can be used if the specific C routines are declared as far routines. There are other do's and don'ts, but this covers most of the basics. The point is that once you've mastered the rules, mixed model programming──not only between C and BASIC but between several other languages──is relatively simple. TILER Correction Dear Dr. Bob, The program TILER printed in MSJ Vol 2, No 3 appears to have an error in it. It seems to me that, contary to the author's assertion, the main message loop requires the TranslateMessage call. Am I missing something subtle?──AJ Dear AJ, No, you aren't missing a thing. TranslateMessage does belong in the main message loop. Anyone who typed in the program and tried to run it would discover that it doesn't work quite right. For instance, the keyboard interface for the Control (System) menu works only partially. The author was under the mistaken impression that you do not need to call TranslateMessage if you don't care about WM_CHAR messages. However, TranslateMessage takes care of a few other things──including part of the keyboard processing for the Control menu. Every Windows application should call TranslateMessage in the main message loop. That loop in TILER (see p. 33 MSJ Vol 2, No. 3) should read as shown in Figure 6. By-the-way, the author admitted to being red in the face when advised of this error. Close Resemblance Dear Dr. Bob, I just received the November issue of National Geographic, and there was a fascinating article on a family of baboons in Kenya. Well, I was certainly surprised when I read the article and discovered that one of the baboons is named──Dr. Bob! Can it really be you? I always knew you were a charming, intelligent fellow, but my, what big teeth you have! Next time I'm in Kenya, I'll be sure to stop by and say hello.──GP Dear GP, If you look really closely you can even see the lap-top computer, modem, and telephone that I use to transmit and receive information between Kenya and the United States. Correction The Windows/386 article in the September issue of MSJ states that Windows/386 will run on the AT&T 6300 computer. This is clearly incorrect. We meant to state that Windows/386 will support the AT&T(R) 6300 display. We should also point out that the error was due to the creative work of MSJ and not the author, Ray Duncan, who most assuredly knows his 386 machines from his 86/286's. Figure 1: Add a new static variable: HBRUSH hbrBkgnd; In WinMain, change: wndclass.style = CS_HREDRAW | CS_VREDRAW; ∙ ∙ ∙ wndclass.hbrBackground = CreateSolidBrush(0L); ∙ ∙ ∙ if( ! RegisterClass(&wndclass) ) return FALSE; to: if( ! hPrevInstance ) { wndclass.style = CS_HREDRAW | CS_VREDRAW; ∙ ∙ ∙ wndclass.hbrBackground = NULL; ∙ ∙ ∙ if( ! RegisterClass(&wndclass) ) return FALSE; } hbrBkgnd = CreateSolidBrush(0L); In WndProc, add this local variable: RECT rect; In both places where it occurs, change: DeleteObject( GetClassWord(hWnd,GCW_HBRBACKGROUND) ); to: DeleteObject(hbrBkgnd); Under SB_THUMBPOSITION, change: SetClassWord( hWnd, GCW_HBRBACKGROUND, CreateSolidBrush( RGB(color[0],color[1],color[2]) ) ); to: hbrBkgnd = CreateSolidBrush( RGB(color[0],color[1],color[2]) ); Add one new case to WndProc: case WM_ERASEBKGND: GetClientRect( hWnd, &rect ); FillRect( (HDC)wParam, &rect, hbrBkgnd ); break; Figure 2: DECLARE SUB Exchange CDECL (a%, b%) ' There is no return value DECLARE FUNCTION Power% CDECL (a%) ' Declaring Power% as FUNCTION allows ' a value to be returned by Power% Figure 3: int fortran called_proc (int a, char b) { /* The function body goes here. Note the * fortran keyword, which will pass * parameters consistent with the BASIC * convention. */ } Figure 4: DECLARE SUB Exchange CDECL (a%, b%) A% = 10 B% = 20 C% = 30 PRINT A%; B%; C% CALL Exchange(A%,B%,C%) PRINT A%; B%; C% /* C routine exchange() */ void exchange(int near *x,int near *y, int near *z) { int a1; int a2; a1 = *x; a2 = *y; *x = *z; *y = a1; *z = a2; } /* A%, B% and C% are passed by near reference * to exchange, which manipulates their * values using near pointers. The new * values become available immediately * after the c routine returns control */ Figure 5: DECLARE FUNCTION AddValue% CDECL (SEG a%, BYVAL b%) D% = 10 E% = 20 F% = D% + E% PRINT D%; E%; F% PRINT D%; E%; AddValue%(D%, E%) ' The FUNCTION declaration allows AddValue AddValue to return a value /* C routine addvalue() */ int addvalue(far *y, int n) { int sum; sum = *y + n; return(sum); } /* The C routine expects a far reference * for its first parameter, as indicated * by SEG a%, and receives its second * parameter by value, as indicated by * BYVAL b%. */ Figure 6: /* Main message processing loop */ while( GetMessage( &msg, NULL, 0, 0 ) ) { TranslateMessage( &msg ); DispatchMessage( &msg ); }