Microsoft Systems Journal Volume 3

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

Vol. 3 No. 1 Table of Contents

──────────────────────────────────────────────────────────────────────────── Preparing for Presentation Manager: Paradox(R) Steps Up to Windows 2.0 Ansa Software is developing versions of the Paradox data base product for the 80386, OS/2, UNIX(R)/XENIX(R), and Microsoft(R) Windows environments, all largely sharing the same code. This article explains how Ansa ported their large MS-DOS(R) application to the Windows environment for one of the new versions. Converting Windows Applications For Microsoft(R) OS/2 Presentation Manager Microsoft OS/2 Presentation Manager offers the best of the Microsoft Windows operating environment and the protected-mode OS/2 operating system. The sample program SayWhat highlights the similarities and differences between Presentation Manager and Windows programming techniques. Programming Considerations In Porting To Microsoft(R) XENIX(R) System V/386 XENIX System V/386, Microsoft's version of UNIX for the 386, supports virtual memory, demand paging, and both 286 and 386 mode execution. This article outlines considerations that help programmers choose between 16- and 32-bit program models when developing applications for XENIX V/386. HEXCALC: An Instructive Pop-Up Calculator For Microsoft(R) Windows HEXCALC, a pop-up window hexadecimal calculator, explores a method of using dialog box templates to define the layout of child window controls on a program's main window, a technique illustrating features of both Microsoft Windows Version 2.0 and the OS/2 Presentation Manager. Effectively Using Far and Huge Data Pointers In Your Microsoft(R) C Programs Programs that use "huge" pointers to access very large amounts of data are often affected by the expense of using relatively slow 32-bit arithmetic. Under certain conditions, huge pointers can be converted to far pointers, resulting in a significant decrease in access time. EMS Support Improves Microsoft(R) Windows 2.0 Application Performance Windows 2.0 uses expanded memory to bank-switch Windows applications, allowing multiple large programs to run simultaneously as if each were the only application running.The article examines how Windows' exploitation of LIM 4.0 expanded memory support can affect your Windows application. EDITOR'S NOTE There has been much speculation about the amount of effort required to move applications from the Windows 2.0 environment to the OS/2 Presentation Manager. With varying estimates and opinions ringing in our ears, we decided to devote a major part of this issue to exploring what's involved in programming for the Presentation Manager. We turned to Michael Geary, who spent a great deal of the last year exploring Presentation Manager. He provides us with a step-by-step guide to moving a Windows 2.0 application to Presentation Manager. To illustrate the differences, we have includeded side-by-side the complete code listings of both versions of his tutorial program. Charles Petzold, inveterate Windows expert, also provides insight on the topic with parallel Microsoft(R) Windows 2.0 and Presentation Manager versions of his instructive HEXCALC program. Ansa Software has had notable success with the MS-DOS(R) version of its Paradox(R) database. Currently under development are Paradox versions for Microsoft Windows, OS/2 Presentation Manager, UNIX(R) 5.3 and XENIX(R), as well as protected-mode 80386. We talked with Richard Schwartz, vice president of software development and cofounder of Ansa, to discover how one software firm is preparing for their move to Presentation Manager. With all the hoopla surrounding the OS/2 systems, we don't want to forget the importance of recent XENIX and MS-DOS developments. In particular, the new version of XENIX for the 386 environments has generated much interest, including questions on the portability between different versions of XENIX. Martin Dunsmuir, who is responsible for XENIX development at Microsoft, answers these questions and others. We look forward to 1988, and the new developments this coming year will bring. Look for MSJ to be there with the details.──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) 1988 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, CodeView and the Microsoft logo are registered trademarks of Microsoft Corporation. PageMaker is a registered trademark of Aldus Corporation. Paradox is a registered trademark of Ansa Software, a Borland Company. Apple and Macintosh are registered trademarks of Apple Computer, Inc. UNIX is a registered trademark of American Telephone and Telegraph Company. RAMpage! is a registered trademark of AST Research, Inc. Advantage! is a trademark of AST Research, Inc. IBM and PC/AT are registered trademarks of International Business Machines Corporation. PS/2 and Micro Channel are trademarks of International Business Machines Corporation. Intel is a registered trademark of Intel Corporation. Above is a trademark of Intel Corporation. Lotus and 1-2-3 are registered trademarks of Lotus Development Company. Microrim and R:BASE are registered trademarks of Microrim, Inc. ████████████████████████████████████████████████████████████████████████████ Preparing for Presentation Manager: Paradox Steps Up to Windows 2.0 ─────────────────────────────────────────────────────────────────────────── Also see the related article: Paradox Under Microsoft(R) OS/2 ─────────────────────────────────────────────────────────────────────────── Craig Stinson☼ Ansa Software, the Belmont, Calif., firm recently acquired by Borland International, sells only one product, but that product, the relational database manager Paradox(R), will appear in four new versions during the coming year. A fifth new version will probably be available in early 1989. The offspring are aimed at different operating environments: Microsoft(R) Windows Version 2.0, OS/2 systems Version 1.0, the OS/2 Presentation Manager, UNIX(R) 5.3 and XENIX(R), and the 80386 running MS- DOS(R) 3.x in protected mode with the help of a 386 MS-DOS compatibility tool. Paradox 386 was shown at last November's Comdex and will probably be available by the time you read this article; it won't require a compatibility tool, just a 386 machine. The OS/2 1.0 version should be released during the first quarter of 1988, the Windows version in the second quarter, the UNIX/XENIX version in the third quarter, and the Presentation Manager version in about a year, depending on the date of the OS/2 Presentation Manager's arrival. All Paradox versions, old and new, will have multiuser capability and will be data-compatible. With versions for all the major personal computing environments (except Macintosh(R)──Ansa is currently evaluating the feasibility of a Mac version), Ansa will be in a position to serve the vast majority of desktop users. And all users at various nodes of a network will be able to access and edit a common database, regardless of the hardware or operating systems they're running. Sound Design Pays Off What type of effort is involved in porting a large, complex MS-DOS application such as Paradox to all these different operating environments? In a recent interview with MSJ, Richard Schwartz, vice president for software development and a cofounder of Ansa, said that the move from one character-based system to another has been relatively straightforward, thanks largely to the foresight of the original designers of Paradox. "We designed Paradox from the beginning to be very clean and portable," says Schwartz. "We tried not to play dirty tricks getting at the operating system or creating hardware dependencies." This is not to say, of course, that the original Paradox doesn't write directly to the screen. "We do, of course, but we've isolated it. We have one module that will access the screen memory map directly. And even the BIOS dependencies are all in one place." The move from a character environment (MS-DOS 3.x) to a graphics environment (Microsoft Windows and the OS/2 Presentation Manager) is a considerably more ambitious undertaking. Schwartz talked about the whys and hows of this move and about how the graphics-based Paradox will look, feel, and perform relative to the original. Why Windows? The move to Windows now, according to Schwartz, will give Ansa advantages in marketing and in implementation when the OS/2 Presentation Manager arrives. "We think it's important to put out the Windows version, not because Windows 2.0 is going to be the primary market-certainly, Presentation Manager market will dwarf the Windows market-but because a lot of companies will be evaluating the Presentation Manager interface through Windows 2.0. And applications that run under Windows early on will have an advantage, given the long evaluation cycles that companies go through." Second, because Windows 2.0 is here now and Presentation Manager is not, the effort required to rethink and recode Paradox for Windows is the best preparation Ansa can undertake for the eventual port to the Presentation Manager-even though the API for Presentation Manager will differ significantly from that for Windows. The Good News How hard was the port to Windows? According to Schwartz, all but the underlying database engine was redesigned, borrowing pieces of the previous architecture and source code. Nevertheless, he asserts, about 50 percent of the original source code emigrated to the new world intact. That's an off-the-cuff, work-in-progress estimate, but even if it is too optimistic, it is still good news for Windows developers: not all programs will have to be rewritten from the ground up to run in the new graphics environments. Moreover, in Ansa's case the conversion will probably take about two person- years of work. That's not bad, considering that the original Paradox was a 14-person-year effort. And the company has done it without hiring a lot of specialized expertise. That's also good news, considering the current scarcity and high market value of Macintosh and Windows programmers. "We tried to get some people with product-level graphics experience. We used a headhunter for a while, but we were very unsuccessful," says Schwartz. "And it turned out that we did very well taking good software engineers who have general computer science sophistication and having them come up to speed under Windows. We found that general computer science background was the most important factor." A Natural Fit Making Ansa's transition from characters to pixels easier was the fact that Paradox, despite its early origins (development for the first version began in the summer of 1981), has always been a highly visual, interactive program. From an interface design standpoint, therefore, the transfer to Windows entailed an expansion of ideas already in place rather than a wholesale reconceptualization. "We've always had a very visual orientation, an object orientation," says Schwartz. "And it was just sort of a natural fit." Data tables in the character-based Paradox, for example, are presented to the user in the form of visual constructs that look much like windows, except that the borders are made of text-graphic characters and don't go all the way around (see Figure 1☼). Like a frame in Framework or a screenful of a spreadsheet, these objects act as portholes onto larger expanses of data, which Paradox calls images. Paradox presents the user with a workspace, much as Windows does, on which he can keep several images visible at once. And though it doesn't support a mouse, the character-based product lets the user manipulate the sizes and locations of images, and the width of columns within them, by positioning a highlight in the right hot spot and pressing cursor keys. This ability to interact immediately with a data construct, instead of having to issue a menu command and describe what one wants, is what Schwartz calls "direct operability," an important component of the original Paradox interface. Visual Interaction "The idea was──as much as possible, anywhere we could──to allow visual interaction, rather than descriptive interaction," says Schwartz. "Instead of asking the user to explain or describe something, we asked him to show it to us. That's very compatible with moving to the Windows environment." Paradox under Windows will look quite similar at first glance to the original version (see Figure 2☼), except that the Lotus-style menu gives way to drop-down menus, the text comes up black-on-white, and the images are fully formed document windows, with title bars, scroll bars, control menus, and sizing icons. The direct operability will still be there, but there'll be more of it. And, of course, there will be mouse support. One new area of direct operability in Windows Paradox is in the specification of sorts. In the character-based version, the user indicated which fields he wanted to be sort keys and in which direction he wanted a field sorted by typing a field number followed by an "a" for ascending or a "d" for descending (see Figure 3☼). The Windows version (see Figure 4☼) simplifies this process and makes it less analytical by letting the user double-click directly on the name of each sort key field. An arrow indicating sort direction (ascending, by default) will appear to the left of the chosen field name; to switch directions, the user merely toggles the arrow by double-clicking on it. The notion of clicking on a value to toggle it or to select from available values appears elsewhere in Windows Paradox. In creating or restructuring a table, for example, where the user is asked for a data type, he can click the mouse to get a menu showing available types. Similarly, in the create/restructure subsystem, a double-click on a field name will summon a dialog box, allowing the user to inspect current validity-check definitions or create new ones. Implementation Advantages Describing the conversion effort, Schwartz downplays the fearsome Windows learning curve, emphasizing instead the implementation advantages conferred by the graphics environment. In particular, he cites the greater degree of independence among the various objects with which the user interacts. In the Windows environment, every table in the workspace, as well as every form, query, and report specification, lives in its own window and has its own message handler. That message handler is responsible for knowing only about itself, how to repaint itself and how to update itself. In the character-based version, more attention had to be paid to the "global state," that is, to the ways in which changes in a table would affect everything else in the workspace. "At the implementation level," says Schwartz, "that means that your code can have an architecture such that everything is purely local to each image. So you can have a local handler that knows how to resize a window or how much of it to repaint. If anything in the size of that window is affected, Windows takes care of sending messages to the other windows to let them know that some other portion needs to be repainted." Debugging Difficulties According to Schwartz, one fairly serious hindrance to the development of Windows Paradox was the lack of debugging tools for use in the Windows environment. "It's a harder problem anyway," Schwartz says, "because there's more going behind the scenes in the Windows environment." The movability and discardability of Windows object code complicates matters, making bugs harder to find. Bugs tend to turn up later in the game as the result of stress on the system because a piece of code has been moved in memory. The current version of CodeView(R) doesn't work in the Windows environment, though Microsoft is working on a Windows-specific version. Symdeb is provided, but Ansa didn't use it extensively, preferring instead to rely on algorithmic-level debugging by embedding printf statements to test values of variables at various points in the program. "In our design environment, we have a monochrome display hooked up as well as an EGA. We've hooked things up so that we can put printfs in the code and have them scroll out on the monochrome display. You get the effect of traditional debugging preserved in the Windows environment." Making debugging even more difficult was the fact that Ansa was working with early prerelease versions of Windows and the Windows development tools (the project began last April). Programmers were sometimes hard pressed to tell which problems they should attribute to their own code, which to the compiler, and which to bugs in the underlying environment. User Advantages The independence of screen objects provides several benefits for users. Most importantly, it means that users can have more things going on at once and still keep track of everything. Instead of being limited to the number of images that will fit one after the other on screen, the user can load up the screen with overlapping windows, thereby achieving whatever level of information density he finds most appropriate. This facilitates such activities as moving from one table to another, copying data or structures from one table to another, discovering correlations between one set of data and another, and so on. It also means that Windows Paradox will appear somewhat less "modal" to users than its text-based ancestor. There are places in the current program where a user has to drop one context, if only briefly, to work in another. The Windows version will minimize the effects of such breaks in continuity. For example, in the character-based version, if the user wants to modify a data-entry form on the fly while entering data, he issues a command to summon the forms subsystem. The data and form he was working on disappear at that point, becoming visible again only after he's finished changing the form spec. In the Windows version, the form subsystem will appear in a child window, with the data remaining visible in the parent window. Other benefits the Windows Paradox user will find include the following: ■ The Windows version will automatically support any high-resolution large-screen displays supported by Windows itself. ■ The report subsystem will offer enhanced font support and will accept bit-mapped images by way of the Windows clipboard. Users can dress up Paradox reports with company logos, signatures, clip art, and the like. Windows Paradox will not accept bit-mapped images as data types, although Schwartz concedes that that's "a natural direction" for the product to evolve in. ■ Windows Paradox can exchange data with other Windows applications by way of dynamic data exchange (DDE). At the Microsoft Excel announcement, Ansa showed an early version of Paradox running as a client of Microsoft Excel, with Paradox started up by means of the macro language and transferring data to Microsoft Excel via DDE (see Figure 5☼). The finished Windows version of Paradox will allow a reversal of this scenario: you can start up and control other applications by means of scripts written in the Paradox application language (PAL). Performance Hits The picture sounds rosy, but Schwartz concedes that the advantages of the Windows environment will exact a few performance penalties. The hits, he suggests, will come less from Windows' need to represent text in graphics mode than from its propensity to swap program modules in and out of memory. Because the program was still under development at this writing, Ansa was not prepared to supply benchmark data. However, Schwartz maintains the company is "quite happy with the Paradox performance on 10-MHz 286s and above." And, he says, the SMARTDrive program, a Microsoft-supplied disk- caching utility tailored to enhance the performance of Windows, speeds things up significantly on all machines. Fears Allayed What messages would Ansa's Schwartz pass along to another Windows application developer? The most obvious one is that the redesign of an MS- DOS program to run under Windows is not the horrendous task it's commonly thought to be, particularly if the program was well designed in the first place. A second notion involves the maintenance of individuality under conditions that seem to impose similarity. "Some people say that all products are going to look alike, that all the creativity has been taken away from the designer in the Windows environment," says Schwartz. "So much has been standardized that everything's going to look the same and work the same. That's certainly true at some level. Still, there is a very diverse set of ways in which one can take advantage of the environment to present a particular application. "You need to concern yourself first with the core model of how an application is to be constructed-with the user-level concepts and how you want to present them. And only after that you look at what the Windows tools are and how you'll use them. The greatest effect on the user is the underlying model, and that is not affected at all by Windows. Windows is just a way to present that, to communicate it more effectively to the user." ─────────────────────────────────────────────────────────────────────────── Paradox Under Microsoft(R) OS/2 ─────────────────────────────────────────────────────────────────────────── Paradox was concieved in the days of very limited resources. Development began in the first half of 1981, months before the announcement of the IBM(R) PC, when 64Kb was as much memory as anyone could expect to have. Over the next several years, as larger machines grew commonplace, Paradox grew in scope and ambition, so that by the time the the product was announced in the Fall of 1885, it required 512Kb to run──practically speaking, that meant a 640Kb computer. However, like a depression era parent, Paradox never forgat what it was like to live in lean times. From the beggining, the product incorporated a virtual memory manager that swapped data as necessary to the hard disk. One of the programs selling points, moreover, was the heuristic approach to the processing of relational queries. An important aspect of this process was machine reasoning about what data was currently in memory and what was swapped out somewhere. Therefore, the large address space afforded by the 80286 and 80386 chips running in protected mode represents a liberation for Paradox, demonstrated by the preliminary statistics shown in the accompanying charts. Judging by the numbers, Paradox running under the OS/2 systems in protected mode should take care of complex queries and large scale sorts much more quickly than the MS-DOS 3.3 version. The 80386 version will do queries about as efficiently as the OS/2 versions and will be even more nimble at sorting. Perhaps the most surprosing information revealed by these early numbers is that Paradox 2.0 running in real mode under OS/2 (in the OS/2 compatibility box) outperforms the same product running on the same hardware under MS-DOS 3.3. According to Microsoft, most MS-DOS programs will run 5 percent slower in the compatibility box. Ansa's Richard Schwartz attributes the improvement to the fact that Paradox is doing a lot of random file access and is therefore able to profit significantly from OS/'s disk caching. Performance statistics for the Microsoft Windows version of Paradox were not yet available as of this writing. ╔═══╗ ║ ║100─┐ 93 ║ ║ │ ▄▄▄ ▒▒ Query ░░ Sort ║ ║ │ ░░░█ ║ ║ 80─┤ 73 ░░░█ 71 ║ S ║ │ ▄▄▄░░░█ ▄▄▄ ║ E ║ │ ▒▒▒█░░░█ 59 ░░░█ 60 ║ C ║ 60─┤ ▒▒▒█░░░█ ▄▄▄░░░█ 46 ▄▄▄ 48 ║ O ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▄▄▄░░░█ ▄▄▄ 44 ║ N ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█ ▄▄▄ ║ D ║ 40─┤ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ║ S ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ║ ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ║ ║ 20─┤ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ║ ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ║ ║ │ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ▒▒▒█░░░█ ╚═══╝ 0─┼─▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀───┬───▒▒▒▀░░░▀─┐ Paradox 2.0 Paradox 2.0 Paradox OS/2 Paradox 386 under DOS 3.3 in the OS/2 in protected under DOS 3.3 Compatability Box mode ════════════════════ ELAPSED TIME IN SECONDS ═══════════════════════ Preliminary performance statistics for three new versions of Paradox, as measured on a PS/2 Model 80. The query involved a join of a 5000-record table and a 10,000-record table; the sort was for the 500-record table. The figures for Paradox OS/2 and Paradox 386 may differ when those products are shipped. ─────────────────────────────────────────────────────────────────────────── ╔═P═╗ ║ E ║ 60─┐ 53% ║ R ║ │ ▒▒ Query ▄▄▄▄ ║ F ║ │ ░░░░█ ║ O ║ 50─┤ ░░ Sort ░░░░█ ║ R ║ │ ░░░░█ ║ M ║ │ 37% ░░░░█ ║ A ║ 40─┤ ▄▄▄▄ 35% 34% ░░░░█ ║ N ║ │ ▒▒▒▒█ ▄▄▄▄ ▄▄▄▄░░░░█ ║ C ║ │ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ E ║ 30─┤ 24% ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ ║ │ ▄▄▄▄ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ I ║ │ 19% ░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ M ║ 20─┤ ▄▄▄▄░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ P ║ │ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ R ║ │ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ O ║ 10─┤ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ V ║ │ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ E ║ │ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ▒▒▒▒█░░░░█ ║ M ║ 0─┴──▒▒▒▒▀░░░░▀────┬────▒▒▒▒▀░░░░▀────┬────▒▒▒▒▀░░░░▀──┐ ╚═T═╝ Paradox 2.0 in the Paradox OS/2 in Paradox 386 under OS/2 compatability protected mode DOS 3.3 box Performance improvements of three new versions of Paradox over Paradox 2.0 running in MS-DOS 3.3. These figures are preliminary and may not reflect actual performance when the products are shipped. ████████████████████████████████████████████████████████████████████████████ Converting Windows Applications For Microsoft(R) OS/2 Presentation Manager Micheal Geary☼ The Microsoft(R) OS/2 Presentation Manager represents the marriage of two technologies: the Windows operating environment and the protected-mode OS/2 operating systems. In any wedding, it's traditional to have something old, something new, something borrowed, and something blue, and Presentation Manager is no exception. Something old is Microsoft Windows. In a sense, Presentation Manager is really Windows. Although there have been a number of changes from the real- mode (MS-DOS(R)) version of Windows, nearly all Windows programming concepts carry over directly to the new environment: message-driven programming, window functions, resources, and so forth. If you know how to program Windows, Presentation Manager will be the icing on the wedding cake. Something new is the protected-mode hardware and OS/2 operating systems. It would not be far from the truth to say this is the environment Windows was really meant for. OS/2 offers true preemptive multitasking, unlike the "round-robin" multitasking of real-mode Windows. Applications no longer have to wait for each other to yield control. Within a single application, you can make good use of OS/2's multiple threads of execution, assigning to different windows their own threads. The protected-mode hardware greatly expands memory addressing capability and eliminates the need for some of the software memory management done by real-mode Windows. Something borrowed is the new Graphics Programming Interface (GPI). Adapted from the mainframe graphics GDDM (Graphics Data Display Manager) and GCP (Graphics Control Program) standards, GPI replaces Windows' Graphics Device Interface (GDI) with a richer, more versatile set of graphics functions. Something blue, as you've already guessed, is IBM. Presentation Manager plays a crucial role in IBM's Systems Application Architecture (SAA) as the user interface portion of SAA, at least for machines with graphics capabilities. IBM has an ambitious goal for SAA: a common user interface and programming interface on a wide range of machines, from PCs to mainframes. Eventually you should be able to take a Presentation Manager application and compile it to run on any machine. For machines without graphics, a subset of Presentation Manager functions will be provided, giving a similar user interface, but running in a character mode environment. Presentation Manager Programming To explore the differences and the similarities between a Presentation Manager application and a Windows application, let's look at a sample program called SayWhat, which is my entry in the "silliest Windows application" contest. I started out with the Hello program that comes with the Windows Software Development Kit and doctored it up a bit. Instead of just displaying the "hello" message in one place, the text wanders around the window randomly, changing its color as it moves. If you make SayWhat into an icon, it spells out the message one letter at a time, with the letter wandering around the icon. A control panel, a modeless dialog box, lets you change the displayed text and adjust how fast it moves and how often it selects a new random direction to move in. ─────────────────────────────────────────────────────────────────────────── The source code for SayWhat is found at the end of the text ─────────────────────────────────────────────────────────────────────────── SayWhat illustrates several programming techniques that are useful for Windows and Presentation Manager. It also points out an irritating flaw in the way many Windows applications do their screen painting and how to improve it. A Windows version and a Presentation Manager version of SayWhat are listed side by side for comparison. When comparing the listings, it is immediately apparent that the overall structure of the two programs is identical. The most important aspects of Windows apply equally to Presentation Manager: messages, window functions, painting, and so on. However, at the detailed coding level we will see many differences. Getting Started At the beginning, both programs have a main function, but in the Presentation Manager application it's called main( ) instead of WinMain( ) as in the Windows application. That's because a Presentation Manager application starts out as an ordinary OS/2 application; what makes it different is its use of the Presentation Manager API. The parameters passed to main are different from those that are passed to WinMain in the Windows version. The lpszCmdLine parameter is replaced by the more conventional, and convenient, argc and argv parameters. There is no nCmdShow parameter. If you create your main application window as an ordinary visible window by using the WinCreateStdWindow function, this is taken care of for you along with the window size. If you require more direct control over your window placement, create your main window as invisible and then set the size and position yourself. Finally, the hPrevInstance and hInstance parameters are missing: hPrevInstance isn't used at all and there is nothing in Presentation Manager corresponding to the GetInstanceData function. Under OS/2, there is no such thing as an instance in the same sense as there is in Windows. There are only OS/2 processes, and each process initializes its own data. Subsequent instances of an application have exactly the same initialization as the first instance. Message Loop The main message loop of a Presentation Manager program is nearly identical to the main loop of a Windows program. Other than some name changes and the use of the hAB parameter, the only difference is that the TranslateMessage function is not used. Since this function was always required anyway, it has been merged with WinGetMsg. Actually, the keyboard messages under Presentation Manager have been substantially simplified. The messages WM_KEYDOWN, along with WM_KEYUP, WM_SYSKEYDOWN, and WM_SYSKEYUP have all been removed, and the information that they provided is incorporated into WM_CHAR. The WM_CHAR message now provides the "raw" keyboard information that the WM_KEYDOWN/UP messages used to give, as well as the "translated" ASCII character code always provided by WM_CHAR. Initialization Before the message loop starts up, we have to initialize the application and create our main window, which is done in the SayInitApp function. The Presentation Manager version begins with two new calls: first, it calls WinInitialize which initializes the program for Presentation Manager and returns a handle to an anchor block, called hAB in our code. The anchor block isn't really used by Presentation Manager, but it's there for compatibility with future SAA environments. However, the hAB parameter is required on several of the Presentation Manager function calls. WinInitialize doesn't actually make the application become a Presentation Manager application. In fact, kernel applications can call WinInitialize if they want to use the local memory management or atom functions provided with Presentation Manager. Then it calls WinCreateMsgQueue, which allocates its message queue. Unlike Windows, in Presentation Manager you have to do this yourself. This gives you more flexibility; you can specify the size of the message queue or just take the default by specifying a size of 0, as SayWhat does. WinCreateMsgQueue is that special call that says "yes, this is a Presentation Manager application." It establishes this thread of execution as a message queue thread, and if you happen to start up a Presentation Manager application on a system that has the regular kernel shell running instead of the Presentation Manager shell, this is the call that puts you into graphics mode. If you have multiple threads, some of the threads may have message queues and some may not, but any thread that wants to create windows must call WinCreateMsgQueue since threads cannot share a queue. Both applications then load some string resources. This is nearly identical in the two versions, except for the hAB parameter used in the Presentation Manager version. Next we pick up display information: the number of colors available, the system font size, and the screen size. In the Windows version, you create an information context which is like a device context, but is used only for gathering information about the device. Then you pick up information on the system font with GetTextMetrics, and get the number of display planes with a GetDeviceCaps call. The Presentation Manager version is a little different. Instead of an information context or device context, it gets a presentation space with the WinGetPS call. Although Presentation Manager also has device contexts, most graphics functions use a presentation space instead, which, in a simple application like SayWhat, can be used much as a device context would be in the Windows version. However, the presentation space is much more powerful and versatile; for example, it can record and later play back graphics calls, much like a metafile. You could avoid having to process WM_PAINT messages this way. (This approach does not enhance performance, but it can be a programming convenience for some applications.) The details of presentation spaces are not discussed here, since SayWhat doesn't do anything fancy with them. Having obtained the presentation space handle, the Presentation Manager version then calls GpiQueryCharBox to get the system font character size and GpiQueryColorData to pick up the number of colors. Note that we're using the total number of distinct solid colors here, not the number of display planes as in the Windows version. Creating a Window Now you can register your window class and create the main window. It is here that you encounter a major difference between Windows and Presentation Manager-the architecture of a standard application window. Under Windows, a standard application window has two main components: the client area, which your code normally deals with, and the nonclient area, which includes all the controls around the edge of the window; title bar, sizing border, menu bar, and so forth. Your window function receives messages for the client and nonclient areas, but you usually pass the WM_NCxxxxx nonclient messages through to DefWindowProc. Although this works for most applications, this architecture has some problems. The behavior of all of the nonclient items is hard-coded into Windows. It is possible to override some of this behavior by intercepting the WM_NCxxxxx messages, but this is fairly cumbersome and very limited. There are also some strange inconsistencies, like the fact that standard scroll bars are part of the special nonclient area, but any non-standard scroll bars are created as child windows and are treated differently. When I was coding the window editor in one of my applications, this client/nonclient architecture was a serious obstacle. I needed complete control over all aspects of a window's behavior, because my application lets the user build and edit window characteristics on the fly. In design mode, I didn't want the nonclient items to behave normally, since they were objects being edited at that point. I finally got things to work reasonably well, but not quite the way I wanted. For example, if the user wished to remove the Maximize icon from a window, I would have to destroy and recreate the entire window. Under Presentation Manager, this client/nonclient architecture has been eliminated. The functionality is still there, but child windows and some new predefined window classes implement it. All of the items that were part of the nonclient area are now child windows, and you can do special things with them simply by subclassing them as you would any other window. There is a new window class, the frame window, which acts as a container for all these child windows and has the smarts to position them appropriately according to the window size. What used to be the client area under Windows is now itself a child window of the frame window. Now, for many applications, all of this won't make much difference. You can use the WinCreateStdWindow function to set up a standard frame window with the desired controls around the border, and things will work pretty much as before. The only significant difference is that some operations that were performed with Windows function calls are now done by sending messages to the various controls. However, in an application where the window frame controls are treated specially, this new architecture makes life much easier. Subclassing a child window is a lot more convenient than trying to fool all of the WM_NCxxxxx messages; it's a simpler, more consistent architecture. It is even reasonable to add more special controls to the window frame, placed anywhere you want. There's another, more subtle area where the window architecture has been cleaned up. Under Windows, every window logically has a parent window and an owner window. The parent determines how screen space is used-a child window is contained within its parent and clipped at the edges of the parent. The owner determines two things; control windows (edit controls, for example) send notification messages to their owner, and pop-up windows disappear when their owner is minimized. These are two different steps, although Windows combines them into a single "parent" window handle and interprets this handle according to the window style bits. For a WS_CHILD window, the "parent" is actually both the parent and the owner. For a WS_POPUP, the actual parent is NULL, and the "parent" is really the owner. Presentation Manager separates the parent and owner window handles, letting you specify them individually. This removes the need for the WS_CHILD, WS_POPUP, and WS_OVERLAPPED window styles; instead, a window is just a window. Furthermore, every window has a parent, because there is one new window, called the desktop window, which occupies the entire screen. Every top level window is actually a child of the desktop window. With all this in mind, let's look at the WinRegisterClass and WinCreateStdWindow calls in SayWhat. You'll see that there isn't much to the WinRegisterClass call. The WNDCLASS structure from the Windows version is gone, because most of what it specified has migrated to the frame window class and also to WinCreateStdWindow. One interesting new option in WinRegisterClass is the CS_SYNCPAINT style bit. If this bit is set, then WM_PAINT messages for windows of this class are not deferred in the customary fashion. Instead, WM_PAINT is sent immediately whenever the window becomes invalidated. This is how the window frame controls get repainted immediately; they have the CS_SYNCPAINT style. In the Windows version, this is handled specially by WM_PAINT, but under Presentation Manager, this feature is available to any window. Since SayWhat's window painting procedure is very simple and quite fast, CS_SYNCPAINT makes for a nice clean display. A window class that takes longer to paint should omit this style bit, and its windows will receive deferred WM_PAINT messages just as they do under Windows. The WinCreateStdWindow call creates SayWhat's frame window, along with its client window and all the frame controls. The frame window handle and the client window handle are returned by this call, the latter through the pointer in the last parameter. The window style bits look pretty familiar, but many of them have FS_ names instead of WS_ names because they are really just indicators to the frame window telling it which child windows to create. Also, there's no size or position information in this call. The trick is that if you give the window the WS_VISIBLE style, it will automatically be assigned the appropriate default size and position. Alternatively, you can create it as invisible and place it where you want (and then make it visible) explicitly with the call WinSetWindowPos. WinCreateStdWindow is just a convenience for creating standard frame windows. There is also a generic WinCreateWindow that can create any kind of window, giving you total control over how the window is set up. You would use this function to create additional child windows or any other kind of special window. Window Functions Now that our window has been created, it's time to look at the window function, SayWhatWndProc. You'll notice that the window functions in the two versions are very similar. As I mentioned before, all of the keyboard input is done under a WM_CHAR message, not under WM_KEYDOWN as in the Windows version, but otherwise it's essentially the same. Mouse input is nearly identical also, except that the message names have been changed to avoid the inconsistency of the right button sending a message called WM_LBUTTONUP/DOWN when the buttons have been swapped-the main button is instead WM_BUTTON1. The other messages processed by SayWhatWndProc are also extremely similar; WM_CREATE, WM_DESTROY, WM_SIZE, WM_PAINT, WM_TIMER, and WM_COMMAND. One very apparent difference is that there is no corresponding wParem = = SIZEICONIC, but there is a new WM_MINMAX message that provides the same information. WM_MINMAX is thus intercepted and processed to find out when the window is being minimized or restored to the normal state. There are some differences in the message parameters for all messages, though. Instead of a 16-bit parameter and a 32-bit parameter, wParam and lParam, there are two 32-bit parameters; lParam1 and lParam2. This allows a little more information to be passed along with messages, but the real reason for this change was for those messages that pass an additional window handle in wParam, such as WM_VSCROLLCLIPBOARD. Under Presentation Manager, window handles and most other handles are 32 bits, not 16 bits, so the wParam had to be expanded to permit this. The hWnd parameter to the window function is also a 32-bit parameter. Watch out for this one if you have any code that assumes handles are 16 bits. Window Painting The SayWhatPaint function handles window painting in both versions. It is rather similar in the two versions, although the Presentation Manager version has no PAINTSTRUCT. Instead, the WinBeginPaint function returns a presentation space handle and gives you the update rectangle as a separate parameter. In addition, it makes use of GpiSetColor instead of using SetTextColor, and uses GpiCharStringAt instead of TextOut. The same work is being performed in both cases. There's a little twist in SayWhat's painting logic in both versions. When you run SayWhat, you will notice that the text wanders around the window cleanly. There is no flicker, even though the text is repainted from scratch each time. Many Windows applications have a little bit of flicker when they paint into their window, which is caused by the usual practice of erasing the background completely and then painting the nonblank portions. That's fine when you paint a window for the first time, but if you want to repaint a portion of the window, there is a split second where the background is blanked out. You've probably seen this effect running Notepad or Write. If you type quickly in either program, the line you are on will flicker a little instead of painting cleanly. In SayWhat, you can see the same problem by selecting the Flicker option on the control dialog box. It's even worse here because the window gets repainted so frequently. How does SayWhat avoid this flicker in its normal mode? Simple: it doesn't erase the background underneath the text it's about to paint. It does erase any other portions of the background if needed, but where the text itself will go, it just writes the text without erasing first. This works because text is normally written opaquely; it completely covers whatever is underneath it. There are two different painting routines to illustrate this in SayWhatPaint. If the bCleanPaint variable is FALSE, it uses the common, flickery painting method, that is, it first erases the entire background and then draws the text. If bCleanPaint is TRUE, it draws the text and erases only the remaining portions of the background. It may seem silly to get worked up about this, but fixing it dramatically improves the appearance of an application. I know this from my own experience: the outline editor in my own application originally had a lot of irritating flicker, which went away when I converted it to use the clean painting method. And, even though it took a little extra code in the paint function, it actually reduced the overall amount of code and complexity. The flicker had been so annoying that I was ready to put in all kinds of special cases to make sure I never invalidated more of the window than was necessary, but with the clean painting method, it no longer matters. Painting is totally invisible to the user now, so if I invalidate a little too much, no one is the wiser. Dialog Boxes SayWhat has two dialog boxes: a modal About box and a modeless control panel, which are both created under the WM_COMMAND case in SayWhatWndProc. The code in both versions is similar except that MakeProcInstance is not necessary in the protected-mode environment. OS/2 and the memory management hardware set up the proper data segment for "call-back" functions such as dialog functions. This is a blessing for anyone who has struggled with mysterious system crashes caused by leaving out a MakeProcInstance under Windows. However, you still need to remember to EXPORT your call back functions and window functions, as was the case before. There is one important difference in the implementation of the dialog box functions themselves. Under Windows, a dialog box function is not coded like a normal window function. In fact, the actual window function for all dialog boxes is a private function inside Windows called DlgWndProc, which calls your dialog function before doing its own message switch statement and if it returns nonzero, it skips its own message processing. One problem with this approach is that it's a little confusing to have dialog functions work differently from normal window functions. Another problem is that it is impossible for a dialog function to return a value back to a SendMessage call. The return value you give isn't returned back through SendMessage; DlgWndProc simply returns zero. (Odd as it may seem, DlgWndProc treats the WM_CTLCOLOR message as a special case and does return the value you provide.) Under Presentation Manager, a dialog function operates like any other window function. Its return value is the real return value, and there is a default message function, WinDefDlgProc, that you call for messages that you don't process yourself. SayAboutDlgProc illustrates this difference and, like most About boxes, doesn't do much else except close itself when you hit the OK button. The other dialog box, SayWhat's control panel, is a little more interesting. This dialog box has no owner window (parent window under Windows). Usually, you specify an owner window when you create a dialog box, which causes three things to happen: the box is automatically positioned relative to the parent, it is always on top of the parent, and it disappears if the parent is made into an icon. However, in SayWhat, I didn't want the latter two actions; I wanted the box to remain visible if SayWhat's window itself is made into an icon so that you could fiddle with the parameters while the icon is displayed. I also wanted to be able to put SayWhat's main window on top of the dialog box. Creating the dialog box with a NULL owner takes care of this; however, I still wanted the box to be positioned relative to the main window instead of being relative to the screen. So, the WM_INITDIALOG code does this positioning explicitly, using ClientToScreen calls in the Windows version and a WinMapWindowPoints call in the Presentation Manager version. WinMapWindowPoints is a handy function, which replaces ClientToScreen and ScreenToClient, since you can specify a source window, a destination window, or both. It will also map as many points as you want in one call. In this program it maps two points, that is, one rectangle. After the mapping, you have to make sure that you haven't moved the dialog box partially offscreen, so you check it against the screen boundaries and adjust it back if necessary. Then, you call SetWindowPos or WinSetWindowPos to set the new position. This function works a little differently under Presentation Manager. If you have used SetWindowPos in Windows 2.0, you know it can do a number of things at once, such as move the window, resize it, or change the window ordering. The default is for it to do all of these, and in the last parameter you tell it what you do not want it to do. WinSetWindowPos does the same, except the last parameter tells it what you do want it to do-just the opposite. Note that the y parameter of WinSetWindowPos takes the window bottom, not the window top. This shows one subtle difference between Windows and Presentation Manager: vertical coordinates run from bottom to top, not top to bottom, that is, position (0,0) is the lower-left corner of a window or the screen, not the upper-left corner. To be consistent with this, a rectangle structure is now stored in the order (left, bottom, right, top) instead of (left, top, right, bottom). If you're familiar with the various GDI mapping modes available in Windows, you have probably noticed that they all run from bottom to top except for MM_TEXT, which runs from top to bottom. This change in Presentation Manager removes that inconsistency──all coordinates normally run the same direction now──but it obviously requires some conversion effort. You can instruct Presentation Manager to use the top-to-bottom coordinates within a window, so that could help in converting existing applications. After positioning the dialog box, the WM_INITDIALOG code initializes its control windows. One item of interest here is the scroll bar initialization in SayInitBar. The Windows version calls two scroll bar functions, SetScrollRange and SetScrollPos, to initialize the scroll bar. Presentation Manager deals with these two at the same time, with a single SBM_SETSCROLLBAR message that sets the position and range. Also, you will find throughout Presentation Manager that this is performed with a message instead of a special function; many of the special-purpose functions for control windows have been removed in favor of messages. If you prefer the approach of calling functions, you can always write your own equivalent functions that send the appropriate messages. The other interesting messages in SayWhatDlgProc are WM_COMMAND and WM_HSCROLL. The latter message gets sent for activity in either of the dialog box's scroll bars, and it calls SayDoBarMsg to set the new scroll bar position and to set the corresponding edit control value. However, in Presentation Manager this message passes you the child window ID of the scroll bar control instead of the window handle. This is convenient, since you need the window ID to tell which scroll bar you're dealing with. The WM_COMMAND message is sent when the user clicks any of the pushbuttons or radio buttons in the dialog box. (It also gets sent when you type in one of the edit controls, but SayWhat ignores it.) The WM_COMMAND processing is nearly identical in the two versions, except that the IsDlgButtonChecked function call is replaced with a BM_QUERYCHECK message in Presentation Manager. Conclusion As you can see, converting a Windows application to Presentation Manager isn't trivial, but it is pretty straightforward. The similarities between the two systems far outweigh the differences. If you're fluent in Windows programming, you've already done the hard part, and you know how different a Windows application is from a conventional MS-DOS or OS/2 program. If you haven't gotten started with Windows yet, well.... What's that I hear in the distance? Yes, it's wedding bells. Don't be late! ─────────────────────────────────────────────────────────────────────────── Source code in this article is referenced W for Windows programs and PM for Presentation Manager programs. ─────────────────────────────────────────────────────────────────────────── Figure 1W: SAYWHAT is the MAKE File for SAYWHAT # SAYWHAT # MAKE file for SAYWHAT (Windows version) sw.obj: sw.c sw.h cl -c -AS -DLINT_ARGS -Gcsw -Oas -W3 -Zdp sw.c sw.res: sw.rc sw.h rc -r sw.rc saywhat.exe: sw.obj sw.res sw.def link4 sw, saywhat/align:16, saywhat/map/line, slibw, sw.def mapsym saywhat rc sw.res saywhat.exe Figure 1PM: SAYWHAT is the MAKE File for SAYWHAT # SAYWHATP # MAKE file for SAYWHAT (Presentation Manager version) swp.obj: swp.c swp.h cl -c -AS -DLINT_ARGS -G2csw -Oat -W3 -Zp swp.c swp.res: swp.rc swp.h rc -r swp.rc saywhatp.exe: swp.obj swp.res swp.def link @swp.lnk mapsym saywhat rc swp.res saywhat.exe Figure 2W: Note that there is no Windows equivalent for SAYWHATP.LNK, the LINK file for the PM version of SAYWHAT. Figure 2PM: SWP.LNK is the Link File for SAYWHAT SWP.LNK swp saywhatp/align:16 saywhatp/map wincalls doscalls mlibc286/NOD swp.def Figure 3W: SW.DEF is the Module Definition File for SAYWHAT ; SW.DEF ; SW.DEF - Module definition file for SAYWHAT (Windows version) NAME SayWhat DESCRIPTION 'Say What!' STUB 'WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 128 STACKSIZE 4096 EXPORTS SayAboutDlgProc @1 SayWhatDlgProc @2 SayWhatWndProc @3 Figure 3PM: SWP.DEF is the Module Definition File for SAYWHAT ; SWP.DEF ; SWP.DEF - Module definition file for SAYWHAT (PM version) NAME SayWhat DESCRIPTION 'Say What!' STUB 'OS2STUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 128 STACKSIZE 8192 EXPORTS SayAboutDlgProc @1 SayWhatDlgProc @2 SayWhatWndProc @3 Figure 4W: SW.RC is the Resource File for SAYWHAT SW.RC #include <style.h> #include "sw.h" STRINGTABLE BEGIN STR_NAME, "SayWhat!" STR_TITLE, "Say What!" STR_WHAT, "Hello Windows!" END SayWhat! MENU BEGIN POPUP "&Say" BEGIN MENUITEM "&What..." , CMD_WHAT MENUITEM SEPARATOR MENUITEM "E&xit" , CMD_EXIT MENUITEM SEPARATOR MENUITEM "A&bout SayWhat!..." , CMD_ABOUT END END DLG_ABOUT DIALOG 19, 17, 130, 83 STYLE WS_DLGFRAME | WS_POPUP BEGIN CTEXT "Microsoft Windows", -1, 0, 8, 127, 8 CTEXT "Say What!", -1, 0, 18, 127, 8 CTEXT "Version 1.00", -1, 0, 30, 127, 8 CTEXT "By Michael Geary", -1, 0, 44, 129, 8 DEFPUSHBUTTON "Ok", IDOK, 48, 62, 32, 14 END DLG_WHAT DIALOG 49, 41, 177, 103 CAPTION "Say What!" STYLE WS_CAPTION | WS_SYSMENU | WS_VISIBLE | WS_POPUP BEGIN CONTROL "Say &What:" -1, "Static", SS_LEFT | WS_GROUP, CONTROL "" ITEM_WHAT, "Edit", ES_LEFT | ES_AUTOHSCROLL CONTROL "&Time Delay:" -1, "Static", SS_LEFT, CONTROL "" ITEM_INTBAR, "ScrollBar", SBS_HORZ | WS_TABSTOP, CONTROL "" ITEM_INTERVAL, "Edit", ES_LEFT | WS_BORDER | WS CONTROL "&Stability:" -1, "Static", SS_LEFT, CONTROL "" ITEM_DISTBAR, "ScrollBar", SBS_HORZ | WS_TABSTOP, CONTROL "" ITEM_DISTANCE, "Edit", ES_LEFT | WS_BORDER | WS_ CONTROL "Painting:" -1, "Static", SS_LEFT, CONTROL "&Clean" ITEM_CLEAN, "Button", BS_AUTORADIOBUTTON | WS_G CONTROL "&Flicker" ITEM_FLICKER, "Button", BS_AUTORADIOBUTTON, CONTROL "Enter" IDOK, "Button", BS_DEFPUSHBUTTON | WS_GRO CONTROL "Esc=Close" IDCANCEL, "Button", BS_PUSHBUTTON | WS_TABSTO END Figure 4PM: SWP.RC is the Resource File for SAYWHAT SWP.RC #include <style.h> #include "sw.h" STRINGTABLE { STR_NAME, "SayWhat!" STR_TITLE, "Say What!" STR_WHAT, "Hello Windows!" } MENU SayWhat! { SUBMENU "~Say", -1 { MENUITEM "~What...", CMD_WHAT MENUITEM SEPARATOR MENUITEM "E~xit", CMD_EXIT MENUITEM SEPARATOR MENUITEM "A~bout SayWhat!...", CMD_ABOUT } } DLGTEMPLATE DLG_ABOUT { DIALOG "", DLG_ABOUT, 19, 17, 130, 83, FS_DLGBORDER | WS_SAVEBITS | WS_VISIBLE { CTEXT "Microsoft Windows", -1, 0, 8, 127, 8 CTEXT "Say What!", -1, 0, 18, 127, 8 CTEXT "Version 1.00", -1, 0, 30, 127, 8 CTEXT "By Michael Geary", -1, 0, 44, 129, 8 DEFPUSHBUTTON "Ok", IDOK, 48, 62, 32, 14 } } DLGTEMPLATE DLG_WHAT { DIALOG "Say What!", DLG_WHAT, 49, 41, 177, 103, FS_TITLEBAR | FS_SYSMENU | FS_BORDER | WS_VISIBLE { CONTROL "Say ~What:" -1, 8, 10, 40, 8, WC_STATIC, SS_LE CONTROL "" ITEM_WHAT, 56, 8, 112, 12, WC_EDIT, ES_LE CONTROL "~Time Delay:" -1, 8, 28, 53, 9, WC_STATIC, SS_LE CONTROL "" ITEM_INTBAR, 56, 28, 88, 8, WC_SCROLLBAR, SBS_H CONTROL "" ITEM_INTERVAL, 152, 26, 16, 12, WC_EDIT, ES_LE CONTROL "~Stability:" -1, 8, 46, 56, 9, WC_STATIC, SS_LE CONTROL "" ITEM_DISTBAR, 56, 46, 88, 8, WC_SCROLLBAR, SBS_H CONTROL "" ITEM_DISTANCE, 152, 44, 16, 12, WC_EDIT, ES_LE CONTROL "Painting:" -1, 8, 64, 40, 8, WC_STATIC, SS_LE CONTROL "~Clean" ITEM_CLEAN, 56, 62, 36, 12, WC_BUTTON, BS_AU CONTROL "~Flicker" ITEM_FLICKER, 96, 62, 42, 12, WC_BUTTON, BS_AU CONTROL "Enter" IDOK, 24, 82, 48, 14, WC_BUTTON, BS_DE CONTROL "Esc=Close" IDCANCEL, 104, 82, 48, 14, WC_BUTTON, BS_PU } } Figure 5W: SW.H is the C Header file for SAYWHAT /* SW.H */ /* SW.H - C header file for SAYWHAT (Windows version) */ /* String table constants */ #define STR_NAME 101 #define STR_TITLE 102 #define STR_WHAT 103 /* Menu command IDs */ #define CMD_ABOUT 201 #define CMD_EXIT 202 #define CMD_WHAT 203 /* Dialog box resource IDs */ #define DLG_ABOUT 301 #define DLG_WHAT 302 /* 'What...' dialog box item IDs */ #define ITEM_WHAT 401 #define ITEM_INTBAR 402 #define ITEM_INTERVAL 403 #define ITEM_DISTBAR 404 #define ITEM_DISTANCE 405 #define ITEM_CLEAN 406 #define ITEM_FLICKER 407 /* Timer IDs */ #define TIMER_MOVE 501 #define TIMER_CHAR 502 Figure 5PM: SWP.H is the C Header File for SAYWHAT /* SWP.H */ /* SWP.H - C header file for SAYWHAT (PM version) */ /* String table constants */ #define STR_NAME 101 #define STR_TITLE 102 #define STR_WHAT 103 /* Menu command IDs */ #define CMD_ABOUT 201 #define CMD_EXIT 202 #define CMD_WHAT 203 /* Dialog box resource IDs */ #define DLG_ABOUT 301 #define DLG_WHAT 302 /* 'What...' dialog box item IDs */ #define ITEM_WHAT 401 #define ITEM_INTBAR 402 #define ITEM_INTERVAL 403 #define ITEM_DISTBAR 404 #define ITEM_DISTANCE 405 #define ITEM_CLEAN 406 #define ITEM_FLICKER 407 /* Timer IDs */ #define TIMER_MOVE 501 #define TIMER_CHAR 502 /* Menu resource IDs */ #define MENU_WHAT 601 Figure 6W: SW.C is the C Source Code Listing for SAYWHAT /* SW.C * SW.C - C code for SayWhat - Windows version */ /* Note - this code *must* be compiled with the -Gc switch */ #ifndef LINT_ARGS #define LINT_ARGS /* turn on argument checking for C runtime */ #endif #include <windows.h> #include <limits.h> #include <stdlib.h> #include <string.h> #include <time.h> #include "sw.h" #define MIN_INTERVAL 1 /* limits for nInterval */ #define MAX_INTERVAL 999 #define MIN_DISTANCE 1 /* limits for nDistance */ #define MAX_DISTANCE 99 /* Static variables */ HANDLE hInstance; /* application instance handle */ HWND hWndWhat; /* main window handle */ HWND hWndPanel; /* contral panel dialog handle */ FARPROC lpfnDlgProc; /* ProcInstance for dialog */ char szAppName[10]; /* window class name */ char szTitle[15]; /* main window title */ char szText[40]; /* current "what" text */ NPSTR pText = szText; /* ptr into szText for icon mode */ char cBlank = ' '; /* a blank we can point to */ RECT rcText; /* current text rectangle */ POINT ptAdvance; /* increments for SayAdvanceText */ POINT ptCharSize; /* X and Y size of a character */ POINT ptScreenSize; /* X and Y size of the screen */ int nDisplayPlanes; /* number of display planes */ DWORD rgbTextColor; /* current text color */ HBRUSH hbrBkgd; /* brush for erasing background */ int nInterval = 40; /* current "Interval" setting */ int nDistance = 30; /* current "Distance" setting */ int nDistLeft = 0; /* change direction when hits 0 */ BOOL bCleanPaint = TRUE; /* clean or flickery painting? */ BOOL bMouseDown = FALSE; /* is mouse down? */ BOOL bIconic = FALSE; /* is main window iconic? */ /* Full prototypes for our functions to get type checking */ BOOL FAR SayAboutDlgProc( HWND, unsigned, WORD, LONG ); void SayAdvanceTextChar( HWND ); void SayAdvanceTextPos( HWND ); void SayChangeColor( HWND ); void SayDoBarMsg( HWND, HWND, WORD, int ); void SayFillRect( HDC, int, int, int, int ); void SayInitBar( HWND, int, int, int, int ); BOOL SayInitApp( HANDLE, int ); void SayInvalidateText( HWND ); void SayLimitTextPos( HWND ); void SayMoveText( HWND, POINT ); void SaySetBar( HWND, int *, int ); void SayExitApp( int ); BOOL FAR SayWhatDlgProc( HWND, unsigned, WORD, LONG ); void SayWhatPaint( HWND ); LONG FAR SayWhatWndProc( HWND, unsigned, WORD, LONG ); void WinMain( HANDLE, HANDLE, LPSTR, int ); /* Dialog function for the "About" box */ BOOL FAR SayAboutDlgProc( hWndDlg, wMsg, wParam, lParam ) HWND hWndDlg; unsigned wMsg; WORD wParam; LONG lParam; { switch( wMsg ) { case WM_COMMAND: switch( wParam ) { case IDOK: EndDialog( hWndDlg, TRUE ); return TRUE; } } return FALSE; } /* Advances to next display character in iconic mode. * Forces in a blank when it reaches the end of string. */ void SayAdvanceTextChar( hWnd ) HWND hWnd; { if( ! bIconic ) return; if( pText = = &cBlank ) pText = szText; else if( ! *(++pText) ) pText = &cBlank; SayChangeColor( hWnd ); SayInvalidateText( hWnd ); } /* Advances text position according to ptAdvance. Decrements * nDistLeft first, and when it reaches zero, sets a new * randomized ptAdvance and nDistLeft, also changes color. * Does nothing if mouse is down, so text will track mouse. */ void SayAdvanceTextPos( hWnd ) HWND hWnd; { int i; if( bMouseDown ) return; SayInvalidateText( hWnd ); if( nDistLeft- < 0 ) { nDistLeft = rand() % nDistance + 1; do { i = rand(); ptAdvance.x = ( i < SHRT_MAX/3 ? -1 : i < SHRT_MAX/3*2 ? 0 : 1 ); i = rand(); ptAdvance.y = ( i < SHRT_MAX/3 ? -1 : i < SHRT_MAX/3*2 ? 0 : 1 ); } while( ptAdvance.x = = 0 && ptAdvance.y = = 0 ); if( ! bIconic ) SayChangeColor( hWnd ); } else { rcText.left += ptAdvance.x; rcText.right += ptAdvance.x; rcText.top += ptAdvance.y; rcText.bottom += ptAdvance.y; } SayInvalidateText( hWnd ); } /* Changes color to a random selection, if color is available. * Forces a color change - if the random selection is the same * as the old one, it tries again. */ void SayChangeColor( hWnd ) HWND hWnd; { HDC hDC; DWORD rgbNew; DWORD rgbWindow; if( nDisplayPlanes <= 1 ) { rgbTextColor = GetSysColor(COLOR_WINDOWTEXT); } else { rgbWindow = GetSysColor(COLOR_WINDOW); hDC = GetDC( hWnd ); do { rgbNew = GetNearestColor( hDC, MAKELONG( rand(), rand() ) & 0x00FFFFFFL ); } while( rgbNew = = rgbWindow || rgbNew = = rgbTextColor ); rgbTextColor = rgbNew; ReleaseDC( hWnd, hDC ); } } /* Handles scroll bar messages from the control dialog box. * Adjusts scroll bar position, taking its limits into account, * copies the scroll bar value into the adjacent edit control, * then sets the nDistance or nInterval variable appropriately. */ void SayDoBarMsg( hWndDlg, hWndBar, wCode, nThumb ) HWND hWndDlg; HWND hWndBar; WORD wCode; int nThumb; { int nPos; int nOldPos; int nMin; int nMax; int idBar; idBar = GetWindowWord( hWndBar, GWW_ID ); nOldPos = nPos = GetScrollPos( hWndBar, SB_CTL ); GetScrollRange( hWndBar, SB_CTL, &nMin, &nMax ); switch( wCode ) { case SB_LINEUP: -nPos; break; case SB_LINEDOWN: ++nPos; break; case SB_PAGEUP: nPos -= 10; break; case SB_PAGEDOWN: nPos += 10; break; case SB_THUMBPOSITION: case SB_THUMBTRACK: nPos = nThumb; break; case SB_TOP: nPos = nMin; break; case SB_BOTTOM: nPos = nMax; break; } if( nPos < nMin ) nPos = nMin; if( nPos > nMax ) nPos = nMax; if( nPos = = nOldPos ) return; SetScrollPos( hWndBar, SB_CTL, nPos, TRUE ); SetDlgItemInt( hWndDlg, idBar+1, nPos, FALSE ); switch( idBar ) { case ITEM_DISTBAR: nDistance = nPos; break; case ITEM_INTBAR: KillTimer( hWndWhat, TIMER_MOVE ); nInterval = nPos; SetTimer( hWndWhat, TIMER_MOVE, nInterval, NULL ); InvalidateRect( hWndWhat, NULL, FALSE ); break; } } /* Terminates the application, freeing up allocated resources. * Note that this function does NOT return to the caller, but * exits the program. */ void SayExitApp( nRet ) int nRet; { if( GetModuleUsage(hInstance) = = 1 ) { DeleteObject( hbrBkgd ); } exit( nRet ); } /* Fills a specified rectangle with the background color. * Checks that the rectangle is non-empty first. */ void SayFillRect( hDC, nLeft, nTop, nRight, nBottom ) HDC hDC; int nLeft; int nTop; int nRight; int nBottom; { RECT rcFill; if( nLeft >= nRight || nTop >= nBottom ) return; SetRect( &rcFill, nLeft, nTop, nRight, nBottom ); FillRect( hDC, &rcFill, hbrBkgd ); } /* Initializes the application. */ BOOL SayInitApp( hPrevInstance, nCmdShow ) HANDLE hPrevInstance; int nCmdShow; { WNDCLASS Class; HDC hDC; TEXTMETRIC Metrics; LoadString( hInstance, STR_NAME, szAppName, sizeof(szAppName) ); LoadString( hInstance, STR_TITLE, szTitle, sizeof(szTitle) ); LoadString( hInstance, STR_WHAT, szText, sizeof(szText) ); hDC = CreateIC( "DISPLAY", NULL, NULL, NULL ); GetTextMetrics( hDC, &Metrics ); nDisplayPlanes = GetDeviceCaps( hDC, PLANES ); DeleteDC( hDC ); ptCharSize.x = Metrics.tmMaxCharWidth; ptCharSize.y = Metrics.tmHeight; ptScreenSize.x = GetSystemMetrics(SM_CXSCREEN); ptScreenSize.y = GetSystemMetrics(SM_CYSCREEN); if( ! hPrevInstance ) { hbrBkgd = CreateSolidBrush( GetSysColor(COLOR_WINDOW) ); Class.style = 0; /* CS_HREDRAW | CS_VREDRAW; */ Class.lpfnWndProc = SayWhatWndProc; Class.cbClsExtra = 0; Class.cbWndExtra = 0; Class.hInstance = hInstance; Class.hIcon = NULL; Class.hCursor = LoadCursor( NULL, IDC_ARROW ); Class.hbrBackground = COLOR_WINDOW + 1; Class.lpszMenuName = szAppName; Class.lpszClassName = szAppName; if( ! RegisterClass( &Class ) ) return FALSE; } else { GetInstanceData( hPrevInstance, (NPSTR)&hbrBkgd, sizeof(hbrBkgd) ); } hWndWhat = CreateWindow( szAppName, szTitle, WS_OVERLAPPEDWINDOW, CW_USEDEFAULT, 0, CW_USEDEFAULT, 0, (HWND)NULL, (HMENU)NULL, hInstance, (LPSTR)NULL ); if( ! hWndWhat ) return FALSE; ShowWindow( hWndWhat, nCmdShow ); UpdateWindow( hWndWhat ); return TRUE; } /* Initializes one scroll bar in the control dialog. */ void SayInitBar( hWndDlg, idBar, nValue, nMin, nMax ) HWND hWndDlg; int idBar; int nValue; int nMin; int nMax; { HWND hWndBar; hWndBar = GetDlgItem( hWndDlg, idBar ); SetScrollRange( hWndBar, SB_CTL, nMin, nMax, FALSE ); SetScrollPos( hWndBar, SB_CTL, nValue, FALSE ); SetDlgItemInt( hWndDlg, idBar+1, nValue, FALSE ); } /* Invalidates the text within the main window, adjusting the * text rectangle if it's gone out of bounds. */ void SayInvalidateText( hWnd ) HWND hWnd; { SayLimitTextPos( hWnd ); InvalidateRect( hWnd, &rcText, FALSE ); } /* Checks the text position against the window client area * rectangle. If it's moved off the window in any direction, * forces it back inside, and also reverses the ptAdvance value * for that direction so it will "bounce" off the edge. Handles * both the iconic and open window cases. */ void SayLimitTextPos( hWnd ) HWND hWnd; { RECT rcClient; POINT ptTextSize; ptTextSize = ptCharSize; if( ! bIconic ) { pText = szText; ptTextSize.x *= strlen(szText); } GetClientRect( hWndWhat, &rcClient ); if( rcText.left > rcClient.right - ptTextSize.x ) { rcText.left = rcClient.right - ptTextSize.x; ptAdvance.x = -ptAdvance.x; } if( rcText.left < rcClient.left ) { rcText.left = rcClient.left; ptAdvance.x = -ptAdvance.x; } if( rcText.top > rcClient.bottom - ptTextSize.y ) { rcText.top = rcClient.bottom - ptTextSize.y; ptAdvance.y = -ptAdvance.y; } if( rcText.top < rcClient.top ) { rcText.top = rcClient.top; ptAdvance.y = -ptAdvance.y; } rcText.right = rcText.left + ptTextSize.x; rcText.bottom = rcText.top + ptTextSize.y; } /* Moves the text within the window, by invalidating the old * position, adjusting rcText according to ptMove, and then * invalidating the new position. */ void SayMoveText( hWnd, ptMove ) HWND hWnd; POINT ptMove; { SayInvalidateText( hWnd ); rcText.left = ptMove.x - (rcText.right - rcText.left >> 1); rcText.top = ptMove.y - (rcText.bottom - rcText.top >> 1); SayInvalidateText( hWnd ); } /* Sets one of the dialog scroll bars to *pnValue. If that * value is out of range, limits it to the proper range and * forces *pnValue to be within the range as well. */ void SaySetBar( hWndDlg, pnValue, idBar ) HWND hWndDlg; int * pnValue; int idBar; { HWND hWndBar; int nMin; int nMax; int nValue; BOOL bOK; hWndBar = GetDlgItem( hWndDlg, idBar ); GetScrollRange( hWndBar, SB_CTL, &nMin, &nMax ); nValue = GetDlgItemInt( hWndDlg, idBar+1, &bOK, FALSE ); if( bOK && nValue >= nMin && nValue <= nMax ) { *pnValue = nValue; SetScrollPos( hWndBar, SB_CTL, nValue, TRUE ); } else { SetDlgItemInt( hWndDlg, idBar+1, GetScrollPos( hWndBar, SB_CTL ), FALSE ); } } /* Dialog function for the control panel dialog box. */ BOOL FAR SayWhatDlgProc( hWndDlg, wMsg, wParam, lParam ) HWND hWndDlg; unsigned wMsg; WORD wParam; LONG lParam; { HWND hWndBar; RECT rcWin; int n; switch( wMsg ) { case WM_COMMAND: switch( wParam ) { case IDOK: KillTimer( hWndWhat, TIMER_MOVE ); GetDlgItemText( hWndDlg, ITEM_WHAT, szText, sizeof(szText) ); if( strlen(szText) = = 0 ) LoadString( hInstance, STR_WHAT, szText, sizeof(szText) ); pText = szText; SaySetBar( hWndDlg, &nInterval, ITEM_INTBAR ); SaySetBar( hWndDlg, &nDistance, ITEM_DISTBAR ); SetTimer( hWndWhat, TIMER_MOVE, nInterval, NULL ); InvalidateRect( hWndWhat, NULL, FALSE ); return TRUE; case IDCANCEL: DestroyWindow( hWndDlg ); return TRUE; case ITEM_CLEAN: case ITEM_FLICKER: bCleanPaint = IsDlgButtonChecked( hWndDlg, ITEM_CLEAN ); return TRUE; } return FALSE; case WM_HSCROLL: if( HIWORD(lParam) ) SayDoBarMsg( hWndDlg, HIWORD(lParam), wParam, LOWORD(lParam) ); return TRUE; case WM_INITDIALOG: GetWindowRect( hWndDlg, &rcWin ); ClientToScreen( hWndWhat, (LPPOINT)&rcWin.left ); ClientToScreen( hWndWhat, (LPPOINT)&rcWin.right ); n = rcWin.right - ptScreenSize.x + ptCharSize.x; if( n > 0 ) rcWin.left -= n; rcWin.left &= ~7; /* byte align */ n = rcWin.bottom - ptScreenSize.y + ptCharSize.y; if( n > 0 ) rcWin.top -= n; SetWindowPos( hWndDlg, (HWND)NULL, rcWin.left, rcWin.top, 0, 0, SWP_NOSIZE | SWP_NOZORDER | SWP_NOREDRAW | SWP_NOACTIVATE ); SetDlgItemText( hWndDlg, ITEM_WHAT, szText ); SendDlgItemMessage( hWndDlg, ITEM_WHAT, EM_LIMITTEXT, sizeof(szText)-1, 0L ); SayInitBar( hWndDlg, ITEM_INTBAR, nInterval, MIN_INTERVAL, MAX_INTERVAL ); SayInitBar( hWndDlg, ITEM_DISTBAR, nDistance, MIN_DISTANCE, MAX_DISTANCE ); CheckDlgButton( hWndDlg, ITEM_CLEAN, TRUE ); return TRUE; case WM_NCDESTROY: FreeProcInstance( lpfnDlgProc ); hWndPanel = NULL; return FALSE; } return FALSE; } /* Painting procedure for the main window. Handles both the * clean and flickery painting methods for demonstration * purposes. */ void SayWhatPaint( hWnd ) HWND hWnd; { PAINTSTRUCT ps; BeginPaint( hWnd, &ps ); SetTextColor( ps.hdc, rgbTextColor ); SayLimitTextPos( hWnd ); if( bCleanPaint ) { /* Clean painting, avoid redundant erasing */ TextOut( ps.hdc, rcText.left, rcText.top, pText, bIconic ? 1 : strlen(szText) ); SayFillRect( ps.hdc, ps.rcPaint.left, ps.rcPaint.top, rcText.left, ps.rcPaint.bottom ); SayFillRect( ps.hdc, rcText.left, ps.rcPaint.top, rcText.right, rcText.top ); SayFillRect( ps.hdc, rcText.left, rcText.bottom, rcText.right, ps.rcPaint.bottom ); SayFillRect( ps.hdc, rcText.right, ps.rcPaint.top, ps.rcPaint.right, ps.rcPaint.bottom ); } else { /* Flickery painting, erase background and paint traditionally */ FillRect( ps.hdc, &ps.rcPaint, hbrBkgd ); TextOut( ps.hdc, rcText.left, rcText.top, pText, bIconic ? 1 : strlen(szText) ); } EndPaint( hWnd, &ps ); if( ! nInterval ) SayAdvanceTextPos( hWnd ); } /* Window function for the main window. */ LONG FAR SayWhatWndProc( hWnd, wMsg, wParam, lParam ) HWND hWnd; unsigned wMsg; WORD wParam; LONG lParam; { FARPROC lpfnAbout; switch( wMsg ) { case WM_COMMAND: switch( wParam ) { case CMD_ABOUT: lpfnAbout = MakeProcInstance( SayAboutDlgProc, hInstance ); DialogBox( hInstance, MAKEINTRESOURCE(DLG_ABOUT), hWnd, lpfnAbout ); FreeProcInstance( lpfnAbout ); return 0L; case CMD_EXIT: DestroyWindow( hWndWhat ); return 0L; case CMD_WHAT: if( hWndPanel ) { BringWindowToTop( hWndPanel ); } else { lpfnDlgProc = MakeProcInstance( SayWhatDlgProc, hInstance ); if( ! lpfnDlgProc ) return 0L; hWndPanel = CreateDialog( hInstance, MAKEINTRESOURCE(DLG_WHAT), (HWND)NULL, lpfnDlgProc ); if( ! hWndPanel ) FreeProcInstance( lpfnDlgProc ); } } break; case WM_CREATE: srand( (int)time(NULL) ); SetTimer( hWnd, TIMER_MOVE, nInterval, NULL ); return 0L; case WM_DESTROY: if( hWndPanel ) DestroyWindow( hWndPanel ); PostQuitMessage( 0 ); return 0L; case WM_KEYDOWN: SayInvalidateText( hWnd ); switch( wParam ) { case VK_LEFT: rcText.left -= ptCharSize.x; ptAdvance.x = -1; ptAdvance.y = 0; break; case VK_RIGHT: rcText.left += ptCharSize.x; ptAdvance.x = 1; ptAdvance.y = 0; break; case VK_UP: rcText.top -= ptCharSize.y >> 1; ptAdvance.x = 0; ptAdvance.y = -1; break; case VK_DOWN: rcText.top += ptCharSize.y >> 1; ptAdvance.x = 0; ptAdvance.y = 1; break; default: return 0L; } SayInvalidateText( hWnd ); nDistLeft = nDistance; return 0L; case WM_LBUTTONDOWN: if( bMouseDown ) break; KillTimer( hWnd, TIMER_MOVE ); bMouseDown = TRUE; SetCapture( hWnd ); SayMoveText( hWnd, MAKEPOINT(lParam) ); break; case WM_LBUTTONUP: if( ! bMouseDown ) break; bMouseDown = FALSE; ReleaseCapture(); SayMoveText( hWnd, MAKEPOINT(lParam) ); SetTimer( hWnd, TIMER_MOVE, nInterval, NULL ); break; case WM_MOUSEMOVE: if( bMouseDown ) SayMoveText( hWnd, MAKEPOINT(lParam) ); break; case WM_PAINT: SayWhatPaint( hWnd ); return 0L; case WM_SIZE: if( wParam = = SIZEICONIC ) { if( ! bIconic ) SetTimer( hWnd, TIMER_CHAR, 1000, NULL ); bIconic = TRUE; } else { if( bIconic ) KillTimer( hWnd, TIMER_CHAR ); bIconic = FALSE; } SayInvalidateText( hWnd ); nDistLeft = 0; SayAdvanceTextPos( hWnd ); return 0L; case WM_TIMER: switch( wParam ) { case TIMER_MOVE: SayAdvanceTextPos( hWnd ); break; case TIMER_CHAR: SayAdvanceTextChar( hWnd ); break; } return 0L; } return DefWindowProc( hWnd, wMsg, wParam, lParam ); } /* Main function for the application. */ void WinMain( hInst, hPrevInst, lpszCmdLine, nCmdShow ) HANDLE hInst; HANDLE hPrevInst; LPSTR lpszCmdLine; int nCmdShow; { MSG msg; hInstance = hInst; if( ! SayInitApp( hPrevInst, nCmdShow ) ) SayExitApp( 1 ); while( GetMessage( &msg, NULL, 0, 0 ) ) { if( hWndPanel && IsDialogMessage( hWndPanel, &msg ) ) continue; TranslateMessage( &msg ); DispatchMessage( &msg ); } SayExitApp( msg.wParam ); } Figure 6PM: SWP.C is the C Source Code Listing for SAYWHAT /* * SWP.C - C code for SayWhat - Presentation Manager version */ #ifndef LINT_ARGS #define LINT_ARGS /* turn on argument checking for C runtime */ #endif #include <os2pm.h> #include <limits.h> #include <stdlib.h> #include <string.h> #include <time.h> #include ╙swp.h╙ #define MIN_INTERVAL 1 /* limits for nInterval */ #define MAX_INTERVAL 999 #define MIN_DISTANCE 1 /* limits for nDistance */ #define MAX_DISTANCE 99 #define COLORDATAMAX 5 /* Static variables */ HAB hAB = NULL; /* anchor block handle */ HMQ hMsgQ = NULL; /* message queue handle */ HWND hWndWhatFrame; /* frame window handle */ HWND hWndWhat; /* client window handle */ HWND hWndPanel; /* control panel dialog handle */ CHAR szAppName[10]; /* window class name */ CHAR szTitle[15]; /* main window title */ CHAR szText[40]; /* current 'what' text */ PSZ npszText = szText; /* ptr into szText for icon mode */ CHAR cBlank = ╒ ╒; /* a blank we can point to */ RECT rcText; /* current text rectangle */ POINT ptAdvance; /* increments for SayAdvanceText */ POINT ptCharSize; /* X and Y size of a character */ POINT ptScreenSize; /* X and Y size of the screen */ LONG lColorMax; /* number of available colors */ LONG lColor; /* current text color index */ SHORT nInterval = 40; /* current 'Interval' setting */ SHORT nDistance = 30; /* current 'Distance' setting */ SHORT nDistLeft = 0; /* change direction when hits 0 */ BOOL bCleanPaint = TRUE; /* clean or flickery painting? */ BOOL bMouseDown = FALSE; /* is mouse down? */ BOOL bIconic = FALSE; /* is main window iconic? */ /* Full prototypes for our functions to get type checking */ ULONG FAR PASCAL SayAboutDlgProc( HWND, USHORT, ULONG, ULONG ); VOID SayAdvanceTextChar( HWND ); VOID SayAdvanceTextPos( HWND ); VOID SayChangeColor( HWND ); VOID SayDoBarMsg( HWND, USHORT, USHORT, SHORT ); VOID SayExitApp( INT ); VOID SayFillRect( HPS, SHORT, SHORT, SHORT, SHORT ); VOID SayInitBar( HWND, SHORT, SHORT, SHORT, SHORT ); BOOL SayInitApp( VOID ); VOID SayInvalidateText( HWND ); VOID SayLimitTextPos( HWND ); VOID SayMoveText( HWND, POINT ); VOID SaySetBar( HWND, SHORT *, SHORT ); ULONG FAR PASCAL SayWhatDlgProc( HWND, USHORT, ULONG, ULONG ); VOID SayWhatPaint( HWND ); ULONG FAR PASCAL SayWhatWndProc( HWND, USHORT, ULONG, ULONG ); void cdecl main( INT, PSZ ); /* Dialog function for the 'About' box */ ULONG FAR PASCAL SayAboutDlgProc( hWndDlg, wMsg, lParam1, lParam2 ) HWND hWndDlg; USHORT wMsg; ULONG lParam1; ULONG lParam2; { switch( wMsg ) { case WM_COMMAND: switch( LOUSHORT(lParam1) ) { case IDOK: WinDismissDlg( hWndDlg, TRUE ); break; } } return WinDefDlgProc( hWndDlg, wMsg, lParam1, lParam2 ); } /* Advances to next display character in iconic mode. * Forces in a blank when it reaches the end of string. */ VOID SayAdvanceTextChar( hWnd ) HWND hWnd; { if( ! bIconic ) return; if( npszText = = &cBlank ) npszText = szText; else if( ! *(++npszText) ) npszText = &cBlank; SayChangeColor( hWnd ); SayInvalidateText( hWnd ); } /* Advances text position according to ptAdvance. Decrements * nDistLeft first, and when it reaches zero, sets a new * randomized ptAdvance and nDistLeft, also changes color. * Does nothing if mouse is down, so text will track mouse. */ VOID SayAdvanceTextPos( hWnd ) HWND hWnd; { SHORT i; if( bMouseDown ) return; SayInvalidateText( hWnd ); if( nDistLeft- < 0 ) { nDistLeft = rand() % nDistance + 1; do { i = rand(); ptAdvance.x = ( i < SHRT_MAX/3 ? -1 : i < SHRT_MAX/3*2 ? 0 : 1 ); i = rand(); ptAdvance.y = ( i < SHRT_MAX/3 ? -1 : i < SHRT_MAX/3*2 ? 0 : 1 ); } while( ptAdvance.x = = 0 && ptAdvance.y = = 0 ); if( ! bIconic ) SayChangeColor( hWnd ); } else { rcText.xLeft += ptAdvance.x; rcText.xRight += ptAdvance.x; rcText.yTop += ptAdvance.y; rcText.yBottom += ptAdvance.y; } SayInvalidateText( hWnd ); } /* Changes color to a random selection, if color is available. * Forces a color change - if the random selection is the same * as the old one, it tries again. */ VOID SayChangeColor( hWnd ) HWND hWnd; { HPS hPS; LONG lWindow; LONG lNew; hPS = WinGetPS( hWnd ); if( lColorMax <= 2 ) { lColor = GpiQueryColorIndex( hPS, WinQuerySysColor( hAB, SCLR_WINDOWTEXT ), 0L ); } else { lWindow = GpiQueryColorIndex( hPS, WinQuerySysColor( hAB, SCLR_WINDOW ), 0L ); do { lNew = rand() % lColorMax; } while( lNew = = lWindow || lNew = = lColor ); lColor = lNew; } WinReleasePS( hPS ); } /* Handles scroll bar messages from the control dialog box. * Adjusts scroll bar position, taking its limits into account, * copies the scroll bar value into the adjacent edit control, * then sets the nDistance or nInterval variable appropriately. */ VOID SayDoBarMsg( hWndDlg, idBar, wCode, nThumb ) HWND hWndDlg; USHORT idBar; USHORT wCode; SHORT nThumb; { SHORT nPos; SHORT nOldPos; ULONG lRange; nOldPos = nPos = (SHORT)WinSendDlgItemMsg( hWndDlg, idBar, SBM_QUERYPOS, 0L, 0L ); lRange = WinSendDlgItemMsg( hWndDlg, idBar, SBM_QUERYRANGE, 0L, 0L ); switch( wCode ) { case SB_LINEUP: -nPos; break; case SB_LINEDOWN: ++nPos; break; case SB_PAGEUP: nPos -= 10; break; case SB_PAGEDOWN: nPos += 10; break; case SB_THUMBPOSITION: case SB_THUMBTRACK: nPos = nThumb; break; case SB_TOP: nPos = LOUSHORT(lRange); break; case SB_BOTTOM: nPos = HIUSHORT(lRange); break; } if( nPos < LOUSHORT(lRange) ) nPos = LOUSHORT(lRange); if( nPos > HIUSHORT(lRange) ) nPos = HIUSHORT(lRange); if( nPos = = nOldPos ) return; WinSendDlgItemMsg( hWndDlg, idBar, SBM_SETPOS, (LONG)nPos, 0L ); WinSetDlgItemShort( hWndDlg, idBar+1, nPos, FALSE ); switch( idBar ) { case ITEM_DISTBAR: nDistance = nPos; break; case ITEM_INTBAR: WinStopTimer( hAB, hWndWhat, TIMER_MOVE ); nInterval = nPos; WinStartTimer( hAB, hWndWhat, TIMER_MOVE, nInterval ); WinInvalidateRect( hWndWhat, NULL ); break; } } /* Terminates the application, freeing up allocated resources. * Note that this function does NOT return to the caller, but * exits the program. */ VOID SayExitApp( nRet ) INT nRet; { if( hWndWhatFrame ) WinDestroyWindow( hWndWhatFrame ); if( hMsgQ ) WinDestroyMsgQueue( hMsgQ ); if( hAB ) WinTerminate( hAB ); exit( nRet ); } /* Fills a specified rectangle with the background color. * Checks that the rectangle is non-empty first. */ VOID SayFillRect( hPS, xLeft, xBottom, xRight, xTop ) HPS hPS; SHORT xLeft; SHORT xBottom; SHORT xRight; SHORT xTop; { RECT rcFill; if( xLeft >= xRight || xBottom >= xTop ) return; WinSetRect( hAB, &rcFill, xLeft, xBottom, xRight, xTop ); WinFillRect( hPS, &rcFill, WinQuerySysColor( hAB, SCLR_WINDOW ) ); } /* Initializes the application. */ BOOL SayInitApp() { HPS hPS; BOOL bOK; hAB = WinInitialize(); if( ! hAB ) return FALSE; hMsgQ = WinCreateMsgQueue( hAB, 0 ); if( ! hMsgQ ) return FALSE; WinLoadString( hAB, NULL, STR_NAME, szAppName, sizeof(szAppName) ); WinLoadString( hAB, NULL, STR_TITLE, szTitle, sizeof(szTitle) ); WinLoadString( hAB, NULL, STR_WHAT, szText, sizeof(szText) ); bOK = WinRegisterClass( hAB, szAppName, (LPFNWP)SayWhatWndProc, CS_SYNCPAINT, 0, NULL ); if( ! bOK ) return FALSE; hWndWhatFrame = WinCreateStdWindow( (HWND)NULL, FS_TITLEBAR | FS_SYSMENU | FS_MENU | FS_MINMAX | FS_SIZEBORDER, szAppName, szTitle, 0L, (HMODULE)NULL, MENU_WHAT, &hWndWhat ); if( ! hWndWhatFrame ) return FALSE; WinShowWindow( hWndWhat, TRUE ); return TRUE; } /* Initializes one scroll bar in the control dialog. */ VOID SayInitBar( hWndDlg, idBar, nValue, nMin, nMax ) HWND hWndDlg; SHORT idBar; SHORT nValue; SHORT nMin; SHORT nMax; { HWND hWndBar; hWndBar = WinWindowFromID( hWndDlg, idBar ); WinSendDlgItemMsg( hWndDlg, idBar, SBM_SETSCROLLBAR, (LONG)nValue, MAKELONG( nMin, nMax ) ); WinSetDlgItemShort( hWndDlg, idBar+1, nValue, FALSE ); } /* Invalidates the text within the main window, adjusting the * text rectangle if it's gone out of bounds. */ VOID SayInvalidateText( hWnd ) HWND hWnd; { SayLimitTextPos( hWnd ); WinInvalidateRect( hWnd, &rcText ); } /* Checks the text position against the window client area * rectangle. If it's moved off the window in any direction, * forces it back inside, and also reverses the ptAdvance value * for that direction so it will 'bounce' off the edge. Handles * both the iconic and open window cases. */ VOID SayLimitTextPos( hWnd ) HWND hWnd; { RECT rcClient; POINT ptTextSize; ptTextSize = ptCharSize; if( ! bIconic ) { npszText = szText; ptTextSize.x *= strlen(szText); } WinQueryWindowRect( hWndWhat, &rcClient ); if( rcText.xLeft > rcClient.xRight - ptTextSize.x ) { rcText.xLeft = rcClient.xRight - ptTextSize.x; ptAdvance.x = -ptAdvance.x; } if( rcText.xLeft < rcClient.xLeft ) { rcText.xLeft = rcClient.xLeft; ptAdvance.x = -ptAdvance.x; } if( rcText.yBottom < rcClient.yBottom ) { rcText.yBottom = rcClient.yBottom; ptAdvance.y = -ptAdvance.y; } if( rcText.yBottom > rcClient.yTop - ptTextSize.y ) { rcText.yBottom = rcClient.yTop - ptTextSize.y; ptAdvance.y = -ptAdvance.y; } rcText.xRight = rcText.xLeft + ptTextSize.x; rcText.yTop = rcText.yBottom + ptTextSize.y; } /* Moves the text within the window, by invalidating the old * position, adjusting rcText according to ptMove, and then * invalidating the new position. */ VOID SayMoveText( hWnd, ptMove ) HWND hWnd; POINT ptMove; { SayInvalidateText( hWnd ); rcText.xLeft = ptMove.x - (rcText.xRight - rcText.xLeft >> 1); rcText.yBottom = ptMove.y - (rcText.yTop - rcText.yBottom >> 1); SayInvalidateText( hWnd ); } /* Sets one of the dialog scroll bars to *pnValue. If that value * is out of range, limits it to the proper range and forces * *pnValue to be within the range as well. */ VOID SaySetBar( hWndDlg, pnValue, idBar ) HWND hWndDlg; SHORT * pnValue; SHORT idBar; { ULONG lRange; SHORT nValue; BOOL bOK; lRange = WinSendDlgItemMsg( hWndDlg, idBar, SBM_QUERYRANGE, 0L, 0L ); nValue = WinQueryDlgItemShort( hWndDlg, idBar+1, &bOK, FALSE ); if( bOK && nValue >= LOUSHORT(lRange) && nValue <= HIUSHORT(lRange) ) { *pnValue = nValue; WinSendDlgItemMsg( hWndDlg, idBar, SBM_SETPOS, (LONG)nValue, (LONG)TRUE ); } else { WinSetDlgItemShort( hWndDlg, idBar + 1, (INT)WinSendDlgItemMsg( hWndDlg, idBar, SBM_QUERYPOS, 0L, 0L ), FALSE ); } } /* Dialog function for the control panel dialog box. */ ULONG FAR PASCAL SayWhatDlgProc( hWndDlg, wMsg, lParam1, lParam2 ) HWND hWndDlg; USHORT wMsg; ULONG lParam1; ULONG lParam2; { HWND hWndBar; RECT rcWin; SHORT n; switch( wMsg ) { case WM_COMMAND: switch( LOUSHORT(lParam1) ) { case IDOK: WinStopTimer( hAB, hWndWhat, TIMER_MOVE ); WinQueryWindowText( WinWindowFromID( hWndDlg, ITEM_WHAT ), sizeof(szText), szText ); if( strlen(szText) = = 0 ) WinLoadString( hAB, NULL, STR_WHAT, szText, sizeof(szText) ); npszText = szText; SaySetBar( hWndDlg, &nInterval, ITEM_INTBAR ); SaySetBar( hWndDlg, &nDistance, ITEM_DISTBAR ); WinStartTimer( hAB, hWndWhat, TIMER_MOVE, nInterval ); WinInvalidateRect( hWndWhat, NULL ); break; case IDCANCEL: WinDestroyWindow( hWndDlg ); break; case ITEM_CLEAN: case ITEM_FLICKER: bCleanPaint = (BOOL)WinSendDlgItemMsg( hWndDlg, ITEM_CLEAN, BM_QUERYCHECK, 0L, 0L ); break; } break; case WM_DESTROY: hWndPanel = NULL; break; case WM_HSCROLL: SayDoBarMsg( hWndDlg, LOUSHORT(lParam1), HIUSHORT(lParam2), LOUSHORT(lParam2) ); break; case WM_INITDLG: WinQueryWindowRect( hWndDlg, &rcWin ); WinMapWindowPoints( hWndWhat, (HWND)NULL, (LPPOINT)&rcWin.xLeft, 2 ); n = rcWin.xRight - ptScreenSize.x + ptCharSize.x; if( n > 0 ) rcWin.xLeft -= n; rcWin.xLeft &= ~7; /* byte align */ n = rcWin.yTop - ptScreenSize.y + ptCharSize.y; if( n > 0 ) rcWin.yBottom -= n; WinSetWindowPos( hWndDlg, (HWND)NULL, rcWin.xLeft, rcWin.yBottom, 0, 0, SWP_MOVE ); WinSetWindowText( WinWindowFromID( hWndDlg, ITEM_WHAT ), szText ); WinSendDlgItemMsg( hWndDlg, ITEM_WHAT, EM_SETTEXTLIMIT, (LONG)sizeof(szText)-1, 0L ); SayInitBar( hWndDlg, ITEM_INTBAR, nInterval, MIN_INTERVAL, MAX_INTERVAL ); SayInitBar( hWndDlg, ITEM_DISTBAR, nDistance, MIN_DISTANCE, MAX_DISTANCE ); WinSendDlgItemMsg( hWndDlg, ITEM_CLEAN, BM_SETCHECK, (LONG)TRUE, 0L ); break; } return WinDefDlgProc( hWndDlg, wMsg, lParam1, lParam2 ); } /* Painting procedure for the main window. Handles both the * clean and flickery painting methods for demonstration * purposes. */ VOID SayWhatPaint( hWnd ) HWND hWnd; { HPS hPS; RECT rcPaint; GPOINT gptText; hPS = WinBeginPaint( hWnd, (HPS)NULL, &rcPaint ); GpiSetColor( hPS, lColor ); SayLimitTextPos( hWnd ); gptText.x = (LONG)rcText.xLeft; gptText.y = (LONG)rcText.yBottom; if( bCleanPaint ) { /* Clean painting, avoid redundant erasing */ GpiSetBackMix( hPS, MIX_OVERPAINT ); GpiCharStringAt( hPS, &gptText, (LONG)( bIconic ? 1 : strlen(szText) ), npszText ); SayFillRect( hPS, rcPaint.xLeft, rcPaint.yBottom, rcText.xLeft, rcPaint.yTop ); SayFillRect( hPS, rcText.xLeft, rcText.yTop, rcText.xRight, rcPaint.yTop ); SayFillRect( hPS, rcText.xLeft, rcPaint.yBottom, rcText.xRight, rcText.yBottom ); SayFillRect( hPS, rcText.xRight, rcPaint.yBottom, rcPaint.yRight, rcPaint.yTop ); } else { /* Flickery painting, erase background and paint traditionally */ WinFillRect( hPS, &rcPaint, WinQuerySysColor( hAB, SCLR_WINDOW ) ); GpiCharStringAt( hPS, &gptText, (LONG)( bIconic ? 1 : strlen(szText) ), npszText ); } WinEndPaint( hPS ); if( ! nInterval ) SayInvalidateText( hWnd ); } /* Window function for the main window. */ ULONG FAR PASCAL SayWhatWndProc( hWnd, wMsg, lParam1, lParam2 ) HWND hWnd; USHORT wMsg; ULONG lParam1; ULONG lParam2; { POINT ptMouse; GSIZE gsChar; HPS hPS; LONG ColorData[COLORDATAMAX]; BOOL bNowIconic; switch( wMsg ) { case WM_BUTTON1DOWN: if( bMouseDown ) break; WinStopTimer( hAB, hWnd, TIMER_MOVE ); bMouseDown = TRUE; WinSetCapture( hAB, hWnd ); ptMouse.x = LOUSHORT(lParam1); ptMouse.y = HIUSHORT(lParam1); SayMoveText( hWnd, ptMouse ); return 0L; case WM_BUTTON1UP: if( ! bMouseDown ) break; bMouseDown = FALSE; WinSetCapture( hAB, (HWND)NULL ); ptMouse.x = LOUSHORT(lParam1); ptMouse.y = HIUSHORT(lParam1); SayMoveText( hWnd, ptMouse ); WinStartTimer( hAB, hWnd, TIMER_MOVE, nInterval ); return 0L; case WM_CHAR: if( ( LOUSHORT(lParam1) & KC_KEYUP ) || ! ( LOUSHORT(lParam1) & KC_VIRTUALKEY ) ) { break; } SayInvalidateText( hWnd ); switch( HIUSHORT(lParam2) ) { case VK_LEFT: rcText.xLeft -= ptCharSize.x; ptAdvance.x = -1; ptAdvance.y = 0; break; case VK_RIGHT: rcText.xLeft += ptCharSize.x; ptAdvance.x = 1; ptAdvance.y = 0; break; case VK_UP: rcText.yBottom -= ptCharSize.y >> 1; ptAdvance.x = 0; ptAdvance.y = -1; break; case VK_DOWN: rcText.yBottom += ptCharSize.y >> 1; ptAdvance.x = 0; ptAdvance.y = 1; break; default: return 0L; } SayInvalidateText( hWnd ); nDistLeft = nDistance; return 0L; case WM_COMMAND: switch( LOUSHORT(lParam1) ) { case CMD_ABOUT: WinDlgBox( (HWND)NULL, hWnd, (LPFNWP)SayAboutDlgProc, NULL, DLG_ABOUT, NULL ); return 0L; case CMD_EXIT: WinDestroyWindow( hWndWhatFrame ); return 0L; case CMD_WHAT: if( hWndPanel ) { WinSetWindowPos( hWndPanel, HWND_TOP, 0, 0, 0, 0, SWP_ZORDER | SWP_ACTIVATE ); } else { hWndPanel = WinLoadDlg( (HWND)NULL, (HWND)NULL, (LPFNWP)SayWhatDlgProc, NULL, DLG_WHAT, NULL ); } } return 0L; case WM_CREATE: /* find out character/screen sizes, number of colors */ hPS = WinGetPS( hWnd ); GpiQueryCharBox( hPS, &gsChar ); GpiQueryColorData( hPS, (LONG)COLORDATAMAX, ColorData ); WinReleasePS( hPS ); lColorMax = ColorData[3]; ptCharSize.x = gsChar.width; ptCharSize.y = gsChar.height; ptScreenSize.x = WinQuerySysValue( (HWND)NULL, SV_CXSCREEN ); ptScreenSize.y = WinQuerySysValue( (HWND)NULL, SV_CYSCREEN ); /* initialize timer */ srand( (INT)time(NULL) ); WinStartTimer( hAB, hWnd, TIMER_MOVE, nInterval ); return 0L; case WM_DESTROY: if( hWndPanel ) WinDestroyWindow( hWndPanel ); WinPostQueueMsg( hMsgQ, WM_QUIT, 0L, 0L ); return 0L; case WM_ERASEBACKGROUND: return 1L; /* don't erase */ case WM_MINMAX: bNowIconic = ( LOUSHORT(lParam1) = = SWP_MINIMIZE ); if( bIconic != bNowIconic ) { bIconic = bNowIconic; if( bIconic ) WinStopTimer( hAB, hWnd, TIMER_CHAR ); else WinStartTimer( hAB, hWnd, TIMER_CHAR, 1000 ); } return 1L; case WM_MOUSEMOVE: if( bMouseDown ) { ptMouse.x = LOUSHORT(lParam1); ptMouse.y = HIUSHORT(lParam1); SayMoveText( hWnd, ptMouse ); } return 0L; case WM_PAINT: SayWhatPaint( hWnd ); return 0L; case WM_SIZE: SayInvalidateText( hWnd ); nDistLeft = 0; SayAdvanceTextPos( hWnd ); return 0L; case WM_TIMER: switch( LOUSHORT(lParam1) ) { case TIMER_MOVE: SayAdvanceTextPos( hWnd ); break; case TIMER_CHAR: SayAdvanceTextChar( hWnd ); break; } return 0L; } return WinDefWindowProc( hWnd, wMsg, lParam1, lParam2 ); } /* Main function for the application. */ void cdecl main( nArgs, pArgs ) INT nArgs; PSZ pArgs; { QMSG qMsg; if( ! SayInitApp() ) SayExitApp( 1 ); while( WinGetMsg( hAB, &qMsg, (HWND)NULL, 0, 0 ) ) { if( hWndPanel && WinProcessDlgMsg( hWndPanel, &qMsg ) ) continue; WinDispatchMsg( hAB, &qMsg ); } SayExitApp( 0 ); } ████████████████████████████████████████████████████████████████████████████ Programming Considerations in Porting to Microsoft XENIX System V/386 ─────────────────────────────────────────────────────────────────────────── Also see the related article: Demand Paging and Virtual Memory ─────────────────────────────────────────────────────────────────────────── Martin Dunsmuir XENIX(R) System V/386 is the Microsoft version of the UNIX(R) Operating System ported to the Intel(R) 386 microprocessor. The product, which was released to OEMs in the summer of 1987, is the first from Microsoft to take full advantage of the features of the Intel 386 microprocessor. In particular, XENIX allows 32-bit applications to be written for the first time. Microsoft has been active in the UNIX business since its inception, and in fact, XENIX predates even MS-DOS(R) as a Microsoft product. Since 1983, the XENIX development effort has concentrated on the Intel microprocessors──the 286 introduced by Intel in 1983 and more recently the 386. By concentrating on one instruction set and chip architecture, Microsoft has been able to develop the world's largest installed base of binary-compatible UNIX systems. Currently, XENIX accounts for about 70 percent of all UNIX licenses sold, or roughly 250,000 units, and more than 1000 different applications are available. Although these numbers may sound small when compared with the numbers quoted for MS-DOS, the majority of these systems are used in multiuser configurations supporting between 2 and 16 users. Most of the current XENIX installed base is on 286-based PCs such as the IBM(R) PC AT(R). XENIX is used primarily as a cost-effective solution for small businesses that require multiuser access; it has sold particularly well in vertical markets that lend themselves to a customized "systems solution." Two such markets are dentistry and accounting. There was great excitement when Intel informed Microsoft that it was planning the 386 chip-and Microsoft set out to find a way to take advantage of the chip's features within the XENIX environment. In particular, Microsoft wanted to give developers the ability to create 32-bit applications and provide support within the operating system for virtual memory and demand paging──features that can greatly increase the throughput of a computer system. Another major design goal was to be sure that existing XENIX 286 users were not prevented from moving up to the 386. Microsoft wanted its installed base to be able to take advantage of the increased performance without having to buy new versions of their applications. Since the 386 chip supports both 16- and 32-bit segments in its architecture, Microsoft has been able to create an environment in which both 16- and 32-bit programs can be executed simultaneously. The features of demand paging and virtual memory are available transparently to both 16- and 32-bit applications. Implementing support for segments independently of the paging subsystem provides full performance for old applications without slowing down the execution of the new 32-bit programs (see Figure 1). Support for 32-bit applications is the key to the continued success of XENIX. It opens the door to the creation of much more powerful programs, as well as making it easier for developers to move existing UNIX applications onto XENIX. UNIX 32-bit programs being ported to XENIX 286 often needed extensive rewriting to make them work well in a 16-bit environment. This article outlines the considerations that will help programmers in choosing between 16- and 32-bit program models when developing applications or migrating to XENIX System V/386. Small-Model Programs In 16-bit mode, as used on XENIX 286, a program is composed of two segments up to 64Kb in size. One segment contains program code; the other segment contains data (the stack is of fixed size and resides in the data segment). An example of a small-model program is shown in Figure 2. The program in Figure 2 has only one data and one code segment, so it is not necessary to change the contents of the segment registers while the program is running. At load time, the operating system initializes them to point to the memory in which the program executes (via the LDT). In particular, the compiler or assembler programmer does not have to take account of changing segment registers during the program execution. Also of note is the fact that in the small model, both integers and pointers to data objects are 16-bit quantities and therefore interchangeable. This is important because many programs implicitly make this assumption. Unfortunately, although many commands and utilities are less than 64Kb in size, most third-party applications are larger than this; they require multiple code and/or data segment support. Figure 3 shows a multisegment program. Large- and Middle-Model Programs can overflow the 64Kb limit with their code or their data, or both at the same time. Programs that exceed 64Kb of code but still have less than 64Kb of data are called middle-model programs. Large-model programs exceed 64Kb in both their code and data segments. Large Code The program is broken down by the linker into pieces that will fit into 64Kb. If a program's code exceeds 64Kb, then it is necessary to place different parts of it in different segments. (Because the breaks occur on function boundaries, each piece can be less than 64Kb in size.) The compiler must also gener-ate code that automatically reloads the CS register when the thread of execution moves from one segment to another. This results in a program that is slightly slower than the equivalent small-model image. The effect is not too drastic because within each subroutine CS remains constant so the frequency of segment register reloads is relatively low in comparison with the number of instructions executed. Large Data Data structures are spread among a number of different segments when data exceeds 64Kb. Again, the linker fills each segment with data structures as fully as possible. The performance penalty to be paid for going to large data is much larger than in the case of code because the frequency of intersegment data accesses is generally much greater than that of intersegment branches. The C compiler does not know which segment a particular data structure resides in at compile time; this causes a large number of unnecessary intersegment calls to be generated. Another problem in moving to large model is the fact that the size of data pointers increases to 32 bits. This means that the size of an integer is no longer equal to the size of a pointer, and programs that rely on this equality, either implicitly or explicitly, break. This is one of the primary problems developers experience when porting existing 32-bit UNIX programs to XENIX 286. A summary of the different models can be seen in Figure 4. Hybrid Model Using the 286, where 16-bit segments are the norm but most useful applications exceed 64Kb in size, it is important for programmers to understand how to design their programs to reduce the effect of the multiple segment accesses. One way of doing this is to select specific data structures to be placed in separate far segments, while keeping indiscriminately accessed data structures (and the stack if possible) in a single near segment. The code generated by the Microsoft(R) C compiler in this case is much more efficient because the far keyword, used to mark specific data structures, gives the compiler a hint as to when it should reload segment registers. Use of a hybrid model with carefully designed programs comes close to the performance of small-model programs, even though they exceed 64Kb in size. However, there is a down side to this approach──it makes programs inherently less portable between XENIX 286 and other UNIX environments. Also, converting an existing 32-bit UNIX application to a hybrid model is complicated by the differences in pointer and integer size that make large- model ports such a problem. 32-Bit Programming In contrast to the complexity of a multisegment 286 program, the native 386 program structure is very simple (see Figure 5). Each program consists of one code segment along with one data segment, and each segment can be very large in size. (The exact limit depends on the availability of real memory and swap space and is typically a few megabytes). Because the address space is large, it is not necessary to support multiple segments in 32-bit mode, either in the operating system or in the C compiler. When a program is loaded, all the segment registers are initialized to static values and remain unchanged while the program is executing. In 32-bit mode, the stack lives in the data segment and grows down to lower addresses, while the data segment extends upward to higher addresses. XENIX 386 programs are truly 32-bit; they support 32-bit integers and all pointers are 32 bits in length. This eases the problems of porting existing 32-bit applications to XENIX 386 in 32-bit mode. Other advantages offered by the 32-bit mode of the 386 are more orthogonal registers and addressing modes, which allow better code generation and more register variables, plus extra instructions that improve in-line code generation. Thirty-two-bit programs generally exhibit a significant performance advantage over the 16-bit versions. New XENIX Applications The introduction of XENIX 386 no longer constrains the developer to the 16- bit architecture. If he chooses, he can develop his application in 32-bit mode. However, the choice between a 16- and a 32-bit architecture for a new application is not as simple as it appears at first glance. 32-bit programs will only execute on XENIX 386, whereas 16-bit applications will execute on both XENIX 286 and XENIX 386. The installed base of XENIX 386 is still small, but it is almost certain to exceed that of XENIX 286 in time. A 16-bit application may be a better choice for developers who want to address the largest possible installed base. Let's look at the trade-offs that must be considered when making the choice between 16 and 32 bits. The developer should ask himself the following questions: ■ What is the size of the application, both code and data? ■ Is the application an existing UNIX program being ported to XENIX? ■ Is the application an existing MS-DOS program or aimed at the MS-DOS, OS/2, and UNIX markets? ■ For new applications, what is the target market for the application? Is it limited to XENIX or does it have wider appeal in other UNIX or DOS markets? ■ What are the application's performance requirements? Application Size In many ways size is the most important consideration; unfortunately for new applications it is most likely the hardest to answer. For a simple application it is probably wise to build the application first as a 32- bit program and then see if it will fit into 16 bits. At this point it should be remembered that large data is a much more serious performance limitation to 16-bit programs-programs with more than 16 bits of code but less than 16 bits of data can be built as 16-bit middle-model programs without serious performance degradation. Another approach that can be used to fit a more complex program into the 64Kb address space is to break it down into a number of separate, communicating processes, each of which fits into the smaller address space. Not all programs are amenable to such an architecture. Breaking an application into pieces can also limit portability into the MS-DOS world. Portability Many developers of UNIX applications for UNIX systems other than XENIX 286 have programs that are designed implicitly for the 32-bit world. This is because XENIX 286 is one of the few UNIX systems to run on a 16-bit processor. Even if size is not a consideration, the work required to port UNIX applications from 32 to 16 bits has often deterred developers from doing the port. Such developers are best advised to build their applications for XENIX 386 only. Debugging those problems summarized earlier in this article is often too great an effort to warrant porting a program to XENIX 286. This extra development effort can be considered for a later release if market pressure is felt. When porting existing MS-DOS applications to UNIX, it is usually more feasible to build an application in 16 bits. This is certainly the best and easiest option if the application contains a significant amount of assembler code. Since the XENIX and MS-DOS macro assemblers accept the same source syntax in 16-bit mode, assembly code that is not environment-specific should port directly to XENIX. Traditionally, UNIX and MS-DOS applications markets have been separated by a wide gap in complexity. This is because the architecture of real-mode MS- DOS programs is very different from UNIX. With the advent of OS/2, the underlying support provided by the two operating systems is now comparable, so it may make sense for new applications to be developed that can easily be hosted in both XENIX and OS/2 environments. If this is the case, it makes more sense to build the application for the 16-bit environment common to both XENIX and OS/2 and to delay the development of a 32-bit application until a 32-bit version of OS/2 becomes available. Another consideration for simpler applications is the use of the C library calls supported by the Microsoft C Compiler under MS-DOS. These calls, which embody a subset of the UNIX C library calls, can make it relatively easy to build a program that can be simply rehosted in both XENIX and MS-DOS environments. A good example of this approach would be the Microsoft 286 C compiler itself, which is recompiled and linked with different run-time libraries for execution on MS-DOS or XENIX 286. The task of creating a common source code for both MS-DOS and XENIX versions of the compiler is greatly facilitated by the fact that the XENIX and MS-DOS linkers both accept the same relocatable format as input (although they generate a different executable file format). XENIX and UNIX Markets Building applications that port easily between XENIX 286 and other UNIX platforms has generally been difficult. It is prudent──if a source portable application is desired──to remain within the 32-bit world. The 32-bit XENIX 386 environment is completely compatible with the System V Interface Definition (SVID), and thus there should be very little difficulty in moving a carefully designed program from XENIX 386 to other UNIX platforms. Performance Although performance is a combination of many factors, it is most strongly linked to the architecture of the program and to the inherent speed of the host computer. All architectural considerations being equal, a 32-bit program will execute faster than a 16-bit program on the same 386 CPU. Applications that are being ported from the earlier 286 or 8086 worlds onto the 386 will experience an increase in raw 16-bit performance, simply by running the code on a 386, that more than offsets the need to consider rehosting into 32-bit mode. For new XENIX applications, especially those being ported from other 32-bit processors, where a 16-bit port is a serious possibility, it is important to understand the performance degradation seen on the 386 between 16-bit and 32-bit code. The operating system itself runs in 32-bit mode, and some part of a program's execution time is spent in this code. The decrease in speed when moving to 16 bits is not as great as a simple comparison of CPU-bound 16 and 32 performance might indicate. Figure 8 shows the relative execution times of two small C programs, "Cpubound" and "IObound," built as small-16, middle-16, large-16, and small-32 programs on XENIX 386. Figures 6 and 7 show the source code of these programs . An analysis of Figure 8 shows that the 32-bit architecture offers a significant performance advantage for CPU-bound programs that do a mix of arithmetic, pointer processing, and function calls. There is no performance difference among the various 16- and 32-bit models chosen for I/O-bound activities where the processing is all within the kernel. Although the performance of a 16-bit application on XENIX 386 falls short of the 32-bit performance, it is still between two and three times greater than the performance when that program is run on an 8-Mhz 286. The difference in performance between the 386 host and the 286 target must be factored in when measuring 16-bit performance on XENIX 386. Conclusions When designing an application for the XENIX 386 environment, the developer must weigh a number of conflicting criteria. The foremost problem is whether to build the program in 16- or 32-bit mode. Further questions must address the intended market as well as the performance and portability required of the completed product. Lastly, it is important to consider future compatibility requirements. Microsoft and AT&T are currently working together to merge XENIX 386 and AT&T's UNIX System V/386 Release 3.2 into a single UNIX system that will be marketed jointly by the two companies. This Merged Product (MP) will support all the existing 286 and 386 executable formats common to UNIX and XENIX on the 386, thereby allowing all existing applications to run. The emphasis for developers using the Merged Product will be to establish the UNIX/386, 32-bit mode program interface as the standard for new applications. This standard will be a superset of the current XENIX System V/386 program interface, without the support for XENIX-specific system call extensions. This means that in the long run there will be one binary standard, developed and supported by Microsoft and AT&T, which will run on all 386 machines running UNIX, thereby stabilizing the market. Developers who would like their programs to be source compatible with the new binary standard may want to avoid the use of XENIX system call extensions before the Merged Product becomes available in mid-1988. This applies particularly to the use of 32-bit applications (see Figure 9). Although kernel support is provided for XENIX extensions in the MP, minimal development tools will be provided. Debugging support will be limited to the UNIX System V/386 Release 3.2 binary standard. Without exception, the functionality of the XENIX call extensions is supported within the framework of the UNIX program interface. Figure 1: Page table entries are maintained in groups of 16. This allows a 286 segment to expand to 64Kb without existing table entries. ░░░░░░░░░░░░░░░░Mapping 286 Programs under XENIX 386░░░░░░░░░░░░░░░░░░ Data Page Table ╔══════════════════════════╗ Selector ║ Available for Expansion ║ LDT ╟──────────────────────────╢ ╔════════════════╗ ╟──────────────────────────╢ 5FH ║ DS #2 ║ ║ 32Kb ║- 8 pages ╟────────────────╢ ╟──────────────────────────╢ mapped 57H ║ DS #1 ║────────────►║ 64Kb ║-16 pages ╟────────────────╢ ╚══════════════════════════╝ mapped 4FH ║ TS #3 ║ Text Page Table ╟────────────────╢ ╔══════════════════════════╗ 47H ║ TS #2 ║─────┐ ║ Unused ║ ╟────────────────╢ │ ╟──────────────────────────╢ 3FH ║ TS #1 ║──┐ │ ║ Unused ║ ╚════════════════╝ │ │ ╟──────────────────────────╢ │ │ ║ 8Kb ║- 2 pages │ │ ╟──────────────────────────╢ mapped │ └──────►║ 64Kb ║-16 pages │ ╟──────────────────────────╢ mapped └─────────►║ 64Kb ║-16 pages ╚══════════════════════════╝ mapped Figure 2: A Small-Model 286 Program ┌────────────────────────────────────────────────────────┐ │ Text Segment Data Segment │█ │ ╔═════════════════════╗64Kb╔═════════════════════╗64Kb│█ │ ║ ║ ║ ║ │█ │ ║ Unused ║ ║ Available heap ║ │█ │ ║ ║ ║ ║ │█ │ ╟─────────────────────╢ ╟─────────────────────╢ │█ │ ║ ║ ║ ║ │█ │ ║ ║ ║ heap (BSS) ║ │█ │ ║ ║ ║ ║ │█ │ ║ Text ║ ╟─────────────────────╢ │█ │ ║ (fixed size) ║ ║ ║ │█ │ ║ ║ ║ Fixed stack ║ │█ │ ║ ║ ║ ║ │█ │ ║ ║ ╟─────────────────────╢ │█ │ ║ ║ ║ ║ │█ │ ║ ║ ║ Initialized data ║ │█ │ ║ ║ ║ ║ │█ │ ║ ║ ║ ║ │█ │ ║ ║ ║ ║ │█ │ ╚═════════════════════╝0 ╚═════════════════════╝0 │█ │ Selector 3FH Selector 47H │█ └────────────────────────────────────────────────────────┘█ █████████████████████████████████████████████████████████ Figure 3: Large-Model 286 Program Layout ╔════════════════╗ ╔════════════════╗ ║ Unused ║ ║ ║ ╟────────────────╢ ║ 1st segment ║ ║ TS #3 ║ ║ available for ║ ║ 8Kb ║ ║ heap ║ 4FH╚════════════════╝ 67H╚════════════════╝ ╔════════════════╗ ╔════════════════╗ ║ ║ ║ ║ ║ TS #2 ║ ║ DS #2 ║ ║ 64Kb ║ ║ far data ║ ║ ║ ║ ║ 47H╚════════════════╝ 5FH╚════════════════╝ ╔════════════════╗ ╔════════════════╗ ║ ║ ║ ║ ║ TS #1 ║ ║ DS #1 ║ ║ 64Kb ║ ║ stack & data ║ ║ ║ ║ ║ 3FH╚════════════════╝ 57H╚════════════════╝ Figure 4: Camparison of 286 Program Models Name of Model Max. Text Max. Data Stack Heap Performance Small <=64Kb <=64Kb Fixed (<64Kb) In Data (<64Kb) Best Middle >64Kb <=64Kb Fixed In Data (<64Kb) Good Compact <=64Kb >64Kb <=64Kb >64Kb Poor Large >64Kb >64Kb <=64Kb >64Kb Poorest Hybrid Data <=64Kb >64Kb Fixed (<64Kb) <64Kb Good Figure 5: Program Layout in 386 Mode ┌─────────────────────────────────────────────────────┐ │ Text Segment Data Segment │█ The stack │ ╔═════════════════╗4Gb ╔═════════════════╗4Gb │█ grows down │ ║ ║ ║ Video RAM ║virtual │█ to 0 virtual, │ ║ ║ ╟─────────────────╢ │█ while the heap │ ║ ║ ║ Unused ║ │█ grows up. The │ ║ ║ ╟─────────────────╢ │█ sum of the │ ║ ║ ║ Shared Data ║ │█ mapped text, │ ║ ║ ╟─────────────────║6400000H │█ data, and │ ║ ║ ║ Available ║ │█ stack cannot │ ║ Unused ║ ║ for heap ║ │█ exceed the │ ║ ║ ║ expansion ║ │█ installation │ ║ ║ ╟─────────────────║ │█ dependent limit │ ║ ║ ║ Heap ║ │█ (typically the │ ║ ║ ╟─────────────────╢ │█ sum of installed │ ║ ║ ║ Initialized Data║ │█ RAM plus the │ ║ ║ ╟─────────────────╢ │█ size of │ ╟─────────────────╢ ║ Stack ║ │█ the paging │ ║ ║ ╟─────────────────╢1880000H │█ area on │ ║ Text ║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ │█ the disk). │ ║ ║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ │█ │ ╚═════════════════╝0 ╚═════════════════╝0 │█ └─────────────────────────────────────────────────────┘█ ██████████████████████████████████████████████████████ Figure 7: IObound.c /* * IObound.c */ #define IMAX 100 #define JMAX 100 #define BFS 2 char buffer[BFS]; main() { int i, j; int fd; /* Create a Disk File */ fd = creat("scratch", 0600); for(i=0; i<IMAX; i++){ sync(); for(j=0; j<JMAX; j++){ write(fd, buffer, BFS); } } sync(); /* Return to Beginning of the File */ lseek(fd, 0, 0); for(i=0; i<IMAX; i++){ sync(); for(j=0; j<JMAX; j++){ read(fd, buffer, BFS); } } /* Remove the File */ unlink('scratch'); exit(0); } Figure 6: Cpubound.c /* * Cpubound.c */ #define IMAX 100 #define JMAX 1000 int id[IMAX]; int jd[JMAX]; main() { int i, j; for(i=0; i<IMAX; i++){ id[i] = i; for(j=0; j<JMAX; j++){ jd[i] = j; calli(id, jd, j); } } exit(0); } calli(i, j, c) int *i, *j; int c; { int t; int ti = i[c]; int tj = j[c]; while(ti- -) t += (*(i++))+(*(j++))+(tj-); return(t); } Figure 8: Performance Table CPU Bound Performance (normalized) Real time User time System time Small-Model 286 32.4 (0.59) 32.2 0.0 Middle-Model 286 40.6 (0.47) 40.5 0.0 Large-Model 286 57.5 (0.33) 57.4 0.0 Small-Model 386 19.0 (1.00) 18.9 0.0 I/O Bound Performace (normalized) Real time User time System time Small-Model 286 38.5 (1.00) 0.3 13.3 Middle-Model 286 38.6 (1.00) 0.7 12.8 Large-Model 286 42.4 (0.91) 0.4 14.8 Small-Model 386 38.4 (1.00) 0.2 12.4 Figure 9: XENIX System Call Extensions to be avoided for portability. ╓┌──────────────┌────────────────────────────────────────────────────────────╖ Entry Point Function chsize adjust file size creatsem semaphore operations nbwaitsem semaphore operations Entry Point Function nbwaitsem semaphore operations opensem semaphore operations sigsem semaphore operations waitsem semaphore operations execseg execute data unexecseg execute data ftime obsolete UNIX time function locking XENIX file locking nap sleep for a short time proctl process specific control function rdchk check for input without reading sdenter XENIX shared data extension sdfree XENIX shared data extension Entry Point Function sdfree XENIX shared data extension sdget XENIX shared data extension sdgetv XENIX shared data extension sdleave XENIX shared data extension sdwaitv XENIX shared data extension shutdn shutdown system swapon control paging devices ─────────────────────────────────────────────────────────────────────────── Demand Paging and Virtual Memory ─────────────────────────────────────────────────────────────────────────── Demand paging is a feature of the XENIX 386 operating system, built on top of the 386 chip architecture that allows a program to run even though all of its pages have not been loaded into memory. Instead, only those pages from the executable image stored on disk that are actually referenced by the program as it runs are loaded into memory. Whenever the program references a page that is not in memory, it causes a "page fault." The XENIX kernel acts in response by loading the requested page from the disk and restarting the faulting program. The effect of demand paging is to reduce the memory usage of a given program to those pages that it actually references during a particular invocation. This set of pages is called the "working set" and is usually smaller than the full size of the program, especially if that program is large. Demand paging occurs without any knowledge on the part of the application. For example, the second pass of the Microsoft 386 C compiler is approximately 300Kb; however its working set on a typical program is closer to 80Kb, depending on which Microsoft C language features are used. The effect of demand paging is to improve the throughput of the system on smaller memory configurations, and because it is not necessary to load programs into memory before they start execution, the latency of command execution can be greatly reduced. Virtual memory allows the real memory associated with a program's heap (allocated via malloc or sbrk calls) to be allocated on demand rather than at the time of the malloc or sbrk function call. A program can be assigned a large address space without incurring additional overhead for pages that would remain unused. When a program makes a memory allocation call, the kernel recognizes the change in the end of the virtual heap. However, it allocates no memory between the end of the old heap and the new location. It is only when a program accesses pages in the new heap region and causes a page fault that the kernel allocates empty pages of real memory. This feature is also called "zero-fill on demand." ████████████████████████████████████████████████████████████████████████████ HEXCALC: An Instructive Pop-Up Calculator for Microsoft Windows Charles Petzold☼ When you design a dialog box for either a Windows program or an OS/2 Presentation Manager program, you have available a whole array of "control windows" that take the form of push buttons, check boxes, scroll bars, list boxes, edit fields, and text items. Simply list these controls in a dialog box template and they magically appear in the dialog box window when the dialog box is invoked. A little further into Windows programming, you realize that these control windows can also be plastered right onto the surface of your window's client area. All it takes is a call to CreateWindow specifying the window class and the style of the control. A demonstration of this technique was shown in my COLORSCR program. (See "A Simple Windows Application for Custom Color Mixing," MSJ, Vol. 2, No. 2.) Then you reach a frustrating impasse. What you really want to do is create a simple dialog box template that describes the size and position of the various controls, and use that template to put the controls on your window's client area. At first there doesn't appear to be any way to do this. The only Windows functions that use the dialog template are CreateDialog and DialogBox. But you want to have these controls on your main window, not on a dialog box. HEXCALC (see Figure 1☼) shows how this is done. HEXCALC is a pop-up window with 29 child window push-button controls. It does not contain even one CreateWindow call. Instead, it uses CreateDialog to create the program's main window. This window and all the child window push-button controls are defined in a dialog box template in HEXCALC's resource script. This article will take a close look at HEXCALC, then compares HEXCALCW, a version for Windows 2.0, with HEXCALCP, which runs under Presentation Manager in the OS/2 systems. Using HEXCALC HEXCALC is a ten-function infix-notation hexadecimal calculator with a full keyboard and mouse interface. It works with 32-bit unsigned long integers and does addition, subtraction, multiplication, division, remainders, bitwise AND, OR, and Exclusive-OR, as well as left and right bit shifts. The buttons for the operations use C notation, except for the bit shift buttons, which display one angle bracket rather than two. To do a calculation, type or click in the first number (up to eight hexadecimal digits) followed by the operation and then the second number. You can show the result by clicking the Equals button or by pressing either the keyboard Equals key or Enter key. To correct your entries, you can use either the BackSpace key, the left cursor movement key, or the Back button. Pressing Escape or clicking the result box clears the current entry. Results are always truncated to 32 bits, just as if you were performing the operation on two unsigned long integers in a C program. The only special handling is a check for division by zero before doing division or a remainder operation. In this case, HEXCALC sets the result to FFFFFFFF. Inspired by PIFEDIT I don't know how many Windows programmers have taken a close look at PIFEDIT, but it does some very interesting things. PIFEDIT's main window looks a lot like a full-screen dialog box, and in fact it is. All those static text fields, edit fields, check boxes, and radio buttons on the surface of PIFEDIT's main window are defined in a dialog box template. Like HEXCALC, PIFEDIT doesn't make any CreateWindow calls. PIFEDIT's main window is created instead with a call to CreateDialog. The messages to this window are processed by a window function in the PIFEDIT program. The technique demonstrated in PIFEDIT and HEXCALC can be used for any program that would otherwise call CreateWindow to create child window controls on the surface of its main window. These controls can include buttons, scroll bars, static text fields, edit fields, list boxes, and even controls that you design yourself. The technique works best when these child window controls are of a fixed size and position relative to the size of a system font character, because that's how they are defined in the dialog box template. This is not the technique that the CALC.EXE program uses, however. CALC draws its own buttons rather than using child window button controls. This results in a faster display, but CALC is then forced to do hit testing on the mouse-click messages. Because push-button controls do their own hit testing, HEXCALC can ignore all mouse messages. The HEXCALC Program The various parts of the Windows 2.0 version of HEXCALC are shown in Figures 2W-6W. With an installed version of the Microsoft C Compiler Version 4.0 or 5.0 and the Windows Software Development Kit Version 2.0, you can create HEXCALCW.EXE by running MAKE HEXCALCW Let's begin by taking a look at the HEXCALCW.RC resource script. This contains the dialog box template that defines HEXCALC's main window and all the push-button controls. Resource Script The dialog box template shown in the HEXCALCW.RC resource script describes the size and appearance of HEXCALC's window. The style bits are WS_OVERLAPPED, WS_CAPTION, WS_SYSMENU, and WS_MINIMIZEBOX. Except for WS_MINIMIZEBOX, these are normal for a modeless dialog box. The text appearing in the caption bar is "Hex Calculator". As is the case with all dialog box templates, the values in the DIALOG statement that specify the window's initial position and size are in units of 1/4 the width and 1/8 the height of a system font character. It is 102 units (or 25 1/2 characters) wide and 122 units (or 15 1/4 characters) high. The 29 buttons displayed in the calculator are defined with PUSHBUTTON statements. The string in double quotes is the text that appears inside the button. The number that follows is the control ID. This is followed by the horizontal and vertical position of the control relative to the upper-left- hand corner of the dialog box's client area. The final two numbers indicate the horizontal and vertical size of the control. All the push buttons are a standard height: 14 units, or 1 3/4 characters. Control IDs The control ID is an integer that the child window uses to identify itself to its parent. The control ID also allows the parent window to send messages to the child window control (using SendDlgItemMessage) without knowing the window handle of the control. A parent can obtain the window handle of a control from its ID using GetDlgItem. Normally Windows programmers use identifiers defined in a header file for these control IDs. The header file is then included in both the program and the resource script using a #include statement. The control IDs I've chosen for these push buttons may appear to be random, but there really is a method to the madness. The control IDs have been set to the ASCII codes of the corresponding number, letter, or symbol that appears inside the push button. As will be seen in the HEXCALC.C listing, this is both a cheap and easy method to add a keyboard interface to the calculator. Clicking on a child window button with the mouse, the child window sends to its parent window (which is HEXCALC's main window) a WM_COMMAND message with wParam equal to the control ID. When the user presses a character key on the keyboard, HEXCALC's main window receives a WM_CHAR message with wParam equal to the ASCII code of the character. This means that the same logic can be used for both key presses and mouse clicks. The CLASS Statement The big difference between HEXCALC's dialog box template and most other dialog box templates is the presence of the CLASS statement. You can read in the Windows Programmer's Utility Guide, that (I quote it in its entirety) "This optional statement defines the class of the dialog box" and the parameter to CLASS "is an integer or a string (enclosed in double quotes) that identifies the dialog box class." At this point, perhaps some additional explanation is required. All windows created in Microsoft(R) Windows are based on a particular window class. Among other things, this class specifies a function (called the window function) that processes messages to the window. Normally in a Windows program you first register a window class and then create a window based on that class by calling CreateWindow. When you create a dialog box by calling CreateDialog or DialogBox, Windows normally uses a default window class that Windows itself defines. The function specified in this window class is internal to Windows. It is this window function that processes the messages to the dialog box. This function will pass many of the messages to a "dialog function" located in your program. The address of this dialog function is specified as a parameter to the CreateDialog or DialogBox function. That's the normal case. However, what we're doing in HEXCALC is somewhat different. By specifying a window class in the dialog box template, you're telling Windows not to use its own window class for dialog boxes created using this template. You're telling Windows to use a different window class, specifically the one indicated in the CLASS statement. This provides Windows with the address of a window function. It is this function that will receive messages for the dialog box. The window function, called WndProc, is part of HEXCALC. For an ordinary dialog box you wouldn't want to do this, because then you would be faced with the problem of duplicating all the logic required for changing the input focus among child window controls when the user presses the Tab key or cursor movement keys. But for a program like HEXCALC, this technique is ideal. Creating the Window Like most Windows programs, HEXCALC begins by registering a window class (HexCalcW) that among other things specifies the window function (WndProc) that will process messages to windows based on that class. But then, rather than call CreateWindow, HEXCALC calls CreateDialog, a function that usually creates a modeless dialog box. The second parameter to the CreateDialog function is the name of the dialog box template, which is also named HexCalcW. The third parameter to CreateDialog is normally the window handle of the dialog box's parent. Because you're using this dialog box as the main window in HEXCALC, this is set to 0. The fourth parameter to CreateDialog is normally a far pointer to the program's dialog function for the dialog box. This is the function to which some (but not all) messages processed by the actual dialog box window function are passed. Usually that window function is internal to Windows, but not when a class for the dialog box is specified in the template. Because the WndProc window function itself is part of HEXCALC, this dialog function is not needed. The fourth parameter is set to NULL. Now it's time to ask: What does Windows do during the CreateDialog call? Consider that Windows has access to the instance handle of the program and the name of the dialog box template. It gets this from the CreateDialog call: hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ; The szAppName variable is a character array that contains the name HexCalcW, the name of the dialog box template. This means that Windows can load the dialog box template into memory. The template (in its ASCII form) begins like this: HexCalcW DIALOG 32768, 0, 102, 122 STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZE CLASS "HexCalcW" CAPTION "Hex Calculator" Using the parameters to the CreateDialog function along with the top of the dialog box template, Windows is able to call CreateWindow with the following parameters: hWnd = CreateWindow ("HexCalcW", "Hex Calculator", WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZE, 32768,0, 102 * 4 / xChar, 122 * 8 / yChar, NULL, NULL, hInstance, NULL) ; This creates HEXCALC's top-level window. Assigning a value of 32768 to the initial x position specifies that Windows use the default position (when using a CreateWindow call in a program, CW_USEDEFAULT is the identifier). The xChar and yChar values shown here would be determined by Windows. They are the width and height of a system font character and are used to translate the dialog box coordinate and size units into pixels. The hWnd parameter returned from this call is eventually passed back to the program as the return value from CreateDialog. Still processing the CreateDialog call, Windows will then create child windows based on the PUSHBUTTON statements in the dialog box template. This is the first PUSHBUTTON statement: PUSHBUTTON "D", 68, 8, 24, 14, 14 This becomes another CreateWindow call: hCtl = CreateWindow ("button", "D", WS_CHILD | WS_VISIBLE | BS_PUSHBUTTON | 8 * 4 / xChar, 24 * 8 / yChar, 14 * 4 / xChar, 14 * 8 / yChar, hWnd, 68, hInstance, NULL) ; Windows will call CreateWindow 29 times for each of the push buttons. You should note that the eighth parameter (the parent window) is set to hWnd. This will permit the child window controls to send WM_COMMAND messages to their parent window. The ninth parameter will be set to the control ID. Because the dialog box template specifies a window class, Windows does almost nothing else during the CreateDialog function except to call CreateWindow multiple times based on the template. When CreateDialog returns, all the windows have been created. The program proceeds normally just as if it had done all that work itself. HEXCALC has no other odd code. It has a normal message loop and a normal window function. So, in effect, HEXCALC is not very different from my earlier COLORSCR program. Both programs create a main window and several child window controls. The difference is that COLORSCR does it with explicit CreateWindow calls. HEXCALC simply gives Windows a dialog box template and says, "Here, you do this stuff. Tell me when you're finished." Window Function I'm not going to explain the actual calculator logic in HEXCALC because that's really not the point of this exercise. However, let's take a look at some of the code near the top of WndProc. Because the control IDs of the push buttons are set to the ASCII code of the push-button text, the keyboard interface is very simple. On a WM_CHAR message, the wParam parameter (the ASCII code of the key) is first converted to upper case and the Enter key is translated to an equals sign. HEXCALC determines if the key corresponds to a button by calling GetDlgItem. This function translates a control ID of a child window into the child window handle: hButton = GetDlgItem (hWnd, wParam) If hButton is 0, then the key does not correspond to a button, and HEXCALC beeps. Otherwise, HEXCALC flashes the button by sending it a couple of BM_SETSTATE messages: SendMessage (hButton, BM_SETSTATE, 1, 0L) ; SendMessage (hButton, BM_SETSTATE, 0, 0L) ; This adds a nice touch to the keyboard interface of HEXCALC and-as you can see-it's almost trivial. When WndProc processes WM_COMMAND messages, it always sets the input focus to the parent window: case WM_COMMAND: SetFocus (hWnd) ; Otherwise, the input focus would be shifted to one of the buttons whenever it was clicked with the mouse. This would alter the appearance of the button and also prevent WndProc from seeing subsequent WM_CHAR messages. The Presentation Manager Version The conversion of HEXCALC to the OS/2 Presentation Manager is relatively straightforward. The result──called HEXCALCP.EXE──is shown in Figures 2PM-6PM. From a distance of about six feet, a Presentation Manager program looks the same as a Windows program. Structurally, the application program interfaces in the two environments are quite similar. However, on closer examination, lots of little changes are required. Some of these changes are described by Michael Geary in "Converting Windows Applications for Microsoft(R) OS/2 Presentation Manager," in this issue. The format of the dialog box template in HEXCALCP.RC is slightly different because it reflects the structure of "standard windows" in the Presentation Manager. The main window is a "frame" window, the client window is a child of the frame, and the push buttons are children of the client window. This parent-child relationship is described by the window definitions within nested brackets in the template. Messages to the client window in HEXCALCP are handled by the ClientWndProc window function. You will remember that HEXCALC must make a call to SetFocus whenever a button is clicked to prevent the button from keeping the keyboard input focus. Using the Presentation Manager, buttons can have a BS_NOMOUSEFOCUS style, which means that they will not receive the input focus when clicked with the mouse. The SetFocus call is made unnecessary by specifying the BS_NOMOUSEFOCUS style for the push buttons in the HEXCALCP.RC resource script. The keyboard interface in HEXCALCP is somewhat cleaner in the Presentation Manager version. First, all keyboard messages are WM_CHAR messages so HEXCALCP doesn't have to process WM_KEYDOWN messages. Secondly, if the key pressed corresponds to one of the buttons, then HEXCALCP sends the button a single BM_CLICK message: WinSendMsg (hWndButton, BM_CLICK, 0L, 0L) ; This message not only causes the button to flash, but also causes the button to send its owner a WM_COMMAND message. In the Windows version, the keyboard message falls through to be processed within the WM_COMMAND case. Termination works a little differently in the two programs. In Windows, the Close option on the system menu generates a WM_CLOSE message. If the message is passed on to DefWindowProc, then Windows destroys the window by calling DestroyWindow and the window function receives a WM_DESTROY message. The window function responds by posting a WM_QUIT message to itself with PostQuitMessage. This message causes the GetMessage function in WinMain to return 0, and the program terminates. In the Presentation Manager, the WM_CLOSE message is still generated by the system menu. If the program passes this message to WinDefWindowProc, then the Presentation Manager posts a WM_QUIT message to the message queue. As in Windows, this message causes the WinGetMsg call in main to return 0. However, the window still exists, so the program must explicitly destroy the window in main using a call to WinDestroyWindow. This function causes the Presentation Manager to destroy the frame window as well as all the children and other descendants of the frame. Figure 2W: HEXCALCW Make File hexcalcw.obj: hexcalcw.c cl -c -D LINT_ARGS -Gsw -Os -W2 -Zp hexcalcw.c hexcalcw.res : hexcalcw.rc hexcalcw.ico rc -r hexcalcw.rc hexcalcw.exe: hexcalcw.obj hexcalcw.def hexcalcw.res link4 hexcalcw, /align:16, /map, slibw, hexcalcw rc hexcalcw.res Figure 2PM: HEXCALCP Make File hexcalcp.obj: hexcalcp.c hexcalcp.h cl -c -D LINT_ARGS -G2sw -W2 -Zp hexcalcp.c hexcalcp.res : hexcalcp.rc hexcalcp.h rc -r hexcalcp.rc hexcalcp.exe: hexcalcp.obj hexcalcp.def hexcalcp.res link hexcalcp, /align:16, /map, slibc5 os2, hexcalcp rc hexcalcp.res Figure 3W: HEXCALCW.RC Resource Script #include <windows.h> HexCalcW ICON hexcalcw.ico HexCalcW DIALOG 32768, 0, 102, 122 STYLE WS_OVERLAPPED | WS_CAPTION | WS_SYSMENU | WS_MINIMIZEBOX CLASS "HexCalcW" CAPTION "Hex Calculator" BEGIN PUSHBUTTON "D", 68, 8, 24, 14, 14 PUSHBUTTON "A", 65, 8, 40, 14, 14 PUSHBUTTON "7", 55, 8, 56, 14, 14 PUSHBUTTON "4", 52, 8, 72, 14, 14 PUSHBUTTON "1", 49, 8, 88, 14, 14 PUSHBUTTON "0", 48, 8, 104, 14, 14 PUSHBUTTON "0", 27, 26, 4, 50, 14 PUSHBUTTON "E", 69, 26, 24, 14, 14 PUSHBUTTON "B", 66, 26, 40, 14, 14 PUSHBUTTON "8", 56, 26, 56, 14, 14 PUSHBUTTON "5", 53, 26, 72, 14, 14 PUSHBUTTON "2", 50, 26, 88, 14, 14 PUSHBUTTON "Back", 8, 26, 104, 32, 14 PUSHBUTTON "C", 67, 44, 40, 14, 14 PUSHBUTTON "F", 70, 44, 24, 14, 14 PUSHBUTTON "9", 57, 44, 56, 14, 14 PUSHBUTTON "6", 54, 44, 72, 14, 14 PUSHBUTTON "3", 51, 44, 88, 14, 14 PUSHBUTTON "+", 43, 62, 24, 14, 14 PUSHBUTTON "-", 45, 62, 40, 14, 14 PUSHBUTTON "*", 42, 62, 56, 14, 14 PUSHBUTTON "/", 47, 62, 72, 14, 14 PUSHBUTTON "%", 37, 62, 88, 14, 14 PUSHBUTTON "Equals", 61, 62, 104, 32, 14 PUSHBUTTON "&&", 38, 80, 24, 14, 14 PUSHBUTTON "|", 124, 80, 40, 14, 14 PUSHBUTTON "^", 94, 80, 56, 14, 14 PUSHBUTTON "<", 60, 80, 72, 14, 14 PUSHBUTTON ">", 62, 80, 88, 14, 14 END Figure 3PM: HEXCALCP.RC Resource Script #include <os2.h> #include "hexcalcp.h" WINDOWTEMPLATE ID_HEXCALC { FRAME "Hex Calculator", 1, 110, 40, 102, 122, WS_VISIBLE | FS_TITLEBAR | FS_SYSMENU | FS_MINBUTTON | FS_BORDER { WINDOW "", FID_CLIENT, 0, 0, 102, 122, "HexCalcP", WS_VISIBLE { PUSHBUTTON "D", 68, 8, 88, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "A", 65, 8, 72, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "7", 55, 8, 56, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "4", 52, 8, 40, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "1", 49, 8, 24, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "0", 48, 8, 4, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "0", 27, 26, 104, 50, 14, BS_NOMOUSEFOCUS PUSHBUTTON "E", 69, 26, 88, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "B", 66, 26, 72, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "8", 56, 26, 56, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "5", 53, 26, 40, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "2", 50, 26, 24, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "Back", 8, 26, 4, 32, 14, BS_NOMOUSEFOCUS PUSHBUTTON "F", 70, 44, 88, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "C", 67, 44, 72, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "9", 57, 44, 56, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "6", 54, 44, 40, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "3", 51, 44, 24, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "+", 43, 62, 88, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "-", 45, 62, 72, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "*", 42, 62, 56, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "/", 47, 62, 40, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "%", 37, 62, 24, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "Equals", 61, 62, 4, 32, 14, BS_NOMOUSEFOCUS PUSHBUTTON "&", 38, 80, 88, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "|", 124, 80, 72, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "^", 94, 80, 56, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON "<", 60, 80, 40, 14, 14, BS_NOMOUSEFOCUS PUSHBUTTON ">", 62, 80, 24, 14, 14, BS_NOMOUSEFOCUS } } } Figure 4W: HEXCALCW.DEF Module Definition File NAME HexCalcW DESCRIPTION 'Hexadecimal Calculator(C) Charles Petzold 1987' STUB 'WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 1024 STACKSIZE 4096 EXPORTS WndProc Figure 4PM: HEXCALCP.DEF Module Definition File NAME HEXCALCP DESCRIPTION 'Hexadecimal Calculator(C) Charles Petzold 1987' STUB 'OS2STUB.EXE' HEAPSIZE 1024 STACKSIZE 4096 EXPORTS ClientWndProc Figure 5W: There is no special header file equivalent for the Windows version. Figure 5PM: HEXCALCP.H Header File #define ID_HEXCALC 1 Figure 6W: HEXCALCW.C is the Source Code Listing #include <windows.h> #include <limits.h> #include <stdlib.h> #include <string.h> #include <ctype.h> 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 [] = "HexCalcW" ; 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 = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = COLOR_WINDOW + 1 ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) return FALSE ; } hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ; ShowWindow (hWnd, nCmdShow) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void ShowNumber (hWnd, dwNumber) HWND hWnd ; DWORD dwNumber ; { char szBuffer [20] ; SetDlgItemText (hWnd, VK_ESCAPE, strupr (ltoa (dwNumber, szBuffer, 16))) ; } DWORD CalcIt (dwFirstNum, nOperation, dwNum) DWORD dwFirstNum, dwNum ; short nOperation ; { switch (nOperation) { case '=' : return dwNum ; case '+' : return dwFirstNum + dwNum ; case '-' : return dwFirstNum - dwNum ; case '*' : return dwFirstNum * dwNum ; case '&' : return dwFirstNum & dwNum ; case '|' : return dwFirstNum | dwNum ; case '^' : return dwFirstNum ^ dwNum ; case '<' : return dwFirstNum << dwNum ; case '>' : return dwFirstNum >> dwNum ; case '/' : return dwNum ? dwFirstNum / dwNum : ULONG_MAX ; case '%' : return dwNum ? dwFirstNum % dwNum : ULONG_MAX ; default : return 0L ; } } long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam) HWND hWnd; unsigned iMessage; WORD wParam; LONG lParam; { static BOOL bNewNumber = TRUE ; static DWORD dwNumber, dwFirstNum ; static short nOperation = '=' ; HWND hButton ; switch (iMessage) { case WM_KEYDOWN: /* left arrow -> backspace */ if (wParam != VK_LEFT) break ; wParam = VK_BACK ; /* fall through */ case WM_CHAR: if ((wParam = toupper (wParam)) == VK_RETURN) wParam = '=' ; if (hButton = GetDlgItem (hWnd, wParam)) { SendMessage (hButton, BM_SETSTATE, 1, 0L) ; SendMessage (hButton, BM_SETSTATE, 0, 0L) ; } else { MessageBeep (0) ; break ; } /* fall through */ case WM_COMMAND: SetFocus (hWnd) ; if (wParam == VK_BACK) /* backspace */ ShowNumber (hWnd, dwNumber /= 16) ; else if (wParam == VK_ESCAPE) /* escape */ ShowNumber (hWnd, dwNumber = 0L) ; else if (isxdigit (wParam)) /* hex digit */ { if (bNewNumber) { dwFirstNum = dwNumber ; dwNumber = 0L ; } bNewNumber = FALSE ; if (dwNumber <= ULONG_MAX >> 4) ShowNumber (hWnd, dwNumber = 16 * dwNumber + wParam - (isdigit (wParam) ? '0' : 'A' - 10)) ; else MessageBeep (0) ; } else /* operation */ { if (!bNewNumber) ShowNumber (hWnd, dwNumber = CalcIt (dwFirstNum, nOperation, dwNumber)) ; bNewNumber = TRUE ; nOperation = wParam ; } break ; case WM_DESTROY: PostQuitMessage (0) ; break ; default : return DefWindowProc (hWnd, iMessage, wParam, lParam) ; } return 0L ; } #include <windows.h> #include <limits.h> #include <stdlib.h> #include <string.h> #include <ctype.h> 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 [] = "HexCalcW" ; 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 = LoadIcon (hInstance, szAppName) ; wndclass.hCursor = LoadCursor (NULL, IDC_ARROW) ; wndclass.hbrBackground = COLOR_WINDOW + 1 ; wndclass.lpszMenuName = NULL ; wndclass.lpszClassName = szAppName ; if (!RegisterClass (&wndclass)) return FALSE ; } hWnd = CreateDialog (hInstance, szAppName, 0, NULL) ; ShowWindow (hWnd, nCmdShow) ; while (GetMessage (&msg, NULL, 0, 0)) { TranslateMessage (&msg) ; DispatchMessage (&msg) ; } return msg.wParam ; } void ShowNumber (hWnd, dwNumber) HWND hWnd ; DWORD dwNumber ; { char szBuffer [20] ; SetDlgItemText (hWnd, VK_ESCAPE, strupr (ltoa (dwNumber, szBuffer, 16))) ; } DWORD CalcIt (dwFirstNum, nOperation, dwNum) DWORD dwFirstNum, dwNum ; short nOperation ; { switch (nOperation) { case '=' : return dwNum ; case '+' : return dwFirstNum + dwNum ; case '-' : return dwFirstNum - dwNum ; case '*' : return dwFirstNum * dwNum ; case '&' : return dwFirstNum & dwNum ; case '|' : return dwFirstNum | dwNum ; case '^' : return dwFirstNum ^ dwNum ; case '<' : return dwFirstNum << dwNum ; case '>' : return dwFirstNum >> dwNum ; case '/' : return dwNum ? dwFirstNum / dwNum : ULONG_MAX ; case '%' : return dwNum ? dwFirstNum % dwNum : ULONG_MAX ; default : return 0L ; } } long FAR PASCAL WndProc (hWnd, iMessage, wParam, lParam) HWND hWnd; unsigned iMessage; WORD wParam; LONG lParam; { static BOOL bNewNumber = TRUE ; static DWORD dwNumber, dwFirstNum ; static short nOperation = '=' ; HWND hButton ; switch (iMessage) { case WM_KEYDOWN: /* left arrow -> backspace */ if (wParam != VK_LEFT) break ; wParam = VK_BACK ; /* fall through */ case WM_CHAR: if ((wParam = toupper (wParam)) == VK_RETURN) wParam = '=' ; if (hButton = GetDlgItem (hWnd, wParam)) { SendMessage (hButton, BM_SETSTATE, 1, 0L) ; SendMessage (hButton, BM_SETSTATE, 0, 0L) ; } else { MessageBeep (0) ; break ; } /* fall through */ case WM_COMMAND: SetFocus (hWnd) ; if (wParam == VK_BACK) /* backspace */ ShowNumber (hWnd, dwNumber /= 16) ; else if (wParam == VK_ESCAPE) /* escape */ ShowNumber (hWnd, dwNumber = 0L) ; else if (isxdigit (wParam)) /* hex digit */ { if (bNewNumber) { dwFirstNum = dwNumber ; dwNumber = 0L ; } bNewNumber = FALSE ; if (dwNumber <= ULONG_MAX >> 4) ShowNumber (hWnd, dwNumber = 16 * dwNumber + wParam - (isdigit (wParam) ? '0' : 'A' - 10)) ; else MessageBeep (0) ; } else /* operation */ { if (!bNewNumber) ShowNumber (hWnd, dwNumber = CalcIt (dwFirstNum, nOperation, dwNumber)) ; bNewNumber = TRUE ; nOperation = wParam ; } break ; case WM_DESTROY: PostQuitMessage (0) ; break ; default : return DefWindowProc (hWnd, iMessage, wParam, lParam) ; } return 0L ; } Figure 6PM: HEXCALCP.C is the Source Code Listing #include <os2.h> #include <limits.h> #include <stdlib.h> #include <string.h> #include <ctype.h> #include "hexcalcp.h" ULONG EXPENTRY ClientWndProc (HWND, USHORT, ULONG, ULONG) ; main () { static CHAR szClassName [] = "HexCalcP" ; HAB hAB ; HMQ hMQ ; HWND hWnd ; QMSG qmsg ; hAB = WinInitialize () ; hMQ = WinCreateMsgQueue (hAB, 0) ; WinRegisterClass (hAB, szClassName, ClientWndProc, 0L, 0, NULL) ; hWnd = WinLoadDlg (HWND_DESKTOP, HWND_DESKTOP, NULL, NULL, ID_HEXCALC, NULL) ; WinSetFocus (HWND_DESKTOP, WinWindowFromID (hWnd, FID_CLIENT)) ; while (WinGetMsg (hAB, &qmsg, NULL, 0, 0)) WinDispatchMsg (hAB,&qmsg); WinDestroyWindow (hWnd) ; WinDestroyMsgQueue (hMQ) ; WinTerminate (hAB) ; return 0 ; } void ShowNumber (hWnd, lNumber) HWND hWnd ; ULONG lNumber ; { CHAR szBuffer [20] ; HWND hWndResult ; hWndResult = WinWindowFromID (hWnd, VK_ESCAPE) ; WinSetWindowText (hWndResult, strupr (ltoa (lNumber, szBuffer, 16))) ; } ULONG CalcIt (lFirstNum, iOperation, lNum) ULONG lFirstNum, lNum ; SHORT iOperation ; { switch (iOperation) { case '=' : return lNum ; case '+' : return lFirstNum + lNum ; case '-' : return lFirstNum - lNum ; case '*' : return lFirstNum * lNum ; case '&' : return lFirstNum & lNum ; case '|' : return lFirstNum | lNum ; case '^' : return lFirstNum ^ lNum ; case '<' : return lFirstNum << lNum ; case '>' : return lFirstNum >> lNum ; case '/' : return lNum ? lFirstNum / lNum : ULONG_MAX ; case '%' : return lNum ? lFirstNum % lNum : ULONG_MAX ; default : return 0L ; } } ULONG EXPENTRY ClientWndProc (hWnd, nMessage, lParam1, lParam2) HWND hWnd ; USHORT nMessage ; ULONG lParam1 ; ULONG lParam2 ; { static BOOL bNewNumber = TRUE ; static ULONG lNumber, lFirstNum ; static SHORT iOperation = '=' ; HWND hWndButton ; SHORT idButton ; switch (nMessage) { case WM_CHAR: if (lParam1 & KC_KEYUP) break ; if (HIUSHORT (lParam2) == VK_LEFT) /* left arrow to */ LOUSHORT (lParam2) = VK_BACK ; /* backspace */ if (HIUSHORT (lParam2) == VK_RETURN) /* return to */ LOUSHORT (lParam2) = '=' ; /* equals */ if (LOUSHORT (lParam2) == 0) break ; LOUSHORT (lParam2) = toupper (LOUSHORT (lParam2)) ; if (hWndButton = WinWindowFromID (hWnd, LOUSHORT (lParam2))) WinSendMsg (hWndButton, BM_CLICK, 0L, 0L) ; else WinAlarm (HWND_DESKTOP, WA_ERROR) ; return 1L ; case WM_COMMAND: idButton = LOUSHORT (lParam1) ; if (idButton == VK_BACK) /* backspace */ ShowNumber (hWnd, lNumber /= 16) ; else if (idButton == VK_ESCAPE) /* escape */ ShowNumber (hWnd, lNumber = 0L) ; else if (isxdigit (idButton)) /* hex digit */ { if (bNewNumber) { lFirstNum = lNumber ; lNumber = 0L ; } bNewNumber = FALSE ; if (lNumber <= ULONG_MAX >> 4) ShowNumber (hWnd, lNumber = 16 * lNumber + idButton - (isdigit (idButton) ? '0' : 'A' - 10)) ; else WinAlarm (HWND_DESKTOP, WA_ERROR) ; } else /* operation */ { if (!bNewNumber) ShowNumber (hWnd, lNumber = CalcIt (lFirstNum, iOperation, lNumber)) ; bNewNumber = TRUE ; iOperation = idButton ; } break ; default : return WinDefWindowProc (hWnd, nMessage, lParam1, lParam2) ; } return 0L ; } ████████████████████████████████████████████████████████████████████████████ Effectively Using Far and Huge Data Pointers In Your Microsoft C Programs Kaare Christian☼ Program pointers are one of the most important features of the C language, because they are often the only way to express your thoughts clearly and precisely. Addresses are critical in all programming languages, but most languages try to take care of addressing details for you, thereby hiding crucial information about what happens when your program executes. Only the C language brings these details to the fore, by allowing you to express pointer operations in the text of the program. In the early, heady days of C, pointers were 16-bit quantities. Newcomers to C had to learn pointer arithmetic as well as C's somewhat difficult notation for using pointers, but they did not have to worry much about the underlying machine, the PDP-11. Pointer operations are beautifully supported by the simple and consistent architecture of the PDP-11, but only within a 64Kb data space. The PC family of computers is based on Intel(R) processors, which use a segmented architecture. If your data fits within one 64Kb space (Microsoft(R) C small- or medium-model programs), then pointer operations on the PC will work efficiently and consistently, just as on the venerable PDP-11. (This discussion applies to the segmentation model of the 8086, 8088, 186, 188 and the real mode on the 286 and 386. Protected mode segmentation is different and is not discussed.) However, if you do need the extra memory that the PC architecture offers, then you must learn to use far and huge pointers. Microsoft C lets you mix data types within a single program. You can create a far pointer in a small-model program or a near pointer in a huge-model program. My experience has been with occasional use of pointers to far or huge data in small-model programs, because the software that I work on tends to be small except for a few large data structures. Many of my comments will apply to all pointers in a compact-, large-, or huge-model program, because in those models data pointers are, by default, far or huge. All of the sample programs in this article were compiled with Microsoft C Version 4.0, using the small model. Besides pointers to far and huge data, Microsoft C also has far and huge data types. In Microsoft C there are two methods for creating a very large array: you can declare it as a huge or far array of integers or you can declare a pointer to a huge or far integer and then use halloc, the huge memory allocation subroutine, to allocate the space. The second approach is preferable because it is more flexible. Declaring a huge array forces you to decide in advance the exact array size; using halloc to create the array allows your program to adjust to the actual amount of memory present on the computer when it executes. This article specifically examines pointers to huge and far data, although some of the caveats and much of the discussion on efficiency also applies to huge or far arrays. As a prerequisite for grasping the following material, you must first understand how pointers are used in C and C casts. If you really know the basics of these two topics, then the program in Figure 1 should be a snap. Before reading the next paragraph, look at the program and predict its output. The answer: 100 and 200. The first printf statement prints the number of integers in the interval between where ip points and where i is located. Since ip was assigned the location 100 integers past where i is stored, naturally the answer is 100. In the second printf statement, ip and the location of i are cast to unsigned and then subtracted. The cast doesn't change the bit patterns, but it does change the operation from a pointer subtraction to an ordinary unsigned subtraction. The second answer is 200 because the two bit patterns differ by that amount. If you ask, why 200, it's because 100 integers at 2 bytes each makes 200. The statement ip += 100 actually added 200 to ip's binary value. If you passed the quiz, then read on. If not, then you need to learn more about ordinary pointer operations before you can understand the extraordinary pointer operations of Microsoft C far and huge pointers. A pointer to far data, usually called a far pointer, is a 32-bit pointer whose arithmetic operations are performed by using just 16 bits. (See the summary in Figure 2.) A far pointer can point anywhere in the PC's address space, but the 16-bit arithmetic limit means that the memory region towards which it points must be less than 64Kb large. For instance, you could use a far pointer to access a dynamically allocated array of 25,000 integers, but you could not use that same far pointer to access an array of 25,000 floats or doubles. This is because the array of integers will take up less than 64Kb, while the array of floats or doubles will take more. A pointer to huge data, called a huge pointer, is also a 32-bit pointer, but its arithmetic operations are performed by using all 32 bits. Thus a huge pointer can point anywhere, at anything. The trade-off, of course, is that huge pointers operate more slowly and use more code than far pointers. Adding an integer to a far pointer isn't much more work than adding two integers, but adding an integer to a huge pointer is roughly twice the work of adding two integers. Far and huge pointers are sometimes used in small- and medium-model programs to access addresses outside the program's 64Kb default data space. They might be used, for example, to access the video display buffer or to access dynamically allocated memory outside the 64Kb data space. In compact- or large-model programs, all data pointers are far pointers, unless you specifically ask for a huge or near pointer. In a huge-model program, all data pointers are huge unless you specifically request a near or far pointer. When you are declaring an ordinary pointer, such as the pointer ip in Figure 1, you place an asterisk in front of the name to signify that *ip is an integer; hence ip itself must be a pointer to integer. To declare a far or huge pointer, you must use the word far or huge with the asterisk. So you would say: int far *npfi; static int huge *hp1, huge *hp2; In a declaration or a cast, the word far or huge is conceptually glued to the asterisk; that's why the word huge appears twice in the second declaration, once for each asterisk. Note that the data type and storage class, static integer in this example, appear only once in the second declaration. The pointers created in the declaration above are designed to access data anywhere in memory, but the memory space for the pointers themselves (4 bytes each) is allocated in the default data segment. The first pointer is named npfi because it is a near pointer to a far integer; the pointer itself is near data, but it points at far data. Nevertheless the vernacular term for npfi is far pointer, meaning a pointer to far data. You could create a near pointer (16-bit pointer) in the far data space, but not in the default data segment, by using the following declaration: int * far fpni; The pointer in this declaration is named fpni to remind you that it is a far pointer to a near integer; the pointer itself (2 bytes) is stored in a far data segment even though it can only point at integers inside the default data segment. This declaration cannot appear inside a procedure, not even inside main, because the declaration specifically tells the compiler to put the pointer in the far data segment, whereas automatic variables in a procedure are stored on the stack. However, adding the storage class static to the declaration would enable it to be placed inside a procedure. The words near, far, and huge are Microsoft extensions to the C language; they help Microsoft C wring the best performance out of the difficult Intel architecture. A declaration that looks like typename far * dataname creates a pointer in the near data segment to access something stored in a far data segment. Conversely, a declaration that looks like typename far dataname creates data in the far data segment. If the word far (or near or huge) is followed by an asterisk, the pointer is allocated locally, but is used to access distant data. If the word far (or near or huge) is followed by the name of a variable, then the variable itself is stored in the far (or near or huge) data segment. The Microsoft(R) C Compiler User's Guide has a chart with several examples of near/far/huge declarations. The first problem that you may encounter when using far and huge pointers in a small- or medium-model program is that the conventional pointer size for passing data pointers to procedures in these programs is 16 bits. Thus you can't pass a far or huge pointer to a procedure, such as strlen, that expects an ordinary near pointer. (If necessary, you can extract a library procedure for different models and link it in your program.) And if you write your own procedures, be careful to declare the parameter correctly as far or huge if you intend to pass it far or huge pointers. Far (or huge) and near pointers as procedure parameters simply can't be mixed. The second problem with far and huge pointers is that the C language standard (both the old Kernighan and Ritchie definition and the emerging ANSI standard) specifies that the difference between two pointers is an integer, and the result of the sizeof operator is also an integer. This can cause some problems, which the program in Figure 3 demonstrates. When the program in Figure 3 is executed with the value 10000 on the command line, the output is the following: Number of Elements: 10000 1.10000 == (unsigned)(f2-f1) 2.10000 == (long)(f2-f1) 3.10000 == (unsigned long)(f2-f1) 4.10000 == (long)(unsigned)(f2-f1) As you can see, everything looks OK. However when you increase the size of the allocation to 40000, which is a perfectly reasonable size for a character array accessed by a far pointer, the result is the following. Number of Elements: 40000 1. 40000 == (unsigned)(f2-f1) 2.-25536 == (long)(f2-f1) 3.-25536 == (unsigned long)(f2-f1) 4. 40000 == (long)(unsigned)(f2-f1) The problem in the second line of the output is that casting the result of the pointer subtraction (a signed integer) to a long is done with sign extension, thereby producing the wrong answer. The same problem could occur with a conventional array containing more than 32,767 elements, but conventional 32Kb element arrays are rare, whereas far and huge pointers exist specifically to manage large arrays. The problem in the third line is very subtle, and it took a trip or two to the Microsoft(R) C Compiler Language Reference Manual to understand the problem. A conversion from a signed integer (the result of subtracting f2 from f1) to an unsigned long first does a sign extension to long, then converts that long to unsigned long. The solution is easy: simply cast the signed integer to unsigned, and then cast that to long (the fourth line). Figure 4 is a program for exploring the pointer arithmetic behavior of huge pointers. Since a huge pointer points at a large amount of storage, there are a few more cases to consider than with far pointers. When you run the program using the value of 10000 as the number of elements, the following is printed: Number of Long Elements: 10000 1. 59152 == (unsigned)(h2-h1) 2. 10000 == (unsigned)(long)(h2-h1) 3. -6384 == l1 = h2-h1 4. 4294960912 == ul1 = h2-h1 5. 10000 == (long)(h2-h1) 6. 10000 == (unsigned long)(h2-h1) 7. 59152 == (long)(unsigned)(h2-h1) The only cases that work correctly cast the result of h2-h1 immediately into a long or into unsigned long. The Microsoft manual warns you that without a cast to long, subtracting two huge pointers will result in an int, with the disastrous results shown above. In this particular case, the cast to long has an important semantic meaning that goes beyond its usual meaning of converting the item to the given type: the cast controls the precision of the result when subtracting huge pointers. The rub is that it requires Pointer differences to be an int, but an int (on a 16-bit PC) isn't large enough. The other distressing fact that you may have noticed is that the casts that allow you to subtract two far pointers are not the same as those that allow you to subtract two huge pointers. This means you must be doubly careful when you use these seemingly similar types; watch out for subtraction. Another difficulty with both far and huge pointers is the common assumption that pointers are well behaved. With the Intel segmented architecture, many different bit patterns allow a given far or huge pointer to access a given location. Thirty-two-bit pointers have two pieces, the high 16 bits, which are loaded into a segment register, and the low 16 bits, which are an offset. In the Intel CPU, the segment register value, shifted left four, is added to the ordinary register to form a 20-bit address. Thus the pointer 0100:0000H (32-bit Intel pointers are often written with a colon in the middle for clarity) will access the location 1000H in memory. This is because the high 16 bits (0100H) are shifted left by four (01000H) and then added to the low 16 bits (0H) to produce the result 01000H. Figure 5 shows two other pointer values that will access location 1000H. The moral here is that comparing two pointers for equality may or may not work, depending on their bit patterns. The program in Figure 6 demonstrates the hazards of comparing far and huge pointers. The first printf statement will print yes or no, depending on whether the two pointers compared are equal. The second printf will print yes if the result of subtracting the two pointers is 0; otherwise it will print no. With ordinary pointers (PDP-11 pointers or 16-bit Intel pointers), the two results will always be the same. However, for two huge pointers that point to the same place, even though they have different bit patterns, the output of the program is the following: 0100:0000 == 0080:0800 ? no (long)(0100:0000 -0080:0800) == 0L ? yes This strange result, two unequal pointers that have no difference, actually makes perfect sense. The C compiler compares pointers by comparing their bit patterns, but it must subtract them more laboriously. First the compiler forms two 20-bit linear addresses just as the CPU does in hardware, then subtracts the linear addresses and divides by the size of whatever the pointer accesses. If you do conventional things with your far and huge pointers, such as move them within an allocated chunk of memory, then comparisons will work fine. However, with anything unconventional such as hand-crafting pointers, picking them up from hardware devices, or acquiring them from foreign subroutine libraries, simple comparison is not wise. You can always compare two pointers by using the expression ((long)(p2 - p1) == 0), but it takes much longer than the typical expression p2==p1. You must keep in mind that the bit value in a far or huge pointer is not a byte offset from location 0. Instead it is in the Intel segment:offset format that must be added together (see Figure 3) to form a byte offset from location 0. Most reasons for converting a pointer to a byte offset from 0 are very bad, but if you must, see Figure 7 to do it correctly. The output of the program in Figure 7 is shown in Figure 8. Conventional pointers are often cast to an unsigned, or to an unsigned long, when a byte offset from 0 is required. This will not work with a far or huge pointer, because of the segment:offset nature of the Intel architecture. The hptr2long macro is necessary to make sense of the address coded in the pointer. (The FP_SEG, FP_OFF macro calls in the hptr2long macro extract the segment and offset parts of a huge pointer. They are defined in the dos.h include file.) Now let's delve into the relative efficiency of far and huge pointers. Before looking at some assembly language code generated by the compiler for far and huge operations, let's think about what must be done in the two cases. For a far pointer, the C compiler can access something that it points towards by loading its high 16 bits into a segment register (usually ES), loading its low 16 bits into an ordinary register, and then accessing that location. For huge pointers the operation is the same. However, pointer arithmetic with far pointers is much simpler than that for huge pointers. For a far pointer, all arithmetic is done with just the low 16 bits. All the compiler has to do is operate on the low 16 bits, just as if they were an ordinary near pointer. However, for a huge pointer the compiler must first do the operation on the low 16 bits, then it must shift the carry or borrow left 12 bits, and then it must perform the operation on the high 16 bits. That's a lot of work, which is sometimes assisted by subroutine calls to library routines. The program in Figure 9 invokes the four most common pointer operations: addition of an integer to a pointer, dereferencing a pointer, comparing two pointers, and subtracting two pointers. The code comes from one of my programs that manages a huge array using near and far pointers. The #defines at the top of the program allow it to be easily compiled using near, far, or huge pointers by defining either the word FAR or HUGE on the C compiler command line. The code generated for each pointer type is shown in Figure 10. The most striking feature of Figure 10 is that the overhead for using near and far pointers is similar; the heavy penalty does not occur until you start to use huge pointers. The most burdensome operation on huge pointers is subtraction; it takes nine in-line instructions, including one subroutine call to a support routine. In my work I needed huge pointers to manage an array of 50,000 long variables. Unfortunately the program calculated too slowly, so I was forced to speed things up. In my program most calculations only access a small region of the array, small enough to be accessed with a far pointer, although the array itself is so large that it has to be managed with huge pointers. My solution was to create a routine that would convert a huge pointer into a far pointer optimally. In my application, this meant that the resulting far pointer could be incremented and/or decremented several thousand times without having the low 16 bits overflow or underflow. Meeting that condition implies that the low 16 bits should be neither too small (danger of underflow) nor too large (danger of overflow). Suppose you have the huge integer pointer 2000:0000H. If you decrement it, the compiler will decrement the low 16 bits and then propagate the borrow into the high bits, yielding the correct answer: 1000:FFFEH. However if 2000:000H is a far integer pointer and you decrement it, the compiler will simply decrement the low 16 bits, yielding a wrong answer: 2000:FFFEH. Far pointer arithmetic only breaks when there is an overflow or a borrow, so that far pointers whose low 16 bits are far from 0H and far from FFFFH (that is, in the neighborhood of 8000H) are quite safe. I wrote a routine called huge2far (see Figure 11) that rearranges a huge pointer to make its offset close to 8000H. This creates a far pointer that can access any element within a few thousand elements of where the pointer points initially. In other applications the optimal structuring of a near pointer might be different. For instance, in an application where the near pointer was only incremented (never decremented), an optional structure would make the low 16 bits as close to 0 as possible. Note that the huge2far routine doesn't work for all possible pointers. For example, there is only one pointer, 0000:0000H, that will access location 0. But in practice huge pointers always point beyond the program text and data, and for addresses in those regions the routine works as claimed. I use huge pointers in my program to manage the array, but whenever an intensive, local calculation is performed I convert a huge pointer into a far, do the calculation, and then, if the resulting pointer value must be saved, I convert the far pointer back into a huge by using a cast. It wouldn't be worthwhile to convert a huge to far to make a single array access, but if you want to sum the next thousand numbers in the array, you can convert huge to far and then use the far pointer a thousand times to access the next thousand array members. The time saved in my program is significant. The code in Figure 10 gives some additional clues about writing more efficient programs with huge pointers. Note that the most expensive operation, subtraction, is often used in loop bounds checks. Code such as the following is very common in C programs (p, q, head, and tail are pointers): while(p++<q) ∙ ∙ ∙ for(p=head;p<tail;p++) ∙ ∙ ∙ If these are huge pointers, each loop expends a lot of time comparing two pointers in each iteration. A more efficient solution would avoid the comparison on each pass through the loop, as in the following: cnt = q - p; while(p++, cnt--) ∙ ∙ ∙ for(cnt=tail-p, p = head; cnt > 0; cnt--, p++) ∙ ∙ ∙ Of course, you have to be careful that the value of p is not changed in the body of the loop. Figure 1: Understanding Pointers main() { int *ip, i; ip = &i; ip += 100; printf("ip - &i: %d\n", ip - &i; printf("(unsigned)ip - (unsigned)&1: %u\n", (unsigned)ip - (unsigned)&i); } Figure 2: Microsoft C Version 4. 0 Data Pointers Near Far Huge Size (bits) 16 32 32 Addressing Anywhere in Anywhere in Anywhere 64Kb default any 64Kb data data segment segment Pointer arithmetic 16 bits 16 bits 32 bits Microsoft C Small Compact Huge defaults Medium Large Figure 3: Pointer Subtraction /* * explore far pointers */ #include <malloc.h> #include <stdlib.h> main(c,v) char *v[]; { unsigned nelems; char far * f1, far * f2; if (c != 2) { printf("usage: %s elements_to_allocate\n", v[0]); exit(); } nelems = (unsigned)atol(v[1]); f1 = (char far *)_fmalloc(nelems * sizeof(char)); if (f1 == (char far *)0) { perror("_falloc failed:"); exit(); } f2 = f1 + nelems; printf("Number of Elements: %u\n", nelems); printf("1. %6u == (unsigned)(f2-f1)\n", (unsigned)(f2-f1)); printf("2. %6ld == (long)(f2-f1)\n", (long)(f2-f1)); printf("3. %6ld == (unsigned long)(f2-f1)\n", (unsigned long)(f2-f1)); printf("4. %6ld == (long)(unsigned)(f2-f1)\n", (long)(unsigned)(f2-f1)); } Figure 4: Pointer Arithmetic Using Huge Pointers /* * explore huge pointers */ #include <malloc.h> #include <stdlib.h> main(c,v) char *v[]; { long nelems; long huge * h1, huge * h2; long l1; unsigned long ul1; if (c != 2) { printf("usage: %s elements_to_allocate\n", v[0]); exit(); } nelems = atol(v[1]); h1 = (long huge *)halloc(nelems, sizeof(long)); if (h1 == (long huge *)0) { perror("halloc failed:"); exit(); } h2 = h1 + nelems; printf("Number of Long Elements: %lu\n", nelems); printf("1. %12u == (unsigned)(h2-h1)\n", (unsigned)(h2-h1)); printf("2. %12u == (unsigned)(long)(h2-h1)\n", (unsigned)(long)(h2-h1)); printf("3. %12ld == l1 = h2-h1\n", l1=h2-h1); printf("4. %12lu == ul1 = h2-h1\n", ul1=h2-h1); printf("5. %12ld == (long)(h2-h1)\n", (long)(h2-h1)); printf("6. %12lu == (unsigned long)(h2-h1)\n", (unsigned long)(h2-h1)); printf("7. %12ld == (long)(unsigned)(h2-h1)\n", (long)(unsigned)(h2-h1)); } Figure 5: Three Distinct Pointer Values Point to Location 1000H Pointer 0x01000000 0x00ff0010 0x00800800 High 16 bits 0x0100 0x00ff 0x80 Low 16 Bits 0x0 0x10 0x800 Intel segmentation 0x01000 0x00ff0 0x800 Address computation +0x0 +0x10 +0x800 ________ ________ __________ Result 0x01000 0x01000 0x1000 Figure 6: Comparing Far and Huge Pointers /* * equality of huge pointers */ main() { int huge *h1, huge * h2; h1 = (int huge *)0x01000000; h2 = (int huge *)0x00800800; printf("%p == %p ? %s\n", h1, h2, h1 == h2 ? "yes" : "no"); printf("(long)(%p - %p) == 0L ? %s\n", h1, h2, (long)(h1 - h2) == 0L ? "yes" : "no"); } Figure 7: Converting Pointers to a Byte Offset from 0 /* * Byte offsets of huge pointers */ #include <dos.h> #include <malloc.h> /* convert a MSC huge pointer into a byte offset from location 0 */ #define hptr2long(p) (((unsigned long)FP_SEG(p))<<4 + (unsigned long)FP_OFF(p)) #define NELEM 5 main() { int huge *hptr[NELEM]; int i; printf(" Huge Offset\n"); printf(" Pointer from 0\n"); for(i=0;i<NELEM;i++) { hptr[i] = (int huge *)halloc(32768L, sizeof(int)); printf("%p %7ld\n", hptr[i], hptr2long(hptr[i])); } } Figure 8: The output of the program in Figure 7. The first pointer, 209A:0000H, accesses byte 209A0H on the PC. Note that 209A0H is equal to 133536 decimal. Huge Offset pointer from 0 209A:0000 133536 309B:0000 199088 409C:0000 264640 509D:0000 330192 609E:0000 395744 Figure 9: Invoking Common Pointer Operations #include <malloc.h> if defined (HUGE) #define PTYPE Huge #define MALLOC halloc(NELEM, sizeof(unsighned) ) #define DTYPE long #elif defined (FAR) #define PTYPE far #define MALLOC _fmalloc(NELEM * sizeof(unsigned) ) #define DTYPE unsigned #else #define PTYPE #define MALLOC malloc(NELEM * sizeof(unsighned) ) #define DTYPE unsigned #endif main() { unsigned PTYPE *head, PTYPE *aspikes, PTYPE *end; unsigned time; DTYPE diff; head = (unsigned PTYPE *)MALLOC; aspikes = head; end = head + NELEM; while ((time = *aspikes) && aspikes++ < end) diff = (DTYPE) (end - aspikes); } Figure 10: Compiler Code Generation for the Program in Figure 9 for Near, Far, and Huge Pointers ╓┌────────────────────┌─────────────────────────┌────────────────────────┌───► C NEAR FAR HUGE end = head + NELEM; add ax,20000 add ax,20000 mov a mov [bp-6],ax ;end mov [bp-8],ax ;end cwd mov [bp-6],dx add a adc d mov c C NEAR FAR HUGE mov c shl d add d mov [ mov [ time = *aspikes mov bx,[bp-8] ;aspikes les bx,[bp-12] ;aspikes les b mov ax,[bx] mov ax,es:[bx] mov a mov [bp-4],ax ;time mov [bp-4],ax ;time mov [ aspikes++ < end mov ax,[bp-8] ;aspikes mov ax,[bp-12] ;aspikes mov a add WORD PTR [bp-8],2 mov dx,[bp-10] cwd cmp ax,[bp-6] ;end add WORD PTR [bp-12],2 add a cmp ax,[bp-8] ;end adc d mov c shl d add d mov c mov b mov [ C NEAR FAR HUGE mov [ mov [ cmp c diff = (DTYPE)(end- aspikes); mov ax,[bp-6] ;end mov ax,[bp-8] ;end pushd sub ax,[bp-8] ;aspikes sub ax,[bp-12] ;aspikes pusha sar ax,1 sar ax,1 pushW mov [bp-2],ax ;diff mov [bp-2],ax ;diff pushW sar d rcr a mov [ mov [ Figure 11: Rearranging Huge Pointers #include <dos.h> char far * huge2far(n) char huge *n; { register unsigned o; register unsigned s; unsigned d; char far *p; o = FP_OFF(n); s = FP_SEG(n); if (o < 0x7ff0) { d = (0x8000 - o) & ~0xf; o += d; s -= (d>>4); } else if (o > 0x8000) { d = (o - 0x7ff0) & ~0xf; o -= d; s += (d>>4); } FP_OFF(p) = o; FP_SEG(p) = s; return p; } ████████████████████████████████████████████████████████████████████████████ EMS Support Improves Microsoft Windows 2.0 Application Performance ─────────────────────────────────────────────────────────────────────────── Also see the related article: Glossary of Window Memory Terms ─────────────────────────────────────────────────────────────────────────── Paul Yao☼ In 1981, IBM introduced its Personal Computer, which, among other things, gave applications developers more memory to work with. You may recall that the best-selling machine at that time was the Apple(R) II, which, with its 6502 processor was able to address 64Kb of RAM memory. The IBM(R) Personal Computer was able to address a full megabyte of memory with its Intel(R) 8086 processor. Subtracting the memory reserved for the hardware, the program address space of 640Kb was ten times the available memory on the Apple computer. Suddenly, programmers and users found themselves with more memory than they knew what to do with. Eventually, users pushed the limits of 640Kb of memory: software became larger as new features were added, spreadsheets and documents grew as users became more sophisticated, and, with advances in chip technology, the price of memory dropped. What once had seemed an enormous amount of memory was no longer enough. To meet the need for more memory, the Expanded Memory Specification (EMS) was introduced in 1984 to provide applications with memory above the 640Kb limit. EMS defines the software interface for a program to access expanded memory. Using EMS, an application can store up to 8 additional megabytes of data in RAM. AST Research Inc. developed a superset of EMS, called the Enhanced Expanded Memory Specification (EEMS), which provided even greater flexibility in the way that expanded memory was mapped for use by applications. The EMS and EEMS specifications were superseded by the Lotus/Intel/ Microsoft Expanded Memory Specification Version 4.0 (LIM EMS 4.0) in August 1987. EMS 4.0 provides up to 32Mb of expanded memory, four times the 8Mb supported under EMS 3.2. By consolidating two similar, but distinct, memory specifications, LIM EMS 4.0 simplifies expanded memory support for Windows since any EMS or EEMS card can be made compatible with LIM EMS 4.0 by adding a device driver. Besides unifying the expanded memory interface, EMS 4.0 provides multitasking support for expanded memory. This article looks at how expanded memory works, how Windows 2.0 uses expanded memory, how virtual memory is implemented in Windows/386, and how expanded memory support will affect your Windows programming. Expanded Memory EMS 4.0 allows applications to go beyond the 640Kb limit by setting aside part of the 8088's 1Mb address space as a porthole into expanded memory, called a page frame. Depending on how system memory is used, page frames range in size from 16Kb to 1024Kb. Page frames are divided into physical pages, which are typically 16Kb. Expanded memory is divided into logical pages, also 16Kb in size. Applications obtain expanded memory by asking the Expanded Memory Manager (EMM) to allocate a certain number of logical pages. The application must ask the EMM to map a set of logical pages into the physical address space of the hardware in order to use them. The application then accesses expanded memory just as it would access conventional memory. To use another set of logical pages, the application asks the EMM to map in a new set of pages. An application cannot access a page when that page is not mapped in. While the physical address space of the 8088 is 1Mb, expanded memory can have up to 32Mb. Figure 1 is a memory map showing the relationship of an MS-DOS(R) application, such as Lotus(R) 1-2-3(R), to expanded memory logical pages. In this example, there are two slots in the physical address space, labeled Slot-1 and Slot-2. The two logical pages, which are labeled Page-3 and Page- 6, are currently mapped into the two slots. Note that the shaded areas are reserved for the operating system and for hardware use (where display memory is located). The single page frame resides above 640Kb, although EMS 4.0 allows expanded memory to be mapped into any portion of the physical address space. Expanded memory requires special hardware and special software. The hardware consists of a card that contains memory chips. The software is a device driver, the EMM, which processes the requests for expanded memory actions and makes the expanded memory board perform its mapping magic. Applications must allocate memory by calling the EMM, and by another call that causes the allocated page to be made accessible by hardware mapping. The third task that applications must perform is to free allocated expanded memory once it is no longer needed. These three steps are analogous to the way that memory is allocated from the Windows memory manager: memory is allocated (either with GlobalAlloc or LocalAlloc), it is mapped into a physical address (via GlobalLock or LocalLock), and it is released when no longer needed (by GlobalFree or LocalFree). The fourth step, unlocking memory (with GlobalUnlock or LocalUnlock), is analogous to the unmapping of expanded memory pages, which in EMS is part of the mapping call. Some EMS boards allow only a single 64Kb page frame, which must physically appear above 640Kb. Other boards permit multiple page frames with no limit on the size or location of the page frames. Even though an EMS 4.0 driver can be written for either type of board, their capabilities require that Windows deal with each a little differently. Older boards leave the memory below 640Kb untouched and provide a relatively small page frame. Newer boards can be configured to take over everything above 256Kb, allowing total bank space to exceed 600Kb. As I will discuss later, the size of the available page frame affects how the Windows memory manager uses expanded memory. I will refer to old-style EMS support, which allows only a single page frame, as "small frame" EMS and to the new-style EMS support, which permits multiple page frames, as "large frame" EMS. Note that the presence of terminate-and-stay-resident programs, network device cards, and other dedicated memory applications, may cause Windows to use a newer EMS card in "small frame" EMS mode. The division between the bankable and the nonbankable portions of a given EMS memory configuration is called the "bank line." Knowing where objects will be allocated ("above the bank line" or "below the bank line") will not affect you if you are writing a single, standalone Windows application. But if you develop applications that "talk" to each other, write your own dynamic-link library, use Windows hooks, or wish to use the clipboard or DDE, you should familiarize yourself with the location of memory objects relative to the EMS bank line. The mapping and unmapping of logical expanded memory pages does not require copying large blocks of data. Instead, it involves modifying a few mapping registers. Once these mapping registers are changed, memory mapping automatically occurs on the expanded memory card and is thus fast and efficient, with little performance degradation. Expanded Memory and Windows Windows Version 2.0 uses expanded memory to bank Windows applications. This expanded memory support lets multiple large Windows applications, such as Microsoft Excel and Aldus PageMaker(R), run simultaneously and as fast as if each were running in Windows by itself. Windows 1.x uses expanded memory to swap typical MS-DOS applications, but not for swapping Windows applications. Windows 2.0 will support these older applications, but with the advantage of banking Windows applications in expanded memory. Memory is the most severely constrained resource in Windows. There are two reasons for this: one due to the hardware, and the other related to the multitasking nature of Windows. The hardware constraint is simply that the address space of the 8086 family of processors is 1Mb. The multitasking nature of Windows means that the demand on memory is potentially unlimited, as more and more applications are run. This problem is partially solved in the design of Windows, which provides a type of virtual memory support. Unnecessary code and resource segments marked "discardable" and which are "least recently used," can be destroyed on demand, freeing up memory. Of course, when the destroyed objects are needed again, Windows will re-read them from the disk (code and resource segments in memory are "read-only," which means that the disk image is guaranteed to be correct). The memory problem is not entirely solved by discarding. Each application has a minimum memory requirement, so that two or more large applications may not be able to run simultaneously. This is a problem because users will most likely want to use the large, powerful applications together, to cut and paste data, initiate DDE conversations, and in general to take advantage of Windows' ability to move quickly between applications. Windows 2.0 solves the problem with its enhanced memory manager, which will bank each application into its own set of expanded memory pages, so that each application is, in effect, running on the machine by itself. This works as long as an EMS card and an EMS 4.0 driver are present. The banking of application code and data is entirely transparent to Windows programs. The Windows memory manager performs all required handshaking with the EMM to allocate memory, map and unmap pages, and to free EMS memory when an application is closed. The memory manager could have been enhanced to page in EMS memory so as to give a single Windows application the full 32Mb of memory that EMS 4.0 supports. The current design was chosen, however, for implementation considerations, performance goals, and the availability of alternative solutions. One implementation consideration in favor of this design is the need for Windows 2.0 to be backwards compatible with Windows 1.x applications. This means that the interface to the memory manager must be the same. To make use of EMS memory, a new interface would be required. However, this interface already exists──in the form of the EMS 4.0 specification. Therefore, if an application has a need for megabytes of memory, it can talk directly to the EMS memory manager. The second reason for the current design is to meet the main performance goal for supporting expanded memory: allowing several large Windows applications to run simultaneously, with minimum overhead incurred at context-switching time. The current implementation insists only that each application be able to run with acceptable performance in 640Kb. After all, not every computer will have expanded memory, and thus Windows' developers cannot assume its existence, except, perhaps, in vertical-market packages where a turnkey system was being developed. The third reason that Windows EMS support does not provide megabytes of expanded memory for applications is that a disk cache serves the same purpose. Code, resource, and other discardable segments can be destroyed and quickly re-read from the disk cache using the loading and discarding capabilities of the memory manager. Windows 2.0 includes a disk cacher, which dynamically allocates and frees EMS pages as needed. Unlike a RAM drive, optimum performance of the disk cache does not depend on a user decision. Windows 2.0, unlike the earlier versions, will run in the compatibility box of OS/2. Even in this mode, Windows will use expanded memory. How EMS Support Works The Windows memory manager is responsible for controlling the dynamic allocation of memory from the global heap, as well as allocation from each task or library's local heap. Here, we are only interested in memory allocated from the global heap. The Windows memory manager arbitrates requests for blocks of memory, handling all the adminstrative overhead when blocks must be moved or discarded to satisfy allocation requests. When a block of memory is freed, that is, returned to the available pool of memory, the Windows memory manager makes a note of this. Windows pages in a fresh bank of EMS memory for every application that is loaded. Figure 2 is a memory map showing three Windows applications; Write, PageMaker, and Microsoft Excel, each in its own bank of expanded memory pages. In Figure 2, the Microsoft Excel pages are all currently mapped into physical addresses. Note that only the minimum number of expanded memory logical pages are allocated for a given application, so that expanded memory can be used as efficiently as possible. Figure 2 shows that all of the pages in Microsoft Excel are mapped in. This happens whenever Microsoft Excel is processing, or, in Windows terms, whenever Microsoft Excel receives a message via the call to GetMessage. If the user clicks on the PageMaker window, a context switch makes PageMaker the active application. During context switching, the memory manager will bank in all of the PageMaker expanded memory pages, causing all of the Microsoft Excel pages to be banked out. This has implications for sharing memory among applications, which will be discussed later. While the application is processing, the allocation of memory banks remains fixed. This means that Windows applications are still running in 640Kb (actually up to 256Kb more, or 896Kb, with a 64Kb EGA card installed), and should be written to obtain satisfactory performance within this memory. The only objects that will be discarded are those that appear in the physical address space, that is, in either the currently mapped EMS memory or below the bank line. Memory objects that are banked out are never discarded. Memory Protection When an application's EMS memory is not mapped into the physical address space, it is protected from other "misbehaving" applications. When Windows is running with large frame EMS, that is, when expanded memory pages can be mapped into any part of the physical address space, each application is completely protected from others. When small frame EMS is running, only those code segments that are in EMS memory are protected. Although this memory protection is not as complete as that of OS/2, in which illegal addressing results in a general protection fault, it does reduce the damage in a way that Windows 1.x did not. Memory Managers While Windows is running with EMS memory, there are really two memory managers present: the Windows memory manager and the EMM. The Windows memory manager is the primary memory manager under Windows 2.0, calling on the EMM only to control expanded memory. Its task is to allocate, map and unmap, and free expanded memory, plus keep track of the EMS registers. Because there are two separate memory managers, a Windows application can call the EMM directly once it knows that EMS is present, to allocate memory on its own. One thing you'll want to know is which objects are allocated above the bank line and are therefore bankable, and which are allocated below the bank line and are thus shareable. This discussion is complicated by the fact that EMS 4.0 supports two types of page frames: small and large. The small page frame for EMS memory is 64Kb, while the large page frame for EMS memory can be up to 896Kb, given the proper hardware setup. Because almost the entire address space above 256Kb can be banked by using the large page frame, it offers the most flexibility to the Windows memory manager. Figure 3 shows how expanded memory is used for both types of page frames. More than one instance of a given application, in the current implementation, will share a given bank (although each instance will have its own data segment), thus preventing any problems in the sharing of instance data, either explicitly through GetInstanceData or implicitly through sharing memory handles. Future versions may offer developers the option of requesting a separate EMS bank for each instance of an application. Certainly large frame gives the memory manager the most flexibility, but even with small frame there are significant advantages to EMS support. For small applications, all of the code and resources will fit into banked memory, allowing many small applications to run with little performance degradation. Applications like the control panel, the calculator, or the calendar can be loaded together with a large application such as Microsoft Excel with a minimum of interference. Small frame EMS support, then, allows the user to take better advantage of the multitasking abilities in Windows. Dynamic-Link Libraries When small frame EMS is present, only the resources and code segments from a task are loaded into the EMS banks. With large frame EMS the memory manager places several other types of objects above the line in the EMS banks. These include task data segments, library discardable code segments, and memory allocated with GlobalAlloc. These last two deserve some attention because they raise important implementation issues. Dynamic-link libraries are accessible from any task. Examples of dynamic- link libraries under Windows include Kernel, User, and GDI, as well as all device drivers (keyboard, mouse, timer, display, and printer). Clearly, library routines, such as the GDI routine TextOut, must be accessible from any task at any time. When large frame EMS is present, the code in dynamic- link libraries is handled in one of two ways: fixed code segments live below the line, discardable code segments live above the line. Large frame EMS support places library discardable segments above the line in the bank of the calling task, creating a possibility that at any time a given library code segment may appear in multiple banks. For example, multiple copies of the dialog box control manager, which contains the window procedures for the edit, push button, and other dialog box controls, might concurrently reside in EMS memory. Although there is redundancy, the net result is that applications have access to the library segments that they need, and little room is taken up in the scarce portion of memory below the bank line. Ordinarily, when dynamic-link libraries allocate memory with GlobalAlloc and a large EMS frame is present, the allocation occurs above the line. There are times, however, when it is necessary for a library to have its global memory below the line. Some libraries may have global memory objects that they need to access at all times. In such a case, the GMEM_NOT_BANKED flag is used in the GlobalAlloc call. Windows 1.x printer drivers run into compatibility problems because they do not use the GMEM_NOT_BANKED flag. Printer drivers tend to be the only libraries that are loaded via the LoadLibrary routine; other libraries are loaded as "dependent modules" of tasks. (A dependent module is a library that must be present for a task to execute.) Libraries loaded with LoadLibrary allocate global memory below the EMS bank line, while dependent modules, by default, allocate global memory above the bank line. Changes to the API Only a single routine, LimitEMSPages, has been added to the Windows API. This routine allows an application to control the number of EMS pages that Windows allocates on its behalf. It does not, however, limit the number of pages that an application can allocate for itself from the EMS memory manager. When you want to run Windows with EMS memory disabled, put the /n switch after the WIN command at the DOS prompt: C> win /n Virtual Memory The biggest advantage of Windows/386 is its support of standard MS-DOS applications. The current implementation of Windows/386 creates multiple virtual machines (VMs), each of which behaves as an independent 8086. Because each is effectively given its own 8086 machine with 640Kb of address space, applications can run independently without interfering with each other. Also, because of the ability of the Intel 80386 to intercept direct memory mapped video output, each MS-DOS application can run in its own window on the screen. Note, however, that Windows/386 does not give each Windows application its own 640Kb partition; instead, all Windows applications run in the same 640Kb partition. This means that Windows applications do not have the same memory advantages that MS-DOS applications have running under Windows/386. Memory banking support in Windows/386 is the same as EMS support in Windows 2.0. Although it works a little differently, because the 386 has hardware support for memory management, the differences will be transparent to the Windows application. Programming Guides The programming guidelines listed here should be no surprise to Windows programmers, who have been told to be good citizens since they started writing their first window procedure. It is useful to reiterate them, however, because Windows EMS support under Windows 2.0 and virtual memory support under Windows/386 require that you follow them strictly. These guidelines will apply to both, but a few are specific to the addition of EMS support and will help you ensure that your applications run in both Windows 2.0 and Windows/386. Efficient Memory Use There are no current plans for Windows memory management to allow Windows applications to allocate past the 1Mb physical address space of the 8086. Instead, Windows performs bank switching of memory, so each application behaves as if it had the entire computer to itself. The Windows memory manager lets you specify that memory allocated from the global heap, when EMS is running, be below the bank line, so that it is always visible from all applications. You do this with the GMEM_NOT_BANKED flag to the GlobalAlloc call. You should regard this as a method of last resort for sharing memory, because memory below the line is very scarce and is only used for the most critical system objects. Unless you have a very compelling reason, do not perform this type of allocation. A good example of a global allocation that must be below the line is the allocation required by a printer driver. Code Structure To optimize the performance of your application with small frame EMS, mark the most important segments as PRELOAD in the module definition (.DEF) file, and list their names at the beginning of the SEGMENTS list. Performance is improved because, when Windows starts loading an application into memory, it starts with the bankable EMS memory. Only after the EMS bank is filled does Windows start using space below the line. Performance improves with small frame EMS because the memory manager will not discard objects from above the bank line. Data Sharing The clipboard, DDE, and libraries are methods of data sharing guaranteed to work in current and future versions of Windows. Other than the clipboard and DDE, you should not share memory by passing global memory handles between applications. Passing long pointers to data objects should also be avoided. You can't be sure that the target data will be banked in when the receiving application is processing. Applications that ignore this guideline will not run under planned versions of Windows 2.0 or Windows/386. Be sure to copy the data from the clipboard into your own data areas before you close and release the clipboard. Windows supports EMS by copying clipboard objects that have been banked out into the currently banked memory. After the clipboard is closed, these objects will be destroyed, rendering them inaccessible to the receiving application. When an application receives a global handle through the clipboard or DDE, it must check the return value to the GlobalLock call. The Windows memory manager copies into the current bank those DDE or clipboard objects that reside in unbanked EMS memory. If the memory manager is not able to perform this copying (such as in a low memory situation), it informs the receiving application by returning null to GlobalLock. Code Sharing One innovation of Windows is its ability to share code. Only a single copy of any given Windows routine is loaded into memory at any given time. It can be accessed, via the dynamic linking mechanism, by several applications. Window procedures are shared among multiple instances of windows and fine- tuned through subclassing. If you write well-behaved Windows applications, code sharing by EMS memory is not affected: since several instances of an application run in the same bank of EMS memory, there is no problem in sharing code, data, or resources. Dynamic-link libraries are one method for sharing code among several cooperating applications. Windows loads library discardable segments above the bank line, so there can be multiple copies of some code segments in EMS memory. This redundancy can be avoided by converting the library into a windowless task and communicating with the calling applications via the various message posting routines (PostMessage and PostAppMessage). You can never execute code that belongs to another task by calling it directly. In Windows routine terms, you can never call GetProcAddress on another task in order to directly call the target code yourself. Although it worked in Windows 1.x, the Windows 2.0 memory manager won't let you do this. Instead, use the SendMessage routine to perform intertask calls. An application can directly call the EMM to allocate EMS memory, provided it follows EMS 3.2 guidelines; it may use the reallocation function (function 17) from LIM EMS 4.0. Windows must be notified so that it does not try to use the 64Kb EMS 3.2 page frame; this is achieved by adding the switch -LIM32 to the call to the resource compiler. Windows avoids putting code in the 64Kb EMS 3.2 window for your application, but it can still use this page frame to bank other applications. Global Memory You should not allocate global memory below the line, that is, you should not use the GMEM_NOT_BANKED switch to GlobalAlloc except as a final resort. When global memory objects are shared among applications, the call to GlobalLock takes care of copying the appropriate blocks of memory, as necessary. Of course, this should only be used with the clipboard and DDE methods to guarantee compatibility with future versions of Windows 2.0, most notably, Windows/386. Your program should always check for failure in allocating memory, reallocating memory, and locking memory. This is critical when using DDE or the clipboard to share data, and it is generally a good Windows programming practice. An invalid far pointer can be deadly, since it allows data to be written or read from anywhere in the computer's address space. Windows Hooks Windows hooks allow you to trap certain types of events before they are placed into the system queue. For example, you can write a keyboard hook to watch for the <Alt>-P key, which may cause a background screen capture program to print the screen on the default printer. The impact of EMS memory on Windows hooks requires some extra programming effort on your part. First of all, Windows hooks must appear below the bank line and should be placed in fixed library seg-ments. Also, because a window hook has no way of knowing whether the initiating task is currently banked in, it must communicate with the initiating task via messages (most likely SendMessage). Debugging Support You can now use CodeView(R), Microsoft's premier debugging tool, to debug Windows 2.0 applications. This is a welcome improvement over Symdeb, a powerful but cryptic debugging tool that appeals mostly to assembly language programmers. CodeView requires either a monochrome monitor or a dumb terminal, because when Windows is running it takes over the screen. Another hardware requirement for using CodeView is that you must have EMS memory installed since CodeView talks directly to the expanded memory manager to minimize the amount of memory taken from the program being debugged. To facilitate debugging when EMS is present, you may want to place the following switch in your WIN.INI file: [kernel] EnableEMSDebug=1 which allows you to set breakpoints within discardable library code. Recall that there can be multiple copies of discardable library code segments, each in a different EMS bank. This switch tells the memory manager to update the debugger's table of library discardable segments during every context switch. Any debugger that works with Windows, including Symdeb, CodeView, Answer, or Atron will work properly with EMS. Conclusion For the most part, expanded memory support under Windows will be transparent to the programmer. With the exception of a single new Windows routine, LimitEMSPages, a few flags, and resource compiler switches, the interface that Windows programmers work with has not changed. What has changed, however, is that there must be stricter adherence to interactions between tasks. Data sharing, for example, must be performed with one of the three supported methods: the clipboard, DDE, or via a shared dynamic-link library. Directly calling into another task's code, which is never a good idea, is not possible with the Windows 2.0 memory manager. Programmers will benefit most from the improved performance of Windows 2.0 when running several large applications, which will make products that interact with applications like Microsoft Excel and PageMaker more attractive to users. Windows programmers can also, of course, write their own large, powerful applications. Because the EMS support is transparent to the Windows programmer, he or she can use the existing programming interface and let the Windows memory manager take advantage of EMS. Figure 1: This memory map shows an MS-DOS application such as Lotus 1-2-3 using EMS 3.2 expanded memory. Physical Address Logical Pages of Space Expanded Memory 1024Kb┌─────────────────┐ ┌─────────────────┐ │ System ROM │ │ L-0 │ 960Kb├─────────────────┤ ├─────────────────┤ │ Unused │ │ L-1 │ ┌──────────┬832Kb├─────────────────┤ ├─────────────────┤ │EMS 3.2 │ │ P-0 ├────┐ │ L-2 │ │Page Frame│ ├─────────────────┤ │ ┌─├─────────────────┤ │made up of│ │ P-1 ├──┐ └►│ │ L-3 │ │four 16Kb │ ├─────────────────┤ │ └─├─────────────────┤ │physical │ │ P-2 │ │ │ L-4 │ │pages. │ ├─────────────────┤ │ ├─────────────────┤ │ │ │ P-3 │ │ │ L-5 │ └──────────┴786Kb├─────────────────┤ │ ┌─├─────────────────┤ │ EGA Memory │ └──►│ │ L-6 │ 640Kb├─────────────────┤ └─├─────────────────┤ │ │ │ L-7 │ │ │ ├─────────────────┤ │ MS-DOS │ │ L-8 │ │ Application │ ├─────────────────┤ │ Program │ │ L-9 │ │ │ ├─────────────────┤ ├─────────────────┤ │ L-10 │ │ Expanded Memory │ ├─────────────────┤ │ Manager │ │ L-11 │ ├─────────────────┤ ├─────────────────┤ │ MS-DOS │ │ L-12 │ └─────────────────┘ ├─────────────────┤ │ L-13 │ ├─────────────────┤─┐16Kb │ L-14 │ │logical ├─────────────────┤─┘page │ L-15 │ ├─────────────────┤ │ ∙ │ │ ∙ │ │ ∙ │ └─────────────────┘ Figure 2: This sample memory map shows Write, Pagemaker, and Microsoft Excel code and data segments loaded into EMS memory. The Micro- soft Excel segments are currently mapped into the physical address space of the CPU. ┌────────────┐ ┌──────────────┐ ┌───────────────────┐ │ Banked ├───────┤ System ROM │ │ Write │ └────────────┘ 960Kb └──────────────┘ ├───────────────────┤ ┌────────────┐ ────────────── │ Write │ │ 12-16Kb │ ────────────── ├───────────────────┤ │ physical ├─────── ────────────── │ Write │ │ pages │ ────────────── ┐ ├───────────────────┤ └────────────┘ ────────────── ├────┐ │ PageMaker │ ┌────────────┐ 768Kb ┌─────────────┐┘ │ ├───────────────────┤ │ Not ├────────┤ EGA │ │ │ PageMaker │ │ Banked │ │ Memory │ │ ├───────────────────┤ └────────────┘ 640Kb └─────────────┘ │ │ PageMaker │ ┌────────────┐ ────────────── │ ┌─├───────────────────┤ │ 24-16Kb │ ────────────── │ │ │ Microsoft Excel │ │ physical ├─────── ────────────── │ │ ├───────────────────┤ │ pages │ ────────────── ┐ └►│ │ Microsoft Excel │ └────────────┘ ────────────── ├──┐ │ ├───────────────────┤ ─ Bank Line ─ 256Kb ┌─────────────┐┘ │ │ │ Microsoft Excel │ ┌────────────┐ │ │ │ └─├───────────────────┤ │ │ │ │ │ │ Write │ │ │ │ Windows │ │ ├───────────────────┤ │ │ │ Shareable │ │ │ Write │ │ │ │ Memory │ │ ├───────────────────┤ │ │ │ │ │ │ PageMaker │ │ Not │ │ │ │ ├───────────────────┤ │ Banked │ ├─────────────┤ │ │ PageMaker │ │ ├────────│ Windows │ │ ├───────────────────┤ │ │ ├─────────────┤ │ │ PageMaker │ │ │ │ Expanded │ │ ┌─├───────────────────┤─┐16Kb │ │ │ Memory │ │ │ │ Microsoft Excel │ ├logical │ │ │ Manager │ │ │ ├───────────────────┤─┘page │ │ │ │ └──►│ │ Microsoft Excel │ │ │ ├─────────────┤ │ ├───────────────────┤ │ │ │ MS-DOS │ │ │ Microsoft Excel │ └────────────┘ └─────────────┘ └─└───────────────────┘ Physical Logical Pages of Address Expanded Memory Space Figure 3: Expanded Memory is Used for Both Types of Page Frames Always Above below the ┌──the line───┐ line Small Large Frame Frame Task databases √ Module (EXE) headers √ Library data segments √ Library fixed Code √ Thunks √ Library resources √ Task data segments √ √ Task Resources √ √ Task data segments √ Library discardable Code √ Dynamically Allocated Memory (via GlobalAlloc) √ ─────────────────────────────────────────────────────────────────────────── Glossary of Windows Memory Terms ─────────────────────────────────────────────────────────────────────────── Bank line - Also called the EMS swap line, it is the memory address below which memory is not banked and above which memory is banked. When Windows is running with expanded memory, the nonbanked memory is always available, while the banked memory is only available when the task that owns the banks is running. Banked memory - Memory that is made to appear within (mapped into) the physical address space of the 8088 by means of a software driver (the Expanded Memory Manager) working with an expanded memory card. Context switch - The changes that Windows must make when the user turns his attention from one Windows application to another. For example, suppose both WindowsWrite and WindowsPaint are open. If the user has been entering text into WindowsWrite and then clicks the mouse in WindowsPaint, the system performs a context switch. The only thing the user sees is that the caption bar changes color. If the newly active window was covered by another overlapping window in Windows 2.0, it is brought to the top. Discardable memory object - One of the flags that the Windows memory manager sets to indicate various characteristics of the memory object when it allocates memory. When the Windows memory manager is unable to satisfy allocation requests by simply moving memory objects, it will throw away discardable memory objects on a least-recently-used basis. Dynamically allocated memory - The memory that a Windows program directly obtains from the Windows memory manager. In this article, the term is synonymous with globally allocated memory. Enhanced Expanded Memory Specification (EEMS) - An extension to the Expanded Memory Specification that allows any part of the 8088 address space to be banked. Expanded Memory Specification (EMS) - This specification, also known as LIM EMS, defines how a program allocates and uses expanded memory. This consists of calls to the expanded memory device driver (see Expanded Memory Manager). Expanded Memory Manager (EMM) - A device driver that controls expanded memory. The EMS defines how a program communicates with the EMM to allocate, map, and free logical pages of expanded memory. Extended memory - The memory above 1Mb on a 80286- or 80386-based microcomputer. Memory of this type can only be directly addressed when running in protected mode, such as in OS/2. Ordinarily, extended memory is used for RAM disks and print spoolers. Most memory cards can be configured for expanded memory, extended memory, or both. Handle - A token that is returned when an object is created. Handles identify Windows' dynamically allocated memory objects. Logical page - A segment of expanded memory. EMS memory is allocated in logical pages, which are typically 16Kb. In order to use data stored in a logical page, EMS must map the logical page into a physical page. Mapping - The process by which a logical page of expanded memory is made to appear in a physical page, that is, within the address space of the 8088, so that programs can read and write to the memory. Old application support - The ability of Windows to execute standard, character-based MS-DOS applications, such as Microsoft Word, Lotus 1-2-3, or Microrim(R) R:BASE(R) System V. Page frame - A collection of physical pages into which expanded memory logical pages can be made to appear. Physical page - The absolute physical space in the 8088's 1Mb of address space into which EMS logical pages are mapped. Thunk - A small piece of code that sits in a fixed place in memory to intercept the calls to a routine located in a moveable or discardable code segment. Thunks are an important part of the dynamic linking mechanism of Windows, allowing the memory manager to move or discard code segments with relatively little overhead expended towards notifying the prospective callers of a routine that the routine is no longer resident. Subsequent calls cause the thunk to reload the segment containing the routine. Return thunks are created to intercept a return to a discarded code segment. Thunks also perform context switches; this is what the thunk created by MakeProcInstance does to ensure that a dialog box procedure is using the correct data segment. Window - Another term for an EMS page frame. Windows memory manager - The part of Windows that loads code and data segments into memory. The Windows memory manager attempts to satisfy dynamic allocation requests by first moving memory objects; if this is not sufficient, it then discards memory objects that are marked as discardable. The memory manager in Windows 2.0 has been extended to bank-switch memory between Windows applications. ████████████████████████████████████████████████████████████████████████████ LIM EMS 4.0: A Definition For the Next Generation of Expanded Memory Marion Hansen and John Driscoll☼ Back in the bad old days, if your spreadsheet program returned a memory- full error, you were out of memory──and out of luck. All that changed in 1984, when Lotus Development Corp., Intel Corp., and Microsoft Corp. smashed the 640Kb conventional memory barrier with their Expanded Memory Specification (EMS). The EMS allowed the creation of another kind of memory: expanded memory. Expanded resides on one or more add-in boards in the computer (Intel's Above(TM) Board and AST's Advantage!(TM) are examples). The EMS defines a segment of memory, located within the first 1Mb of memory, as the page frame. The page frame is a window into expanded memory. Just after an application program starts executing, it can allocate a certain number of 16Kb pages of expanded memory for its own use. By mapping pages in and out of the frame, the program can access any area of expanded memory it has allocated for itself. MS-DOS(R) cannot access expanded memory directly; instead, the job of handling the extra memory goes to the Expanded Memory Manager (EMM), which is defined by the EMS. EMM functions can be added to any program to enable it to use expanded memory. (See "Expanded Memory: Writing Programs that Break the 640Kb Barrier," MSJ, March 1987, for full details on expanded memory and how to write programs that use it.) To avoid confusion, remember that EMS is the Expanded Memory Specification, and that EMM is the Expanded Memory Manager. EMS (the spec) defines the EMM (the manager), and the EMM provides the functions that application programs need to use expanded memory. In the fall of 1987 Lotus, Intel, and Microsoft released the latest EMS, Version 4.0. This article describes the enhancements to EMS 4.0 and how to use them for even greater expanded memory performance. New Feature Set Intel sent a free EMM 4.0 upgrade to all registered Above Board users. Except for faster execution of expanded memory tests at power-on, however, users will not see improved performance with their existing programs until they have access to new application programs written specifically to the EMS 4.0 spec. Even so, certain features of EMS 4.0 have generated excitement in the expanded memory world: ■ With AST's endorsement, EMS 4.0 is now the expanded memory standard for the personal computer industry. ■ The 15 new functions defined by EMS 4.0 provide faster performance and more efficient use of memory. ■ EMS 4.0 supports a full 32Mb of expanded memory; earlier versions only supported up to 8Mb. The OS/2 systems don't run on 8088- or 8086-based computers. EMS 4.0 extends the useful life of existing MS-DOS-based applications on 8088-, 8086-, 80286-, and 80386-based computers. ■ Many software vendors are already updating their programs (such as Lotus(R) 1-2-3(R) Version 3 and Microsoft Windows 2.0) in order to take advantage of EMS 4.0's new features. ■ Software written for earlier versions of the EMS is compatible with EMS 4.0. ■ EMM 4.0 runs on existing hardware; you don't need new expanded memory boards to install EMM 4.0. However, not all expanded memory boards currently support every enhancement to EMM 4.0. For example, the AST RAMpage!(R) board and the Intel Above Board 2 support placing the page frame in memory below 640Kb, while the Intel Above Board 286 currently does not. EMS 4.0 Enhancements The enhancements to EMS 4.0 improve performance and memory efficiency and make it easier to write programs that use expanded memory. ■ With EMM 4.0, one calls maps in all logical pages that can be physically mapped to one page frame; earlier versions required each page to be mapped separately. ■ New functions allow application programs to dynamically increase and decrease the amount of expanded memory allocated to them. With each program using just the amount of expanded memory it needs, multiple programs can share expanded memory more efficiently. ■ Now you can name handles-the values that the EMM assigns and uses to identify a block of memory requested by an application program-so that data and code can be shared by application programs. ■ EMS 4.0 adds special functions for operating systems (such as MS- DOS) and environments (such as Windows and Desqview) to allow programs to isolate themselves from all other software in the system, thus providing a safe environment for programs sharing expanded memory. ■ New EMS functions directly support executing code in expanded memory. Earlier versions supported the execution of code in expanded memory, but implementation was cumbersome. For example, before EMS 4.0, terminate-and-stay-resident (TSR) programs required commands to explicitly map the new context into expanded memory when the TSR program was called up and to restore the previous context when the TSR returned control to the application program. Now the job of mapping and restoring contexts can be handled directly with EMS 4.0 functions, which allows for easier and more efficient execution of code. ■ Earlier EMS versions put the page frame in an unused 64Kb block of memory between 640Kb and 1Mb. EMS 4.0 supports the page frame anywhere in the first 1Mb of memory. Hardware may limit the location of the page frame-in an 80286-based computer such as an IBM(R) PC/AT(R), for example, the page frame can reside only between 256Kb and 1Mb. For an 80386-based computer or an IBM PS/2(TM) computer with the Micro Channel(TM), on the other hand, the page frame can be anywhere in the first 1Mb of memory. ■ Before EMS 4.0, the page frame held four pages. Now you can define a page frame of up to eight pages in memory above 640Kb. The size of the page frame in memory below 640Kb is limited only by the amount of available memory. (In some implementations of the EMM, you can use all 640Kb for the page frame by putting the EMM itself into expanded memory.) ■ Although the standard size of expanded memory pages is 16Kb, some expanded memory boards use smaller pages. EMS 4.0 supports both 16Kb pages and smaller-sized pages. The Page Frame Although EMM 4.0 supports locating a page frame in any available memory between 0Kb and 1Mb, how you plan to use the page frame determines whether you'll place it above or below 640Kb. For application programs, continue to locate the page frame above 640Kb because existing applications may not work when the page frame is located below 640Kb. Locate page frames used by most operating systems and environments in memory below 640Kb. (An exception to this is the Windows/386 EMM, which cannot use a page frame located below 640Kb.) In any case, you cannot create a page frame in memory below 640Kb unless you first create a page frame in memory above 640Kb. New EMM Parameters The LIM EMM 4.0 device driver has new parameters, one of which is of special interest to users who are concerned with speed and efficiency. The handle count parameter lets you tell the EMM to support as many handles as a particular application program needs. (A handle is a value that the EMM assigns and uses to identify a block of memory requested by an application program.) The EMM allocates memory based on the number of handles requested by the application. By specifying a small handle count, you can save conventional memory and allow the EMM to run faster. The EMM 4.0 handle count default is 64 handles; programs written for earlier versions use a maximum of 32 handles. If you are using older programs with EMM 4.0, consider changing the EMM handle count to 32 for faster execution. The maximum number of handles is 254. The EMM Functions The EMM functions provide the tools application programs need to use expanded memory. EMS 4.0 doubled the number of EMM functions. Figure 1 lists all 30 functions; the new functions are shaded. Functions 16 through 30 are new to EMM 4.0. This section describes each new function. Functions 26, 28, and 30 are for use by operating systems (such as MS-DOS) and environments (Windows and Desqview, for example); the rest are for application programs. Program Functions Get/Set Partial Page Map (Function 16) handles situations Functions 8, 9, and 15 cannot handle. Functions 8 and 9, respectively, handles only four- page page frames and can save and restore expanded memory pages only when the page frame is located between 640Kb and 1Mb. Function 15 saves and restores all the pages (no matter how many pages are in the page frame), even pages located below 640Kb. Function 16 also saves and restores all pages anywhere between 0 and 1Mb, but, unlike Function 15, it lets you choose the pages you want to save or restore. Function 16 has three subfunctions. The Get Partial Page Map subfunction saves a partial mapping context for specific mappable memory regions in a system. This subfunction can be faster than Function 15 because it saves only a subset of the entire mapping context and uses much less memory for the save area. You can use the Get Partial Page Map subfunction with or without a handle, while Function 8 requires a handle. The Set Partial Page Map subfunction restores a partial mapping context for specific mappable memory regions in a system. Like Get Partial Page Map, this subfunction uses far less memory for the save area than Function 15, and you can use it with or without a handle. The Get Size of Partial Page Map Save Array subfunction returns the storage requirements for the array passed by the Get/Set Partial Page Map subfunctions. Use this subfunction before the other two subfunctions. Map/Unmap Multiple Handle Pages (Function 17) maps or unmaps (in one invocation) logical pages into as many physical pages as the system supports. This means less overhead than mapping pages one at a time (as required by EMS versions before 4.0). Use this function instead of Function 5 for application programs that do a lot of page mapping. Reallocate Pages (Function 18) allows an application program to dynamically increase or decrease the number of logical pages allocated to an EMM handle, so programs can share expanded memory more efficiently. Function 18 does not determine the number of pages a program needs; rather, the program itself must specify the number of pages to allocate or deallocate (depending on its needs at any one time). Get/Set Handle Attribute (Function 19) defines a handle as volatile or nonvolatile. If it is nonvolatile, the handle, its name (if it has one), and the contents of the pages allocated to it are maintained after a warm boot. For example, this function saves the contents of a RAM disk in expanded memory after a warm boot. Function 19 is not supported by most hardware because it disables memory refresh signals for a long period of time. Function 19 has three subfunctions. The Get Handle Attribute subfunction returns the attribute (volatile or non-volatile) associated with a handle. The Set Handle Attribute subfunction changes the attribute associated with a handle. Before using Function 19's other two subfunctions, you can use the Get Attribute Capability subfunction to determine whether the EMM supports the non-volatile attribute. By naming the handle associated with a block of memory, Get/Set Handle Name (Function 20) lets application programs share the same area in expanded memory associated with a handle. Assigning a name to a handle also protects the memory specified by the handle because only a program that knows the handle name can get that handle and use the memory assigned to it. Function 20 has two subfunctions. The Get Handle Name subfunction gets the eight-character name assigned to a handle. The Set Handle Name subfunction assigns an eight-character name to a handle. Get Handle Directory (Function 21) has three subfunctions. The Get Handle Directory subfunction returns an array which contains all active handles and their associated names (if any). The Search for Named Handle subfunction searches the handle name directory for the specified handle name. If it finds the name, the subfunction then returns the handle number associated with the name. An application program is able to use this subfunction to determine if a shared handle exists. The Get Total Handles subfunction returns the total number of handles that the EMM supports. (Different versions of EMM support different numbers of handles. EMM 4.0 supports up to 254 handles; earlier versions support just 32). Functions 22 and 23 make executing code in expanded memory easier and more efficient. Alter Page Map and Jump (Function 22) changes the memory mapping context and transfers control to the specified address. The original memory mapping context is lost. This function is analogous to the FAR JUMP in the 8086 family architecture. Alter Page Map and Call (Function 23) contains two subfunctions. The Alter Page Map and Call subfunction saves the current memory mapping context, alters the specified memory mapping context, transfers control to the specified address, and restores the state of the specified mapping context after the return. This subfunction is analogous to the FAR CALL in the 8086 family architecture. The Alter Page Map and Call subfunction pushes information onto the stack. The Get Page Map Stack Space Size subfunction returns the number of bytes of stack space the Alter Page Map and Call subfunction needs to do this. Use the information provided by Get Page Map Stack Space Size to alter the stack size before using either Function 22 or the Alter Page Map and Call subfunction. Move/Exchange Memory Region (Function 24) lets you move large amounts of memory without mapping and unmapping pages. You can move or exchange up to 84 pages without mapping any logical pages. Function 24 has two subfunctions. The Move Memory Region subfunction copies and the Exchange Memory Region subfunction exchanges: conventional memory to conventional memory, conventional memory to expanded memory, expanded memory to conventional memory, and expanded memory to expanded memory. The subfunctions maintain the current mapping context, so the program does not have to save and restore it. Get Mappable Physical Address Array (Function 25) tells an application what physical memory is mappable. Function 2 (get page frame address) gives the segment address of the first four pages in the page frame. Use Function 25 to find the amount of mappable physical memory for page frames larger than four pages. Function 25 has two subfunctions. The Get Mappable Physical Address Array subfunction returns an array containing the segment address and physical page number for each mappable physical page in a system. The array is a cross reference between physical page numbers and the actual segment addresses for each mappable page in the system. The array is sorted by segment address in ascending order, but the physical page numbers associated with the segment address are not necessarily in ascending order. The Get Mappable Physical Address Array Entries subfunction gets the number of entries needed by the array returned by the Get Mappable Physical Address Array subfunction. Before using the Get Mappable Physical Address Array Entries subfunction, use the Get Mappable Physical Address Array subfunction to determine how much memory to allocate for storing the physical address array. Allocate Raw Pages (Function 27) with two subfunctions allocates the number of raw pages requested by the operating system or environment and assigns a unique EMM handle to these pages. (Some expanded memory boards have pages of less than the standard 16Kb. These non-standard sized memory pages are called raw pages.) Handles assigned to raw pages are called raw handles. Function 4 (Allocate Pages) allocates only 16Kb pages; it does not support raw pages. Programs whose page frames are in conventional memory must use Prepare Expanded Memory Hardware for Warm Boot (Function 29) when they detect Ctrl-Alt-Del. This function prepares the expanded memory hardware for an impending warm boot. Environment Functions The following functions are for inclusion in operating system and environment programs so the operating system/environment knows the kind of hardware support available for expanded memory. Never use Function 26, 28, or 30 in application programs. Get Expanded Memory Hardware Information (Function 26) is only for use by operating systems (such as DOS) and environments (such as Windows 386 and Desqview). The operating system/environment can disable Function 26 at any time. The function has two subfunctions. The Get Hardware Configuration Array subfunction returns an array containing expanded memory hardware configuration information for use by operating systems and environments. The operating system/environment uses this information to determine the hardware support for expanded memory, including raw page size, alternate register sets available, context save area size, and register sets for DMA channels. Some expanded memory boards have pages of less than the standard 16Kb, called raw pages. Function 3 (Get Unallocated Page Count) returns only the number of 16Kb pages. The Get Unallocated Raw Page Count subfunction returns the number of unallocated raw mappable pages and the total number of raw mappable pages in expanded memory to the operating system/environment. Alternate Map Register Set (Function 28) is for use by operating systems and environments only. It has nine subfunctions. The Get Alternate Map Register Set subfunction responds in one of two ways, depending on the setting of the map register set which is active when the function is invoked. If the map register set is equal to zero, a pointer to a context save area is returned. If the map register set is greater than zero, the number of the alternate map register set is returned. The Set Alternate Map Register Set subfunction responds in one of two ways, depending on the map register set specified. If the alternate map register set equals zero, map register set zero is activated, and the contents of the map register context save area is copied into register set zero on each expanded memory board in the system. If the alternate map register set specified is not zero, the alternate map register set specified is activated. The restore area, which the operating system is pointing to, is not used. The Get Alternate Map Save Array Size subfunction returns the storage requirements for the map register context save area that is referenced by the other subfunctions. If an alternate map register is available, the Allocate Alternate Map Register Set subfunction gets its number and copies the currently active alternate map register set's contents into the newly allocated alternate map register set's mapping registers. This does not change the alternate map register set in use but prepares the new alternate map register set for a subsequent Set Alternate Map Register Set subfunction. Operating systems can use this subfunction to quickly switch mapping contexts. The Deallocate Alternate Map Register Set subfunction returns the alternate map register set to the memory manager. The memory manager can then reallocate the alternate map register set. This subfunction also makes the mapping context of the specified alternate map register unavailable for reading or writing, thus protecting the pages previously mapped in an alternate map register set by making them inaccessible. The current alternate map register set cannot be deallocated, so memory currently mapped into conventional and expanded memory is inaccessible. The four DMA subfunctions let operating systems/environments use DMA register sets. (Hardware that supports these subfunctions is not yet available.) If a DMA register set is available, the Allocate DMA Register Set subfunction gets the current number of a DMA register set for an operating system/environment. This subfunction is useful with multitasking operating systems, in which you would like to switch to another task when one task is waiting for DMA to complete. The Deallocate DMA Register Set subfunction deallocates the specified DMA register set. The Enable DMA on Alternate Map Register Set subfunction allows DMA accesses on a specific DMA channel to be associated with a specific alternate map register set. This function is useful in a multitasking operating system, where you would like to switch to another task until the first task's DMA operation completes. The Disable DMA on Alternate Map Register Set subfunction disables DMA accesses for all DMA channels which were associated with a specific alternate map register set. Enable/Disable OS/E Function Set (Function 30) is only used by operating systems and environments. It includes three subfunctions which are Enable OS/E Function Set subfunction that will enable Function 26 (Get Expanded Memory Hardware Information), Function 28 (Alternate Map Register Sets), and also Function 30 (Enable/Disable Operating System Functions). The Disable OS/E Function Set subfunction disables Functions 26, 28, and 30. The Return Access Key subfunction lets the operating system/environment return the access key to the EMM. Returning the access key to the EMM enables access to the operating system/environment function set. Example Programs The following two examples (written in Microsoft(R) C, Version 5.0, and Microsoft MASM, Version 5.0) illustrate how functions new to EMS 4.0 can be implemented. The first example shows how expanded memory can be shared between two application programs. The first program (SAVSCR.EXE) saves the current screen, initializes a blank screen in expanded memory, and then exits (see Figure 2). The second program (SWPSCR.EXE) saves its current screen, displays the blank screen and the first program's current screen, and then restores its own current screen. Before it can use expanded memory, SAVSCR.EXE must first see if the EMM is present (by getting the device name from the device driver header). Because SAVSCR.EXE uses 4.0 functions, it also checks the EMM version (Function 7), and exits if EMM is earlier than version 4.0 (see Figure 3). SAVSCR.EXE next determines the number of unallocated pages in expanded memory (Function 3) and exits if there are fewer pages than it needs. Then the program allocates the expanded memory pages it needs and gets an EMM handle (Function 4), assigning the unique name SHARED to the handle (Function 20). This unique handle name will be used later by the second application program to find the handle so it can use the same expanded memory (see Figure 4). Next, SAVSCR.EXE uses the Map/Unmap Multiple Handle function (Function 17) to map the needed pages into the page frame. This is done with one function call; earlier versions of the EMM require one call for each page (see Figure 5). Then the SAVSCR.EXE program copies the current screen to logical page 0 and physical page 0, using the Move Memory Region subfunction (Function 24). Although this particular move does not span any more than one expanded memory page, the same function call can transfer up to 1Mb of memory without mapping in any logical pages of expanded memory (see Figure 6). Finally, SAVSCR.EXE unmaps all of the pages using the Map/Unmap Multiple Handle Pages function (Function 17) and then exits without returning the handle to the EMM (see Figure 5). Expanded memory is now protected. The second program, SWPSCR.EXE, can find this same expanded memory by using the Search for Named Handle subfunction. First, SWPSCR.EXE verifies that the EMM is present and is version 4.0 or greater, as the first program did. However, unlike the SAVSCR.EXE program, SWPSCR.EXE does not need to allocate pages because it can use the same handle named SHARED that SAVSCR.EXE received. SWPSCR.EXE uses the Search for Named Handle subfunction (Function 21) to determine if the handle named SHARED is present. If the first program (SAVSCR.EXE), has not been executed already, SWPSCR.EXE will exit, because it could not find the handle named SHARED. Finally, SWPSCR.EXE uses the Exchange Memory Region subfunction (Function 24) to: swap its current screen with the logical page that has the blank screen; swap the blank screen with the logical page that has the original screen; and then restore its own current screen. (Because the Exchange Memory Region subfunction handles all mapping, none of the logical pages have to be mapped before this call.) SWPSCR.EXE unmaps all expanded memory pages be-fore exiting (Function 17) as seen in Figure 7. SWPSCR.EXE illustrates how EMM 4.0 has simplified executing code in expanded memory. (For an idea of just how much EMM 4.0 has accomplished in this area, refer to the KERNEL module in "Expanded Memory: Writing Programs That Break the 640Kb Barrier," MSJ, March 1987.) As in the first example, MAPCALL.EXE (see Figure 8) checks for the version of EMM and allocates expanded memory. Then it uses the MS-DOS Load Overlay function to load two modules (MODULE1.EXE and MODULE2.EXE) into expanded memory (see Figure 9). Next, MAPCALL.EXE uses the Alter Page Map and Call subfunction (Function 23) to execute MODULE1.EXE (see Figure 10). MODULE1.EXE in turn uses the Search for Named Handle subfunction to determine which handle to use and uses the Alter Map Page and Call subfunction to execute MODULE2.EXE (see Figure 11). MODULE2.EXE prints a message to the screen and does a far return. The far return causes the EMM to restore the map-ping specified in the old map page definition and return control to MODULE1.EXE (see Figure 12). MODULE1.EXE then does a far return, which causes the EMM to return control to MAPCALL.EXE and to restore the original mapping context. MAPCALL.EXE then releases its handle and exits to MS-DOS. To Get EMS 4.0 If you are interested in developing application programs that use expanded memory, call Intel for a free copy of the Lotus/Intel/Microsoft Expanded Memory Specification. From the United States and Canada, call (800) 538-3373. Outside the United States and Canada, call (503) 629-7354. Figure 1: EMM 4.0 FUNCTIONS ╓┌──────────────────────────┌────┌───────────────────────────────────────────╖ Function Name No. Description GET STATUS 1 Returns a status code to tell you whether Function Name No. Description GET STATUS 1 Returns a status code to tell you whether the EMM is present and the hardware/software is working correctly. GET PAGE FRAME ADDRESS 2 Gives the program the location of the page frame. GET UNALLOCATED PAGE 3 Tells the program the totalCOUNT number of pages in expanded memory and the number of unallocated pages. ALLOCATE PAGES 4 Allocates the number of expanded memory pages requested by the program; assigns a unique EMM handle to the set of pages allocated. MAP HANDLE PAGE 5 Maps the specified logical page in expanded memory to the specified physical page within the page frame. Function Name No. Description DEALLOCATE PAGES 6 Deallocates the pages currently allocated to an EMM handle. GET EMM VERSION 7 Returns the version number of the EMM software. SAVE PAGE MAP 8 Saves the contents of the page mapping registers of all expanded memory boards. RESTORE PAGE MAP 9 Restores the contents of the page mapping registers. 10 Reserved. 11 Reserved. GET EMM HANDLE COUNT 12 Returns the number of active EMM handles. GET EMM HANDLE PAGES 13 Returns the number of pages allocated to a Function Name No. Description GET EMM HANDLE PAGES 13 Returns the number of pages allocated to a specific EMM handle. GET ALL EMM HANDLE 14 Returns the active EMM handles PAGES and the number of pages allocated to each one. GET/SET PAGE MAP 15 Saves and restores the mapping context of the active EMM handle. GET/SET PARTIAL PAGE MAP 16 Saves a partial mapping context for specific mappable memory regions in a system. MAP/UNMAP MULTIPLE 17 Maps/unmaps (in a single HANDLE PAGES invocation) logical pages into as many physical pages as the system supports. REALLOCATE PAGES 18 Increases or decreases the amount of expanded memory allocated to a handle. GET/SET HANDLE 19 Lets an application determine and ATTRIBUTE Function Name No. Description GET/SET HANDLE 19 Lets an application determine and ATTRIBUTE set a handle as volatile or non-volatile. GET/SET HANDLE NAME 20 Gets the eight-character name currently assigned to a handle assigns an eight- character name to a handle. GET HANDLE DIRECTORY 21 Returns information about active handles and the names assigned to each. ALTER PAGE MAP AND JUMP 22 Alters the memory mapping context and transfers control to the specified address. ALTER PAGE MAP AND CALL 23 Alters the specified mapping context and transfers control to the specified address. A return can then restore the context and return control to the caller. MOVE/EXCHANGE MEMORY 24 Copies or exchanges a region ofREGION memory from conventional to conventional memory, Function Name No. Description from conventional to conventional memory, conventional to expanded memory, expanded to conventional memory, or expanded to expanded memory. GET MAPPABLE PHYSICAL 25 Returns an array with the segment ADDRESS ARRAYaddress and physical page number for each mappable physicalpage in a system. GET EXPANDED MEMORY 26 Returns an array containing the HARDWARE INFORMATION hardware capabilities of the expanded memory system. ALLOCATE RAW PAGES 27 Allocates the number of non-standard size pages that the operating system requests and assigns a unique EMM handle to these pages. ALTERNATE MAP REGISTER 28 Lets an application program SET simulate alternate sets of hardware mapping registers. Function Name No. Description registers. PREPARE EXPANDED 29 Prepares the expanded memory MEMORY HARDWARE FOR hardware for an impending WARM BOOT warm boot. ENABLE/DISABLE OS/E 30 Enables and disables EMM Functions designed for use by operating systems and environments (Functions 26, 28, and 30). Figure 2: SAVSCR Main Program #include <c:\msc\include\dos.h> #include <c:\msc\include\memory.h> #include <c:\msc\include\stdio.h> #include <c:\emm\demo\emm.h> /* set up size and base of video ram */ #define VIDEO_RAM_SIZE 4000 #define VIDEO_RAM_BASE 0XB8000000 union REGS inregs, outregs; struct SREGS segregs ; char far *video_ram_ptr = {VIDEO_RAM_BASE); /* video start address (CGA)*/ unsigned long int video_ram_size =; /* bytes in video ram */ unsigned int emm_handle ; /* our emm handle */ char emm_device_name[] = "EMMXXXX0"; /* Device Name of EMM */ char emm_handle_name[8] ="shared" ; /* name for handle to be shared char far *emm_ptr; /* pointer to page frame */ char far *(*page_ptr) ; /* pointer to page in the frame * int pages_needed = 4; struct log_phys { int log_page_number; int phys_page_number; } current_pages [4] ; struct log_phys far *map_unmap_ptr ; int result ; /* result passed back from function calls */ main () { check_emm_version_number(); /*Check for EMM > 4.0 */ result = get_expanded_memory(&emm_ptr, pages_needed, &emm_handle, emm_handle_name); if (result != 0) exit(1); /* exit if error */ result = map_unmap_multiple_pages (current_pages, /* Map in */ emm_handle, 1); /* pages */ move_exchg_to_expanded(MOVE_MEMORY_REGION, /* Copy video screen to */ video_ram_ptr, emm_handle, 0, video_ram_size); /* logical page 0 */ /* make a null video screen at logical page 1 */ page_ptr = (emm_ptr + 0x4000); /* make null screen at */ memset (page_ptr, 0, VIDEO_RAM_SIZE); /* at logical page 1 */ /* Unmap all pages so they are protected */ result = map_unmap_multiple_pages (current_pages, emm_handle, 0); } Figure 3: This function checks to see if the EMM is present and if the version number is >= 4.0. It uses EMM function 7, GET. VERSION NUMBER. Using EMM Function 7 GET.VERSION NUMBER int check_emm_version_number() { char *emm_device_name_ptr ; inregs.h.ah = 0x35; /* Use the DOS get interrupt function (0x35) to */ inregs.h.al =EMM_INT;/* get the pointer at interrupt vector 0x67, */ intdosx(&inregs, &outregs, &segregs);/* and check for device name. */ emm_device_name_ptr = (segregs.es * 65536) + 10; if (memcmp(emm_device_name, emm_device_name_ptr,8) !=0) { printf("Expanded memory manager not present\n"); exit(1); } inregs.h.ah = GET_VERSION ; /* set function code and check for */ int86(EMM_INT,&inregs,&outregs);/* version >= 4.0 */ if (outregs.h.ah != 0) exit(1); if ((outregs.h.ah == 0) & (outregs.h.al < 0x40)) { printf("Expanded memory manager does not support LIM 4.0"); exit(1) ; } } Figure 4: This function gets the amount of expanded memory requested, returns a pointer to the page frame, and assigns the name to the handle. Obtaining the Requested Amount of Memory int get_expanded_memory(emm_ptr_ptr, pages, emm_handle_ptr, name) char *(*emm_ptr_ptr); /* Pointer to expanded memory page frame */ int pages; /* Number of pages to allocate */ unsigned int *emm_handle_ptr; /* Pointer to emm handle */ char *name; { inregs.h.ah = GET_UNALLOCATED_PAGE_COUNT ; /* Check to see if there */ int86(EMM_INT, &inregs, &outregs); /* enough unallocated pages left.*/ if (outregs.h.ah != 0) return(1); if (outregs.x.bx < pages) return(2); inregs.h.ah = ALLOCATE_PAGES ; /* Get a handle and allocate */ inregs.x.bx = pages; /* the requested pages.*/ int86(EMM_INT, &inregs, &outregs); if (outregs.h.ah != 0) return(3); *emm_handle_ptr = outregs.x.dx ; inregs.h.ah = GET_FRAME_ADDRESS; /* Get page frame segment address */ int86(EMM_INT, &inregs, &outregs); /* and make it a pointer. */ if (outregs.h.ah != 0) return(4); *emm_ptr_ptr = (unsigned long int) (outregs.x.bx *65536); inregs.x.ax = SET_HANDLE_NAME ; /* assign name to handle */ inregs.x.dx = *emm_handle_ptr ; inregs.x.si = FP_OFF(name) ; segregs.ds = FP_SEG(name); int86x(EMM_INT, &inregs, &outregs, &segregs); if (outregs.h.ah != 0) return(5); return(0); } Figure 5: Implementing the MAP/UNMAP MULTIPLE PAGES EMM Function int map_unmap_multiple_pages (log_phys_pages,handle,map_unmap) struct log_phys *log_phys_pages ; /* Pointer to log_phys struct */ unsigned int handle; /* Handle to map or unmap */ unsigned int map_unmap; /* 0 = map, 1 = unmap */ { int i ; struct log_phys *temp_ptr; temp_ptr = log_phys_pages; for (i=0 ; i<=3; i++) { /* Setup the structure to Map or unmap the logical pages 0 to 3 */ log_phys_pages->phys_page_number = i; if (map_unmap == 1) log_phys_pages->log_page_number = i; else log_phys_pages->log_page_number = 0xFFFF ; log_phys_pages++ ; } inregs.x.ax = MAP_UNMAP_MULTIPLE_PAGES ; inregs.x.dx = handle; inregs.x.cx = 4; inregs.x.si = FP_OFF(temp_ptr); segregs.ds = FP_SEG(temp_ptr); int86x(EMM_INT,&inregs,&outregs,&segregs); if (outregs.h.ah != 0) return(1); return(0); } Figure 6: This function implements the MOVE or EXCHANGE MEMORY REGION function to move or exchange conventional memory with expanded memory pages. Implementing the MOVE or EXCHANGE MEMORY REGION Function int move_exchg_to_expanded(function_number, conv_buffer, handle, page, length) unsigned int function_number ; /* Move or Exchange*/ char far *conv_buffer ; /* conventional memory with */ int handle; /* EMM memory associated with this handle*/ int page ; /* at this physical page */ unsigned long int length; /* and for this many bytes */ { #pragma pack(1) /* Make sure the following structure is byte aligned */ struct move_exchg { unsigned long int region_length; char source_type ; unsigned int source_handle ; unsigned int source_offset ; unsigned int source_seg_page; char dest_type; unsigned int dest_handle; unsigned int dest_offset; unsigned int dest_seg_page; } move_exchg_struct; struct move_exchg *move_exchg_ptr; move_exchg_struct.region_length = length ; move_exchg_struct.source_type = 0; move_exchg_struct.source_handle = 0; move_exchg_struct.source_offset = FP_OFF(conv_buffer); move_exchg_struct.source_seg_page = FP_SEG(conv_buffer); move_exchg_struct.dest_type = 1; move_exchg_struct.dest_handle = handle; move_exchg_struct.dest_offset = 0 ; move_exchg_struct.dest_seg_page = page; inregs.x.ax = function_number; move_exchg_ptr = &move_exchg_struct; inregs.x.si = FP_OFF(move_exchg_ptr); segregs.ds = FP_SEG(move_exchg_ptr); int86x(EMM_INT, &inregs, &outregs, &segregs); if (outregs.h.ah != 0) exit(1); return(outregs.x.ax) ; } Figure 7: The Main Program for SWPSCR #include <c:\msc\include\dos.h> #include <c:\msc\include\memory.h> #include <c:\msc\include\stdio.h> #include <c:\emm\demo\emm.h> #define VIDEO_RAM_SIZE 4000 #define VIDEO_RAM_BASE 0XB8000000 union REGS inregs, outregs; struct SREGS segregs ; char far *video_ram_ptr = {VIDEO_RAM_BASE}; /* video start address (CGA)*/ unsigned long int video_ram_size ={4000}; /* bytes in video ram */ unsigned int emm_handle ; /* emm handle */ char emm_device_name[] = "EMMXXXX0"; /* Device Name of EMM */ char emm_handle_name[8] ="shared" ; /* name for handle to be shared */ char far *(*expanded_memory_ptr) ; /* pointer to page frame */ char far *(*page_ptr) ; /* pointer to page in the frame */ long target_time,current_time ; struct log_phys { /* structure to hold the mapping of logical */ int log_page_number; /* pages to physical pages */ int phys_page_number; } current_pages [4] ; main () { check_emm_version_number(); search_for_handle(emm_handle_name, &emm_handle); /* Exchange screens*/ move_exchg_to_expanded(EXCHANGE_MEMORY_REGION,video_ram_ptr, emm_handle, 1, video_ram_size); time(&current_time); /* Delay so user can see screen changes */ target_time = current_time + 3; while (current_time < target_time) time(&current_time); /* Display Screen */ move_exchg_to_expanded(EXCHANGE_MEMORY_REGION, video_ram_ptr, emm_handle, 0, video_ram_size); time(&current_time); /* Delay so user can see screen change */ target_time = current_time + 3; while (current_time < target_time) time(&current_time); /* Restore original screen */ move_exchg_to_expanded(EXCHANGE_MEMORY_REGION,video_ram_ptr, emm_handle, 1, video_ram_size); result = map_unmap_multiple_pages (current_pages, emm_handle, 0); exit(0) ; } Figure 8: The Main Program for MAPCALL #include <c:\msc\include\dos.h> #include <c:\msc\include\memory.h> #include <c:\msc\include\stdio.h> #include <c:\emm\demo\emm.h> union REGS inregs, outregs; struct SREGS segregs ; unsigned int emm_handle ; char emm_device_name[] = "EMMXXXX0"; /* Device Name of EMM * char far emm_handle_name[8] ="mapcall"; /* Name for handle */ char far mod1_name[] ="c:\\emm\\demo\\module1.exe\0"; /* Name of modules */ char far mod2_name[] ="c:\\emm\\demo\\module2.exe\0"; /* to be loaded */ char far *emm_ptr ; char far *(*page_ptr) ; int pages_needed = 16 ; struct log_phys { int log_page_number; int phys_page_number; } ; struct log_phys far current_pages[4]; struct log_phys far map_call_pages ; int result; main () { check_emm_version_number(); result = get_expanded_memory(&emm_ptr, pages_needed, &emm_handle, emm_handle_name); if (result != 0) exit(1); /* Map in pages */ result = map_unmap_multiple_pages (current_pages, emm_handle, 1); *page_ptr = emm_ptr; /* Load Module 1 into logical page 0 */ load_overlay(mod1_name, page_ptr, 0); /* at physical page 0 */ /*Load Module 2 into logical page 1 at physical page 1 */ load_overlay(mod2_name, page_ptr, 1); /* Unmap all pages */ result = map_unmap_multiple_pages (current_pages, emm_handle, 0); /* Map and call to module in page 0 and physical page 1 */ map_call_pages.log_page_number = 0; map_call_pages.phys_page_number = 0; map_and_call(&map_call_pages, 1, &current_pages, 0, emm_handle); inregs.h.ah = DEALLOCATE_PAGES ; /* Release handle before exiting */ inregs.x.dx = emm_handle ; int86(EMM_INT, &inregs, &outregs); exit(0); } Figure 9: Implementing the ALTER PAGE MAP AND CALL EMM Function int map_and_call(new_map_ptr, new_length, old_map_ptr, old_length, handle) struct log_phys *new_map_ptr; char new_length; struct log_phys *old_map_ptr; char old_length; unsigned int handle; { # pragma pack(1) /* Make sure structure is byte aligned */ struct map_call_struct { unsigned int offset_target_address ; unsigned int seg_target_address ; char new_page_map_length ; unsigned int offset_new_page_map; unsigned int seg_new_page_map ; char old_page_map_length ; unsigned int offset_old_page_map; unsigned int seg_old_page_map ; } map_call; struct map_call_struct *map_call_ptr; map_call_ptr = &map_call ; map_call.offset_target_address = 0 ; map_call.seg_target_address =0xd000; map_call.new_page_map_length = new_length; map_call.offset_new_page_map = FP_OFF(new_map_ptr); map_call.seg_new_page_map = FP_SEG(new_map_ptr); map_call.old_page_map_length = old_length; map_call.offset_old_page_map = FP_OFF(old_map_ptr); map_call.seg_old_page_map = FP_SEG(old_map_ptr); inregs.h.ah =0x56; /* Set up for Alter Page Map and Call EMM function */ inregs.h.al = 0; inregs.x.dx = handle ; map_call_ptr = &map_call ; inregs.x.si = FP_OFF(map_call_ptr); segregs.ds = FP_SEG(map_call_ptr); int86x(EMM_INT,&inregs,&outregs,&segregs); if (outregs.h.ah != 0) return(1); return(0) ; } Figure 10: Implementing the MS-DOS Load Function 4BH This function implements the MS-DOS load function 4BH. It sets AL to 3 causing the function to load the file and apply the relocation factor without executing the file. int load_overlay(load_file_name,relocation_ptr,page) char *load_file_name ; char *(*relocation_ptr); unsigned int page; /* physical page at which to load */ { struct reloc { unsigned int load_seg; /* Which segment to load file */ unsigned int reloc_factor; /* Which segment to use for relocation */ } reloc_struct; struct reloc *reloc_struct_ptr; reloc_struct.load_seg = FP_SEG(*relocation_ptr) + (page * 0x400); reloc_struct.reloc_factor = FP_SEG(*relocation_ptr) + (page * 0x400); inregs.h.ah = 0x4B ; /* Dos Exec function code */ inregs.h.al = 3; /* load but do not execute */ inregs.x.dx = FP_OFF(load_file_name); segregs.ds = FP_SEG(load_file_name); reloc_struct_ptr = &reloc_struct ; inregs.x.bx = FP_OFF(reloc_struct_ptr); segregs.es = FP_SEG(reloc_struct_ptr); intdosx(&inregs, &outregs, &segregs); } Figure 11: NAME Start ; Use the DOSSEG directive to ensure that the code segment is the ; first segment in the module. Since this piece of code will be ; loaded in at the page frame at D000:0000H the Alter Page Map and ; Call will use D000:0000H as the entry point. DOSSEG data SEGMENT PUBLIC 'DATA' Data ends CODE SEGMENT PUBLIC 'CODE' ASSUME CS:CODE, DS:DATA start proc far push ds PUSH dx mov DX,data ; set up data seg into ds mov ds,dx pop dx mov ah, 09 ; set function code for DOS display string mov dx, offset enter_msg int 21H mov si, offset handle_name mov ax, 5401H ; set function to search for named ; handle int 67H ; and invoke EMM or ah, ah jnz exit mov si, offset map_call ; Set up registers for Alter Page ; Map and Call mov al, 0 ; indicate that values are pages mov ah, 56H ; set function to map and call int 67H ; invoke emm or ah, ah jnz exit mov ah, 09 ; set function for DOS display ; string mov dx, offset exit_msg int 21H exit: pop ds ret start endp CODE ENDS data SEGMENT PUBLIC 'DATA' ; cr equ 0Dh lf equ 0AH ; log_phys_map_struct STRUC log_page_number DW ? phys_page_number DW ? log_phys_map_struct ENDS map_call_struct STRUC target_address DD ? ; Pointer to which EMM will transfer ; control new_page_map_length DB ? ; Number of new pages to be mapped on ; call. new_page_map_ptr DD ? ; Pointer to array of ; log_phys_map_struc. old_page_map_length DB ? ; Number of pages to mapped on ; return. old_page_map_ptr DD ? ; pointer to array of ; log_phys_map_struc reserved DW 4 DUP (?) map_call_struct ENDS ; new_map log_phys_map_struct <1,1> ; mapping before call old_map log_phys_map_struct <0,0> ; mapping after call map_call map_call_struct <0D0004000H,1,new_map,1,old_map> handle_name db 'mapcall',0 ; handle name is ascciz string enter_msg db 'Entering Module 1',cr,lf,'$' exit_msg db 'Exiting Module 1',cr,lf,'$' Data ends end start Figure 12: NAME Start (MODULE2.EXE) DOSSEG data SEGMENT PUBLIC 'DATA' Data ends CODE SEGMENT PUBLIC 'CODE' ASSUME CS:CODE, DS:DATA start proc far push ds PUSH dx mov DX,data ; Set up date segment mov ds,dx pop dx mov ah, 09 ; Set function code for DOS display string. mov dx, offset enter_msg int 21H mov ah, 09 ; Set function code for DOS display string. mov dx, offset exit_msg int 21H pop ds ret start endp CODE ENDS data SEGMENT PUBLIC 'DATA' cr equ 0Dh lf equ 0AH enter_msg db 'Entering Module 2',cr,lf,'$' exit_msg db 'Exiting Module 2',cr,lf,'$' Data ends end start ════════════════════════════════════════════════════════════════════════════

Vol. 3 No. 2 Table of Contents

Microsoft(R) Windows Adapts to the Unique Needs of the Japanese Market A Japanese version of Microsoft Windows offers support for double-byte kanji characters and has the ability to convert kana characters into kanji. It also enables software vendors to build graphical applications that run on many disparate Japanese hardware systems despite their incompatibility. Utilizing OS/2 Multithread Techniques in Presentation Manager Applications While OS/2 provides the preemptive multitasking that Windows lacks, the Presentation Manager's message-based architecture can cause problems when doing lengthy processing. OS/2 threads offer alternative solutions to the problems that occur when programming in Presentation Manager. OS/2 LAN Manager Provides a Platform for Server-Based Network Applications The OS/2 LAN Manager API, with its named pipe and remote procedure call facilities, offers a rich set of functions for building network intrinsic applications. These applications can exploit the potential of distributed processing from an OS/2 or DOS workstation. Writing OS/2 Bimodal Device Drivers: An Examination of the DevHlp API Device drivers are the necessary linkage between the operating system and peripheral devices that enable you to use special hardware with standard applications software. Our study of OS/2 continues with an in-depth look at the unique capabilities of OS/2 device drivers. Exploring the Structure and Contents of the MS-DOS(R) Object Module Format A programmer does not usually need to think about the contents of the intermediate files prepared for the linker when using commercial compilers. This excerpt from The MS-DOS(R) Encyclopedia examines the object module format and provides insights into the operation of LINK and LIB. A Guide to Program Editors, the Developer's Most Important Tool Text editors for programmers have improved greatly, prompting MSJ to conduct an evaluation of twelve editors. The three editors that we thought were best──BRIEF(R), ME, and VEDIT PLUS──are reviewed in depth, and a features chart gives you a good idea of what all twelve can do. EDITOR'S NOTE This issue of MSJ covers the broadest range of topics yet. In addition to our continuing coverage of the OS/2 systems with articles on the OS/2 LAN Manager, bimodal device drivers, and the Presentation Manager, we have an article on developing software for the Japanese market, a tutorial on the MS-DOS(R) object file format, and a comprehensive look at available program editors. As time goes by, we pass yet another milestone in the progress of OS/2. With the shipment of Version 1.0 of the standard edition, OS/2 has become a users' as well as a developers' product, and many developers have begun to focus on the advanced managers. The Presentation Manager is no longer just a concept or a specification; now that it is in developers' hands, it is a real development environment. In this issue, Ray Duncan explains how to write a device driver for OS/2, and Charles Petzold examines the use of OS/2 threads within Presentation Manager programs. OS/2 has the potential not only to improve the applications we associate with MS-DOS, but to open the way for entirely new classes of applications. Among the most exciting of these is distributed processing. PC networking to date has in most cases meant little more than file and printer sharing. We present an overview of the possibilities for the development of server-based distributed processing applications with the OS/2 LAN Manager. Recently in talking with subscribers, we were reminded just how important, and how personal, the choice of a program editor is to the professional programmer. We heard many different opinions, all of which were uncharacteristically strong. Given the importance of the subject, we thought it worthwhile to depart from our normal subject material and commissioned a comprehensive review of the available editors. The results were surprising. Please drop us a line if you think we should review other categories of programmer tools. We want this to be your Journal. Please write and tell us how we can make it better for 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 DONNA PUIZINA Administrative Assistant Copyright(C) 1988 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, XENIX, CodeView, and MS-DOS are registered trademarks of Microsoft Corporation. IBM and PC/AT are registered trademarks of International Business Machines Corporation. Personal System/2 and PS/2 are trademarks of International Business Machines Corporation. BRIEF is a registered trademark of UnderWare, Inc. CP/M is a registered trademark of Digital Research, Inc. dBASE and dBASE III PLUS are registered trademarks of Ashton-Tate Corporation. Hercules is a registered trademark of Hercules Computer Technology. Intel is a registered trademark of Intel Corporation. Macintosh is a registered trademark of Apple Computer, Inc. NEC is a registered trademark of NEC Corporation. Paradox is a registered trademark of Ansa Software, a Borland Company. 3+Open is a trademark of 3Com Corporation. Solution Systems is a registered trademark of Solution Systems. UNIX is a registered trademark of American Telephone and Telegraph Company. WordStar is a registered trademark of MicroPro International Corporation. WYSE is a trademark of WYSE Technology. ████████████████████████████████████████████████████████████████████████████ Bringing Windows to the Expanding Japanese Market by Tom Sato and Lin F. Shaw Machine independence is a cornerstone of the Microsoft(R) Windows philosophy. While this particular issue is largely ignored in the United States-due to the fact that the PC environment is dominated by IBM(R) PC AT(R) machines and compatibles──in Japan, Windows-based software is truly compatible across incompatible hardware architectures. Microsoft Windows has given the Japanese community of independent software vendors (ISVs) the opportunity to develop applications with only one set of source codes for a wide range of machines, thus eliminating the laborious adaptation of software to different machines. For Western ISVs there is an added advantage. Windows applications written for IBM PCs and compatibles run on all the various Japanese computers. Windows-based applications, therefore, present a tremendous export opportunity for Western ISVs, since Japan is a country where real growth in the software industry has yet to come (see Figures 1 and 2☼). Microsoft Windows Version 1.03 was first released by NEC in December 1986 for use on its PC-9801 series of personal computers, and in its first six months sold over 25,000 copies. Matsushita has also joined the original family of Windows equipment manufacturers (OEMs) by releasing their own adaptations of Windows for their new machines at the September 1987 Tokyo DATA show (see Figures 3 and 4☼). The Japanese Market In 1982, NEC launched the PC-9801 series. Fourteen compatible variations later, with its millionth unit shipped last March, the PC-9801 series dominates the Japanese PC market, with a market share estimated at over 60 percent. The rest of the market is split among various companies, all of which have released incompatible MS-DOS(R) machines. In spite of these numbers, however, the fact is that the penetration rate of personal computers in the Japanese office is nowhere near that of the United States, as many Japanese businessmen have yet to become accustomed to PCs. The primary reason is that the Japanese language has a great many character sets and kanjis (Chinese characters). This has prevented the spread of mechanical typewriters──they have more than 4000 characters and are very clumsy to use──and, therefore, computers. Amazingly, most business correspondence in Japan is still done by hand. Fortunately, there is a sign that this is all going to change. Several years ago, some of the electronics giants started to sell electronic Japanese typewriters. For the most part, these are 8-bit computers with an LCD screen, a thermal transfer printer and built-in syllabic kana to kanji conversion software. These machines provide more capability, portability, efficiency, and also better prices than the previous generation of mechanical typewriters. This is the latest booming business in Japan, with an installed base of more than 5 million units. Nevertheless, these electronic typewriters still have limitations──they are not as flexible as word-processing software on 16-bit PCs; storage is limited and the editing screen is really too small for general office use. However, the popularity of the Japanese electronic typewriters has led to significant increases in the sale of substantially more powerful 16-bit PCs. The standard configuration for a typical Japanese PC is an 80286 running at 10 MHz, a 640x480 pixel screen, 640Kb of RAM, 256Kb of ROM for kanji fonts, and dual 1.2Mb floppy disk drives. Machines with an internal 20Mb hard disk drive are also becoming more popular, and there are now machines with higher screen resolution and more detailed kanji fonts available. However, the speed of Japanese computers is slower than that of their counterparts in the United States. This is largely due to a great deal of overhead, for instance searching for kanji bitmaps and having to deal with larger screen and kana-kanji interfaces. Japanese Language Modern Japanese is written in a mixture of kanji characters and two types of kana signs. There are 46 basic kana signs; 45 basic syllables arranged as in Figure 5☼, consisting of five vowels in the order a, i, u, e, o and then five vowels preceeded in turn by nine consonants in the order k, s (or alternatively sh), t (or ts), n, h (f), m, y, r, w, and the final consonant for kana, n. Two small strokes on the upper right hand corner of a character are added as voiced marks or dakuten. They apply to kana beginning with k, s, t, and h and represent 20 more syllables beginning with voiced consonants g, z, d, and b respectively. A small round circle on the upper right hand corner is a semi voicing sound mark (han-dakuten). It applies to the kana beginning with h and represents five further syllables begining with voiced consonants p. Further, nine of the kanas are sometimes written in small characters to form dipthongs (see Figure 6☼). There are two types of kana signs: kata-kana and hira-gana There is a one-to-one correspondence between the kata-kana and hira-gana; for example, for each syllable there is both a hira-gana letter and kata-kana letter. Hira-gana is most commonly used with a mixture of kanji, and kata- kana is always used for foreign words. Kata-kana is a simplified form of hira-gana, written for the most part in simple strokes, whereas hira-ganas are written with curves. On an 8x8 bitmap, hira-gana is too intricate to describe. Therefore, both the Japanese keyboard and the Japanese 1-byte character code for alphanumeric characters contain only the kata-kana (see Figure 7☼). Kanji Characters Kanji characters are used frequently to represent one or more kana characters. There are many homonyms in Japanese and the use of kanji clarifies the meaning of the written word. Nouns and verbs are typically kanjis or a mixture of hira-ganas and kanjis. A typical kanji dictionary lists over 15,000 kanji characters. However, the Japanese elementary schools only teach 1878 of them and in practice about 3500 kanjis have normal daily use. The same kanji character may have different pronunciations depending on its context or the other kanji with which it forms a word. For example, a kanji character for the word "leaf" has three pronunciations: ha, ba, and yo-u. Conversely, the same pronunciation is used for many kanji; for example, as many as twenty different kanjis can be pronounced ai. Text Layout Japanese is traditionally written vertically in columns running from top to bottom and from right to left across the page. Most books and newspapers are written this way. However, it can also be written horizontally as English is. Business correspondence, in fact, is usually written horizontally. Most Japanese software, therefore, displays horizontally and only supports vertical columns when printing a document or on a screen in sophisticated desktop publishing software. Japanese text is written without spaces between words. The use of a mixture of kanji and kana in a regular sentence provides implicit breaks to a sentence and the Japanese would have a very difficult time reading text of only kana characters. It would be analogous to reading English text without spaces between words. Japanese text can wrap from one line to another at any point, since there are no spaces between words. This includes two successive kanji which make up one logical "word." There is no need to justify Japanese text, as all Japanese characters are the same size. Shift-JIS Code Japanese Industrial Standards (JIS) define two levels of kanjis for computers. The JIS C 6226 standard defines the image and the code of the 3500 most commonly used kanji characters as level 1 kanjis. Level 2 defines the optional additional 2500 kanjis which are more obscure and are used for the names of people and places. Most 16-bit computers in Japan now support level 2, making the number of kanjis usually available to Windows more than 6000. They are stored in bank-switched ROMs as 16x16 pixel images. Kanjis, as well as hira-ganas, kata-kanas, alphanumeric characters, Greek and Russian characters, and other signs supported in ASCII character sets, are coded in the JIS coding scheme. JIS was designed like ASCII to be a transmission standard as well as a storage standard. When transmitting data over communications lines, reliability is extremely important. That is why parity exists for 7-bit ASCII. The JIS standard was also encoded in 7 bits to use parity. In order to be able to distinguish between 7-bit ASCII and 7-bit JIS, the ASCII shift-in (SI) and shift-out (SO) controls were used. A kanji character is composed of two successive 7-bit patterns, limiting us to 16,384 values. In order to avoid the 32 control characters, space, and rub-out, there are only 94x94 (8836) available and defined characters. As the use of parity in personal computers became less important, and since many U.S. applications did not understand SI and SO, Microsoft defined a new kanji encoding scheme for MS-DOS; the Microsoft Kanji Encoding Scheme, which uses two 8-bit values. It is commonly known as the Shift-JIS Code, because the character codes of JIS have been shifted. Figures 7☼ and 8 describe how this works. Of all the codes available between 00H and FFH, 00H to 7FH are ASCII characters and control codes, and A0H to DFH are kata-kana characters. This leaves 64 positions for the first byte of the double-byte value. The second byte could be between 00H and FFH. Since 64x256 is 16,384, there is more than enough room for all the JIS characters. Since JIS has a 94x94 matrix, Microsoft decided to map it into a 47x188 matrix in the set of 64x256 possibilities. This allowed 68 values to be avoided for the second byte. The control characters between 00H-1FH and 7FH-FFH were not used in order to avoid conflict with U.S. applications, nor were FDH and FEH used, because CP/M(R) 86 was using them when the scheme was introduced. Also, 20H-3FH were avoided in order to make parsing of characters such as ";", "(,)", and ":" easier for U.S. language compilers. Figure 8 shows how the Microsoft Kanji Encoding Scheme is shifted from the JIS coding scheme without actually changing the original JIS ordering. Just remember that a code between 81H-9FH and E0H-FCH means that the following code is the second byte of the double-byte character and together they give the code of a kanji character. Kana-Kanji Conversion With all these hira-gana, kata-kana and kanji characters, making a computer to support the Japanese language becomes complex. On the MS-DOS level, to input Japanese, a kana-kanji conversion program is installed as a device driver. There are various kana-kanji conversion programs on the market and most of them are porting their software to run under Windows. Usually kana-kanji conversion units are packaged free of charge with applications software. Companies lacking a kana-kanji conversion program can buy a license from kana-kanji software vendors. These conversion programs are typically activated by pressing a "hot key." In the case of the NEC(R) PC-9801 series, for example, the hot-key combination is CTRL-XFER and the bottom of the screen becomes the conversion window. There are various methods of input but all have kata-kana input and romanji (Roman character) input. Although the keyboard supports all the kata-kana characters, many Japanese are familiar with the standard English QWERTY typewriter keyboard and prefer to input using Roman characters. For example, the word "kanji" is typed K A N J I and as you type it in, the conversion program accepts the Roman characters and displays the correct hira-ganas. When the <XFER> key is pressed, the program searches through its dictionary to find the kanji characters for that word. This process is complicated for various reasons, including the Japanese homonyms mentioned earlier. Secondly, the inflectional endings of Japanese verbs increase the number of word roots so much that the dictionary may have to contain well over 100,000 word roots. Obviously, this would require an enormous amount of memory. The problem is solved by software that draws on a grammar of Japanese inflection to analyze the phonetic spelling typed in by the user. The software displays the most appropriate and most commonly used kanji combinations. Good software will give the correct kanji approximately 80 percent of the time. If the software gives the wrong kanjis, the user can hit the <XFER> key again and the next kanji choice will appear. To see all the different ways of spelling the word, you press <SHIFT>-<XFER>. Some conversion packages can convert multiple phrases, even a whole sentence. Figure 9☼ shows the kana-kanji conversion unit as it would appear in a Windows session. Kanji Classification Microsoft Windows 1.03 applications can be classified according to their abilities to process kanji input. Please note that this specification is only intended for Windows 1.03. There will be additions in the Windows 2.0 specification. Level 1: Original U.S. Applications. These applications are not able to treat double-byte kanji text. Most of the first releases of U.S. applications are level 1 applications. Level 2: Modified U.S. Applications. These applications can process double-byte kanji characters as a single entity. They accept and display shift-JIS kanji inputs, advance the cursor and handle backspaces with kanji text, sort kanji text, and do right justification on kanji text. However, these applications do not handle in-line kana-kanji conversion, and the conversion process is transparent to these applications. The conversion is done by a separate conversion module called KKAPP which is activated and deactivated using special keys. In-line conversion means that the kana and kanji input actually takes place in the applications window and not in the pop-up windows at the bottom of the screen. KKAPP defines the user interface for kanji conversion for all level 2 applications. Most kanji applications are currently at this level. Level 3: Applications with Full Kanji Support. These applications can handle kana-kanji conversion. They call the KKAPP using the level 3 application function ConvertRequest. They allow the user to change a kanji selection from a list of kanji candidates having the same pronunciation, input kanji strings without using an explicit convert command (free input), do full sentence or full paragraph conversion, and select kanji candidates from a menu list using the mouse. Conversion Under Windows 1.03 Under MS-DOS, the conversion module traps the keyboard input, converts it to the appropriate 2-byte characters and then transfers those characters to MS-DOS. Under Windows there are six modules that are involved in kana-kanji conversion: the keyboard driver, USER, FEP, KKAPP, KKLIB, and the application. Figure 10 describes each of the steps. Keyboard Driver Kana-kanji conversion starts with the user typing in a key. This keyboard input is processed by the Windows keyboard driver and sent to USER. The USER library module turns keyboard input into a Windows message (either WM_KEYUP or WM_KEYDOWN) and sends the message to the appropriate module. If WH_KEYBOARD has been set using SetWindowsHook, then USER sends the keyboard messages to the keyboard filter; otherwise, USER sends the keyboard message to the current application. The KKLIB library module sits between USER and KKAPP and controls the flow of keyboard input. Upon request from KKAPP, KKLIB will install the keyboard filter function to receive all the keyboard input. In the filter function, KKLIB decides whether to send the keyboard message to KKAPP or to return the keyboard message to USER without processing it (as when the kana-kanji conversion mode is turned off). KKLIB also provides an entry point in level 3 applications for kana-kanji conversion requests or inquiries. This function in KKLIB (ConvertRequest()) in turn translates these requests into Windows messages (WM_ CONVERTREQUEST) and sends the messages to KKAPP. The KKAPP application module does the kana-kanji conversion. Depending on which kind of application is using KKAPP (level 2 or level 3), KKAPP will take the appropriate actions. If a level 2 application is the active window and the conversion mode is on, KKAPP will open up its conversion window to display the conversion process, and send the final result to the application through WM_CHAR messages. If a level 3 application is active, KKAPP does not display the conversion process and simply answers convert requests from the application. One KKAPP is required for each FEP. The front end processor module does the actual conversion. This module has the dictionary and the conversion algorithm and KKAPP interfaces directly to it. It is installed as an MS-DOS device driver. A level 2 application is not involved in kana-kanji conversion. It simply receives the converted results in WM_CHAR messages from KKAPP. A level 3 application does participate in kana-kanji conversion by calling ConvertRequest() and receives the results back in the KanjiStruct parameter. Double-Byte Kanji Writing a Windows application for the Japanese market involves a number of things. First, all messages and resources in the resource file need to be translated into Japanese. The double-byte support code of the resource compiler is enabled when the keyword KANJI is seen in the resource file. This keyword must be followed by four numbers stating the two ranges of valid first byte ranges of kanji. Figure 11☼ provides an example of the translated resource file for the clipboard desktop application. Windows virtualizes 2-byte support by providing string handling routines. Applications should not make any assumption about the system they are running on. Using the function AnsiUpper to convert to an upper character or the function AnsiLower to convert to a lower character enables the same application to run correctly both in the U.S. and in other countries. Windows further provides the AnsiNext call to scan forward and AnsiPre to scan backwards. It is important to use these calls so that an application does not get out of sync and misinterpret consecutive kanjis as different kanjis. Another problem is that the second byte of the shift-JIS character set overlaps with Roman characters starting at 40H. Applications should be careful not to mistreat the second byte of a kanji as a Roman character, particularly when implementing the searching function or when parsing path names looking for the '\' file separator. In screen editing, applications that use Windows Edit Control will run without modification in Japan. Applications that do not use Edit Control should buffer two WM_CHAR messages to make one single TextOut() call when 2-byte kanjis are encountered. When the application is advancing, backspacing, positioning the cursor, and line-wrapping on the screen, it must treat 2-byte kanjis as one entity. Lastly, sorting that involves kanji is slightly more complicated. The shift-JIS character set places its 1-byte kanas in the A0H-CFH range, and the kanjis in the two ranges 80H-9FH and E0H-FCH. In Japan, 1-byte kanas are sorted after 1-byte Ro-man characters in the 20H-7EH range, but before all the 2-byte characters. This necessitates swapping kana characters with the kanjis which are in the 80H-9FH range when doing a sort. In Conclusion Microsoft is currently working on an additional specification for the kana-kanji interface for Windows 2.0. In the meantime, ISVs should be able to see that a major opportunity exists to export Windows-based applications to the Japanese market. Hopefully, the information provided here will offer some insight into the work required to pursue it. ISVs interested in developing Japanese Windows applications are urged to contact the OEM Technical Support Group, Microsoft KK, Sanban cho Yayoi kan, 6-2 Sanban cho, Chiyoda ku, Tokyo, JAPAN, for more detailed information. We look forward to hearing from you. Figure 8: Microsoft Kanji Encoding Scheme 00 • 10 • 21 7E ▼ ▼ 20 • 21►┌───────────────────────┐ │ │ 30 • │ │ │ JIS level 1 │ 40 • │ │ │ │ 50 • ├───────────────────────┤ │ │ 60 • │ JIS level 2 │ │ │ 70 • │ │ 7E►└───────────────────────┘ 80 • 81►┌──────────────┐┌───────┬──────────────────────┐ │ Microsoft translation of JIS level 1 │ 90 • │ ││ │ │ ├──────────────┤├───────┼──────────────────────┤ A0 • 9F►└─────────────┘└───────┴─────────────────────┘ Microsoft translation of JIS level 2, part 1 B0 • │ │ │ │ │ │ C0 • 40 7E 80 9E 9F FC │ │ │ │ │ │ D0 • │ │ │ │ │ │ ▼ ▼ ▼ ▼ ▼ ▼ E0 • E0►┌──────────────┐┌───────┬──────────────────────┐ EF►│ Microsoft translation of JIS level 2, part 2 │ F0 • F0►├──────────────┤├───────┼──────────────────────┤ FC►Extra room for extensions to the JIS kanji standard └──────────────┘└───────┴──────────────────────┘ • • • • • • • • • • • • • • • • 00 10 20 30 40 50 60 70 80 90 A0 B0 C0 D0 E0 F0 Figure 10: The kana-kanji conversion process under Windows Version 1.03. ┌─────────────────────────────┐ │ Keyboard Drivers │ ░░ Level 1 │ │ └─▒──▓──────────────────────░─┘ ▒▒ Level 2 ▒ ▓ ░ ┌─▼──▼──────────────────────▼─┐ ▓▓ Level 3 │ USER.EXE │ │ ▓▓▓▓▓▓▓ │ └─▒──▓──────────▓─▒▒▒─▓────░─┘ Keyboard ▓ ▒ ▒ ▓ ░ hook set Conversion ░ ▒ ▓ mode off ░ ┌─────▼──▼────┐ ▓ ▒ ▒ ▓ No │ KKLIB ▓▓▓▓▓▓▓ ▒ ▒ ▓ keyboard │ ▒▒▒▒▒▒▒▒▒ ▒ ▓ hook └─────▒──▓────┘ ▒ ▓ ░ ▒ ▓ ▒ ▓ ░ ┌─────────────┐ ┌─────▼──▼────┐ ▒ ▓ ░ │ FEP │◄───►│ KKAPP │ ▒ ▓ ░ │ │ │ │ ▒ ▓ ░ └─────────────┘ └─────▒──▓───┘ ▒ ▓ ░ ▒ ▓ ▓ ▒ ▓ ░ Conversion Virtual ▒ ▓ ░ result kanji ▒ ▓ ░ ▒ ▓ key ▒ ▓ ░ ▒ ▓ interface ▒ ▓ ░ ▒ ▓ ▓ ▒ ▓ ░ Conversion request and ░ ▒ ▓ results back ▒ ▓ ░ ▒ ▓ ▓ ▒ ▓ ░ ╔══▼══▼═══▓═══════════▼═▼════▼═══╗ ║ Applications ║ ║ ║ ╚════════════════════════════════╝ ████████████████████████████████████████████████████████████████████████████ Utilizing OS/2 Multithread Techniques in Presentation Manager Applications Charles Petzold☼ Microsoft(R) Windows is a multitasking operating environment──but not really. As most Windows programmers know, Windows is actually a "nonpreemptive multitasking" environment. It does not perform the preemptive time- slicing we normally associate with a multitasking system. Instead, Windows multitasks programs based on the presence of "messages" (which often represent keyboard and mouse input) in the programs' message queues. When a Windows program calls the GetMessage function to retrieve the next message from its message queue, and the message queue is empty, Windows suspends the program. Windows then switches to another program with a nonempty message queue. This causes that other program to return from its own GetMessage call to process the message. Only one Windows program is running at any time. The rest are suspended in the GetMessage function. Windows programmers are well aware of the problems associated with this form of nonpreemptive multitasking. If a Windows program requires a long period of time to process a message, then other programs running under Windows are effectively halted for the duration. Windows programmers must use some special techniques (which we'll look at in this article) when doing lengthy processing to prevent the program from suspending the rest of the system. Microsoft OS/2 Presentation Manager is a windowing environment with a message-based architecture much like that of Microsoft Windows. But unlike Windows, the OS/2 Presentation Manager runs under a true priority-based preemptive multitasking operating system. At first, the preemptive multitasking of the OS/2 systems would seemingly eliminate the problems associated with the nonpreemptive nature of Windows. You might conclude that Presentation Manager programs can spend as much time as they need processing a message without worrying about suspending other programs. But this is not so. The problems that arise when doing lengthy processing in Windows result more from the message-based architecture rather than from nonpreemptive multitasking. In that sense, the Presentation Manager is the same as Windows. The real difference is that the Presentation Manager provides more and better solutions to the problem of lengthy processing. That's what we'll explore here. The "Big Job" Problem Presentation Manager programs can usually process most keyboard and mouse input very quickly. In a word processing program, for example, a character typed from the keyboard need only be inserted into the document and displayed on the screen. But many programs must also carry out commands that require more lengthy processing. My term for this lengthy processing is the "big job." In a Presentation Manager spreadsheet program, the big job is a recalculation or the execution of a long macro. In a database program, the big job is a file sort or indexing. In a word processing program, it's a pagination or spelling check. In a CAD program, it's redrawing the screen. In a communications program, it's reading the serial port when an incoming character is not immediately available. In most any Presentation Manager program, printing is a big job. A big job is anything that takes more than 1/10 second. This is based on the recommendation in the Presentation Manager documentation that programs take no more than 1/10 second to process a message. Although this is a guideline rather than a hard-and-fast rule, I'll refer to it here as the "1/10 second rule." To explore the big job problem, let's write a Presentation Manager program that does a big job-specifically, a calculation called the "savage" benchmark that is often used to test floating-point speed. The program will allow repeating the savage calculation 10, 100, 1000, or 10,000 times depending on a menu selection. I'll use the "alternate" floating-point library in this program so that the calculation isn't affected by the presence of an 80287 math coprocessor. The program will have menu options to start the calculation running and-if possible-to abort it before it's finished. The 10,000 repetitions of the savage benchmark calculation takes about 3 minutes on an 8-MHz IBM(R) PC/AT(R Even 10 repetitions violate the 1/10 second rule. Five different versions of this program will be examined, with each successive version more sophisticated in its approach to multitasking under the Presentation Manager. The Naive Approach The simplest approach to a big job is shown in the BIGJOB1 program in Figures 1 and 2. Most of the code in BIGJOB1's client window procedure, which is called ClientWndProc, handles WM_COMMAND messages from the program's menu. When you select an option from the Repetitions menu, BIGJOB unchecks the currently selected option and checks the option you choose. When you select Start from the menu, BIGJOB1 disables the Start option, enables the Abort option, and begins the calculation. After it's finished with the big job, the program reenables the Start option and then exits the window procedure. Because BIGJOB1 does the entire calculation in response to a WM_COMMAND message from the menu, it obviously violates the 1/10 second rule. BIGJOB1 is a bad program. I show it only to illustrate how such programs clog up the rest of the Presentation Manager. While BIGJOB1 is doing the big job, you cannot switch to another program running under the Presentation Manager. The system seemingly ignores all keyboard and mouse input until the calculation is finished. Although the Abort option is present on BIGJOB1's menu, you can't use the keyboard or mouse to select that option. Once you begin the big job, you have to wait until it's finished to be able to do anything else. At first, this seems disturbing. Isn't the Presentation Manager supposed to be multitasking? And if so, why does one program apparently cause the whole system to grind to a halt? What is happening here is a predictable result of the message-based architecture of the Presentation Manager. Using Messages Programs that run under the Presentation Manager are message driven. In the normal case, a program spends most of its time suspended in WinGetMsg (the Presentation Manager equivalent of the Windows GetMessage function) waiting for a message. Each window created by a program has a window procedure that processes messages to the window. At least one of these window procedures──the window procedure for the client window──is located in the Presentation Manager program. The other window procedures (such as those for the frame window, the title bar window, and the menu window) can be found in the Presentation Manager PMWIN.DLL dynamic-link library. Some of the messages for a window are stored in the program's message queue. These messages are called "queued" messages and are sometimes said to be "posted" to the queue. The queued messages are retrieved from the message queue when a program calls WinGetMsg and are dispatched to the window procedure by WinDispatchMsg. Other messages are sent directly to the window procedure, bypassing the program's message queue. Regardless of whether a message is posted to a message queue or sent directly to a window procedure, and regardless of whether a window procedure is contained in the program or in a dynamic-link library, messages to windows created by a thread of execution are always processed in that thread. A particular thread of execution can only do one thing at a time. A thread cannot be multitasked with itself. This seems obvious, yet the message-based architecture of the Presentation Manager sometimes obscures this simple fact. When you select Start from BIGJOB1's menu, the program begins the big job. You then try to use the Alt-Tab key combination to switch to another program. The window that must process these keystrokes is BIGJOB1's frame window. But the window procedure for the frame window must run in the same thread as the client window, and the client window is busy doing the big job. This means that the Alt-Tab keyboard message cannot be processed until the calculation is finished, and until BIGJOB1 exits ClientWndProc and then calls WinGetMsg to retrieve the message from the queue. This explains why the Presentation Manager seems to be ignoring keyboard input while BIGJOB1 is calculating. Serialization of Input There may, however, be a way around this. As you know, you can also use the mouse to make another window active. Maybe that will work. To test this possibility, you must first position the mouse pointer on top of another program's window and then use the keyboard to select Start from BIGJOB1's menu. While it is calculating, you press the mouse button and... nothing happens. This again is initially disturbing. Because the Presentation Manager is a true multitasking system, the other program ought to be able to read that mouse click even while BIGJOB1 is calculating. The Presentation Manager should also be able to change the active window from BIGJOB1 to the other program. This does not happen for a couple of reasons. First, the Presentation Manager serializes all keyboard and mouse input in a system message queue. Keyboard and mouse input is passed one message at a time to an application's message queue based on which window has the input focus (in the case of keyboard messages) and which window is underneath the mouse pointer. The serialization of mouse and keyboard input in a system message queue is required to correctly handle "type-ahead" and "mouse-ahead" input from the user. One of the keystrokes or mouse clicks in the system message queue could have the effect of changing the active window and the focus window. Subsequent keyboard input must then go to that new window. This won't work if mouse and keyboard input is not passed to applications in the same order that it enters the system message queue. Therefore, a keyboard or mouse message cannot be passed to a particular application until all previous keyboard and mouse messages have been processed. In this particular case, another application cannot read a mouse message until BIGJOB1 processes all its keyboard input. The keyboard input that it hasn't processed is the release of the key that caused the menu to send the WM_COMMAND message that started the calculation. Thus, because BIGJOB1 renders itself resistent to keyboard or mouse input, it also prevents all other programs running under the Presentation Manager from getting keyboard or mouse input. Even if another program could read a mouse click, the Presentation Manager cannot change the input focus from BIGJOB1 to another program while BIGJOB1 is busy doing the calculation. In order to change the input focus, the Presentation Manager must send a WM_SETFOCUS message to the window losing the input focus. That WM_SETFOCUS message is blocked because the window that must receive the message is part of BIGJOB1's thread, and BIGJOB1 is busy doing the big job. Messages are not like hardware interrupts. Although a window procedure can be sent a message as a result of calling WinDefWindowProc, and a window procedure can be sent a message as a result of calling some other Presentation Manager functions, these are examples of recursion in window procedures. Messages do not preemptively interrupt a thread and start execution someplace else in the same thread. Now that we've seen how BIGJOB1 effectively disables keyboard and mouse input in the Presentation Manager, the reason for the 1/10 second rule should be obvious. Presentation Manager programs must continually interact with the system, retrieving and processing their messages promptly. Not Exactly Like Windows As bad as BIGJOB1 is, the Presentation Manager can still multitask programs while BIGJOB1 is running. This is a difference between the Presentation Manager and Windows. If you have a Windows version of BIGJOB1 running under Windows along with the Windows CLOCK program, CLOCK will stop dead while BIGJOB1 is calculating. Under Windows, only one program can be running at any time. However, when you run BIGJOB1 in the Presentation Manager along with a Presentation Manager version of CLOCK (like the one included in my book, Programming the OS/2 Presentation Manager, Microsoft Press, 1988) you'll find that CLOCK continues to run while BIGJOB1 is calculating. CLOCK and BIGJOB1 run concurrently. CLOCK works by setting a timer and then processing a WM_TIMER message every second. A WM_TIMER message does not have to be serialized with the keyboard and mouse input. CLOCK can continue to receive these messages even if BIGJOB1 has clogged up keyboard and mouse input. Using a Timer Once you acknowledge that BIGJOB1 is a very bad Presentation Manager program, you are stuck with the problem of structuring it so that it works correctly. The use of a timer in CLOCK suggests one possible remedy: you can divide a big job up into little pieces, set the Presentation Manager timer, and do each little piece on receipt of a WM_TIMER message. This is a solution that works both in the Presentation Manager and in Windows. The BIGJOB2 program uses a timer to do the big job. The files BIGJOB2, BIGJOB2.C, and BIGJOB2.DEF are shown in Figure 3. Compiling BIGJOB2 also requires the BIGJOB.H and BIGJOB.RC files shown in Figure 1. When you select the Start option from BIGJOB2's menu, BIGJOB2 calls WinStartTimer to start the timer, disables the Start option, enables the Abort option, and initializes a few variables. The savage calculation is done once for each WM_TIMER message. Thus, for 1000 repetitions, the big job is finished after 1000 WM_TIMER messages. WM_TIMER messages are low-priority queued messages. If the message queue contains keyboard or mouse messages, these messages will be retrieved from the queue and processed before a WM_TIMER message. Thus, BIGJOB2 continues to read keyboard and mouse input and can allow the user to select Abort from BIGJOB2's menu, move or resize BIGJOB2's window, or to move to another program. The entire system-including BIGJOB2-continues to function normally all the time that BIGJOB2 is doing the calculation. Timer Problems The timer approach is not too bad for BIGJOB2, but it's easy to imagine cases where the timer would be inadequate. A program using the timer for a big job must continually reenter and leave the processing loop with every WM_TIMER message. This is easy to structure when a single loop is involved, but it becomes a nightmare for more complex jobs with lots of nested loops. The timer also slows down the big job. It is not possible to receive WM_TIMER messages at a rate faster than that of the hardware clock. Under OS/2, this means the program receives a WM_TIMER message only once every 31.25 milliseconds. But BIGJOB2 spends less than 20 milliseconds (on an 8- MHz IBM PC/AT) processing each WM_TIMER message. Because the calculation is paced by the timer, the calculation won't finish any faster on a 20-MHz 80386 machine. Clearly, as a general solution to the big job program, the timer approach must be rejected. The Peek Message Another solution that is very common in Windows is the use of the PeekMessage function (called WinPeekMsg in the Presentation Manager). The PeekMessage function is very similar to GetMessage. Under Windows, the PeekMessage function first checks if there are messages waiting in the program's message queue. If so, PeekMessage fills the message structure with the next message from the queue and returns a non-zero value. If the message queue of the program calling PeekMessage is empty, then Windows switches to a program with a nonempty message queue to allow that program to process its own messages. (This is often called "yielding" to other programs.) When no messages remain in any program's message queues, then PeekMessage returns control to the original program and returns a zero value. PeekMessage allows all Windows programs to process outstanding messages but then returns control to the program calling PeekMessage after all this message processing is completed. Using PeekMessage allows a program to do a big job while periodically allowing itself and other programs to process their messages. Several Windows programs, including SPOOLER and TERMINAL, use PeekMessage in this way in order to simulate multitasking. Programs that print frequently use PeekMessage to allow a user to cancel a print job from a dialog box. The WinPeekMsg function in the Presentation Manager is very similar to PeekMessage. However, in the Presentation Manager, WinPeekMsg is used by a program to process its own messages rather than to yield to other programs. Multitasking occurs normally in the Presentation Manager if a program does not clog up the processing of keyboard and mouse input. The WinPeekMsg function provides a good solution to the big job problem. BIGJOB3 in Figure 4 shows how this is done. Like BIGJOB2, BIGJOB3 does the full calculation in response to a WM_COMMAND message. However, within the calculation loop, BIGJOB3 calls WinPeekMsg to determine if any messages are in its message queue. If so, it removes them with WinGetMsg and dispatches them to a window procedure with WinDispatchMsg, just as in the normal message loop in main. One of these messages could be another WM_COMMAND message generated when the user selects Abort from the menu. BIGJOB3 uses the bContinueCalc variable to indicate when the calculation has been aborted. You'll notice that some special processing is required for the WM_QUIT message. This message is posted to the message queue by the Presentation Manager as a default response when the user selects Close from the system menu. The WM_QUIT message should not be removed from the queue within the window procedure. Instead, BIGJOB3 exits the window procedure so that the WM_QUIT message can be retrieved from the message queue in the main function. Although message peeking usually works well in Windows and Presentation Manager programs, it's always a little messy in practice. The PeekMessage and WinPeekMsg functions must be called frequently enough to give the system a good response time, yet there's an inordinate amount of code involved. If the big job must be aborted, it's sometimes difficult to get out of a calculation loop in a structured manner. In short, code involving WinPeekMsg often looks like a kludge. A Second Thread The solutions in BIGJOB2 and BIGJOB3 are applicable in both Windows and the Presentation Manager. Now let's do something that is impossible in Windows but a natural in the Presentation Manager: create a second thread of execution to do the big job. When a program contains multiple threads of execution, the multiple threads run concurrently. All threads in a process share the program's resources──such as open files, memory, semaphores, and queues──but each thread has its own CPU state, dispatching priority, and stack. Within a program, a second thread of execution looks like a function. All local automatic variables in the thread function are private to the thread because they are stored on the thread's stack. Local static variables in the thread function can be shared by all threads based on that same function. When programming for the Presentation Manager, threads fall into two categories: a thread is either a "message queue thread" or a "nonmessage queue thread." A thread becomes a message queue thread when it calls WinCreateMsgQueue. It reverts back to being a nonmessage queue thread when it calls WinDestroyMsgQueue. A Presentation Manager program always creates a message queue in at least one thread. A thread must create a message queue before it can create windows. The message queue is used to store messages for all of the windows created in the thread. However, other threads in a Presentation Manager program do not need to create message queues if they do not create windows. Thread Restrictions In a Presentation Manager program, nonmessage queue threads have some advantages over message queue threads──and some disadvantages. The good news is that a nonmessage queue thread is not bound by the 1/10 second rule. Because the thread does not receive or process messages, it needn't worry about clogging up the processing of messages in message queue threads. Thus, a nonmessage queue thread is often ideal for doing big jobs. The bad news is that nonmessage queue threads are restricted in the Presentation Manager functions they may call. Basically, a thread without a message queue cannot create windows, cannot send a message to a window procedure in a message queue thread, and cannot call any function that causes a message to be sent to a window procedure. Some of these restrictions are obvious: a nonmessage queue thread cannot create a window because it has no queue to store messages to that window. However, a nonmessage queue thread can call some functions that affect windows created in message queue threads. For example, a nonmessage queue thread can obtain a presentation space handle for a window created in a message queue thread and can paint something on the surface of that window. Nonmessage queue threads cannot send messages to message queue threads. The WinSendMsg function is not allowed. Nor can they call functions that send messages. For example, WinDestroyWindow cannot be called from a nonmessage queue thread because it sends a window procedure a WM_DESTROY message. The functions that are forbidden to nonmessage queue threads are listed in the Presentation Manager documentation. Although a nonmessage queue thread cannot send a message using WinSendMsg, the thread can post a message by calling WinPostMsg. This latter function places the message in a thread's message queue and returns immediately. Semaphores Threads within a single process must often communicate with each other in various ways. The execution of threads must be coordinated so that the threads don't step on each other's toes. This requires some handshaking. Often, threads must also signal each other and pass data among themselves. A message queue thread generally communicates with a nonmessage queue thread by using semaphores. A nonmessage queue thread communicates with a message queue thread by using posted messages. Both threads can also access common global variables. The BIGJOB4 program in Figure 5, which uses a nonmessage queue thread to do a big job, shows this signaling and communication. The global variable lSemTrigger is a RAM semaphore. This semaphore is set during the WM_CREATE message in ClientWndProc. The window procedure then creates a second thread by calling the OS/2 DosCreateThread function using SecondThread as the thread function. SecondThread waits for lSemTrigger to be cleared. ClientWndProc clears the semaphore when the user selects Start from BIGJOB4's menu. SecondThread then does the calculation. When it's finished, it posts a WM_CALC_DONE message to the window procedure. (This is a "user-defined" message defined within BIGJOB4.C.) The elapsed time of the calculation is passed to the window procedure in the lParam1 variable that accompanies this message. Aborting the Calculation BIGJOB4 also allows the user to abort the calculation. When Abort is selected from the menu, the window procedure sets the bContinueCalc global variable to FALSE and disables the Abort menu option. SecondThread checks this bContinueCalc variable during the calculation and breaks from the loop when the variable is no longer non-zero. The thread then sets the semaphore and posts a WM_CALC_ABORTED to the window. On receipt of this message, the client window enables the Start menu option again. The semaphore is already set, so SecondThread can't proceed with a new calculation until Start is chosen again. This is an example of some simple handshaking between the two threads. Note that the semaphore is used only for blocking and unblocking the nonmessage queue thread. A message queue thread should not be made to wait on a semaphore because that may violate the 1/10 second rule. If absolutely necessary, a nonmessage queue thread could suspend a message queue thread for very short periods of time by calling DosSuspendThread or DosEnterCritSec. This is sometimes helpful when both threads access global variables. (It's not necessary in BIGJOB4 when the threads access bContinueCalc because this variable is altered or accessed in one machine code instruction.) The message queue thread in the BIGJOB4 program calls DosSuspendThread to suspend SecondThread after exiting the message loop in main before destroying the window and message queue. SecondThread could be in the middle of a calculation at this time. Suspending the thread prevents it from posting a message to the message queue after the message queue is destroyed. Thinking Threads A nonmessage queue thread is nearly essential in Presentation Manager programs that must read input other than the keyboard and the mouse. An obvious example of this is a communications program. Such a program would have a client window in the message queue thread that processes keyboard messages, writes the characters to the communications port using DosWrite, and (if local echo is in effect) also writes the characters to the surface of the window. The nonmessage queue thread reads the communications port with the DosRead function. Used most efficiently, this function does not return unless a character has been read from the serial port. A message queue thread should not call DosRead to get input from the serial port because it may violate the 1/10 second rule. When the nonmessage queue thread reads a character, it can post a user-defined message to the window. The client window processes the message by displaying the character to the window. A Presentation Manager program using queues for interprocess communication should also create a nonmessage queue thread for reading the queue. The nonmessage queue thread calls the DosReadQueue function with the "no wait" flag set to 0, thus blocking the thread until there is something in the queue. In BIGJOB4, the second thread is created once and unblocked whenever it has to do the big job. If a program has numerous big jobs handled in lots of different thread functions, it would be best not to create all these threads initially, but to create a thread each time it is needed. When a thread is finished with its big job, it can post a message to the client window and terminate using the DosExit call with a first parameter of 0. Object Windows BIGJOB4 shows how a program can handle big jobs in a nonmessage queue thread. A Presentation Manager program can also do big jobs in a second message queue thread that creates "object windows." Object windows are created in a message queue thread just like normal windows. They have a window procedure that processes messages to the object window, again just like normal windows. However, object windows do not appear on the screen and do not get keyboard and mouse input. In fact, object windows basically receive only two messages: WM_CREATE and WM_DESTROY. The WM_CREATE message is sent during the WinCreateWindow call that creates the object window, and the WM_DESTROY message is sent during the WinDestroyWindow call that destroys the window. (In the pre-release version of the Presentation Manager I have, object windows also receive a WM_ADJUSTWINDOWPOS message concurrently with the WinCreateWindow call.) You normally use object windows specifically to receive messages from other windows, perhaps to implement an object-oriented programming language. But object windows also help out in processing big jobs. If a thread creates only object windows, then there are only a very limited number of messages the window procedure can receive. This greatly simplifies the processing of these messages in the object window's window procedure. There are several advantages to using object windows for big jobs. The object window is not restricted to a subset of Presentation Manager functions. You can thus use messages for all communications between the client window and the object window. If you've come to regard message-driven architecture as inherently superior to traditional top-down architecture, you may prefer this. The use of object windows in separate threads is more in tune with the architecture of the Presentation Manager than are simple nonmessage queue threads. The use of an object window in a second message queue thread is shown in the BIGJOB5 program in Figure 6. The BIGJOB5.C file defines six messages that the client window and object window use for communicating with each other. BIGJOB5 generally creates the second thread after creating the program's main window. BIGJOB5's SecondThread function looks a lot like the main function. Although it does not call WinInitialize, it registers a window class and creates a window. The WinCreateWindow call to create an object window is very simple: the parent window parameter is set to HWND_OBJECT, the second parameter is set to the name of the window class, and all other parameters are set to 0 or NULL. SecondThread then sends a WM_OBJECT_CREATED message to ClientWndProc to notify it that the object window has been created and is ready for work. SecondThread then enters a normal message loop. When you select Start from BIGJOB5's menu, the client window posts the message WM_START_CALC to the object window. Similarly, selecting Abort from BIGJOB5's menu causes the client window to post a WM_ABORT_CALC message. Note as well that ClientWndProc processes the normal WM_CLOSE message (generated by selecting Close from the system menu) by posting a WM_QUIT message to the object window. ObjectWndProc begins the big job in response to the WM_START_CALC message. While doing the calculation, it can be interrupted only by one of two possible messages posted to its message queue. These messages are WM_ABORT_CALC and WM_QUIT. In either case, the calculation can be unconditionally aborted. In the calculation loop, ObjectWndProc calls the WinQueryQueueStatus function and checks the QS_POSTMSG bit of the return value. (It should be necessary only to check that the return value is non-zero, but in the version of the Presentation Manager I used, this function was a little buggy.) This check of WinQueryQueueStatus is almost as simple as checking bContinueCalc in BIGJOB4, but not nearly as complex as the code involved with using WinPeekMsg in BIGJOB3. ObjectWndProc notifies ClientWndProc that it has finished or aborted a calculation by posting the messages WM_CALC_DONE and WM_CALC_ABORTED respectively. ClientWndProc uses these messages to enable its menu for further calculations. Ending the program in an orderly manner (that is, where both main and SecondThread destroy their own windows and message queues) is a little tricky. ClientWndProc processes a WM_CLOSE message by posting a WM_QUIT message to the object window. This causes ObjectWndProc to abort the big job if it's doing one at the time. When this WM_QUIT message is retrieved from SecondThread's message queue, WinGetMsg returns 0. SecondThread destroys its window, its message queue, and posts a WM_OBJECT_DESTROYED message to ClientWndProc. On receipt of this message, ClientWndProc posts itself a WM_QUIT message and terminates normally. The use of an object window for big jobs is certainly more complex than the use of a nonmessage queue thread, so you may not like it. Moreover, the object window approach is not good for some big jobs. For example, when a second thread must use DosRead to get input from a communications port or DosReadQueue to get input from a queue, the thread is blocked within the OS/2 function and obviously cannot check the message queue status using WinQueryQueueStatus. No More "Please Wait" We started out looking at BIGJOB1, a program that did the job it was meant to do, but did it in a way that was not advantageous for the user. Our immediate rejection of this program and our search for better ways of doing big jobs indicates some major changes in our conception of proper behavior in application programs. In a traditional single-tasking nonwindowed environment, of course you have to wait while the database program is sorting a file. When you start a file sort, it's time to take a coffee break. In a traditional multitasking operating environment, you may be able to let the database program sort in the background, and you can work on another program while waiting for the sort to finish. In a multitasking window environment like the Presentation Manager, however, we are satisfied only when the user can continue to interact with a program even when it's doing a big job. Obviously, the complexities involved with structuring a program in this way require some extra work on the part of the programmer. But that makes the program better to use. Just as we can no longer tolerate programs that require the user to memorize scores of commands, we can no longer tolerate programs that throw up a "Please Wait" message and require a user to wait until the program finishes its big job. Figure 1: BIGJOB.RC resource script file #include <os2.h> #include "bigjob.h" MENU ID_RESOURCE { SUBMENU "~Repetitions", IDM_REPS { MENUITEM " 10", IDM_10, MIA_CHECKED MENUITEM " 100", IDM_100 MENUITEM " 1000", IDM_1000 MENUITEM " 10000", IDM_10000 } SUBMENU "~Action", IDM_ACTION { MENUITEM "~Start", IDM_START MENUITEM "~Abort", IDM_ABORT, MIA_DISABLED } } BIGJOB.H header file #define ID_RESOURCE 1 #define IDM_REPS 1 #define IDM_ACTION 2 #define IDM_10 10 #define IDM_100 100 #define IDM_1000 1000 #define IDM_10000 10000 #define IDM_START 20 #define IDM_ABORT 21 /* Definitions, functions, and variables for BIGJOBx.C */ #ifndef RC_INVOKED /* This stuff not needed for .RC file */ #define STATUS_READY 0 #define STATUS_WORKING 1 #define STATUS_DONE 2 ULONG EXPENTRY ClientWndProc (HWND, USHORT, ULONG, ULONG) ; HAB hab ; double Savage (double A) { return tan (atan (exp (log (sqrt (A * A))))) + 1.0 ; } VOID CheckMenuItem (HWND hwnd, SHORT iMenuItem, BOOL bCheck) { HWND hwndParent = WinQueryWindow (hwnd, QW_PARENT, FALSE) ; HWND hwndMenu = WinWindowFromID (hwndParent, FID_MENU) ; WinSendMsg (hwndMenu, MM_SETITEMATTR, MAKEULONG (iMenuItem, TRUE), MAKEULONG (MIA_CHECKED, bCheck ? MIA_CHECKED : 0)) ; } VOID EnableMenuItem (HWND hwnd, SHORT iMenuItem, BOOL bEnable) { HWND hwndParent = WinQueryWindow (hwnd, QW_PARENT, FALSE) ; HWND hwndMenu = WinWindowFromID (hwndParent, FID_MENU) ; WinSendMsg (hwndMenu, MM_SETITEMATTR, MAKEULONG (iMenuItem, TRUE), MAKEULONG (MIA_DISABLED, bEnable ? 0 : MIA_DISABLED)) ; } VOID PaintWindow (HWND hwnd, SHORT iStatus, SHORT iRep, LONG lTime) { static CHAR *szMessage [3] = { "Ready", "Working ...", "%d repetitions in %ld msec." } ; CHAR szBuffer [60] ; HPS hps ; WRECT wrc ; hps = WinBeginPaint (hwnd, NULL, NULL) ; GpiErase (hps) ; WinQueryWindowRect (hwnd, &wrc) ; sprintf (szBuffer, szMessage [iStatus], iRep, lTime) ; WinDrawText (hps, -1, szBuffer, &wrc, DT_CENTER | DT_VCENTER) ; WinEndPaint (hps) ; } #endif Figure 2: BIGJOB1 Make File bigjob1.obj : bigjob1.c bigjob.h cl -c -FPa -G2sw -W3 -Zp bigjob1.c bigjob.res : bigjob.rc bigjob.h rc -r bigjob.rc bigjob1.exe : bigjob1.obj bigjob1.def bigjob.res link bigjob1, /align:16, /map, /nod slibc slibcp slibfa os2, bigjob1 rc bigjob.res bigjob1.exe BIGJOB1.DEF Module Definition File NAME BIGJOB1 DESCRIPTION 'BIGJOB Demo Program No. 1(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc BIGJOB1.C - Naive Approach to Lengthy Processing Job #define INCL_WIN #include <os2.h> #include <math.h> #include <stdio.h> #include "bigjob.h" INT main (VOID) { static CHAR szClassName [] = "BigJob1" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ClientWndProc, CS_SYNCPAINT | CS_SIZEREDRAW, 0, NULL) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX | FS_MENU, szClassName, "BIGJOB Demo No. 1", 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1, ULONG mp2) { static SHORT iCalcRep, iCurrentRep = IDM_10 ; static SHORT iStatus = STATUS_READY ; static ULONG lElapsedTime ; double A ; SHORT i ; switch (msg) { case WM_COMMAND: switch (LOUSHORT (mp1)) { case IDM_10: case IDM_100: case IDM_1000: case IDM_10000: CheckMenuItem (hwnd, iCurrentRep, FALSE) ; iCurrentRep = LOUSHORT (mp1) ; CheckMenuItem (hwnd, iCurrentRep, TRUE) ; break ; case IDM_START: EnableMenuItem (hwnd, IDM_START, FALSE) ; EnableMenuItem (hwnd, IDM_ABORT, TRUE) ; iStatus = STATUS_WORKING ; WinInvalidateRect (hwnd, NULL) ; iCalcRep = iCurrentRep ; lElapsedTime = WinGetCurrentTime (hab) ; for (A = 1.0, i = 0 ; i < iCalcRep ; i++) A = Savage (A) ; lElapsedTime = WinGetCurrentTime (hab) - lElapsedTime ; iStatus = STATUS_DONE ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; case IDM_ABORT: /* Not much we can do here */ break ; default: break ; } break ; case WM_PAINT: PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } Figure 3: BIGJOB2 Make File bigjob2.obj : bigjob2.c bigjob.h cl -c -FPa -G2sw -W3 -Zp bigjob2.c bigjob.res : bigjob.rc bigjob.h rc -r bigjob.rc bigjob2.exe : bigjob2.obj bigjob2.def bigjob.res link bigjob2, /align:16, /map, /nod slibc slibcp slibfa os2, bigjob2 rc bigjob.res bigjob2.exe BIGJOB2.DEF Module Definition File NAME BIGJOB2 DESCRIPTION 'BIGJOB Demo Program No. 2(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc BIGJOB2.C - Timer Approach to Lengthy Processing Job #define INCL_WIN #include <os2.h> #include <math.h> #include <stdio.h> #include "bigjob.h" #define ID_TIMER 1 INT main (VOID) { static CHAR szClassName [] = "BigJob2" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ClientWndProc, CS_SYNCPAINT | CS_SIZEREDRAW, 0, NULL) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX | FS_MENU, szClassName, "BigJob Demo No. 2", 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mpl, ULONG mp2) { static double A ; static SHORT i, iCalcRep, iCurrentRep = IDM_10 ; static SHORT iStatus = STATUS_READY ; static ULONG lElapsedTime; switch (msg) { case WM_COMMAND: switch (LOUSHORT (mp1)) { case IDM_10: case IDM_100: case IDM_1000: case IDM_10000: CheckMenuItem (hwnd, iCurrentRep, FALSE) ; iCurrentRep = LOUSHORT (mp1) ; CheckMenuItem (hwnd, iCurrentRep, TRUE) ; break ; case IDM_START: if (!WinStartTimer (hab, hwnd, ID_TIMER, 1)) { WinAlarm (HWND_DESKTOP, WA_ERROR) ; break ; } EnableMenuItem (hwnd, IDM_START, FALSE) ; EnableMenuItem (hwnd, IDM_ABORT, TRUE) ; iStatus = STATUS_WORKING ; WinInvalidateRect (hwnd, NULL) ; iCalcRep = iCurrentRep ; lElapsedTime = WinGetCurrentTime (hab) ; A = 1.0 ; i = 0 ; break ; case IDM_ABORT: WinStopTimer (hab, hwnd, ID_TIMER) ; iStatus = STATUS_READY ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; default: break ; } break ; case WM_TIMER: A = Savage (A) ; if (++i == iCalcRep) { lElapsedTime = WinGetCurrentTime (hab) - lElapsedTime ; WinStopTimer (hab, hwnd, ID_TIMER) ; iStatus = STATUS_DONE ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; } break ; case WM_PAINT: PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } Figure 4: BIGJOB3 Make File bigjob3.obj : bigjob3.c bigjob.h cl -c -FPa -G2sw -W3 -Zp bigjob3.c bigjob.res : bigjob.rc bigjob.h rc -r bigjob.rc bigjob3.exe : bigjob3.obj bigjob3.def bigjob.res link bigjob3, /align:16, /map, /nod slibc slibcp slibfa os2, bigjob3 rc bigjob.res bigjob3.exe BIGJOB3.DEF Module Definition File NAME BIGJOB3 DESCRIPTION 'BigJob Demo Program No. 3(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc BIGJOB3.C - PeekMessage Approach to Lengthy Processing Job #define INCL_WIN #include <os2.h> #include <math.h> #include <stdio.h> #include "bigjob.h" INT main (VOID) { static CHAR szClassName [] = "BigJob3" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ClientWndProc, CS_SIZEREDRAW, 0, NULL) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX | FS_MENU, szClassName, "BigJob Demo No. 3", 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1,ULONG mp2) { static BOOL bContinueCalc = FALSE ; static SHORT iStatus = STATUS_READY ; static SHORT iCalcRep, iCurrentRep = IDM_10 ; static ULONG lElapsedTime ; double A ; SHORT i ; QMSG qmsg ; switch (msg) { case WM_COMMAND: switch (LOUSHORT (mp1)) { case IDM_10: case IDM_100: case IDM_1000: case IDM_10000: CheckMenuItem (hwnd, iCurrentRep, FALSE) ; iCurrentRep = LOUSHORT (mp1) ; CheckMenuItem (hwnd, iCurrentRep, TRUE) ; break ; case IDM_START: EnableMenuItem (hwnd, IDM_START, FALSE) ; EnableMenuItem (hwnd, IDM_ABORT, TRUE) ; iStatus = STATUS_WORKING ; WinInvalidateRect (hwnd, NULL) ; iCalcRep = iCurrentRep ; bContinueCalc = TRUE ; lElapsedTime = WinGetCurrentTime (hab) ; qmsg.msg = WM_NULL ; for (A = 1.0, i = 0 ; i < iCalcRep ; i++) { A = Savage (A) ; while (WinPeekMsg (hab, &qmsg, NULL, 0, 0, PM_NOREMOVE)) { if (qmsg.msg == WM_QUIT) break ; WinGetMsg (hab, &qmsg, NULL, 0, 0) ; WinDispatchMsg (hab, &qmsg) ; if (!bContinueCalc) break ; } if (!bContinueCalc || qmsg.msg == WM_QUIT) break ; } lElapsedTime = WinGetCurrentTime (hab) - lElapsedTime ; if (!bContinueCalc || qmsg.msg == WM_QUIT) iStatus = STATUS_READY ; else iStatus = STATUS_DONE ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; case IDM_ABORT: bContinueCalc = FALSE ; break ; default: break ; } break ; case WM_PAINT: PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } Figure 5: BIGJOB4 Make File bigjob4.obj : bigjob4.c bigjob.h cl -c -FPa -G2sw -W3 -Zp bigjob4.c bigjob.res : bigjob.rc bigjob.h rc -r bigjob.rc bigjob4.exe : bigjob4.obj bigjob4.def bigjob.res link bigjob4, /align:16, /map, /nod slibc slibcp slibfa os2, bigjob4 rc bigjob.res bigjob4.exe BIGJOB4.DEF Module Definition File NAME BIGJOB4 DESCRIPTION 'BigJob Demo Program No. 4(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc BIGJOB4.DEF ── Second Thread Approach to Lengthy Processing Job #define INCL_WIN #define INCL_DOS #include <os2.h> #include <math.h> #include <stdio.h> #include "bigjob.h" #define WM_CALC_DONE (WM_USER + 0) #define WM_CALC_ABORTED (WM_USER + 1) VOID FAR SecondThread (VOID) ; BOOL bContinueCalc = FALSE ; HWND hwndClient ; SHORT iCalcRep ; LONG lSemTrigger ; TID idThread ; UCHAR cThreadStack [4096] ; INT main (VOID) { static CHAR szClassName [] = "BigJob4" ; HMQ hmq ; HWND hwndFrame ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ClientWndProc, CS_SIZEREDRAW, 0, NULL) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX | FS_MENU, szClassName, "BigJob Demo No. 4", 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; DosSuspendThread (idThread) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1, ULONG mp2) { static SHORT iCurrentRep = IDM_10 ; static SHORT iStatus = STATUS_READY ; static ULONG lElapsedTime ; switch (msg) { case WM_CREATE: DosSemSet (&lSemTrigger) ; if (DosCreateThread (SecondThread, &idThread, cThreadStack + sizeof cThreadStack)) WinAlarm (HWND_DESKTOP, WA_ERROR) ; break ; case WM_COMMAND: switch (LOUSHORT (mp1)) { case IDM_10: case IDM_100: case IDM_1000: case IDM_10000: CheckMenuItem (hwnd, iCurrentRep, FALSE) ; iCurrentRep = LOUSHORT (mp1) ; CheckMenuItem (hwnd, iCurrentRep, TRUE) ; break ; case IDM_START: iStatus = STATUS_WORKING ; WinInvalidateRect (hwnd, NULL) ; iCalcRep = iCurrentRep ; bContinueCalc = TRUE ; DosSemClear (&lSemTrigger) ; EnableMenuItem (hwnd, IDM_START, FALSE) ; EnableMenuItem (hwnd, IDM_ABORT, TRUE); break ; case IDM_ABORT: bContinueCalc = FALSE ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; default: break ; } break ; case WM_CALC_DONE: iStatus = STATUS_DONE ; lElapsedTime = mp1 ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; break ; case WM_PAINT: PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } VOID FAR SecondThread () { double A ; int i ; LONG lTime ; while (1) { DosSemWait (&lSemTrigger, -1L) ; lTime = WinGetCurrentTime (hab) ; for (A = 1.0, i = 0 ; i < iCalcRep ; i++) { if (!bContinueCalc) break ; A = Savage (A) ; } lTime = WinGetCurrentTime (hab) - lTime ; DosSemSet (&lSemTrigger) ; if (bContinueCalc) WinPostMsg (hwndClient, WM_CALC_DONE, lTime, 0L) ; else WinPostMsg (hwndClient, WM_CALC_ABORTED, 0L, 0L) ; } } Figure 6: BIGJOB5 Make File bigjob5.obj : bigjob5.c bigjob.h cl -c -FPa -G2sw -W3 -Zp bigjob5.c bigjob.res : bigjob.rc bigjob.h rc -r bigjob.rc bigjob5.exe : bigjob5.obj bigjob5.def bigjob.res link bigjob5, /align:16, /map, /nod slibc slibcp slibfa os2, bigjob5 rc bigjob.res bigjob5.exe BIGJOB5.DEF Module Definition File NAME BIGJOB5 DESCRIPTION 'BigJob Demo Program No. 5(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc ObjectWndProc BIGJOB5.C - Object Window Approach to Lengthy Processing Job #define INCL_WIN #define INCL_DOS #include <os2.h> #include <math.h> #include <stdio.h> #include "bigjob.h" #define WM_OBJECT_CREATED (WM_USER + 0) #define WM_START_CALC (WM_USER + 1) #define WM_ABORT_CALC (WM_USER + 2) #define WM_CALC_DONE (WM_USER + 3) #define WM_CALC_ABORTED (WM_USER + 4) #define WM_OBJECT_DESTROYED (WM_USER + 5) VOID FAR SecondThread (VOID) ; ULONG EXPENTRY ObjectWndProc (HWND, USHORT, ULONG, ULONG) ; HWND hwndClient, hwndObject ; UCHAR cThreadStack [8192] ; INT main (VOID) { static CHAR szClassName [] = "BigJob5" ; HMQ hmq ; HWND hwndFrame ; QMSG qmsg ; TID idThread ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ClientWndProc, CS_SIZEREDRAW, 0, NULL) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX | FS_MENU, szClassName, "BigJob Demo No. 5", 0L, NULL, ID_RESOURCE, &hwndClient) ; EnableMenuItem (hwndClient, IDM_START, FALSE) ; if (DosCreateThread (SecondThread, &idThread, cThreadStack + sizeof cThreadStack)) WinAlarm (HWND_DESKTOP, WA_ERROR) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } ULONG EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, ULONG mp1, ULONG mp2) { static SHORT iCalcRep, iCurrentRep = IDM_10 ; static SHORT iStatus = STATUS_READY ; static ULONG lElapsedTime ; switch (msg) { case WM_OBJECT_CREATED: EnableMenuItem (hwnd, IDM_START, TRUE) ; break ; case WM_COMMAND: switch (LOUSHORT (mp1)) { case IDM_10: case IDM_100: case IDM_1000: case IDM_10000: CheckMenuItem (hwnd, iCurrentRep, FALSE) ; iCurrentRep = LOUSHORT (mp1) ; CheckMenuItem (hwnd, iCurrentRep, TRUE) ; break ; case IDM_START: EnableMenuItem (hwnd, IDM_START, FALSE) ; EnableMenuItem (hwnd, IDM_ABORT, TRUE) ; iStatus = STATUS_WORKING ; WinInvalidateRect (hwnd, NULL) ; iCalcRep = iCurrentRep ; WinPostMsg (hwndObject, WM_START_CALC, MAKEULONG (iCalcRep, 0), 0L) ; break ; case IDM_ABORT: WinPostMsg (hwndObject, WM_ABORT_CALC, 0L, 0L) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; default: break ; } break ; case WM_CALC_DONE: iStatus = STATUS_DONE ; lElapsedTime = mp1 ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; EnableMenuItem (hwnd, IDM_ABORT, FALSE) ; break ; case WM_CALC_ABORTED: iStatus = STATUS_READY ; WinInvalidateRect (hwnd, NULL) ; EnableMenuItem (hwnd, IDM_START, TRUE) ; break ; case WM_PAINT: PaintWindow (hwnd, iStatus, iCalcRep, lElapsedTime) ; break ; case WM_CLOSE: if (hwndObject) WinPostMsg (hwndObject, WM_QUIT, 0L, 0L) ; else WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ; break ; case WM_OBJECT_DESTROYED: WinPostMsg (hwnd, WM_QUIT, 0L, 0L) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } VOID FAR SecondThread () { static CHAR szClassName [] = "BigJob5.Object" ; HMQ hmq ; QMSG qmsg ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClassName, ObjectWndProc, 0L, 0, NULL) ; hwndObject = WinCreateWindow (HWND_OBJECT, szClassName, NULL, 0L, 0, 0, 0, 0, NULL, NULL, 0, NULL, NULL) ; WinPostMsg (hwndClient, WM_OBJECT_CREATED, 0L, 0L) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndObject) ; WinDestroyMsgQueue (hmq) ; WinPostMsg (hwndClient, WM_OBJECT_DESTROYED, 0L, 0L) ; DosExit (0, 0) ; } ULONG EXPENTRY ObjectWndProc (HWND hwnd, USHORT msg, ULONG mp1, ULONG mp2) { double A ; SHORT i, iCalcRep ; LONG lQueueStatus, lTime ; switch (msg) { case WM_START_CALC: iCalcRep = LOUSHORT (mp1) ; lTime = WinGetCurrentTime (hab) ; for (A = 1.0, i = 0 ; i < iCalcRep ; i++) { lQueueStatus = WinQueryQueueStatus (HWND_DESKTOP) ; if (lQueueStatus & QS_POSTMSG) break ; A = Savage (A) ; } if (lQueueStatus & QS_POSTMSG) break ; lTime = WinGetCurrentTime (hab) - lTime ; WinPostMsg (hwndClient, WM_CALC_DONE, lTime, 0L) ; break ; case WM_ABORT_CALC: WinPostMsg (hwndClient, WM_CALC_ABORTED, 0L, 0L) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return 0L ; } ████████████████████████████████████████████████████████████████████████████ OS/2 LAN Manager Provides a Platform for Server-Based Network Applications Alan Kesler☼ The introduction of personal computer──based networks has provided workgroups with systems that are capable of sharing expensive peripherals and supporting applications. This article provides a historical perspective of application development on PC network platforms, examines the impact of the OS/2 systems on PC network applications, and introduces the programmatic interfaces and capabilities provided by the Microsoft(R) OS/2 LAN Manager. Historical Perspective Applications that operate on PC networks can be categorized in one of three ways. Standalone applications see the network as nothing more than a shared printer and shared disk. They could just as easily be used on a standalone personal computer. Network aware applications recognize the existence of the network and use some of the network communication and data-sharing capabilities. Multiuser databases like dBASE III PLUS(TM) or Paradox(R) are the best examples of applications that are network aware. Many network aware applications may also be used in a standalone configuration. Finally, there are network intrinsic applications. These applications──electronic mail, multiuser document editing, and distributed database applications──don't make sense in a single personal computer or a standalone configuration. What sets these applications apart from the standalone and network aware applications is their use of distributed computing on the PC LAN; with standalone and network aware applications, all computing occurs in the network workstation. Only the program file and data are downloaded from the network server. This style of network-based computing has some disadvantages, such as underutilization of computing power sitting idle elsewhere on the network. Moreover, data-intensive applications will suffer performance problems when all computing occurs in the workstation. With back-end database servers, applications designed on a server and requester model can offer much greater performance to many more users. Finally, security is threatened by database applications that place large amounts of unnecessary data on the network, since all data must be sent to the network workstation in order to be processed. With distributed applications, user permission and data access can be verified at the server while a request is being processed. Only the requested information need be placed on the network and sent to the requesting workstation. Network intrinsic applications are set apart from both standalone and network aware applications through their use of distributed computing. In a distributed computing model the network application is partitioned into a workstation or front-end process and a server-based or back-end process. The workstation sends transactions to, and receives responses from, the server process. One example is a store-and-forward electronic mail system for a PC network. Users send electronic mail messages from their workstations using a message editor. The message is then sent and accepted by a mail server that runs in a network server on the LAN. The mail server stores and forwards the message along the delivery path until it is requested for delivery by the recipient. Few Distributed Apps The most frequently used applications on a network are standalone and network aware applications. There have been few network intrinsic or distributed network applications. Reasons for the lack of distributed network applications include the lack of a sophisticated operating system for the PC network platform, proprietary server environments, and the complexity of programming. During the 1980s, we've seen computing platforms downsized from mainframes and minicomputers to desktop personal computers that provide similar or even more raw computing power than their larger predecessors. Unlike the downsizing of computing platforms from the mainframe to the minicomputer, we have not seen a large-scale migration of crucial data processing applications to the desktop personal computer or PC network. The hardware platform has been in place with more powerful 80386-based workstations, but the software has, for the most part, not followed. This is because PC operating systems and PC LAN system software lacked the power and sophistication of a multitasking operating system required for many mission critical data processing tasks. Many believe that with IBM and Microsoft's introducing Operating System/2, applications once unable to operate on PC network systems will now migrate down to the PC network platform. IBM's Systems Application Architecture (SAA) is rooted in the concept of application and data portability across IBM's large, medium, and Personal System/2(TM) systems. Proprietary Environments The network system software that functions in a network file server is designed to provide high-performance, multiuser access to shared network resources such as disks and printers. Network system software vendors like 3Com, Novell, and IBM each implemented a proprietary means of making DOS a multitasking operating system, since network server software is by nature multitasking. The proprietary software environments, some of which were openly documented by the vendor, were all based on different application programming interfaces (APIs). An application developer who wanted to create a distributed network application for the leading network system software providers needed to implement the server portion of the application several times, once for each leading network system vendor. Nearly all application developers elected not to write to these proprietary interfaces for the reasons of maintenance, duplicate development effort, and inevitable support problems. Few distributed applications were developed to these proprietary interfaces, and most distributed applications were developed directly by the network system software providers who knew the proprietary environment and`were capable of supporting the software interfaces. The introduction of Operating System/2 changes the development environment dramatically. Now, leading network system software vendors will support OS/2 as an industry-standard multitasking kernel within the network server. Developers of network distributed applications have the advantage of an industry-standard operating system that has more than 250 application programming interfaces. Also, those who create applications that use OS/2 as the operating system for the workstation portion of the application have the added advantage of writing to the same APIs for both the workstation and the server software. APIs Historically, developers of network applications have had an opportunity to choose from several APIs for data-sharing and interprocess communication services. These include APIs supported by DOS 3.x and the Microsoft redirector, network BIOS or NetBIOS APIs, and APIs directly in the transport layer of network protocols. DOS 3.x and redirector APIs provide basic file-sharing and record-locking capabilities. Network aware applications such as dBASE III PLUS are written to these APIs. This class of application involves intensive data sharing. Transport-specific APIs enable a developer to create communications- oriented applications. Applications such as electronic mail, local and remote network bridging, and message routing have the advantage of using the specific capabilities of a given network transport. Unfortunately, applications developed to a specific interface are not portable if another network transport layer is used. For example, an application that interfaces directly to TCP/IP for its internetworking support cannot function on an IBM or ISO protocol without first being modified to interface with the specific functions of those respective network transport protocols. NetBIOS, a communication-intensive protocol introduced with IBM's PC Net in 1985, supports session-level interaction between workstations and servers. As the name implies, NetBIOS is a low-level protocol that requires the application developer to write directly to a network BIOS. Perhaps the greatest advantage of the NetBIOS interface is that it is supported or emulated by all leading network system software vendors. An application developer who writes to the NetBIOS API can be reasonably sure that the application will run on many network systems and therefore garner a large market share. However, NetBIOS requires the application developer to understand a detailed communication protocol at very low levels in the system, therefore making NetBIOS applications difficult to develop. Also, error conditions that result from NetBIOS transactions (send, receive, etc.) need special programming over and above that required by the underlying operating system. Applications developed to the NetBIOS protocol include electronic mail and transaction-oriented database applications. The OS/2 LAN Manager The Microsoft OS/2 LAN Manager is a network system software product that provides file, print, resource, security, and network management services to a work group using a local area network. A joint development project of Microsoft and 3Com, the OS/2 LAN Manager is based on an OS/2 network kernel, supports both DOS and OS/2 network workstations, has sophisticated network management features, and provides an open system with complete application programmer interfaces, including programming documentation and development tools. The Microsoft OS/2 LAN Manager includes both workstation and server software. The workstation software includes user interface and requester functions for DOS and OS/2 network workstations. The server operates on top of the OS/2 kernel. Thus, as an application, the OS/2 LAN Manager is hosted by OS/2 and is an OS/2 application in the network server. Because it functions as an OS/2 application in the server, it can take advantage of the features of OS/2, including the ability to use the 16Mb of protected-mode memory provided by OS/2. The protected-mode memory allows OS/2 LAN Manager to coexist with any other application written to OS/2 APIs. Applications written to OS/2 can operate side-by-side in the same computer that is acting as a network server for a PC LAN. This is perhaps the most important benefit gained from the use of the OS/2 kernel. The OS/2 LAN Manager is based on OS/2, unlike network system software that is based on proprietary implementations of multitasking DOS. Any application developer will be able to create an application that uses any of the more than 250 APIs within OS/2 and the application can run side-by-side with the OS/2 LAN Manager. This frees developers from writing to proprietary server interfaces. It is important to note that IBM has announced a network system software product called OS/2 LAN Server, which also is hosted by OS/2. Protocol-Independent Applications written to take advantage of the security, network management, or any other LAN Manager feature are portable to many different network environments. This is because the OS/2 LAN Manager itself is designed to be a transport protocol──independent system. The diagram in Figure 1 describes the framework of multiple network transport layers supported beneath OS/2 LAN Manager. For reference, the Microsoft OS/2 LAN Manager architecture is mapped against the Open Systems Interconnection (OSI) model. Network system software like 3Com's 3+Open(TM) product line that incorporates the Microsoft OS/2 LAN Manager will be supported on multiple network transport layers. Network applications written to take advantage of OS/2 LAN Manager APIs will have an opportunity for a greater share of the market because the Microsoft OS/2 LAN Manager will support specific needs of more environments by giving the customer the choice of network transport. As shown in Figure 1, the OS/2 LAN Manager has several protocol-independent interfaces including the NetBIOS and hardware adapter driver (Media Access Control) levels. Future developments will yield a network transport programming interface and an IBM(R) LLC (Logical Link Control) 802.2 interface. Backward-Compatible The Microsoft OS/2 LAN Manager supports equivalent calls to today's application programmer interfaces (NetBIOS, DOS redirector calls) in both an OS/2 and DOS environment. Today's DOS and NetBIOS APIs are supported and are compatible with the installed base of network applications written to DOS and NetBIOS. These include both normal DOS calls and Interrupt 5CH calls from the OS/2 compatibility mode. An Open System The OS/2 LAN Manager is an open system. Application programmer interfaces are provided for all important system functions. The system is hosted by OS/2, making it fully compatible with the more than 250 APIs provided by the kernel, in addition to the APIs shown in Figure 2. IPC Highly integrated with the OS/2 kernel over which it operates, the Microsoft OS/2 LAN Manager extends the capabilities of OS/2 "pipes" or interprocess communication across the network, allowing an application to use remote procedure calls to processes running in the same computer or a different one elsewhere on the network. This extension or redirection of OS/2 interprocess communication across the network is often referred to as "named pipes" or redirected pipes. It is this ability to redirect interprocess communication across the network that is the foundation of distributed and network intrinsic applications (see Figure 3). Of course, named pipes are not required to build an application that uses PC LAN interprocess communication and remote procedure calls. What makes the OS/2 LAN Manager implementation of named pipes different is the ease of implementation for the developer when compared with similar implementations based on NetBIOS or other program-to-program communications. Where NetBIOS and other program-to-program protocols often require specific, in-depth knowledge of networking issues, named pipes provide a high-level interface that offers developers of DOS-based applications an opportunity to quickly and easily create network intrinsic applications. Remote Procedure Calls Before providing an in-depth comparison of the use of named pipes versus NetBIOS for remote procedure calls on a PC network, it helps to review the types of transactions required to perform this task. The example below is generalized to include the procedures necessary for a secure, transaction-oriented, comprehensive commercial application. Included in the application are user authentication, error logging, access control, and access logging. In general, the requesting workstation and responding server will have to perform the following tasks: ■ user asks for a session ■ user sends username ■ server sends a challenge that is used by the workstation to provide the correct password validation back to the server ■ user sends password ■ server validates password, username ■ user sends remote procedure call ■ server checks for access control (can the user perform the remote procedure call) ■ server performs proper error response and logging if access/permission denied ■ server performs remote procedure call ■ server logs access to server process used by remote procedure call ■ server closes session ■ server gets next request Named Pipes vs. NetBIOS Named pipes are transaction-oriented and provide a much higher-level interface to the application developer. The calling convention of named pipes is much less complicated than that required with a NetBIOS solution. A single named pipe function call equates to many NetBIOS calls. To demonstrate the relative simplicity of the named pipe API in relation to NetBIOS APIs, a side-by-side comparison is provided in Figure 4. It is important to note that the named pipe example is presented in C pseudocode and more closely approximates real C language code. The NetBIOS example is presented with a high-level procedural description of steps that would need significant embellishment to evolve to the detail of the C language pseudocode. This was done because the NetBIOS example would be too lengthy if provided in detailed C pseudocode. Even with a pseudocode example, the differences in complexity of the calling convention between named pipes and NetBIOS is obvious. A remote procedure call requires two basic pieces: the workstation portion that initiates the call and a server portion that responds to and acts on the call request. The function call necessary to request a remote procedure call from an OS/2 LAN Manager workstation to an OS/2 LAN Manager server is provided in Figure 4. For clarity, some of the arguments (like buffer lengths, for example) have been excluded from the C pseudocode example. You will notice that the DosCallNmPipe function takes the name of a server, the name of a pipe, application name and request, and receive buffer as arguments. The complete remote procedure call request is performed with a single function call. All user validation, auditing, error checking, and reporting is performed automatically by the OS/2 LAN Manager server, since a named pipe can be shared by an OS/2 LAN Manager server. The OS/2 LAN Manager security system, network management, audit trail, resource accounting, and user validation are a side benefit of using named pipes. To provide a similar capability using a NetBIOS API on an OS/2 LAN Manager system would require workstation code with the functional characteristics shown in Figure 4. Notice that with the NetBIOS example, not only is the remote procedure call more complex, but special-purpose code must be developed to validate user access, report on resource usage, and handle special case error conditions. Named pipes are treated as any other shared server resource, and thus special-purpose code is not necessary as the error conditions, validation, and reporting are handled automatically by the Microsoft OS/2 LAN Manager. It is also important to note that the NetBIOS example does not include special code to handle error conditions that would require clean-up operations. Named pipes can be either stateless, like DosCallNmPipe, or allow remote procedure calls, like DosTransactNmPipe. Use of the DosCallNmPipe procedure call provides for stateless remote procedure calls (sessions only maintained during a transaction) and thus are less prone to error and don't require special clean-up. Use of DosTransactNmPipe allows a series of RPCs on the same session with the network server. In addition to error conditions that result from session management, hard errors, such as Abort, Retry, Ignore, Fail, occur. Since named pipes are an extension of OS/2 pipes, built-in OS/2 error recovery is automatically used should a hard error occur. With NetBIOS, hard errors must be anticipated and dealt with within the application code itself. Remote Procedure: Server Portion Remote procedure calls that are initiated using named pipes from the workstation must be supported by remote procedure management code on the network server. The pseudocode in Figure 5 shows the APIs required by the network server portion of the distributed application, as well as the pseudocode necessary to provide similar capabilities if NetBIOS is used rather than named pipes. Note that in the NetBIOS example, all password and user verification, reporting, auditing, and resource management must be supported directly by the application code running in the network server. The system-level capabilities - portability, stateless operation, multiple RPC calls, auditing, security, administration, error handling, and higher performance - are built into the Microsoft OS/2 LAN Manager and are therefore automatically available to any application that uses named pipes for interprocess communication across the network. Portability OS/2 LAN Manager is hosted by the OS/2 kernel; therefore, applications written to OS/2 APIs can be ported to perform remote procedure calls and function in a distributed configuration on the PC LAN with only minor modifications to the application code. This is not true if the application was ported from OS/2 to use NetBIOS as an interprocess communication API. It is also important to note that applications written to OS/2 can run without modification in an OS/2 LAN Manager network server. Of course, this is not the case with all network system software. Many vendors who provide PC network systems for business work groups don't provide an effective OS/2 server environment for OS/2 applications installed in the server. Stateless Operation A stateless distributed application maintains a session with the server only during the length of a single procedure call. Each procedure call to the server from the workstation creates a session, performs the remote procedure call, and closes the session. With named pipes this is performed using DosCallNmPipe. To perform the remote procedure call using NetBIOS, the NetBIOS application must perform a call, send, receive, and hang up. The NetBIOS example requires the session to live longer than a simple request/receive transaction. This is particularly important with error management. The longer a session is open, the more likely an error may occur. If you don't have a stateless design, special case code has to be provided to perform a clean-up from potential errors. Multiple RPC Calls Named pipes also include procedure calls that are useful when the application transacts multiple remote procedure calls on the same session to the server. For some applications, this is the desired means for remote procedure calling. Applications that perform normal DOS reads, DOS writes, or DosTransactNmPipe calls to a named pipe are operating in such a multicall mode, which may offer performance advantages because the session is maintained across multiple transactions and doesn't have to be reestablished for every transaction between the requester and the server. Auditing Many distributed applications require sophisticated audit trail facilities that track who performed the remote procedure call, from which workstation, at what time, and how long the remote procedure call took. Using a NetBIOS-based distributed application, all of the functions required for auditing would have to be specially developed as part of the distributed application. With the Microsoft OS/2 LAN Manager, the network administrator can flag one or more specific named pipes that exist on the server and mark them to be audited; this causes a record to be entered in the OS/2 LAN Manager audit trail each time a named pipe is opened or closed. The record generated in the audit trail can be used as the foundation of a bill-back system. It includes the date and time of access, who accessed the named pipe, the type of access (open/close), and the duration of the access. Security The Microsoft OS/2 LAN Manager incorporates a sophisticated user security system that includes encrypted user log-on validation, access control list, error reporting, and real-time event notification of attempted security breaches should access to a shared resource be attempted by an unauthorized user. All of these capabilities are available to a distributed application that uses named pipes as the means for interprocess communication across the network. Access to named pipes is controlled by the server's standard user logon, password encryption, group membership, and access permission mechanism. The capabilities are provided without requiring any additional programming by the application developer. Treated by the server as a shared resource, the named pipe can be placed in the server's resource list and given access permission using the OS/2 LAN Manager administrative software. Once logged as a shared server resource, the OS/2 LAN Manager will report on the status of the resource just as it would any other shared resource on the network. Attempts to use the named pipe resource will be logged in the system audit trail. Should excessive unauthorized access to the resource be attempted, a real-time message will be sent to advise a predetermined network administrator of the attempted security breach. Finally, network errors resulting from the use of the named pipe will be automatically logged in the system's error file and can be reviewed by the network administrator. Administration The OS/2 LAN Manager has a window-oriented administrative user interface that supports real-time administration of shared network resources. If distributed applications are designed to use named pipes as the means for interprocess communication, the LAN Manager administrative interface can be used to view and manage access to the redirect pipes without any development specifically embedded in the distributed application. OS/2 LAN Manager provides management of named pipes, including the ability to view real-time user access to the named pipe. Data includes local machine name, user name, resource name, total session time, and session idle time. In addition, OS/2 LAN Manager provides the ability to force a user to disconnect from a named pipe and automatically disconnect workstation and server sessions based on user activity and an administrator defined timeout parameters. If a named pipe is open between a workstation and server (there is an open handle), the session will not timeout. A built-in pop-up message system can be used directly by the distributed application to inform users with a real-time message. The message system can also be used by the network administrator to send messages to individual users. Error Handling Pipe management code is part of the operating system kernel within OS/2. It adheres fully to the operating system's method of error reporting, including use of standard OS/2 error codes and issuance of hard error pop- ups where appropriate with abort/retry/ignore/fail error handling. Because NetBIOS session capability is not a supported interprocess communication within the operating system, distributed applications written to NetBIOS must include a significant amount of specific error-handling code. Named pipes also provide advantages for the server portion of the remote procedure code. If a named pipe fails during processing, the operating system performs all the cleanup. But if any NetBIOS send fails, the application will have to cancel any asynchronous receive requests that have been posted and hang up the session. Each of these clean-up tasks could generate an error as well, from which the application must be programmed to recover. Performance The Microsoft OS/2 LAN Manager named pipe API supports function calls such as DosTransactNmPipe and DosCallNmPipe that do entire transactions in a single system call. By contrast, multiple NetBIOS calls would be necessary to perform the same function as the single named pipe call. Because named pipes call the operating system less often, distributed applications based on named pipes for remote procedure calls and interprocess communication are likely to yield high-performance results. Tradeoffs Many of the advantages achieved by the developer who uses named pipes for PC network interprocess communication have been outlined. There are, however, costs to the developer who uses OS/2 LAN Manager named pipes, which deserve serious consideration from any developer who is planning to create a distributed network application. Currently, named pipes have been announced as a fully supported API on systems that incorporate the Microsoft OS/2 LAN Manager. 3Com's 3+Open product line is an example of one such system. Although it is reasonable to assume that other vendors who provide network system software product lines will incorporate the Microsoft OS/2 LAN Manager product, and that these systems will also support named pipes, there are many more installed network systems that support NetBIOS and will continue to do so. The developer of a network application will, at least initially, choose between capability and installed base market share. For many developers, the advantages of named pipes and the market position of companies who have announced Microsoft OS/2 LAN Manager-based systems is encouragement enough to forgo NetBIOS and develop to the named pipe interface. Even if a developer of network applications already knows and understands the NetBIOS API, using named pipes will require additional education. Microsoft has taken direct steps toward reducing the need for further education with its Microsoft University classes. These classes deal with the fundamentals of developing OS/2 applications as well as applications development for the Microsoft OS/2 LAN Manager. Microsoft is also sponsoring several OS/2 LAN Manager developers' conferences this year. The high-level interface and simplicity of named pipes, the classes at Microsoft University, and the developers' conference will benefit developers of any network application. Application Platform Although the success of network systems incorporating the OS/2 LAN Manager won't be known until they are introduced about mid-year 1988, the Microsoft OS/2 LAN Manager provides a rich platform for application development. The strong points of the Microsoft OS/2 LAN Manager currently available to developers of network applications and end customers include: ■ use of an industry-standard multitasking operating system, OS/2 ■ high performance and reliability ■ open system architecture ■ support for OS/2 APIs as well as NetBIOS and DOS redirector calls ■ support for OS/2 pipes and extension of them across the network using named OS/2 pipes ■ rich system-level function that lets applications benefit from the administrative functions already built into the system Trends The advent of IBM OS/2 and the use of industry standard multitasking operating systems as the kernel to network system software provides an opportunity for the development of applications that reside in the network server and provide distributed computing power to the PC network. Network applications that once had to be developed to use proprietary, vendor- specific operating system interfaces will now be developed to industry- standard OS/2 operating system interfaces. In addition, Microsoft has simplified network interprocess communication with the OS/2 named pipes capability. OS/2 LAN Manager APIs can significantly reduce the overhead of developing a network intrinsic application. People in the industry believe that the OS/2 network servers and these new APIs will result in many new applications, such as database servers, mail servers, name/directory servers, document library services, accounting (using a distributed database server as the back end), and communication gateways. The industry direction by Microsoft, IBM, and 3Com of providing and marketing open system server platforms based on Operating System/2 as an industry-standard operating system kernel will benefit both the developer and end customer of PC network systems with more powerful and functional application software. Figure 1: The Microsoft OS/2 LAN Manager has a protocol-independent design. ┌─────────────────────────────────────────────────────────────────────────┐ │ ╔═════════════╗ ╔═══════════════════════════════════╗ │█ │ ║ Application ║ ║ Microsoft OS/2 LAN Manager Server ║ │█ │ ╟─────────────╢ ║ and Workstation Software ║ │█ │ ║ Presentation║ ╚═══════════════════════════════════╝ │█ │ ╚═════════════╝ │█ │ │█ │ ╔═════════════╗ ╔════════╗ ╔════════╗ ╔════════╗-NetBIOS │█ │ ║ Session ║ ║ IBM ║ ║ ║ ║ ║ │█ │ ╟─────────────╢ ║NetBIOS/║ ║ TCP/IP ║ ║ ISO ║ │█ │ ║ Transport ║ ║ Token ║ ║ ║ ║ ║ │█ │ ╟─────────────╢ ║ Ring ║ ║ ║ ║ ║ │█ │ ║ Network ║ ╚════════╝ ╚════════╝ ╚════════╝ │█ │ ╚═════════════╝ │█ │ │█ │ ╔═════════════╗ ╔═══════════════════════════════════╗-802.2 LLC │█ │ ║ Data Link ║ ║ Adapter Interface and ║ │█ │ ╟─────────────╢ ║ Protocol Manager ║ │█ │ ║ Physical ║ ╚═══════════════════════════════════╝-Media │█ │ ╚═════════════╝ Access │█ │ Control │█ └─────────────────────────────────────────────────────────────────────────┘█ ██████████████████████████████████████████████████████████████████████████ Figure 2: System functions supported by an application programming interface include, but are not limited to, those shown here. Network Interface Media Access Control (network adapter driver interface) Protocol manager (to support multiple, simultaneous network protocols) Network Management/Administration Audit trail Messaging Alerter Error log Network error reporting Network statistics Input/Output Devices and Pipes Named pipes (redirected pipes) Spoolers Character devices NetBIOS Mail slots Network Access Use/un-use network resources Access control list security Server/workstation configuration Resource sharing Network console Net Run (OS/2 compute server) Logon scripts User profiles Figure 3: OS/2 pipes are used to redirect interprocess communication within a single computer. Named pipes redirect interprocess communication and make it possible for applications to transparently use computing resources in other workstations or servers on the network. User ╔══════════════╗ ┌──────Interface──────┐ ║ ░░░░░░░░░░░ ║█ │ │ ║ ░░░░░░░░░░░░ ║█ OS/2 Pipe OS/2 Pipe ║ ░░░░░░░░░░░░ ║█ │ │ ║ ░░░░░░░░░░↔↨ ║█ ▼ ▼ ╔═╝──────────────╚═╗ Database ◄──OS/2 Pipe──► Report ║ ----‼‼‼‼‼‼‼‼‼‼‼ ║█ Manager Writer ╚══════════════════╝█ ───────────────────────────────────────────────────── ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ╔══════╗ Standalone PC ║│ ║█ Server ║│▄▄▄▄▄║█ ╔═══════════╗ ║┌───┬┐║█ ║ ░░░░░░░░ ║█ ║│ ││║█ ║ ░░░░░░░░░ ║█ ┌──────────Name Database ║│ ││║█ ║ ░░░░░░░░░ ║█ User (Redirected)────►Manager ║│ ││║█◄PC Network►║ ░░░░░░░↔↨ ║█ Interface OS/2 Pipe ↑ ║└───┴┘║█ ╔═╝───────────╚═╗ │ OS/2 Pipe ║||||||║█ ║ ----‼‼‼‼‼‼‼‼ ║█ └──────────Name ┌─► ↓ ║||||||║█ ╚═══════════════╝█ (Redirected)──┘ Report ╚══════╝█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ OS/2 Pipe Writer ▀▀▀▀▀▀▀ Workstation Figure 4: A pseudocode comparison of a LAN manager API call (shown on top) and the equivalent NetBIOS calls (shown below). LAN Manager API Call DosCallNmPipe("\\servername\pipe\appname", RpcRequBuf, RpcRespBuf) NetBIOS API Call Begin Procedure Get username from the user Get password from the user Establish session with server Send username to server Receive server challenge (used for password encryption) Do the encryption challenge using server challenge and user password Result of logic is challenge response Send challenge response with remote procedure request to server Receive remote procedure call response Close session with server End Procedure Figure 5: The LAN Manager itself handles a great deal of the administrative details required through NetBIOS. LAN Manager API Call DosMakeNmPipe(\"pipe\appname", handle) loop /* for each remote procedure call */ DosConnectNmPipe(handle) DosRead(handle, RpcReqBuf) Do the remote procedure call DosWrite(handle, RpcRespBuf) DosDisconnectNmPipe(handle) endloop NetBIOS API Call Begin_Loop Listen for user session Establish user session Receive username Do encryption logic to generate challenge Send challenge Receive challenge response and remote procedure call request Do encryption logic to validate user challenge response Open and match username in application specific access control file If access control supports group memberships, then verify membership If validation fails, Generate an audit trail record that access was denied Send an access denied response Close session GotoBegin_Loop Do the remote procedure call Send the remote procedure call response Close the session Calculate time duration of remote procedure call handling and generate audit trail record of service provided for this user End_Loop ████████████████████████████████████████████████████████████████████████████ Writing OS/2 Bimodal Device Drivers: An Examination of the DevHIp API Ray Duncan☼ Device drivers are the modules of an operating system that issue commands directly to peripheral devices and cause data to be transferred between those devices and RAM. Drivers are thus the bottom, most hardware- dependent layer of the system software. They shield the operating system kernel from dealing with hardware I/O port addresses and the peculiarities and characteristics, such as number of tracks or sectors per track, of a particular peripheral device, just as the kernel in turn shields applications programs from the details of file and memory management. By convention, Microsoft(R) OS/2 device drivers reside in individual disk files with the extension .SYS. The essential OS/2 drivers for the keyboard, display, and disk device are selected on the basis of the hardware environment and automatically linked to the operating system during booting. Other optional device drivers can be loaded by DEVICE= statements in the CONFIG.SYS file on the boot disk and are referred to as installable drivers. However, all OS/2 systems drivers have the same physical and logical structure and an identical relationship to the OS/2 kernel. OS/2 device drivers are in many ways similar to MS-DOS(R) device drivers, but they also have unique capabilities and requirements, including: ■ fully interrupt-driven operation ■ bimodal (real mode and protected mode) operation ■ support for multitasking and overlapped I/O ■ the ability to deinstall after the driver is fully initialized ■ support for device monitors (applications that can register with the driver to filter its data stream) ■ support for ROM BIOS service calls by real-mode applications The experienced MS-DOS programmer must be particularly wary of terminology that sounds familiar even though the associated driver function is very different. A full-fledged OS/2 driver for any given device is at least an order of magnitude more complex than its MS-DOS equivalent. Device Driver Types OS/2 device drivers are categorized into two groups: character device drivers and block device drivers. Whether a given driver is a character device or block device driver determines how the associated device is viewed by OS/2 and what functions the driver itself must support. Character device drivers control peripheral devices that perform input and output a character (or byte) at a time, such as a terminal or a printer. A single character device driver ordinarily supports a single hardware unit, although a given file can contain more than one driver. Each character device has a one- to eight-character logical name, and can be opened by using this name for input or output by an application program as though it were a file. The logical name is strictly a means of identification of the driver to OS/2 and has no physical equivalent on the device. Block device drivers control peripheral devices that transfer data in chunks rather than a byte at a time. Block devices are usually randomly addressable, such as floppy disk or hard disk drives, but they can also be sequential in nature, such as a magnetic tape drive. A block driver can support more than one physical unit and can also map two or more logical units onto a single physical unit, such as a partitioned hard disk. OS/2 assigns single-letter drive identifiers (A:, B:, and so on), instead of logical names, to block devices. The first letter assigned to a given block device driver is determined solely by that driver's position in the chain of all drivers, that is, by the number of units supported by the block drivers that have been loaded before it. The total number of letters assigned to the driver is determined by the number of logical units that the driver supports. Device Driver Structure OS/2 device drivers are written as small-model Macro Assembler programs with one code and one data segment and are linked as a segmented or "new" EXE file. Figure 1 is a diagram of the physical structure of a device driver file; the data segment always lies in the first part of the file, with a structure called the device driver header at its beginning. The header contains information about the driver that the OS/2 kernel can inspect when it needs to satisfy an I/O request by an applications program (see Figures 2 and 3); all of the device drivers in the system are chained together through pointers that are stored in the device driver headers. A driver has two major components: the strategy routine and the interrupt routine. Although they have the same names as the two most important sections of an MS-DOS installable driver, they function in a very different manner. The Strategy Routine The OS/2 kernel will initiate an I/O operation by calling the driver's strategy routine (whose offset is found in the device driver header), passing the address of an information packet called a request header in registers ES:BX. The first 13 bytes of the request header have a fixed format and are followed by data that varies according to the operation being requested (see Figure 4). The most crucial field of the request header is the command code, which selects a particular driver subfunction (see Figure 5). Many of the subfunctions can be carried out immediately at the time of the strategy call; in such cases, the driver simply places any necessary information to be returned to the kernel into the request header and sets the done bit in the status word. However, some subfunctions, such as read and write, involve an unpredictable delay and cannot be completely carried out during the strategy call without an adverse impact on overall system performance. The request headers for such subfunctions are chained onto the driver queue or work list, the actual I/O operation is started if the physical device happens to be idle, and then the strategy routine returns control to the kernel so that other work within the system can go forward. Incidentally, block device drivers are expected to sort their pending work list so as to minimize the average transfer time. The driver strategy routine normally executes at the highest privilege level (Ring 0), with the exception of the initialization subfunction, which executes at Ring 2 (the same as an application with I/O privilege). The strategy routine must be fully reentrant and capable of processing multiple requests simultaneously. The Interrupt Routine The driver interrupt routine is entered from the kernel as a result of a hardware interrupt from the physical device, when an I/O operation is either finished or has been aborted due to a hardware error. The interrupt routine queries the peripheral controller to determine the outcome of the I/O operation, sets the done bit and the appropriate status and error information in the request header, removes the request header from the work list, and notifies the kernel that the operation is complete. The interrupt routine then examines the driver's work list to determine whether other I/O operations are pending for the device. If additional request headers are waiting, the interrupt routine initiates the operation that is at the head of the queue before returning control to the kernel by clearing the carry flag and issuing a FAR RET. In cases where several devices (and their drivers) share the same interrupt level, the kernel will call each driver's interrupt routine in turn until one indicates its acceptance of the interrupt by clearing the carry flag; the other interrupt routines must return with the carry flag set. Since hardware interrupts are by nature asynchronous and unpredictable, an OS/2 device driver must be capable of properly servicing an interrupt in either protected mode or real mode. In order to assist the driver, OS/2 always provides special bimodal pointers to request headers-pointers that are valid in either real mode or protected mode. This is accomplished by reserving an area of physical memory to hold request headers and a set of selector values such that the selector for each header is exactly the same as its segment (paragraph) address. As an extra aid to the driver, OS/2 locks down memory segments that are the source or destination of a data transfer and converts their segment:offset or selector:offset addresses into linear 32-bit physical addresses before passing the read or write request to the driver. Unique Aspects Three particularly interesting features of OS/2 device drivers have no counterpart under MS-DOS: the necessity to provide ROM BIOS-equivalent services for real-mode applications, support for device monitors, and the ability of a driver to deinstall itself on demand. The ROM BIOS requirement stems from the propensity of many popular MS-DOS application programs to call the ROM BIOS directly rather than perform their I/O in a well-behaved manner through documented MS-DOS services. The most frequent reason for such ROM BIOS calls is to improve performance or because MS-DOS does not offer the necessary functionality (EGA palette programming, for example). Since the ROM BIOS code is mostly incompatible with the constraints of a protected-mode multitasking environment──it is not reentrant and it frequently masks off interrupts, among other problems──an OS/2 driver must capture the interrupt vectors that invoke ROM BIOS services for its device. It can then either interlock the ROM BIOS routines to prevent interference with the execution of protected- mode applications or substitute a new set of routines that provide equivalent services. Device monitors are an OS/2 innovation that allow keyboard enhancers, macro-expanders, print spoolers, and similar utilities to be written and installed in a hardware-independent manner. An applications program that wishes to filter the data stream of a particular character device driver can issue an OS/2 Application Program Interface (API) function call to register as a monitor for that device. Then the OS/2 kernel acts as the intermediary and asks the driver if it wishes to accept the monitor registration. If the driver assents, it will cooperate with the kernel to pass each character it reads from or writes to the device through a buffer belonging to the monitor program, which can add, delete, or substitute characters at its discretion. Finally, extension of the device driver definition with the DeInstall command code provides efficient use of memory and a more controlled environment for character drivers. A character device driver, under MS- DOS, can be superseded by simply installing another driver with the same logical device name; the first driver loaded has no way to prevent being superseded, and the memory it occupied is simply lost. Under OS/2, if a driver is loaded that has the same logical name as a previously loaded driver, the kernel first sends a request header with the DeInstall command code to the previous driver. The first driver can then release its interrupt vectors and memory and return a success status, after which the new driver is allowed to initialize itself. Alternatively, if the first driver refuses the DeInstall command by returning an error code, the kernel aborts the installation of the new driver with the same name. Device Driver Helpers Under MS-DOS, device drivers must be self-contained. They can use a very limited subset of the Interrupt 21H function calls during their initialization (character output, get MS-DOS version, get or set interrupt vector), but once the system is fully loaded and running, device drivers are not allowed to invoke any MS-DOS system services at all. MS- DOS, as a single-tasking operating system, assumes that only one I/O request will be in progress at any given time. If a driver that is in the process of carrying out an I/O request from the kernel in turn requests a different operation from the ker-nel, the context of the original request is destroyed, and the system will crash. OS/2, on the other hand, was designed specifically for multitasking, and most of its internal routines are fully reentrant. As a result, OS/2 provides a battery of services to device drivers, called device driver helpers, or DevHlps, much as it does to application programs, although the nature of these services is of course much different. Moving functionality that all drivers need into the kernel makes drivers smaller, simpler, and easier to debug and ensures that critical driver activities are carried out uniformly and efficiently. Unlike the kernel's API, in which parameters are passed on the stack and each function has a distinct, named entry point, the DevHlp interface uses a register-based parameter-passing convention and a common entry point. The driver is provided with a bimodal pointer to the DevHlp entry point in the request packet it receives from the kernel during its initialization. When the DevHlp entry point is called, a particular DevHlp function is selected by the value in register DL. Additional parameters and addresses are passed in other general registers as demanded by the particular function being requested. The status of a DevHlp function is usually returned to the driver in the zero or carry flags, error codes in register AX, and addresses in registers DS:SI or ES:DI (see Figure 6 for an example of a typical DevHlp call). ─────────────────────────────────────────────────────────────────────────── Also see: Summary of the way Device Drivers Queue and Process I/O Requests ─────────────────────────────────────────────────────────────────────────── DevHlps and Driver Modes Whenever an OS/2 device driver has control of the CPU, it executes in one of four different modes or contexts: kernel mode, interrupt mode, user mode, and init mode. Understanding these modes and their implications is essential to the task of writing an OS/2 driver, since the DevHlp services available to a driver are determined by its current mode. A driver is in kernel mode when its strategy routine is called by the kernel, usually as the immediate result of an I/O request by an application. The driver executes at the highest privilege level (Ring 0). Nearly every DevHlp service is available to the driver in kernel mode. The driver executes in interrupt mode as the result of a hardware interrupt. The OS/2 kernel fields the interrupt, saves all registers, and enters the driver's interrupt routine via a far call. The driver executes at the highest privilege level (Ring 0). A somewhat restricted set of DevHlp services is available to a driver in interrupt mode. The driver executes in user mode when it is entered as a result of a software interrupt by an application executing in the real-mode session (sometimes called the DOS 3.x compatibility box). The only drivers that must concern themselves with this mode are those that provide equivalents to the original IBM(R) PC ROM BIOS services in a manner consistent with the multitasking and memory protection requirements of OS/2, such as the keyboard, video, or floppy disk driver. Since a driver only runs in user mode when the CPU is in real mode, the concept of privilege levels is irrelevant, but you can think of the driver as executing at the highest privilege level since no protection mechanisms are operative. Relatively few of the DevHlp services are available to the driver in user mode. A driver runs in init mode only once: when its strategy routine is called with a request packet containing the initialization command code, immediately after the driver is loaded into memory during the system boot process. In init mode, the driver runs as though it were an application program IOPL segment, that is, at the next-to-lowest privilege level (Ring 2). A limited set of dynamic-link API calls, mostly concerned with file management (DosOpen, DosRead, DosWrite, and so on), as well as a subset of the DevHlp services (mainly those associated with hardware and memory management), are available to a driver in init mode. Task time and interrupt time are two other terms in the OS/2 device driver documentation regarding driver execution context. A driver is executing at task time when it is called synchronously as the direct result of some application program API request or real-mode software interrupt; this includes both the user mode and kernel mode contexts described above. Interrupt time refers to driver routines being invoked asynchronously as the result of an external hardware event; consequently, the identity of the currently executing process and the state of the CPU (real mode or protected mode) is unpredictable. Categories of DevHlps The roughly 40 DevHlp functions fall into nine categories (see Figure 7): process management, memory management, hardware management, request packet management, character buffer management, timer management, monitor management, semaphore management, and ABIOS communication (IBM PS/2(TM) only). Some of these categories of services are used by every OS/2 device driver, because they allow the driver to perform overlapped, interrupt-driven I/O operations in a well-behaved manner, or they provide access to memory addresses that the driver would otherwise be unable to reach. The others are present only for convenience or to assist in the construction of unusual or highly complex drivers. A more detailed look at the most commonly used groups of DevHlp functions also brings up some interesting points about the operation of OS/2 device drivers and their relationship to the kernel. Process Management The basic technique by which a driver performs overlapped or asynchronous I/O is very simple. When the strategy routine of the driver is called, it sets up the I/O operation (or adds it to the list of pending operations, if one is already in progress) and simply returns to the OS/2 kernel with the done bit cleared in the request header status word. The kernel then suspends the thread that initiated the I/O request. (For more information about OS/2 threads and processes, see "OS/2 Multitasking: Exploiting the Protected Mode of the 80286," MSJ, Vol. 2 No. 2, p. 27.) When the I/O operation is eventually completed, the driver's interrupt routine updates the request packet associated with the operation with the appropriate status word and any other necessary information and then calls the DevHlp routine DevDone. This signals the kernel to awaken the thread that owns the I/O request, which can then perform any necessary post-I/O processing, such as translating the driver's status information into appropriate values to be returned to an application program. A driver can also use the DevHlps Block and Run to suspend a thread while an I/O operation is in progress, particularly if there is a lengthy amount of code in the driver that must be executed after the operation is complete. Putting such code in the strategy routine is preferable to putting it in the interrupt routine, since other lower-priority interrupts are locked out by the hardware while the interrupt routine is executing. The strategy routine sets up the I/O operation and then calls Block with a 32-bit identifier of its own choosing. When the Interrupt routine notes that the I/O operation is complete, it calls Run with the same 32-bit identifier. This will cause the kernel to wake up the original thread within the strategy routine, which can proceed with any necessary post-I/O processing and then exit back to the kernel with the request header's done bit set. The biggest problem with this approach is ensuring that the 32-bit value used in a pair of Block and Run calls is unique within the system, since Run wakes up all threads that have blocked with that particular 32-bit identifier. The DevHlp services Yield and TCYield are provided for drivers that need to transfer long strings of data from one memory location to another, such as a RAMdisk, or whose devices do not support interrupts. A call to Yield simply tells the kernel's scheduler to give any threads with the same or higher priority a chance to run before returning control to the currently executing thread, while TCYield allows only those threads that have a time-critical priority to take a turn. A driver can check the value of YieldFlag or TCYieldFlag (two variables whose addresses are returned by GetDOSVar, another DevHlp) to determine whether any other threads are waiting for the CPU and bypass the call to Yield or TCYield if no other thread is eligible to execute. Such a check along with a possible Yield should be made at least every 3 milliseconds to avoid interference with the proper operation of other drivers. Hardware Management Since the strategy and interrupt routines of device drivers execute at the highest privilege level (Ring 0), they have a free hand with the machine if they want it. As a result, two different drivers that access the same hardware addresses, such as a serial mouse driver and a communications driver that are both configured for COM2, can theoretically come into conflict. However, OS/2 has DevHlps to arbitrate interrupt vector and 8259 Programmable Interrupt Controller (PIC) usage for cooperating drivers. Well-behaved drivers will use these DevHlps to gain access to the hardware resources they need. The DevHlp services SetIRQ and UnsetIRQ assist drivers in capturing or releasing interrupt vectors. When a driver calls SetIRQ to hook its interrupt handler to a given interrupt request (IRQ) level, it also specifies whether it is willing to share the level with another driver. If it specifies that it wants to own the interrupt exclusively, subsequent requests for the same IRQ level by other drivers will fail; if another driver has already signed up for the interrupt, the request by the current driver will fail. When a hardware interrupt occurs, a dispatcher in the OS/2 kernel initially receives control, saves all registers, then enters the driver's interrupt handler via a far call. When several drivers share an interrupt level, the dispatcher calls each driver's handler in turn. The handler must access its device and determine whether that device caused the interrupt. If the driver finds that its own device did not trigger the interrupt, it returns to the kernel with the carry flag set. On the other hand, if the handler finds that it owns the interrupt, it must service its device, possibly start another I/O operation if any are waiting, call the DevHlp service EOI to dismiss the interrupt at the 8259 PIC, and then return to the kernel with the carry flag clear. Note that, although drivers can directly access their own device's ports, manipulation of the PIC should be reserved for the kernel to isolate drivers from the interrupt architecture. Memory Management A driver specifies the amount of storage to reserve for its code and data segments in the information it returns to the kernel at load time. Later on, if the driver needs more memory for tables, buffers, or other data structures, it can use the DevHlp services AllocPhys and FreePhys to dynamically allocate and release such memory. The memory that is assigned to the driver by AllocPhys is not movable or swappable, and the driver can specify whether it wants the memory located above or below the 1Mb boundary. One of the trickiest aspects of an OS/2 driver's operation is memory addressing. Since a driver must be able to execute and handle interrupts equivalently in either protected mode or real mode, it might encounter three types of addresses: protected-mode selector:offset pairs, real- mode segment:offset pairs, and the linear or physical 32-bit addresses used by DMA channels and some devices. The operating system's virtual memory manager, which may shuffle segments around to collect unused fragments together or swap segments out to disk, must also be taken into account. The OS/2 kernel takes several measures to shield drivers from most of the potential problems with memory addressing. First, whenever the kernel requests an explicit transfer operation from a driver with a request packet containing the read or write command codes, it passes the driver a physical 32-bit address that has already been "locked," that is, the virtual memory manager has been notified that the memory in question should not be moved or swapped. When the kernel passes the driver an address of a structure (such as a request packet) or a procedure (such as the DevHlp entry point) that the driver will need to access directly, it arranges things so that the address is valid in either real mode or protected mode, that is, the protected-mode selector for the structure or procedure is the same as its real-mode segment. Such an address is called a bimodal pointer. Next, the kernel provides six DevHelp functions which assist a driver by converting physical 32-bit addresses to virtual (segment:offset or selector:offset) addresses and back again: AllocateGDTSelector, PhysToGDTSelector, PhysToUVirt, UnPhysToVirt, and VirtToPhys. The first three are used to access static structures (such as memory allocated by AllocPhys or an adapter's memory-mapped I/O locations). The remainder are used when a driver needs temporary access to data that is transient or belongs to a process. The PhysToVirt service is particularly interesting, because it permits a driver which is executing in real mode to address memory locations above the 1Mb boundary. If the driver is running on a PC/AT(R) or compatible and has to reach extended memory in real mode, PhysToVirt uses the undocumented LOADALL instruction to set the CPU's "shadow" addressing registers, which are normally loaded invisibly from a descriptor table when a segment register is loaded, so as to allow the memory access. On a PS/2, PhysToVirt takes advantage of hardware support for faster mode switching and simply switches the CPU into protected mode, providing the driver with a selector to the memory. When the driver is finished with the virtual address it obtained from PhysToVirt, it calls the DevHlp UnPhysToVirt so that the selector can be reused, at which time the kernel will also restore the original CPU mode if necessary. Finally, for cases in which a memory address is passed to a driver in an IOCTL data packet or by some other channel of communication, the OS/2 kernel provides the DevHlp services Lock, Unlock, and VerifyAccess. The driver first calls VerifyAccess with the selector, offset, and length of the memory involved in the requested operation to make sure that the application is really entitled to access the memory. If VerifyAccess succeeds, the driver calls the Lock function to immobilize the segment and VirtToPhys to obtain the physical address, and then proceeds with the I/O operation, calling Unlock afterwards to put the segment back under control of the system's memory manager. Monitor Management Under MS-DOS, terminate-and-stay-resident (TSR) utilities have been tremendous commercial successes. Such utilities intercept keystrokes at the hardware or BIOS level while other applications are running and take some special action when a particular key, called a hot key, is detected. In order to make such utilities possible in protected mode, OS/2 allows applications to register as a monitor for a specific character device, if supported by the device driver. The raw data stream from the device is then passed through a pair of buffers belonging to the application before it is transferred from the driver to the kernel; the application can delete, add, or remap characters in the data stream as it chooses or simply watch for its hot key. As you might expect, a driver's support for monitors involves a rather complicated interplay of calls from the application to the kernel, the kernel to the driver, and the driver to the kernel, and requires a high degree of cooperation among the three to preserve the integrity of the system. A special kernel module, called the monitor dispatcher, arbitrates among the three components and takes care of moving data from the driver through the buffers of one or more monitors and then back to the driver. An application registers as a monitor for a character device by calling DosMonOpen with the logical name of the device. It then calls DosMonReg with the monitor handle returned by DosMonOpen and the addresses of two buffers that will be used for input and output of data packets from the driver. If registration succeeds, the application becomes an integral part of the data stream and must use the functions DosMonRead and DosMonWrite to pass the character data through its buffers in a timely fashion. The application uses the function DosMonClose to terminate its connection with the device driver, after which it is removed from the data stream. From the driver's point of view, it informs the kernel that it is prepared to support monitors by issuing the DevHlp call MonitorCreate, which establishes an initial "empty chain" of device monitors and assigns a monitor handle to it. The driver may choose to create the monitor chain during its initialization or wait until there is a need for it. The driver is informed that an application is attempting to register as a monitor by the receipt of a special IOCTL request packet (Category 0AH Function 40H); it then calls the DevHlp service Register, which causes the kernel's monitor dispatcher to insert the application's buffers into the monitor chain at the appropriate point. Once the monitor chain is created, the driver must pass each character it receives from the device through the chain by calling the DevHlp service MonWrite. A notification routine within the driver is called by the kernel's monitor dispatcher when data emerges from the end of the monitor chain. The driver passes the data onward to the kernel in the usual manner. It is then distributed to applications via the API function DosRead. When the driver is informed by the kernel that an application has issued DosMonClose, the driver calls the DevHlp service DeRegister with the application's PID, resulting in the monitor dispatcher removing the application's buffers from the monitor chain. When the driver notes that all monitor connections are closed, it can call the DevHlp MonitorCreate again with a parameter that causes the monitor chain to be destroyed. Other Services The DevHlp services listed under semaphore management in Figure 7 allow a driver to communicate directly with an application via a system semaphore. Of course, a driver cannot create or open a system semaphore using the normal API calls, but if an application passes the handle for a system semaphore to the driver in an IOCTL packet, the driver can use the DevHlp service SemHandle at task time to obtain a new handle for the semaphore for use at interrupt time. The DevHlp functions SemRequest and SemClear, which the driver can call to obtain or release ownership of the semaphore, are the equivalents of the API functions DosSemRequest and DosSemClear. The Timer DevHlps assist a driver in performing polled I/O without degrading the system for devices that do not support interrupts or permit a driver to set up a watchdog timer to detect lost interrupts or I/O operations that never complete. The function SetTimer is called with the address of a routine within the driver that should be entered on each timer tick; this is the equivalent of chaining onto Interrupt 1CH (the ROM BIOS timer tick vector) under MS-DOS. The DevHlp service TickCount is a more general version of SetTimer; it also allows the number of ticks between entries to the driver's timer tick handler to be specified. The driver can remove its timer tick handler from the system's list of all such handlers by calling ResetTimer. The other device helper services which have not already been listed play minor roles, and are present mostly for convenience. For instance, the DevHlps listed under Request Packet Management in Figure 7 assist the driver in maintaining and sorting a linked list of request headers for pending I/O operations, but do not provide any functionality that the driver couldn't do for itself with a little extra code. Similarly, the DevHlps listed under Character Buffer Management provide simple support for a ring buffer of characters that are received or transmitted from the device. A Skeleton Device Driver As an illustration of the essential elements of an OS/2 device driver, TEMPLATE.ASM (see Figure 8) has the source code for a skeleton character driver called TEMPLATE.SYS. This driver performs no useful function; it simply returns a success code for all request packets that the kernel passes to it. However, TEMPLATE demonstrates the segment ordering and naming conventions that should be used in an installable driver, the use of API calls at initialization time, and a method by which initialization code and data can be discarded to minimize a driver's memory requirements. TEMPLATE can serve as a starting point for your own installable device drivers. In order to assemble and link TEMPLATE.SYS, you use the source file TEMPLATE.ASM, the module definition file TEMPLATE.DEF (see Figure 9), the Microsoft Macro Assembler, and the Microsoft Segmented Object Linker. Enter the commands shown in Figure 10. If the assembly and link are successful, you can copy the file TEMPLATE.SYS to the root directory of your boot disk, add DEVICE=TEMPLATE.SYS to the CONFIG.SYS file, and then restart the system to see the TEMPLATE driver's sign-on message. Summary OS/2 device drivers are architecturally and functionally far more complex than their MS-DOS ancestors, principally because they need to support bimodal operation and fully interrupt-driven I/O. This article has provided a brief overview of OS/2 device driver structure and capabilities. Figure 1: Structure of an OS/2 installable device driver file. OS/2 drivers are linked as small-model EXE files, containing one code and one data segment. By convention, driver files always have the extension .SYS. Note that code and data used only by the driver's initialization routine can be positioned at the segments and discarded after driver initialization is completed to minimize the driver's memory overhead. Start ╔═════════════════════════════════╗ ──────┐ of file║ EXE file header ║ │ ╠═════════════════════════════════╣ │ ║ Device driver header ║ Driver ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣ data ║ ║ segment ║ Resident driver data ║ │ ║ ║ │ ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣ ══════╡ ║ Initialization data (discarded) ║ │ ║ ║ │ ╠═════════════════════════════════╣ Driver ║ Resident driver code ║ code ║ ║ segment End ╠═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═ ═╣ │ of file║ Initialization code (discarded) ║ │ ╚═════════════════════════════════╝ ──────┘ Figure 2: OS/2 device driver header. In character device drivers, the name or units field contains the logical name of the device, left-justified and padded with spaces; in block device drivers, the first byte of the field contains the number of logical units supported by the driver (filled in by OS/2), followed by 7 bytes of reserved space. offset 0┌──────────────────────────────────────────┐ │ link to next driver header, offset │ 2├──────────────────────────────────────────┤ │ link to next driver header, selector │ 4├──────────────────────────────────────────┤ │ device attribute word │ 6├──────────────────────────────────────────┤ │ offset, strategy entry point │ 8├──────────────────────────────────────────┤ │ reserved │ 0AH├──────────────────────────────────────────┤ │ │ │ name or units (8 bytes) │ │ │ 12H├──────────────────────────────────────────┤ │ │ │ reserved (8 bytes) │ │ │ └──────────────────────────────────────────┘ Figure 3: The device attribute word in the device driver header, which describes the characteristics and capabilities of the associated driver. ╓┌─────────┌─────────────────────────────────────────────────────────────────╖ Bit(s) Meaning 15 = 1 if character device = 0 if block device 14 = 0 (reserved) 13 = 1 if non-IBM-compatible format (block devices) = 0 if IBM-compatible format (that is, FAT ID byte is valid) 12 = 1 if device name is not to be protected by "sharer" module (character devices only) Bit(s) Meaning (character devices only) = 0 if sharer controls contention for the device 11 = 1 if Device Open/Close functions (character devices) or Removable Media function (block devices) supported = 0 if Device Open/Close/Removable Media not supported 10 = 0 (reserved) 7-9 = function level, 001 to indicate OS/2 device driver 4-6 = 0 (reserved) 3 = 1 if current CLOCK device 2 = 1 if current NUL device 1 = 1 if current standard output device (stdout) 0 = 1 if current standard input device (stdin) Bit(s) Meaning 0 = 1 if current standard input device (stdin) Figure 4: A prototype OS/2 device driver request header. The OS/2 kernel requexts a driver operation by passing the bimodal address of a request header containing operation-specific parameters, and the driver communicates the results of the operation back to the kernel by placing status and other information in the same request header. In order to support multiple concurrent I/O requests, the driver maintains a private queue of request headers for its pending operations. This is done with the aid of the kernel DevHlp functions. 0┌──────────────────────────────────────────┐ │ length of request header │ 1├──────────────────────────────────────────┤ │ block device unit number │ 2├──────────────────────────────────────────┤ │ command code (driver subfunction) │ 3├──────────────────────────────────────────┤ │ returned status │ │ │ 5├──────────────────────────────────────────┤ │ reserved area (4 bytes) │ │ │ 9├──────────────────────────────────────────┤ │ queue linkage (4 bytes) │ │ │ 0DH├──────────────────────────────────────────┤ │ │ │ command-specific data (length varies) │ │ │ └──────────────────────────────────────────┘ Figure 5: Command Codes and Their Associated Driver Functions ╓┌────────┌──────────────────┌───────────────────────────────────────────────► Command Code Subfunction Name Subfunction Action Command Code Subfunction Name Subfunction Action 0 Init Initialize driver (called at driver load time). Tests for existence and operability of peripheral device register interrupt handler and returns size of resident code and data segments to kernel. Block drivers also return number of units supported and an array of pointers to BIOS Parameter Blocks (BPBs). 1 Media Check Returns a code indicating whether the media has been changed. 2 Build BPB Returns a pointer to the BIOS Parameter Block for the indicated disk type. The BPB for a disk describes its physical characteristics (sector size, number of tracks, tracks per sector, and so on). 3 Reserved Command Code Subfunction Name Subfunction Action 3 Reserved 4 Read Transfers data from the device to memory. 5 NonDestructive Returns the next character from the device, if Read one is available, without removing it from the input buffer. 6 Input Status Returns a flag indicating whether characters are waiting in the device's input buffer. 7 Input Flush Discards all data waiting in the device's input buffer and cancels pending read requests. 8 Write Transfers data from memory to the device. 9 Write with Verify Transfers data from memory to the device, performing a read-after-write verification if possible. Command Code Subfunction Name Subfunction Action possible. 10 Output Status Returns a flag indicating whether characters are waiting in the device's output buffer. 11 Output Flush Discards all data waiting in the device's output buffer and cancels pending write requests. 12 Reserved 13 Device Open Initializes a character device for subsequent I/O if necessary. 14 Device Close Performs any necessary post-I/O processing (such as transmitting a form feed to a list device). 15 Removable Media Returns a code indicating whether the media is removable. Command Code Subfunction Name Subfunction Action 16 Generic IOCTL General-purpose mechanism for communication between the driver and application programs. 17 Reset Media Called by the kernel to acknowledge and clear the "uncertain media" error condition previously returned by the driver. 18 Get Logical Drive Returns the drive code for the logical unit Map currently mapped to the specified physical unit. 19 Set Logical Drive Notifies the driver which logical unit will be Map used for the next access to the specified physical unit. 20 DeInstall Driver Signals the driver to release its interrupt vectors and memory. Called when a character device driver is superseded by the loading of another driver with the same logical device Command Code Subfunction Name Subfunction Action another driver with the same logical device name. 21 Reserved 22 Partitionable Returns the number of partitionable fixed disks Fixed Disk supported by the driver. 23 Get Fixed Disk Returns the identities of the logical disks, if Map any, supported by driver on the specified hard disk. 24-26 Reserved Figure 6: Example of a DevHlp call. This code allocates 64Kb of physical memory above the 1Mb boundary for use by the driver. When the driver wishes to access the memory block, it must use PhysToVirt or PhysToUVirt to convert the physical address stored in 'bufptr' into an appropriate virtual address for the current CPU mode. devhlp dd ? ; contains bimodal pointer ; to DevHlp common entry point ; (from Init routine) bufptr dd ? ; receives 32-bit physical ; address of allocated block . . . ; allocate 64Kb of memory ; above 1Mb for use by driver ; AX:BX = size in bytes mov ax,1 ; AX = high word mov bx,0 ; BX = low word mov dh,0 ; 0 = above 1Mb boundary mov dl,18h ; DevHlp 18H = AllocPhys call devhlp ; indirect call to DevHlp ; common entry point jc error ; jump if allocation failed ; save 32-bit physical ; address of memory block mov word ptr bufptr,bx mov word ptr bufptr+2,ax . . . Figure 7: DevHlp services by category, their function numbers, and the driver contents in which they may be used (K = kernel mode, I = interrupt mode, U = user mode, N = initialization mode). ╓┌────────────────────┌──────┌──┌──┌──┌──┌───────────────────────────────────╖ DevHlp DevHlp DevHlp Name Code K I U N Action DevHlp DevHlp DevHlp Name Code K I U N Action Process Management DevDone 01H K I Signals that the specified operation is complete. Kernel unblocks any threads waiting for the operation. Block 04H K U Blocks the calling thread until a timeout occurs of thread is unblocked by Run. Run 05H K I U Activates a thread that was previously suspended by a call to Block. Yield 02H K Surrenders the CPU to any other threads of the same or higher priority that are ready to execute. DevHlp DevHlp DevHlp Name Code K I U N Action TCYield 03H K Similar to Yield, but allows other threads to execute only if they have a time-critical priority. Memory Management AllocPhys 18H K N Allocates a block of fixed (not swappable or movable) memory. Caller can specify whether the block should be above or below the 1Mb boundary. FreePhys 19H K N Releases a block of memory previously allocated with AllocPhys. Lock 13H K N Locks a memory segment, that is, flags the segment as nonmovable and nonswappable. DevHlp DevHlp DevHlp Name Code K I U N Action Unlock 14H K I N Unlocks a memory segment. Verify Access 27H K Verifies that a user process has access to a segment. PhysToVirt 15H K I N Converts a 32-bit physical (linear) address to a virtual address. UnPhysToVirt 32H K I N Signals that the virtual addresses previously obtained with PhysToVirt can be reused. PhysToUVirt 17H K Converts a 32-bit physical address to a virtual address accessed through the current Local Descriptor Table (LDT). VirtToPhys 16H K N Converts a virtual address DevHlp DevHlp DevHlp Name Code K I U N Action VirtToPhys 16H K N Converts a virtual address (segment:offset or selector:offset) to a 32-bit physical address. AllocateGDTSelector 2DH K N Allocate one or more GDT selectors for use by the driver. PhysToGDTSelector 2EH K N Map a physical address and length onto a GDT selector. RealToProt 2FH I Switch the CPU into protected mode. ProtToReal 30H I Switch the CPU into real mode. Hardware Management SetIRQ 1BH K N Sets the interrupt vector for the indicated interrupt request (IRQ) level to point to the driver's DevHlp DevHlp DevHlp Name Code K I U N Action level to point to the driver's interrupt handler. UnSetIRQ 1CH K I N Releases the interrupt vector for the specifies IRQ level. EOI 31H I N Issues an end of interrupt to the appropriate 8259 PIC for the specified IRQ level. Request Packet Management PushReqPacket 09H K Adds a request packet to the driver's linked list of packets. SortReqPacket 0CH K Inserts a request packet into the driver's linked list of packets, sorted by sector number. DevHlp DevHlp DevHlp Name Code K I U N Action PullReqPacket 0AH K I Removes the oldest request packet from the driver's linked list. AllocReqPacket 0DH K Allocates a block of memory large enough to hold the largest possible request packet and returns a bimodal pointer. FreeReqPacket 0EH K Releases a request packet previously allocated with AllocReqPacket. Character Buffer Management QueueInit 0FH K I U N Establishes a ring buffer for storage of characters and initializes its pointers. QueueRead 12H K I U Returns and removes a character from DevHlp DevHlp DevHlp Name Code K I U N Action QueueRead 12H K I U Returns and removes a character from the specified buffer. QueueWrite 11H K I U Puts a character into the specified buffer. QueueFlush 10H K I U Resets the pointers to the specified buffer. Timer Management SetTimer 1DH K N Adds a timer tick handler to be called on every timer tick to the system's list of such handlers. TickCount 33H K I U N Adds a timer tick handler to be called at specified intervals to the system's list of such handlers, or modifies the interval at which an DevHlp DevHlp DevHlp Name Code K I U N Action modifies the interval at which an existing handler is called. ResetTimer 1EH K I N Removes a timer tick handler from the system's list of such handlers. Monitor Management MonitorCreate 1FH K N Creates or removes an empty monitor chain. Register 20H K Adds a process to a monitor chain. MonWrite 22H K I U Passes a data record to a monitor chain for filtering. MonFlush 23H K Removes all data from a monitor chain. DevHlp DevHlp DevHlp Name Code K I U N Action DeRegister 21H K Removes a jprocess from a monitor chain. Semaphore Management SemHandle 08H K I Converts a process's handle for a system semaphore to a virtual address that the driver can use at interrupt time. SemRequest 06H K U Claims (sets) a semaphore. If the semaphore is already set, blocks the thread until the semaphore is available. SemClear 07H K I U Clears a semaphore, restarting any threads that were blocking on the semaphore. DevHlp DevHlp DevHlp Name Code K I U N Action semaphore. Miscellaneous Device Helpers SchedClockAddr 00H K N Obtains the address of the kernel's routine to be called on each clock tick. Used only by the system's clock driver. SendEvent 25H K I Signals the kernal that a critical event has been detected, such as the Session Manager hot key or a Ctrl- Break. GetDOSVar 24H K N Returns a bimodal pointer to a table of useful kernel addresses and variables. SetROMVector 1AH K N Captures an interrupt vector DevHlp DevHlp DevHlp Name Code K I U N Action SetROMVector 1AH K N Captures an interrupt vector normally used in real mode to invoke a ROM BIOS function, returning the previous contents of the vector for chaining. ROMCritSection 26H U Protects ROM BIOS code from interrupts that might cause a cantext switch. Called by a driver in user mode. ABIOS Communication Device Helpers (IBM PS/2 only) ABIOSCall 36H K I U N Invokes an Advanced BIOS (ABIOS) service on behalf of the driver. ABIOSCommonEntry 37H K I U N Transfers to an ABIOS common entry point on behalf of the driver. DevHlp DevHlp DevHlp Name Code K I U N Action GetLIDEntry 34H K N Obtains a logical ID (LID) for a physical device for use with subsequent ABIOS calls. FreeLIDEntry 35H K N Releases a Logical ID (LID) for a physical device. Figure 8: Installable Driver Template name template page 55,132 title 'TEMPLATE - installable driver template' .286c ; ; TEMPLATE.ASM: A program skeleton for an installable ; OS/2 character device driver. ; ; The driver command code routines are stubs only and have ; no effect but to return a nonerror "Done" status. ; ; ; maxcmd equ 26 ; maximum allowed command code stdin equ 0 ; standard device handles stdout equ 1 stderr equ 2 cr equ 0dh ; ASCII carriage return lf equ 0ah ; ASCII line feed extrn DOSWRITE:far DGROUP group _DATA _DATA segment word public 'DATA' ; device driver header... header dd -1 ; link to next device driver dw 8880h ; device attribute word dw Strat ; "Strategy" routine entry point dw 0 ; (reserved) db 'TEMPLATE' ; logical device name db 8 dup (0) ; (reserved) devhlp dd ? ; DevHlp entry point wlen dw ? ; receives DOSWRITE length ; Strategy routine dispatch table ; for request packet command code dispch dw Init ; 0 = initialize driver dw MediaChk ; 1 = media check on block device dw BuildBPB ; 2 = build BIOS parameter block dw Error ; 3 = not used dw Read ; 4 = read (input) from device dw NdRead ; 5 = nondestructive read dw InpStat ; 6 = return input status dw InpFlush ; 7 = flush device input buffers dw Write ; 8 = write (output) to device dw WriteVfy ; 9 = write with verify dw OutStat ; 10 = return output status dw OutFlush ; 11 = flush output buffers dw Error ; 12 = not used dw DevOpen ; 13 = device open dw DevClose ; 14 = device close dw RemMedia ; 15 = removable media dw GenIOCTL ; 16 = generic IOCTL dw ResetMed ; 17 = reset media dw GetLogDrv ; 18 = get logical drive dw SetLogDrv ; 19 = set logical drive dw DeInstall ; 20 = deinstall dw PortAcc . ; 21 = reserved dw PartFD ; 22 = partitionable fixed disks dw FDMap ; 23 = get fixed disk unit map dw Error ; 24 = not used dw Error ; 25 = not used dw Error ; 26 = not used ident db cr,lf,lf db 'TEMPLATE Example OS/2 Device Driver' db cr,lf ident_len equ $-ident _DATA ends _TEXT segment word public 'CODE' assume cs:_TEXT,ds:DGROUP,es:NOTHING Strat proc far ; device driver Strategy routine, ; called by OS/2 kernel with ; ES:BX = address of request packet mov di,es:[bx+2] ; get command code from packet and di,0ffh cmp di,maxcmd ; supported by this driver? jle Strat1 ; jump if command code OK call Error ; bad command code jmp Strat2 Strat1: add di,di ; command code * 2 ; branch to appropriate routine call word ptr [di+dispch] Strat2: ; store subfunction status in mov es:[bx+3],ax ; AX into request packet ret ; back to OS/2 kernel Strat endp Intr proc far ; driver Interrupt handler clc ; signal that we owned interrupt ret Intr endp ; Command code routines are called by the Strategy routine ; via the Dispatch table with ES:BX pointing to the request ; header. Each routine should return ES:BX unchanged, ; and AX = status to be placed in request packet: ; 0100H if 'done' and no error ; 0000H if thread should block pending interrupt ; 81xxH if 'done' and error detected (xx=error code) ; MediaChk proc near ; function 1 = Media Check mov ax,0100h ; return 'done' status ret MediaChk endp BuildBPB proc near ; function 2 = Build BPB mov ax,0100h ; return 'done' status ret BuildBPB endp Read proc near ; function 4 = Read (Input) mov ax,0100h ; return 'done' status ret Read endp NdRead proc near ; function 5 = Nondestructive Read mov ax,0100h ; return 'done' status ret NdRead endp InpStat proc near ; function 6 = Input Status mov ax,0100h ; return 'done' status ret InpStat endp InpFlush proc near ; function 7 = Flush Input Buffers mov ax,0100h ; return 'done' status ret InpFlush endp Write proc near ; function 8 = Write (Output) mov ax,0100h ; return 'done' status ret Write endp WriteVfy proc near ; function 9 = Write with Verify mov ax,0100h ; return 'done' status ret WriteVfy endp OutStat proc near ; function 10 = Output Status mov ax,0100h ; return 'done' status ret OutStat endp OutFlush proc near ; function 11 = Flush Output Buffers mov ax,0100h ; return 'done' status ret OutFlush endp DevOpen proc near ; function 13 = Device Open mov ax,0100h ; return 'done' status ret DevOpen endp DevClose proc near ; function 14 = Device Close mov ax,0100h ; return 'done' status ret DevClose endp RemMedia proc near ; function 15 = Removable Media mov ax,0100h ; return 'done' status ret RemMedia endp GenIOCTL proc near ; function 16 = Generic IOCTL mov ax,0100h ; return 'done' status ret GenIOCTL endp ResetMed proc near ; function 17 = Reset Media mov ax,0100h ; return 'done' status ret ResetMed endp GetLogDrv proc near ; function 18 = Get Logical Drive mov ax,0100h ; return 'done' status ret GetLogDrv endp SetLogDrv proc near ; function 19 = Set Logical Drive mov ax,0100h ; return 'done' status ret SetLogDrv endp DeInstall proc near ; function 20 = DeInstall Driver mov ax,0100h ; return 'done' status ret DeInstall endp PartFD proc near ; function 22 = Partitionable ; Fixed Disks mov ax,0100h ; return 'done' status ret PartFD endp FDMap proc near ; function 23 = Get Fixed Disk ; Logical Unit Map mov ax,0100h ; return 'done' status ret FDMap endp Error proc near ; bad command code in request header mov ax,8103h ; error bit + 'done' status ; + "Unknown Command" code ret Error endp Init proc near ; function 0 = Initialize Driver mov ax,es:[bx+14] ; pick up DevHlp entry point mov word ptr devhlp,ax mov ax,es:[bx+16] mov word ptr devhlp+2,ax ; set offsets to end of code ; and data segments mov word ptr es:[bx+14],offset _TEXT:Init mov word ptr es:[bx+16],offset DGROUP:ident ; display sign-on message... push stdout ; handle for standard output push ds ; address of message push offset DGROUP:ident push ident_len ; length of message push ds ; receives bytes written push offset DGROUP:wlen call DOSWRITE mov ax,0100h ; return 'done' status ret Init endp _TEXT ends end Figure 9: Sample Module Definition File ; ; module definition file for ; TEMPLATE OS/2 character device driver ; LIBRARY TEMPLATE PROTMODE Figure 10: MASM TEMPLATE MASM TEMPLATE; LINK TEMPLATE,TEMPLATE.SYS,,DOSCALLS,TEMPLATE; Summary of the way Device Drivers Queue and Process I/O Requests. ┌─────────────────┐ ┌────────────────────┐ │ OS/2 Kernal │ │ Hardware Interrupt │ └────────┬────────┘ └──────────┬─────────┘ ┌────────────▼────────────┐ ┌────────────▼─────────────┐ │ ES:BX ────► I/O Request │ │ OS/2 Interrupt Manager │ └────────────┬────────────┘ └────────────┬─────────────┘ ┌────────────▼────────────┐ ┌────────────▼─────────────┐ │ Device Strategy Routine │ │ Device Interrupt Routine │ └────────────┬────────────┘ └────────────┬─────────────┘ ┌────────────▼────────────┐ ┌────────────▼─────────────┐ │Call DevHlp PushReqPacket│ │Service Hardware Interrupt│ └──────┬──────────────────┘ └────────────┬─────────────┘ ┌──────▼────────┐ ┌────┐ ┌────────────▼───────┐ │Is Device Idle?├──►│ No ├┐ │Is Request Complete │ ┌────┐ └──────┬────────┘ └────┘│ ┌────┤ or Error ├─►│ No ├┐ ┌──▼──┐ │ │ └────────────────────┘ └────┘│ │ Yes │ │ ┌──▼──┐ ┌─────────────────┐ │ └──┬──┘ │ │ Yes ├─────►│Set Packet Status│ │ ▼ │ └─────┘ └───────┬─────────┘ │ Call StartNextRequest │ ┌───────────▼─────────┐ │ │◄─────────────────┘ ┌───────┤ Call DevHlp DevDone │ │ ┌──────┴─────────┐ │ └─────────────────────┘ │ │ Far Return │ ▼ ┌────────────────┐ │ └────────────────┘ Call StartNextRequest─────►│ Far Return │◄───┘ └────────────────┘ StartNextRequest │ ┌──────────▼────────────┐ │ Call DevHlp │ │ Pull ReqPacket │ └──────────┬────────────┘ ┌──────────▼────────────┐ │ Start Hardware │ │ Processing of Request │ └──────────┬────────────┘ ┌───────▼────────┐ │ Far Return │ └────────────────┘ ████████████████████████████████████████████████████████████████████████████ Exploring the Structure and Contents of the MS-DOS Object Module Format☼ Richard Wilton A programmer writing a compiler, an assembler, or a language translator must know the details of object module format and processing. To take advantage of LIB and LINK, a language translator must construct object modules that conform to the format and usage conventions specified by Microsoft. Although some MS-DOS language translators generate executable 8086- family machine code directly from source code, most produce object code instead. Typically, a translator processes each file of source code individually and leaves the resulting object module in a separate file bearing an .OBJ extension. The source-code files themselves remain unchanged. After all of a program's source-code modules have been translated, the resulting object modules can be linked into a single executable program. Because object modules often represent only a portion of a complete program, each source-code module usually contains instructions that indicate how its corresponding object code is to be combined with the object code in other object modules when they are linked. The object code contained in each object module consists of a binary image of the program plus program structure information. This object code is not directly executable. The binary image corresponds to the executable code that will ultimately be loaded into memory for execution; it contains both machine code and program data. The program structure information includes descriptions of logical groupings defined in the source code (such as named subroutines or segments) and symbolic references to addresses in other object modules. The program structure information is used by a linkage editor, or linker, such as Microsoft LINK to edit the binary image of the program contained in the object module. The linker combines the binary images from one or more object modules into a complete executable program. The linker's output is an EXE file-a file containing machine code that can be loaded into RAM and executed (see Figure 1). The linker leaves intact all of the object modules it processes. Object code thus serves as an intermediate form for compiled programs. This form offers two major advantages: ■ Modular intermediate code. The use of object modules eliminates the overhead of repeated compilation of an entire program whenever changes are made to parts of its source code. Instead, only those object modules affected by source-code revisions need be recompiled. ■ Shareable format. Object module format is well defined, so object modules can be linked even if they were produced by different translators. Many high-level-language (HLL) compilers take advantage of this commonality of object-code format to support "interlanguage" linkage. Object Module Contents Object modules contain five basic types of information. Some of this information exists explicitly in the source code (and is subsequently passed on to the object module), but much is inferred by the program translator from the structure of the source code and the way memory is accessed by the 8086. Binary image. As described earlier, the binary image comprises executable code (such as opcodes and addresses) and program data. When object modules are linked, the linker builds an executable program from the binary image in each object module it processes. The binary image in each object module is always associated with program structure information that tells the linker how to combine it with related binary images in other object modules. External references. Since an object module generally represents only a small portion of a larger program that will be constructed from several object modules, it usually contains symbols that permit it to be linked to the other modules. Such references to corresponding symbols in other object modules are resolved when the modules are linked. For example, consider the following short C program: main() { puts("Hello, world\n"); } This program calls the C function puts() to display a character string, but puts() is not defined in the source code. Rather, the name puts is a reference to a function that is external to the program's main() routine. When the C compiler generates an object module for this program, it will identify puts as an external reference. Later, the linker will resolve the external reference by linking the object module containing the puts() routine with the module containing the main() routine. Address references. When a program is built from a group of object modules, the actual values of many addresses cannot be computed until the linker combines the binary image of executable code and the program data from each of the program's constituent object modules. Object modules contain information that tells the linker how to resolve the values of such addresses, either symbolically (as in the case of external references) or relatively, in terms of some other address (such as the beginning of a block of executable code or program data). Debugging information. An object module can also contain information that relates addresses in the executable program to the corresponding source code. After the linker performs its address fixups, it can use the object module's debugging information to relate a line of source code in a program module to the executable code that corresponds to it. Miscellaneous information. Finally, an object module can contain comments, lists of symbols defined in or referenced by the module, module identification information, and information for use by an object library manager or a linker (for example, the names of object libraries to be searched by default). Terminology When the linker generates an executable program, it organizes the structural components of the program according to the information contained in the object modules. The layout of the executable program can be conceptually described as a run-time memory map after it has been loaded into memory. The basic structure of every executable program for the 8086 family of microprocessors must conform to the segmented architecture of the microprocessor. Thus, the run-time memory map of an executable program is partitioned into segments, each of which can be addressed by using one of the microprocessor's segment registers. This segmented structure of 8086-based programs is the basis for most of the following terminology. Frames. The memory address space of the 8086 is conceptually divided into a sequence of paragraph-aligned, overlapping 64Kb regions called frames. Frame 0 in the 8086's address space is the 64Kb of memory starting at physical address 00000H (0000:0000 in segment:offset notation), frame 1 is the 64Kb of memory starting at 00010H (0001:0000), and so forth. A frame number thus denotes the beginning of any paragraph-aligned 64Kb of memory. For example, the location of a 64Kb buffer that starts at address B800:0000 can be specified as frame 0B800H. Logical segments. The run-time memory map for every 8086 program is partitioned into one or more logical segments, which are groupings of logically related portions of the program. Typically, an MS-DOS program includes at least one code segment (that contains all of the program's executable code), one or more data segments (that contain program data), and one stack segment. When a program is loaded into RAM to be executed, each logical segment in the program can be addressed with a frame number──that is, a physical 8086 segment address. Before the MS-DOS loader transfers control to a program in memory, it initializes the CS and SS registers with the segment addresses of the program's executable code and stack segments. If an MS-DOS program has a separate logical segment for program data, the program itself usually stores this segment's address in the DS register. Relocatable segments. In MS-DOS programs, most logical segments are relocatable. The loader determines the physical addresses of a program's relocatable segments when it places the program into memory to be executed. However, this address determination poses a problem for the MS- DOS loader, because a program may contain references to the address of a relocatable segment even though the address value is not determined until the program is loaded. The problem is solved by indicating where such references occur within the program's object modules. The linker then extracts this information from the object modules and uses it to build a list of such address references into a segment relocation table in the header of executable files. After the loader copies a program into memory for execution, it uses the segment relocation table to update, or fix up, the segment address references within the program. Consider the example in Figure 2, in which a program loads the starting addresses of two data segments into the DS and ES segment registers. The actual addresses of the _DATA and FAR_DATA segments are unknown when the source code is assembled and the corresponding object module is constructed. The assembler indicates this by including segment fixup information, instead of actual segment addresses, in the program's object module. When the object module is linked, the linker builds this segment fixup information into the segment relocation table in the header of the program's EXE file. Then, when the EXE file is loaded, the MS-DOS loader uses the information in the EXE file's header to patch the actual address values into the program. Absolute segments. Sometimes a program needs to address a predetermined segment of memory. In this case, the program's source code must declare an absolute segment so that a reference to the corresponding frame number can be built into the program's object module. For example, a program might need to address a video display buffer located at a specific physical address. The following assembler directive declares the name of the segment and its frame number: VideoBufferSegSEGMENT at 0B800h Segment alignment. When a program is loaded, the physical address of each logical segment is constrained by the segment's alignment. A segment can be page aligned (aligned on a 256-byte boundary), paragraph aligned (aligned on a 16-byte paragraph boundary), word aligned (aligned on an even-byte boundary), or byte aligned (not aligned on any particular boundary). A specification of each segment's alignment is part of every object module's program structure information. High-level-language translators generally align segments according to the type of data they contain. For example, executable code segments are usually byte aligned; program data segments are usually word aligned. With an assembler, segment alignment can be specified with the SEGMENT directive and the assembler will build this information into the program's object module. Concatenated segments. The linker can concatenate logical segments from different object modules when it builds the executable program. For example, several object modules may each contain part of a program's executable code. When the linker processes these object modules, it can concatenate the executable code from the different object modules into one range of contiguous addresses. The order in which the linker concatenates logical segments in the executable program is determined by the order in which the linker processes its input files and by the program structure information in the object modules. With a high-level-language translator, the translator infers which segments can be concatenated from the structure of the source code and proceeds to build appropriate segment concatenation information into the object modules it generates. With an assembler, the segment class type can be used to indicate which segments can be concatenated. Groups of segments. Segments with different names may also be grouped together by the linker so that they can all be addressed within the same 64Kb frame, even though they are not concatenated. For example, it might be desirable to group program data segments and a stack segment within the same 64Kb frame so that program data items and data on the stack can be addressed with the same 8086 segment register. In high-level languages, it is up to the translator to incorporate appropriate segment grouping information into the object modules it generates. With an assembler, groups of segments can be declared with the GROUP directive. Fixups. Sometimes a compiler or an assembler encounters addresses whose values cannot be determined from the source code. The addresses of external symbols are an obvious example. The addresses of relocatable segments and of labels within those segments are another example. A fixup is a language translator's way of passing the buck about such addresses to the linker. Typically, a translator builds a zero value in the binary image at locations where it cannot store an actual address. Accompanying each such location is fixup information, which allows the linker to determine the correct address. The linker then completes the fixup by calculating the correct address value and adding it to the value in the corresponding location in the binary image. The only fixups the linker cannot fully resolve are those that refer to the segment address of a relocatable segment. Such addresses are not known until the program is actually loaded, so the linker, in turn, passes the responsibility to the MS-DOS loader by creating a segment relocation table in the header of the executable file. To process fixups properly, the linker needs three pieces of information: the LOCATION of the value in the object module, the nature of the TARGET (the address whose value is not yet known), and the FRAME in which the address calculations are to take place. Object modules contain the LOCATION, TARGET, and FRAME information the linker uses to calculate the appropriate address for any given fixup. Consider the program in Figure 3. The statement: start: call far ptr FarProc contains a reference to an address in the logical segment FarSeg2. Because the assembler does not know the address of FarSeg2, it places fixup information about the address into the object module. The LOCATION to be fixed up is 1 byte past the label start (the 4-byte pointer following the call opcode 9AH). The TARGET is the address referenced in the call instruction-that is, the label FarProc in the segment FarSeg2. The FRAME to which the fixup relates is designated by the group FarGroup and is inferred from the statement ASSUME cs:FarGroup in the FarSeg2 segment. There are several different ways for a language translator to identify a fixup. For example, the LOCATION might be a single byte, a 16-bit offset, or a 32-bit pointer, as in Figure 3. The TARGET might be a label whose offset is relative either to the base (beginning) of a particular segment or to the actual LOCATION. The FRAME might be a relocatable segment, an absolute segment, or a group of segments. Taken together, all the information in an object module that concerns the alignment and grouping of segments can be regarded as a specification of a program's run-time memory map. In effect, the object module specifies what goes where in memory when a program is loaded. The linker can then take the program structure information in the object modules and generate a file containing an executable program with the corresponding structure. Structure Although object modules contain the information that ultimately determines the structure of an executable program, they bear little structural resemblance to the resulting executable program. Each object module is made up of a sequence of variable-length object records. Different types of object records contain different types of program information. Each object record begins with a 1-byte field that identifies its type. This is followed by a 2-byte field containing the length (in bytes) of the remainder of the record. Next comes the actual structural or program information, represented in one or more fields of varied lengths. Finally, each record ends with a 1-byte checksum. The sequence in which object records appear in an object module is important. Because the records vary in length, each object module must be constructed linearly, from start to end. More important, however, is the fact that some types of object records contain references to preceding object records. Because the linker processes object records sequentially, the position of each object record within an object module depends primarily on the type of information each record contains. Object Record Types Microsoft(R) LINK currently recognizes 14 types of object records, each one of which carries a specific type of information within the object module. Each type of object record is assigned an identifying six-letter abbreviation, but these abbreviations are used only in documentation, not within an object module itself. As already mentioned, the first byte of each object record contains a value that indicates its type. In a hexadecimal dump of the contents of an object module, these identifying bytes identify the start of each object record. Figure 4 lists the types of object records supported by LINK. The value of each record's identifying byte (in hexadecimal) is included, along with the six-letter abbreviation and a brief functional description. The functions of the 14 types of object records fall into six general categories: ■ Binary data (executable code and program data) is contained in the LEDATA and LIDATA records. ■ Address binding and relocation information is contained in FIXUPP records. ■ The structure of the run-time memory map is indicated by the SEGDEF, GRPDEF, COMDEF, and TYPDEF records. ■ Symbol names are declared in LNAMES, EXTDEF, and PUBDEF records. ■ Debugging information is in the LINNUM record. ■ Finally, the structure of the object module itself is determined by the THEADR, COMENT, and MODEND records. Object Record Order The sequence in which the types of object records appear in an object module is fairly flexible in some respects. Several record types are optional, and if the type of information they carry is unnecessary, they are omitted from an object module. In addition, most object record types can occur more than once in the same object module. And, because object records are variable in length, it is often possible to choose, as a matter of convenience, between combining information into one large record or breaking it down into several smaller records of the same type. As stated previously, one important constraint on the order in which object records appear is the need for some types of object records to refer to information contained in other records. Because the linker processes the records sequentially, object records containing such information must precede the records that refer to it. For instance, two types of object records, SEGDEF and GRPDEF, refer to the names contained in an LNAMES record. Thus, an LNAMES record must appear before a SEGDEF or GRPDEF record that refers to it so that the names in the LNAMES record are known to the linker by the time it processes the SEGDEF or GRPDEF records. References Between Object Records Object records can refer to information in other records either indirectly, by means of implicit references, or directly, by means of indexed references to names or other records. Implicit references. Some types of object records implicitly reference another record in the same object module. The most important example of such implicit referencing is in the FIXUPP record, which always contains fixup information for the preceding LEDATA or LIDATA record in the object module. Any time an LEDATA or LIDATA record contains a value that needs to be fixed up, the next record in the object module is always a FIXUPP record containing the actual fixup information. Indexed references to names. An object record that refers to a symbolic name, such as the name of a segment or an external routine, uses an index into a list of names contained in a previous object record. The first name in such a list has the index number 1, the second name has index number 2, the third has index number 3, and so on. Altogether, a list of as many as 32,767 (7FFFH) names can be incorporated into an object module-generally adequate for even the most verbose programmer. (LINK does, however, impose its own version-specific limits.) Indexed references to object records. An object record can also refer to a previous object record by using the same type of index. In this case, the index number refers to one of a list of object records of a particular type. For example, a FIXUPP record might refer to a segment by referencing one of several preceding SEGDEF records in the object module. In that case, a value of 1 would indicate the first SEGDEF record in the object module, a value of 2 would indicate the second, and so on. The index-number field in an object record can be either 1 or 2 bytes long. If the number is in the range 0-7FH, the high-order bit (bit 7) is 0 and the low-order 7 bits contain the index number, so the field is only 1 byte long (see Figure 5). If the index number is in the range 80-7FFFH, the field is 2 bytes long. The high-order bit of the first byte in the field is set to 1, and the high-order byte of the index number (which must be in the range 07FH) fits in the remaining 7 bits. The low-order byte of the index number is specified in the second byte of the field (see Figure 6). The same format is used whether an index refers to a list of names or to a previous object record. Microsoft 8086 Object Record Formats Just as the design of the Intel(R) 8086 microprocessor reflects the design of its 8-bit predecessors, 8086 object record formats are reminiscent of the 8-bit software tradition. In 8-bit systems, disk space and RAM were often at a premium. To minimize the space consumed by object records, information is packed into bit fields within bytes and variable- length fields are frequently used. Microsoft LINK recognizes a major subset of Intel's original 8086 object module specification (Intel Technical Specification 121748-001). Intel also proposed a six-letter name for each type of object record and symbolic names for fields. These names are documented in The MS-DOS Encyclopedia and appear in the order shown earlier in Figure 4. The Intel record types that are not recognized by LINK provide information about an executable program that MS-DOS obtains in other ways. For example, information about run-time overlays is supplied in LINK's command line rather than being encoded in object records. (Because they are ignored by LINK, they are not included in the Encyclopedia.) All 8086 object records conform to the format shown in Figure 7. The record type field is a 1-byte field containing the hexadecimal number that identifies the type of object record (see Figure 4). The record length is a 2-byte field that gives the length of the remainder of the object record in bytes (not including the bytes in the record type and record length fields). The record length is stored with the low-order byte first. The body field of the record varies in size and content, depending on the record type. The checksum is a 1-byte field that contains the negative sum (modulo 256) of all other bytes in the record. In other words, the checksum byte is calculated so that the low-order byte of the sum of all the bytes in the record, including the checksum byte, equals zero. Figure 1: Generation of an executable (EXE) file ╔═════════════════╗ ║ Source ║ ║ Code ║ ╚════════╦════════╝ ┌─────────╨─────────┐ │Language translator│ │ or assembler │ └─────────╥─────────┘ ▼ ╔═════════════════╗ ┌──────────────┐ ╔═════════════════╗ ║ Object module ║◄═══╡Object module ╞════►║ Object library ║ ║ (OBJ file) ║ │librarian(LIB)│ ║ (LIB file) ║ ╚════════╦════════╝ └──────────────┘ ╚════════╦════════╝ ╚════════════════════╦══════════════════════╝ Linker (LINK) ║ ▼ ╔═════════════════╗ ║Executable binary║ ║ image (EXE file)║ ╚════════╦════════╝ MS-DOS loader ▼ Program runs Figure 2: Obtaining Data Segment Starting Addresses mov ax,seg _DATA mov ds,ax ; make _DATA segment ; addressable through DS mov ax,seg FAR_DATA mov es,ax ; make FAR_DATA segment ; addressable through ES Figure 3: A sample "program" containing statements from which the assembler derives fixup information. title fixups FarGroup GROUP FarSeg1, FarSeg2 0000 CodeSeg SEGMENT byte public 'CODE' ASSUME cs:CodeSeg 0000 9A 0000 -- R start: call far ptr FarProc 0005 CodeSeg ENDS 0000 FarSeg1 SEGMENT byte public ; part of FarGroup 0000 FarSeg1 ENDS 0000 FarSeg2 SEGMENT byte public ASSUME cs:FarGroup 0000 FarProc PROC far 0000 CB ret ; a FAR return FarProc ENDP 0001 FarSeg2 ENDS END Figure 4: Types of 8086 Object Records Supported by Microsoft LINK ID byte Abbreviation Description 80H THEADR Translator Header Record 88H COMENT Comment Record 8AH MODEND Module End Record 8CH EXTDEF External Names Definition Record 8EH TYPDEF Type Definition Record 90H PUBDEF Public Names Definition Record 94H LINNUM Line Number Record 96H LNAMES List of Names Record 98H SEGDEF Segment Definition Record 9AH GRPDEF Group Definition Record 9CH FIXUPP Fixup Record A0H LEDATA Logical Enumerated Data Record A2H LIDATA Logical Iterated Data Record B0H COMDEF Communal Names Definition Record Figure 5: One-byte Index Number. ┌────────────────────────────────────────────────────┐ │ │█ │ bit 7 6 5 4 3 2 1 0 │█ │ ╔═══╦══════════════════════════════════╗ │█ │ ║ 0 ║ Index Number ║ │█ │ ╚═══╩══════════════════════════════════╝ │█ │ │█ └────────────────────────────────────────────────────┘█ ████████████████████████████████████████████████████ Figure 6: One-byte Index Number. bit 7 6 5 4 3 2 1 0 7 6 5 4 3 2 1 0 ╔══╦══════════════════════════╦═══════════════════════════════╗ ║ 1║ High-Order Byte ║ Low-Order Byte ║ ╚══╩══════════════════════════╩═══════════════════════════════╝ First byte Second byte Figure 7: 8086 Object Record Format ┌───────────────────────────────────────────────┐ │ │█ │ ╔══════╦═════╦═════╦════/═════╦═══════╗ │█ │ ║Record║ Record ║ Body ║ Chk ║ │█ │ ║ type ║ length ║ ║ sum ║ │█ │ ╚══════╩═════╩═════╩════/═════╩═══════╝ │█ │ │█ └───────────────────────────────────────────────┘█ ████████████████████████████████████████████████ ████████████████████████████████████████████████████████████████████████████ A Guide to Program Editors, the Developer's Most Important Tool ─────────────────────────────────────────────────────────────────────────── Also see the following related articles: The Brief Editor A BRIEF Journey The ME Text Editor Inside ME A Guide to Developers ─────────────────────────────────────────────────────────────────────────── Tony Rizzo The personal computing platform has evolved into a serious and important programming environment. Whenever one thinks of a programming environment, what usually comes to mind is the compiling or assembling of source code and the tools associated with that process, namely compilers and assemblers. However, compilers and assemblers are the very last links in a long chain of events and may represent only a small fraction of a program's development cycle. Throughout this cycle, from concept to design to the actual building of a program, runs a fundamental thread──the evolution of the text that is the source code of a program. What the professional programmer really spends most of his or her time doing is writing and editing the source code by working with a text editor. In the early days of personal computing, many programmers used word processors. WordStar(R), for example, often doubled as a text editor, providing the ability to create a "text" file by stripping any special codes prior to actually writing out the file. It was the first real text manipulation program available for the PC, and there are many programmers who still refuse to consider any other alternative. WordStar continues to influence even the newest compilers as evidenced by the emergence of the integrated editor/compiler environment. These compilers provide a WordStar-compatible (or keystroke-compatible) integrated editor. The PC software business has grown at a tremendous rate, and the "garage/home-brewed" software of yesterday has in many cases given way to sophisticated software teams working in highly competitive and fast- paced environments. A major factor in a programmer's success is productivity. New and faster compilers, assemblers, and debuggers are emerging all the time. Compilers now offer sophisticated code optimization capabilities, and tools such as CodeView(R) provide sophisticated debugging aid. But such tools are of little use without source code. The key to enhancing productivity is the ability to create well-designed source code in the least possible time. Consequently, WordStar no longer meets the strenuous demands of the professional PC programmer. There is a wide variety of editors available to programmers, ranging in price from as little as $40 to more than $300. Some editors are based on mainframe and minicomputer predecessors. Some were specifically developed to operate within the PC environment. Some label themselves as intended strictly for the professional programmer. Some purport to be all things to all people. Whatever their individual heritage or claim to fame might be, there are enough options that a programmer might have difficulty in choosing one editor. The technological advancements of recent generations of PCs have brought us faster, more sophisticated hardware capabilities, particularly in graphics. Each editor offers a unique set of features──multiple windows, EGA support, configurable keyboards, built-in macro languages, and the ability to compile a program using the programmer's preferred compiler or assembler on the fly from within the editor itself. Selecting the "right" editor is a very personal decision. How many times have you heard a fellow programmer complain about his editing tools? Perhaps you've spent most of your professional career working on a UNIX(R) system, only to discover that Emacs and VI don't quite make it in the PC world. Perhaps you only use a mainframe-based editor because you feel safe in knowing how it operates, even though its performance is clearly awful on your AT. Or perhaps you are that WordStar person who'd rather fight than switch. In order to help with your decision, MSJ set out to do a comprehensive review of all the commercially available and advertised program editors. We settled on twelve analysis areas by which to judge each editor: ■ basic editor functions ■ user interface ■ macro capabilities ■ file handling ■ special features ■ support for unique language features ■ print capabilities ■ word processing features ■ documentation ■ setup/installation ■ help/tutorial ■ speed Our first objective was to analyze basic editing and productivity features such as invoking a compiler from within the editor, editing multiple files simultaneously, and multiple windows. Our survey of programmers' requirements also indicated the need for certain word processing features to create quick documentation, write notes, memos, and so on. The ability to wordwrap, justify, and center is also handy. Speed is important, but we weren't as interested in the raw speed required, for example, to go from the top to the bottom of a document as we were in factors such as the number of keystrokes necessary to effect such a move. Often the time spent or wasted in keyboard manipulations plays a much larger role in one's perception of a product's quickness than raw speed. The difference between 2 and 4 seconds is not always as significant as having to use six keystrokes instead of two to do something. Nevertheless, for those of you who value such measurements we have included two. These speed measurements were all recorded on an unmodified 8-MHz IBM(R) PC/AT(R) (model 339). The chart☼ accompanying the reviews lists the capabilities of all twelve editors. We have listed the editors in order from highest rating to lowest. The editors that received the three highest ratings were chosen for more detailed review. In addition, we offered the developers the opportunity to comment on some of the underlying design and technology issues they had to deal with in order to create their editors. We also encouraged them to give us a glimpse into their plans for the future. What we were looking for in this search-and-discover mission was proof that text editors have been keeping pace with other, more heavily advertised systems tools. We think our results point to the best-engineered editors now available on the market. Editing tools may not seem as important to obtain as the latest and greatest optimizing compiler. However, given the capabilities of this new generation of editors, we strongly suggest that you seriously reevaluate your current text editor. See how it stacks up against the best editors now on the market. You may just discover that a big boost to your productivity, as well as to your competitiveness as a programmer, awaits you. Microsoft Systems Journal believes strongly in the success of well- engineered products, whether they are Microsoft's or another vendor's. The three text editors that received the highest ratings are highly recommended──they clearly deserve your attention. There was one big surprise among the top three editors. Magma Systems' ME retails for all of $39.95. What can one say about such a discovery? The reviewer has considerable programming experience and he was very impressed, enough to consider it superior to other editors costing many times its price. Microsoft feels that OS/2 is going to become the operating system of choice for the majority of PCs over the next several years and is the ideal platform for the development of the next generation of applications. Is there an editor out there that will not only keep you competitive today but also in the protected-mode operating system world of tomorrow? Read on and find out. ─────────────────────────────────────────────────────────────────────────── The BRIEF Editor ─────────────────────────────────────────────────────────────────────────── Matt Trask BRIEF(R), an acronym for "Basic Reconfigurable Interactive Editing Facility," is a full-featured programmer's editor for DOS-based personal computers. Marketed by Solution Systems(R), it is moderately priced at $195. Even though it is not my usual editor, BRIEF is intuitive enough and the on-line help is good enough that I was able to use it to write this review. Documentation The documentation consists of two wire-bound paperback manuals and a quick reference booklet, enclosed in the typical IBM-style slip case so that they can maintain their own shelf space. The User's Guide includes a tutorial, an alphabetical reference to the editor's commands, and an index. In addition there is a complete summary of messages that BRIEF can generate and a chapter with some common questions and answers about editor operation. The Macro Language Guide is a complete tutorial and reference for BRIEF's built-in macro capability. As in any language reference there is a section that defines the syntax and structure of the language. Two examples of real-world macros are given-a searching package and a restore function. The section on the restore function is an informative and sometimes humorous visit with one of the editor's authors, Dave Nanian, as he demonstrates the process of developing a macro that can restore the editor's state (such as open files, search pattern, search direction, and windows) on startup to the point where it was previously exited. This section is an excellent insight into program development methods for any new programmer as Nanian goes through the whole cycle (idea, fleshing it out, pseudo-code, detail work, getting stuck, maintenance) in the example. The documentation includes a card that summarizes the side effects of the installation procedure so that you will have an idea of what the setup program will or will not do to your AUTOEXEC.BAT before you run it. The quick reference booklet is somewhat superfluous as it is easier to use the on-line help than to find and page through a printed reference. Although the tutorial provided with BRIEF is not of the flashy on-line computer-assisted-instruction type, it is thorough and more than adequate to get a new user up and running in a short amount of time. The first forty-odd pages of the User Guide are an interactive quick-start lesson that goes through the few editor features needed for most purposes. This tutorial covers startup, shutdown, editing, cursor motion, help, undo, cutting, pasting, windows, and other editing functions. At the end of this section there is a quiz to test your comprehension of the material by solving a typical editing problem. The next five chapters cover BRIEF in greater depth, getting into theory of operation and more advanced functionality. Setup and Installation BRIEF comes with a program called SETUP.EXE that configures many of the editor's operating parameters. You need to run this program once before using the editor, but the default values are adequate for getting started. SETUP can be run again later to reconfigure the editor as you become more familiar with the available features and wish to tune them to your preferences. CONFIG.SY and AUTOEXEC.BAT are modified by the setup program to add device drivers, if needed, and set environment variables that inform the editor of your choices. The documentation for the AUTOEXEC changes is adequate in case you wish to change these by hand instead of using SETUP.EXE. In addition, the setup program generates and compiles a startup macro that contains initialization commands for screen colors, autosave, and other options that can be customized. You can edit and recompile this macro to add any other actions you desire at startup. Among the configurable parameters are display parameters (color preferences, cursor shape, use of 43-line mode on EGA, snow test on CGA adapters, and video page test), filename extensions (compiler control commands, word processing features and autoindentation, including normal, smart, and template), and safety features (enable autosave, set autosave timeout, use of backup files, depth of undo stack, and enable session save/restore). Other parameters include initial insert/overstrike mode, empty spaces filled with tabs or spaces, whether to use Ctrl-Z at end of file, keyboard autorepeat rate, default line length, case sensitivity during searches, and use of regular expressions in searches. Context-sensitive help is available during setup. The setup help has a message, "Press Alt-H if you need more assistance," that appears and flashes intermittently after a period of inactivity on the keyboard. This attention to detail in the user interface is indicative of the thoroughness of the product. The only complaints I have with the setup procedure are the assumptions about drives and directories. SETUP asks which disk is to be used and then creates a subdirectory called \BRIEF, rather than asking where the editor's files should be kept. It assumes that the target drive is also the boot drive and therefore creates and modifies AUTOEXEC.BAT and CONFIG.SYS in the root directory of this drive rather than finding out where the real copies of these files are. Help The context-sensitive pop-up help system is excellent. Pressing Alt-H brings up a help menu during text entry or editing. This menu is the entry into a hierarchical help window subsystem that covers such topics as blocks, keys and commands, and windows. After selecting one of the menu choices, another press of Alt-H shows which key or key combination performs that editor function. If you are currently in the middle of an editor function such as Alt-E (open and edit a new file), Alt-H goes directly to the help window that describes that function. There is also an on-line quick reference available from the help menu that lists all available editor functions and their current key assignments. Another menu selection accepts a keystroke and tells you what function is performed by that key. Basic Editor Functions BRIEF does almost everything imaginable in the areas of cursor motion, block operations, delete, copy, search-and-replace, and so on, and does them quite well. One powerful feature not usually found in PC editors is the availability of UNIX-style regular expressions for searching, in addition to simple string matching. For instance, the angle bracket in a search for "<if" causes BRIEF to find all lines beginning with "if," and a search for "{[Hh]i} world" will find "hi world" and "Hi world." The ability to adjust the keyboard repeat rate at setup time affects the cursor motion. I made the mistake of setting this at the maximum repeat rate for efficiency, only to watch as a repeated backspace to correct an error about 10 characters to the left gobbled up all the text on the line. I was rescued by the undo (Alt-U) capability, which is configurable as a stack that saves editor actions, not just deleted text as some editors do. This is usually set up to save the 30 most recent actions, but you can configure it to your liking. Word processing is not a strong point of BRIEF, but it is certainly adequate for writing documentation and memos. A macro package is provided that can be attached to a file extension such as .DOC or .TXT and automatically enable the available word processing features when a file with this extension is edited. BRIEF's features include wordwrap, margins, centering, and automatic paragraph reformat. If you want more, you can enhance the supplied source code to the word processing macros. A truly ambitious programmer could conceivably write a full-featured word processor by using nothing more than BRIEF's macro language. User Interface The user interface in BRIEF is more visual than other, more terminal- oriented editors. It takes advantage of special PC features such as color and line drawing characters that are usually not available in non-PC environments. Since every key on the keyboard is reconfigurable, you can assign the editing functions such as search or delete in any way that you please. Macros can be assigned to individual keys or can be executed by the Execute Command key (F10 in the default configuration). This lets you configure BRIEF to look like another editor more to your liking, for instance PC/Vi or WordStar. A package that configures BRIEF to PC/Vi's user interface is available from the authors. The remember and playback functions make it easy to record keystrokes on the fly for reuse if you need to repeat a sequence of actions. Screen layout is one area in which personal preference often borders on fanaticism. BRIEF has all the things that I like on the screen, no more and no less. Besides the name of the current document in the top border, the status line at the bottom of the screen shows the current line and column occupied by the cursor and the time of day. If you don't like a border around the screen, you can disable it at setup time. You can modify foreground, background, and message colors with SETUP to support whatever is available on your system. BRIEF uses tiled windows instead of the overlapping Macintosh-style windows. I generally prefer the latter, but in an editing context, a window is usually used when more than one file has to be visible, so this is not really a limitation. A 43-line display mode is available when running on an EGA but the tiny characters are a little tough on the eyes. This large screen mode would be useful if you are in an extended editing session that requires the use of windows, since you can make each window larger or have more windows. The cursor shape changes to indicate which mode (insert or overstrike) is currently in effect. The space past the end of the last character on a line is referred to as virtual space──the cursor is allowed to move there but it changes shape to indicate that it is in virtual space rather than on top of a space character. Nonstandard enhanced displays such as the WYSE(TM) 700 and Hercules(R) Graphics Plus are supported at their maximum resolution by device drivers that are installed by SETUP. Unique Language Features BRIEF supports autoindenting and delimiter checking for all languages. The C language support is quite extensive. "Smart" indenting uses a knowledge of the language syntax to automatically indent and outdent as control structures open and close. Template indenting provides for automatic completion of C language keywords. The keyword completion can be confusing; it is probably useful for a programmer who only uses C. Macros BRIEF is truly outstanding in its macro capabilities. The built-in LISP- like macro language is a complete general-purpose programming language. It covers all the features that you would expect in a language except port I/O and system calls. Even this is not too much of a limitation; if you want to write a program such as a word processor with BRIEF macros, the built-in "dos" macro can execute external programs that perform these operations. The exit codes from such programs are retrieved by BRIEF and are returned to the calling macro. If the external program needs to return data, a file can be used for communication. Rudimentary timer functionality is performed by registering a macro for idle time processing when the keyboard is inactive. This mechanism operates BRIEF's autosave feature whenever there is a period of keyboard inactivity. The amount of time delay before autosave starts is a setup option. Two sets of primitives are provided: language primitives such as if, while, get_parm, and editing primitives such as inq_position for cursor control and del_edge for window modification. These primitives are the equivalent of the function library that is provided with most language translators for linkage with your own programs. Some of the available functions are atoi (convert an ASCII number in a string to an integer), beep (sound the PC's speaker), color (set the foreground, background, and message colors), compress (shorten multiple white spaces to single ones), create_window (create an overlapping window such as that which the help subsystem uses), keyboard_flush (discard any pending keystrokes), remember (record a sequence of keystrokes to playback later), and write_buffer (write the current file out to disk). File Handling Maximum file size is limited only by the available disk space. BRIEF automatically pages a file to disk as needed to free up memory for editing. Inserting the contents of one file into another by using the multiple file editing commands and the block copy command is straightforward. Special Features BRIEF supports an extensive list of translators for operation within the editor, which includes automatic return to the source file on error with the cursor positioned at the cause of the error. Some of the supported translators are MASM, Lattice and Microsoft(R) C, dBASE(R), R-M COBOL, Lahey FORTRAN, Microsoft QuickBASIC, and Oregon Pascal-2. Many other compilers are supported, and examples show you how to integrate a compiler that is not supported into this environment. Multiple files can be opened at the same time with the Alt-B buffer command, which pops up a list of all currently open files for direct selection. Some or all of these files can be displayed in tiled windows simultaneously. I don't find windows to be particularly helpful on a system with a small screen such as an EGA. The Macintosh paradigm is only useful in that it models the way work is done by showing a pile of windows (tasks) currently available, but the current task is given a full-sized screen (or close to it) to work on. If you split an 80-column screen into left and right halves, enough work, such as code comments, is obscured that it is nearly useless. On the other hand, if you use BRIEF with a WYSE 700 display that supports 50-lines-by-160-columns, four full screens can be shown at a time. BRIEF has two features of great value: a support conference on the BIX network and autosave. The conference gives a user direct access to the program's authors, an excellent supplement to the staff support offer by Solution Systems, as well as a forum for macro exchange. The autosave feature writes a copy of the current file out to disk with the extension .ASV whenever the keyboard is idle and deletes this file on a normal exit to DOS. If there is a power failure (or you're working at home and your two year old discovers the Reset button), this file enables you to recover with no lost data. Speed I am accustomed to using 286-and 386-based systems where program performance is usually not a concern. One editor that I customarily use practically requires these faster systems to be useful. I installed BRIEF on an XT-type system to see if this was the case and found no discernible difference. Of course, major compute-intensive operations such as a large search-and-translate benefit from a faster CPU, as is true of most programs. The responsiveness of the keyboard and other parts of the user interface is crisp, with no noticeable delays. Print Capabilities Other than the ability to mark and print a block, there is no special support for printing in BRIEF. You can set right margins and use embedded formfeed characters for pagination. However, printer features such as fonts are not supported. Vendor Support Vendor support is very good. I had a lot of trouble getting through the initial installation of BRIEF due to a bug in another product that was installed by my AUTOEXEC.BAT. After a short description of the symptoms, the very sharp support person said, "Oh, you must be running xxx.COM; it has a bug in the use of the SI register." Instead of threatening you with imprisonment in return for no guarantee of product usefulness, the license agreement restricts you to using BRIEF on only one keyboard at a time. Solution Systems also offers a service in which you can have your name embedded into the code along with the serial number so that you can keep track of who is using your program. An OS/2 version of BRIEF is currently under development for release in early 1988, and BRIEF's author tells me that the current UNIX version is being ported to other popular UNIX environments that support Sys V.3. Conclusions There are a lot of things to like about BRIEF. It takes advantage of available PC features such as 43-line mode on the EGA. Multiple windows make it easy to keep a spec in view while writing new source code. Compilation within the editor with automatic return of the cursor to the error can save a lot of time waiting for printouts of compile-time errors. One power failure is all that it takes to appreciate autosave-the value is not in the saving of typing time, but in the time saved not having to reconstruct the thought processes behind your most recent changes. The one feature that makes BRIEF worth the effort of learning to use a new editor is its programmability. The restore macro is probably the best advance in productivity since editors that can operate on multiple files. The ability to return to your PC and invoke BRIEF such that it restores all opened files, cursor positions, and search strings allows you to casually walk away without worrying about the time it would take to restore your mental state when it is time to start again. If Solution Systems would just modify the restore macro to save multiple sessions for different concurrent projects, an excellent program would be even better. ─────────────────────────────────────────────────────────────────────────── A BRIEF Journey ─────────────────────────────────────────────────────────────────────────── David Nanian and Michael Strickman From the start, we envisioned that BRIEF would need to run under a number of different operating systems. To make this task easier, and because it's good programming practice, BRIEF was designed to be as modular as possible. We made logical divisions along functional boundaries, splitting the main program into a number of independently operating subsystems that communicated through standard function interfaces. While many of the data structures are shared between modules, all of the substructures that are related to a particular module are handled by that module alone. The basic modules, or subsystems include: virtual memory, windows, logical display (refresh), physical device, input handling, macro interpretation, undo, buffer modification, and command dispatch. Each of these subsystems can be replaced with functions tailored to a particular target system. The only requirement is that the new code must adhere to the original subsystem interface. In the case of operating system subsystem replacement, this usually means including some intermediate "glue" routines. Most of BRIEF's subsystems were designed to act as independently as possible. For example, the part of BRIEF that handles changes to files doesn't know anything about windows, nor does it know how the file it is manipulating is going to be displayed. It works purely at a stream level, and expects that other subsystems will provide the file's stream data and present the results to the user. Similarly, the refreshing subsystem doesn't really know anything about the physical representation of the windows that it's refreshing since it's completely device-independent. The subsystem only assumes that it is refreshing ASCII text (from the file subsystem) onto some sort of rectangular area that can be addressed in x,y coordinates through the physical device interface. The algorithm redisplays the minimum amount of information necessary; at the physical device level these operations might translate to bit BLTs on a graphics display, block moves on a memory-mapped CRT, or scrolling operations on a terminal. Substantial portions of the editor have been implemented in BRIEF's macro language. We chose the macro language over either C or Assembler for several reasons. First, the macros themselves are virtual, both in size and in their interface to BRIEF. Virtual size means that the virtual memory subsystem reads portions of the macro as they're needed, so the entire macro does not have to be in memory at one time. The virtual interface means that the macros communicate with a "BRIEF" machine, rather than with any particular operating system or hardware, so they can be ported without change from environment to environment. More importantly, BRIEF was designed to let the user completely customize the editor. We could have made the source code for the entire package available. Although this option would allow the most complete reconfiguration of the program──essentially, reconfiguration without limits──it also creates many problems. Configuration requires extensive knowledge of BRIEF's internals, making "simple" configuration all but impossible; technical support is more difficult; updating the package is extremely time-consuming; moving the configured system to other environments is almost impossible; and, our secrets would be available to all of our competitors. Clearly, this was not the way to go. Another option was to provide users with a standard "library" of editing functions which would, in essence, be the BRIEF subsystems. A library could be provided through a .LIB file, a .DLL under OS/2, or even a run- time interface. While a library would have made it easier to update users, the other problems still remained. We decided to take our virtualization philosophy a step further. Rather than forcing the user to deal with the specifics of the system BRIEF was running on, we provided the user with a set of "virtual" interfaces that allow complete configuration without worrying about the environment or specific, low-level implementation details. By writing a large portion of BRIEF in the macro language, we provide an opportunity for users to learn from the coding that we've done, and to change that code to match their own style. BRIEF's modular design makes it easier for us to port BRIEF to other environments. For example, we are currently porting BRIEF to OS/2. The BRIEF port consists mainly of changes allowing it to conform to the virtual memory capabilities of the operating system. Refreshing code, window management, macros, and high-level input remain largely unchanged. The facilities provided by BRIEF's device drivers come primarily from the operating system, and hence that system-specific code has been removed. Much of the OS/2 version of BRIEF is substantially less complicated than the one that runs under DOS. The only problem with the OS/2 port so far has been our dependence on the small programming model and the DOS memory allocator. BRIEF makes extensive use of DOS memory allocation blocks, and we have stayed with the small model (with some far routines) to get the most out of PC and XT computers. Under OS/2, memory allocation has changed and we can no longer assume that allocated blocks returned from the operating system are paragraph-aligned segments (which, under DOS, we could store as a 16-bit pointer with an implied 0 offset). Although a similar scheme could have been implemented under OS/2, the overhead and large number of selectors required made it impractical. We have converted BRIEF to a large model program, and a lot of time has been spent converting various pointer references in C and assembly language. Overall, the port to OS/2 has been greatly assisted by BRIEF's modular design. We anticipate that porting the program to environments such as the Presentation Manager, UNIX(R) (and possibly even the Macintosh(TM)) will be easier because of this design. ─────────────────────────────────────────────────────────────────────────── The ME Text Editor ─────────────────────────────────────────────────────────────────────────── Greg Comeau Editing text is an important part of program development and maintenance. Like most other programmers, a good portion of my time is spent using a text editor. The editor of choice must be powerful, customizable, extensible, reliable, and of course fast. The Magma Editor Version 1.31, or ME as Magma Systems calls it, stands out among the text editors I looked at. It supports editing of multiple files, full and split-screen windows (pop-up or pull-down), 43-line EGA mode, regular expression searches, cut-and-paste block commands, a C-like macro language, compiles from within the editor returning with the cursor on any offending lines of source, and more. Just before deadline, I received the latest version of ME, Version 2.0 (which as of this writing is in beta testing), and it's loaded with new features. ME comes with help files, a macro compiler, sample macros, and a utility called CTAGS in addition to the editor itself. Installation was as simple as inserting the distribution floppy and typing INSTALL. Magma puts comments in the installation script so that you can modify it in case you don't want to install the whole package. Documentation The documentation for ME is not outstanding. Most commercial software comes in a sturdy box containing high-quality documentation, usually in a slip-case binder. ME arrived on a disk packaged in a disk mailer, which is not exactly an overwhelming presentation. After installation, I found out that you have to print your own copy of a file named MANUAL if you want a hard copy of the documentation. The manual is very disorganized and has no index, and the command reference is located in a file called KEYCHART rather than in the manual itself. It was very hard to find sections describing certain features. However, the manual is complete, and all features of ME are described at least once. Examples of all commands and functions are provided; more complicated features had more than one example. Perhaps the best feature of ME's documentation is that it makes it perfectly clear from the start how to exit from the editor, something that some editors don't do. Although I haven't seen it, Magma Systems promises that Version 2.0 will have a new, bound and typeset manual. On-line Help Once ME is installed, you invoke it by typing ME followed by the filename. Then, once the program is running, you can go into an on-line help subsystem that is not very elaborate but does the job. The text for the on-line help is on disk at all times, and you can change it to suit your needs. Changing the text to the help system is as simple as editing or creating a new text file by using ME and letting ME know where the file is located at startup, usually in \ME\HELP. Version 2.0 will have an expanded help subsystem that is more readable and easier to use. The general behavior of ME is consistent and reliable. I found no surprises and felt comfortable with it. The Ctrl-key combinations are set up logically and if you don't like them they can be changed. Major features such as buffering, windowing, and block operations are complete and operate without hesitation. Buffers and Windows Much of ME's functionality is based on buffer and window operations. A buffer is an area of memory that holds data; in an editor, buffers are responsible for holding varying lines of text and are important tools for a programmer's productivity. ME supports two different types of buffers; the first is called a scratch (or pick) buffer, which is useful for moving text on the screen from one area to another or between windows. ME can handle up to ten scratch buffers at a time. So, for instance, you could delete or mark a few lines from one part of the program you are editing and place them somewhere else in the file. Another possibility is to add the same group of lines to many files, say a copyright message or some variable declarations, by loading each file, moving to the appropriate place, and inserting the scratch buffer. The second type of buffer can have files loaded into it and can be seen in a window, a view into a buffer displayed on your screen. Each window is limited to a certain number of lines ranging from 1 to 24, so you can have up to 24 windows active at any one time. (You can have an unlimited number of buffers if you use ME's macro language described below.) Since things can get cluttered, ME allows windows to be exploded so as to fill the whole screen. Other windows displayed at the time of the explosion do not go away; they become temporarily invisible until you shrink the window currently in use. ME typically loads with only one window displaying the source file that was given as its command-line argument. You can add extra windows by specifying more files on the command line or by creating new windows after a session has started with the split-window command. Once multiple windows have been established, you can delete a window, increase or decrease the number of lines, choose between or explode windows, and change the color of windows. Version 2.0 of ME will have pop-up windows for adding messages to a customized user interface and vertical windows for comparing source files. Macros ME supports various flavors of macros, the simplest of which is the keyboard macro. This macro allows you to record all your keystrokes so that you don't have to worry about typing them over and over if you use them repeatedly. Keyboard macros can also be transformed into another type of macro, called a named macro, if you find that you must abort the current edit session or find yourself creating the same keyboard macro over and over. Named macros allow you to create commands that the ME user interface does not provide directly. For instance, ME can only display 24 windows at a time; since there are 10 scratch buffers and one buffer per window, the ME interface permits a maximum of 34 buffers. Using a macro of your own creation you could add as many buffers as you'd like. For instance, mymacro() { int bufid; bufid = create_buffer("tempbuf"); show_buffer(bufid); ∙ ∙ ∙ close_window(); delete_buffer(bufid); } creates a buffer named "tempbuf" and attaches it to a new window. It can then have code to process and perhaps save the data in that window; at some point it must close the window and delete the buffer. An excellent example of a macro is shown in Figure 1. It contains a sample ME macro script to compile Microsoft C Version 4.0 programs and report any errors found. Notice its C-like syntax and its use of variables. The example uses most of ME's macro language constructs including constants, strings, arrays, arithmetic operations, compiler directives (such as #include), flow-control statements (for, while, if-then-else, and switch), function calls, global variables, local variables, parameters, and built- in functions. You could change this example to conform to different languages or different compilers; this would usually require the modification of just five or six lines. The ability to code macros in a C- like language is a strong selling point for ME. Most other editors that come with a macro language usually contain some sort of LISP subset for this, and the result is many times more complicated to write, read, and debug. Because of the popularity of the C language, most programmers will find ME's macro language extremely easy to use. ME also supports keyboard remapping: the program keeps track of key values so that any keystroke can be bound to another key, an ME macro, a user macro, or even another keyboard map. This is useful for generating multiple-key command sequences, either for a new group of macros or for emulating other editors. An interesting new feature in Version 2.0 will be a "typeamatic" mode for keyboard repetition. Version 2.0 of ME includes two notable changes to the macro language. One is the ability to use strings as the targets of a switch/case statement, making the macro language more powerful and efficient. Before Version 2.0, a user could only execute a switch statement that evaluated to a numeric argument, and that ruled out all strings, including those containing the ASCII values for the digits 0 through 9. That meant that any selections performed with strings had to be done either via if statements or by encoding a numeric value in an array that also had a string counterpart and then using the array index to access the string. Now you can switch directly on a string as well as specify strings as the target constants for each respective case statement. This is a facility that most high-level languages cannot handle. The other major change to the macro language is that ME now allows calls to C functions directly from the macro language, providing for a very flexible interface. You can call all routines internal to ME, and now you can write parts of your larger or slower macros in C. Also, if you need to call some of the C run-time libraries, this lets you do so without having to create new macros. Instead, you just add an entry to a table and recompile ME. (This option is available only if you've purchased the source code for ME, which costs $95. Magma includes a README file suggesting the use of MSC 4.0 or higher for any compilations.) Other additions to the language include labels, the goto statement, and hexadecimal constants, all of which make the ME macro language much richer without going overboard. The end result is that macro generation is more readable, structured, and easier to perform when compared with most of the other macro-based editors on the market, which force you to use a terse and error-prone LISP-like language. LISP and its subsets are powerful, but not very productive or familiar to many programmers. Regular Expressions Many concepts from the UNIX operating system and its associated toolkit have made their way into mainstream computing in spite of their terseness and complexity. One such concept is regular expressions, which let you build very complicated pattern-matching commands using metacharacters to facilitate text searches. For instance, let's say you wanted to search for every occurrence of "abc" in the current window and change it to "ABC." That's simple enough with most editors. Now let's add a slight twist to the search: all the occurrences of "abc" must occur at the very beginning of a line. Most editors would fail at such a simple request. Looking through ME's list of metacharacters (see Figure 1 for a sample of the on-line menu for Search and Substitute Commands) you may notice the " " character. By using this special character in the previous request you could now look for the pattern " abc" instead of "abc." Another problem that comes up often is swapping two columns on a line or table. This can be accomplished by using " \(?*\) \(?*\)" as the search pattern and " \2 \1" as the replacement pattern. Such a feat often requires writing a general-purpose conversion utility, but comes naturally to ME. Programmer-specific Features For the programmer, ME has a few unique language features. One is its support for autoindenting while in input mode. Another useful feature is the match-brace command, which finds a closing curly brace or a closing parenthesis if the cursor is positioned at an opening one (and vice versa). Compile/next scenarios are supported with macros. And although Magma doesn't supply any language-specific syntax templates or syntax- based editing even on a simple level, you can certainly write macros to handle this. It is surprising that templates are not provided for the C language; when I mentioned this to the people at Magma, they said they were working on it and will have it in the next release of ME. Magma provides the CTAGS utility with every copy of ME. CTAGS was originally created for BSD UNIX and allows you to create a file containing a list of filenames and line numbers of every function you use in your code. Later, in an editing session, you can automatically create a new window containing the source file of any function in the list with the cursor properly positioned on it merely by responding to the go-to- function command sequence with the function name. Still, CTAGS is limited in that the file it creates does not change as your source code changes. It can be helpful in a large programming project though, especially if you are unfamiliar with the project. ME's command set has some unique features. The 8-bit-mode command is used for entering ASCII characters that are generally classified as unprintable or extended, that is, those with values less than 32 and greater than 127. Most editors allow you to enter only one such character at a time, and ME is no exception. However, ME also lets you enter a sequence of extended commands by switching into 8-bit-mode. This is very handy for generating terminal escape sequences or for emulating a strange file format. Another interesting point about ME is its source code option. Because it is written in C, the curious can use the source code to learn various data structure concepts by looking at a real-life application. Also, you can convert your most heavily used macros into true built-in commands by modifying some of ME's internals. Finally, some programmers may be interested in contacting Magma for an OEM license so they can include ME in their product line and/or applications as well as configuring it to their own needs. Magma is now working on an OS/2 version of ME, which will support all features of Version 2.0 as well as OS/2-specific concepts. One such planned feature is extending ME's autosave ability so that buffers get saved by another task. That way your edit sessions will not be interrupted abruptly. Printing ME does not support printing. If you need to print a copy of your source code or documentation, you must write a macro to handle it since Magma doesn't provide any. This was annoying since it meant getting intimate with ME's macro language, but it does demonstrate how powerful and adaptable ME can be. For instance, I wrote a macro that picked up the current filename and printed out a listing of the program in the buffer much like the UNIX pr command. It included line numbers, page breaks, and a header identifying the file and the page number. One could even expand on such a macro to accept a list of dot commands to perform such tasks as centering, justifying, underlining, and boldfacing. Vendor Support Magma's technical support is excellent. When I called them for support, I was always promptly greeted by a member of the staff, and my questions were answered eagerly and completely. Need I say more? Further, if you happen to be a member of BIX, you can communicate directly with Magma Systems on-line. Overall Impression ME is one of the top programmer-oriented editors on the market. It contains many concepts ideal for maximizing programmer productivity and stands out in four areas. The first area is its command set. High among its list of features are its various macros accessible either on the fly or via its macro language. This allows for custom editing and flexibility when dealing with complicated or repetitious tasks. Also, its use of buffering and windowing offers programmers access to multiple files, a necessity even when working on small projects. The next area of importance is ME's consistency. Its operation is reliable in every aspect. In general, the user interface is well thought out and its behavior is not erratic. Programmers do not need to spend time trying to fight with their tools so that they can do their editing. Also, when programmers do need to reconfigure part of ME's operation, it is a very smooth transformation. As for speed, ME performs very quickly. Most cursor operations occur instantaneously, and ME is usually waiting for you instead of the other way around. Similarly, non-window-oriented commands are also speedy, including searches for regular expressions and macro functions written by the user. Finally, ME contains features that are usually found in more expensive editors costing several hundred dollars, but its price tag is only $39.95. I'm told that the new release, Version 2.0, will retail for $89.95. It is surely the most economical editor allowing for such a powerful environment for programmers and is surely one of the bargains of 1988. Even at double the price, there's no question about its price/performance ratio. ME is fast, complete, reliable, and productive──exactly what a programmer's editor should be. ─────────────────────────────────────────────────────────────────────────── CompuView's VEDIT PLUS ─────────────────────────────────────────────────────────────────────────── Randall Swan VEDIT PLUS, from CompuView, is an excellent programming editor with many powerful options and a high degree of customization. The program is very quick to load, respond, and execute all commands. The commands are organized in a logical manner, and the on-line help and tutorials in the documentation considerably speed up the learning process. VEDIT enables you to edit multiple files simultaneously in separate screens or windows and to store text or macros in multiple text registers, which saves a great deal of time. VEDIT offers you a choice of command mode, in which you type specific commands, or visual mode, in which you use predefined function and Ctrl- key combinations to specify edit functions, thus providing much flexibility. The editor can be extensively tuned to an individual programmer's environment by using the command mode to write command sequences and storing them as keyboard macros with specially designated function keys and Ctrl-key combinations. Documentation VEDIT PLUS has fairly substantial documentation──378 pages in a 6-by- 81/2-inch three-ring binder. It includes a tutorial, a user guide, a programming guide, a section on installation, and a reference section. The tutorial covers topics ranging from simple editing operations to more complicated ones. There is also a 30-Minute Tutorial in the introduction to get started quickly. However, specific keystrokes are conspicuously absent from the documentation; they are described only in the on-line help screens. Another problem with the documentation is that it was written for all installations of VEDIT PLUS, including the CRT terminal version and various memory-mapped machine-specific versions. Setup To set up VEDIT PLUS, the manual tells you to simply create a VEDIT subdirectory and copy the one disk into it. Several things in the installation instructions, however, are not fully explained. An INSTALL.EXE program allows for extensive customization of VEDIT. From a menu in INSTALL called Main Menu Tasks, you can modify the keyboard layout, add keystroke macros, and change print, edit, file- handling, and screen display parameters. You can also change edit switch settings, visual settings, and the command mode interface. It is possible to create numerous configurations by running INSTALL VPLUS.COM "outfile".COM, each identified by a different sign-on message. On-line Help On-line help is easily accessible by pressing Alt-F1, though this keystroke is explained only in the introductory tutorial. The first screen displays all the edit functions with corresponding keystrokes, the next screen displays the separate default and expert keyboard layouts, and the third displays status line messages and editing tasks. You can access on-line help by entering command mode and typing EH to see a list of topics or H to display five screens of information: basic and text register commands; extended, jump, operating system, and print commands; miscellaneous and numeric register commands; keyboard control, command modifiers, help aids, generic/indirect, numeric specifiers, and miscellany; and numeric, relational, and logical operators and operator precedence. You can skip the menu by typing H followed by a command to display a description of that particular command, or EH followed by a task name to display a description of commands needed for that particular task. You can select an individual command or function from any of the help screens and obtain a detailed description by typing its name. After selecting any individual item, however, pressing any other keystroke returns to the text rather than back to the help screen, which makes it difficult to look up more than one item at a time. There is also no way to exit back to the editor without either selecting a particular item or paging through all the help screens by hitting Return. You can edit and expand all three help files. One small source of confusion is that the two preconfigured keyboard layouts, default and expert, use different keys to define the same functions. The expert layout offers more predefined functions. The explanations in the help functions depend on which layout has been installed. There are no pop-up or pull-down help menus, nor is the help context-sensitive. You cannot access the help screens in the middle of a command. Basic Editor Functions Cursor movement is performed with the standard up, down, right-, and left-arrow keys. The Goto function allows for cursor movement to the beginning or end of the file, to a specific line number, or to previously set markers in the text. In general, though, it is not possible to move the cursor to an empty part of the screen. You have a choice of three modes for cursor movement: mode 0 causes the cursor to move left from the end of a long line to the end of a shorter line; mode 1 (default) allows the cursor to move straight up and down regardless of line length but moves the cursor left if new text is added when the cursor is beyond the end of the line; and mode 2 allows straight up and down movement but adds new text at the cursor position, inserting spaces between the end of the line and the new text. VEDIT doesn't allow you to define columns, but in other respects the block functions are very flexible. There are 36 text registers, labeled 0 to 9 and A to Z, into which blocks of text can be moved or copied, either overwriting or appending it (using a plus sign) to any text already in the register. However, this operation is cumbersome since the command must be invoked three times, with a different sequence of keystrokes each time. The defined block is not highlighted, so the only way to see the beginning of the block is to repeat the block function (F9) followed by S for the Swap option. Inserting a block displaces any text on that line to the right. There are no functions to move or copy text without defining a block, but the RC command copies a specific line or range of characters in the edit buffer to a specified register. The search-and-replace functions offer a number of options. For example, both the Find (F2) and Replace (Alt-F2) functions allow you to use a previously defined search-and-replace string and start the search at the beginning of the edit buffer. The default setting for search-and-replace is not case-sensitive but can be set to distinguish between upper- and lowercase. Pattern-matching codes, such as |A for any letter, can also be used in search-and-replace functions. You can set wordwrap in Task 5 (Change Edit Parameters) of the INSTALL program, using the EP command, or selecting the Word Wrap option from the User function to specify a right margin, or 0 for no wordwrap. The default value for wordwrap is 0 in the preconfigured editor version, which is a little disconcerting when one is first using VEDIT. Paragraphs are formatted by using Indent or Undent to specify the left margin, setting wordwrap and justification, and then selecting Format Paragraph. The indentation (tab or offset) of the first line of the paragraph is preserved. Automatic conversion of upper-to-lowercase and vice versa is done by selecting the Uc/lc option of the Misc function to convert the character at the cursor. CompuView provides a V-SPELL spelling checker for any files created with VEDIT. User Interface All 10 of the function keys, most of the Alt-function-key combinations, and some Ctrl-key combinations are defined in the two preconfigured keyboard layouts, default and expert. The normal displayable characters and the Enter key (Ctrl-M) cannot be redefined, but all of the function keys, sequences of Ctrl- or Alt-letter combinations, or any escape sequence can be redefined by running INSTALL and selecting Modify Keyboard Layout. The default assignments can all be cleared or retained. Each edit function name, followed by the current assignment, is prompted in sequence and can be retained as is by pressing Enter or reassigned to a different function key, Ctrl-key combination, or escape sequence. The normal visual edit mode displays a status line at the top of the screen that shows the current line number, column number, and filename. The status line is replaced by the options of any edit function that is invoked or an error message. Switching into command mode puts "Command" in the status line and a "Command:" prompt at the bottom of the screen. You can set the number of lines for paging and lines displayed (three by default) above and below the cursor as the screen scrolls, the column for the horizontal scroll margin, and the continuation character (the hyphen by default) used for automatic wordwrap when the horizontal scroll margin is reached to different values by running INSTALL. Reverse video on monochrome systems and the colors on CGA or EGA systems can be set to new defaults by running Task 10 in INSTALL (Change Screen Dependent Parameters) or by using the YEA command to change both the foreground and background colors of the current window. VEDIT supports any screen size up to 70-lines-by-250-columns. You can set the size by running INSTALL and changing the number of screen lines and length of displayed line in Task 10 and the screen line length in Task 11 (Additional Memory Mapped Installation Features) as well as setting Using High Speed EGA Color Board to Yes. Printing VEDIT offers such standard page layout capabilities as margins, paragraph formatting, and justification. CompuView also markets a more sophisticated page formatter program called V-PRINT (not included with the editor) that can create headings, tables of contents, and indexes, do complete paragraph formatting, and interpret page formatter commands in VEDIT text files. You can print text by issuing a PR command, which puts the filename and page number at the top of each page, or by loading the PRINT.VDM macro, which performs simple print formatting. The only way to print line numbers, headers, and footers is by modifying the PRINT.VDM macro. Line numbering can be automatically added to the file by creating a simple command macro to set the starting line number, then creating a loop to insert the line number at the beginning of the line, add a specified increment, and find the next line. VEDIT PLUS does not support special features, such as different fonts. Unique Language Features Language-specific editing features are found under the Misc menu. Positioning the cursor on the braces, brackets, angle brackets, or parentheses and selecting the Match Parentheses option moves the cursor forward or backward to the matching pair, showing any syntax errors. The automatic indenting mode can be set with the ES command to enable Edit Switch 3 or by choosing the User function and selecting Auto-Indent. Each new line will automatically indent the same amount as the previous line. Macro Capabilities VEDIT macros can be stored as command macros in a text register and executed with the Macro function or the RL command and the M command followed by the register number to load and execute the macro in the specified register. You can build keystroke macros by running Task 3 of the INSTALL program or by using the Define function (Ctrl-D) and executing the macro by pressing the defined function or Ctrl-key combination. Text register macros are temporary unless you save the macro or add the sequence of commands, including a command to copy to a specific register to the VEDIT.INI file. A keystroke macro can be any combination of a sequence of command mode commands, visual mode edit functions, or keystrokes. Text register macros, however, can only be sequences of one or more commands. The command mode used with macros is, in effect, a special macro language, since a macro can combine any series of commands into a single function, like a small program. Several macros are provided with the editor and defined in the preconfigured default keyboard layout, and there are even more with the expert layout. The documentation also has several examples of command macros. You can define any number of additional or replacement macros and assign them to any function key or Ctrl- or Alt-letter combination. With a little effort, any user-defined macro can be added to the definitions on the help screens. You can also give a command macro a meaningful name and save it as a file with the Register Save command, but this is not possible with a macro that includes functions and recorded keystrokes. File Handling The maximum file size for editing in VEDIT is half the available disk space, since both an input and an output file are required. If the input and output files are on different drives, the maximum size is the whole disk. If backward disk buffering is used, the maximum size is one third of the disk size, since temporary equivalents to input and output files must be created. Automatic forward and backward disk buffering handles large files by reading a section of the file, called a file page, into the edit buffer, then writing it to an output file when it reaches the end of the edit buffer and reading in another file page. The size of the file page is 8Kb by default. Backward disk buffering reads a file page from the output file back into the edit buffer, writing text from the end of the edit buffer out to a temporary disk file; forward buffering subsequently reads first from the temporary file, then from the input file. You can merge files by using the EG command to specify an entire file or a line range to extract from one file and insert into the file being edited. The EL command displays the file or specific portion with line numbers. A section of text can be written from one file into one of the text registers and placed in a second file if there is not enough memory to reserve a 64Kb segment for EA-EZ. To create windows you split the current window in half horizontally or vertically. The number of possible windows is limited by the screen size, since the minimum window size is one line and 15 columns, plus a border, and there are no overlapping windows. Seventy windows are possible on a standard 80-column-by-24-line screen. Windows are independently configurable as to size, number of lines or columns, and foreground/background colors, and each is given a one- character name shown in the top border line and highlighted when the window is active. The Window function allows you to create a window, specifying top, bottom, left or right position, number of lines or columns, and the name of the window. You can also delete a window, switch between windows, and zoom the current window up to full screen size, which is a very nice feature. One helpful feature is the option of using the command mode as an on-line calculator for integer calculations or finding the numeric value of any ASCII character. The only drawback is that it cannot handle decimal or fractional values. VEDIT is seriously limited in that it does not provide for invoking a compiler from within the editor or returning to the text with the cursor positioned on the particular text line when a compiler error occurs. Conclusion VEDIT is a very good editor but I do have some complaints about it, mostly concerning the lack of a direct link to a compiler to run from the editor or return directly to error lines and certain organizational features that should be improved. There should only be one preconfigured keyboard layout; providing two different layouts is unnecessary and confusing. The functions really ought to be listed alphabetically for easy reference in both the help screens and the documentation, and specific function and Ctrl-key combinations should be described in the documentation as default values that can be changed. The command mode, though very powerful, is unnecessarily cryptic because the command names are often letter combinations that are arbitrary rather than mnemonic. The functions that provide one-line option menus when selected should be identified as such. Several of these functions, such as Misc or User, have names that give no indication of the options that lie beneath them. Still, VEDIT's many options- speed, ability to switch between command and visual modes, and the ease with which you can customize it to meet your own needs-make it a powerful, flexible, and efficient editor. It is highly recommended. Figure 1: This sample macro script will compile Microsoft C Version 4.0 programs and report any errors found. /*MSCCOMP - this file contains macros for automating the compile and * error-search procedures under Microsoft 4.0. To compile the * current buffer, press Alt-C. To search for errors, press Ctrl-N. * * Written by Marc Adler of MAGMA SYSTEMS. January 1987 */ #define ALT_C 174 #define CTRL_N 14 #define NO 0 #define YES 1 int reached_eof; init() { assign_key("compile", ALT_C); assign_key("next_error", CTRL_N); } compile() { string currfile, extension; string errmsg; int i; errmsg = "Error - the file is not a C file. <ENTER to continue>"; currfile = filename(); /* Make sure that the current buffer is a C file */ if ((i = index(currfile, ".")) > 0) /* get the extension */ { extension = substr(currfile, i+1, 3); if (extension != "c" && extension != "C") /* is it a C file? */ { get_tty_str(errmsg); /* NO!!! Error!!! */ return; } } else /* the buffer has no extension, so it's an error */ { get_tty_str(errmsg); return; } writefile(currfile); /* save the current version & compile it */ set_buffer_modified(currbuf(), 0); os_command(sprintf("msc /AL /Zi /J %s; > errs", currfile)); next_error(); /* see if there are errors */ } next_error() { int id, old_id; int n; string cl, foo; old_id = currbuf(); /* save the id of the source buffer */ if ((id = find_buffer("errs")) == 0) { id = create_buffer("errs"); reached_eof = NO; } id = setcurrbuf(id); /* Microsoft C 4.0 puts out error messages in this format: */ /* filename(line #) : messagetype error# message */ if (reached_eof == NO && fsearch("([0-9][0-9]*) : ?*error") > 0) { n = atoi(substr(cl = currline(), currcol()+1, 5)); /* get line # */ gobol(); /* move to the next error */ if (!down()) reached_eof = YES; show_buffer(old_id); /* go to the source buffer and */ goline(n); /* move to the offending line */ message(cl); /* display the error message */ get_tty_char(); /* and let the user acknowledge */ } else /* Reached the end of the error file */ { foo = get_tty_str("NO MORE ERRORS"); delete_buffer(id); /* get rid of the error buffer */ show_buffer(old_id); /* and set the current buffer */ } } ─────────────────────────────────────────────────────────────────────────── Inside ME ─────────────────────────────────────────────────────────────────────────── Marc Adler ME was born out of frustration with current editing environments. Programmers always have a wish list of features that they would like in their editing environment, but which their editor does not support. Magma's goal was to develop an editor that offered both flexibility and extensibility. Users would then be able to create an environment tailor- made to meet their specific programming needs. The ability to reconfigure was at the top of our list when working on the design for a new editor. This meant the editor needed a macro language that would allow one to write new commands. Unfortunately, most of the editors that provide macro capability have LISP or TECO-like macro languages which are extremely cumbersome to use. The growing popularity of the C language led us to believe that our greatest audience would be C programmers, and thus we focused our attention on their needs. This eliminated the need to put thousands of parentheses in ME's macro code, as would be required by the LISP syntax. However, a macro language should be at a "higher level" than a traditional third generation programming language such as C. The programmer should not have to worry about pointers, addresses, and memory allocation when creating a macro. Therefore, we decided that our macro language would differ from C with respect to the ease of programmability. We also wanted the macros to run as quickly as possible. Therefore a macro would need to be compiled into a low-level intermediate code before being executed by the editor's interpreter. The intermediate code would contain opcodes for assignment, control flow, and procedure invocation, and would be low-level enough that an intermediate code file could be turned into an assembly language routine and linked in with the editor. Dynamic typing of variables is one feature that saves the macro programmer a bit of time. Variable declarations are usually at the programmer's discretion, but in our design the macro interpreter itself figures out the correct typing at run-time. String manipulation should be performed as easily as possible. ME's macro language relieves C programmers from constant worry about memory allocation when dealing with strings by performing all memory allocation internally. Linking in C Functions To expand their environments, users should have access to windowing, database, and possibly communications libraries. The ability to integrate these functions at the macro language level is very desirable. Unfortunately, if all of this functionality were put directly into ME's kernel, the resulting size would make the editor very unwieldy. The ability to call external C functions from within the macro language was added in version 2.0 of ME in order to make the editor truly reconfigurable. A table was provided to let ME users add C functions to the editor's database of callable routines. To add an external function to the macro language, two items are inserted into this table-the name of the function and a reference to the function. For instance, if a programmer wants to add a database routine called db_create(), then an entry such as the following is added to the table: { "create_database", db_create } After adding this entry, the table needs to be recompiled and linked into the editor. Whenever a programmer wants to access this function in the macro language, the line create_database("my_db"); is coded into the macro program. The macro interpreter, upon seeing this function, looks at the external function table and calls the function. OS2 and Porting ME is written almost completely in the C ANSI standard. This allows the source to be compiled by any compiler supporting the ANSI standard. ME will soon support OS/2 protected mode. OS/2's Family API was designed so that most DOS applications could easily be ported to protected mode. Since ME uses the standard C memory allocation calls, one immediate benefit will be the increased file size that the user will be able to edit. At the macro language level, support will be provided for the user to access dynamic-link libraries. Future Directions Since ME is available with full source code, Magma Software Systems is anxious to have its users determine our future direction. Various applications have been written in ME's macro language, including a DBASE programming system, and a Computer-Aided Software Engineering (CASE) system. The ability to access external C functions will allow our users to be more creative in their applications. We will work with our users to insure that ME will be able to fully support their needs. ─────────────────────────────────────────────────────────────────────────── DEVELOPERS ─────────────────────────────────────────────────────────────────────────── BRIEF Solution Systems 541 Main St., Suite 410B South Weymouth, MA 02190 (617) 337-6963 or (800) 821-2492 Epsilon Editor Lugaru Software 5740 Darlington Rd. Pittsburgh, PA 15217 (412) 421-5911 KEDIT Mansfield Software Group, Inc. P.O. Box 432 Storrs, CT 06268 (203) 429-8402 ME Magma Software Systems 138-23 Hoover Ave. Jamaica, NY 11435 (201) 792-3954 MICRO/SPF Phaser Systems, Inc. 115 Sansome St. San Francisco, CA 94104 (415) 434-3990 The Norton Editor Peter Norton Computing, Inc. 2210 Wilshire Blvd., #186 Santa Monica, CA 90403 (213) 453-2361 PC/VI Custom Software Systems (CSS) P.O. Box 678 Natick, MA 01760 (617) 653-2555 PMATE Phoenix Technologies, Ltd. 320 Norwood Park South Norwood, MA 02062 (617) 769-8310 Uni Press Emacs Uni Press Software 2025 Lincoln Highway Edison, NJ 08817 (800) 222-0550 VEDIT PLUS CompuView Products, Inc. 1955 Pauline Blvd. Ann Arbor, MI 48103 (313) 996-1299 WordPerfect Program Editor (part of the WordPerfect Library) WordPerfect Corporation 288 West Center St. Orem, Utah 84057 (801) 226-7877 XTC Wendin Box 3888 Spokane, WA 99220-3888 (509) 624-8088 ════════════════════════════════════════════════════════════════════════════

Vol. 3 No. 3 Table of Contents

SQLWindows Brings a Graphical User Interface to SQL Database Applications Gupta Technologies, Inc., will soon release SQLWindows, an innovative developement system that makes it easier to create database applications by combining the graphic user interface of Windows with the power of programming in SQL. We talked with the designers of this new product. The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces The Graphics Programming Interface (GPI) of the OS/2 Presentation Manager (PM) introduces many new concepts for programmers. Charles Petzold examines the three types of presentation space, data structures internal to the PM that act as a passport to using its graphics functions. Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution OS/2 semaphores are an interprocess communication facility for coordinating concurrent threads of execution. They can serialize access to code and allow a thread to signal that an event has occured. This article looks at the four types of semaphores; exclusive system, nonexclusive system, RAM, and FSRam. Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library Dynamic-link libraries are among the most valuable facilities of OS/2. They permit multiple applications to share only one copy of a function. This article provides programmers with a road map that demonstrates that writing a dynamic-link library is not as complicated as it may seem at first glance. New Compiler Technology Boosts Microsoft(R) QuickBASIC 4.0 Productivity Using proprietary threaded pseudocode technology, the latest version of Microsoft QuickBASIC offers the best features of both a compiler and an interpreter, eliminating the need to choose between the two. It provides an instant, integrated programming environment. Debug Microsoft(R) Windows Programs More Effectively with a Simple Utility Unfortunately, many traditional debugging techniques don't work in the Windows environment. The DEBUG utility provides print statementlike debugging within Windows. The utility will also record and display internal program events and lets you manage the process with a central control panel. An Examination of the Operating Principles of the Microsoft Object Linker The Microsoft Object Linker (LINK) is a powerful development tool that supports segment ordering, run-time memory management, and dynamic overlays. We discuss the details of how LINK works to resolve memory references for more efficient and simpler programs. EDITOR'S NOTE This issue's lead article takes and advance look at Gupta's SQLWindows product, a fine example of systems engineering using Microsoft Windows(R) to provide a graphical user interface for the building of end-user-programs. It will be one of the first commercial systems to exploit the client/server application architecture that is fundamental to distributed processing. In a short period of time, the SQL language has become an important computer standard and implementations now exist on a wide variety of mainframe and minicomputer systems. SQL is also available on personal computer systems in a variety of incarnations. The Database Manager included in the IBM(R) Extended Edition of OS/2 may provide the most important implementation of SQL, since it is part of Systems Application Architecture (SAA). Products competing with the IBM offering, including the Ashton-Tate/Microsoft SQL Server, the Oracle Database Add-In for 1-2-3(R), and the Gupta SQL products discussed here, use the same SQL statements and process them in the same way. This allows software developers to build compatible applications that can access data on a variety of servers. We will present a variety of articles on SQL in coming issues that will concentrate on how SQL can serve as a bridge between the application you develop and the variety of SQL databases on local and remote systems. This issue also takes a look at designing OS/2 dynamic-link libraries, shareable libraries that run efficiently in a multitasking environment. A guide to three OS/2 presentation spaces provides some helpful tips on choosing the right one for your application. Four types of OS/2 semaphores used for coordinating concurrent threads of execution are explored. We examine the new compiler technology in Microsoft QuichBASIC Version 4.0 and debug windows applications with a utility that lets you manage program debugging more effectively. Look for the DEBUG utility code and all our source code listings on DIAL, CompuServe(R), and two public access bullitin 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 KAREN STRAUSS Assistant 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) 1988 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, MS XENIX, and CodeView are registered trademarks of Microsoft Corporation. QuickC is a Trademark of Microsoft Corporation. CompuServe is a registered trademark of CompuServe Incorporated. dBase is a registered trademark of Ashton-Tate Corporation. Hayes is a registered trademark of Hayes Microcomputer Products, Inc. IBM and PC/AT are registered trademarks of International Business Machines Corporation. Macintosh is a registered trademark of Apple Computer, Inc. 1-2-3 is a registered trademark of Lotus Development Corporation. PostScript is a registered trademard of Adobe Systems, Inc. UNIX is a registered trademark of American Telephone and Telegraph Company. WordStar is a registered trademark of MicroPro International Corporation. ████████████████████████████████████████████████████████████████████████████ SQLWindows Brings a Graphical User Interface to SQL Database Applications ─────────────────────────────────────────────────────────────────────────── Also see the following related articles: SQL Server Inside Microsoft Windows ─────────────────────────────────────────────────────────────────────────── Craig Stinson☼ Gupta Technologies, Inc., the Menlo Park, Calif., vendor of distributed database software, is preparing to release a one-of-a-kind Microsoft(R) Windows application called SQLWindows. The product is an application development system specialized for database engines based on structured query language (SQL). It is designed to let programmers create data entry and manipulation tools that combine the user interface sophistication of Windows on the front end with the power and universality of SQL on the back end. Scheduled for shipment in July, SQLWindows is the third link in a triad of data management products that Gupta Technologies calls The SQL System. The other two components are SQLBase, a database server tailored for the 80286/80386 environment, and SQLNet, which is a transparent gateway to IBM's DB2 (future versions will offer connectivity to other mainframe DBMS engines, as well). Gupta Technologies was founded in 1984 by Umang P. Gupta and D. Bruce Scott, two former key players at Oracle Corp. Gupta was the vice president and general manager of the microcomputer products division at Oracle, and Scott is coauthor of the Oracle database management system. The company's first product, SQLBase, made news when it was introduced in 1986 because it was the first true database server, as opposed to a multiuser DBMS running on a file server, designed specifically to run on Microsoft-Networks-based microcomputers. An expanded version of this product recently received attention when Lotus Development Corp. announced that it would be the engine for the forthcoming Lotus/DBMS product. Lotus will, of course, supply its own application development facilities for Lotus/DBMS. However, because the Lotus engine will be essentially the same product as SQLBase and will incorporate the same API, applications written in any language to run under SQLBase will run unchanged under Lotus/DBMS. This includes applications that are written in Gupta's SQLWindows. "SQLWindows works with SQLBase today and with the future Lotus/DBMS Server," says Umang Gupta. "And in the future we expect to provide similar support for other database servers. A design objective from the very beginning has been to make the product work with other SQL engines." Gupta stresses that the forthcoming SQL Server from Microsoft and Ashton- Tate is among the products that are targeted for support. "SQLWindows is unique," continues Gupta. "There's no other player we know of that has a product in the market, or close to market, that is Presentation Manager- or Windows-based, engine-independent, and tied both to the Windows environment and SQL." A Presentation Manager version of SQLWindows is also in the works; Gupta believes it will be ready by the fourth quarter of 1988. After that, the company plans to turn its attention to the Macintosh(R). "Our long-term plans," says Gupta, "assume that the platforms will be PCs, PS/2(R)s running Presentation Manager or Windows, Macintoshes, or UNIX(R) workstations running some standardized UNIX windowing system." Covering all the front-end platforms, providing compatibility with the growing number of SQL engines in the marketplace, and at the same time expanding SQLNet to provide connectivity with other mainframe DBMS engines besides DB2, is, to say the least, an ambitious program. Gupta Technologies currently has about 25 employees, including 12 full-time programmers. "We plan to increase our development and marketing staff significantly in the very near future," says Gupta. Interacting Windows Gupta and Michael Geary, chief designer of SQLWindows, gave MSJ a look at a beta version of the company's newest product. SQLWindows consists of three main components: a visual forms-layout editor (similar to the dialog editor that comes with the Microsoft Windows Toolkit), an outline editor, and a programming language called SQLWindows Applications Language (SAL). The outline editor and layout editor function as interacting windows: changes made in either window immediately update the other window. Figure 1☼ shows a sample SQLWindows application in a very early stage of development. The programmer has created a sample "About" box. The box is displayed in the layout editor window (the lower window in Figure 1☼), while a portion of the corresponding source code is shown as selected in the outline window. The solid diamonds in the outline mark headings that have subordinate entries; the open diamonds indicate entries that can't be expanded further. In Figure 1☼, the "Contents" entry, which is subordinate to "Dialog Box: AboutBox", has been expanded to reveal two lines of text and a single push button. Figure 2☼ zooms in on a different portion of the source code for the same "About" box. Here the focus is on the push button, and you can see the button's title, its location and size attributes, its keyboard "accelerator" (the keystroke that will be considered equivalent to a mouse click on the button), and the "message actions" associated with this push button. In the context of this application there's only one push button event of interest (the user clicks on it or performs the equivalent action on the keyboard), therefore this object only needs a single message action. The message action entry says, in effect, "When the user clicks, terminate this dialog and return to the parent window." SAM_Click means the button has been clicked, and SalEndDialog and SalParentWindow are functions supplied in the SQLWindows Application Language. Progressive Disclosure Gupta emphasizes that SQLWindows is designed to serve the needs of programmers, and the outline approach to source code display is very much in keeping with that objective. "A lot of 4GLs take a sort of dialog box approach to developing code," he says. "You add a field somewhere, and a dialog box pops up and asks you all sorts of questions." The drawback to that style of working, asserts Gupta, is that you can't see your code as it develops because it's all encapsulated in the dialog boxes. "That might suit an end user because an end user doesn't necessarily want to see a lot of code. But a programmer──at least sometimes──needs to be able to see all of it. You have to give him a means of doing that, a sort of progressive disclosure. An outline is the appropriate means." Designer Geary says the idea of incorporating an outline editor into SQLWindows occurred to him as he was studying documentation for some other application development systems that were not based on an outline. "I noticed in a couple of cases that they drew an outline in their documentation as a way of explaining what they were doing. And I thought, if they draw an outline to explain what they're doing, let's use an outline to do what we're doing." The programmer using SQLWindows can move freely between the outline editor and the layout window, working in whichever window is more convenient at any given moment. The windows are simply two representations of a common underlying data structure; changes made in either window alter the underlying structure and are immediately reflected in the other window. Figures 3 and 4☼, illustrate these two ways of working. In Figure 3 the programmer is inserting a new data field into the layout window, using one of the tools in the SQLWindows Draw menu. In Figure 4, the programmer has made a change directly to the outline by pressing the Insert key and typing. Note that the programmer's action in Figure 4☼ is inappropriate. A form window cannot be added to a menu, so SQLWindows objects. The programmer has three choices: to return to the outline and edit the entry (Esc), to throw out the bogus entry (No), and to accept the entry (Yes). If Yes is chosen, SQLWindows leaves the entry in place but converts it into a comment, thus allowing the programmer to attend to the problem later. Access to SQL All access to SQL takes place via 15 functions supplied in SQLWindows. The list includes SQLImmediate, which immediately executes an SQL statement; SQLPrepare, which readies an SQL statement for later execution; SQLConnect, which connects the application to a specified database; SQLDisconnect; SQLFetch, which retrieves a given row of data; SQLFetchNext; and SQLFetchPrevious. Figure 5☼ demonstrates the use of the SQLImmediate function. The application in Figure 5☼ is a database used by Geary to track bugs in the prerelease versions of SQLWindows. Figure 6☼ shows a completed form used as part of this database. The selected section of code is the action taken by the New command from the application's Bug menu. A validation test is performed first (ValidateInsert). If that fails, an SAL message box function is called. Otherwise, two calls to SQLImmediate are made──the first to determine the next sequential bug number (select max(bugno) + 1), and the second to make the appropriate insertion. Gupta stresses that the SQLWindows programmer has explicit control of all SQL statements generated by the system. "In some other 4GLs," he maintains, "SQL statements are implicitly generated, and the programmer or user is not actually executing SQL directly. To the extent that that's the case, the programmer has less control, or no control. In SQLWindows, you can execute SQL statements to your heart's content, any time you want, in any order you want." Internal and External Functions The call to ValidateInsert in Figure 5☼ is an example of what SQLWindows terms "internal functions," functions that are not part of the system's built-in repertoire but can be developed by the programmer using SAL functions and SQL functions to suit his or her own needs. The system also provides for external functions, which can be either dynamic-link libraries or static libraries written in C. Figure 7☼ shows the declaration of functions stored in two external libraries, SQLWIN.EXE and USER.EXE, which is part of the Microsoft Windows Toolkit. Figure 8☼ illustrates the code for an internal function, DoQuery, that calls SQL functions to prepare, execute, and fetch one row of data. Why Windows? Why did Gupta Technologies choose to create an SQL front end in Microsoft Windows? Ask the designer and the CEO, and you get two variations of the same answer. Says Geary, "Windows has the user interface that I like. It's really oriented toward building applications that are both easy to learn and easy to use once you've learned them. It also gives us the tools that we can pass on to our SQLWindows developers to let them build applications that have a good, crisp feel to them." Says Gupta, "A major premise of our business is that users want unified access to personal, departmental, and corporate database systems and that SQL is the lingua franca that makes this access possible. A second major premise is that access to database servers will take place from advanced applications written to take advantage of the next generation of PCs. And that means from OS/2 Presentation Manager and Microsoft Windows-and not from dumb terminals." Clearly, the technology of "industrial-strength database management," as Gupta calls it, is changing dramatically these days. SQL is emerging as a standard medium for database access. And the arrival of 80386-based PCs and high-speed local area networks has enabled database servers running on PCs to meet departmental needs as cost effectively as minicomputer- based systems. Gupta Technologies has demonstrated a strong sense of direction with these developments. It made an early commitment to SQL and developed the first PC-based database server product. In Gupta's words, the company "made a few good calls." On the front end, advances in interface technology have signaled the slow beginning of the end for traditional, minicomputer-style, text-based 4GLs. Within the next year or two, we can expect to see a myriad of new approaches to application development from software houses whose roots are planted firmly in the microcomputer world. The OS/2 systems and Presentation Manager will play a dominant role in this development. By utilizing the Microsoft Windows technology of today, Gupta and SQLWindows are in the position to be trendsetters in the Presentation Manager and OS/2 environment of the not-so-far-off future. ─────────────────────────────────────────────────────────────────────────── SQL Server ─────────────────────────────────────────────────────────────────────────── SQLWindows is the front-end interface to Gupta's SQLBase. When the Ashton- Tate/Microsoft SQLServer, a powerful relational database server for PC local area work groups, becomes available, Gupta plans to modify SQLWindows to serve as a front-end application for it. SQL Server is based on technology licensed from Sybase, and runs on top of OS/2-based networks and communicates with workstations running under OS/2 or DOS 3.2 or higher. SQL Server is based on a superset of the ANSI standard Structured Query Language (SQL) that applications use to communicate with relational databases. SQL is transparent to the user and complements languages such as dBASE(R), through which users gain access to data. SQL Server uses a client/server approach that cleanly splits the functions of a database management system into a back-end (server) component where data is managed and a front-end (client) component that allows data to be accessed by users. SQL is used as the access language between the client and server. The client workstation communicates with SQL Server over PC-based networks using TRANSACT-SQL, a superset of ANSI standard SQL with a set of extensions that include flow control, temporary variables, transaction management, precompiled SQL queries (stored procedures), and special stored procedures called triggers. SQL Server creates the concept of an intelligent database server, centralizing database intelligence by using stored procedures and triggers. Stored procedures minimize message and network traffic from the workstation to the server and can result in performance that is many times greater than standard SQL queries. Triggers are stored procedures which are automatically executed or "fired" when database values are inserted, updated, or deleted. Using stored procedures and triggers, SQL Server provides a consistent method of maintaining database integrity across applications. SQL Server has an open platform architecture thus allowing multiple workstation applications to access back-end database services. This gives users the ability to utilize a variety of PC applications and languages against the same data at the same time. A client application programming interface (API), DB-Library, may be used by the application developer to integrate a program with SQL Server. SQL Server runs as a single multithreaded process under OS/2 with the capability of managing many users and multiple databases simultaneously. This results in high transaction throughput and efficient use of computer resources. A 386-based SQL Server can be expected to support more than 35 users accessing a 300Mb database with response time under a second. With its transaction-oriented DBMS kernel, SQL Server makes the database constantly available for such administrative tasks as backup, recovery, database updates, design changes, and integrity rule changes even while users continue to access the database. SQL Server also maximizes transaction throughput so that performance, as seen by the individual user, stays virtually constant as users are added to the network. The system can be administered from anywhere on the network, so that there is no need for a separate administrator's console. ─────────────────────────────────────────────────────────────────────────── Inside Microsoft Windows ─────────────────────────────────────────────────────────────────────────── Michael Geary, designer of SQLWindows, likes a lot of things about programming in Microsoft Windows. "To begin with," he says, "there's the user interface. One of the nice things about Windows is that the Windows style guide spells out an excellent user interface in great detail. That makes life easier for the programmer, and it also makes things much easier for the user. Once someone has learned one application, it's a lot easier to go learn another one." Geary said he appreciates the fact that Windows provides the tools to build its interface. "When I want to write a menu, I don't have to write code to say how menus work. All the basic user interface tidbits are built in," he said. Messages and Child Windows Geary, who had programmed extensively in both the Macintosh and the MS- DOS(R) environments before taking on SQLWindows (among his credits is the respected communications package Transend/PC), was particularly impressed by two aspects of Windows programming: messages and child windows. "I'm often asked to compare the Macintosh and Windows," he says. "From the user's point of view and the programmer's point of view, they're very similar. But I was really struck with the way Windows took the Mac's notion of events and generalized that into the message concept. Messages are like a superset of events. What you can do in Windows that you can't do on the Mac is make up your own messages and send them from one application to another. That's what makes DDE [Dynamic Data Exchange] possible; DDE is just a protocol for sending messages." As for child windows, there's basically only one kind of window on the Macintosh, Geary explains. Each window is essentially a peer of all the others, and once a program has built a window it's up to the programmer to manage everything that goes on inside it. The Macintosh offers such tools as push buttons, radio buttons, scroll bars, and so on, but the program has to manage the details of where they're placed and how they're used. In Windows, the common objects by means of which the user interacts with the application, such as the various components of a dialog box, are typically implemented as child windows. And because child windows behave essentially the same way parent windows do, a lot of things happen automatically that would have required explicit programming on the Macintosh. Subclassing "Another thing I like about the child window concept," adds Geary, "is that Windows supplies you with a number of predefined classes of child windows and then makes it easy for you to subclass them. You just take the predefined class and add on some behavior of your own by interposing your own window function in front of the one that comes with it, in effect filtering out the messages that you want to handle differently." Geary used this technique extensively in programming SQLWindows. Objects in the forms-layout editor window have to behave one way when the user of SQLWindows is developing an application and another way when the end user of that application is working with it. For example, the developer needs to be able to click on such objects as push buttons in order to size and position them; the end user wants them to behave like normal push buttons. "I was able to do all that split behavior just by using the subclassing technique," Geary explains. "I put my own window function ahead of the standard one. At design time I handle many of the messages differently; at run time, I just pass them through." Geary says he created a number of window classes from scratch, including a list window that handles columns as well as rows, somewhat like a spreadsheet, and can fetch data from a database on the fly (a "regular" Windows list window requires that all its data be in memory). "The nice thing about this," he elaborated, "is that my own window classes coexist with the built-in Windows classes, and there's not much special I have to do to make my own objects work side by side with pre-existing ones. The rest of my program treats them all the same; it doesn't care whether the window was provided by me or by Windows." Resource Management Other advantages to programming in Windows involve the management of memory and the keyboard. Because all code in Windows is relocatable and discardable, the programmer, according to Geary, is liberated from both memory fragmentation problems and the need to juggle the trade-offs associated with code overlays. "In programs that I've done in the past," he explains, "I've had to use overlays a lot. And it's always been a pain trying to decide, ╘How heavily do I overlay this? What's the target machine size? Whom do I optimize this for?' I've been through that process a number of times and it's awful. You can't satisfy everybody. "But in Windows, all you do is break up the program into a lot of code segments. Each code segment is an independent entity as far as being loaded into memory is concerned, and Windows just loads in as many as it can fit. When it runs out of room, it moves them around or discards them as needed. In other words, it optimizes for all the different machines at once. "You have to do a little extra work to take advantage of Windows' memory management, but it's much better than dealing with a fixed-overlay structure." As for keyboard management, Geary applauds the fact that Windows supplies the programmer with information that eliminates the need for direct programming of the hardware. "In Windows," he said, "keyboard handling is very simple. They send you messages for everything. With any keypress, you get WM_KEYDOWN. When any key is released, you get WM_KEYUP. You can distinguish between the first press of a key and presses that come from the auto repeat mechanism. "Basically, you can look at the keyboard at whatever level you like, without being an ill-behaved program. There's no incentive to go to the hardware, because they've provided all the stuff you want to do in the first place." Finally, Geary cites the advantages afforded to programmers and users alike by DDE and the Windows Clipboard. He believes the presence of a well- defined standard for interapplication communications will have a long- range impact on the types of software people write. "Philosophically, the Clipboard and DDE allow people to get away from building complex all-in-one applications and open up the possibility of getting back to building smaller programs that work effectively together." ████████████████████████████████████████████████████████████████████████████ The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces Charles Petzold☼ Programmers experienced with Microsoft(R) Windows will discover that much of the OS/2 systems Presentation Manager is familiar territory. The windowing and user interface portion of the Presentation Manager is obviously derived from Windows, and many of the window messages are identical. However, the Graphics Programming Interface (GPI) is mostly new and offers some very significant enhancements over the Windows Graphics Device Interface (GDI). GPI is derived largely from IBM's Graphical Data Display Manager (GDDM) and 3270 Graphics Control Program (GCP) with some features, such as bitmaps and regions, derived from Windows. GPI also reflects influence from many other graphics interfaces, ranging from the Graphical Kernel System (GKS) to PostScript(R). One of the first hurdles in approaching GPI is dealing with the concept of the presentation space (PS). What makes it difficult is that the Presentation Manager supports three different types of presentation spaces, the "cached micro-PS," the "micro-PS," and the "normal-PS." Which type of presentation space you choose for your application depends on a variety of factors, some of which I'll discuss here. The subject becomes more interesting when you realize that the type of presentation space you choose determines to some degree how you structure a Presentation Manager application for drawing. I'll demonstrate this in three programs, CACHEDPS, MICROPS, and NORMALPS, each of which uses one of the three available types of presentation space. Each program displays the same simple graphics shown in Figure 1☼. The text string "Graphics Programming Interface" is displayed in the center of the window with dotted lines drawn from the corners of the text string to the corners of the window. Both the text and the dotted lines are displayed in red. GPI Drawing Functions Before we discuss the presentation space, let's take a quick look at the functions that these three programs use for drawing the text string and the dotted lines. The CACHEDPS.C program calls these GPI drawing functions in its ClientWndProc function during processing of the WM_PAINT message. Many of the GPI drawing functions require a point in x and y coordinates, which is provided to the function as a structure of type POINTL. The POINTL structure is defined in the OS2DEF.H header file like this: typedef struct _POINTL { LONG x ; LONG y ; } POINTL ; The LONG data type is simply a 32-bit unsigned long integer. To define a POINTL structure in a C program you can use struct _POINTL ptl ; or more simply: POINTL ptl ; By convention, the names of POINTL variables begin with the prefix ptl. (One big difference between GPI and some other high-level graphics languages is that GPI does not use floating point. This is for performance reasons──floating point calculations on personal computers are not yet fast enough for a highly interactive graphical system such as the Presentation Manager.) GPI includes the concept of a current position. To draw a line, the first step is to set the current position to the beginning of the line. This requires assigning coordinate values to the x and y fields of the POINTL structure and calling the GpiMove function: ptl.x = ... ; ptl.y = ... ; GpiMove (hps,&ptl) ; I'll discuss the hps parameter of GpiMove shortly. GpiMove does not draw anything; it merely sets the current position. To draw the line, set the fields of the structure to the end point of the line and call GpiLine: ptl.x = ... ; ptl.y = ... ; GpiLine (hps,&ptl) ; The GpiLine function also moves the current position to the indicated point. It certainly is a nuisance to set two fields of a POINTL structure before calling GpiMove or GpiLine. However, you'll find that the use of a structure to represent points offers a nice clean consistency in GPI function syntax. For example, the GpiQueryCurrentPosition function, which obtains the current position, has the same syntax as GpiMove: GpiQueryCurrentPosition (hps,&ptl) ; Upon returning from the function, the ptl structure is filled with the coordinates of the current position. The use of a structure for points also allows syntax consistency in those GPI functions that require multiple points. One such function is GpiPolyLine, which draws a series of lines beginning at the current position. You define an array of POINTL structures like this: POINTL aptl [15] ; The call to GpiPolyLine requires the number of points and the array name: GpiPolyLine (hps,15L, aptl) ; That GpiPolyLine call is almost equivalent to this code: for (i = 0 ; i < 15 ; i++) GpiLine (hps,aptl + i) ; The points you specify in GPI functions are in "world coordinates." Although GPI supports several transforms to convert world coordinates to pixels, I'll defer discussion of these transforms. By default, world coordinates are in units of pixels relative to the lower left corner of the window. Values on the x (horizontal) axis increase to the right; values on the y (vertical) axis increase going up. CACHEDPS.C uses the function GpiCharStringAt to display the text string: GpiCharStringAt (hps, &ptl,lTextLength,cText) ; Under default conditions, the baseline of the left side of the first character is positioned at the point given in the ptl structure. The cText parameter is a pointer to a character string or a character array, and lTextLength is the length of the string. For example, this code displays the text "Hello" at the point (10, 10): ptl.x = 10 ; ptl.y = 10 ; GpiCharStringAt (hps, &ptl,5L,"Hello") ; By default, the font that GPI uses is the normal system font, the same font that appears in Presentation Manager title bars, menus, and dialog boxes. GPI Attributes CACHEDPS.C calls two functions to set the attributes that GPI uses when drawing. Before drawing anything, the program calls GpiSetColor: GpiSetColor (hps,CLR_RED) ; This causes all text and lines displayed after this function call to be colored red. Before displaying the four lines, CACHEDPS.C calls another attribute function to set a dotted line type style: GpiSetLineType (hps, LINETYPE_DOT) ; GPI also has functions that obtain information from the system. One of the query functions used in CACHEDPS.C is GpiQueryTextBox. This function fills a POINTL array with the coordinate positions of the four corners of an imaginary box that would surround a particular character string if the string were displayed at the point (0, 0). The CACHEDPS program uses this information to draw the four lines. Near the beginning of WM_PAINT processing, CACHEDPS.C makes a call to GpiCreateLogColorTable. In the version of the Presentation Manager I used for this article, the default background color is black and the default foreground color is white. Thus, the GpiErase function erases the window to black. The call to GpiCreateLogColorTable in CACHEDPS reverses this so that the default background color is white and the foreground color is black. Whether this function will be necessary in the final version of the Presentation Manager is not clear as of this writing. The calls to GpiRestorePS, GpiSavePS, and GpiResetPS in CACHEDPS.C during the WM_PAINT message are temporary fixes to avoid some bugs in the version of the Presentation Manager I have. These functions should not be necessary when the Presentation Manager is in final shape. The PS and DC The first parameter to every GPI function in CACHEDPS.C is the variable named hps. This variable is defined as type HPS, a handle to a presentation space. Before a Presentation Manager program can draw on the display, it must either create a presentation space or use a presentation space that has already been created by the Presentation Manager. How you do this depends on the type of presentation space you decide to use. The presentation space is basically a data structure internal to the Presentation Manager. All the attributes of the presentation space──the current position, the current background and foreground colors, the current line style type, transforms, and others──are stored here. For the normal-PS, the presentation space can also store collections of GPI function calls. A Windows programmer might at first assume that the presentation space is equivalent to the Windows display context (DC), but they are really two different things. In fact, the Presentation Manager includes the concept of a display context. In the Presentation Manager, the display context usually describes a physical output device, such as the video display, printer, or plotter. A display context can also be a block of memory that is used as if it were an output device; it can also be a metafile. You can think of the display context as comprising the output device and its device driver. A presentation space does not itself imply any type of output device. Instead, the presentation space must be associated with a particular display context. The reason for the separation of the presentation space and the display context will be evident later. The conceptual relationship between an application program, the presentation space, and the display context is shown in Figure 2. Of the three different types of presentation spaces supported by the Presentation Manager, normal-PS, sometimes also called the full-PS, is the most versatile, with micro-PS second and cached micro-PS being the least versatile. To use a normal-PS or micro-PS, a program must create the presentation space by calling the function GpiCreatePS. Strictly speaking, these are the only two types of presentation space supported by GPI. The cached micro-PS is provided courtesy of the component of the Presentation Manager concerned with the windowing and user interface functions, sometimes called the "Win" or "User" component. The Presentation Manager creates the micro-PS for you. The Cached Micro-PS The CACHEDPS program shown in Figure 3 uses a cached micro-PS. (For more information on cached micro-PS see Microsoft(R) Operating System/2 Software Development Kit, Presentation Manager Specification, Vol. 1, p. 210) A program can obtain a handle to a cached micro-PS by calling WinBeginPaint or WinGetPS. CACHEDPS.C uses both of these functions. You can call the WinGetPS function while processing any window message. After you're done with the presentation space you release it by calling WinReleasePS: hps = WinGetPS (hwnd) ; ∙ ∙ ∙ < use GPI functions > ∙ ∙ ∙ WinReleasePS (hps) ; CACHEDPS calls WinGetPS during the WM_CREATE message. The only GPI function the program calls at that time is GpiQueryTextBox. CACHEDPS needs to obtain the dimensions of the character box only once because the height and width of the text string will not change while the program is running. The functions WinGetPS and WinReleasePS must be called as a pair while processing a single message. Do not call WinGetPS while processing one message and WinReleasePS while processing another. During processing of a WM_PAINT message, a program calls WinBeginPaint to obtain a handle to a cached micro-PS and WinEndPaint to release the handle: hps = WinBeginPaint (hwnd, NULL,NULL) ; ∙ ∙ ∙ < use GPI functions > ∙ ∙ ∙ WinEndPaint (hps) ; A cached micro-PS has several important characteristics. First, the cached micro-PS is always associated with the video display, or more precisely, with a window on the video display. That makes the cached micro-PS easy to use (WinGetPS requires only one parameter) but also limits it to the screen. Second, all attributes of the presentation space are set to their default values when the handle is obtained by calling WinGetPS or WinBeginPaint. This is why CACHEDPS.C sets the color to red and the line style type to dotted when it processes the WM_PAINT message. Finally, when using the cached micro-PS a program usually draws in units of pixels. You can change that with a few calls to certain GPI functions, but it is not quite as easy as when a program creates its own presentation space by calling GpiCreatePS. Windows programmers will note that CACHEDPS is structured much like a Windows program. If you're converting a program from Windows to the Presentation Manager, you may want to use a cached micro-PS to ease the conversion. The WM_PAINT Message CACHEDPS does all of its drawing while processing the WM_PAINT message. Handling the WM_PAINT message correctly is an important structural consideration in both Windows programs and Presentation Manager programs. A window procedure receives a WM_PAINT message whenever an area of the window has become invalid, meaning that the area no longer contains what the program originally drew there. If a portion of a window has been covered and then becomes uncovered, the Presentation Manager will post a WM_PAINT message to that window. A window procedure can also receive a WM_PAINT message when the window has been changed in size. Although a Presentation Manager program can draw on a window at almost any time, the program must be able to update the entire window on receipt of a WM_PAINT message. Programmers often satisfy this requirement by doing all window painting during the WM_PAINT message and none at any other time. As you'll see, dealing with WM_PAINT messages becomes somewhat simpler with the normal-PS. The Micro-PS Because the cached micro-PS is always associated with a window on the video display, you cannot use a cached micro-PS for drawing on another output device, such as a printer or a plotter. To use a printer or plotter you must create either a micro-PS or normal-PS. A program can also create a micro-PS for drawing on the program's window. The MICROPS program in Figure 4 shows how this is done. MICROPS.C creates the presentation space by calling GpiCreatePS while processing the WM_CREATE message in ClientWndProc. GPIT_MICRO in the GpiCreatePS function tells GPI to create a micro-PS. GpiCreatePS also requires a handle to a display context with which the presentation space will be associated. For a video display window, this display context handle is obtained from WinOpenWindowDC: hdc = WinOpenWindowDC (hwnd) ; The presentation space includes an imaginary drawing surface called the "presentation page." (I promise that this is the last term I'll be presenting here.) You define the size and units of the presentation page in the GpiCreatePS function. You set the size of the page by the SIZEL structure. Setting the cx and cy fields to 0 makes the page size the same as the size of the entire video display. The PU_PELS identifier indicates that the coordinates of the page are in units of pixels. You can instead use one of the identifiers shown in Figure 5 for units other than pixels. Using a pixel-coordinate page is often the best approach for simple graphics output. Information obtained from the Presentation Manager windowing system, such as the size of the window obtained during the WM_SIZE message, is always in units of pixels regardless of the presentation page size. You would have to convert those coordinates and sizes to the page units using GpiConvert. The presentation space handle obtained from GpiCreatePS is valid until the presentation space is destroyed by a call to GpiDestroyPS. MICROPS destroys the presentation space while it's processing the WM_DESTROY message, which is the last message the window procedure receives. Until that time, the hps value must be used during all WM_PAINT and WM_SIZE messages, so it is stored in a static variable. Note that MICROPS sets the color and line style type during WM_CREATE processing rather than during WM_PAINT processing. The micro-PS exists until it is explicitly destroyed so any GPI attributes that a program sets remain in the presentation space until they are changed. In contrast, a cached micro-PS is reset to default attributes each time a program obtains it. The syntax of the function WinBeginPaint for a micro-PS is a little different from that for a cached micro-PS. Rather than use WinBeginPaint to obtain the presentation space handle, the existing handle is passed to the function as the second parameter. (In the version of the Presentation Manager that I used for this article, WinBeginPaint and WinEndPaint were not working correctly; that's why they are shown in MICROPS.C between comment delimiters.) Windows programmers, watch out! You'll find it natural to do something like this during the WM_PAINT message when using a cached micro-PS: hps = WinBeginPaint (hwnd, NULL,NULL) ; ∙ ∙ ∙ < draw using default colors > ∙ ∙ ∙ GpiSetColor (hps,CLR_RED) ; ∙ ∙ ∙ < draw using red > ∙ ∙ ∙ WinEndPaint (hps) ; However, if you switch to a micro-PS, you might first alter the code like this: WinBeginPaint (hwnd,hps, NULL) ; ∙ ∙ ∙ < draw using default colors > ∙ ∙ ∙ GpiSetColor (hps,CLR_RED) ; ∙ ∙ ∙ < draw using red > ∙ ∙ ∙ WinEndPaint (hps) ; This will work fine during the first WM_PAINT message but not during subsequent messages because the color will still be set to red. Remember that the micro-PS retains all GPI attributes that you change until you change them again. You can avoid such problems by using GpiSavePS and GpiRestorePS to save and restore the GPI state: WinBeginPaint (hwnd,hps, NULL) ; ∙ ∙ ∙ < draw using default colors > ∙ ∙ ∙ GpiSavePS (hps) ; GpiSetColor (hps,CLR_RED) ; ∙ ∙ ∙ < draw using red > ∙ ∙ ∙ GpiRestorePS (hps,-1L) ; WinEndPaint (hps) ; Or you can explicitly set the color back to the default value at the end of WM_PAINT processing: WinBeginPaint (hwnd, hps, NULL) ; ∙ ∙ ∙ < draw using default colors > ∙ ∙ ∙ GpiSetColor (hps,CLR_RED) ; ∙ ∙ ∙ < draw using red > ∙ ∙ ∙ GpiSetColor (hps, CLR_DEFAULT) ; WinEndPaint (hps) ; Comparing CACHEDPS.C and MICROPS.C listings reveals only a subtle change in structure-two GPI attribute functions called during the WM_PAINT message in CACHEDPS are handled in MICROPS during the WM_CREATE message. The real change in structure occurs when you move up to the normal-PS. The Normal-PS The micro-PS gives a program access to only a subset of the GPI functions. Although it is a very substantial subset and includes all the basics, it excludes all functions that pertain to GPI "segments"; these are supported only by the normal-PS. A segment (not to be confused with a memory segment in 80286 microprocessor architecture) is a stored collection of graphics drawing and attribute commands. You open a segment, you call a bunch of GPI functions, and then you close the segment. All the functions are stored in the segment. You can then cause these commands to be drawn on the output device by issuing one of several functions such as GpiDrawChain. The NORMALPS program shown in Figure 6 gives you a little taste of segments. The graphics output of NORMALPS depends only on the size of the window. So, during the WM_SIZE message, a segment is created by a call to GpiOpenSegment. The attribute and drawing commands are called just as they were in the two earlier programs. But these graphics orders are not displayed just yet; they are instead saved in the segment. During the WM_PAINT message, the segment is displayed by calling GpiDrawChain. Obviously this simplifies WM_PAINT processing considerably. You can create many segments in your program; they are saved as part of the presentation space. A segment can be either chained or unchained. The root chain consists of all chained segments; GpiDrawChain draws all the segments in the root chain. Unchained segments can be called from other segments. When an unchained segment is called, the coordinate positions specified in all the drawing commands in the segment can be transformed with translation, scaling, or rotation, which is useful for graphical modeling. You can edit segments, change the order in which segments in the root chain are drawn, and test whether an object drawn in a segment is within a specified radius of a particular point. This is very handy for mouse hit- testing. Besides the support of segments, the normal-PS has another advantage over the micro-PS. It is the only presentation space that can be reassociated with another display context; that is, you can disassociate the presentation space from the video display and then associate it with the printer. Because the segments are part of the presentation space, you need not recreate them. The segments are ready to be drawn on the printer. Of course, the use of segments is overkill in NORMALPS, as it would be in many other small Presentation Manager programs. The basic rule is to use the simplest type of presentation space that meets your needs. If you can do everything you need to do using the cached micro-PS, use that. Don't take a full set of luggage on an overnight trip. Figure 2: The relatonship between an application program, the presentation space, and the display context. ┌─────────────────────────────────────────────────────────────┐ │ │█ │ ╔══════════════════════════════════════════╗ │█ │ ║ Application Program ║ │█ │ ╚════════════════════╤═════════════════════╝ │█ │ draws to the │█ │ ↓ │█ │ ╔══════════════════════════════════════════╗ │█ │ ║ Presentation Space ║ │█ │ ╚════════════════════╤═════════════════════╝ │█ │ which is associated with a │█ │ ↓ │█ │ ╔══════════════════════════════════════════╗ │█ │ ║ Display Context ║ │█ │ ╚════════════════════╤═════════════════════╝ │█ │ which causes output to be displayed on a │█ │ ↓ │█ │ ╔══════════════════════════════════════════╗ │█ │ ║ Physical Output Device ║ │█ │ ╚══════════════════════════════════════════╝ │█ └─────────────────────────────────────────────────────────────┘█ ██████████████████████████████████████████████████████████████ Figure 3: CACHEDPS Make File cachedps.obj : cachedps.c cl -c -G2sw -W2 -Zp cachedps.c cachedps.exe : cachedps.obj cachedps.def link cachedps, /align:16, NUL, os2, cachedps CACHEDPS.DEF Module Definition File NAME CACHEDPS DESCRIPTION 'Demonstrates Cached Micro-PS(C) Charles Petzold, 1988' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc CACHEDPS.C-Demonstrates Cached Micro-PS #define INCL_GPI #include <os2.h> #include <stddef.h> MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ; main () { static CHAR szClientClass [] = "CachedPS" ; HAB hab ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX, szClientClass, "Cached Micro-PS", 0L, NULL, 0, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2) HWND hwnd ; USHORT msg ; MPARAM mp1 ; MPARAM mp2 ; { static CHAR szText [] = "Graphics Programming Interface" ; static LONG lTextLength = sizeof szText - 1L ; static LONG alColorData [] = { CLR_BACKGROUND, RGB_WHITE, CLR_NEUTRAL, RGB_BLACK } ; static POINTL ptlTextStart, aptlLineStart [4], aptlTextBox [TXTBOX_COUNT] ; static SHORT cxClient, cyClient ; HPS hps ; POINTL ptl ; SHORT sIndex ; switch (msg) { case WM_CREATE: hps = WinGetPS (hwnd) ; GpiQueryTextBox (hps, lTextLength, szText, TXTBOX_COUNT, aptlTextBox) ; WinReleasePS (hps) ; break ; case WM_SIZE: cxClient = LOUSHORT (mp2) ; cyClient = HIUSHORT (mp2) ; ptlTextStart.x = (cxClient - aptlTextBox [TXTBOX_BOTTOMRIGHT].x - aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ; ptlTextStart.y = (cyClient - aptlTextBox [TXTBOX_TOPLEFT].y - aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ; for (sIndex = 0 ; sIndex < 4 ; sIndex ++) { aptlLineStart [sIndex] = aptlTextBox [sIndex] ; aptlLineStart [sIndex].x += ptlTextStart.x ; aptlLineStart [sIndex].y += ptlTextStart.y ; } break ; case WM_PAINT: hps = WinBeginPaint (hwnd, NULL, NULL) ; GpiSavePS (hps) ; /* temp fix */ GpiResetPS (hps, GRES_ATTRS) ; /* temp fix */ GpiCreateLogColorTable (hps, LCOL_RESET, LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */ GpiErase (hps) ; GpiSetColor (hps, CLR_RED) ; GpiCharStringAt (hps, &ptlTextStart, lTextLength, szText) ; GpiSetLineType (hps, LINETYPE_DOT) ; GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMLEFT) ; ptl.x = 0 ; ptl.y = 0 ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMRIGHT) ; ptl.x = cxClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ; ptl.y = cyClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ; ptl.x = 0 ; GpiLine (hps, &ptl) ; GpiRestorePS (hps, -1L) ; /* temp fix */ WinEndPaint (hps) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } Figure 4: MICROPS Make File microps.obj : microps.c cl -c -G2sw -W2 -Zp microps.c microps.exe : microps.obj microps.def link microps, /align:16, NUL, os2, microps MICROPS.DEF Module Definition File NAME MICROPS DESCRIPTION 'Demonstrates Micro-PS(C) Charles Petzold, 1988' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc MICROPS.C-Demonstrates Micro-PS #define INCL_WIN #define INCL_GPI #include <os2.h> #include <stddef.h> MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ; HAB hab ; main () { static CHAR szClientClass [] = "MicroPS" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX, szClientClass, "Micro-PS", 0L, NULL, 0, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2) HWND hwnd ; USHORT msg ; MPARAM mp1 ; MPARAM mp2 ; { static CHAR szText [] = "Graphics Programming Interface" ; static HPS hps ; static LONG lTextLength = sizeof szText - 1L ; static LONG alColorData [] = { CLR_BACKGROUND, RGB_WHITE, CLR_NEUTRAL, RGB_BLACK } ; static POINTL ptlTextStart, aptlLineStart [4], aptlTextBox [TXTBOX_COUNT] ; static SHORT cxClient, cyClient ; HDC hdc ; POINTL ptl ; SHORT sIndex ; SIZEL sizl ; switch (msg) { case WM_CREATE: hdc = WinOpenWindowDC (hwnd) ; sizl.cx = 0 ; sizl.cy = 0 ; hps = GpiCreatePS (hab, hdc, &sizl, PU_PELS | GPIF_DEFAULT | GPIT_MICRO | GPIM_NORMAL | GPIA_ASSOC) ; GpiCreateLogColorTable (hps, LCOL_RESET, LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */ GpiQueryTextBox (hps, lTextLength, szText, TXTBOX_COUNT, aptlTextBox) ; GpiSetColor (hps, CLR_RED) ; GpiSetLineType (hps, LINETYPE_DOT) ; break ; case WM_SIZE: cxClient = LOUSHORT (mp2) ; cyClient = HIUSHORT (mp2) ; ptlTextStart.x = (cxClient - aptlTextBox [TXTBOX_BOTTOMRIGHT].x - aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ; ptlTextStart.y = (cyClient - aptlTextBox [TXTBOX_TOPLEFT].y - aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ; for (sIndex = 0 ; sIndex < 4 ; sIndex ++) { aptlLineStart [sIndex] = aptlTextBox [sIndex] ; aptlLineStart [sIndex].x += ptlTextStart.x ; aptlLineStart [sIndex].y += ptlTextStart.y ; } break ; case WM_PAINT: /* BeginPaint (hwnd, hps, NULL) ; */ GpiErase (hps) ; GpiCharStringAt (hps, &ptlTextStart, lTextLength, szText) ; GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMLEFT) ; ptl.x = 0 ; ptl.y = 0 ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_BOTTOMRIGHT) ; ptl.x = cxClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ; ptl.y = cyClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ; ptl.x = 0 ; GpiLine (hps, &ptl) ; /* EndPaint (hps) ; */ WinValidateRect (hwnd, NULL, FALSE) ; /* temp fix */ break ; case WM_DESTROY: GpiDestroyPS (hps) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } Figure 5: The identifiers used in the GpiCreatePS function to set the units of the presentation page. Page Units Identifier Units PU_ARBITRARY Pixels (with adjustment) PU_PELS Pixels PU_LOMETRIC 0.1 millimeter PU_HIMETRIC 0.01 millimeter PU_LOENGLISH 0.01 inch PU_HIENGLISH 0.001 inch PU_TWIPS 1/1440 inch Figure 6: NORMALPS Make File normalps.obj : normalps.c cl -c -G2sw -W2 -Zp normalps.c normalps.exe : normalps.obj normalps.def link normalps, /align:16, NUL, os2, normalps NORMALPS.DEF Module Definition File NAME NORMALPS DESCRIPTION 'Demonstrates Normal-PS(C) Charles Petzold, 1988' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc NORMALPS.C-Demonstrates Normal-PS #define INCL_WIN #define INCL_GPI #include <os2.h> #include <stddef.h> MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ; HAB hab ; main () { static CHAR szClientClass [] = "NormalPS" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE | FS_SIZEBORDER | FS_TITLEBAR | FS_SYSMENU | FS_MINMAX, szClientClass, "Normal-PS", 0L, NULL, 0, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } MRESULT EXPENTRY ClientWndProc (hwnd, msg, mp1, mp2) HWND hwnd ; USHORT msg ; MPARAM mp1 ; MPARAM mp2 ; { static CHAR szText [] = "Graphics Programming Interface" ; static HPS hps ; static LONG lSegmentName = 1 ; static LONG lTextLength = sizeof szText - 1L ; static LONG alColorData [] = { CLR_BACKGROUND, RGB_WHITE, CLR_NEUTRAL, RGB_BLACK } ; static POINTL aptlTextBox [TXTBOX_COUNT] ; HDC hdc ; POINTL ptl, ptlTextStart, aptlLineStart [4] ; SHORT cxClient, cyClient, sIndex ; SIZEL sizl ; switch (msg) { case WM_CREATE: hdc = WinOpenWindowDC (hwnd) ; sizl.cx = 0 ; sizl.cy = 0 ; hps = GpiCreatePS (hab, hdc, &sizl, PU_PELS | GPIF_DEFAULT | GPIT_NORMAL | GPIM_NORMAL | GPIA_ASSOC) ; GpiCreateLogColorTable (hps, LCOL_RESET, LCOLF_INDRGB, 0L, 4L, alColorData) ; /* temp fix */ GpiQueryTextBox (hps, lTextLength, szText, TXTBOX_COUNT, aptlTextBox) ; GpiSetDrawControl (hps, DCTL_ERASE, DCTL_ON) ; GpiSetDrawingMode (hps, DM_RETAIN) ; break ; case WM_SIZE: cxClient = LOUSHORT (mp2) ; cyClient = HIUSHORT (mp2) ; ptlTextStart.x = (cxClient - aptlTextBox [TXTBOX_BOTTOMRIGHT].x - aptlTextBox [TXTBOX_BOTTOMLEFT].x) / 2 ; ptlTextStart.y = (cyClient - aptlTextBox [TXTBOX_TOPLEFT].y - aptlTextBox [TXTBOX_BOTTOMLEFT].y) / 2 ; for (sIndex = 0 ; sIndex < 4 ; sIndex ++) { aptlLineStart [sIndex] = aptlTextBox [sIndex] ; aptlLineStart [sIndex].x += ptlTextStart.x ; aptlLineStart [sIndex].y += ptlTextStart.y ; } GpiDeleteSegment (hps, lSegmentName) ; GpiOpenSegment (hps, lSegmentName) ; { GpiSetColor (hps, CLR_RED) ; GpiCharStringAt (hps, &ptlTextStart, lTextLength, szText) ; GpiSetLineType (hps, LINETYPE_DOT) ; GpiMove (hps,aptlLineStart + TXTBOX_BOTTOMLEFT) ; ptl.x = 0 ; ptl.y = 0 ; GpiLine (hps, &ptl) ; GpiMove (hps,aptlLineStart + TXTBOX_BOTTOMRIGHT); ptl.x = cxClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPRIGHT) ; ptl.y = cyClient ; GpiLine (hps, &ptl) ; GpiMove (hps, aptlLineStart + TXTBOX_TOPLEFT) ; ptl.x = 0 ; GpiLine (hps, &ptl) ; } GpiCloseSegment (hps) ; break ; case WM_PAINT: /* WinBeginPaint (hwnd, hps, NULL) ; */ GpiDrawChain (hps) ; /* WinEndPaint (hps) ; */ WinValidateRect (hwnd, NULL, FALSE) ; /* temp fix */ break ; case WM_DESTROY: GpiDeleteSegment (hps, lSegmentName) ; GpiAssociate (hps, NULL) ; GpiDestroyPS (hps) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } ████████████████████████████████████████████████████████████████████████████ Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution Kevin Ruddell OS/2 semaphores are a powerful and general mechanism for coordinating multiple concurrent threads of execution. One common use of semaphores is to serialize access to pieces of code and the resources referenced by the code. The other common use of semaphores is to allow one thread to signal another thread that an event has occurred, for example, that an I/O operation has been completed. The OS/2 systems fully support both these uses of semaphores. There are four types of semaphores in OS/2: exclusive system, nonexclusive system, RAM, and FSRam (Fast-Safe Ram). FSRam semaphores are a new addition to Version 1.1 of OS/2. Nonexclusive system and RAM semaphores can be used either for serialization or signaling, while exclusive system and FSRam semaphores are intended only for serialization. FSRam semaphores are intermediate in safety and their performance is comparable to that of RAM semaphores. System semaphores can be used by threads of different processes that do not share memory and they offer some safety advantages over the other two types. Future versions of OS/2 will provide additional protection mechanisms and network support for system semaphores. When access to a reusable resource, such as a file, a data structure, or the screen must be serialized, we associate a semaphore with the resource and require that the semaphore have only one owner at a time. A thread then attempts to gain ownership of a semaphore by issuing a DosSemRequest or DosFSRamSemRequest call. If the semaphore is unowned, the call returns a value of zero and the calling thread now owns the semaphore. When the thread is already owned, the call either returns immediately (if the Timeout parameter is zero) or else waits n milliseconds (if Timeout = n) or it waits indefinitely (if Timeout = -1). We say the thread is blocked if it must wait for the completion of a semaphore request. If the requesting thread gains ownership of the semaphore before the timeout period expires, it immediately returns a zero value. Otherwise, it waits until the timeout period has elapsed and returns a non-zero value. A thread surrenders ownership of a semaphore by issuing a DosSemClear or DosFSRamSemClear call. FSRam and exclusive system semaphores (system semaphores where the NoExclusive option was not selected in the DosCreateSem request that created the semaphore) are well suited to serialization. RAM semaphores and nonexclusive system semaphores can provide serialization, but you should follow some guidelines when using them. Whether semaphores are used for serialization or for signaling, they can cause the hazards of deadlock, violations of serialization, and process termination while owning resources. By careful analysis of the application, knowledge of the OS/2 semaphore functions, and employment of some simple rules, these hazards can be avoided. ─────────────────────────────────────────────────────────────────────────── Basic semaphore operation for serialization of reusable resources. ─────────────────────────────────────────────────────────────────────────── ────────────────── ─┐ ╔═══════════╗ DosSemRequest sem={1,2,...n}├─╢THREAD{sem}╟───► DosFSRamSemRequest │ ║issues call║ ────────┬───────── THREAD{1}│ ╚═══════════╝ ▼ THREAD{2}│ R ◄───────────────────┐ ∙ │ ▼ │ ∙ │ THREAD{sem} owns R │ ∙ │ ─────────────────────────────────── │ THREAD{n}│ When THREAD{sem} owns the resource │ ─┘ all other threads requesting access │ │ to R are blocked │ ──────────────────┬──────────────── │ │ ╔════════▼═════════╗ │ ║THREAD{sem} issues║ │ │ ║ DosSemClear ║ │ ╔═════════════════════╗ ║ DosFSRamSemClear ║ │ ║ THREAD{sem} either ║ ╚══════════════════╝ ▼ ║ reissues a request ║ ──────────────────────── ╔════════════╗ ◄───║ for R or goes off ║◄───THREAD{sem} relinquishes───►║ R ║ ║to do something else.║ ownership of R ║ becomes ║ ╚═════════════════════╝ ──────────────────────── ║ availiable ║ R = Reusable Resource and an associated semaphore ╚════════════╝ Semaphore Types When you write a program with semaphores, you should ask yourself two questions: Is this semaphore for serialization or signaling? Will it be used between threads of one process or between different processes? The answers to these questions indicate which type of semaphore you should use and the system calls with which you will access it. Generally, you should use RAM semaphores for serialization of or synchronization between threads within one process, FSRam semaphores or exclusive system semaphores for serialization of threads in different processes, and nonexclusive system or RAM semaphores for signaling between threads in different processes. All OS/2 semaphore operations are performed on a semaphore handle, which is a 32-bit quantity used to access the semaphore. In the case of a RAM or FSRam semaphore, the handle is the semaphore data structure's address; in the case of a system semaphore, it is the value that is returned by the DosCreateSem or by the DosOpenSem call. RAM Semaphores RAM semaphores are mainly useful for coordinating the execution of threads within a process. They are fast, since they use a simple data structure, a double word of data storage, and receive relatively little management or protection from OS/2. RAM semaphores can be used between processes that share memory, which could be created by a DosAllocShrSeg call. However, RAM semaphores should not be used for serialization between processes, since OS/2 will not free a RAM semaphore if the owner terminates while owning it. They are fine, though, for providing serialization between threads within one process and can be employed for signaling within or between processes. The RAM semaphore data structure is a double word of storage that must be initialized to 0 (unowned/clear) before it is used as a semaphore. It should then be accessed using any of the following: DosSemClear, DosSemRequest, DosSemSet, DosSemSetWait, DosSemWait, or DosMuxSemWait. System Semaphores System semaphores are the most flexible, safe, and easy-to-use type of semaphore, but the overhead needed to maintain them exacts a performance penalty. They do not require shared memory, and their ownership is relinquished when their owner terminates, so they are suitable for serialization between processes. Each process that wishes to use a system semaphore must get a handle from OS/2 by making a DosCreateSem or DosOpenSem call. The process supplies a null-terminated semaphore name with the same format as the name of a file in the subdirectory \SEM\, for example, \SEM\RESOURCE.LCK. OS/2 then returns a handle for future access. The process also specifies whether nonowning processes can alter the state of the semaphore while it is owned. System semaphores are usually created nonexclusive for signaling applications and exclusive for serialization applications. DosCreateSem is used if the semaphore does not already exist (an error code is returned if it exists) and initializes the new semaphore to unowned. DosOpenSem is used if another process has created the system semaphore, and an error code is returned if it does not exist. If, in your application, one process will definitely run first, it can issue DosCreateSem; later processes can then issue DosOpenSem. If the order of execution of processes is not known, each one can call DosCreateSem and, if this fails because the semaphore already exists, then call DosOpenSem. When a process no longer needs to reference a system semaphore, it should issue a DosCloseSem call. The system semaphore itself will be deleted when all of the processes using the semaphore have called DosCloseSem. If a process terminates with open system semaphores, then the semaphores are closed by the system (see Figure 1). Normally, a thread should not request a semaphore while it already owns it. This would cause it to block waiting for itself to free the semaphore, resulting in deadlock. A system semaphore created with the exclusive option does not cause a thread to block if the thread makes a DosSemRequest while owning the semaphore. Instead, the semaphore's use count is incremented. Each time the owning thread issues a DosSemClear, the semaphore's use count is decremented, and when the use count becomes 0, the semaphore is released. Note that such a semaphore is not the same as a classical counting semaphore, which will be simulated later in this article. If another thread attempts to do a DosSemRequest on an exclusive semaphore that is already owned, it will block and wait for the use count to go to 0. Nonexclusive system semaphores behave just as RAM semaphores do when they are used with DosSemClear, DosSemRequest, DosSemSet, DosSemSetWait, DosSemWait, and DosMuxSemWait. FSRam Semaphores FSRam semaphores provide serialization between processes, combining most of the speed of RAM semaphores with much of the safety of system semaphores. They use a simple data structure and have a small system overhead, so their performance is very good. Since they store the data to track semaphore ownership properly, FSRam semaphores permit DosExitList processing to free an owned resource when the owning process terminates. Just as with exclusive system semaphores, FSRam semaphores support recursive DosFSRamSemRequest's by incrementing a use count, and support DosFSRamSemClear's by decrementing a use count. When the use count goes to 0, the semaphore becomes unowned. (Again, note that such a semaphore is different from a classical counting semaphore.) DosFSRamSemRequest and DosFSRamSemClear are the only OS/2 calls available for FSRam semaphores. The FSRam semaphore data structure exists in memory shared by the cooperating processes and must have its length field set to 12 and all of its other fields initialized to 0 before its first use. ─────────────────────────────────────────────────────────────────────────── Comparison of Semaphore Types ─────────────────────────────────────────────────────────────────────────── ┌────────────────────────────────────────────────────────────────────┐ │Semaphore │ │ │ │█ │Types │ Use │Protection │Performance │█ ├──────────┼─────────────┬─────────────────┼────────────┼────────────┤█ │ │ │Between Threads │ │ │█ │Exclusive │Serialization│in Different │Some Safety │ │█ │System │Only │Processes │Provided │ │█ ├──────────┼─────────────┼─────────────────┤Managed │Moderate │█ │Non- │Serialization│Between Threads │by OS/2 │ │█ │exclusive ├─────────────┤in Different │ │ │█ │System │Signaling │Processes │ │ │█ ├──────────┼─────────────┼─────────────────┼────────────┼────────────┤█ │ │ │Between Threads │ │ │█ │ │Serialization│Within One │Minimal │ │█ │ │ │Process │Protection──│ │█ │RAM ├─────────────┼─────────────────┤Not │High │█ │ │ │Between Threads │Managed │ │█ │ │Signaling │in the Same or │by OS/2 │ │█ │ │ │Different Process│ │ │█ ├──────────┼─────────────┼─────────────────┼────────────┼────────────┤█ │ │ │ │Some │ │█ │FSRam │Serialization│Between Threads │Protection──│ │█ │(Fast-Safe│Only │in Different │Partially │High │█ │Ram) │ │Processes │Managed │ │█ │ │ │ │by OS/2 │ │█ └──────────┴─────────────┴─────────────────┴────────────┴────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Serialization In OS/2, semaphores can ensure that no more than one thread at a time has access to a protected resource. This is done by assigning a semaphore to the protected resource and adopting the programming convention that each piece of code that accesses the protected resource must be preceded by a call to DosSemRequest (or one to DosFSRamSemRequest) and must be followed by a call to DosSemClear (or one to DosFSRamSemClear). It is essential that every thread accessing the protected resource follow this procedure, or time-dependent errors may occur. The thread whose semaphore request returns without error or time-out "owns" the semaphore, and all other threads that issue a similar request will block. This permits the owning thread to have exclusive access to the code that affects the protected resource. When the owning thread no longer needs to access the protected resource, it issues a semaphore clear and thereby gives up its ownership. If another thread is blocked because of a request on the newly released semaphore, it is permitted to proceed and access the protected resource. For example, suppose two threads use a semaphore to serialize access to a resource. You might end up with code such as that shown in Figure 2, where the ──1 indicates that the calling thread will wait indefinitely until the requested semaphore becomes available. The programming convention described previously should always be strictly followed unless it is unnecessary for a particular application. Even a slight deviation can lead to a failure of serialization, deadlock, or both. An FSRam semaphore can be safely used for serialization between threads of different processes if each process provides for recovery in case it terminates while owning the semaphore. Before terminating, each process should set up an ExitList routine to ensure the integrity of the resource protected by the semaphore by making a call to DosExitList. When the process terminates, the ExitList routine will execute and should first issue DosFSRamSemRequest to acquire the semaphore, then clean up the resource, and finally issue DosFSRamSemClear in order to free the resource for use by other processes. If the FSRam semaphore is owned by a thread of the terminating process, at the point when DosFSRamSemRequest is issued during the execution of an ExitList routine, the owning thread ID is forced to become the current thread ID and the use count for the semaphore is set to 1. This allows the ExitList routine to put the resource into a consistent state and then free it with DosFSRamSemClear (see Figure 3). RAM semaphores should only be used to serialize threads within a single process. In this case, they are efficient (since there is little system overhead), convenient (since the threads inherently share memory), and safe (since the process only ends when all of its threads end). It is possible to use a nonexclusive system semaphore for serialization, but all threads that access the semaphore must follow the programming conventions for serialization. Nonexclusive system semaphores generally behave in the same way RAM semaphores do, except that if the owner dies while owning one, the next thread to return from a DosSemRequest on it will own it and get an error code. The new owner, an ordinary thread or an ExitList routine, can clean up the resource and issue DosSemClear. Exclusive system semaphores can be safely used to provide serialization between different processes, since the system semaphore data structure contains the ID of the owning thread, and any attempt by one thread to change the state of an exclusive system semaphore owned by another thread will cause the thread to block or report an error. When a thread ends, either individually or because its process terminates, while owning a system semaphore, a flag is set in the semaphore data structure. The next thread that gets the semaphore by means of a call to DosSemRequest gets the error code "owner ended owning semaphore" and can take suitable recovery measures. A subsequent call to DosSemClear will free the semaphore and clear the error flag. ExitList processing for system semaphores used for serialization is very similar to the ExitList processing for FSRam semaphores. Signaling RAM or nonexclusive system semaphores allow one thread to detect an event in another . For example, suppose code fragment F1 in thread1 must execute before F2 in thread2, and the system semaphore with handle sigSem has been created with the NoExclusive option and has been set with DosSemSet. The two threads could appear as shown in Figure 4. In the worst case, thread2 will execute the call to DosSemWait before thread1 finishes executing F1, at which time it blocks. When thread1 finishes executing F1, it clears sigSem, and thread2 proceeds to execute F2. The semaphore functions DosSemSet, DosSemClear, DosSemWait, DosSemSetWait, and DosMuxSemWait are used for signaling. DosSemSet sets a semaphore, and DosSemClear clears it. DosSemWait blocks the current thread until the semaphore is cleared or until an optional time-out interval has elapsed. DosSemSetWait is an indivisible concatenation of DosSemSet followed by DosSemWait. DosMuxSemWait blocks the current thread until one of a list of semaphores is cleared or until an optional time-out interval is over. You should not use the signaling calls with exclusive system semaphores. As an example, consider a thread that issues DosSemSetWait. If the exclusive system semaphore is already owned, an error is returned. Otherwise, the thread sets the semaphore and waits for it to clear. However, this will never happen since no other thread can clear it. Figure 5 is an example of the use of DosSemSetWait, similar to the one mentioned above with DosSemWait but this time in an infinite loop. The code fragment F2 can execute only once for each execution of F1. Figure 6 demonstrates the use of DosMuxSemWait. In this case, thread4 blocks until thread1, thread2, or thread3 clears its semaphore. Thread4 proceeds and takes whatever action is appropriate in response to the signaling thread. As with serialization, a thorough analysis is necessary when using semaphores to implement signaling in order to avoid the hazards of deadlock and synchronization failure. Circular Buffers Suppose two threads of one process are communicating via a circular buffer, that is, thread1 writes to Buffer and thread2 reads from Buffer. Buffer has a fixed capacity, bufSize, and pointers to the locations of the newest (head) and oldest (tail) items. The buffer is circular in the sense that each thread accesses location 0 after it accesses location bufSize──1. Buffer is empty when head=tail and is full when tail=head+1 (mod bufSize). Suppose that mutexSem, emptySem, and fullSem are RAM semaphores and that DosSemClear(&mutexSem); DosSemSet(&emptySem); DosSemClear(&fullSem); head=tail=0; is executed before the threads are spun off. Then the two threads would appear as shown in Figure 7. The protected resource in this case is Buffer, together with its associated variables (head, tail, emptySem, and fullSem). Each thread accesses the protected resource between calls to DosSemRequest and to DosSemClear on the mutual exclusion semaphore, known as mutexSem, so the accesses are made serially, and there are no race conditions. Before the threads are spun off, the protected resource is in a consistent state: head==tail, emptySem is set, and fullSem is clear. Each thread changes the state of the protected resource so that if it was consistent before entering the critical section, it is consistent on leaving the critical section. For example, if thread1 adds an item that occupies the last available free space in Buffer, it then sets the fullSem semaphore. Thread1 always clears the emptySem semaphore since it has just added an item to Buffer. Similarly, thread2 sets the emptySem semaphore when it removes the last item from the buffer and always clears the fullSem semaphore. If Buffer is full, thread1 sets fullSem and then blocks on fullSem. In this case, emptySem is not set and, since thread1 is blocked outside its critical region, thread2 can enter its critical region, remove an item from the buffer, and clear fullSem, allowing thread1 to proceed. If Buffer is empty, thread2 sets emptySem and then blocks on emptySem. Since fullSem is not set and thread2 is blocked outside its critical section, thread1 can enter its critical section, add an item to Buffer, and clear emptySem, allowing thread2 to proceed. Simulating a Classical Counting Semaphore A classical counting semaphore can be easily simulated with the semaphore facilities that are offered by OS/2. ClassicCountSem is an integer variable that can only be accessed, apart from initialization, via the two atomic operations P and V (see Figure 8). To simulate classicCountSem and its associated operations P and V, use the OS/2 semaphores countSem for signaling and mutexSem for serialization, both initially clear, and the integer variable count, initially nonnegative (see Figure 9). All of the code in the P and V routines that changes count and countSem lies in mutual exclusion regions bracketed by calls on mutexSem, simplifying the analysis of these routines. These routines would work correctly with all references to countSem removed, but P would burn up a lot of cycles testing conditions that are not satisfied. The wait on countSem prevents useless activity, but it also allows P to proceed if there is a chance that it will run to completion. P will block on countSem only if count is 0. When V executes, count is incremented and countSem is cleared, permitting P to run until count is found to be 0 again. Therefore, whenever V "produces" a unit, any waiting P's have an opportunity to "consume" it. Scheduling The precise timing and behavior of threads synchronized by means of semaphores is affected by the activity of other threads in the system, such as priority and position in the list of blocked threads. If several threads are waiting on a semaphore, your application should not count on one in particular to be scheduled to run, as factors outside your control may affect the decision of the scheduler. DosSemWait, DosSemSetWait, and DosSemRequest are level-triggered. This means, for example, that a thread blocked by one of these calls will only return if the blocking semaphore remains clear until the blocked thread can actually run. If some other thread, in the meantime, sets the semaphore again, then the blocked thread will remain blocked. If the semaphore remains clear until the blocked thread can be scheduled and actually run, then the call returns and the once-blocked thread continues with its execution. DosMuxSemWait, on the other hand, is edge-triggered. That is, a thread that is blocked by a call to DosMuxSemWait will return if one of the semaphores in its list is cleared, even if the semaphore is set by another thread before the blocked thread can run again. Conclusion Semaphores in OS/2 can be used to provide serialized access to protected resources by associating a semaphore with the resource and combining OS/2 function calls with programming conventions to ensure that the semaphore has only one owner at a time. Semaphores can also allow one thread to signal another that an event has occurred. By analyzing the particular application, you can pick the appropriate semaphore type and OS/2 function calls. Figure 1: Using System Semaphores if( rc=DosCreateSem( NoExclusive, &sysSem, &semName)) if( rc == ERROR_ALREADY_EXISTS) DosOpenSem( &sysSem, &semName) ; else <...error...> <...use the semaphore...> DosCloseSem( sysSem) ; Figure 2: Two Threads Serializing Access to a Resource long resourceSem = 0; thread1() { ∙ ∙ ∙ DosSemRequest( &resourceSem, -1L); <... read/modify resource ...> DosSemClear( &resourceSem); ∙ ∙ ∙ } thread2() { ∙ ∙ ∙ DosSemRequest( &resourceSem, -1L); <... read/modify resource...> DosSemClear( &resourceSem); ∙ ∙ ∙ } Figure 3: ExitList Processing for a Fast-Safe Ram Semaphore main() { ∙ ∙ ∙ DosExitList( 1, &Cleanup); /* add to exit list */ ∙ ∙ ∙ } Cleanup() { if( DosFSRamSemRequest( &sem, 0L) != ERR_TIMEOUT) { <... put in a consistent state ...> DosFSRamSemClear( &sem); } DosExitList( 3, 0); /* goto next ExitList routine */ } Figure 4: Signaling Between Threads DosSemSet( sigSem); thread1() ∙ ∙ ∙ F1 DosSemClear( sigSem); ∙ ∙ ∙ } thread2() ∙ ∙ ∙ DosSemWait( sigSem, -1L); F2 ∙ ∙ ∙ } Figure 5: Using DosSemSetWait thread1() ∙ ∙ ∙ while(1) { F1 DosSemClear( sigSem) ; } ∙ ∙ ∙ } thread2() ∙ ∙ ∙ while(1) { DosSemSetWait( segSem, -1L) ; F2 } ∙ ∙ ∙ } Figure 6: Using DosMuxSemWait struct { int numSem; int res1; unsigned long semHandle1; int res2; unsigned long semHandle2; int res3; unsigned long semHandle3; } muxSemList ; int muxIndex; thread1() ∙ ∙ ∙ F1 DosSemClear( sigSem1) ; ∙ ∙ ∙ } thread2() ∙ ∙ ∙ F2 DosSemClear( sigSem2) ; ∙ ∙ ∙ } thread3() ∙ ∙ ∙ F3 DosSemClear( sigSem3) ; ∙ ∙ ∙ } thread4() ∙ ∙ ∙ muxSemList.numSem = 3 ; muxSemList.res1 = 0; muxSemList.semHandle1 = sigSem1 ; muxSemList.res2 = 0 ; muxSemList.semHandle2 = sigSem2 ; muxSemList.res3 = 0 ; muxSemList.semHandle3 = sigSem3 ; ∙ ∙ ∙ DosMuxSemWait( &muxIndex, &muxSemList, -1L) ; switch( muxIndex) { case 1: ∙ ∙ /* respond to F1 */ ∙ case 2: ∙ ∙ /* respond to F2 */ ∙ case 3: ∙ ∙ /* respond to F3 */ ∙ } } Figure 7: Example of a Circular Buffer thread1() ∙ ∙ ∙ <... get item c ...> DosSemWait( &fullSem, -1L) ; DosSemRequest( &mutexSem, -1L); Buffer.head = c; /* store c in Buffer */ head++; /* advance head of Buffer */ head %= bufSize; /* wrap around to beginning */ if((head==tail)||((tail==0)&&(head==bufSize-1))) DosSemSet( &fullSem); /* set if full */ DosSemClear( &emptySem); /* not empty */ DosSemClear( &mutexSem); ∙ ∙ ∙ } thread2() ∙ ∙ ∙ DosSemWait( &emptySem, -1L); DosSemRequest( &mutexSem, -1L); c = Buffer.tail; /* get c from Buffer */ tail++; /* advance tail of Buffer */ tail %= bufSize; /* wrap around to beginning */ if( head==tail) DosSemSet( &emptySem); /* set if empty */ DosSemClear( &fullSem); /* not full */ DosSemClear( &mutexSem); <... use item c ...> ∙ ∙ ∙ } Figure 8: Classical Counting Semaphore P() { <... wait until classicCountSem > 0 ...> classicCountSem-; } V() { classicCountSem++; } Figure 9: Simulating a Counting Semaphore under OS/2 P() { int blocked=1; while( blocked == 1) { DosSemWait( &countSem); /* wait til maybe ok */ DosSemRequest( &mutexSem, -1L); /* mutual excl */ if( count == 0) /* not ready yet */ DosSemSet( &countSem); /* set up block */ else { count-; /* decrement count */ blocked-; /* set up loop exit */ } DosSemClear( &mutexSem); /* mutual excl */ } } V() { DosSemRequest( &mutexSem, -1L); /* mutual excl */ count++; /* increment count */ DosSemClear( &countSem); /* free waiters */ DosSemClear( &mutexSem); /* mutual excl */ } ████████████████████████████████████████████████████████████████████████████ Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library Ross M. Greenburg☼ You are in a maze of twisty little passages, all alike. >USE DYNAMIC-LINK LIBRARY I see no DLL here. >MAKE DLL I don't know how to make a DLL. >INVENTORY You have: ■ an 80286 machine ■ sufficient memory ■ OS/2 ■ An OS/2 toolkit. In the toolkit is: ■ a text editor ■ a C compiler ■ an assembler ■ a linker >USE TOOLS IN TOOLKIT TO MAKE DLL Huh? Just as in the game ADVENTURE, not knowing the keywords when you're trying to create a dynamic-link library can be very frustrating. Once you know the keywords, though, you can explore new areas of the game and bring home prizes and treasures. The objective of this article is to help teach you some of the new keywords and techniques you need to build a dynamic- link library of your own. What Is a DLL? The idea of dynamic-link libraries is one of the most important concepts that the OS/2 systems introduce. Although the principle of DLLs has been around for some time-they are usually called shareable libraries in other operating systems-OS/2 makes such a shareable library an intrinsic part of the operating system. In fact, the system library functions themselves are DLLs in OS/2, and are clearly separated by device type, making upgrading easy. Under MS-DOS(R), after you compile a program, you then link it with other portions of the program and with portions from a library of commonly used routines. The end result is a standalone piece of code that is loaded into memory, has its outstanding address references resolved, and is then executed. The physical file resulting from the link contains portions of the library it used; two programs that use printf will each contain a copy of the library functions that comprise that ubiquitous function. In a single-tasking operating system, with a sufficiently large hard disk, this really isn't a problem. In a multitasking operating system that allows for shared memory use, loading multiple copies of the same code seems wasteful. OS/2 obviates this by permitting only one copy of a given function to be loaded in memory and to have this copy shared by any task that wants to use it. Since the function itself is not physically part of the program file, it is possible for the executable to be rather small and to update the library only as required. The concept of separate overlay files, and complicated linkers, is no longer needed; just include the specific DLLs needed and let the operating system do the rest. However, the advantages of using DLLs go far beyond the convenience; there is a functionality to DLLs that you can exploit in many different ways. For example, there is the ability for two or more completely separate and distinct programs to share memory by simply accessing the same run- time routine. This communication, already an intrinsic part of OS/2, can be fine-tuned with DLLs to fit your exact needs in a complicated environment. This is not without a price, though; there are some tricks, cautions, and caveats to writing an operable DLL. I discovered some of them the hard way while preparing a simple example of DLL use. Of course, if you want to avoid the supposed complexity of these useful techniques, there is nothing in OS/2 to prevent you from using the standard, and more familiar, linking techniques of the past, except, perhaps, the knowledge that there is a better way. A dynamic-link library is not simply a new method of linking an old library. There are some intrinsic differences between the two techniques. A look at how statically linked libraries are linked into your code will help you to understand how the new Dynalink approach differs and why it is an improvement. Static Linking When you compile a standard C program, the resulting output of the compiler is an object file, which contains a number of different types of records for procedures and routines, externally accessible variables, local stack variables, and so on. Each item has a unique record type associated with it. There is also a unique record type that indicates where the code for a routine starts. Another record type has the name of the routine and a pointer to that routine's code. Some record types indicate that the routine requested is not found and hence must be external to the object module; other record types indicate that the object is an externally located data item. Each call to a routine that cannot be resolved within the particular source module is changed into a parameter that simply involves an "external item" record.You can help the compiler by specifying the call type as being a near or far routine, or a near or far external item of some other type. After you've finished compiling all your various source modules into their object modules, you link them together, along with some appropriate libraries, and end up with an executable piece of code. The linker examines each object module it sees, usually in the order in which they're presented, and keeps a list of all record types that either indicate a request for an external item or define a global item. Then, as it sees record types indicating actual code routines, it determines that routine's placement and resolves all calls for it into an actual address within the eventual output file. Far calls, of course, indicate not just an offset within a 64Kb segment, but allow additional segments to be addressed, which in turn permit the creation of much larger programs. There is really nothing intrinsically foreign to the compiler in the concept of mixed memory-model code as long as it knows how a routine will be called. The compiler generates a far return for routines defined as far calls and near returns for near calls. Addressing far data items is resolved in a similar fashion: the compiler puts out a record type that the linker can understand and resolve into an actual segment and offset. There is an extra step for the actual loading and executing of the code, discussed later in this article. Whatever items are not resolved within the linking of the various object modules are next searched for in the libraries. These libraries are basically object modules with nothing but local references resolved. A module in the library usually starts off as a simple object file and is then stored and indexed into a library as an entire unit. It is not stored on a routine-by-routine basis, but on an object-file-by-object-file basis instead. The appropriate routine or external item is found in the library, and the module is then pulled from the library and inserted into the executable form. All references to it are resolved, and the process continues. An important consideration is that the object module originally loaded into the library as one unit is pulled from it as one unit as well, even if only one of the functions specified in the routine is referenced. The end result is a totally self-contained image, which, when executed, is loaded at some address, called the base address. The base segment address is added to all of the other segment addresses throughout the code in the mysterious load routines, and finally, with one simple call or jump, your program is executed. That's a basic, not-too-technical description of how static linking works. The differences between dynamic linking and the idea of statically linking an already existing library are not all that substantial, but the end product is. Consequently, the conceptual design of the dynamic-link library is different. Dynamic Linking With a normal library, you compile all of the object modules you need and then use a librarian program to create a library. The library itself is in a strange format, suitable only for linkers and librarian programs. Things are a little different with DLLs, though. First, there are two separate link steps. You must link the constituent object file members that form the DLL together and then link your own code with the resulting DLL. Creating the DLL itself, however, requires some work. After you've created the object files that will make up DLL, you link them and a special module definition file with the normal linker to create DLL. Its format is really no different from a normal EXE file-it even has the "MARKZIBO" as its first two bytes. It is therefore well suited for the standard system loader to load as if it were actually a program. Later on, the system does just that for the initialization routine. The new library will have an extension of DLL, as this name is hardwired into any affiliated EXE files. The special module definition file, or DEF, describes the external interface for each of the accessible routines: their public names and their attributes. Anything not specifically mentioned in the DEF file cannot be routinely accessed by an outside program. This definition file is called the export module definition file. By running the export DEF file through a program called Import Librarian (IMPLIB), you can create a special library file. This library file is conceptually similar to the standard notion of a library and hence has the LIB extension (see Figure 1). Instead of providing a LIB file, another option is to create what is in essence the inverse of the export DEF file. Such a file is called an import module definition file, which also has the extension DEF (see Figure 2). When you link the DLL with your own code, the linker sees the special record format of the import library (the LIB file created by IMPLIB), or reads the import DEF file and creates special records that OS/2's program load facilities understand. The end result of a link using DLLs is a hybrid file that can be considered a partial EXE and a partial OBJ at the same time. A compiled object module resolves local variables and routines into a segment and an offset, leaving external references virtually undefined. The Dynalink program results in an EXE coming from the linker with its external references to DLL routines effectively unresolved. At this point, I'm going to refer to segments as selectors, since I am referring to DLLs implemented under OS/2 running in protected mode. Part of OS/2's program loader recognizes that the EXE it's about to load contains DLL calls. Finding these records causes a lookup on an internal table to determine if the DLL has already been loaded. Only one copy of any code within the DLL is loaded-all sessions and processes share this copy. Each module in the DLL is defined as a "load-at-run-time" or a "load-on- demand" module. Regardless of which definition is chosen, a selector is allocated for each module, and all references to those modules are now resolved into a selector and offset pair. If the module has been defined as a load-at-run-time module, then the actual code for the module is read from the file and loaded into memory, and any outstanding linkages are resolved. When trying to locate a DLL, by the way, the operating system looks in the directory paths specified in your environment's LIBPATH variable. Value of Protected Mode Consider what happens when a load-on-demand function is called before that selector points to valid code: a page fault occurs, and the memory management module can easily resolve what the problem is, load the appropriate code, and let the program continue operating as if nothing had happened. Subsequent calls to routines within the same selector would operate without a page fault. Once the page fault mechanism, an intrinsic part of OS/2 and protected mode applications, has been enabled, it is transparent whether or not a requested page exists in real memory or in virtual memory. The 80286 and 80386 chips have a table within them called the Local Descriptor Table, or LDT, which holds selectors and the characteristics of these selectors. There is an LDT for each of the processes currently running. If a process attempts to access memory by using a selector not within its LDT, the hardware will cause a fault to occur──effective hardware protection of memory space. The Global Descriptor Table, or GDT, is similar to the LDT (see Figure 3), except that all tasks can access the selectors, and their associated memory, contained therein. Although this seems like a simple way to make a selector and its data space accessible to multiple processes, OS/2 does not use the GDT for shared memory access; instead it makes an entry into the LDT of each process. This is because memory space used by a DLL should only be accessible to the processes actually using the DLL. Using the GDT would make the DLL selectors available to every session and process on the system. When a request for memory allocation is made to OS/2, the type of memory (shared or nonshared) is included in the request, and an entry is made in the LDT for all processes permitted to share this memory. Actually, whenever shareable memory is allocated, an entry is made in every process's LDT. Processes requiring access to the memory have proper entries in the LDT; other tasks have illegal contents that will cause a memory fault to be generated if they are used to access DLL-allocated memory. Only the kernel (Ring 0) code can write to these tables, however. (Device drivers also run at Ring 0, so they'd have write access to the descriptor tables as well.) What's the Big Deal? So far, a relatively efficient mechanism exists for linking in routines as required at run time instead of just once at link, all automatically and transparently. However, just what are the advantages of such an ability? First, swapping the DLL routines in and out of memory becomes pretty easy; the LDT has a "present" bit indicating whether the requested segment is in memory or not. If not in memory, a page fault occurs as described above, and the swapped-out DLL routine can be brought into real memory. Since the selector itself is just an index into a table that contains real address information, the individual DLL modules can end up anywhere in memory, transparent to your own code. Program code without some data space associated with it is a rarity; pure code can't manipulate items, although it is frequently useful in mathematical routines. The 8086/8088 family of processors used the data segment register to address its data space. The 80286/80386 family of chips requires data to be addressed through a selector as well. The information for the data selector is also stored in the LDT. By setting the appropriate bits in the LDT entry for a given selector, its associated memory can be made private or publicly accessible, or it can be set to be written to as read-only. Data selectors can even require a certain level of privilege in the code attempting to access it. Any illegal operation causes a fault to occur, and OS/2 can deal with the faulting process as required. This means that, with the LDT set properly, memory can be shareable between tasks and protected from illegal or erroneous access, and other interesting memory uses and control techniques are made possible. As such, the DLL can be controlled and fine-tuned in a variety of different ways, through the DEF files. Defining the DEF File There are two different types of DEF files. The EXPORT definition file lets the world know what the various entry points and their characteristics are. The IMPORT definition file indicates which functions of many potential DLLs will be used and should therefore be linked at run time. A DLL can have both IMPORTS and EXPORTS defined in the same DEF file. The IMPORT library is created by processing the EXPORT definition file through IMPLIB. Let's look at each piece separately, along with its available features and options. These options must all be entered in the appropriate DEF file in uppercase. LIBRARY Statement The EXPORT DEF file requires several fields (see Figure 4). The most important one that is required is the LIBRARY field, which defines the file as a DLL Export definition file, instead of a normal application DEF file. The LIBRARY statement must be the first one in the DEF file, allowing the linker (and IMPLIB) to have a head start on what is to come. The first argument, [name], to the LIBRARY statement is the eventual output name for the created DLL. The filetype, or extension, will automatically be DLL. When the DLL is first loaded, you may want to initialize some things, for instance, setting specific data items, and assuring that certain system resources are available. Each DLL can have an initialization routine that will be called when the DLL is first loaded and there are no other references to it, or upon each invocation of the DLL. The second argument, [init_type], lets you specify whether you want the initialization routine called every time the DLL is invoked (INITINSTANCE), or once, when the DLL is first loaded (INITGLOBAL, the default). NAME Statement Similarly, if the linker sees the NAME statement, it understands that you are creating an application and not a DLL. The NAME statement allows you to specify whether the application is Windows- or Presentation Manager- compatible and, if so, whether it is capable of running in real mode or protected mode. If you specify WINDOWAPI as the second argument, then the application requires Windows in order to execute. Specifying WINDOWCOMPAT means that it is Windows-compatible and can run in a window group under OS/2. Finally, specifying NOTWINDOWCOMPAT indicates that the application requires its own screen group when running. The name command allows you to specify with [appname] the name the application will have after linking. Naturally, the default extension is EXE. You'll get a warning message from the linker if the name you choose for the executable name and the name you choose in the NAME line don't match. CODE Statement All code segments within the DLL share a similar set of attributes unless otherwise specified. The default attributes are set with the CODE statement. There are a few other optional parameters that are allowed (but ignored) in the CODE statement for compatibility with Windows and the Presentation Manager. The [load] parameter indicates whether you want the segment automatically loaded upon DLL invocation (PRELOAD) or to wait until the segment is actually accessed with a call (LOADONCALL, the default). In an application with large areas of the code that might never be called, you no longer need to load those library calls into memory all at once. For example, if you had a very large error recovery routine that would only get called once in a great while, you wouldn't need to give up the memory space it required until the error occurred. Another example would be having many levels of help menus that are rarely used──a perfect fit for DLLs. When a LOADONCALL routine is called for the first time, it is loaded automatically and will stay loaded in memory unless it is marked as discardable. That means that its associated memory can be freed as required with fresh copies loaded from disk. If you use the [executeonly] option to specify that other processes cannot read this segment, then, even though the LDT marks the selector as globally accessible, it cannot be read or treated like a data segment selector by any process without the appropriate privilege level. The default, EXECUTEREAD, allows the memory allocated to this selector to be read for purposes other than execution. It is important to note that routines containing such items as a switch/case statement cannot be made EXECUTEONLY since a portion of the code space is used as data space for storage of the switch label table. Only code segments with a high enough privilege level can access the hardware directly. You can use the [iopl] parameter to specify that a segment has this ability. The default (NOIOPL) makes sense; unless otherwise specified, an attempt to access the hardware directly, for example the com port, will cause an immediate fault. When you allow a segment to access hardware directly, include the IOPL parameter in the CODE line. It is probably best to specify IOPL only when required. OS/2 still requires that you make a system call to request the privilege of hardware access. The way privilege level functions in OS/2 is an important factor when trying to understand the implications of the IOPL parameter. The Intel programmer's reference manuals for the 80286 and 80386 provide a detailed description of the 80286/80386 IOPL levels. The 80286/80386 chip prohibits direct transitions between code segments with different levels of privilege. The default privilege level for application code in OS/2 is Ring 3. Ring 2 code segments can access hardware directly. The only way to transfer from one privilege level to another is through what is known as a call gate, which has a specific selector type in the LDT and an actual "destination selector," the selector belonging to the actual code segment of the privileged call. Also, the gateway has its own attendant privilege level and can only be called by code segments of the same privilege level. Conceptually, when a call is made to a privileged routine, it passes through the call gate before passing to the privileged routine. Since the only way through the call gate would be with either a CALL instruction (going into the routine) or a RET (coming back from the routine), the call gateway provides extra code security, but at the cost of some additional hardware overhead. Each transition via a gateway causes parameters on the stack to be copied to a new stack──another interesting security feature of the 80286, since a program with a lower privilege level could manipulate the return address on the stack otherwise. A routine utilizing the IOPL parameter uses up an additional slot in the LDT table. Although the LDT table has 8Kb of possible entries in it-each LDT entry takes up 8 bytes, so an entire segment has been allocated to the LDT in OS/2-5Kb of those are reserved for all shared segments throughout the entire system, including the selector space used for DLL selectors. This leaves you with about 3Kb of LDT entries for private use, which is probably enough for the foreseeable future. This has been modified to a 3-to-1 shared-to-private ratio in the next release of OS/2. The final parameter in the CODE statement allows you to specify whether the segment is a NONCONFORMING or a CONFORMING segment. This also deals with the IOPL privilege level and can be pretty confusing at first. Consider it to be the inverse of the gateway approach. Normally, a segment will execute with the privilege level of the calling segment. However, there are times when this might not be appropriate; consider a Ring 3 communications protocol checking routine called from a Ring 0 device driver. In this situation, you might not want to allow the protocol checker to operate with the higher privilege of its calling segment. The default case, the NONCONFORMING parameter, would cause the Ring 3 routine to execute at Ring 3. Set to CONFORMING, it would execute at the privilege level of the routine calling it, the device driver running at Ring 0. Data Space Definitions Just as code segments have a method of setting default parameters, the data segments also allow certain parameters to be set with the DATA statement, which shares some of its parameter list with the CODE statement. This makes a great deal of sense because these parameters describe how to make the default settings for each data selector in the LDT. The format of the DATA statement is therefore very similar to the CODE statement. As before, [load] indicates whether the data segment should be loaded upon first invocation or when the first access to the selector's address is made. The default condition is LOADONCALL; however, you can specify invocations of load with PRELOAD. The [readonly] parameter helps you to determine whether the data segment can be written into with the default parameter of READWRITE or should be protected against write access (READONLY). Attempts to write to a READONLY segment cause a hardware fault. You use [instance] to specify whether or not the data segment, the DGROUP data segment in most cases, should be automatically allocated upon invocation, and if so, whether there should be one copy allocated for the entire DLL (SINGLE, the default setting for DLLs) or each instance of DLL use should have its own automatic data segment allocated (MULTIPLE, the default setting for applications). If no automatic allocation is required, then the parameter should be set to NONE. Each data segment can have its own IOPL level, which allows you to set the minimum privilege level required to access this data segment. Setting the [iopl] parameter to IOPL means that only Ring 2 and higher privileged levels are granted access to the data segment. The default, NOIOPL, enables Ring 3 code segment routines to have access to the data affiliated with the data segment. This allows you to create an interesting interface between IOPL and NOIOPL segments through common shared memory, like passing a message through a keyhole. Finally, [shared] allows you to determine whether or not a data segment marked as a READWRITE segment can be shared among different tasks. If it is marked as shareable, then only one segment is allocated at load time, and any process with a privilege level sufficient to write to it can do so. The default, NONSHARED, does not permit write access to a common data segment and causes a separate copy to be loaded for each instance. If a data segment is marked as READONLY, then by definition it is shareable. Segment Parameters Unless otherwise specified, code and data segments have the attributes you set in the CODE and DATA statements, or their predefined default values if you don't describe them. However, using the SEGMENTS statement, you can specify the individual characteristics for a given named segment. The [CLASS 'classname'] parameter is an option that lets you specify that the <segmentname> parameter, which is required, be assigned to the class specified. If you don't specify a classname, then the CODE classname will be assigned to code segments and "unclassed" data segments will be assigned to the DGROUP classname, or whatever was specified in the appropriate DATA group definition for the object/segment. EXPORTS Statement The EXPORTS statement is the only method of informing the outside world about the routines of the DLL and is applicable only to DLLs. Unless specified by inclusion in the EXPORTS section of the DEF file, a DLL routine is invisible to applications. A name used internally within the DLL need not be the name the application knows the routine by; you can easily make the outside name different from the internal name. This gives you a class of functions serving a similar purpose and then lets you categorize them with a meaningful prefix. If you want, you can allow access to the function by its ordinal (or the routines library "slot" number) instead of by its name, by specifying the desired ordinal, obviously unique for the DLL, preceded by an at (@) sign. If you do, lookups will be faster at load time, and less space will be required for the in-memory search list. There is a drawback to using ordinals, though; once you pick an ordinal number, you're stuck with it. This makes debugging a bit harder. Of course, the ordinal approach is good for DLLs whose internal structure you'd rather keep secret. If you do decide on the [@ordinal] option, then you may have to consider using the [RESIDENTNAME] option as well; normally, if an ordinal is used, OS/2 does not keep the specified external name available. If you don't use the ordinal parameter, OS/2 keeps the name resident in its search tables. If you include the use of any privileged functions in your routine, you have to let the linker know how many words to reserve for parameter copying by using the [pwords] variable. This information is later passed to the loader for construction of call gates. Since a calling task will have its own parameters copied as it passes through the gateway, you would have to reserve that space beforehand. IMPORTS Statement The IMPORTS section enables you to specify which external DLL routines you need in your application. Again, like the EXPORTS lines, you can specify a name your routine uses when it is trying to resolve external routines. You could, therefore, create a debugging DLL and a normal DLL and be able to link between them only by changing the <modulename> or the <entryname> associated with the named routine. The name of the application or DLL containing the desired <entryname> is the <modulename> specified in the EXPORTS statement for the DLL. The <entryname> can also be an ordinal number. If the optional [name=] parameter is not specified, then the default name by which the routine will be known will be the same as <entryname>. You must specify an internal name, however, if you've specified an ordinal number instead of an <entryname>. You can include other statements in the DEF file(s). They are summarized in Figure 5. Using the DEF Files There are two specific ways to use the DEF files: first, just include them on the command line to the linker, and second, pass them to the Import Library Manager utility, IMPLIB. IMPLIB is a standard part of the developer's toolkit in OS/2. If you're creating a DLL and the application to use that DLL, you don't necessarily need IMPLIB, since you can create the EXPORT and IMPORT library definition files as you desire. However, if you're creating a DLL for other applications to use, such as a commercial functions library or a replacement for a product that already exists, then IMPLIB should be part of your development cycle. IMPLIB takes one or more definition files for input and then produces what appears to be a simple LIB file for output. This allows you to include the LIB file in the link step. Assuming that you had two DLLs, called COM_INP.DLL and COM_OUT.DLL, each with their associated DEF files, you could create an IMPLIB statement such as the one shown in Figure 6. Then, simply distribute the COM_STUF.LIB and the two DLLs, keeping the internal details of the DLLs to yourself. Special versions of these import library files can allow for different public definitions of your DLL, for instance, one for production, and one for debugging. A DLL Example In attempting to create a DLL, I ran into a number of difficulties, some of which cannot, by the very nature of the DLL and multitasking software, be resolved. Deadlocks can occur in DLLs just as they do in other types of software. Think of the requirements of a multisession process such as a "chat" facility: multiple copies of the same process running, each of which occasionally generates a message, which is added to some internal queue. Each message generated must be collected by all other processes before it can be erased from the queue of outstanding messages, and each such message must eventually be displayed by each process. Finally, each process must be able to login or logout from the chat session and must have a unique identifier. I've designed such a facility, called DLL_CHAT (see Figure 7 for the code listings), as a method of demonstrating some of the unique abilities and problems that occur when using a DLL as the glue to hold a multiprocess concept like this together. It may not be useful on a single-screen machine, but if the output were going to a number of communications ports, it would be. You should note that all the problems inherent with this code design can be easily solved by including the OS/2 system resource of queues. However, that approach wasn't used, primarily because it wouldn't have required DLLs and wouldn't have been as efficient, either. Stepwise Design One advantage of using DLLs in this application is the ability not only to have private and shared memory, but for separately compiled and executed tasks to utilize the same code simultaneously. There is nothing to prevent users of DLL_CHAT from following my coding conventions and creating their own user-friendly interfaces──the bane of spiffy-concept designers. In fact, there is no reason why differently designed front ends couldn't be used for each session. Starting with this concept, I designed this code with most of the capabilities in the DLL. One ability of my DLL, called CHATLIB (see Figure 8 for code listings), is to provide for initialization code that is executed either on the first invocation of the DLL only, or on each invocation. This initialization routine is called before the process itself starts to run. This DLL only calls its initialization routine the first time, so its EXPORT file contains the INITGLOBAL parameter. Since this is the default condition, you can exclude it if you wish. The routine I use in this DLL is simple, setting certain default conditions and allocating some required queue space. First, the login procedure has to advise the library code that another consumer and provider of messages has suddenly appeared. To make things easier, the login procedure returns some user identifier to the process. It is useful to include an ID when generating new messages, when consuming old ones, and, of course, when logging out. The login procedure could have been part of the initialization routine, which would have been of type INITINSTANCE. However, since the initialization routine is run before the application gets control, I felt uneasy about logging in before the main routine had even been reached. As you can see, the initialization routine is used for things that I consider to have a "system" nature, while logging in seems to be more of an "application" task. However, this is only a personal sentiment. When the DLL sees the login, it allocates and assigns whatever global and local objects and structures are necessary for the new process. Where the actual allocations of memory would be made had to be decided in the design, since the memory could be allocated either in the DLL (becoming, in essence, a hidden object from the client code) or in the per-process code itself. There are advantages to having a DLL routine allocate memory that is globally accessible to all processes but that only the DLL routines know about. Also, a login causes each message already in the queue to appear unread to the newly logged-in task. Later, when requests are made for an outstanding and unread message, these messages will be returned. The general design of the DLL causes a sharing of the cleanup task on each call to the get-a-message routine. When a message is passed to the DLL, it is added to a queue, a structure that includes a flag word with one bit for each session. A mask word with a set bit for each empty task slot is used for the initial value of this flag word. An "or" operation is then used to obtain the current task ID, allowing the sender of the message to indicate that it has already received the message. When a process fetches a new message, it sets the bit in the flag word to show that it has done so. Then, when that flag indicates that all processes have received a copy of the message, the message can be removed from the global queue. Therefore, each process has to be able to manipulate that queue directly or must call a routine that has that ability. I've opted for a more modular design; using a routine to specifically remove the message from the queue, or to add a message to the queue, allows me to isolate the queue itself. Although the queue resides in global memory at this point, perhaps in the future it might reside on some node on a network or some memory device that might require a higher privilege level. Therefore, isolating the routine that physically modifies the queues is a good idea. Since there isn't a human attached to each session, each session sends a message only after a random amount of time has passed. And, just to keep things interesting, there is a suitable sleep period while the imaginary typist is entering his or her message, letting messages build up in the queue. Whenever the sender is not typing or sending a message, it is executing a loop that constantly seeks the outstanding message count. Blocking on a null message count would prohibit the sender from sending a message. Of course, OS/2 lets you have two different threads, one of which could block on a null message count within the DLL, but that is outside the scope of this article. Displaying of received messages occurs on a per-process basis. This could cause problems if the display takes place when the session is not the current foreground session; messages could scroll off the virtual screen while the process is in the background, an unacceptable condition in a real chat system. If a message is to be displayed but the session is not the foreground session, it is stored in an internal display queue. Normally, this queue would be a linked list of calloc'd memory. For DLL_CHAT, a short character array was used, one which would fill up quickly. Eventually, when it does fill up, it will stop fetching messages from the DLL queue. Since the DLL queue is constantly fed by all of the sessions, it too will fill up. Another session will then block when attempting to add a message to the queue. This condition can escalate until all sessions are blocked. Therefore, before any session sends a message, it checks to determine if room exists in the queue. OS/2 is a multitasking operating system, so a routine must not be interrupted between the time it determines there is room on the queue and actually adding the message to the queue. However, you don't want to starve other sessions, especially not those related in any way to DLL_CHAT. To prevent this, DLL_CHAT uses a globally accessible system semaphore and assigns the semaphore immediately upon entry to any collision-sensitive routine. Other processes trying to enter one of these routines would block on this flag and wait for it to free up or for a certain amount of time to pass. If the flag didn't change within the specified time-out period, then an error condition would be returned to the calling task. Finally, there is the logout routine. When the session gets a quit or exit command from the keyboard, it simply exits. However, as an intrinsic part of exiting, all DosExitList routines are called. The exit routine, in turn, calls the DLL's logout routine, which then sets the semaphore and proceeds to loop through the outstanding message list. For each outstanding message, it sets the flag as if the process had already received the message. After each flag word has been set, it is examined to determine if it has been read by all processes; if so, it is removed from the queue. Each message on the queue is a member of a linked list, and its memory is allocated from the global memory pool. When removing a message from the queue, the pointers of the other messages it points to are modified to point to each other, and then the memory is deallocated. Caveats and Warnings There are a few things you have to be aware of when you're designing your DLLs. I mentioned the extraordinary lengths I went to in the original design of DLL_CHAT to assure that certain areas of the code are protected against two competing tasks attempting to access them at once. This is a problem inherent in any multiprocessing system. Typically, the problem is called "reentrancy," that is, a piece of code being entered by a calling process before another process has finished its call. The actual definition of reentrancy is much stricter than this, but this is good enough for our purposes. Utilizing semaphores is effective in most circumstances. However, the method I chose would not work best without some of the special hooks OS/2 provides for the safe use of semaphores. Consider what happens if the session currently executing a semaphored routine happens to be interrupted and then gets killed by some high-priority event after it sets the semaphore, but before the semaphore is released. Such an event could be as normal as a keystroke being entered, or, if attached to a comm port, the modem losing carrier, or something even more esoteric. There is no guarantee that it will return to where it left off, but if it doesn't return and finish the routine, the semaphore will forever be marked as in use. OS/2 does offer an alternative if you use one of the system semaphores. The semaphore is created with a DosOpenSem call, which returns a semaphore handle, similar to a file handle. By using other semaphore calls, a process can effectively keep reentrancy from occurring. In the event that a process owning the semaphore at that time, and therefore blocking others waiting on it, gets killed for some reason, even unintentionally, the system will effectively call DosCloseSem, which clears the semaphore if set and restores it as a system resource if there are no other references to it. Each individual session of DLL_CHAT uses a few resources, which I wanted to make sure were properly cleaned up as a session exits. I used the OS/2 system call DosExitList to add some specific routines to each session's exit list, the list of routines that OS/2 executes on my behalf between the time the client program dies and the time it is buried. Currently, the exit routine simply calls the logout procedure, which in turn resets the systemwide flag word and the bits in each message and finally cleans up the message base and any outstanding semaphores. When designing a DLL, you should always think of worst-case scenarios, such as what would happen if this line of code were running while ten other processes were running those ten different lines of code. Since you cannot effectively control what the other processes are doing as they start to execute common areas of code, it is better to design the code as modularly as possible and be sure to semaphore around areas sensitive to multitasking occurring at just the wrong time──chances are that it will. Remember that you must not only program defensively against other processes using the DLL routines and their attendant data; if you opt to use OS/2 threads, you have to protect against their reentrant use of the DLL routines. Most of these considerations regarding DLLs can also be important when designing a threaded program. When considering unanticipated or asynchronous interruptions, you should think about signal catching and about not doing it in a DLL. If you're going to use the system to set a routine to catch a particular asynchronous event, such as program termination or Ctrl-C trapping performed with the DosSetSigHandler system call, doing it in the DLL can be dangerous. This is a general concept that can be applied toward program design not just in OS/2 but in any multiprocessing operating system. The concept of resource plays a critical role here. Who owns the resource of a signal catcher──an interrupt vector or another system facility in a multiprocessing operating system? Generally, the operating system itself owns the resource. When you take over such a system resource for your own purposes, you must remember that other processes might cause the resource to be used──and that you have no control over the event that resulted in the use of the resource. Normally, this would not be much of a problem, since the state of the resource can be saved before you take it over and then restored when you're done with it. But with at least one system facility, DosError, which allows a process to suspend hardware error processing, the state cannot be preserved for the next call or use. Thus, the state of the system can get pretty confused. Similarly, it is probably a good idea to avoid DosSetVect, which lets your exception handler be called when certain conditions, such as attempts to execute an illegal opcode, occur. Generally, if there is a possibility that some state in the operating system cannot be preserved owing to your taking over a system resource or facility, you should consider designing your code differently. If you must include such calls in your code, be sure to isolate thoroughly those portions of the code from other client members of the DLLs, and to preserve all other aspects of your process state. Be sure to terminate normally, too, not in a unique way, since DLLs have some special characteristics that are properly dealt with in automatic exit list processing upon client death. Design Considerations When designing your program to use DLLs, you have to be careful about a few things in your initial program design. First, access to all DLL routines is through a far call. So, although you can use the small memory models in the client section of your code and in the DLL itself, the external definition of the DLL routines must indicate it is a far routine. As such, the routine itself must also indicate in its prototype that it is a far routine; or else the CALL and RET statement types won't match. Remember to design your call to a DLL routine and the routine itself using the Pascal calling convention instead of the C calling convention. Although this is not a strict requirement (however, both calling sequences must match), it's a good idea for several reasons. First, it's required if you're using the routine as an IOPL gate. Also, it's more efficient, since the stack restore takes place only once in the called routine itself, and more languages support the Pascal convention, allowing your DLL to be used by these other languages. What about the data allocated in the DLL? That, too, must be addressed as far data from the client routines. Locally, within the DLL, it can be addressed as near or far as required. Before your client code ever executes, the initialization routine for the DLL will already have executed. Expecting any initialization by the main routine in your client code would be premature. Therefore, your DLL initialization code should only access data within the DLL itself, since the startup code may not even have allocated memory as of yet. You can find out a great deal when the loader calls your initialization routine, including the handle by which the DLL has been opened as well as the size of the heap in the calling EXE program. You can find more information on this in the OS/2 Programmer's Guide. What of the differences between global and instance data items? They can be confusing, since each DLL module has no easy method to determine whether the data object it is using is a private object or one common to all tasks. This can be tricky, since many programmers routinely use temporary pointers to objects that they place in global data space instead of allocating it on the stack for local use. It is important to recognize the differences between global data, such as items defined and allocated outside the scope of any C routine, and globally accessible data. In the first case, it is really local data, that is, data local to the client process itself and not accessible to other clients of the DLL. In the second case, it is accessible to all clients of the DLL, and that can fool you if you're not careful. You must be sure that globally accessible data items don't change value when you're not looking. Keeping items on a local stack frame is probably the safest bet. Items that are kept around without changing value are best placed in private client data space. You can easily indicate through the DATA statement in the EXPORT file which data segments you want to be allocated on a private per-client basis and which you want to be globally accessible. If a segment is marked as READONLY, then it is globally accessible. The Microsoft(R) C data group named CONST should always be marked as READONLY; this allows for only one copy of literal strings to be loaded for the entire DLL. Using Microsoft C There were some rumors floating around for a while that it was impossible to create DLLs with a C compiler because the stack segment (SS) did not equal the data segment (DS) upon entry into a DLL──clearly a run-time issue and not a compiler problem. Since the library has many routines that expect them to be equal, it appeared at first that creating DLLs in C was expressly forbidden, but this is not the case. There are some specific enhancements available in the Microsoft C Version 5.1 compiler that make writing C DLLs very easy. A new pragma, pragma data_seg, allows you to specify for any function that later loads its own data segment exactly which data segment to use. By specifying the data segment as #pragma data_seg (segment_name) you not only facilitate using DLLs, but you have more control over which data segment all initialized static and global data will reside in. The default data segment name if you don't specify one is the name used by DGROUP, which depends on the memory model you use. This is half of the solution of which data segment to use in the DLL. The other half is to specify the called function as one that uses the previously saved data segment with the _loadds keyword. Upon entry into a _loadds function, the current DS register is saved, the last one specified by the pragma data_seg directive is written into it, the function is executed, and the saved DS is restored upon exit. This is not such a new concept, since you've been able to use /Au as a compiler option for some time now, but this lets you specify some capability on a function-by- function basis. In order for the compiler to know, in advance, that the routine is going to be part of a dynamic-link library, the new keyword _export has been added. In particular, if the function has an I/O privilege level (IOPL) associated with it, then the number of words to reserve for the privilege level transition stack copy operation can be easily calculated at compile time if _export is used. In fact, if you use _export as part of your function definition, the number of words to reserve as indicated in the DEF file is ignored. When setting up the various data segments into their constituent types (SHARED and READONLY, for instance), you should also take a look at the map file produced from the link; some additional segments might be created that you hadn't thought about. In particular, some NULL segments are created for each group as _CONST and _BSS. So as not to confuse the linker, each member of the group should be specified within the SEGMENTS section of the EXPORT DEF file, and you need only mention the special segments, those with attributes different from the default setting of the DATA statement. Creating the Initialization Routine for Your C DLL Remember that the DLL, once passed through the linker, looks much like an EXE file. In fact, the same load routine used for your own client module loads the DLL itself. And, if you've defined an initialization routine within the DLL, it will be executed almost as if it were a standalone routine; it is called immediately after the DLL is loaded, and then only called upon subsequent loads of the DLL, if you specify so. You can easily tell the loader where the initialization routine is located by including a small assembly language routine as part of your DLL and linking it into the DLL. In fact, it is probably not a bad idea to have a module similar to the one in Figure 9 and to always give your DLL initialization routine the same name. The secret of the initialization routine is just that the only program the loader will find is the one that is addressed by the END START directive. The MSC compiler throws a small monkey wrench in your path. Meaning to be helpful, the compiler throws the _acrtused variable into each object module. This forces the linker to include some of the startup routines from the C run-time library into the eventual output of the linker, which the compiler thought was going to be a normal EXE file. To prevent this code from being loaded into your DLL, you should define the variable yourself, as external data: int _acrtused = 0x1234; or some number of particular meaning to you. Also, to have global data items show up in the named segment for the particular object module that you are linking, it should be initialized, declared as static, or both. Finally, you could also allocate static or global data using the keyword const along with the appropriate compiler switches. When writing your own DLL, you should use the -Gs switch on the MSC compiler to disable stack checking. Aside from the slight gain in efficiency──somewhat smaller code and one less function call per function──the DLL requires this since the stack segment is different for each client process and the size of the stack may vary on a per-client basis as well. In the few places where you really need stack checking, MSC provides you with an abundant set of pragmas and routines. MSC 5.1 includes some welcome additions to the run-time libraries package. These include a library that provides multithread support, a run-time library that is statically linked with your routines to provide for a DLL written almost entirely in C, and a run-time library that, in and of itself, is a dynamically linked library and works with EXE and other DLL libraries. The new DLL run-time libraries also allow you to use any of the functions that you've grown accustomed to. There are some subtle differences in the way certain things are handled in a multitasking/multiprocessing environment, but they are hidden from view in some of the header files. The errno variable, for example, is now a macro that translates into a function call. This is done because a table must now be used in the functions of the run-time library to enable a single run-time package to handle errors from multiple sources. By the look of things and how they operate, it is most likely safe to assume that semaphores were used throughout the library──to keep a call using a globally accessible variable from being clobbered by two client processes trying to use it simultaneously. This forces a heavy overhead in system calls to frequently called routines, but there is little choice in this matter. Remember that the libraries had to be written under a worst- case scenario, and you pay a penalty in speed and efficiency for the safety inherent in putting semaphores around the dangerous routines (see Figure 10). Certain idiosyncracies do exist in the C libraries, however. For example, the memory allocation functions alloc, malloc, and calloc follow the ANSI calling convention. However, the ANSI calling convention does not allow you to specify whether memory should be private or shared between different processes. Microsoft has yet to add functions to the library that allow this; this is the reason for the rather convoluted routines used in DLL_CHAT for dealing with allocating memory in a shared way. Conclusion With the introduction of DLLs in OS/2, a new programming environment has been created. Much like Windows and Presentation Manager programming, it has its own strict rules, but they make a great deal of sense once the underlying design concept and limits of the chip set and the appropriate portions of OS/2 are understood. You can avoid a lot of these sticky problems by piece-at-a-time programming: get as much of your program to work using routines in a more normal library using the library utilities and then move the routines out into a DLL. By adding the functionality and safeguards required for shared memory access between sessions and reentrancy problems, you can easily create a program that uses up less disk space, less memory space, and allows for interprocess communication in whatever manner you wish to design-not bad features for a new operating system to be written around. And once you've been through the maze of twisty little passages, it isn't so hard the next time to get through it rapidly and collect that treasure. The secret is just knowing a couple of key phrases and thinking ahead before you enter the maze. Figure 1: Once a DLL is created, use IMPLIB to create a LIB file from the EXPORT DEF file. The LIB file provides a dynamic-link reference that is built into the EXE file. The OS/2 loader then uses this information to resolve DLL references when the EXE file is executed. By providing both the DLL and LIB files, the internal format of DLL remains hiddenfrom the user of the DLL. Step 1: Create the DLL ┌───────────────────────┬──┐ │OBJ Files │ │ ╔═════╗ └───────────────────────┘ │ ┌──►║ DLL ║ ┌───────────────────────┐ │ ╔══════╗ │ ╚═════╝ │Static Library │ ├─────►║LINKER╟──────┤ └───────────────────────┘ │ ╚══════╝ │ ╔═════╗ ┌───────────────────────┐ │ └──►║ MAP ║(Optional) │Export Definition File │ │ ╚═════╝ └───────────────────────┴──┘ Step 2: Create the LIB file ┌───────────────────────┐ ╔══════╗ ╔══════════╗ │Export Definition File ├────────►║IMPLIB╟─────────►║ LIB FILE ║ └───────────────────────┘ ╚══════╝ ╚══════════╝ Step 3: Create an Application that Uses the DLL ┌───────────────────────┬─┐ │LIB File │ │ ╔═════╗ └───────────────────────┘ │ ┌──►║ DLL ║ ┌───────────────────────┐ │ ╔══════╗ │ ╚═════╝ │Application OBJ Files │ ├──────►║LINKER╟──────┤ └───────────────────────┘ │ ╚══════╝ │ ╔═════╗ ┌───────────────────────┐ │ └──►║ MAP ║(Optional) │Static Library │ │ ╚═════╝ └───────────────────────┴─┘ Step 4: Load the EXE File and Resolve DLL References ┌─────┬─┐ │ DLL │ │ └─────┘ │ ╔═══════════╗ ╔═════════════════════════════════╗ ├──────►║OS/2 LOADER╟──────►║ IN MEMORY EXECUTION OF EXE FILE ║ ┌─────┐ │ ╚═══════════╝ ╚═════════════════════════════════╝ │ EXE │ │ └─────┴─┘ Figure 2: Using IMPORT DEFINITION files is a better method for developing your DLL than using LIB files. There is no reason not to use IMPLIB for your finished DLL and its distribution. The EXPORT DEF file allows you to define everything that you need to. ┌──────────────────────────┬─┐ │ OBJ Files │ │ └──────────────────────────┘ │ ┌──────────────────────────┐ │ ╔═════════╗ ╔═══════╗ │ Static Library │ ├───►║ LINK ╟────┬──►║ DLL ║ └──────────────────────────┘ │ ╚═════════╝ │ ╚═══════╝ ┌──────────────────────────┐ │ │ ╔═══════╗ │ EXPORT DEF File │ │ └──►║ MAP ║(Optional) └──────────────────────────┴─┘ ╚═══════╝ ┌──────────────────────────┬─┐ │ IMPORT DEF File │ │ └──────────────────────────┘ │ ┌──────────────────────────┐ │ ╔═════════╗ ╔═══════╗ │ OBJ Files │ ├───►║ LINK ╟────┬──►║ EXE ║ └──────────────────────────┘ │ ╚═════════╝ │ ╚═══════╝ ┌──────────────────────────┐ │ │ ╔═══════╗ │ Static Library │ │ └──►║ MAP ║(Optional) └──────────────────────────┴─┘ ╚═══════╝ Figure 3: A Local Descriptor Table (LDT) contains a list of private descriptors accessible only to the local task. These descriptors can point to data segments, executable segements, call gates, and task gates. There can be at most 8192 descriptors, and each LDT can be, at most, 65,355 bytes long. Note that all LDT descriptors must reside, or be defined, in the Global Descriptor Table. GDT ╔══════════════════════════════╗ ┌─────────────────────────────────────┐ ┌──►║ LDT(1) ║ │GLOBAL DATA SEGMENT DESCRIPTORS │ │ ║DATA SEGMENT DESCRIPTORS ║ ├─────────────────────────────────────┤ │ ║EXECUTABLE SEGMENT DESCRIPTORS║ │GLOBAL EXECUTABLE SEGMENT DESCRIPTORS│ │ ║CALL GATE DESCRIPTORS ║ ├─────────────────────────────────────┤ │ ║TASK GATE DSCRIPTORS ║ │GLOBAL CALL GATE DESCRIPTORS │ │ / : / ├─────────────────────────────────────┤ │ ║ : ║ │GLOBAL TASK GATE DESCRIPTORS │ │┌─►║ LDT(N) ║ ├─────────────────────────────────────┤ ││ ║DATA SEGMENT DESCRIPTORS ║ │GLOBAL TASK STATE SEGMENT DESCRIPTORS│ ││ ║EXECUTABEL SEGMENT DESCRIPTORS║ ├─────────────────────────────────────┤ ││ ║CALL GATE DESCRIPTORS ║ │(LOCAL DESCRIPTOR TABLE DESCRIPTORS) ├─┘│ ║TASK GATE DESCRIPTORS ║ │ LDT(1) DESCRIPTOR │ │ ╚══════════════════════════════╝ / : / │ │ LDT(N) DESCRIPTOR ├──┘ └─────────────────────────────────────┘ Figure 4: DEF File Options and Parameters LIBRARY [name][init_type] NAME [appname][apptype] CODE [load][executeonly][iopl][conforming] DATA [load][readonly][instance][iopl][shared] SEGMENTS <segmentname> [CLASS 'classname'] [load][readonlyexecuteonly][iopl][conforming][shared] EXPORTS <name>[=internalname][@ordinal][RESIDENTNAME][pwords] IMPORTS [name=]<modulename>.<entryname> Figure 5: Additional DEF File Options STUB 'filename' Allows you to specify the name of a DOS 3.x file to be run if this file is run under DOS instead of under OS/2. PROTMODE Indicates that this file can only be run in protected mode. An aid to the linker. OLD This statement allows you to preserve the names associated with ordinal numbers in a multi-DLL environment. REALMODE The opposite of PROTMODE, this indicates the program can only be run in real mode, and is an aid to the linker. EXETYPE Insures that the specified operating system is the current one for the program. You can specify OS2, WINDOWS. HEAPSIZE Determines how much local heap must be allocated within the automatic data segment. STACKSIZE Allows you to specify how much space should be reserved in the stack segment when the program is run. Figure 6: A Typical IMPLIB Statement IMPLIB COM_STUF.LIB COM_INP.DEF COM_OUT_DEF Figure 7: ─────────────────────────────────────────────────────────────────────────── Also see: An overview of the way DLL_CHAT interfaces to its dynamic-link library ─────────────────────────────────────────────────────────────────────────── DEF File Format for DLL_CHAT IMPORTS CHATLIB.login IMPORTS CHATLIB.get_msg_cnt IMPORTS CHATLIB.send_msg IMPORTS CHATLIB.logout IMPORTS CHATLIB.get_msg Header File for DLL_CHAT /* Header file for DLL_CHAT */ struct gdtinfoarea{ unsigned long time; unsigned long milliseconds; unsigned char hours; unsigned char minutes; unsigned char seconds; unsigned char hundreths; unsigned timezone; unsigned timer_interval; unsigned char day; unsigned char month; unsigned year; unsigned char day_of_week; unsigned char major_version; unsigned char minor_version; unsigned char revision_number; unsigned char current_screen_group; unsigned char max_num_of_screengrps; unsigned char huge_selector_shift_count; unsigned char protect_mode_indicator; unsigned foreground_process_id; unsigned char dynamic_variation_flag; unsigned char maxwait; unsigned minimum_timeslice; unsigned maximum_timeslice; unsigned boot_drive; unsigned char reserved[32]; }; struct ldtinfoarea{ unsigned current_process_pid; unsigned parent_pid; unsigned priority_of_current_thread; unsigned thread_id_of_current_thread; unsigned screen_group; unsigned subscreen_group; unsigned current_process_is_in_fg; }; Code Listing for DLL_CHAT /********************************************************************* * DLL_CHAT.C - A demonstration program using a demo DLL * * (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal * * This is the main body of the CHAT program, interfacing with * and calling the DLL as if it were a bunch of routines * available with a far call: which it is! * * * Compile with: * cl /c chat.c * link chat,chat,,slibce+doscalls,chat * */Remember: move the DLL itself into your DLL library directory ********************************************************************* #include <stdio.h> #include <stdlib.h> #include "chat.h" #define TRUE 1 #define FALSE 0 #define OK TRUE #define NOT__OK FALSE #define MAX_MSG 100 #define MAX_MSG_LEN 80 #define NULLP (void *)NULL /* The following OS/2 system calls are made in this module: */ extern far pascal dosexitlist(); extern far pascal dossleep(); /* The following DLL system calls are made in this module: */ extern far _loadds pascal login(); extern far _loadds pascal logout(); extern far _loadds pascal get_msg_cnt(); extern far _loadds pascal get_msg(); extern far _loadds pascal send_msg(); /******************************************************************** * This is where the messages are stored, once received and * formatted. This could probably be replaced easily with a call * to calloc(), but then we wouldn't have to block on being a * background process ********************************************************************/ char msg_array[MAX_MSG + 1][MAX_MSG_LEN]; /* Must be global so that before_death() can access it to logout */ int my_id = 0; #define MAX_SLEEP 2 /******************************************************************** * before_death() * * Called the coins are lowered onto the eyes of this invocation * of CHAT. Any exit will cause this routine to be called. After * this routine calls the DLL logout procedure, it removes the * next exitroutine in the exit list. ********************************************************************/ void far before_death() { logout(my_id); dosexitlist(3, before_death); } /******************************************************************** * main() * * After logging in (which returns a unique login id), the * before_death() routine is added onto the exitlist. Then the * main loop: * * If there are any messages, read them into out memory buffer * (provided there is room) and kick up the count. After * retrieving all the msgs which will fit, call the display * routine for each msg. Zero the count of messages when done. * * Every couple of seconds, sleep for a little while (as if a * human typing on a keyboard), then send the message to all * other members of CHAT. * * CHAT can only be exited (in its current form) with a * control-C or error condition. ********************************************************************/ main() { int msg_cnt = 0; int msg_id = 0; int l_cnt; char tmp_buf[MAX_MSG_LEN]; int mcnt; printf("Logged into CHAT as user:%d\n", my_id = login()); dosexitlist(1, before_death); while (TRUE) { for (m_cnt = get_msg_cnt(my_id); m_cnt ; m_cnt-) { get_msg(my_id, (char far *)tmp_buf); if (msg_cnt <= MAX_MSG) sprintf(msg_array[msg_cnt++],"(%d)%s",my_id,tmp_buf); } if (ok_to_disp()) { for (lp_cnt = 0 ; lp_cnt < msg_cnt; lp_cnt++) disp_msg(msg_array[lp_cnt]); if (msg_cnt > MAX_MSG) disp_msg("Looks like you might have lost some") disp_msg(" messages while you were away\n"); msg_cnt = NULL; } if (rand() % (my_id + 1)) { dossleep((long)(rand() % MAX_SLEEP) * 1000L); sprintf(tmp_buf, "Test message #%d from Session #%d\n", msg_id++, my_id); if (send_msg(my_id, (char far *)tmp_buf) == NOT_OK) printf("?Can't send a message....\n"); } } } disp_msg(ptr) char *ptr; { printf("%s", ptr); fflush(stdout); } extern far pascal dosgetinfoseg(); ok_to_disp() { struct gdtinfoarea far *gdt; struct ldtinfoarea far *ldt; unsigned gseg; unsigned lseg; dosgetinfoseg((char far *)&gseg, (char far *)&lseg); gdt = (struct gdtinfoarea far *)((long)gseg << 16); ldt = (struct ldtinfoarea far *)((long)lseg << 16); return( gdt->foreground_process_id == ldt->parent_pid); } ─────────────────────────────────────────────────────────────────────────── An overview of the way the example program, DLL_CHAT, interfaces to its dynamic-link library. ─────────────────────────────────────────────────────────────────────────── ┌──────────────────────────────────────────────────────────────────────────┐ │ DLL_CHAT.C (First session of DLL_CHAT) │ ├─────────────────┬───────────────────┬──────────────────┬─────────────────┤ │ login() │ get_msg_cnt() │ get_msg() │ send_msg() │ └───┬────────────┴────┬─────────────┴─────┬───────────┴───┬────────────┘ │ │ my_id │ my_id │ my_id │ │ my_id │ msg_cnt │ │ │ status │ │ │ │ ptr │ ptr │ │ │ │ │ │ │ │ │ ┌───▼───────┴─────┬────▼────────┴─────┬─────▼──────┴─────┬───▼────────┴────┐ │ login() │ get_msg_cnt() │ get_msg() │ send_msg() │ ├─────────────────┼───────────────────┼──────────────────┼─────────────────┤ │(returns first │(returns msg cnt │ (copies first │(allocates memory│ │unused login │of outstanding │ outstanding │as required for │ │slot) │messages for │ msg for this │message structure│ │ │login id) │ id into array │and message, then│ │ │ │ at ptr) │copies msg at ptr│ │ │ │ │into this memory)│ ├─────────────────┴───────────────────┴──────────────────┴─────────────────┤ │ DLL_CHAT.LIB (Other sessions of DLL_CHAT) │ └──────────┬──────────────────┬──────────────────┬───────────────┬─────┘ │ │ │ │ │ │ │ │ │ my_id my_id │ my_id │ my_id │ │ │ │ msg_cnt │ │ │ status │ │ │ │ ptr │ ptr │ ┌───┴───────▼─────┬────┴────────▼─────┬─────┴───────▼────┬────┴──────▼─────┐ │ login() │ get_msg_cnt() │ get_msg() │ send_msg() │ ├─────────────────┴───────────────────┴──────────────────┴─────────────────┤ │ DLL_CHAT.C (Other sessions of DLL_CHAT) │ └──────────────────────────────────────────────────────────────────────────┘ Figure 8: DEF File for CHATLIB LIBRARY CHATLIB INITGLOBAL DATA SINGLE SHARED EXPORTS login EXPORTS get_msg_cnt EXPORTS send_msg EXPORTS logout EXPORTS get_msg Code listing for CHATLIB /********************************************************************** * CHATLIB.C - A demonstration Dynamic Link Library * * (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal * * This DLL, when used with the CHAT program acts as a central * repository for all messages being passed * * Compile with: * * cl /AL /Au /Gs /c chatlib.c * * Note - Though broken here the following two lines are * entered as one line: * * link startup+chatlib+chatmem,chatlib.dll,,llibcdll+doscalls, * chatlib/NOE * * Remember to move the DLL itself into your DLL library * directory * ********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <malloc.h> #include <dos.h> #define TRUE 1 #define FALSE 0 #define OK TRUE #define NOT_OK FALSE #define NULLP ((char *)NULL) #define GET_SEM (dossemrequest(&semaphore, -1L)) #define RLS_SEM (dossemclear(&semaphore)) /* The following OS/2 system calls are made in this module: */ extern far pascal dossemrequest(); extern far pascal dossemclear(); /* The following external calls are made in this module: */ char *my_calloc(); /* This semaphore used to coordinate access to "critical" areas */ long semaphore = 0; /******************************************************************* * This structure defines the members of the linked list * which starts at master_ptr. Once a structure is allocated, * it is never released, although the character array member * msg_ptr points to will be released when the message is no * longer needed ********************************************************************/ #define MSG struct _msg MSG{ MSG *next_ptr; /* Point to next MSG, or NULLM */ char *msg_ptr; /* Point to the actual message */ int msg_len; /* length of the message - optional */ unsigned f_word; /* flag_word. When set to 0xfff */ /* all chat members have seen this */ /* message, so it can be freed */ }; int flag_word = 0xffff; /* This is the word that f_word is */ /* set to initially. It is modified */ /* so that each bit is "off" if that*/ /* "user" is logged in */ #define NULLM ((MSG *)NULL) MSG *master_ptr = NULLM; /* Where the linked list begins */ /******************************************************************** * new_msg_struct(pointer to last MSG) * * Allocates a new MSG, initializes the contents of the * structure, and sets the linked list if not the first time * called (last_msg != NULLM) * * Returns a pointer to the structure allocated, NULLM if an error ********************************************************************/ MSG *new_msg_struct(MSG *last_msg) { MSG *tmp_ptr; if ((tmp_ptr = (MSG *)my_calloc(sizeof(MSG), 1)) == NULLM) return(NULLM); tmp_ptr->next_ptr = NULLM; tmp_ptr->msg_ptr = NULLP; tmp_ptr->msg_len = NULL; tmp_ptr->f_word = flag_word; if (last_msg != NULLM) last_msg->next_ptr = tmp_ptr; return(tmp_ptr); } /******************************************************************** * initialize() * * Called either by the initialization routine of the DLL, or by * the first login. It allocates the first MSG structure, then * allocates and sets up for the next member ********************************************************************/ void far _loadds pascal initialize() { if ((master_ptr = new_msg_struct(NULLP)) == NULLM) { printf("Couldn't allocate MSG memory for header...\n"); exit(1); } new_msg_struct(master_ptr); } /******************************************************************** * login() * * If the master MSG structure hasn't been allocated already by * an earlier call to initialize() (by the DLL initialize * routine), then make the call now. Memory has already been * allocated, therefore, so now give ourselves access to the * segment we've allocated. * * Get the next free bit slot in the flag word, set it to * indicate it's in use, then return our login id. ********************************************************************/ int far _loadds pascal login() { int log_id; int tmp_msk; if (master_ptr == NULLM) { printf("Init in login\n"); initialize(); } my_getseg(); GET_SEM; for (log_id= 0 ; log_id < 16 ; log_id++) { tmp_msk = mask(log_id); if (flag_word & tmp_msk) { flag_word &= ~tmp_msk; RLS_SEM; return(log_id); } } RLS_SEM; printf("Login slots all used up!\n"); exit(1); } /******************************************************************** * get_msg_cnt(login_id) * * For every MSG structure in the linked list with an associated * message attached to it, increment a counter if the id in * question hasn't received it yet, then return that counter * when we fall off the end. ********************************************************************/ int far _loadds pascal get_msg_cnt(int id) { MSG *tmp_ptr; int tmp_cnt = 0; int tmp_msk = mask(id); GET_SEM; for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr) { if (!(tmp_ptr->f_word & tmp_msk)) if (tmp_ptr->msg_len) tmp_cnt++; } RLS_SEM; return(tmp_cnt); } /******************************************************************** * send_msg(login_id, pointer_to_message) * * If there are no other "chatter's" logged in, simply return. * (Flag_word or'ed with our mask would be 0xfff) * * Find a free MSG structure (guaranteed to have at least one, * since every write leaves a free one allocated if its the last * one in the linked list). * * Allocate memory for the message, copy the message into it, * then assign the pointer in the structure and the length of * the message. Finally, allocate a new structure if required. ********************************************************************/ int far _loadds pascal send_msg(int id, char far *ptr) { MSG *tmp_ptr = master_ptr; int tmp_len = strlen(ptr) + 1; if ((flag_word | mask(id)) == 0xffff) return(OK); GET_SEM; while (tmp_ptr->msg_len) tmp_ptr = tmp_ptr->next_ptr; if ((tmp_ptr->msg_ptr = my_calloc(tmp_len, 1)) == NULLP) { printf("Can't allocate %d bytes for msg\n", tmp_len); RLS_SEM; return(NOT_OK); } strcpy(tmp_ptr->msg_ptr, ptr); tmp_ptr->msg_len = tmp_len; tmp_ptr->f_word = (flag_word | mask(id)); if (tmp_ptr->next_ptr == NULLM) { if (new_msg_struct(tmp_ptr) == NULLM) { printf("Can't allocate new MSG_header\n"); free_msg(tmp_ptr); RLS_SEM; return(NOT_OK); } } RLS_SEM; return(OK); } /******************************************************************** * logout(login_id) * * Mark every mesage as read (freeing them if now "totally" * read),reset the flag word, and then indicate that the logout * worked. ********************************************************************/ int far _loadds pascal logout(int id) { MSG *tmp_ptr; int tmp_msk = mask(id); GET_SEM; for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr) mark_msg(id, tmp_ptr); flag_word |= mask(id); RLS_SEM; printf("In logout ... Hit a Key:");fflush(stdout); getch(); printf("\n\n\n\n"); return(0); } /******************************************************************** * get_msg(login_id, pointer to buffer) * * Find the first message the login_id hasn't read, then strcpy * it into the buffer supplied. Then mark the message as read * (freeing as required). ********************************************************************/ int far _loadds pascal get_msg(int id, char far *ptr) { MSG *tmp_ptr = master_ptr; int tmp_msk = mask(id); GET_SEM; for(tmp_ptr = master_ptr; tmp_ptr; tmp_ptr = tmp_ptr->next_ptr) { if (!(tmp_ptr->f_word & tmp_msk)) { strcpy(ptr, tmp_ptr->msg_ptr); mark_msg(id, tmp_ptr); RLS_SEM; return(TRUE); } } RLS_SEM; return(FALSE); } /******************************************************************** * mark_msg(login id, pointer to message structure) * * Mark our bit in the MSG f_word as set. If then set to * 0xffff, the message is "totally" read, so free it. * ******************************************************************* * * free(pointer to message structure) * * If there is a string associated with this structure, free the * memory so used, then zero out the pointer and the msg_len ********************************************************************/ mark_msg(int id, MSG *ptr) { ptr->f_word |= mask(id); if (ptr->f_word == 0xffff) free_msg(ptr); } free_msg(MSG *ptr) { if (ptr->msg_ptr) my_free(ptr->msg_ptr); ptr->msg_ptr = NULLP; ptr->msg_len = NULL; } /* GENERAL ROUTINES */ /* This routine merely returns with the bit corresponding * to our login set */ mask(int log_id) { return(1 << (log_id - 1)); } Additional Module for CHATLIB /* CHATMEM.C - Memory allocation routines for shared DLL memory * * (C) 1988, By Ross M. Greenberg for Microsoft Systems Journal * * This module contains three functions. Allocation of memory, * de-allocation of memory and the getseg call. * * The current ANSI calloc/alloc/malloc sequence does not allow * for an additional parameter to specify if memory requested * through these functions is to be sharable or private. * Therefore the MSC library calls all allocate private memory. * * These routines allocate a 64K chunk of memory, requested from * OS/2 as a sharable chunk, then dole it out using the DosSub * allocation calls. * * Only one 64K chunk is allocated: if more memory is desired an * additional call to dosallocseg would have to be made and all * sessions already logged in given access to the chunk. Out of * laziness, that was not done for these demonstration * routines. * * * Compile with: * * cl /AL /Au /Gs /c chatmem.c * * Note - Though broken here, the following two lines are * entered as one line: * * link startup+chatlib+chatmem,chatlib.dll,,llibcdll+doscalls, * chatlib/NOE * * Remember to move the DLL itself into your DLL library * directory ********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <malloc.h> #include <dos.h> #define TRUE 1 #define FALSE 0 /* The following OS/2 system calls are made in this module: */ extern far pascal dosallocseg(); extern far pascal dosfreeseg(); extern far pascal dossuballoc(); extern far pascal dossubset(); extern far pascal dossubfree(); extern far pascal dosgetseg(); extern far pascal dossemrequest(); extern far pascal dossemclear(); #define NULLP (char *)NULL /* This semaphore is so that we don't hurt ourselves as we * allocate and deallocate memory */ long memory_semaphore = NULL; /* This is the actual selector which the 64K dosallocseg() * call returns */ unsigned major_selector = NULL; /******************************************************************** * my_calloc(number_of_items,size_of_item) * * Emulates the more typical calloc call, but returns memory * which can later be allocated as sharable. * * After the first call (which causes a 64K chunk to be * allocated), all subsequent calls cause a dossuballoc call to * be made, and a long pointer to the returned memory to be * created and returned * * The 64K chunk must be initialized by the dossubset call * before it can be used by other dossub functions. * * Because the dossubfree call requires a size, the size * requested plus the sizeof an int is actually allocated and * the size of the total request is then stored in the first two * bytes of the returned character array. The ptr returned, * however, is this memory location plus the initial sizeof and * int-therefore the bookkeeping is transparent to the * application task. ********************************************************************/ char * my_calloc(size1, size2) int size1; int size2; { unsigned long selector; int stat; char *ptr; int sizeit = (size1 * size2) + sizeof(int); dossemrequest(&memory_semaphore, -1L); if (!major_selector) { if (stat = dosallocseg(0, &major_selector, 3)) { printf("dosalloc error:%d\n", stat); dossemclear(&memory_semaphore); return(NULLP); } if (stat = dossubset(major_selector, 1, 0)) { printf("Error in dossubset:%d\n", stat); dossemclear(&memory_semaphore); return(NULLP); } } selector = 0; if (stat = dossuballoc(major_selector, &selector, sizeit)) { printf("dossuballoc error:%d\n", stat); dossemclear(&memory_semaphore); return(NULLP); } dossemclear(&memory_semaphore); ptr = (char *)(((long)major_selector << 16) + (long)selector); memset(ptr, (char)NULL, sizeit); *(int *)ptr = sizeit; return(ptr + sizeof(int)); } /******************************************************************** * my_free(pointer_to_a_character_array_previously_my_calloc'ed) * * Subtract sizeof an int from the pointer, dereference as an * int, then free that number of bytes. * ********************************************************************/ my_free(ptr) char *ptr; { int stat; ptr -= sizeof(int); dossemrequest(&memory_semaphore, -1L); if (stat = dossubfree(major_selector, FP_OFF(ptr), *(int *)ptr)) { printf("Error freeing: %lx\n", ptr); exit(1); } dossemclear(&memory_semaphore); } /******************************************************************** * my_getseg() * * Causes the memory affilaited with the major_selector to * become accessible to this process. ********************************************************************/ my_getseg() { int stat; if (stat=dosgetseg(major_selector)) printf("Error on getseg:%d\n", stat); return(stat); } } Figure 9: A DLL Initialization Routine EXTRN INITROUTINE:FAR ASSUME CS:_TEXT _TEXT SEGMENT BYTE PUBLIC 'CODE' START PROC FAR call INITROUTINE ; the real initialization routine ret START ENDP _TEXT ENDS END START ; defines auto-init entry point Figure 10: An Example of Semaphored Printf() vs. "Free-style" Printf() Consider the two code fragments below. In the semaphore version of the code, individual printf()'s are not interrupted──whatever they are printing will be printed to completion with other calls to the same printf() routine waiting on a semaphore before proceeding. This semaphore is reset when the current process using the library function is exiting from the library function. So-called "free-style" functions, on the other hand, can be interrupted at any time-even in the middle of printing. Therefore, you might have two (or more!) streams of data merged into some unreadable mess. It's worse, however, if global data is in use by the interrupted free- style function. Such data can be modified by the "interrupter," and could cause unpredictable results. Program 1 Program 2 main() main() { { printf("Task1:%d\n", cnt++); printf("Task2:%d\n", cnt++); } } Semaphored Free Style Task1:1 Task2:1 Task2:1 Task1:1 Task1:2 Task1:2 Task1:3 Task1:3 Task1:4 Task2:2 Task2:2 Task2:3 Task2:3 Task2:4 Task1:4 Task1:4 ∙ ∙ ∙ ∙ ∙ ∙ What would the apparent results be if two sessions, each normally independent of the other, but sharing a Dynamic-Link Library version of printf(), were to try to execute the printf() routine in a semaphored versus a free-style approach? The two program fragments above, each running as a separate session, show the differences. In the semaphore version, each call made to printf() by each session is run to completion, although the "set" might be interrupted. In the free-style version, there is not even a guarantee that any given printf() will finish before it is interrupted. The same would also hold true for multithreads of the same session using a common printf() routine. The motto? "If in doubt, use semaphores." ████████████████████████████████████████████████████████████████████████████ New Compiler Technology Boosts Microsoft QuickBASIC 4.0 Productivity Augie Hansen☼ Compiler technology takes a quantum leap forward with the most recent version of Microsoft(R) QuickBASIC. If you already have Microsoft QuickBASIC Version 3.0, you received an invitation to upgrade to Version 4.0 for a nominal charge, but don't let the word "upgrade" fool you. The Microsoft QuickBASIC 4.0 compiler is a brand new product. The compiler, as in earlier versions, comes in two flavors: a traditional command-line oriented compiler, BC, and a speedy in-memory integrated environment, QB, that truly deserves the label "instant environment." Microsoft QuickBASIC 4.0 is based on threaded pseudocode technology that has been under development at Microsoft for a number of years. The p-code technology, as it is called, draws upon the threaded-code interpreter environments used by FORTH programming products for many years, but with some important extensions and improvements. This article will take a close look at threaded p-code and the way it is applied to Microsoft QuickBASIC 4.0. We'll show you how the tightly coupled editor, p-code interpreter, and debugger all work together to provide instant BASIC programming that will boost your productivity. The development of a video terminal emulation program provides an example of the program in action. Features/Enhancements The p-code technology used in Microsoft QuickBASIC 4.0 offers a programming environment with startling advantages over earlier integrated environments. When you type a program line and press Enter or the down arrow key, the line is immediately compiled to intermediate p-code. At the same time, the line is formatted by the editor so that all BASIC keywords are displayed in uppercase, and all operators are surrounded by spaces. The line is also checked in real time for syntax errors. If any are found, they are reported in a dialog box; do not ignore such errors. The system lets you move off the line after it has told you about the problem, but be sure the line is syntactically correct before moving on to some other task. The net result is that a program is always ready to run virtually instantly. You can select Start in the Run menu (or press Shift-F5) at any time to see the results of your programming labors. The first time you start a program, it is translated from editable intermediate p-code to executable threaded p-code, which is as close to unoptimized native machine code as you can get without actually being there. This is as far as the code can be taken so that it can still be returned to the ASCII representation for editing. You can stop program execution at any time, change the values of variables, add or delete any number of lines, and then return to running the program in the blink of an eye. The trick is that Microsoft QuickBASIC 4.0 works on one small unit of code at a time; it decompiles only what you are going to be working on. If, for example, you are working on changes to a SUB (subprogram), only that SUB is returned to an editable intermediate p-code state. When you finish your changes, you simply select Run-Continue (or press F5) to continue executing where you left off before editing. Since all variable values are preserved, your program resumes execution in exactly the same state it was in before you interrupted it. Built-in debugging, as well as the syntax checking already mentioned, is patterned after Microsoft CodeView(R). Within the Microsoft QuickBASIC 4.0 instant environment, you can set watch expressions, watchpoints, and breakpoints, single-step program execution, and even backtrack through previously executed statements. The separate command-line version of the Microsoft QuickBASIC 4.0 compiler, the BC.EXE program, provides three important benefits. To begin with, it lets you produce disk-based executable programs in either standalone form or smaller executable versions that work with a single run-time module. Second, it provides a debugging code option that lets you use CodeView for source level debugging of your programs with the best available full-screen debugger. Finally, it makes mixed-language programming possible, allowing you to use existing routines written in Microsoft C (Version 5.0 and QuickC(TM)), Pascal, FORTRAN, and the Macro Assembler (MASM). Recursion has been added to this version of Microsoft QuickBASIC. Although recursion offers no substantial code size or speed advantages over equivalent iterative programming methods, some types of problems, such as the Quick Sort procedure and DOS-directory traversal, lend themselves conceptually to recursive solutions. Another valuable addition to BASIC is the user-definable data type, which lets you define structured data types that are equivalent to C structures and Pascal records. Records are important in the creation of composite data types which simplify organizing and tracking data and ease the burden of dealing with random-access files. Microsoft QuickBASIC 4.0 supports only IEEE number formats. The Microsoft binary format used in earlier versions of the compiler has been dropped, but functions have been added in order to permit data stored in the Microsoft binary format to be translated to the IEEE format. Also, automatic detection and use of a numeric data processor (NDP), a math coprocessor, has been added. Executable programs can run on any IBM- compatible machine regardless of whether it has an NDP. If one is present, it is used to obtain the greatest execution speed; if not, math operations are emulated in software. There is no longer any need to have two separate versions of programs for different hardware configurations. The help system now has three levels. One level is visible on the status line at the bottom of the screen whenever a menu or command selection is visible at the top. You can also get a series of general help panels by pressing F1 or pressing Alt-H and choosing the General... option. Best of all, a new context-sensitive help feature gives you on-line access to BASIC manual pages for all statements, functions, and other keywords. You simply move the editing cursor into the item of interest, and press Shift-F1 or select the Topic option from the Help menu. The view window shrinks as much as necessary to display the help frame above your source code (see Figure 1☼). The editor is still active while the help information is on the screen, letting you continue to edit while looking at the syntax of a statement. After you have viewed the help window, press Esc to close it. For those of you who are beginning to program under an OS/2 system, Microsoft QuickBASIC 4.0 will be provided as a real-mode option under the BASIC compiler Version 6.0. The BASIC compiler 6.0, which is completely compatible with Microsoft QuickBASIC 4.0, is essentially what Microsoft will provide for OS/2 BASIC programming. All OS/2 system calls will be supported directly from BASIC in the OS/2 version of the compiler. Program Development The Microsoft QuickBASIC development cycle is shown in Figure 2. The figure compares the Version 3.0 development cycle with the Version 4.0 cycle. As you can see, the compile step appears to be missing from the 4.0 cycle, at least from your perspective as a developer. The compile step really is there, of course, but it happens so fast that it's not usually visible to you. When you first read in a new module's source from disk, you only see a compile delay as the statements are parsed and compiled, and even that process is fast. Creating a new program or editing a program that is already in memory produces an interpreter- like operation. Transitions among the three steps are bidirectional. You can switch from the "run" mode to the "edit" mode and back easily and quickly, or between run and "debug", or edit and debug. The seamless integration of the edit, run, and debug steps into an instant environment is something to behold. Microsoft QuickBASIC 4.0 is heavily geared toward modular programming. The recommended units of modularity are procedures called subprograms (SUBs) and functions (FUNCTIONs). FUNCTIONs are new to the environment and should not be confused with the DEF FNs of older BASICs. Subprograms have many of the attributes of BASIC subroutines, but you don't have to GOSUB much anymore. Indeed, there are many reasons favoring the use of subroutines instead of traditional BASIC subprograms. SUBs accept parameters and provide for local storage. A variable defined inside a subprogram is not visible outside that subprogram unless you go out of your way to make it visible by sharing it. You can pass values into a SUB via parameters that use the call-by-reference method, which makes the address of a parameter available to the SUB so that the statements in the SUB can affect the parameter itself. If you want call-by-value parameter passing, which means that a SUB can use the value passed as a parameter but cannot access the original storage location, you can simulate it by assigning parameters into intermediate variables and passing the intermediates. You can also surround the expression or variable identifier in parentheses to force call-by-value parameter passing. Functions are similar to subprograms, except that they return a value by assigning the value to the function name. You can use functions in expres- sions just as you use BASIC intrinsic functions, such as the INPUT$ function. You can declare variables local to either subprograms or functions as STATIC so that they retain their values from one call to the next. Alternatively, you can declare them without the STATIC keyword, in which case all local variables are dynamic. They are managed on the stack and exist only while the procedure is executing. When a program is in memory, it typically has code at two levels: module level and procedure level. Module-level code is at the top level. Items declared and defined at module level are global and are available throughout the module. Procedure-level code is contained within procedures and has local scope by default. Statements within procedures can access module-level items if those items are declared in DIM or COMMON statements. Procedures can also declare SHARED access to module-level items. Quick Libraries are another important tool for writing modular programs. You can create procedures in any Microsoft language, compile them to OBJ files, and collect them into Quick Libraries (QLB extension) that can then be loaded into the Microsoft QuickBASIC environment. The procedures in loaded Quick Libraries are immediately available for use in the active program. You can also ship libraries to other Microsoft QuickBASIC 4.0 users to incorporate into their programs. You can create Quick Libraries and standard standalone libraries (LIB extension) either from the Microsoft QuickBASIC instant environment or from the command line. However, the one-step process of the instant environment makes creating libraries straightforward and simple. Threaded P-Code As we have already seen, Microsoft QuickBASIC 4.0 is both a compiler and an interpreter. The compiler keeps your program in a pseudocode state that is about 90 percent machine code. This p-code retains enough information to allow reconstruction of your original source while having only one internal representation of the program in memory at a time. This is crucial because the program is always ready to be run, debugged, and edited without a significant amount of translation time, and almost no execution speed penalty. And because of the single representation of your program instead of separate source, object, and executable representations in memory, there is more room for your program. The threaded p-code interpreter is not a discrete program entity, but rather a distributed interpreter. When you key in a BASIC source line, Microsoft QuickBASIC immediately checks its syntax. If errors are found, you are obliged to remove them before proceeding. If the syntax is good, the source line is translated into a threaded p-code representation that consists of a series of executor addresses, each of which is simply the memory address of an executor. An executor is a routine that participates in doing the work of a BASIC statement. A single BASIC statement usually results in a series of executors being chained together, or threaded. The threading is achieved by the distributed interpreter, which consists of two machine instructions. The cost is four bytes and seven CPU cycles per executor. Each executor looks like this: Executor routine + LODSW ES + JMP AX That's it. Your program is executed simply by chaining together all the executors that are needed to carry out its tasks. There is no wasteful CALL/RET instruction overhead, so your program runs quickly-about as fast as unoptimized machine code will run. Figure 3 shows how Microsoft QuickBASIC handles your program. The environment has three states called "parsed," "symbolic," and "threaded." A threaded p-code system with only the parsed and threaded states would appear slow because the time needed to translate from parsed code to threaded p-code accumulates. However, by introducing the intermediate symbolic state, Microsoft has achieved the goal of instant program changes. The parsed state is the result of parsing your BASIC statements, either from keyboard input or from disk files. Most of the syntax checking is done during this translation. Microsoft QuickBASIC 4.0 has the portions of your program that are affected by editing changes to COMMON, SHARED, or DEF statements in this state while the changes are made. Translation back to the threaded state occurs at a rate of about 60,000 lines per minute on an 8-MHz IBM(R) PC/AT(R). The symbolic state is distinguished by the efficient symbol table information that permits fast data access. Most editing that does not involve global impacts is done in this state. Only the portions of your program that are affected by editing need to be brought to this state. Your program is run and debugged in the threaded state; it is in the threaded state most of the time. Translation from symbolic to threaded state involves inserting type checking code, binding procedure calls, control structures, labels to memory addresses, linking COMMON data, and generating the p-code executor addresses. When you make nonglobal changes to your program, only the affected portions are returned to the symbolic state. Before the program can run again, the edited portions are translated back to threaded state. The round trip from threaded to symbolic and back to threaded occurs at about 150,000 lines per minute on a typical 8MHz AT machine. Because you usually deal with a single program item at a time, such as a FUNCTION or SUB, this translation is virtually instantaneous. Debugging Made Easy The tight coupling between the run step and the edit step (see Figure 1☼) is also true of the relationship of the run and debug steps. The built- in debugging feature is a substantial subset of the Microsoft CodeView full-screen symbolic debugger. It lets you observe any variable or set of variables in your program, single-step execution, set breakpoints, trace the stack, and get a statement execution history. If you detect a run- time error condition, you can use the editor to make needed changes quickly and return immediately to the run step. SET NEXT STATEMENT is also available, and allows the programmer to set the next statement to be executed. You can set a watchpoint, which is an expression that causes program execution to stop when it becomes TRUE. The debugger opens a "watch window" just above the view window to your source code and displays all watch information in it. To observe the value of a variable or an expression as you run your program, you can set watch expressions and breakpoints. The debugger displays the values of watch expressions in the watch window, stopping at each breakpoint so you can see the values before continuing execution. You can also single-step program execution (F8) to track values as each statement is executed. In this mode, all statements in SUBs and FUNCTIONs are single-stepped. You can also do procedure stepping (F10), which treats each SUB and FUNCTION as if it were a single statement. In addition, you can turn on Trace mode, which will do a slow-motion single- step through your program, making flow analysis and diagnosis a breeze. A history feature, enabled by either Trace On or a separate History On activation, allows you to record up to the last 20 lines executed in your program. You can then scroll backward and forward through the lines to examine program execution. This is particularly useful when you need to follow branching patterns of IF and SELECT CASE statements and to observe looping statements. If the built-in debugging features aren't enough for your needs, you can also create EXE files with debugging code that let you use CodeView for greater access to the most intimate details of your program. CodeView is not supplied with Microsoft QuickBASIC, but it comes with MASM and other Microsoft language products. To debug pure BASIC code, the built-in debugger is probably more powerful than CodeView. However, CodeView compatibility is invaluable for mixed-language programming. Smart Editor The editing capabilities of Microsoft QuickBASIC 4.0 are quite impressive. Syntax-directed editing, "pretty printing" of source code lines, and WordStar(R) editing-command compatibility are among the editor's many features. Microsoft QuickBASIC breaks a BASIC program module into editable and compilable units, such as SUBs and FUNCTIONs. As you create or edit, the editor keeps track of these units and displays information about them on demand in a program item display window. You select the program item you want to edit by moving a highlight and choosing one of several commands. The editor supports split-screen editing so that you can edit one item while viewing another. It also allows you to switch to an immediate window to execute BASIC statements in what is the equivalent of direct mode in earlier versions of the BASIC interpreter. The split screen capability also works when debugging, which effectively allows you to view both the calling code and the called code simultaneously. You can use the editor as a standard text editor for ASCII files if you wish. You simply turn off the BASIC Syntax Checking feature in the Edit menu in order to disable the sensitivity to BASIC language statement syntax. Command-line Compiler The Microsoft QuickBASIC instant environment is great when learning to program in BASIC, experimenting, and doing modest program development tasks. Should you get involved in large program development projects, perhaps working with other programmers, or if you need to do mixed- language programming using C, Pascal, FORTRAN, or MASM in addition to BASIC, you will probably need to use the command-line compiler. The BC program, in concert with MAKE, LINK, and LIB, provides the traditional compiler you need to handle large projects. Figure 4 shows how the programs work together to produce executable programs and libraries. Note that you can even use the QB instant environment as a front-end development tool for the BC compiler. When you ask QB to produce an EXE file, it calls on BC and the linker, LINK, to translate your program source into optimized machine instructions. Mixed-language programming involves the use of Microsoft-format object libraries that can be linked into an executable program. Both the QB and BC programs let you create object library (LIB) files. The LINK program supplied with Microsoft QuickBASIC can link object modules produced by the C, Pascal, FORTRAN, and BASIC compilers, as well as by MASM. Figure 5 shows the way an executable program might be created by combining Microsoft QuickBASIC 4.0, QuickC or C 5.0, and MASM 5.0 object modules. The latest CodeView debugger can debug such a mixed-language program at the appropriate source level as long as you specify that debugging code be included at each compilation and assembly step. You can also create Quick Libraries to be used by Microsoft QuickBASIC. User-defined Types Microsoft QuickBASIC 4.0 adds user-defined types to its growing list of modern programming features. This is something that BASIC has needed for a long time. A user-defined type is a data type that contains more than one element. C has structures and unions, and Pascal has records; now Microsoft QuickBASIC has TYPEs. User-defined types let you model program data to fit real-world conditions. Instead of having lots of separate variables that appear to have no relationship, you can now define data types that clearly show data relationships. Each element in a defined type must have a type name of INTEGER, LONG, SINGLE, DOUBLE, STRING (with a length specifier), or another user-defined type. For example, you can easily mirror the structure of an inventory-control card. Use the keyword TYPE to define the structure of the data, as shown in Figure 6. Having defined the type, you can declare variables of this type by using the DIM, REDIM, COMMON, STATIC, and SHARED statements. To reserve storage for a set of 100 cards, you declare an array of items of this type: DIM CardTable(100) AS_ CardType You must define all user types at the module level. However, you can declare variables of the defined type at both the module level and the procedure level. Within a defined type, each string must have a fixed length, which is specified on a string-by-string basis by the asterisk number modifier. Thus, PartNumber AS STRING * 20 allocates a 20-character string to the part number. In programs, you can use the new functions LTRIM$ and RTRIM$ to remove leading and trailing blanks, respectively, in fixed-length strings. The Microsoft QuickBASIC documentation favors the use of the term "record" to describe a variable of user-defined type. To access a record element, use the period operator. The following statement assigns a value of 20 to the StockQty element of the tenth card in CardTable: CardTable(10).StockQty_ = 20 To make things even more interesting, you can use defined data types in the definition of elements of yet other user-defined types. The Supplier AS SupplyType element, for example, indicates that a variable of the user-defined type SupplyType is part of the user-defined CardType definition. A Video Terminal Emulator The best way to see how Microsoft QuickBASIC 4.0 works is to try it. The program shown in Figure 7 and called ADM3A.BAS is a simple video terminal emulator. It is a vehicle for showing off several of the new features of Microsoft QuickBASIC and demonstrating the user interface and is a useful program in its own right. ADM3A.BAS makes an IBM-compatible PC act like a simple video terminal, the Lear Siegler adm3a. I wrote this terminal emulation because the adm3a terminal is recognized by most UNIX(R) and XENIX(R) systems as well as many other multiuser systems. The emulator allows me access to several visually oriented programs, from the VI editor to custom-designed database programs. Figure 8☼ shows the Microsoft QuickBASIC Run menu. To run the emulator from the Microsoft QuickBASIC environment, you would execute the Start command by highlighting it on the menu or by simply pressing the keyboard shortcut command, Shift-F5, while in the editing mode. Figure 8☼ shows the selection of the Make EXE File... command, which produces an executable program that you can run at the DOS command level. All Microsoft QuickBASIC commands are available in menus like this, and all frequently used commands have shortcut keyboard aliases. The ADM3A program consists of a single module with a half-dozen subprograms. The module-level code declares SUBs to let the compiler check both the number of parameters to be passed to each SUB and their types. Figure 9☼ shows the program item list that is maintained by Microsoft QuickBASIC 4.0. The list keeps track of modules and procedures within modules. You use this work screen to select, move, and delete modules and procedures. The smart editor normally allows you to view and edit one program item at a time. However, you can split the window into two parts with the View menu to see different parts of a program at the same time. ts The module-level code defines a set of symbolic constants for logical values, colors, and cursor control. When you read a BASIC program, seeing a cold, hard number like 3 gives you virtually no information about what the value means. The context may provide some help, but COLOR 3, 1 is still a bit mysterious if you don't know what the numbers 3 and 1 stand for. Using symbolic names for constant values is a better practice. Thus, the definition CONST BLACK = 0, _BLUE = 1, ..., CYAN = 3 lets you write a statement like COLOR CYAN, BLUE that provides the information a reader needs at a glance. Notice the definition of a "record" data type, WinType. The "window type" relates variables for the top, bottom, left, and right of a screen window. The variables CmdWin and ViewWin are declared to be WinType record type by the dimension statements. The assignment statements following the variable declarations set up two window areas on the ADM3A program screen. The top screen row becomes the command window, and the rest of the screen is the view window. This arrangement works well because the physical screen of an adm3a terminal has 24 screen rows and is 80 columns wide. Putting the command line at the top of the screen keeps it from being a distraction as the user's eyes move from screen to keyboard and back. The InitScreen procedure takes care of partitioning the screen into the command window and the view window portions. The VIEW PRINT statement causes scrolling to occur within the view window when print statements attempt to write past the last line. The command window is stationary in this program. Communications setup is easy in BASIC. The OPEN COM statement takes a string parameter that specifies the serial port (COM1: or COM2:), the transmission speed, and settings for parity and number of data bits and stop bits. It opens the communication channel, if the necessary hardware is installed, and sets up transmit and receive buffers and an interface to your program. The ADM3A program uses an optional DOS environment variable to customize its setup. If the variable COMPARMS is defined in the DOS environment, its value is used as the OPEN COM string argument. Otherwise, ADM3A uses a set of built-in values. It then sets the values of the communication port address and the BREAK signal mask, which are used in the BreakSignal procedure. The main program loop alternately monitors the keyboard and the incoming communication line buffer for input. From the perspective of the local system, the program takes input from the keyboard and sends it to the remote system over the communication line. It takes incoming data from the communication line and puts it on the screen. This basic pattern is altered by certain special codes. When the user types anything at the keyboard, it is sent to the remote system unless it is an extended code. The three ADM3A commands, Break, Dial, and Quit, are initiated by extended codes, which are two-byte sequences that have a NULL character as the first byte. When INKEY$ returns an extended code, the value is sent in the form of an argument to the DoCommand subprogram, which examines the second byte, the scan code, and compares it with a list of command codes. The Alt-b and Alt-d key combinations cause other procedures to be invoked. The Alt-q command quits the emulator program by closing the communications channel, restoring the display screen to full size and normal attribute, and homing the cursor. The fourth scan code recognized by DoCommand is that of the PC's Del key. On a terminal, the Del key sends the ASCII DEL code (127), but the PC does not. So DoCommand converts the internal code to the one expected by the remote system. The Dial procedure implements simple keyboard dialing. When the user types Alt-d, the program responds with the prompt "Number:" in the view window and waits for a telephone number to be typed. After receiving a number, the Dial procedure uses the ATDT string (assuming tone dialing on a Hayes(R)-compatible modem) as a prefix for the telephone number and sends the dialing command to the modem. Once connected to the remote system, the user must log in and start a terminal session in the manner expected by the remote host. During a session, it may be necessary to tell the host to stop whatever it is doing, which is usually done by pressing the Break key, if there is one. UNIX and XENIX systems will accept a Del key for this purpose, too. The BreakSignal procedure, initiated by the Alt-b command, sends a true break signal to the host to get its attention. BreakSignal works by setting the break bit in the communication port control register, holding it high for a period of time that exceeds that of a normal character transmission period, and then clearing the break bit. The delay period, fixed at a half second in this implementation, is created by the Delay procedure, which uses the BASIC TIMER function in a loop to control the period. Delay gets the starting time and adds the requested period to it, then continually checks for the arrival of the future time specified by the sum. To prevent machine lockup at midnight, or whenever the clock on the PC rolls over its count of seconds to 0, the Delay procedure aborts if the current time value drops below the starting time. Received data is similarly analyzed as it comes in. Most characters are simply printed on the PC screen in the view window. However, a small set of incoming character codes have special meaning: control codes that cause absolute and relative cursor positioning and screen clearing. The adm3a lacks line and character insert and delete features, so the emulation is simple. If the Esc code is seen, it could signal the start of a cursor- positioning command. The cursor position is set by an escape sequence of the form Esc=rc, where r is a row number and c is a column number, each encoded as a character, and the ASCII space (decimal 32) represents row or column 0. The logic of the ADM3A program detects the Esc code and looks for the equal sign. If one is seen in the next character position, two additional characters are read and converted to row and column numbers relative to the view window values (Top and Left), and the cursor is positioned. If the character following the Esc code is not an equal sign, the program simply prints the escape character and whatever follows it. All other characters are examined by the ProcessInput procedure to see whether they should be printed. Some codes are commands. Ctrl-z, for example, clears the screen. Care is taken to handle boundary conditions as the real adm3a terminal does. A request to move the cursor right when it is already at the right window border, for example, uses a LOCATE statement to implement the automatic margins feature, which moves the cursor to the beginning of the next line, if there is one. Debugging and testing is aided greatly by the built-in Microsoft QuickBASIC debugging features. Figure 10☼ shows the screen appearance after a watch expression, CmdKey$, and a breakpoint on the SELECT CASE statement have been set. When you run the program, it stops at the breakpoint so you can observe the value of the watch expression. You can have multiple watch expressions and breakpoints. Press F5 to continue execution after it stops at a breakpoint. The sample session depicted in Figure 11☼ shows the ADM3A program accessing a XENIX system. It is running the VI editor on the /etc/termcap file, showing the termcap entry for the adm3a terminal, among others. Any other program that knows how to interact with an adm3a terminal, and most of them do, can run successfully with this emulation. Programs that use fancy line-drawing characters, inverse video, and special character formatting will not look too hot on an adm3a and may not even run at all. You can use this emulation as a starting point for more sophisticated emulations, such as DEC VT100-series terminals. Also, you may want to add file transfer and other "intelligent terminal" features to round out the program. Microsoft QuickBASIC has what it takes to do that and much more. Welcome to the Future The technology in Microsoft QuickBASIC offers many benefits to its users. Microsoft QuickBASIC ushers in the era of significantly higher programmer productivity and injects a great deal of fun into what has traditionally been drudgery. Where to from here? Microsoft representatives have said publicly that a significant amount of in-house work on current and future language products is based on p-code technology, which will be the basis of compiler products for other languages and some application programs. I hope that these products will be announced in the near future. Figure 2: The Microsoft QuickBASIC Development Cycle ┌─────────────────────────────────────────────────┐ │ Microsoft QuickBASIC 3.0 │ │ ╔════════════╗ ╔════════════╗ │ │ ║ EDIT ╠══════════════►║ COMPILE ║ │ │ ║ ║ ║ ║ │ │ ╚═══════════╝◄══════╗ ╚══════╦═════╝ │ │ ║ ║ ║ │ │ ╔═════╩══════╗ ╚═══════╦══════▼═════╗ │ │ ║ DEBUG ║ ║ RUN ║ │ │ ║ ║◄══════════════╣ ║ │ │ ╚════════════╝ ╚════════════╝ │ ├─────────────────────────────────────────────────┤ │ Microsoft QuickBASIC 4.0 │ │ ╔════════════╗ ╔════════════╗ │ │ ║ EDIT ║═══════╗ ║ COMPILE ║ │ │ ║ ║ ║ ║ ║ │ │ ╚════════╦══╝◄════╗ ║ ╚════════════╝ │ │ ║ ║ ║ ║ │ │ ╔══╩══════▼══╗ ║ ╚══════►╔════════════╗ │ │ ║ DEBUG ║ ╚═════════╣ RUN ║ │ │ ║ ║◄══════════════╣ ║ │ │ ╚════════════╝ ╚════════════╝ │ └─────────────────────────────────────────────────┘ Figure 3: Microsoft QuickBASIC 4.0 p-code ┌───────────────────────────────────────────────────┬────────┐ │ ╔═════════════════════╗ │ │ │ ┌─────────►║ ASCII ║ │ Disk │ │ │ ║ TEXT ║ │ or │ │ │ ╚════════════════╤════╝ │ Screen │ ├───────│─────────────────────────PARSER────────────├────────┤ │ │ ▼ │ │ │ │ ╔═════════════════════╗ │ M │ │ │ ║ PARSED ║ │ │ │ │◄─────────╢ ║ │ e │ │ │ ╚════════════════╤════╝ │ │ │ LISTER BINDER ▼ │ m │ │ │ ╔════╧════════════════╗ │ │ │ │ ║ SYMBOLIC ║ │ o │ │ │◄─────────╢ ║ │ │ │ │ ╚════════════════╤════╝ │ r │ │ │ BINDER ▼ │ │ │ │ ╔════╧════════════════╗ │ y │ │ │ ║ THREADED ║ │ │ │ └──────────╢ ║ │ │ │ ╚═════════════════════╝ │ │ └───────────────────────────────────────────────────┴────────┘ Figure 4: The Microsoft QuickBASIC 4.0 Programming Environment ╔═════════╗ ║ BAS ╟────┐ ╚═════════╝ │ ┌─────────┐ ╔═════════╗ │ ┌─────────►│ LIB ├───►║ LIB ║ ╔═════════╗ │ ┌────────┐ ┌───┴────┐ └────┬────┘ ╚═════════╝ ║ BI ╟──┐ └──►│ │ │ │ │ ╚═════════╝ └────►│ │ │ │ ┌────▼────┐ ╔═════════╗ │ QB ├────►│ BC ├────►│ LINK ├───►║ EXE ║ ╔═════════╗ ┌────►│ │ │ │ └─────────┘ ╚═════════╝ ║ MAK ╟──┘ ┌──►│ │ │ │ ╚═════════╝ │ └────────┘ └────────┘ │ ╔═════════╗ │ ║ QLB ╟────┘ ╚═════════╝ Figure 5: Mixed-Language Programming ╔═════════╗ ┌────────┐ ║ BAS ╟─►│ │ ╚═════════╝ │ BC │ │ │ ╔═════════╗ ╔═════════╗ │ │──►║ OBJ ╟───────┐ ║ BI ╟─►└────────┘ ╚═════════╝ │ ╚═════════╝ │ │ ╔═════════╗ ┌────────┐ ┌───▼────┐ ╔═════════╗ ┌──────┐ ║ C ╟─►│ │ │ │─►║ EXE ╟─►│ │ ╚═════════╝ │ CL │ ╔═════════╗ │ │ ╚═════════╝ │ │ │ │──►║ OBJ ╟──►│ LINK │ ╔═════════╗ │ CV │ ╔═════════╗ │ │ ╚═════════╝ │ │─►║ QLB ╟─►│ │ ║ H ╟─►└────────┘ └───────┘ ╚═════════╝ └──────┘ ╚═════════╝ │ │ ╔═════════╗ ┌────────┐ ╔═════════╗ │ ║ ASM ╟─►│ MASM │──►║ OBJ ╟───────┘ ╚═════════╝ └────────┘ ╚═════════╝ Figure 6: Example of the TYPE Keyword TYPE CardType PartNumber AS STRING * 20 Description AS STRING * 40 UnitCost AS SINGLE StockQty AS INTEGER ReorderQty AS INTEGER CurrentQty AS INTEGER Supplier AS SupplyType END TYPE Figure 7: Video Terminal Emulator ' ADM3A Terminal Emulator ' Version 1.0 ' ' This program emulates a Lear Siegler adm3a video terminal. The ' emulator gives PC users the ability to run full-screen video ' programs on UNIX and XENIX systems, and others that support a ' video terminal interface. ' ' Author: Augie Hansen ' Released: 1-14-88 DEFINT A-Z '─ Subprogram declarations. DECLARE SUB BreakSignal () DECLARE SUB Delay (Period!) DECLARE SUB Dial () DECLARE SUB DoCommand (CmdKey$) DECLARE SUB InitScreen () DECLARE SUB ProcessInput (Code$) '─ Manifest constants. CONST TRUE = -1, FALSE = NOT TRUE CONST BLACK = 0, BLUE = 1, GREEN = 2, CYAN = 3 CONST RED = 4, MAGENTA = 5, BROWN = 6, WHITE = 7 CONST BRIGHT = 8, BLINK = 128 CONST CURSOROFF = 0, CURSORON = 1 CONST BUFSIZE = 512 CONST SPACE = 32 CONST ROWS = 25, COLS = 80 CONST BANNERCOL = 4, COMMANDCOL = 33 CONST TESTMODE = 0 '─ Screen management data. TYPE WinType Top AS INTEGER Left AS INTEGER Bottom AS INTEGER Right AS INTEGER Fgnd AS INTEGER Bkgnd AS INTEGER Standout AS INTEGER END TYPE DIM CmdWin AS WinType DIM ViewWin AS WinType CmdWin.Top = 1 CmdWin.Bottom = 1 CmdWin.Left = 1 CmdWin.Right = 80 CmdWin.Fgnd = BLACK CmdWin.Bkgnd = WHITE CmdWin.Standout = BROWN + BRIGHT ViewWin.Top = 2 ViewWin.Bottom = 25 ViewWin.Left = 1 ViewWin.Right = 80 ViewWin.Fgnd = WHITE ViewWin.Bkgnd = BLUE ViewWin.Standout = WHITE + BRIGHT '─ Set cursor-positioning offsets. RowOffset = SPACE - ViewWin.Top ColOffset = SPACE - ViewWin.Left '─ Install an error-recovery mechanism. ON ERROR GOTO ErrorRecovery '─ Set up the emulator screen. InitScreen '─ Set communications parameters. Parm$ = ENVIRON$("COMPARMS") ' Check environment. IF Parm$ = "" THEN Parm$ = "COM2:1200,E,7,1" ' Use defaults. END IF Port$ = LEFT$(Parm$, 4) IF Port$ = "COM1" THEN PortAddress = &H3FB ELSE PortAddress = &H2FB END IF BreakMask = &H40 ' Break control bits '─ Open the communications channel. OPEN Parm$ FOR RANDOM AS #1 LEN = BUFSIZE ' ' Main communications loop. ' ' Check the keyboard for input. Send all normal characters typed by ' the user to the communications port for transmission to the remote ' system. If the user presses any of the emulator command keys, run ' the associated procedure. ' Main: EscapeFlag = FALSE DO '─ Process keyboard input for commands and characters. UserKey$ = INKEY$ IF LEN(UserKey$) > 1 THEN DoCommand UserKey$ ELSEIF UserKey$ <> "" THEN '─ Send the character to the remote system. PRINT #1, UserKey$; END IF '─ Check the communications line for received characters. DO IF EOF(1) THEN EXIT DO END IF Received$ = INPUT$(1, #1) ' Read a single character. '─ Look for cursor-positioning command. IF EscapeFlag = TRUE THEN IF Received$ = "=" THEN CursorRow = ASC(INPUT$(1, #1))-RowOffset CursorCol = ASC(INPUT$(1, #1))-ColOffset LOCATE CursorRow, CursorCol ELSE PRINT CHR$(27); ' The retained Esc code. PRINT Received$; END IF EscapeFlag = FALSE ELSE ProcessInput Received$ END IF LOOP LOOP END ErrorRecovery: RESUME Main ' ' BreakSignal ' ' Send a 'break' signal to the communications port. ' ' SUB BreakSignal SHARED PortAddress, BreakMask '─ Set the break bit. OUT PortAddress, (INP(PortAddress) OR BreakMask) '─ Mark time for the break period. Delay .5 '─ Clear the break bit. OUT PortAddress, (INP(PortAddress) AND NOT BreakMask) END SUB ' ' Delay ' ' Produce a specified delay. The delay period is specified in ' seconds as a single-precision number with tenth-second precision. ' ' SUB Delay (Period!) STATIC Start! = TIMER '─ Loop for specified period. Abort if clock rolls over. DO Now! = TIMER IF (Now! - Start! < Period!) OR (Now! < Start!) THEN EXIT SUB END IF LOOP END SUB ' ' Dial ' ' Ask the user for a telephone number and dial it. ' ' SUB Dial INPUT "Number: ", Phone$ PRINT #1, "ATDT" + Phone$ END SUB ' ' DoCommand ' ' Examine the extended key code to see whether it is an Emulator ' program command. If it is, execute the requested command. If it ' is not, return to the caller without doing anything. ' ' SUB DoCommand (CmdKey$) STATIC SHARED CmdWin AS WinType SELECT CASE ASC(RIGHT$(CmdKey$, 1)) CASE 16 ' Alt+q - Quit the emulator. ' Close the communications channel. CLOSE ' Restore full screen. VIEW PRINT ' Clear the screen and "home" the cursor. COLOR WHITE, BLACK CLS LOCATE CmdWin.Top, CmdWin.Left, CURSORON END CASE 32 ' Alt+d - Dial a number Dial CASE 48 ' Alt+b - Send break signal. BreakSignal CASE 83 ' PC keyboard Del key - Send an ASCII DEL PRINT #1, CHR$(127); CASE ELSE ' Unknown command - ignore it. END SELECT END SUB ' ' InitScreen ' ' Set up command bar (1 line), guarantee that the cursor is turned ' on, and establish the active terminal display window (24 lines). ' ' SUB InitScreen STATIC SHARED CmdWin AS WinType, ViewWin AS WinType '─ Initialize the screen for text and 80 columns. SCREEN TEXTMODE WIDTH COLS, ROWS COLOR WHITE, BLACK CLS '─ Draw the command window on the top line. LOCATE CmdWin.Top, CmdWin.Left, CURSORON COLOR CmdWin.Fgnd, CmdWin.Bkgnd PRINT SPACE$(CmdWin.Right - CmdWin.Left + 1) '─ Display the program banner. LOCATE CmdWin.Top, CmdWin.Left + BANNERCOL COLOR CmdWin.Standout, CmdWin.Bkgnd PRINT "ADM3A EMULATOR"; '─ Display a command summary. LOCATE CmdWin.Top, CmdWin.Left + COMMANDCOL COLOR CmdWin.Fgnd, CmdWin.Bkgnd PRINT "Break (Alt+b) Dial (Alt+d) Quit (Alt+q)" '─ Initialize the terminal screen. VIEW PRINT ViewWin.Top TO ViewWin.Bottom COLOR ViewWin.Fgnd, ViewWin.Bkgnd CLS END SUB ' ' ProcessInput ' ' Check input from the communications line and analyze it. Act on ' any adm3a terminal commands codes. Pass anything else unchanged ' to the terminal screen. ' ' SUB ProcessInput (Code$) STATIC SHARED EscapeFlag, ViewWin AS WinType SELECT CASE ASC(Code$) CASE 8 ' ASCII backspace character IF POS(0) > ViewWin.Left THEN ' Nondestructive backspace LOCATE , POS(0) - 1 END IF CASE 10 ' ^J - New-line character IF CSRLIN < ViewWin.Bottom THEN LOCATE CSRLIN + 1 ELSE PRINT Code$; END IF CASE 11 ' ^K - Up-line command IF CSRLIN > ViewWin.Top THEN LOCATE CSRLIN - 1 END IF CASE 12 ' ^L - Form-feed character ' adm3a use as nondestructive space IF POS(0) < ViewWin.Right THEN LOCATE , POS(0) + 1 ELSEIF (POS(0) = ViewWin.Right) AND _ (CSRLIN < ViewWin.Bottom) THEN LOCATE CSRLIN + 1, ViewWin.Left END IF CASE 13 LOCATE , ViewWin.Left CASE 26 ' ^Z - Clear the screen CLS CASE 27 ' Esc - Could be start of cursor sequence EscapeFlag = TRUE CASE 30 ' ^^ - Cursor to home position LOCATE ViewWin.Top, ViewWin.Left CASE ELSE PRINT Code$; END SELECT END SUB ████████████████████████████████████████████████████████████████████████████ Debug Microsoft Windows Programs More Effectively with a Simple Utility Kevin P. Welch☼ Debugging is an indispensable and unavoidable step in the development of any software program. With conventional MS-DOS(R) programs, debugging usually starts with the incorporation of numerous print statements into the program. If this doesn't work, the battle escalates to sessions with DEBUG, CodeView(R), or even to utilizing a hardware-level debugger. Unfortunately for Windows programmers, many of these traditional techniques won't work in the Windows environment. The last few months have seen significant improvements in Windows debugging──most notably the announcement of CodeView for Windows──but programmers continue to struggle with the difficult task of debugging and fine-tuning Windows applications. Approaches to Debugging From a theoretical perspective, there are many ways to classify software debugging methods. One common approach breaks debugging into three general categories of invasive, noninvasive, and combined techniques. Invasive debugging typically involves modifying and recompiling application source code. The debugging tools and their entry points simply become part of the program, which makes the technique easy to implement. Two examples of this approach are hard-coded message boxes and print statements whose output is directed to display or temporary log files. This approach often suffices to catch a large proportion of coding and algorithmic errors. With skilled use, this method can serve as the major debugging tool for large and complicated applications. However, this approach requires that you have access to the source code in question (a problem when you are debugging an interaction with a program you didn't write). In addition, the mere inclusion of the debugging statements or tools into the application sometimes changes the very character of the problem, further complicating the situation. This is especially true with system-related issues like memory management and interprocess communication. Noninvasive debugging does not involve modification or recompilation of an application. The debugging tool and its associated control functions are external to the application and can usually be applied to an existing program. Some examples of this approach include message loggers (essentially a form of application subclassing), journaling hooks, alias libraries, execution profilers, and even strict hardware-level debuggers. If used correctly, the noninvasive approach can provide a great deal of externally derived diagnostic information. This technique is especially useful in debugging complex interactions between applications. Furthermore, using debugging tools that are external to the application provides a limit on the possible compounding of errors. On the negative side, this approach is often complicated, it only reports on externally discernable events, and it tends to generate quantities of unimportant information surrounding a few vital pieces of data. The programmer usually has to do the interpreting of events reported, sorting and searching for what is really important. Combined debugging uses both invasive and noninvasive techniques. In most cases, the program need only be recompiled with a different compiler switch, which generates symbol files or other output useful to the debugging tool. Some examples of this approach include CodeView, Symdeb, and other more complicated hardware-debugging tools capable of working with application internals. This approach probably represents the most powerful of these debugging techniques, and is capable of capturing and describing even the most insidious coding or system error. Also, while tracking the problem the programmer can usually inspect internal data structures, which provides even greater insight into a given situation. To use this approach effectively, the programmer needs to have an intimate understanding of both the underlying operating system and the hardware. The power of this technique is offset by a steep learning curve, and the generation of a great deal of information. Many programmers hesitate to use it except for the most puzzling and difficult of problems. Windows Debug Utility After developing a number of Windows applications and struggling with the problems of debugging, I decided to develop a simple invasive debugging tool that would provide reasonable insight into the operations that occurred within a troublesome application. This effort resulted in the Windows Debug utility, a small object module (about 4Kb compiled) that can be conditionally linked into your program. It provides print statementlike debugging capabilities within the Windows environment. Once integrated into your application, the tools provided by the utility can easily be used to display or record a variety of internal program events. The entire debugging process is managed with a central control panel (see Figure 1☼) accessible through the system menu of the application. Using this control panel you can: ■ turn debugging ON or OFF ■ filter out unwanted debugging events ■ direct debugging output to the display and/or a log file Debug Utility Functions The debug module linked into your application is built from three separate files. Using the files DEBUG (make file for debug module), DEBUG.H (header file containing definitions and function declarations), and DEBUG.C (source code for debug utilities), you can create the object module and integrate it into your program. (Due to space limitations, the code for the DEBUG utility could not be included. It must be downloaded from any of our bulletin boards. -Ed.) The debug module consists of four functions, three of which are referenced explicitly within your host application. The first of these is the DebugSetup function, which is responsible for creating the debug viewport window and appending the Debug... control panel menu option to the system menu of the main application window. To call this function you must provide a window handle, a debug control panel message, and the maximum number of lines to be maintained by the debug viewport. The DebugSetup function assumes that there is a system menu associated with the main application window handle that you provide. If your main window does not contain a system menu and you wish to use some other means to provide access to the debug control panel, you can easily do so. Whenever the Debug... option is appended to the system menu (see Figure 2☼), the control panel message number is associated with the option and is returned. The actual message number is returned in the wParam portion of the WM_SYSCOMMAND message. To display the control panel you have to explicitly trap this message and call the DebugControl function. The parameter specifying maximum viewport lines in DebugSetup limits the amount of memory consumed by the utility while you view debug statements in the viewport. The debug viewport consists of a movable listbox containing a series of formatted debugging statements. Debugging statements displayed in this viewport are successively appended until the specified maximum is reached. At that point, each additional debugging statement displaces an earlier one. Although the maximum number of debugging statements maintained by the viewport is limited only by memory, in most situations you should limit the number to less than 100. The DebugControl function lets you display the debug control panel and change the current debugging options. This function is normally called as part of the WM_SYSCOMMAND message handling code, which is invoked whenever the user selects the Debug... option from the system menu. This function calls DebugControlDlgFn using the predefined DebugControl dialog box resource. DebugControlDlgFn is responsible for processing all the messages related to the debug control panel dialog box. The initialization section (under WM_INITDIALOG) of this function translates the current global debug- options data structure into corresponding button states and edit fields. The command section (under WM_COMMAND) processes the various button messages and accordingly enables or disables fields. The termination section (under BUG_OK) retrieves the current control panel status and translates it back into the global options data structure. At the same time it parses (albeit in a crude manner) the filter edit field and defines a list of accepted debug statement classification codes. Finally, the Debug function generates formatted debugging statements and is typically called throughout your application. The first parameter to this function is the statement classification code, which is used by the filter section of the Debug function. If the debug filter is active, this classification code is checked against a list of to-be-filtered categories. Only messages with codes on this list are displayed or written to disk, enabling you to filter out messages you're not interested in. For example, you could arbitrarily assign a classification code of 1 to all paint-related events or processes, a code of 2 to clipboard-related ones, and a code of 3 to all others. If you wished to exclude all but clipboard- and paint-related debugging statements, you would activate the filter from the control panel with a range of 1,2. All statements assigned code 3 would be excluded from the viewport or log-file listing, yielding an abbreviated execution profile that would let you concentrate on particular facets of your program. The second parameter to the Debug function is a format control string defining the physical contents of the resulting statement. This control string is identical to that used by the standard C printf function. The parameters following this format string are used in conjunction with the format control string and are converted and output accordingly. Figure 3 illustrates how the Debug function might be used. Note how the variable number of parameters is handled by the Debug function. Unless you increase the size of the pass-by-value structure, you are limited to 64 bytes of parameter storage. You may wish to experiment with alternative ways of handling the parameter list. One suggestion is to use the C 5.0 vprintf function along with either the ANSI C or UNIX(R) System V variants. Integrating the Debug Utility Before you can use the tools provided by the debug module to probe the workings of your application, you must perform several steps. Copy the following files to the directory where you are creating your application: DEBUG.H DEBUG.C Define a new compiler command line variable in your application make file. This variable can be used within a conditional compilation pragma, thus controlling the generation of debugging code. For example, you might define and use the compiler flags seen in Figure 4. Modify your application's make file so that the module DEBUG.OBJ is included whenever you build your program. See Figure 5 for an example. Define a debug control message number in your application header file. This message is returned by Windows whenever you select the Debug... option from the system menu. Export DebugControlDlgFn by defining it in your application DEF file. Failure to do this will result in erratic behavior. For example, you might change the export section of your DEF file from EXPORTS TestWndFn @1 to EXPORTS TestWndFn @1 DebugControlDlgFn @2 Add the lines shown in Figure 6 to your application's resource file. Call the DebugSetup function shortly after creating your main application window. Be sure your main window has a system menu and that you pass a valid handle to this function. Note that it is good practice to enclose this function call within a conditional compilation pragma: #if DEBUG DebugSetup( hMainWnd, BUG_CONTROL, 100 ); #endif Include the DEBUG.H file in each source code module for which you use routines from the debug utility. The header file includes function definitions and calling conventions necessary to the routines. Define debugging statements throughout your program in areas of particular interest. Try to categorize your debugging statements. Note that debugging by default is OFF and that statements encountered before it is explicitly activated are ignored. One example of this is the use of debugging statements to probe the CREATEWINDOW message processing section of your main window function. Recompile and link your application. Using the Debug Utility To get the best understanding of the debug utility let's look at a sample application like BUGTEST.EXE. Figure 7 shows how to create this application, with each file containing references to the debug utility. Throughout the journal listings of these files the sections which reference the debug utility are clearly identified. You can use these changes as a template when you incorporate the utility into your own applications. Pay special attention to the TestWndFn in the BUGTEST.C file (see Figure 8 for the code listings for BUGTEST). This window function contains several explicit references to the debug utility. First you see a series of debugging statements that output the contents of several Windows messages. Note how the messages are classified and remember that you can filter categories to limit the statements generated. You should also notice the special handling of the BUG_CONTROL message in the WM_SYSCOMMAND processing section of the function. This message is generated whenever you access the Debug... option from the system menu. The call to DebugControl displays the debug control panel from which you can activate or deactivate debugging and define various output options. To create BUGTEST.EXE you need Microsoft C 5.0, Windows 2.03, and the Windows 2.03 Software Development Kit (SDK). If you don't have the 2.03 SDK, you can probably get everything to work correctly (with perhaps minor modifications) using the 1.04 SDK. Although the BUGTEST.ICO file is not included in the code listings, you can easily create your own icon with the editor in the Windows SDK. Once you have built the BUGTEST application, you can experiment with the debug utility. When you first bring up the program, a standard pop-up window appears. Using either the mouse or the keyboard you can move, resize, and make this window into an icon just as you would any other window. Whenever you perform one of these operations, numerous debugging statements are generated, but they are ignored by the debug utility until you explicitly turn debugging ON through the control panel. To access the control panel you select the Debug... option from the system menu. The debug control panel is displayed. From this dialog box you can activate debugging and define options. You can check the count and display boxes, which causes debugging statements to be enumerated and the results displayed in a pop-up listbox (see Figure 9☼). Using this listbox you can watch the debugging statements as they are generated by the main application (see Figure 10☼). Remember that the filter option allows you to select particular categories of debugging statements. Also note that this implementation of the debug utility is set for 32 filter categories. If these are insufficient, you can easily change the MAX_FILTER definition in DEBUG.C to a larger number and recompile the utility. The log-file option works almost identically to the display option, except that debugging statements are appended to the disk file you specify. You can simultaneously display and log, although your system will slow considerably. When using log files I add the following line to the "extensions section" of my WIN.INI file: LOG=NOTEPAD.EXE ^.LOG This lets you double-click on any log file and view its contents using the notepad provided with Windows. Be aware that notepad is not capable of loading large log files, but this shouldn't be a major problem. Conclusion The debug utility is not about to displace the likes of Windows CodeView. However, for simple debugging it can provide a great deal of insight into the workings of an application and should be a useful addition to every programmer's toolbox. The debug utility is capable of solving a large number of Windows problems. With the code provided here as a framework, you may extend this utility into many interesting areas. You could use journaling hooks to create a general purpose message tracker to explore problems between applications. You could experiment with multiple debug viewports into a single application. Or you could move the utility into a dynamically linked library and debug multiple applications simultaneously. These, and hopefully many other creative enhancements are left to you. Figure 3: A Sample Use of the DEBUG Utility Debug( 1, "WM_SIZE: [%u,%u], LOWORD(lParam),HIWORD(lParam)); Debug( 2, "WM_MOVE: [%u,%u]", LOWORD(lParam),IWORD(lParam)); Debug( 3, "WM_CHAR: [%c,%u]", wParam, wParam ); Debug( 4, "WM_ACTIVATE: [%s]", (wParam)?"on":"off" ); Figure 4: Sample Compiler Flags for Compiling the DEBUG Utility Code STDFLAGS=-c -u -AS -FPa -Gsw -Os -Zep BUGFLAGS=-c -u -AS -FPa -Gsw -Os -Zep -DDEBUG bugtest.obj: bugtest.c cl $(BUGFLAGS) bugtest.c Figure 5: Sample Make File to Include DEBUG within an Application Change your make file from bugtest.exe: bugtest.obj bugtest.def bugtest.res link4 bugtest /AL:16 /NOE,,,slibw,bugtest.def to bugtest.exe: bugtest.obj debug.obj bugtest.def bugtest.res link4 bugtest+debug /AL:16 /NOE,,,slibw,bugtest.def Figure 6: Debug Control Panel Definitions #include "debug.h" DebugControl DIALOG LOADONCALL MOVEABLE DISCARDABLE 8, 20, 185, 81 STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "OFF - debug &inactive." BUG_OFF,"button",STD_BUTTON,5,3,130,12 CONTROL "ON - debug &active." BUG_ON,"button",STD_BUTTON,5,15,128,12 CONTROL "&Count debug events." BUG_COUNT,"button",STD_CHECKBOX,8,26,114,12 CONTROL "&Display in debug window." BUG_DISPLAY,"button",STD_CHECKBOX,18,38, CONTROL "&Filter" BUG_FILTER,"button",STD_CHECKBOX,18,50,34,12 CONTROL "" BUG_FILTERLIST,"edit",STD_EDITFIELD,55,50,78,12 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12 CONTROL "" BUG_LOGFILE,"edit",STD_EDITFIELD ,55,63,78,12 CONTROL "" -1,"static",SS_BLACKFRAME | WS_CHILD,142,0,1,81 CONTROL "Ok" BUG_OK,"button",STD_BUTTON,148,4,32,14 CONTROL "Cancel" BUG_CANCEL,"button",STD_BUTTON,148,21,32,14 END Figure 7: A list of the files required to build the sample test program that incorporates the DEBUG utility. BUGTEST application make file BUGTEST.C application source code BUGTEST.RC application resource file BUGTEST.ICO application icon (referenced by resource file) BUGTEST.DEF application link definition Figure 8: Make File for BUGTEST STDFLAGS=-c -u -AS -FPa -Gsw -Os -Zep BUGFLAGS=-c -u -AS -FPa -Gsw -Os -Zep -DDEBUG bugtest.res: bugtest.rc bugtest.ico rc -r bugtest.rc debug.obj: debug.h debug.c cl $(STDFLAGS) debug.c bugtest.obj: bugtest.c cl $(BUGFLAGS) bugtest.c bugtest.exe: bugtest.obj bugtest.def bugtest.res debug.obj link4 bugtest+debug /AL:16 /NOE,,,slibw,bugtest.def rc bugtest.res Code Listing for BUGTEST /* * DEBUG TEST PROGRAM - SOURCE CODE * * LANGUAGE : Microsoft C 5.0 * MODEL : small * STATUS : operational * * 12/11/87 1.00 - Kevin P. Welch - initial creation. * */ #include <windows.h> #include "debug.h" /* local definitions */ #define BUG_CONTROL 201 /* function definitions */ LONG FAR PASCAL TestWndFn( HWND, WORD, WORD, LONG ); /**/ /* * MAINLINE - BUG TEST PROGRAM * * This mainline initializes the test program and processes * and dispatches all messages relating to the debug test * window. * */ int PASCAL WinMain( hInstance, hPrevInstance, lpsCmd, wCmdShow ) HANDLE hInstance; HANDLE hPrevInstance; LPSTR lpsCmd; WORD wCmdShow; { /* local variables */ MSG Msg; /* current system message */ /* initialization */ if ( TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow ) ) { /* process system messages until finished */ while ( GetMessage( (LPMSG)&Msg, NULL, 0, 0 ) ) { TranslateMessage( (LPMSG)&Msg ); DispatchMessage( (LPMSG)&Msg ); } /* terminate application */ exit( Msg.wParam ); } else exit( FALSE ); } /**/ /* * TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow ) : BOOL; * * hInstance current instance handle * hPrevInstance handle to previous instance * lpsCmd command line string * wCmdShow window display flag * * This utility function performs all the initialization required * for testing the debug utility. Included in this program is * the registry and creation of the main window & the installation * of the debug utility code. * */ static BOOL TestInit( hInstance, hPrevInstance, lpsCmd, wCmdShow ) HANDLE hInstance; HANDLE hPrevInstance; LPSTR lpsCmd; WORD wCmdShow; { /* local variables */ HWND hWnd; /* current window handle */ BOOL bResult; /* result of initialization */ WNDCLASS WndClass; /* window class */ /* initialization */ bResult = FALSE; /* register window class */ if ( !hPrevInstance ) { /* define MAZE window class */ memset( &WndClass, 0, sizeof(WNDCLASS) ); WndClass.lpszClassName = (LPSTR)"TestWindow"; WndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); WndClass.lpszMenuName = (LPSTR)NULL; WndClass.style = CS_HREDRAW | CS_VREDRAW; WndClass.lpfnWndProc = TestWndFn; WndClass.hInstance = hInstance; WndClass.hIcon = LoadIcon( hInstance, "BugTestIcon" ); WndClass.hbrBackground = (HBRUSH)(COLOR_MENU + 1); /* register maze window class */ if ( RegisterClass( (LPWNDCLASS)&WndClass ) ) { /* create window */ hWnd = CreateWindow( "TestWindow", /* class name */ "Debug Test Window", /* caption */ WS_TILEDWINDOW, /* style */ CW_USEDEFAULT, /* x position */ CW_USEDEFAULT, /* y position */ CW_USEDEFAULT, /* width */ CW_USEDEFAULT, /* height */ (HWND)NULL, /* parent window */ (HMENU)NULL, /* menu */ hInstance, /* application */ (LPSTR)NULL /* other data */ ); /* continue if successful */ if ( hWnd ) { /* Here is where the debug utility is * installed into the program. A response * message number is provided along with the * maximum number of debug statements which * will be maintained by the listbox. The * larger this number, the less global memory * available for your application. */ #if DEBUG DebugSetup( hWnd, BUG_CONTROL, 100 ); #endif /* make window visible */ bResult = TRUE; ShowWindow( hWnd, wCmdShow ); } } } /* return result */ return( bResult ); } /**/ /* * TEST WINDOW MESSAGE PROCESSING PROCEDURE * * TestWndFn( hWnd, wMessage, wParam, lParam ) : LONG FAR PASCAL * * hWnd window handle * wMessage message number * wParam additional message information * lParam additional message information * * This window function processes all the messages related to * the debug test window. Using the system menu the user can * display the debug control panel dialog box. * */ LONG FAR PASCAL TestWndFn( hWnd, wMessage, wParam, lParam ) HWND hWnd; WORD wMessage; WORD wParam; LONG lParam; { /* local variables */ LONG lResult; /* result of message */ /* initialization */ lResult = FALSE; #if DEBUG /* sample debugging output */ switch( wMessage ) { case WM_MOVE : Debug( 1, "WM_MOVE: [%u,%u]", HIWORD(lParam), LOWORD(lParam) ); break; case WM_SIZE : Debug( 1, "WM_SIZE: [%u,%u]", HIWORD(lParam), LOWORD(lParam) ); break; case WM_CHAR : Debug( 2, "WM_CHAR: [%c,%u]", wParam, wParam ); break; case WM_ACTIVATE : Debug( 3, "WM_ACTIVATE: %s", (wParam)?"activate":"inactivate" ); break; case WM_ACTIVATEAPP : Debug( 3, "WM_ACTIVATEAPP: %s", (wParam)?"activate":"inactivate" ); break; case WM_PAINT : Debug( 4, "WM_PAINT:" ); break; default : break; } #endif /* process each message */ switch( wMessage ) { case WM_SYSCOMMAND : /* system command */ /* In here you need to handle the special case where the * user asks for the debug control panel to be * displayed. * To do so you need to trap the control panel response * message you provided when installing the debug * utility. */ /* process sub-message */ switch( wParam ) { #if DEBUG case BUG_CONTROL : /* debug control panel */ DebugControl( hWnd ); break; #endif default : lResult = DefWindowProc( hWnd, wMessage, wParam, lParam ); break; } break; case WM_DESTROY : /* destroy window */ PostQuitMessage( 0 ); break; default : /* send to default */ lResult = DefWindowProc( hWnd, wMessage, wParam, lParam ); break; } /* return normal result */ return( lResult ); } DEF File for BUGTEST NAME BUGTEST DESCRIPTION 'Debug Test Utility' STUB '\BIN\WINSTUB.EXE' CODE MOVEABLE DATA MOVEABLE MULTIPLE HEAPSIZE 4096 STACKSIZE 4096 EXPORTS TestWndFn @1 DebugControlDlgFn @2 Resource File for BUGTEST /* * DEBUG TEST PROGRAM - RESOURCE FILE * * LANGUAGE : Microsoft C 5.0 * MODEL : small * STATUS : operational * * 12/11/87 1.00 - Kevin P. Welch - initial creation. * */ #include <style.h> #include "debug.h" BugTestIcon ICON bugtest.ico DebugControl DIALOG LOADONCALL MOVEABLE DISCARDABLE 8, 20, 185, 81 CAPTION "Debug Control Panel" STYLE WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_POPUP BEGIN CONTROL "OFF - debug &inactive." BUG_OFF,"button",STD_RADIO,5,3,130,12 CONTROL "ON - debug &active." BUG_ON,"button",STD_RADIO,5,15,128,12 CONTROL "&Count debug events." BUG_COUNT,"button",STD_CHECKBOX,8,26,114,12 CONTROL "&Display in debug window." BUG_DISPLAY,"button",STD_CHECKBOX,18,38, CONTROL "&Filter" BUG_FILTER,"button",STD_CHECKBOX,18,50,34,12 CONTROL "" BUG_FILTERLIST,"edit",STD_EDITFIELD,55,50,78,12 CONTROL "&Log to" BUG_LOG,"button",STD_CHECKBOX,18,62,36,12 CONTROL "" BUG_LOGFILE,"edit",STD_EDITFIELD ,55,63,78,12 CONTROL "" -1,"static",SS_FRAME | WS_CHILD,142,0,1,81 CONTROL "Ok" BUG_OK,"button",DEF_BUTTON,148,4,32,14 CONTROL "Cancel" BUG_CANCEL,"button",STD_BUTTON,148,21,32,14 END ████████████████████████████████████████████████████████████████████████████ An Examination of the Operating Principles of the Microsoft Object Linker☼ Richard Wilton MS-DOS(R) object modules can be processed in two ways: they can be grouped together in object libraries or they can be linked into executable files. All Microsoft language translators are distributed with two utility programs that process object modules: the Microsoft Library Manager (LIB) creates and modifies object libraries and the Microsoft Object Linker (LINK) processes the individual object records within object modules to create executable files. The following discussion focuses on LINK because of its crucial role in creating an executable file. What LINK Does The function of LINK is to translate object modules into an executable program. LINK's input consists of one or more object files (OBJ files) and, optionally, one or more libraries (LIB files). LINK's output is an executable file (EXE file) containing binary data that can be loaded directly from the file into memory and executed. LINK can also generate a symbolic address map listing (MAP file)──a text file that describes the organization of the EXE file and the correspondence of symbols declared in the object modules to addresses in the executable file. Building an Executable LINK builds two types of information into an EXE file. First, it extracts executable code and data from the LEDATA and LIDATA records in object modules, arranges them in a specified order according to its rules for segment combination and relocation, and copies the result into the EXE file. Second, LINK builds a header for the EXE file. The header describes the size of the executable program and also contains a table of load-time segment relocations and initial values for certain CPU registers. See Pass 2 in the LINK Internals section. (See MSJ, Vol. 3 No. 2, "Exploring the Structure and Contents of the MS-DOS(R) Object Module Format," for more information on LEDATA, LIDATA, and other object records supported by LINK-Ed.) Relocation and Linking In building an executable image from object modules, LINK performs two essential tasks: relocation and linking. As it combines and rearranges the executable code and data it extracts from the object modules it processes, LINK frequently adjusts, or relocates, address references to account for the rearrangements (see Figure 1). LINK links object modules by resolving address references among them. It does this by matching the symbols declared in EXTDEF and PUBDEF object records (see Figure 2). LINK uses FIXUPP records in order to determine exactly how to compute both address relocations and linked address references. Object Module Order LINK processes input files from three sources: object files and libraries specified explicitly by the user (in the command line, in response to prompts by LINK, or in a response file) and object libraries named in object module COMENT records. LINK always uses all of the object modules in the object files it processes. In contrast, it extracts individual object modules from libraries-only those object modules needed to resolve references to public symbols are used. This difference is implicit in the order in which LINK reads its input files: ■ object files that are specified in the command line or in response to the Object Modules prompt ■ libraries that are specified in the command line or in response to the Libraries prompt ■ libraries that are specified in COMENT records The order in which LINK processes object modules influences the resulting executable file in three ways. First, the order in which segments appear in LINK's input files is reflected in the segment structure of the executable file. Second, the order in which LINK resolves external references to public symbols depends on the order in which it finds the public symbols in its input files. Finally, LINK derives the default name of the executable file from the name of the first input object file. Segment Order In general, LINK builds named segments into the executable file in the order in which it first encounters the SEGDEF records that declare the segments. (The /DOSSEG switch also affects segment order.) This means that the order in which segments appear in the executable file can be controlled by linking object modules in a specific order. In assembly language programs, it is best to declare all the segments used in the program in the first object module to be linked so that the segment order in the executable file is under complete control. Order in Which References Are Resolved LINK resolves external references in the order in which it encounters the corresponding public declarations. This fact is important as it determines the order in which LINK extracts object modules from libraries. When a public symbol required to resolve an external reference is declared more than once among the object modules in the input libraries, LINK uses the first object module that contains the public symbol. This means that the actual executable code or data that is associated with a particular external reference can be varied by changing the order in which LINK processes its input libraries. For example, imagine that a C programmer has written two versions of a function named myfunc() that is called by the program MYPROG.C. One version of myfunc() is for debugging; its object module is found in MYFUNC.OBJ. The other is a production version whose object module resides in MYLIB.LIB. Under ordinary circumstances, the programmer links the production version of myfunc() by using MYLIB.LIB (see Figure 3). In order to use the debugging version of myfunc(), the programmer explicitly includes its object module (MYFUNC.OBJ) when LINK is executed. This causes LINK to build the debugging version of myfunc() into the executable file because it finds the debugging version in MYFUNC.OBJ before it finds the other version in MYLIB.LIB. To exploit the order in which LINK resolves external references, it is important to know LINK's library search strategy: each individual library is searched repeatedly (from first library to last, in the sequence in which they are input to LINK) until no further external references can be resolved. The example in Figure 4 demonstrates this search strategy. Library LIB1.LIB contains object modules A and B, library LIB2.LIB contains object module C, and the object file MYPROG.OBJ contains the object module MAIN; modules MAIN, A, and C each contain an external reference to a symbol declared in another module. When this program is linked with LINK MYPROG,,,LIB1+LIB2 LINK starts by incorporating the object module MAIN into the executable program. It then searches the input libraries until it resolves all the external references: ■ process MYPROG.OBJ, find unresolved external reference to A ■ search LIB1.LIB, extract A, find unresolved external reference to C ■ search LIB1.LIB again; reference to C remains unresolved ■ search LIB2.LIB, extract C, find unresolved external reference to B ■ search LIB2.LIB again; reference to B remains unresolved ■ search LIB1.LIB again, extract B ■ no more unresolved external references, so end library search The order in which the modules appear in the executable file thus reflects the order in which LINK resolves the external references; this, in turn, depends on which modules were contained in the libraries and on the order in which the libraries are input to LINK. Name of the Executable If no filename is specified in the command line or in response to the Run File prompt, LINK derives the name of the executable file from the name of the first object file it processes. For example, if the object files PROG1.OBJ and PROG2.OBJ are linked with the command LINK PROG1+PROG2; the resulting executable file, PROG1.EXE, takes its name from the first object file processed by LINK. Order and Combinations LINK builds segments into the executable file by applying the following sequence of rules: Segments appear in the executable file in the order in which their SEGDEF declarations first appear in the input object modules. Segments in different object modules are combined if they have the same name and class and a public, memory, stack, or common combine type. All address references within the combined segments are relocated relative to the start of the combined segment. ■ Segments with the same name and either the public or the memory combine type are combined in the order in which they are processed by LINK. The size of the resulting segment then equals the total size of the combined segments. ■ Segments with the same name and the stack combine type are overlapped so that the data in each of the overlapped segments ends at the same address. The size of the resulting segment equals the total size of the combined segments. The resulting segment is always paragraph aligned. ■ Segments with the same name and the common combine type are overlapped so that the data in each of the overlapped segments starts at the same address. The size of the resulting segment equals the size of the largest of the overlapped segments. Segments with the same class name are concatenated. If the /DOSSEG switch is used, the segments are rearranged in conjunction with DGROUP. These rules allow the programmer to control the organization of segments in the executable file by ordering SEGMENT declarations in an assembly language source module, which produces the same order of SEGDEF records in the corresponding object module, and by placing this object module first in the order in which LINK processes its input files. A typical MS-DOS program is constructed by declaring all executable code and data segments with the public combine type, thus enabling the programmer to compile the program's source code from separate source code modules into separate object modules. When these object modules are linked, LINK combines the segments from the object modules according to the above rules to create logically unified code and data segments in the executable file. Segment Classes LINK concatenates segments with the same class name after it combines segments with the same segment name and class. For example, Figure 5 shows the following compiling and linking: MASM MYPROG1; MASM MYPROG2; LINK MYPROG1+MYPROG2; After MYPROG1.ASM and MYPROG2.ASM have been compiled, LINK builds the _TEXT and FAR_TEXT segments by combining segments with the same name from the different object modules. Then, _TEXT and FAR_TEXT are concatenated because they have the same class name ('CODE'). _TEXT is before FAR_TEXT in the executable file because LINK encounters the SEGDEF record for _TEXT before it finds the SEGDEF record for FAR_TEXT. Segment Alignment LINK aligns the starting address of each segment it processes according to the alignment specified in each SEGDEF record. It adjusts the alignment of each segment it encounters regardless of how that segment is combined with other segments of the same name or class. (The one exception is stack segments, which always start on a paragraph boundary.) Segment alignment is particularly important when public segments with the same name and class are combined from different object modules. Note what happens in Figure 6, where the three concatenated _DATA segments have different alignments. To enforce the word alignment and paragraph alignment of the _DATA segments in Module2 and Module3, LINK inserts one or more bytes of padding between the segments. Segment Groups A segment group establishes a logical segment address to which all offsets in a group of segments can refer, that is, all addresses in all segments in the group can be expressed as offsets relative to the segment value associated with the group (see Figure 7). Declaring segments in a group does not affect their positions in the executable file; the segments in a group may or may not be contiguous and can appear in any order as long as all address references to the group fall within 64Kb of each other. LINK reserves one group name, DGROUP, for use by Microsoft language translators. DGROUP is used to group compiler-generated data segments and a default stack segment. LINK Internals Many programmers use LINK as a "black box" program that transforms object modules into executable files. Nevertheless, it is helpful to observe how LINK processes object records to accomplish this task. LINK is a two-pass linker; that is, it reads all its input object modules twice. On Pass 1, LINK builds an address map of the segments and symbols in the object modules. On Pass 2, it extracts the executable code and program data from the object modules and builds a memory image-an exact replica-of the executable file. The reason LINK builds an image of the executable file in memory, instead of just copying code and data from object modules into the executable file, is that it organizes the executable file by segments and not according to the order in which it processes object modules. The most efficient way to concatenate, combine, and relocate the code and data is to build a map of the executable file in memory during Pass 1 and then fill in the map with code and data during Pass 2. In Versions 3.52 and later, whenever the /I (/INFORMATION) switch is specified in the command line, LINK displays status messages at the start of each pass and as it processes each object module. If the /M (that is, /MAP) switch is used in addition to the /I switch, LINK also displays the total length of each segment declared in the object modules. This information is helpful in determining how the structure of an executable file corresponds to the contents of the object modules processed by LINK. Pass 1 During Pass 1, LINK processes the LNAMES, SEGDEF, GRPDEF, COMDEF, EXTDEF, and PUBDEF records in each input object module and uses the information in these object records to construct a symbol table and an address map of segments and segment groups. Symbol Table As each object module is processed, LINK uses the symbol table to resolve external references (declared in EXTDEF and COMDEF records) to public symbols. If LINK processes all the object files without resolving all the external references in the symbol table, it searches the input libraries for public symbols that match the unresolved external references. LINK continues to search each library until all the external references in the symbol table are resolved. Segments and Groups LINK processes every SEGDEF record according to the segment name, class name, and attributes specified in the record. LINK constructs a table of named segments and updates it as it concatenates or combines segments. This allows LINK to associate each public symbol in the symbol table with an offset into the segment in which the symbol is declared. LINK also generates default segments into which it places communal variables declared in COMDEF records. Near communal variables are placed in one paragraph-aligned public segment named c_common, with class name BSS (block storage space) and group DGROUP. Far communal variables are placed in a paragraph-aligned segment named FAR_BSS, with class name FAR_BSS. The combine type of each far communal variable's FAR_BSS segment is private (that is, not public, memory, common, or stack). As many FAR_BSS segments as necessary are generated. After all the object files have been read and all the external references in the symbol table have been resolved, LINK has a complete map of the addresses of all segments and symbols in the program. If a MAP file has been requested, LINK creates the file and writes the address map to it. Then LINK initiates Pass 2. Pass 2 In Pass 2, LINK extracts executable code and program data from the LEDATA and LIDATA records in the object modules. It builds the code and data into a memory image of the executable file. During Pass 2, LINK also carries out all the address relocations and fixups related to segment relocation, segment grouping, and resolution of external references, as well as any other address fixups specified explicitly in object module FIXUPP records. If it determines during Pass 2 that not enough RAM is available to contain the entire image, LINK creates a temporary file in the current directory on the default disk drive. (LINK Version 3.60 and later use the environment variable TMP to find the directory for the temporary scratch file.) LINK then uses this file in addition to all the available RAM to construct the image of the executable file. (In versions of MS-DOS earlier than 3.0, the temporary file is named VM.TMP; in Version 3.0 and later, LINK uses Interrupt 21H Function 5AH to create the file.) LINK reads each of the input object modules in the same order as it did in Pass 1. This time it copies the information from each object module's LEDATA and LIDATA records into the memory image of each segment in the proper sequence. This is when LINK expands the iterated data in each LIDATA record it processes. LINK processes each LEDATA and LIDATA record along with the corresponding FIXUPP record, if one exists. LINK processes the FIXUPP record, performs the address calculations necessary for relocation, segment grouping, and resolving external references, and then stores binary data from the LEDATA or LIDATA record, including the results of the address calculations, in the proper segment in the memory image. The only exception to this process occurs when a FIXUPP record refers to a segment address. In this case, LINK adds the address of the fixup to a table of segment fixups; this table is used later to generate the segment relocation table in the EXE header. When all the data has been extracted from the object modules and all the fixups have been carried out, the memory image is complete. LINK now has all the information it needs to build the EXE header (see Figure 8). At this point, therefore, LINK creates the executable file and writes the header and all segments into it. Summary By using LINK to rearrange and combine segments, a programmer can generate an executable file in which segment order and addressing serve specific purposes. Careful use of LINK leads to more efficient use of memory and simpler, more efficient programs. LINK's characteristic support for segment ordering, for run-time memory management, and for dynamic overlays has an impact in many different situations. Programmers who write their own language translators must bear in mind the special conventions followed by LINK in support of Microsoft language translators. Application programmers must be familiar with LINK's capabilities when they use assembly language or link assembly language programs with object modules generated by Microsoft compilers. LINK is a powerful program development tool and understanding its special capabilities can lead to more efficient programs. Figure 1: A simple relocation. Both object modules contain code that LINK combines into one logical segment. In this example, LINK appends the 50H bytes of code in Module2 to the 64H bytes of code in Module1. LINK relocates all references to addresses in the code segment so that they apply to the combined segment. ╔════════════════════╗ ╔════════════════════╗ ║ Code segment ║ ║ ║ ║ (64H bytes) ║ ║ Code segment ║ ║ ║ ║ (50H bytes) ║ ║Label1 at offset 10H║ ║ ║ ╚════════════════════╝ ╚════════════════════╝ Module 1 Module 2 ╔══════════════════════╗ ║ Code segment ║ ║ (B4H bytes) ║ ║ ║ ║ Label1 at offset 10H ║ ║ Label2 at offset 74H ║ ║ ║ ╚══════════════════════╝ Combined code segment Figure 2: Resolving an external reference. LINK resolves the external reference in Module1 (declared in an EXTDEF record) with the address of Label2 in Module2 (declared in a PUBDEF record). Module 1 Module 2 ╔══════════════════════════╗ ╔══════════════════════════╗ ║ Code segment ║ ║ Code segment ║ ║ EXTDEF Label2 ║ ║ PUBDEF Label2 ║ ║ jmp Label2 ║ ║ ║ ║ ∙ ║ ║ Label2: ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ╚══════════════════════════╝ ╚══════════════════════════╝ Combined code segment ╔═════════════════════════╗ ║ Code segment ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ║ ║ jmp Label2 ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ║ ║ Label2: ∙ ║ ║ ∙ ║ ║ ∙ ║ ╚═════════════════════════╝ Figure 3: Ordered object module processing by LINK. (a) With the command LINK MYPROG,,,MYLIB, the production version of myfunc() in MYLIB.LIB is used. (b) With the command LINK MYPROG+MYFUNC,,, MYLIB, the debugging bersion of myfunc() in MYFUNC.OBJ is used. ┌────────────────┐ ┌─────────────────────┐ │ Main () │ │ │ │ { ├────►│ EXTDEF for myfunc() ├──┐ ┌───────────────┐ │ x=myfunc(y) ; │ │ │ │ │ Executable │ │ } │ └─────────────────────┘ ├──►│ code contains │ └────────────────┘ MYPROG.OBJ │ │ myfunc() │ MYPROG.C │ │ derived from │ │ │ either │ ┌────────────────┐ ┌─────────────────────┐ │ │ MUFUNC.OBJ or │ │ myfunc(a) │ │ │ │ │ MYLIB.OBJ │ │ int a; ├────►│ PUBDEF for myfunc() ├──┤ └───────────────┘ │ { │ │ │ │ │ ∙ │ └─────────────────────┘ │ │ ∙ │ MYFUNC.OBJ │ │ ∙ │ │ │ } │ ┌─────────────────────┐ │ └────────────────┘ │ ∙ │ │ MYFUNC.C │ ∙ │ │ │ ∙ │ │ │ PUBDEF for myfunc() ├──┘ │ ∙ │ │ ∙ │ │ ∙ │ └─────────────────────┘ MYLIB.LIB Figure 4: Library search order. Modules are incorporated into the executable file as LINK extracts them from the libraries to resolve external references. Start of program─┐ ┌─────────────┐ ┌─────────────┐ ┌────────────────┐ ┌──────────────┐┐│ │ ModuleA │ │ ModuleC │ │ ModuleMAIN │ │ ModuleMAIN │├┘ │ Call C │ │ Call B │ │ Call C │ ├──────────────┤┘ ├─────────────┤ └─────────────┘ └────────────────┘ │ ModuleA │ │ ModuleB │ LIB2.LIB MYPROG.OBJ ├──────────────┤ │ │ │ ModuleC │ └─────────────┘ ├──────────────┤ LIB1.LIB │ ModuleB │ └──────────────┘ MYPROG.EXE Figure 5: Segment order and concatenation by LINK. The start of each file, corresponding to the lowest address, is at the top. MYPROG1.ASM ┌──────────────────────────────┐ MYPROG1.OBJ │ _TEXT SEGMENT public 'CODE' │ ┌───────────────────┐ ├──────────────────────────────┤ │SEGDEF for_TEXT │ │FAR_TEXT SEGMENT public 'CODE'├─►│SEGDEF for FAR_TEXT├───┐ ├──────────────────────────────┤ │SEFDEF for_DATA │ │ │ _DATA SEGMENT public 'DATA' │ └───────────────────┘ │ └──────────────────────────────┘ │ │ MYPROG2.ASM │ ┌──────────────────────────────┐ MYPROG2.OBJ │ │ _TEXT SEGMENT public 'CODE' │ ┌───────────────────┐ │ ├──────────────────────────────┼─►│SEGDEF for_TEXT │ │ │FAR_TEXT SEGMENT public 'CODE'│ │SEGDEF for FAR_TEXT├───┤ └──────────────────────────────┘ └───────────────────┘ │ ┌─────────────────────────────────────────────────────┘ │ MYPROG1.EXE │ ┌────────────┐─┐ ─┐ │ │ │ ├─ _TEXT │ │ │ │ │ segment │ └──►│ │ │ │ ├────────────┤═╡ ├─ 'CODE' class │ │ │ │ │ │ ├─ FAR_TEXT │ │ │ │ segment │ │ │ │ │ ├────────────┤═╡ ─┘ │ │ ├─ _DATA └────────────┘─┘ segment Figure 6: Alignment of combined segments. LINK enforces segment alignment by padding combined segments with unitialized data bytes. Module 1 Module 2 Module 3 ╔═══════════════════╗ ╔════════════════════╗ ╔════════════════════╗ ║ DATA SEGMENT byte ║ ║ DATA SEGMENTS word ║ ║ DATA SEGMENTS para ║ ║ public 35H bytes ║ ║ public 35H bytes ║ ║ public 35H bytes ║ ╚═══════════════════╝ ╚════════════════════╝ ╚════════════════════╝ 00H┌─────────────────────────────┐─┐ │ Module 1 │ ├─35H bytes (byte aligned) 35H├─────────────────────────────┤─┘ │█████████████████████████████│ 36H├─────────────────────────────┤─┐ │ Module 2 │ ├─35H bytes (word aligned) 6BH├─────────────────────────────┤─┘ │█████████████████████████████│ │█████████████████████████████│ 70H├─────────────────────────────┤─┐ │ Module 3 │ ├─35H bytes (paragraph aligned) └─────────────────────────────┘─┘ Resulting _DATA segment in .EXE file Figure 7: Example of group addressing. The first MOV loads the value 00H into AX (the offset of TestData relative to DataSeg2); the second MOV loads the value 100H into AX (the offset of TestData relative to the group DataGroup). DataGroup GROUP DataSeg1,DataSeg2 CodeSeg SEGMENT byte public 'CODE' ASSUME cs:CodeSeg mov ax,offset DataSeg2:TestData mov ax,offset DataGroup:TestData CodeSeg ENDS DataSeg1 SEGMENT para public 'DATA' DB 100h dup(?) DataSeg1 ENDS DataSeg2 SEGMENT para public 'DATA' TestData DB ? DataSeg2 ENDS END Figure 8: How LINK Builds an EXE File Header ╓┌───────┌──────────────────────────────────┌────────────────────────────────► Offset Contents Comments 00H 'MZ' EXE file signature 02H Length of executable ┐ image MOD 512 │ │ Total size of all segments 04H Length of executable image in │ plus EXE file header 512-byte pages, including last │ partial page (if any) ┘ 06H Number of run-time Number of segment fixups segment relocations 08H Size of the EXE header in Size of segment relocation table Offset Contents Comments 08H Size of the EXE header in Size of segment relocation table 16-byte paragraphs 0AH MINALLOC: Minimum amount Size of uninitialized data and/or of RAM to be allocated above stack segments at end of program end of the loaded program (in (0 if /HI switch is used) 16-byte paragraphs) 0CH MAXALLOC: Maximum amount 0 if /HI switch is used; value of RAM to be allocated above specified with /CP switch; end of the loaded program (in FFFFH if /CP and /HI switches 16-byte paragraphs) are not used 0EH Stack segment (initial value for Address of stack segment relative SS register); relocated by to start of executable image MS-DOS when program is loaded 10H Stack pointer (initial Size of stack segment in bytes value for register SP) Offset Contents Comments 12H Checksum One's complement of sum of all words in file, excluding checksum itself 14H Entry point offset (initial ┐ value for register IP) │ │ MODEND object record that 16H Entry point segment (initial │ specifies program start address value for register CS); │ relocated by MS-DOS when │ program is loaded ┘ 18H Offset of start of segment relocation table relative to start of EXE header 1AH Overlay number 0 for resident segments; >0 for overlay segments Offset Contents Comments 1CH Reserved ════════════════════════════════════════════════════════════════════════════

Vol. 3 No. 4 Table of Contents

DARWIN: Merrill Lynch Develops a New Workstation Based on Windows 2.0 In order to easily access data from numerous incompatible databases and provide a graphical interface with report writing capabilities, Merrill Lynch turned to the Windows development environment and created DARWIN (Data Access and Reporting Through Windows). CodeView(R) for Windows Provides an Interactive Debugging Environment An interactive debugger allows immediate interaction with an executing program. CodeView for Windows (CVW) is a special version of the Microsoft debugger that understands such Windows specific concepts as messages, the Windows Memory Manager and dynamic-link libraries. OS/2 Graphics Programming Interface: An Introduction to Coordinate Spaces Coordinate spaces, which define drawing areas, are an important component of the OS/2 Presentation Manager. Transforms allow the mapping of objects from one coordinate space to the next. Coordinate spaces under the micro-PS and some of the GPI functions for handling transforms are explored. Microsoft(R) Macro Assembler Version 5.1 Simplifies Macros and Interfacing Macro Assembler 5.1 provides significant new features that enable the programmer to easily develop macros and interface to high-level languages. Other new features include the addition of high-level constructs such as ELSE extensions, local labels, and type directives. Color Mixing Principles and How Color Works in the Raster Video Model The underlying principles of color generation are complex. Exploring some common color mixing models such as the RGB (Red-Green-Blue) and HSV (Hue-Saturation-Value) models leads to a better understanding of the methods employed for creating colors on PC video displays. Creating User-Defined Controls for Your Own Windows Applications The ability to define your own controls functionally extends the Windows user interface, giving your applications greater visual appeal and the user greater program control. Palette, a color mixing program, utilizes several such controls, as provided in a program called Spectrum. SQL Server Brings Distributed DBMS Technology to OS/2 Via LAN Manager SQL Server, in combination with the OS/2 LAN Manager, brings mainframe and minicomputer power to networked personal computers. It adds stored procedures, triggers, and an advanced transaction-oriented kernel, essential features for assuring data integrity, to SQL. Ask Dr. Bob EDITOR'S NOTE What does OS/2 mean for the professional programmer? For one thing, it means new ways of thinking about how programs are developed. By itself, multitasking is a boon to programmers; it's a real time-saver to have an editor, compiler, and debugger all running concurrently. Debugging under OS/2 with the Presentation Manager (PM) offers some new alternatives that just weren't feasible under DOS. In the last issue we presented a debugging utility for Windows applications consisting of about 600 lines of code. Building an equivalent debugging tool under Presentation Manager is a trivial task. Let's assume that you have at your disposal a UNIX(R)-like grep function that will locate whatever strings you might be looking for. Within your PM application, whenever you want to generate debugging information you simply include a printf statement, for example: printf('17 BeginPaint returned 0x%04x\n', usResult); You would then start your PM application from a windowed instance of CMD.EXE: start myPMapp | grep '.*' which would bring up the PM application in a window, while the printf output ends up being piped to the instance of grep running in the CMD.EXE window. With grep, you could, for example, filter out everything but type 17 messages simply by starting the PM application as follows: start myPMapp | grep '^17' The appropriate messages would then be filtered out. Of course, you would need a much more elaborate OS/2 VIO-based filter for significantly greater flexibility. The multitasking/windowing facilities available under OS/2 and Presentation Manager present programmers with a far richer set of options, in many cases making formerly complex tasks much simpler. The successful OS/2 programmer will be the one with the vision to put these new advantages to work.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor KAREN STRAUSS Assistant 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) 1988 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, and CodeView are registered trademarks of Microsoft Corporation. CompuServe is a registered trademark of CompuServe, Inc. dBASE, dBASE III, and dBASE III PLUS are registered trademarks of Ashton-Tate Corporation. EtherNet is a registered trademark of Xerox Corporation. IBM is a registered trademark of International Business Machines Corporation. Token-Ring is a trademark of International Business Machines Corporation. Intel is a registered trademark of Intel Corporation. Above is a trademark of Intel Corporation. AST and RAMpage! are registered trademarks of AST Research, Inc. SQLBase is a registered trademark of Gupta Technologies, Inc. UNIX is a registered trademark of American Telephone and Telegraph Company. ████████████████████████████████████████████████████████████████████████████ DARWIN: Merrill Lynch Develops a New Workstation Based on Windows 2.03 Tony Rizzo and Karen Strauss What do you do when you need information from a wide variety of databases, each with its own access method, and you want to ensure vendor independence in workstation hardware and applications software, but you also want to provide a common, graphics-oriented user interface with report-writing capabilities? You turn to the Windows development environment and write a snappy application that uses all of the tools that Windows puts at your disposal. Corporate MIS, a relatively new division of Merrill Lynch & Co., Inc., is responsible for providing headquarters management with financial control and human resources information. In the past, this information has resided in many separate database systems, each with its own access method, usually incompatible with the others. Because of the difficulties in combining them, about a year ago Corporate MIS began considering new ways of accessing data through personal computers. A key issue for Corporate MIS was the development of a strategy to allow different programming teams within the division to work independently of each other, yet adhere to common design specifications. For example, the database design group would work separately from the information presentation group, but each would develop applications to be compatible and consistent across all groups. Corporate MIS came up with a plan to base all future organization of its data on the relational model and use Structured Query Language (SQL) as the means of accessing the data. Its database designers would be free to select the relational technology best suited to any given task. The database engine could be based on a LAN, a mainframe, or a database machine──the only requirement would be that the technology had to be able to execute "standard" SQL queries. Workstation designers directly concerned with how data is presented on a PC to the end user would only have to deal with generating standard SQL requests for data and handling data returned to the workstation from the database engine. On the user side, the Corporate MIS plan required that workstation designers use a common interactive graphical interface. With a PC-based graphical interface a designer no longer had to be concerned with building applications that rely, for example, on asynch or 3270-emulation windows. Also, Corporate MIS wanted to provide users with a common report writer facility to simplify formatting data. This plan envisioned a common development tool that any workstation designer could incorporate into an application whenever that application had to access and format data. It was at this point that Corporate MIS turned to its PC Software Development group. "Our charter was to design a method for querying SQL databases using icons or pictures of what the user wanted to do," says Anthony Pizi, an assistant vice president within Corporate MIS responsible for PC software development. Pizi decided to use the Microsoft(R) Windows Version 2.03 environment as his tool's common graphical user interface; after all, Microsoft Excel, with its integrated, Windows-based interface, had been an instant success with Merrill Lynch analysts. "This was our first major development effort using Windows, so the project took on a subtheme demonstrating that Windows really could deliver the graphical interface and software integration that we needed," says Pizi. "And so far, so good." As the software begins to take shape, according to Pizi, Merrill Lynch is simultaneously reorganizing its PC hardware, under which most PCs will eventually be connected to various LAN systems, such as EtherNet(R) and Token-Ring(TM). Defining the System Pizi had to implement and integrate three specific functions into his design. First, a front-end system of menu options, dialog boxes, and pop-ups would let a user formulate complex requests by simply pointing and clicking on various fields and commands with a mouse. Second, the application had to be able to take requests formulated by the user, translate them into SQL requests, send them to the appropriate database subsystem, and handle the returned data. Finally, the application required the ability to format data easily in any manner. This set of tools would not be tied to any specific database. The original model might, for example, be based on Gupta Technologies' SQLBase(R), but any engine could later be integrated into the system. Furthermore, the entire application would be written to be NetBIOS- compatible, permitting it to run on any workstation attached to any LAN supporting NetBIOS. The workstation/LAN environment would be hooked into a SQL Network Gateway, providing links to such mainframe databases as Merrill Lynch's IBM(R) DB2 system. For end users the resulting applications and workstations would provide: ■ consistent hardware configurations ■ a consistent user interface ■ remote access to many corporate databases ■ a simple means of generating SQL queries ■ substantial report writer capabilities These new systems would improve Corporate MIS's ability to leverage existing systems and human resources, offering: ■ reduced costs ■ increased productivity ■ improved information flow ■ enhanced and more informed decision making ■ better control of the PC environment Building DARWIN Pizi's group started to experiment with the Windows development environment and to develop the strategies for communicating with remote back-end database systems. Once the group had demonstrated their ability to talk with a database through various front-end models they put together under Windows, Pizi felt they were ready to begin. The design for Data Access and Reporting Through Windows (DARWIN) was turned over to Steve Canny and Brian Hayden, two senior programmer/analysts. "On December 15 of last year the first line of DARWIN code was written," says Pizi. Five months later DARWIN was virtually complete. As Canny tells it, "In the beginning, building DARWIN was a classic example of design on the fly. Neither Brian [Hayden] nor I were Windows masters. We would see some portion of what needed to be done and go off and see if we could get Windows to do it. It was kind of like a jigsaw puzzle, where you find pieces one at a time. Occasionally two or three pieces would fall into place together. "One of us would come up with the basics of an idea, we'd discuss it, then implement it in the next version of the software. Since I had the most experience with graphics programming, I developed the user interface. Brian knew the database end of things and designed the link to the database server. Brian also developed the code for all of the DARWIN dialog boxes that present data to the user. Eventually, we were able to get the entire thing to compile, and the beta version of DARWIN was born." DARWIN and Microsoft Excel Originally, DARWIN was to be responsible for displaying and printing the query results. "We had managed to put together a low-level stub for handling the display and were sweating about the thought of writing a display and print manager that had reasonable features," Canny says. "Fortunately, it was just about then that Tony [Pizi] came in shaking his hands over his head and talking fast, the way he usually does when he's had a particularly good brainstorming session. He told us about his idea to send DARWIN's output directly to Microsoft Excel, and we told him he was crazy. Two days later we began to think that maybe it wasn't so crazy. Then we became absolutely convinced that sending the output directly to Microsoft Excel was the way to go," says Canny. A user goes into DARWIN, builds a query, and the data goes directly into a Microsoft Excel spreadsheet. This idea resulted in a substantial breakthrough, and the third tool for which DARWIN was responsible began to fall into place. The design changed so that Microsoft Excel became, in a sense, a part of DARWIN; the spreadsheet would serve as DARWIN's report writer (see Figure 1) and would, in fact, be responsible for bringing up DARWIN itself. Using DARWIN To bring up DARWIN, the user executes a Microsoft Excel macro, which supplies DARWIN with its menu bar and hides Microsoft Excel. Once DARWIN is running, the macro controls whether the user is in DARWIN itself or in Microsoft Excel by monitoring the appropriate menu option provided in the two applications. The user doesn't even have to know how to start Microsoft Excel or run a macro if a program such as the hDC Computer Corporation ClickStart applications organizer is used──simply clicking on an icon will start DARWIN. DARWIN establishes the underlying connections to the required database. The database can be a server on a network, or it can be a mainframe database that is linked to via a SQL Network Gateway (see Figure 2). Gupta's SQLBase supplies the necessary software to make this mainframe link look like just another local network-based server to DARWIN. DARWIN offers the user database choices. Tables of data are grouped together as categories; each category consists of preestablished collections and groupings of data tables. The user first selects the desired database category. Once a selection is made, DARWIN provides a list of the fields in each table and menus of tools for extracting the required data (see Figures 3, 4, and 5☼). DARWIN's client area, which is an iconic representation of the query being assembled, is divided into two child windows. The top window containing the short horizontal scroll bar is the Choice List window; the other is the Database Context window. The Choice List area corresponds to the SQL SELECT clause; it displays the columns that will make up the result table, that is, the information for which the database is being queried. The Database Context window corresponds to the FROM and WHERE clauses; it shows the tables from which the selected columns come and the constraints and joins that are detailed in the WHERE clause. [For more information on SQL see "SQL: A Short Primer" in this issue──Ed.] To the user of the DARWIN query tool, the tables of data are represented on the screen as boxes, and they consist of one or more columns represented on the screen as rows, not to be confused with rows of data. These boxes are simple child windows (windows without scroll bars, menu bars, and so on) that can be collapsed by clicking on the minimize arrow and dragged around the client area with a mouse. Creating Joins Joins are created by clicking the left mouse button on the first data item, then clicking the right mouse button on a second data item in another table. The left-right button combinations are analogous to setting up a "from (the left button) ... to (the right button)" relationship. After the right button is clicked DARWIN draws a line from one table to another (see Figure 6☼). The join that is formed by default is an equi- join, the most commonly used join. The type of join created is easily modified by the user (see Figure 7☼). Join constraints are established by clicking on an appropriate column and using the dialog box that is then presented. Several constraint options, such as the mathematical symbols for greater than, less than, less than or equal to, and so on, are offered. The user establishes data constraints through a similar process (see Figure 8☼). The user is thus able to formulate multitable queries based on joins made only through mouse manipulations and menu choices presented by DARWIN. With a visual summary of database tables displayed on screen, users can easily discover relationships between collections of data that they didn't realize they had access to. The user is provided with all the information and tools necessary to build a detailed query, based on standard Windows objects: windows, listboxes, and pull-down/pop-up menus. Figure 9☼ shows the resulting graphical representation of a join with a data constraint. DARWIN takes the user-supplied information, which is stored in a single dynamic local data structure, and creates a standard SQL query. Once the actual SQL statements are created, the user clicks on the RUN! command, and DARWIN does the rest, going out to the necessary tables, collecting the data, and writing it out to a CE_BIFF file, which is the Microsoft Excel file format. When DARWIN indicates that a query has been completed and a result table thus created, the user simply toggles back to Microsoft Excel by clicking on the View Output option under the Window pull-down menu (see Figure 10☼) and finds the data, supplied through the CE_BIFF file that has just been created, waiting in a standard Microsoft Excel spreadsheet (see Figure 11☼). According to Pizi, because Microsoft Excel was designed with the capacity to handle database information, the spreadsheet already had some of the most important tools for becoming a report writer. Data can be modified and formatted and statistics generated by using all of Microsoft Excel's spreadsheet, formatting, and charting features. The user can even click on the A1 cell, and Microsoft Excel will return information related to the underlying SQL request made in DARWIN (see Figure 12☼). The user can also view any actual SQL query statement that is in the process of being built, and experienced users are even able to modify the query through a feature called SQL EDIT (see Figures 13 and 14☼). At this point, according to Pizi, more complex nested queries are not yet supported, but a knowledgeable user can build more complex requests through SQL EDIT. Tight Integration Microsoft Excel and DARWIN are very tightly integrated. "We like people to ask, 'Are you in DARWIN or Microsoft Excel?'" Hayden says. "It is so seamless, it is hard to tell where one ends and the other begins [see Figure 15]. Basically, when the table screens are up, you are in DARWIN. When you have the data you see in the report, you are in Microsoft Excel." Hayden credits Microsoft Excel for this now-you-see-it, now-you-don't quality. "Microsoft Excel has an incredibly rich program language and allows you to use Windows function calls via macros that make Windows environment calls. The macro we've developed in Microsoft Excel maximizes an application, in our case DARWIN, and adds DARWIN's menu bar," he says. "The command is 'minimize Excel, maximize DARWIN,' so DARWIN comes up on the screen. Microsoft Excel is the landing pad for the data DARWIN provides," says Hayden. When the user wants to view the data in Microsoft Excel, the process is reversed. Figure 16 shows an early version of the macro; a close inspection of the macro demonstrates how simple it is to go from the spreadsheet to DARWIN and back. Powerful Environment The power resulting from the integration of DARWIN and Microsoft Excel was not immediately obvious to Canny and Hayden. Canny says, "When we first explored Microsoft Excel as a possibility, it took a while to assimilate the technology. Over time, as we better understood the environment we realized what a really great opportunity there was for developing a unique application." For example, the format in which data returned by DARWIN would be represented required much thought, and various scenarios were drawn up. Canny says, "At first, there was a good deal of experimentation. We didn't have all the specs on Microsoft Excel and were waiting around for that. The CE_BIFF format was not our first choice; we arrived at it after trying a number of different alternatives. Our first attempt was to have DARWIN write out a simple comma-delimited ASCII text file. "But then the Microsoft Excel development people clued us in to the CE_BIFF format. The availability of different fonts and the ability to set results in a single cell, as well as the accuracy of the CE_BIFF format, made it immediately apparent that CE_BIFF was the way to go. It was straightforward and well documented by Microsoft. With two or three days of research and two or three days of coding, we had it working." Canny now considers Microsoft Excel to be a cooperative environment because it can be used as a Windows programming tool or an adjunct. "You write programs for things that Microsoft Excel does not specialize in, and you send to Microsoft Excel the jobs it does specialize in," he says. "People want calculations, editing, and graphs to make it nice for the boss. Microsoft Excel already does this very well, and we didn't see reinventing those tasks in DARWIN." For all its strengths, the Microsoft Excel environment has its frustrations as well, Hayden says. "Microsoft Excel appears to break Microsoft's own recommended standard that you never hog memory, that the application give it up as soon as it finished. Microsoft Excel goes out and asks, 'How much memory is here? Give me all of it.' It is breaking the friendliness standards." In contrast to Microsoft Excel, which uses all the available memory, DARWIN uses about 175Kb of memory. It takes up the most memory (just over 300Kb) when you are using the constraint dialog box, the pull-down menu that lets users set parameters on their queries. Windows Challenges Hayden says he felt challenged by the need to reconcile himself to the device-independent nature of the Windows environment. Canny concurs: "Windows is a very different programming environment from the old sequential model, in which program control flow goes to the first function, the second function, the third function, goes back to the second function, and finishes. With Windows, you have a series of independent, often asynchronous processes, triggered by another process. That is a very new concept for most people who work in the field." Hayden adds, "You've got to follow the modular specs. If you start playing tricks, you're dead." Hayden feels that Windows is an important step forward in application development. "Windows is like C at the very beginning: a beautiful language, but with no libraries. That means initially developing an application is very difficult. You have to rebuild repetitive routines, although people are starting to release some Windows libraries to make things easier. But once you understand it, Windows is really good. It is a liberating mindset." SQL The design team appreciates SQL because of the rigorous mathematical underpinning established by E.F. Codd in his original paper on relational databases ("A Relational Model of Data for Shared Data Banks," Communications of the ACM 13, No. 6, June 1970). According to Pizi, "This mathematical basis is our assurance that a database designed with the techniques of normalization will serve the emerging and changing needs of the organization in the future. It gives us a firm foundation on which to build." This appreciation of SQL is reflected in some of the design considerations that went into DARWIN. As Pizi points out, he wanted to remain true to what SQL actually is, namely a rigorously defined query language. Therefore, when DARWIN generates a SQL query, it creates a pure set of SQL statements that have no other purpose than to go out and extract data. DARWIN itself has no ability to format data or set up column headings; a number of the current implementations of SQL include user shells to handle such things. Pizi feels that this is a report writer issue, the execution of which should be handled locally at the user's "personal" machine. According to Pizi, "An IBM 3090, for example, should not be spending any of its time formatting data and worrying about where to put it; you shouldn't use the mainframe to manipulate the cursor." This reflects the design team's view of what distributed processing is all about. This approach isolates DARWIN from the data it is working with, which could come from any database. According to Hayden, "DARWIN itself doesn't know anything about the database." This isolation allows Corporate MIS's database designers to use any database technology they want. SQL API and DDE DARWIN speaks to a SQL Applications Programming Interface (SQL API) that Hayden wrote. The interface translates DARWIN's query output into whatever vendor-specific APIs may be out there. From the workstation designer's perspective, DARWIN will become a tool that offers the designer a virtual SQL interface to all of Merrill Lynch's relational database subsystems. According to Pizi, eventually "Windows' Dynamic Data Exchange [DDE] facility will be the pipeline to these APIs." Says Hayden, "DDE fits in where DARWIN talks to another application that is doing the database querying, in this case the SQL API. In the future, when we go to different databases simultaneously, DARWIN as a program does not have to be re-released." The SQL API will simply be modified as necessary for each different database. It sits on one end of DARWIN, and DDE will handle the interface between the two. "Since DARWIN is growing into a large and complex system with more than 10,000 lines of code, the question of managing complexity has to be raised," says Canny. According to Pizi, the current design falls into several categories: 48 percent of the code is devoted to Windows overhead, 32 percent makes up the SQL API, 18 percent is dedicated to graphics, and 2 percent of the code deals with creating CE_BIFF files. The one piece of advice that Canny has for other Windows developers is to "make sure that all Windows code is built in a modular fashion. With DARWIN, we need to be able to make additions to it without upsetting what already exists. The program needs to be organized into separate modules, with simple input and simple output that is well defined." The SQL API is an example of this design goal. "Our SQL API isn't really part of Windows," said Hayden. "It is more like a device driver. It breaks DARWIN off from the database." Based on the SQL library of Gupta's LAN- based SQLBase product, the core of the SQL API is founded on seven "shell" functions, which are shown in Figure 17. According to Pizi, this ability to insulate DARWIN from the particular SQL engine is almost as important as DARWIN's point-and-click queries. It relieves the database administrator of the need to support multiple proprietary report writers, which are typically tied to specific database engines. Most importantly, everything conforms to only one standard. The DARWIN-Microsoft Excel combination anticipates in some ways the integration promised by the MS(R) OS/2 SQL Server and the IBM OS/2 Extended Edition. The Merrill Lynch developers say they will enhance the SQL API, which now interfaces to Gupta Technologies' SQLBase engine, to support SQL Server and the IBM product as they become available. They expect most leading database vendors to respond to user demand for a standardized interface between SQL engines and front-end user interfaces. Virtual Data Dictionary In order to communicate with any given database, DARWIN must understand that system's data dictionary. DARWIN also needs certain information that existing data dictionaries do not provide, such as printname, defined column widths, and standard data types. "Our virtual dictionary is a group of seven tables that supplement all the other data dictionaries. All DARWIN requires is that these seven tables be present in those data dictionaries," says Canny. If the tables are present in a given data dictionary, DARWIN can communicate with them (see Figure 18 for an example of one of these tables). What DARWIN Really Is As Pizi puts it, DARWIN is another tool that the application programmer will add to his or her collection of development tools. It simplifies the designer's work because the interface to the data and the user are already in place. It frees the designer to spend more time dealing with what the end user of an application actually needs and provides a standard means for creating a design. But what about end users? "It's not completely clear whether DARWIN is a tool for end users or application developers. It falls somewhere in between," Pizi says. He does not expect DARWIN to be released as a standalone product, though he knows that plenty of people will ask for it. However, Pizi does think that users will eventually push the product further ahead with their own Microsoft Excel macros. "There are rich possibilities here that we intend to explore," he says. Even Microsoft Excel's developers were excited when they saw a demonstration of DARWIN working with the spreadsheet, and now they closely follow the Merrill Lynch product's development, according to Pizi. "We're taking a tool that they produced and actually using it the way they intended." Canny and Hayden say DARWIN is just a first step toward Merrill Lynch's goal of vendor independence in both workstation hardware and application software by moving the company's development platform to a common, stable, graphics-oriented user interface: Windows 2.03 and Windows/386 today, OS/2 Presentation Manager in the future. In fact, Pizi and his team eagerly await the official release of the Presentation Manager. They look forward to its rich set of graphical tools and especially to the freedom from the DOS memory limitations that OS/2 will offer. Despite the somewhat steep learning curve, DARWIN is now falling into place, and future development looks promising. As Canny says, "We are now in the process of sizing up what we have, cleaning it up, and applying a more structured approach. We don't move forward as quickly, but we no longer step backward either. Also, this approach allows us to be more predictable with our releases. This is very important now that we've gotten management's attention; they ask us for promises and expect us to deliver on them. We've made the transition from a 'no questions asked' R&D group to a more traditional and responsible systems development group." Concludes Pizi, "We're producing real tools. This isn't a toy any longer." Figure 1: DARWIN acts as an intermediary, or "front end" between Merrill Lynch's SQL databases and the analysis and reporting applications of its users. ┌───────────────┐ ┌───────────────────────────────────┐ │▓▓▓▓▓▒▒▒▒▒░░░░░│█ │─██────────────Darwin───────────↑↓─│█ │▓▓▓ Merrill ░░│█◄─────Queries──────┤┌────────────────────────────────┐│█ │▓▓▓ Lynch ░░│█ ││ DARWIN │▒│█ │▓▓▓ Database ░░│█ ││ │▒│█ │▓▓▓ Universe ░░│█ ││ Data Acces and Reporting │▒│█ │▓▓▓▓▓▒▒▒▒▒░░░░░│█──Requested Data──►││ Through Windows │▒│█ └───────────────┘█ ││ │▒│█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │└────────────────────────────────┘▼│█ │ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█ └───────────────────────────────────┘█ ┌───────────────────────────────────┐ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ├─██───────Microsoft Excel───────↑↓─│█ Formatted │┌──┬─────┬─────┬─────┬─────┬─────┐│█ Output │├──┼─────┼┌─────────────┐──┼─────┤▒│█◄─────────────────┘ │├──┼─────┼│ Microsoft │──┼─────┤▒│█ │├──┼─────┼│ Excel │──┼─────┤▒│█ │├──┼─────┼└─────────────┘──┼─────┤▒│█ │├──┼─────┼─────┼─────┼─────┼─────┤▒│█──User Formatted Reports──► │└──┴─────┴─────┴─────┴─────┴─────┘▼│█ │ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█ └───────────────────────────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Figure 2: A detailed view of Merril Lynch's proposed front-end/back-end interconnections. Host Computer ┌─────────────────────────────┐ │ MVS │ ├─────┬────────┬─────┬────────┤ │ │Teradata│ │ CICS │ │ TSO │Director│ DB2 ├────────┤ │ │Program │ │SQL Host│ └──┬──┴───┬────┴──┬──┴─┬───┬──┘ │ │ └────┘ └─────────────────────LU 6.2 APPC/PC────┐ │ └─────────────────────────────────────────────┐ │ └──────────────────────────────────┐ │ │ ┌────────────────┐ ┌─────▼──────┐ ┌─────▼──────┐ │ ┌───┤ Communications │ │ Interface │ │ Interface │ │ │ │ Processor │ │ Processors │ │ Processors │ │ │ └───────┬────────┘ └─────┬──────┘ └─────┬──────┘ │ │ │ │ │ │ │ ┌───────┴───────────────────┴─────────────────┴──────┐ │ │ │ Ynet │ ≈ │ └───┬──────────────┬──────────────┬──────────────┬───┘ │ │ │ │ │ │ │ │ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ ┌─────┴─────┐ │ │ │ Access │ │ Access │ │ Access │ │ Access │ │ │ │ Module │ │ Module │ │ Module │ │ Module │ │ │ │ Processor │ │ Processor │ │ Processor │ │ Processor │ │ │ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ └─────┬─────┘ │ │ │ │ │ │ │ │ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ │ │ ▓▒░Data░▒▓ ▓▒░Data░▒▓ ▓▒░Data░▒▓ ▓▒░Data░▒▓ │ │ ▓▒Storage▓ ▓▒Storage▓ ▓▒Storage▓ ▓▒Storage▓ │ │ ▓▒░Units▒▓ ▓▒░Units▒▓ ▓▒░Units▒▓ ▓▒░Units▒▓ ┌─┴───────┐ │ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ ▓▒░░░░░░▒▓ │ SQL │ │ │ Network │ TCP/IP │ Gateway │ OSI ┌─────────────┐ └─┬───────┘ XNS (June │ DARWIN │ │ 1988) │ Workstation │ │ ┌────┴───────┐ └──────┬──────┘ │ │ Function ├────────┬──────┴─────NetBIOS LAN──────┬─────────────┴─┐ │ Call │ ┌──────┴──────┐┌──────┴──────┐┌──────┴──────┐ ┌─────┴────┐ │ Translator │ │ DARWIN ││ DARWIN ││ DARWIN │ │ SQL │ └────────────┘ │ Workstation ││ Workstation ││ Workstation │ │ Database │ └─────────────┘└─────────────┘└─────────────┘ │ Server │ └──────────┘ Figure 15: DARWIN: Top Level Dataflow Diagram ┌──────────────┐ │ Standard SQL │ │ Text Queries │ └┬────────────┘ ┌───────────────┐ │ │ ┌───────────────────────────────────┐ │▓▓▓▓▓▒▒▒▒▒░░░░░◄───┘ └───┼─██────────────Darwin───────────↑↓─│█ │▓▓▓ Merrill ░░│█ │┌────────────────────────────────┐│█ │▓▓▓ Lynch ░░│█◄──────────────────┤│ DARWIN │▒│█ │▓▓▓ Database ░░│█ NetBIOS Network ││ │▒│█ │▓▓▓ Universe ░░│█──────────────────►││ Data Acces and Reporting │▒│█ │▓▓▓▓▓▒▒▒▒▒░░░░░│█ ││ Through Windows │▒│█ └───────────────┘█ ┌────────────►│ │▒│█ ▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀ │ │└────────────────────────────────┘▼│█ ┌▼──────────┴┐ ┌───┤ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█ │ Result │ │ └─────────────────────────────────┘█ │ Table │ │ ▀▀│▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀▀▀▀▀▀│▀│▀▀▀ └────────────┘ │ Mouse and Graphical │ │ ┌─────┴─┐ Keyboard Input Display │ │ │Results│ ╔═╧══════════════╗ │ │ │ ┌───┤ Table │ ║ ░░░░░░░░░░░░░░ ◄─────┘ │ │ │ └───────┘ ║ ░░░░░░░░░░░░░░ ║█ Control │ ║ ░░░░░░░░░░░░░░ ║█ │ │ ▓▓▓▒░░▼░░▒▓▓▓ ║ ░░░░░░░░░░░░░░ ║█ Messages ▓▓▓▒░░░░░▒▓▓▓ ╔═╝────────────────╚═╗ │ │ ▓▓▓CE_BIFF▓▓▓ ║ ‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼‼ ║█ │ │ ▓▓▓ Files ▓▓▓ ╚═══════════════════╝█ │ │ ▓▓▓▒░░░░░▒▓▓▓ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀│▀│▀▀▀▀ │ │ ▓▓▓▒░░░░░▒▓▓▓ User │ │ │ │ │ │ │ │ │ │ │ │ │ │ │ ┌────────────────┴─▼─────────────▼─┴┐ │ ├─██───────Microsoft Excel───────↑↓─│█ ┌───────────┐ └────────────►┌──┬─────┬─────┬─────┬─────┬─────┐│█ │≡≡≡≡≡≡≡≡≡≡≡├┐ │├──┼─────┼┌─────────────┐──┼─────┤▒│█ │≡≡≡≡≡≡≡≡≡≡≡│├┐ │├──┼─────┼│ Microsoft │──┼─────┤▒│█ │≡≡≡≡≡≡≡≡≡≡≡│││ │├──┼─────┼│ Excel │──┼─────┤▒│█ │≡≡≡≡≡≡≡≡≡≡≡│││◄─User Formatted──┤├──┼─────┼└─────────────┘──┼─────┤▒│█ │≡≡≡≡≡≡≡≡≡≡≡│││ Reports │├──┼─────┼─────┼─────┼─────┼─────┤▒│█ │≡≡≡≡≡≡≡≡≡≡≡│││ │└──┴─────┴─────┴─────┴─────┴─────┘▼│█ └┬──────────┘││ │ ◄≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡≡► ╝│█ └┬──────────┘│ └───────────────────────────────────┘█ └───────────┘ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Figure 16: DARWIN.XLM ╓┌───┌───────────────────────────────────────────────────────────────────────► A 1 DARWIN Macro 2 PC Software Development 3 4 5 6 =APP.MAXIMIZE() 7 =ECHO(FALSE) 8 =ADD.MENU(1,B6:C8) 9 =ADD.MENU(2,B6:C8) 10 =ADD.MENU(3,B6:C8) 11 =ADD.MENU(4,B6:C8) 12 =ADD.MENU(5,B6:C8) 13 =ADD.MENU(6,B6:C8) 14 =EXEC("C:\WINDOWS\DARWIN\DARWIN.EXE",3 15 =HIDE() 16 =RETURN() 17 18 OUTPUT 19 =ECHO(FALSE) 20 =IF(GET.DOCUMENU(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(A29),GOTO(A21)) A 20 =IF(GET.DOCUMENU(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(A29),GOTO(A21)) 21 =DIRECTORY("C:\WINDOWS\DARWIN") 22 =OPEN("UNNAMED.XLS") 23 =COLUMN WIDTH(22,"C1") 24 =COLUMN WIDTH(22,"C2") 25 =COLUMN WIDTH(22,"C3") 26 =COLUMN WIDTH(22,"C4") 27 =DISPLAY(FALSE,FALSE,TRUE,TRUE,0) 28 =FULL(TRUE) 29 =RETURN() 30 31 LAYOUT 32 =IF(GET.DOCUMENT(1,"UNNAMED.XLS")="UNNAMED.XLS",GOTO(CLOSE),GOTO(DARWIN)) 33 =APP.ACTIVATE("DARWIN",FALSE) 34 =SEND.KEYS("%{F10}",TRUE) 35 =APP.MINIMIZE() 36 =RETURN() 37 38 39 A 39 40 CLOSE 41 =ACTIVATE("UNNAMED.XLS") 42 =CLOSE(FALSE) 43 =APP.ACTIVATE("DARWIN",FALSE) 44 =SEND.KEYS("%{F10}",TRUE) 45 =APP.MINIMIZE() 46 =RETURN() Figure 17: DARWIN SQLBase Interface Shell Functions 1 BOOL SQLExecute(string, bindstring); LPSTR string; /* null terminated SQL statement */ LPSTR bindstring; /* null terminated bind data */ Returns: TRUE if completed correctly. FALSE if error. ─────────────────────────────────────────────────────────────────────────── Note: The bindstring may be NULL, indicating that there is no bind value. ─────────────────────────────────────────────────────────────────────────── 2 int SQLFetchNextRow(); Returns: > 0 : number of fields in the row = 0 : no more information to fetch (past EOF) = ──1 : ERROR occurred ─────────────────────────────────────────────────────────────────────────── Note: This routine fetches the next logical row from the database into a SQL API internal structure for later use by SQLGetField. ─────────────────────────────────────────────────────────────────────────── 3 LPSTR SQLGetField(fieldnumber, datatype, flag); int fieldnumber; /* desired field (first field=1) */ LPINT datatype; /* datadictionary/ metaphore datatypes */ BOOL flag; /* TRUE=remove leading & trailing blanks */ Returns: LPSTR, which must be cast to the correct type for usage. NULL if field is null. ─────────────────────────────────────────────────────────────────────────── Notes: Setting flag = TRUE is suggested because SQLBase pads fields to maximum field width. If returned data type = string -> cast as LPSTR long -> cast as LPLONG date -> cast as LPDOUBLE double-> cast as LPDOUBLE typedef double FAR *LPDOUBLE /* defined in SQLAPI.H */ typedef long int FAR *LPLONG /* defined in SQLAPI.H */ ─────────────────────────────────────────────────────────────────────────── 4 LPSTR SQLGetFieldAsText(fieldnumber, datatype, flag); Returns: LPSTR to a NULL-terminated string. NULL if field is null. ─────────────────────────────────────────────────────────────────────────── Note: Setting flag = TRUE is suggested because SQLBase pads fields to maximum field width. Numericals are padded to 27 digits in ASCII representation. ─────────────────────────────────────────────────────────────────────────── If returned data type = string -> use as LPSTR long -> use as LPLONG date -> use as LPDOUBLE double-> use as LPDOUBLE typedef double FAR *LPDOUBLE /* defined in SQLAPI.H */ typedef long int FAR *LPLONG /* defined in SQLAPI.H */ 5 BOOL SQLtoDlgListBox(string, bindstring, hDlg, DlgItem); LPSTR string; /* SQL SELECT statement */; LPSTR bindstring; /* null terminated bind data */ HWND hDlg; /* handle of a dialog box */ WORD DlgItem; /* item number of destination list box */ Returns: TRUE if completed successfully. FALSE if error. ─────────────────────────────────────────────────────────────────────────── Note: This function allows one function call (from a dialog box) to perform a complete SQLBase execute and fetch sequence. The fields are laid out as a single list box item with the fields separated by a blank space. Field width is maintained as returned by SQLBase as strings. ─────────────────────────────────────────────────────────────────────────── 6 int SQLToBiffFile(select, bindstring, bifffilename); LPSTR select; /* SQL SELECT statement */ LPSTR bindstring; /* null-terminated bind data */ LPSTR bifffilename; /* filename for BIFF file */ Returns: Number of rows written. ─1 if error occurred. ─────────────────────────────────────────────────────────────────────────── Note: This function allows one function call to perform a complete SQLBase execute-and-fetch sequence. The fields are laid out as cells within a new Excel Biff file. ─────────────────────────────────────────────────────────────────────────── Bindstring and bifffilename may be NULL. 7 int SQLErrorNum(); Returns: An integer defining the type of error, if any, that occurred. ─────────────────────────────────────────────────────────────────────────── Note: To maintain the design that hides SQLBase specifics, the SQLBase internal error number and information will be translated into DARWIN internal values yet to be defined in SQLAPI.H. ─────────────────────────────────────────────────────────────────────────── Figure 18: DAR_COLUMNS contains information on the columns that comprise a table. For each column in a table there is one row in DAR_COLUMNS. Each row in the table is a column in DAR_COLUMNS. ╓┌──────────────┌────────────────┌─────────────┌─────────────────────────────╖ ┌─────COLUMN NAME─────┐ INTERNAL EXTERNAL DATA TYPE DESCRIPTION ┌─────COLUMN NAME─────┐ INTERNAL EXTERNAL DATA TYPE DESCRIPTION Tablename Internal Char (15) The internal name of the Table Name table that contains the column. Columnname Internal Char (15) The internal name of the Column Name column. Printname Column Print Char (30) The print name or external Name name of the column as it appears in DARWIN's graphical representation of the data. Datatype Data Type Integer The data type of the column. 0 = Integer 1 = Real (floating point) 2 = Character 3 = Date ┌─────COLUMN NAME─────┐ INTERNAL EXTERNAL DATA TYPE DESCRIPTION 3 = Date Precision Field Width Integer Contains the maximum number of digits or characters for datatypes INTEGER, REAL, or CHARACTER. If the column is a date datatype the field contains a 0. Fraction Fraction Size Integer The number of digits to the right of the decimal point. This is used for real (floating point) numbers. Resolution Data Integer This column is not Interpretation supported. The field = 0. Keyusage Key? Integer Indicates if the column is ┌─────COLUMN NAME─────┐ INTERNAL EXTERNAL DATA TYPE DESCRIPTION Keyusage Key? Integer Indicates if the column is part of the primary key. 0 = Column is not part of primary key 1 = Column is part of primary key Domain Domain Integer This column is not used. The field = 0. Navalue N/A Value Char (30) This column is not used. The field = 0. Naused N/A Used Integer This column is not used. The field = 0. Dorder Display Order Integer The order in which the columns in the table are displayed. ┌─────COLUMN NAME─────┐ INTERNAL EXTERNAL DATA TYPE DESCRIPTION table are displayed. Description Description Char (200) A text description of the of Column column. ████████████████████████████████████████████████████████████████████████████ CodeView for Windows Provides an Interactive Debugging Environment Paul Yao and David Durant☼ Microsoft(R) Windows and CodeView(R) are two products that have made a significant impact on the PC world, mostly because of their highly interactive user interfaces. The CodeView user interface is based on character-oriented windows and makes a debugging tool accessible in a way that was not previously available on microcomputers. The Windows user interface is a suggested style and a set of powerful tools used to implement this style. This article will examine CodeView for Windows (CVW), a product that is familiar to many DOS programmers, but, until now, has not been "Windows- aware." First, we'll look at why an interactive debugger is useful to programmers, followed by an examination of the major stumbling blocks of Windows programming and how to avoid some of them. After a brief discussion of CVW's hardware requirements, we will relate our experiences using a beta copy of CVW. Finally, a sample debugging session will trace the messages involved in the creation of an application's main window. CodeView employs many of the features found in Windows applications (see Figure 1☼), including drop-down menus, dialog boxes, windows, and mouse support. It is paradoxical that Windows programmers have for so long been unable to use a tool so akin to their own environment. There is a simple explanation for this: the two products were created independently by different development groups within Microsoft. CodeView was not originally developed with Windows applications in mind; it was geared toward the development of DOS applications. Although its interface is similar to that of Windows, CVW is not itself a Windows application in the usual sense. Instead, it is a special version of the original CodeView that has been made aware of the way in which the Windows memory manager juggles memory. What strikes you first when using CVW is that it does not share the screen with Windows; rather, it sends output to its own display monitor in exactly the same way that Symdeb, another interactive debugger, does. Interactive Debugger In the Windows programming classes that we teach, we are constantly astounded by the number of people who have never used an interactive debugger. There are certainly useful alternatives to an interactive debugger, such as the embedding of calls to MessageBox to display data and the inclusion of calls to fprintf to output trace information to a communications port. However, these techniques are cumbersome when compared with the power and flexibility available in an interactive debugger, where you have total control of your computer while your program is processing. Without an interactive debugger, you must stop, rewrite, and recompile your code before you can begin execution. An interactive debugger offers you the flexibility to decide how to investigate the operation of your program. It allows for spontaneous interaction with a running program at a level not otherwise possible. Getting started with an interactive debugger is no trivial task, however. Setting up a debugging environment takes time, and you must achieve a minimum proficiency with a debugger before it is truly useful. With the tight development schedules that programmers face, the preparation of a debugging environment somehow never gets done. Still, it is definitely worth the time; people who start working with an interactive debugger reap enormous benefits almost immediately. Development Problems Even with an interactive debugger, debugging a Windows application presents some unique challenges. It would be bad enough if the dynamic loading, movement, and discarding of memory objects were the only source of problems. However, the structure of Windows programs, the lack of hardware memory protection, and the need to be good at so many things, such as Windows Software Development Kit (SDK) routines, Intel(R) architecture, and C programming, contribute to the difficulties encountered when coding and debugging Windows applications. For some of these problems, the only solution is time and experience. Windows programming takes everything you know about programming and turns it on its ear──as the saying goes, everything you know is wrong. Taking advantage of the power of Windows/Presentation Manager programming requires you to learn a new way of thinking. Six months of full-time Windows programming, assuming prior experience with C, is a fair estimate. While you are gaining this experience, however, there is error checking available for free, provided by the compiler and Windows, itself. It requires only that you flip the right switches. Here are a number of suggestions for overcoming some common Windows development problems by using these free services. Always use the highest level of compiler warning switches (W2 or W3), and aim for a clean compile. Prototype every routine that you write. For examples of prototyping, just look at the WINDOWS.H file, where you will see declarations like the one shown below: BOOL FAR PASCAL TextOut (HDC, short, short, LPSTR, short); This declaration gives the compiler some very useful information, such as the return value (in the example above, Boolean), the calling convention (FAR and PASCAL), the number of parameters (5), and the type of each parameter. If you prototype your routines, you can quickly and easily eliminate some C programming errors. For example, in the course of development, it is common to add parameters to various routines. If, however, you fail to change every call to a routine that has one more (or one less) parameter, your application will contain a bug. This bug may bite you by causing your application to fail immediately. In this case, you will probably search until you find the problem. Or the bug may sit and wait, creating sporadic and unpredictable problems in your program. Because tthis type of bug can be found almost instantly, prototyping can save you much wasted time and effort and should always be used with a W2 or W3 compiler warning switch. Avoid casting. It is redundant in many situations, and can hide bugs in others. Let the compiler tell you about type mismatches, and in those cases respond by casting. The following casting (the two LPPAINTSTRUCTs and the LPSTR) is redundant. hDC = BeginPaint (hWnd, (LPPAINTSTRUCT)&ps); TextOut (hDC, 10, 10, (LPSTR)"It's a Long Way to Tipperary",28); EndPaint (hWnd, (LPPAINTSTRUCT)&ps); It was included in the earliest examples of Windows programs, however, because the C compiler did not support prototyping at that time. Every version of the Microsoft C compiler since C Version 4.0 supports prototyping and eliminates the need to cast in many circumstances. Prototyping enables the compiler to promote a near pointer to a far pointer, based on the prototypes in WINDOWS.H. Be sure that you include every window procedure, dialog box procedure, enumeration procedure, callback procedure, and subclass procedure found in the EXPORTS list of your application's module definition (DEF) file. This is a very common error and wastes much time. The lack of compiler and linker error checking in this regard makes this problem difficult to find. Install and regularly use a debugging version of Windows, not to be confused with a Windows-compatible debugger, such as CodeView. You create this by using special copies (from the SDK) of KERNEL.EXE, USER.EXE, and GDI.EXE. A debugging version of Windows performs error checking not available in the retail version of Windows. It catches problems like bad handles and wild pointers and notifies you via a debugging terminal, which you must have ready to receive the fatal exit messages, sometimes referred to as "RIP" (Rest In Peace) codes. Be sure to include the symbol files, KERNEL.SYM, USER.SYM, and GDI.SYM, which are used to display stack trace information when RIP code is generated. With a debugging version of Windows installed, be sure to set the following switches in your WIN.INI file: [kernel] EnableHeapChecking=1 EnableFreeChecking=1 This causes all free bytes to be filled with the value CH. This protects you in two ways: first, if a wild pointer from your program causes a write to occur in a free block, a fatal exit message is sent to your debugging terminal. Second, you are protected if your program tries to jump to a free block. The value CCH translates into the machine instruction INT 3, which is a call to the debugger. A wild jump, then, causes the debugger to come alive, allowing you to display a stack trace and determine the part of your program from which execution departed. Because of these benefits, the use of these switches is strongly recommended. If you are using Expanded Memory Specification (EMS) memory (or running Windows/386, which has built-in EMS support), make sure you set the following switch in WIN.INI: [kernel] EnableEMSDebug=1 This switch makes sure the debugger is told when EMS pages are banked in and banked out and lets you set breakpoints within the discardable code segments of dynamic-link libraries, where many Windows SDK routines reside. Hardware Setup CVW has some unique hardware requirements, such as the need for a second monitor. We will review the choices available and discuss the need for EMS 4.0 memory. If you have been using Symdeb, the setup of the second monitor is the same, and you are ready for CVW. You have two choices: a monochrome display adapter (MDA) with a monochrome monitor or an asynchronous terminal with a null-modem adapter. The first choice is the preferable one, because you can then use CVW in full-screen mode. An asynchronous terminal is useful, though, since you can take advantage of the access to local variables that CVW provides. However, you must operate in line mode without full-screen support. When operating in full-screen mode, CVW lets you use the mouse to select menu items, and control its operation (when CVW has control). In this situation, CVW cooperates nicely by sharing the mouse with Windows. However, you must load a DOS mouse driver, which CVW will then use for its mouse support. Even if you don't plan on using a mouse (in line mode, for example), you still must install a driver for it. Otherwise, the Windows mouse freezes up, although you can still operate Windows with the keyboard interface commands. We hope this problem is corrected before CVW is released. EMS 4.0 Memory CVW requires you to have EMS memory present. CVW takes up 138Kb of main system memory and then employs EMS memory for its code, data, and symbol space. The available options depend on the specific Windows product that you are using. With Windows 2.x, you must have an EMS board and an EMS 4.0 or later driver. We experimented with an Intel Above(TM) Board, as well as with an AST(R) RAMpage!(R) board. Both worked well, but you must make sure that you have the correct version of the driver. EMS 4.0 support is built into Windows/386, which CVW can use. There are two things to keep in mind here. First, you must have a minimum of 640Kb main memory and 512Kb extended memory in order for EMS support to be available under Windows/386. Second, you must play a trick on Windows/386 so that its EMS support is loaded first; you then start CVW before starting Windows. This process is described in more detail in the documentation. CodeView and Windows Now that CodeView is available for debugging Windows applications, Windows programmers may ask if its debugging capabilities can really be adapted to the unique and extremely complex Windows environment. The answer is yes, but it is a qualified yes. Let's look at CodeView's strengths and weaknesses in the Windows environment──remember that our experience is based on a beta release of the software. Windows Programs As every Windows programmer quickly learns, a Windows application program does not control its own destiny; it lives at the mercy of messages sent by the user, by Windows, by other programs, and by itself. Windows programs do not always have control of the processor; instead they spend most of their time suspended, waiting for a message to arrive. When a message is delivered to a Windows program, it gains control of the processor and must respond to the particular message. The message is often delivered to the GetMessage processing loop. From there control is transferred to the appropriate window procedure. The decision is based on a piece of information that is part of a message, the window handle. Once a window procedure receives a message, it determines the message type and typically branches to a subprocedure referred to as a message procedure. Each message procedure is independent of the others in the sense that the processing of one message cannot assume that some other message has already been received or will subsequently be received. It can only check the current state of the application, such as the currently active cell of a spreadsheet, and react accordingly. The classic example of the extent to which Windows applications must react to messages is the WM_PAINT message. In response to a WM_PAINT message, a window procedure must be capable of recreating the contents of the client area at any time, for any reason, and under any circumstances. A Windows program, then, tends to be reactive; it is the user and not the program that controls the flow of processing. A Windows program can be thought of as a state machine, with the state stored in external variables. Windows programs also differ from normal programs in that they are object-oriented. Window classes, windows, display contexts, GDI objects, and loaded resources are all objects in a general sense: they are identified by a handle, you do not have to be concerned with the underlying data structures, and a given type of object, such as a window, can be manipulated in a standard set of ways (any window can be moved, sized, repainted, sent a message, and so on). Also, Windows is a library-oriented environment, specifically dynamic- link libraries. All the toolkit routines are contained in dynamic-link libraries. You can use dynamic-link libraries to share code, resources, data, and to control the allocation of some limited resource, such as system memory, or a local area network link. CodeView Debugging Programs have two parts: code and data. One of the chief uses of an interactive debugger like CodeView is to enable you to follow the path that your program takes through the code. During the course of such a "trace" operation, you can view and even modify the data on which your code operates. CodeView offers three methods of tracing execution: executing the program one line at a time, executing the program in "slow motion," and executing the program at normal speed but halting its execution at predetermined points within the code or based on prespecified data conditions. Executing the program one line at a time means either one source line or one object instruction at a time, depending on the mode that CodeView is currently in. Running in slow motion means that approximately four instructions per second will be executed and that the display of code and data are synchronized with program execution, continuously updated to reflect the current program location and data values. At any time you are running in slow motion, execution of the program can be halted to permit other interactions with CodeView. Hereafter, running the program in slow motion will be referred to as executing the program, while running the program at normal speed, which requires the Go command, will be referred to as just running the program. The third technique, running the program at normal speed and halting its execution at predetermined points, consists of setting breakpoints, watchpoints, and tracepoints. Setting a breakpoint means specifying a point in the code at which program execution is to halt. This is done by finding that line of code in the code display window and clicking upon it with a mouse or by using the breakpoint set command, bp. When you do this, the line will be displayed in high intensity. You can remove the breakpoint by clicking on the line a second time, which will return it to normal intensity. As many as 19 breakpoints can be set at a time. A brief word about an idiosyncrasy of CVW: CVW indicates that a breakpoint has been set by highlighting a line of code. However, highlighting also indicates the current line when tracing execution. These two unrelated uses make it difficult to determine if the current line is set as a breakpoint or not. To find out, you simply have to trace execution to another line. Just as breakpoints are execution halts set within the code, watchpoints and tracepoints are execution halts based on data values. The watchpoint is a conditional statement involving one or more program variables, such as "character count==80". Program execution halts at the instruction and causes the value of watchpoint to become true. A tracepoint is also a statement involving one or more program variables, such as iIndex, but it need not be a conditional statement. This is because the program is going to halt anytime the value of a tracepoint changes, regardless of what value it changes to. One nice thing about breakpoints, watchpoints, and tracepoints is that they remain in effect even after the program is finished executing. Thus you can set the watchpoints and breakpoints and run the program several times without having to reset them. One not-so-nice thing about watchpoints and tracepoints concerns local variables. Local variables, as most programmers know, are in effect only while the routine in which they are defined is actually being executed and are thus known only to CodeView at that time. Therefore, if you wish to set a watchpoint or a tracepoint involving a local variable, you must first set the breakpoint and execute the program up to that breakpoint so that the halted execution is now within the routine that contains the local variable. Once you have done this you can then define the watchpoints and tracepoints that involve that local variable, remove the breakpoint, and continue to execute or run the program. A limitation of CodeView or any interactive debugger is that input is shared between your program and your debugger. Your program needs input, of course, which it receives in the form of mouse and keyboard input, and debuggers also require input. In the case of CodeView, both keyboard and mouse input can be used. However, if you wish to debug mouse or keyboard input, switching into a debugger can be annoying. There are ways around this, of course, and we mention them not to detract from CodeView, but merely to identify an area that requires special attention when using an interactive debugger. For example, setting a breakpoint to trace through WM_MOUSEMOVE processing can be difficult, if not impossible. Another limitation inherent in any interactive debugger is that, with the interruptions caused by breakpoints and the ability to trace program execution, time- dependent operations can be difficult to debug. Thus, CVW is limited in its ability to work with programs dependent on WM_TIMER messages for their normal operation. Also, if there are external, real-time events that your Windows program must track, the time-dependent portions of your program may be impossible to debug with an interactive debugger. Windows Debugging Given that Windows programs react to messages, can be viewed as state machines, and are in an object-oriented and dynamic-link library environment, how can CVW help in debugging Windows programs? CVW handles the reactive nature of a Windows application program very well because of its ability to trace the flow of execution through code. By setting breakpoints, watchpoints, and tracepoints, you can see which message types are being received and what a program does in response to any given message. Later on, we will do just that by tracing the message traffic associated with the creation of an application's main window. CVW also fits well with the state-machine nature of a Windows application program. Since the state of the application is maintained in program variables and the user can watch those program variables change as the program runs, even halting execution when the variables reach some unexpected value, the user can remain aware of the state of the program as it is running and detect problems in that area. CVW is of limited use in dealing with the object-oriented nature of a Windows program. This is not so much a flaw in CodeView as simply a result of the fact that objects are kept outside the program's data segment. Pens, brushes, fonts, bitmaps, globally allocated memory objects, and the data structures that describe a window are beyond easy access for CodeView. If you wish to look at the contents of a memory object, a cursor, icon, or bitmap, HeapWalker is your best bet. If you want to find out about a GDI object, you can call the GetObject routine to transfer values into a program variable that can then be viewed with CodeView. If you wish to see information on any other object, such as a window, use the appropriate toolkit routine, GetWindowWord for example, to transfer the information to a program variable. CVW is helpful in an object-oriented environment because you can look at handles and pointers, which are, after all, located within your program's variables. By setting tracepoints you can detect changes in the value of handles, and you will be able to trap events like the unintentional overwriting of a handle or pointer value. Also, you can determine when a null handle has been returned and be certain that a failure has occurred. CVW is also useful in working with dynamic-link libraries. CVW permits you to perform source-level tracing into your dynamic-link libraries, giving you a more complete picture of what is happening during the execution of your application. If you want to view the code contained in one of the Windows dynamic-link libraries, such as Kernel, User, or GDI, CVW lets you do this. However, its usefulness depends on whether you understand 8086 assembly language or not. Because you do not have the source code to these dynamic-link libraries, you can only trace through them on a machine-language level. If you are reasonably proficient in reading 8086 assembler, this can be invaluable for determining exactly what an SDK routine is doing under a particular set of circumstances. Sample Session We are now ready to walk through a sample CVW debugging session. Our purpose is to show you how setting a breakpoint on a window procedure's switch statement allows you to watch the messages associated with the creation of a window. Getting Started The program that we will trace is the "Hello Windows" program from the Sample Source Code disk of the Windows Version 2.03 SDK, which you should be able to copy from the Windows SDK to walk through this debugging session yourself. The first step is to modify the program's make file so that it has the appropriate compiler and linker switches. This involves adding the Zi and Od switches to the compile line and the /CO switch to the link line. The complete make file is shown in Figure 2. Once you have recreated HELLO.EXE, you start CVW by using one of the two command lines that are shown in Figure 3. If you are using an MDA adapter with a monochrome display, which enables full-screen mode debugging, use the first command line shown in Figure 3. If you are using an asynchronous terminal with a null-modem adapter as your debugging workstation, which enables line-mode debugging, use the second command line. In the rest of this sample debugging session, full-screen debugging mode is used. When CVW starts, you will see the two screens shown in Figures 4 and 5.☼In Figure 4, you see that CodeView has started. In Figure 5 however, you see that Windows itself has not yet started. Setting Breakpoints First, set a breakpoint at the window procedure's switch statement, which involves locating a source line and then either clicking with the left button or using the bp command to indicate the necessary source line. Figure 6☼ shows a breakpoint set with the bp command highlighted. Go Next, start execution. When you issue the Go command, you first notice that Windows starts running. Figure 7☼ shows the display screens after Windows has started, when the system has encountered the breakpoint set at the switch statement in the Hello program's window procedure, HelloWndProc. Note that the Windows display contains the familiar blue of the desktop, although nothing currently occupies it. In the CodeView display, you'll see that you have mouse control and are able to issue CodeView commands. Adding to the Watch List At this point, we would like to add the local variable "message" to the watch list. Because we want the value to be displayed in hexadecimal, a type modifier is added to the w? command. We add message to the watch list by typing the following at the CodeView prompt: > w? message, x Figure 8☼ shows that the "watch window" has been opened and that the current value of message (0024H) is being displayed. A quick look at WINDOWS.H reveals that this value represents the WM_GETMINMAXINFO message. Even though a message has been received for our window, it has not been processed yet. To allow the message to process, we again issue the g (GO) command, after which another breakpoint is encountered, and the display appears as shown in Figure 9☼. Notice that the CVW display shows that the value of message is 0081H and that nothing seems to have changed in the Windows display. The messages next received are: WM_GETMINMAXINFO WM_NCCREATE WM_CALCSIZE WM_CREATE WM_SHOWWINDOW WM_SETVISIBLE WM_ACTIVATEAPP The Nonclient Area The first message that causes anything to appear on the Windows display is the message WM_NCACTIVATE. The NC part of the message stands for nonclient. As Figure 10☼ shows, this simply draws the border of the window, along with the System Box and the Minimize and Maximize Boxes. The very next message, WM_GETTEXT, seems to cause the caption to be drawn. In fact, we suspect that the prior WM_NCACTIVATE sent a WM_GETTEXT message to retrieve this Windows message so as to properly display the caption information (see Figure 11☼). Another group of messages appear that affect the display. These messages are: WM_ACTIVATE WM_SETFOCUS WM_NCPAINT WM_SYNCPAINT Erasing the Background Figure 12☼ demonstrates the effect of the message WM_ERASEBKGND. Clearly, the background of the window has been erased with the main window's default background brush, which is a white brush. A pair of messages then appears: WM_SIZE WM_MOVE You should note that the size and the move messages appear at window creation time, which is useful to know if you plan to respond to these two messages. Think of these messages as saying, "Your window is changing in size from nothing to something and moving from nowhere to somewhere." Hello Windows! Finally, when a WM_PAINT message appears, the familiar "Hello Windows" message, shown in Figure 13☼, is displayed. It seems that so many messages have flown by as part of the window's creation process, but there are still two messages coming in, namely: WM_NCHITTES WM_SETCURSOR The last message to appear is the WM_MOUSEMOVE message. This concludes the immediate window creation message traffic. In all, 20 messages were received by this window procedure before the window creation process was complete. At this point, if you move the mouse cursor, another WM_MOUSEMOVE message is sent to the application's window procedure. You need to disable the breakpoint; otherwise you would be trapped forever by mouse movement messages. You do this either by clicking again on the breakpoint (to toggle the breakpoint) or by issuing the following command: > bc 0 Quitting CodeView Before you quit CodeView, you need to close down Windows. This is because Windows modifies the system, including several system vectors that must be restored if the system is to operate in a normal fashion. Once this is done, you exit CodeView with the Quit command: > quit A Breakthrough When it becomes available, CodeView for Windows will be a breakthrough for Windows developers. Its interface is intuitive and easy to use. Its ability to access local variables, and to display data structures and evaluate complex C expressions gives it power that puts it in a class by itself. However, its main memory requirement (138Kb) and the necessity for EMS memory may limit its usefulness for some developers. In contrast, Symdeb (43Kb), although lacking the full-screen user interface and access to local variables that are part of CodeView, will always be useful for certain Windows developers. Also, the copy of CodeView that we looked at did not have some of the internal Windows hooks that Symdeb has, such as the ability to dump the global heap, dump the free list, and so on. This is a minor issue, however, when compared with the many great advantages that CodeView's user interface provides. Figure 2: The complete make file for compiling the Hello program, to be used with CodeView. # # Standard command line definitions # cp=cl -d -c -AS -Gsw -Od -Zpie # # Standard inference rules # #.c.obj:$(cp) $ .c # # The C File List # hello.obj: hello.c hello.h hello.res: hello.rc hello.ico hello.h rc -r hello.rc hello.exe: hello.obj hello.res hello.def link4 hello/CO,/align:16,,slibw/NOE,hello.def rc hello.res Figure 3: CodeView for Windows can be started in several ways, depending depending on what type of secondary monitor or terminal is in use. Command when using an MDA adapter: C> cvw /2 /L hello.exe \windows\win.com hello.exe Command when using an asynchronous terminal and null modem adapter: C> cvw /C=com1 /T /L hello.exe \windows\win.com hello.exe ████████████████████████████████████████████████████████████████████████████ OS/2 Graphics Programming Interface: An Introduction to Coordinate Spaces Charles Petzold☼ PIXELS. You can't draw with them. You can't draw without them. Pixels are the single greatest impediment to device-independent graphics programming. Part of the problem is that different output devices have different pixel resolutions. A 150-pixel-high image is 5 inches tall on a CGA display but only a half inch on a 300-dots-per-inch laser printer. Moreover, many display output devices have different horizontal and vertical resolutions. If you are working with pixels, you will have trouble drawing round circles and square squares. To avoid these problems, many high-level graphics programming languages give the programmer a means to draw in units other than pixels. The programmer specifies coordinate positions in terms of inches, millimeters, or other convenient units. The graphics system then converts these coordinates to pixels when drawing. The conversion of points from one coordinate system to another is called a "transform." At the very least, transforms should hide from the program the resolution of the output device and any differences in horizontal and vertical resolution. But transforms also play an important role in "modeling"─the construction of images from individual elements. Transforms provide the means to manipulate these elements before they are displayed. The GPI (Graphics Programming Interface) component of the OS/2 Presentation Manager supports transforms both for working in units other than pixels and for more sophisticated modeling tasks. In fact, the GPI contains so many transform functions that the subject can be quite confusing to the GPI newcomer. This article discusses the GPI transforms that are supported under the "micro-presentation space" (or micro-PS). As I discussed in "The Graphics Programming Interface: A Guide to OS/2 Presentation Spaces," (MSJ, Vol. 3 No. 3), the micro-PS supports only a subset of GPI functions. However, working with a subset can certainly be an advantage when learning a new topic because you can simply ignore everything not included in the subset. I Point, Therefore I Am When you draw with GPI functions, you specify a two-dimensional point using the POINTL structure. The POINTL structure contains two fields named x and y. GPI maps the point you specify in the POINTL structure to a point on the output device (which can be the screen, a printer, or a plotter). At first, this seems like simple analytic geometry. The points you use in GPI functions are simply points in a Cartesian coordinate system, as shown in Figure 1. We can represent a point in this coordinate space by the notation (x,y). The point (0,0) is the center where the horizontal and vertical axes meet. This is good for starters, but its simplicity is deceptive. Let's pose a few basic questions: ■ Where is the origin? That is, when you use a POINTL structure with the x and y fields both set to zero, where is that point mapped to on the output device? ■ How do units of x and y in the POINTL structure correspond to the natural pixel units of the output device? Do increasing values of x really go to the right? Do increasing values of y really go up? ■ Is it necessary for values of x to represent a position on the horizontal axis and for values of y to represent a position on the vertical axis? Can the entire coordinate system be rotated in some way to look like Figure 2? ■ Is it even necessary for the x and y axes to be at right angles to each other? Or can the coordinate system look like the one that is shown in Figure 3? The answers to these questions are: whatever you want them to be. Using GPI transforms, you control exactly how the points you specify in GPI functions are mapped to the output device. In fact, these four sets of questions are related to the four basic types of transforms: ■ Translation: Where the point (0,0) is mapped to on the output device. ■ Scaling: How units of x and y correspond to pixels. ■ Rotation: How the x and y axes are oriented. ■ Shear: Whether the two axes are at right angles to each other or not. We'll examine these four types of transforms in more detail later in this article. Visualizing Transforms One way of thinking about graphics transforms is to imagine the axes shown in Figures 1, 2, and 3 as being superimposed on an output device, such as the video display. The points you use in GPI functions specify a point in the coordinate space. That point corresponds to a particular pixel position on the screen. Using GPI transform functions, you can effectively shift the axes so that the origin is at a different location on the screen. You can stretch or compress an axis so that the units you use relate to pixels in different ways. You can rotate the axes so that objects you draw appear to be rotated. You can tilt one or both axes so that objects are distorted. But you'll find that this visual approach is not the best way to think about GPI transforms. It's relatively easy if you're dealing with only two coordinate systems (the coordinate space in which you draw and the natural coordinate space of the output device), but the micro-PS subset of GPI defines three additional coordinate systems between these two. You'll find that the visual approach is inadequate and that instead you must rely on formulas. These formulas govern how a point is mapped from one coordinate space to the next. Five Coordinate Spaces The five micro-PS coordinate spaces are shown in the table in Figure 4. The "Transform" column of the table shows the name of the transform that maps between each pair of coordinate spaces. The last column shows the GPI function that lets you set or change the transformation formula. The points you specify in GPI drawing functions are in world coordinate space. These points are eventually mapped to the output device, which is the media coordinate space. The Media and the Device The best place to start is at the bottom of Figure 4 with the windowing system transform. This is the easiest transform because it's one you can't control and therefore do not have to worry about. The windowing system transform is handled within the Presentation Manager. Media coordinate space is dependent on the hardware of the output device. The video display, for example, is organized in rows and columns of pixels. Each pixel is one unit in media space. For convenience, the origin-the point (0,0)-is defined to be the lower-left corner of the display. Increasing values of x go to the right; increasing values of y go up. But you never use media space coordinates in GPI functions. To draw on the display, you need a handle to a presentation space. The presentation space is always identified with a particular window. The window has its own coordinate system known as device space. The origin is at the lower-left corner of the window. Like media space, units are pixels. Increasing values of x go to the right; increasing values of y go up. The windowing system transform maps from device space to media space. This transform is based on the position of the window relative to the screen. pixels from the left side of the screen and 100 pixels from the bottom. The windowing system transformation formulas are: xmedia = xdevice + 50 ymedia = ydevice + 100 Thus, the point (0,0) in device space is mapped to the point (50,100) in media space. These are simple transformation formulas that effectively shift the origin of the coordinate space. This type of transform is called a "translation." The Presentation Page The presentation page (also known more simply as the page) is the coordinate space in which a picture is constructed for display. You can visualize the page as precisely that──a page of paper. On the screen, you view the whole page or a part of the page in a program's window. The units of the page can be pixels or other units of measure (such as inches or millimeters) or anything else that is convenient for the application. The lower-left corner of the page usually coincides with the lower-left corner of device space (the window), but it doesn't have to. The presentation page maps to device space through the device transform. This is the transform that allows a program to work in units other than pixels. When you create a presentation space by calling the function GpiCreatePS, the device transform is set for you. In the GpiCreatePS function, you specify the units of the presentation page (for example, by using the identifier PU_LOMETRIC for units of 0.1 millimeter or the identifier PU_LOENGLISH for units of 0.01 inch) and the size of the page in those units. The size of the page in page units is specified in a SIZEL structure that is then passed to GpiCreatePS. The SIZEL structure has two fields named cx and cy. For example, if you decide to use PU_LOENGLISH page units and you want the page to be 81/2 by 11 inches, set cx to 850 and cy to 1100. If you use 0 values in the SIZEL structure, the size of the page is set equal to the size of the output medium. For a window on the display, this is the size of the screen. The device transform is defined in terms of a RECTL (rectangle) structure. After you call GpiCreatePS to create a presentation space, you can obtain the device transform rectangle by calling GpiQueryPageViewport ( hps, &rcl) ; where rcl is a structure of type RECTL. You will find that the values are set as is shown in Figure 5. The device transform formulas, shown in Figure 6, are rather ugly. The rcl variable (a structure of type RECTL) is the device transform rectangle, and sizl (a structure of type SIZEL) contains the page size in page units. If the xLeft and yBottom fields of the RECTL structure are 0 (as they are when you first create the presentation space), then these two formulas reduce to those shown in Figure 7. This transform does exactly what we expect: it transforms the page units (which could be 0.1 millimeter or 0.01 inch) into pixels by multiplying them by a factor that relates those units to the pixel size. This type of transform is known as "scaling." The complete formula also allows for translation. The size of the page that you specify in the GpiCreatePS function does not affect the device transform. Whatever page size you choose, GPI will calculate a device transform appropriate to it. However, the page size affects clipping because the graphics field is initially set to the size of the page. If you create a presentation space using GpiCreatePS, you will probably want to leave the device transform alone. However, you can do some tricks with it, such as setting different scaling factors or translating the origin of the presentation page to someplace other than the lower-left corner of the window. For example, if you wish to put the origin in the middle of the window, first obtain the device transform rectangle by calling GpiQueryPageViewport, add half the width of the client window to the xLeft and xRight fields and half the height of the client window to the yBottom and yTop fields, and then set the new device transform by calling GpiSetPageViewport. You'll also need to change the graphics field clipping area to encompass the whole window in page units. Windows programmers should note that the function GpiSetPageViewport does the job of several Windows functions: SetViewportOrg, SetWindowOrg, and SetViewportExt, in addition to SetWindowExt. However, it is not quite as easy to use as the Windows functions. Cached Micro-PS If you use a cached micro-PS, the presentation page is always in units of pixels. You can use the device transform to change that. For example, suppose you want to draw in units of 0.01 inch. You first obtain a handle to the window device context: hdc = WinOpenWindowDC(hwnd) ; You then make four calls to DevQueryCaps to determine the dimensions of the screen in pixels and the number of pixels per meter: DevQueryCaps (hdc, CAPS_WIDTH, 1L, &xScreenPixels) ; DevQueryCaps (hdc, CAPS_HEIGHT, 1L, &yScreenPixels) ; DevQueryCaps (hdc, CAPS_VERTICAL_RESOLUTION, 1L, &yPixelsPerMeter) ; DevQueryCaps (hdc, CAPS_HORIZONTAL_RESOLUTION, 1L, &xPixelsPerMeter) ; Because the size of the cached micro-PS page is the size of the screen, you define the fields of a RECTL structure like this: rcl.xLeft = 0 ; rcl.yBottom = 0 ; rcl.xRight = ((xScreenPixels - 1) * xPixelsPerMeter * 254 + 500000) / 1000000 ; rcl.yTop = ((yScreenPixels - 1) * yPixelsPerMeter * 254 + 500000) / 1000000 ; The ratio 254/1,000,000 converts meters to 0.01 inch. The number 500,000 is for rounding. Thus, the xRight and yTop fields are set to the width and height of the screen in 0.01 inch. You then call: GpiSetPageViewport (hps, &rcl) ; You also need to set the graphics field to the size of the window in page units. To obtain this, you can use the GpiConvert function to convert from device space to page coordinates: WinQueryWindowRect (hps, &rcl) ; GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 2L, (PPOINTL) &rcl) ; GpiSetGraphicsField (hps, &rcl) ; And you're done. Obviously the device transform is not easy to work with. It's easiest to create a micro-PS using GpiCreatePS and specify the page size and page units you want. That defines the device transform once and for all. Constructing the Picture If you don't use a transform, other than the device transform as it is set by GpiCreatePS, then everything you draw using GPI functions is in page units-the units you selected in the GpiCreatePS function. (Actually, there are some exceptions: GPI regions are always defined in device units, for example.) The origin is located in the lower-left corner of the window. Increasing values of x move to the right; increasing values of y move up. As you construct a picture on the presentation page, you also have available two additional transforms: the modeling transform and the default viewing transform. The points you use in GPI functions are in world coordinate space. The modeling transform converts those points to model coordinate space. The default viewing transform converts the points in model space to points in the presentation page space. For some simple purposes, the modeling and default viewing transforms are interchangeable. But a general rule to follow is this: the transforms closest to the hardware (that is, the transforms at the bottom of Figure 4) should be changed less frequently than the transforms furthest from the hardware. The simplest approach is to use the default viewing transform to apply to the whole picture and different modeling transforms to apply to pieces of the picture. Suppose you want to work with a coordinate system in which the center of the window is the origin. You use the default viewing transform to accomplish that. (The TRANSFRM program discussed later does exactly that.) Suppose you want to draw a picture in which a particular object appears in several different places. You can draw the object with different modeling transforms. The same points are passed to the GPI drawing functions, but the modeling transform causes those points to be transformed. Transformation Matrix Both the modeling transform and the default viewing transform are based on a standard two-dimensional "transformation matrix" common in many high-level graphics languages such as GKS (Graphical Kernel System). In GPI, you specify a transformation matrix as a structure of type MATRIXLF. (The LF part of the structure name stands for LONG and FIXED, the two data types used for the structure fields.) The MATRIXLF structure is defined in the PMGPI.H header file, like this: typedef struct _MATRIXLF { FIXED fxM11 ; FIXED fxM12 ; LONG lM13 ; FIXED fxM21 ; FIXED fxM22 ; LONG lM23 ; LONG lM31 ; LONG lM32 ; LONG lM33 ; } MATRIXLF ; At first glance, the jumble of numbers in the field names looks like a real mess. But it's actually quite simple. The nine fields of this structure are the nine elements of a 3-by-3 matrix: │ fxM11 fxM12 lM13 │ │ │ │ fxM21 fxM22 lM23 │ │ │ │ lM31 lM32 lM33 │ The M in the field names stands for "matrix," and the numbers are simply the row and column positions of each field within the matrix. The letter l prefix on some of the fields stands for LONG. The fx prefix stands for FIXED. The FIXED data type is a 32-bit signed long integer that is interpreted by GPI as a 16-bit signed integer and a 16-bit unsigned fraction. The FIXED data type allows GPI to use fractions without resorting to time-consuming floating-point operations. Figure 8 shows various examples of FIXED values. In some of the following examples I use normal decimal point notation for FIXED numbers. Keep in mind that these are really 32-bit integers. You can convert a FIXED variable to a float or double variable by dividing by 65536.0: dNumber = (fxNumber / 65536.0) ; You can convert a float or double variable to FIXED by multiplying by 65536 and casting the result: fxNumber = (FIXED) (65536 * dNumber) ; The transformation matrix is actually slightly simpler than the version shown above. When you use a MATRIXLF structure in the GPI transform functions, the lM13 and lM23 fields must be set equal to 0 and the lM33 field must be set equal to 1: │ fxM11 fxM12 0 │ │ │ │ fxM21 fxM22 0 │ │ │ │ lM31 lM32 1 │ GPI uses a matrix multiplication to convert a point (x,y) to a point (x',y'). First, GPI constructs a 1-by-3 matrix from the original point (x,y) by appending a 1. Then that matrix is multiplied by the 3-by-3 transformation matrix. The resultant 1-by-3 matrix contains the points x', y', and 1 and is shown in Figure 9. If (x,y) is a point in world coordinate space, then GPI applies the modeling transform to determine the point (x',y') in model coordinate space. If (x,y) is a point in model coordinate space, then GPI applies the default viewing transform, and the resultant point (x',y') is in presentation page units. The transformation matrix can also be represented as a pair of formulas that describe the matrix multiplication: x' = (fxM11) x + (fxM21) y + lM31 y' = (fxM12) x + (fxM22) y + lM32 Note that the points x' and y' are functions of both x and y. This is not the case in the device transform and windowing system transform. For a new presentation space, the modeling and default viewing transformation matrices are both identity matrices. Only the fxM11, fxM22, and lM33 fields are set to 1. The other fields are set to 0: │ 1.0 0.0 0 │ │ │ │ 0.0 1.0 0 │ │ │ │ 0 0 1 │ Thus, the transformation formulas look like this: x' = x y' = y The TRANSFRM Program The TRANSFRM program shown in Figure 10 allows you to experiment with the GPI transformation matrix. The micro-PS is created in ClientWndProc during the processing of the WM_CREATE message. It uses page units of PU_LOENGLISH, or 0.01 inch. During the WM_PAINT message, the window procedure draws a ruler and a stick figure enclosed in a box, as shown in Figure 11☼. The ruler has tick marks every inch (100 units). The figure is enclosed in a box that is 2 inches high and 2 inches wide. TRANSFRM sets the default viewing transform while processing the WM_SIZE message. This sets the origin of model space to the center of the window. The ruler is always drawn with a default modeling transform, which is set during the WM_PAINT message with the following function call: GpiSetModelTransformMatrix ( hps, 0L, NULL, TRANSFORM_REPLACE) ; The figure is drawn with a modeling transform that you define in a dialog box (see Figure 12☼). This transform is set using the MATRIXLF structure named matlfModel: GpiSetModelTransformMatrix ( hps, 9L, &matlfModel, TRANSFORM_REPLACE) ; In all cases the figure is drawn from the DrawFigure function using the same array of POINTL structures. Different modeling transformation matrices, however, can change the appearance of the object. Translation Suppose you have in your program an array of POINTL structures that defines a 100-unit square with the bottom-left corner at point (0,0). You want to draw this square using these points, but you want the bottom-left corner to be positioned at point (100,200). This requires a form of transform known as translation. Nonzero values of the lM31 and lM32 elements in the transformation matrix cause an image to be shifted or translated from one place to another without any distortion of the image: │ 1.0 0.0 0 │ │ │ │ 0.0 1.0 0 │ │ │ │ lM31 lM32 1 │ The transformation formulas are: x' = x + lM31 y' = y + lM32 The matrix translates the point (0,0) to the point (lM31,lM32). If the matrix represents the default viewing transform, then the point (0,0) in model coordinate space is the same as the point (lM31,lM32) in page coordinate space. TRANSFRM.C uses translation in order to set the model space origin to the center of the window. During the processing of the WM_SIZE message in the client window procedure, TRANSFRM sets the default viewing transformation matrix so that the lM31 field is half the width of the client window in page units and lM32 is half the height. Because the WM_SIZE message reports the size of a window in pixels, TRANSFRM must use the GpiConvert function to convert the center position from device space to page space. Changing the position of the figure relative to the ruler can be done by entering different values of lM31 and lM32 in the TRANSFRM dialog box. Figure 13☼ shows the result when lM31 equals 150 and lM32 equals ──50. The figure is shifted right by 150 units (1.5 inches) and down by 50 units (0.5 inch). As you can see, translation will always transform a square to another square. The object is simply moved to a different location on the output device. Scaling The fxM11 and fxM22 fields of the MATRIXLF structure are the "scaling" fields. If fxM11 is greater than 1.0, then the resultant image is expanded along the x axis. Values less than 1.0 cause the width of the image to be compressed. Similarly, fxM22 affects the height of the image. If all other fields of the matrix are set to their default values, the transformation matrix for scaling is │ fxM11 0.0 0 │ │ │ │ 0.0 fxM22 0 │ │ │ │ 0 0 1 │ and the transformation formulas are: x' = (fxM11) x y' = (fxM22) y In general, scaling causes a square to be transformed into a rectangle. Figure 14☼ shows TRANSFRM with fxM11 set to 3.0 and fxM22 set to 0.5.\ Reflection Reflection is a form of scaling that uses negative scaling factors. Negative values of the fxM11 and fxM12 fields cause the image to be flipped horizontally or vertically. For example, if you use this transformation matrix │ -1.0 0.0 0 │ │ │ │ 0.0 1.0 0 │ │ │ │ 0 0 1 │ then the transformation formulas are: x' = -x y' = y This reflection flips the image around the vertical axis (see Figure 15☼). Similarly, this matrix │ 1.0 0.0 0 │ │ │ │ 0.0 -1.0 0 │ │ │ │ 0 0 1 │ causes a reflection around the horizontal axis and turns the object upside down (Figure 16☼). Shear The shear transform effectively tilts one of the coordinate axes in such a way that it is not at right angles to the other axis. The resultant drawing is distorted: a square is mapped to a parallelogram. A nonzero value of fxM21 causes x-shear: │ 1.0 0.0 0 │ │ │ │ fxM21 1.0 0 │ │ │ │ 0 0 1 │ The transformation formulas are: x' = x + (fxM21) y y' = y This shifts a point left or right by an amount dependent upon the distance between the point and the horizontal axis. Figure 17☼ shows the TRANSFRM screen when fxM21 is set to 1.0. Similarly, the fxM12 field causes y-shear. The transformation matrix is: │ 1.0 fxM12 0 │ │ │ │ 0.0 1.0 0 │ │ │ │ 0 0 1 │ Figure 18☼ shows y-shear when fxM12 is set to 1.0. Shear sometimes suggests a three-dimensional view of an object. However, because a square is always mapped to a parallelogram, there is no real depth perspective. Now try this matrix, which combines y-shear and negative x-shear: │ 1.0 1.0 0 │ │ │ │ -1.0 1.0 0 │ │ │ │ 0 0 1 │ The result appears in Figure 19☼, in which the image is rotated 45 degrees counterclockwise and enlarged somewhat. This suggests that a combination of scaling and shear can produce rotation, which is exactly what happens. Rotation From basic trigonometry we know that to rotate an image a degrees around the origin in a counterclockwise rotation requires the following formulas: x' = x * cos(a) ── y * sin(a) y' = x * sin(a) + These formulas fit very nicely into the transformation matrix: │ cos(a) sin(a) 0 │ │ │ │ ─sin(a) cos(a) 0 │ │ │ │ 0 0 1 │ For example, to rotate an image 45 degrees counterclockwise around the origin, use: │ 0.707 0.707 0 │ │ │ │ ─0.707 0.707 0 │ │ │ │ 0 0 1 │ To rotate an image 90 degrees counterclockwise, use: │ 0.0 1.0 0 │ │ │ │ -1.0 0.0 0 │ │ │ │ 0 0 1 │ You can experiment with rotation angles in TRANSFRM by selecting the "Angle..." button to invoke a second dialog box from the first. This is shown in Figure 20☼. A rotation of 112.5 degrees and a scaling factor of 2.5 is seen in Figure 21☼. Working Backwards The TRANSFRM program also allows you to derive a transformation matrix by selecting three corners of the box that encloses the stick figure. You select the lower-left corner by clicking with the mouse, the lower-right corner by clicking with the mouse while holding down the Shift key, and the upper-left corner by clicking with the mouse while holding down the Ctrl key. Matrix Multiplication One reason why matrices are used to represent graphics transforms is that the cumulative effect of two transforms is equivalent to the product of the two matrices. For example, suppose you want to first double the height of an object and then rotate the object 45 degrees counterclockwise. You would multiply the two matrices that describe these two transforms, as shown in Figure 22. In the resultant matrix, the element in the ith row and jth column is the summation of element-by-element products of the ith row of the first matrix and the jth column of the second matrix. Matrix multiplication is associative but not commutative. If you want to first rotate an object 45 degrees and then double the height, the composite transformation matrix would be calculated as shown in Figure 23. A combination of matrices is often used with rotation to choose the point around which the object is rotated. For example, suppose you have stored a series of points that describes an object. You want to rotate this object 90 degrees around the point (100,100). This is equivalent to translating the object by ──100 units on the x axis and ──100 units on the y axis, rotating the image around the origin, then translating it back 100 units on the x and y axes. The three matrices that describe these transforms are multiplied together in sequence as shown in Figure 24. The result is: │ 0.0 1.0 0 │ │ │ │ ─1.0 0.0 0 │ │ │ │ 200 0 1 │ This is the same as rotating the image 90 degrees and then translating it 200 units along the x axis. Combining Transforms You do not have to do the matrix multiplication in your program. GPI can do the multiplications for you by using the TRANSFORM_ADD and TRANSFORM_PREEMPT parameters to such functions as SetModelTransformMatrix and SetDefaultViewMatrix. The use of GPI matrix multiplication to combine three transforms is demonstrated in the REVOLVE program shown in Figure 25. This program does some rudimentary animation using the GPI transforms. It shows the stick figure drawn the same way that it was in the TRANSFRM program, but subjected to three transforms in the REVOLVE function SetModelTransform. The transforms are varied on each WM_TIMER message, so the figure moves and changes in shape. The three transforms are effectively multiplied together through the use of the parameter TRANSFORM_ADD when GpiSetModelTransformMatrix is called for the second and third matrices. The first transform scales the height of the figure by a factor ranging between ─1 and 1. (The factor is obtained from a sine function.) This gives the figure the appearance of falling head-over-heels. The second transform rotates the figure. The third transform scales the figure by the same factor on both the x and y axes. First, this scaling factor is decreased, which makes the figure look like it's falling away from you; then it is increased, which makes the figure look like it's falling toward you. Conclusion The key to creating sophisticated graphics applications is based on the ability to understand the interrelationships between coordinate spaces and the transforms required to move from one to another. The transforms themselves are controlled by transformation formulas and represented by transformation matrices. GPI provides a rich set of functions for handling transformations, allowing simple programs such as TRANSFRM and REVOLVE to create animated sequences. Although GPI was not designed for animation, it's nice to discover that it's features can be exploited for such purposes. Figure 1: The Cartesian coordinate system that forms the basis of our intuitive notion of the GPI coordinate system. But it's not that simple. ∙ +y ∙ ∙ ∙ ◄ ∙-x ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ +x∙ ► ∙ ∙ ∙ -y ∙ ▼ Figure 2: A rotated coordinate system. ∙ ∙ +y +x ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ -x -y ∙ ∙ ▼ ▼ Figure 3: A coordinate system in which the two axes are not at right angles. ∙ +y ∙ ∙ ∙ ◄ ∙-x ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ ∙ +x∙ ► ∙ ∙ ∙ -y ∙ ▼ Figure 4: The five coordinate spaces in the micro-PS subset of GPI. Transforms map from one coordinate space to the next. ╓┌────────────┌──────┌────────────┌───────────────┌──────────────────────────╖ Coordinate Units Transform Types of GPI Function to Space Transformation Set the Transform ┌─────────────────────┐ │World Arbitrary│ └───────────────┬─────┘ │ ┌───────────────────────────────────────────────────────┐ └─►│Modeling Translation │ │ Scaling GpiSetModelTransformMatrix│ │ Rotation │ │ Shear │ └─────┬─────────────────────────────────────────────────┘ ┌─────────────────────┐ │ │Model Arbitrary│◄─┘ └───────────────┬─────┘ │ ┌───────────────────────────────────────────────────────┐ └─►│Default Translation │ │Viewing Scaling GpiSetModelTransformMatrix│ │ Rotation │ │ Shear │ Coordinate Units Transform Types of GPI Function to Space Transformation Set the Transform │ Shear │ └─────┬─────────────────────────────────────────────────┘ ┌─────────────────────┐ │ │Page Metrics, │◄─┘ │ Pixels, or│ │ arbitrary│ └───────────────┬─────┘ │ ┌──────────────────────────────────────────────────────┐ └─►│Device Translation GpiCreatePS and │ │ Scaling GpiSetPageViewport │ └─────┬────────────────────────────────────────────────┘ ┌─────────────────────┐ │ │ Device Pixels │◄─┘ │(window) │ └───────────────┬─────┘ │ ┌──────────────────────────────────────────────────────┐ └─►│Windowing Translation (None) │ │System │ └─────┬────────────────────────────────────────────────┘ Coordinate Units Transform Types of GPI Function to Space Transformation Set the Transform └─────┬────────────────────────────────────────────────┘ ┌─────────────────────┐ │ │ Media Pixels │◄─┘ │(screen) │ └─────────────────────┘ Figure 5: Initial values of the device transform rectangle after creating a presentation space. Device Transform RECTL Field Initial Value rcl.xLeft 0 rcl.yBottom 0 rcl.xRight Page width in pixels ──1 rcl.yTop Page height in pixels ──1 Figure 6: Device Transformation Formulas rcl.xRight - rcl.xLeft + 1 x = x * ──────────────────────────── + rcl.xLeft device page sizl.cx rcl.yTop - rcl.yBottom + 1 y = y * ──────────────────────────── + rcl.yBottom device page sizl.cy Figure 7: Reduced Device Transformation Formulas Page width in pixels x = x * ──────────────────────── device page Page width in page units Page height in pixels y = y * ───────────────────────── device page Page height in page units Figure 8: Examples of FIXED Values FIXED Value Interpretation 0x1 1/65536 0x2000 1/8 0x10000 1 0xAC000 10 3/4 0xFFF8F000 ─7 1/16 Figure 9: GPI Uses Matrix Multiplication to Convert Points │ fxM11 fxM12 0 │ │ │ │ │ │ x y 1 │ * │ fxM21 fxM22 0 │ = │ x' y' 1 │ │ │ │ │ │ lM31 lM32 1 │ Figure 10: Source Code for the TRANSFRM Program TRANSFRM Make File #-------------------- # TRANSFRM make file #-------------------- transfrm.obj : transfrm.c transfrm.h cl -c -G2sw -W3 -Zp transfrm.c transfrm.res : transfrm.rc transfrm.h rc -r transfrm transfrm.exe : transfrm.obj transfrm.def transfrm.res link transfrm, /align:16, NUL, os2, transfrm rc transfrm.res TRANSFRM.C Source Code /*------------------------------------------- TRANSFRM.C -- Demonstrates GPI Transforms -------------------------------------------*/ #define INCL_WIN #define INCL_GPI #include <os2.h> #include <math.h> #include <stdio.h> #include <stdlib.h> #include "transfrm.h" MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ; HAB hab ; int main (void) { static CHAR szClientClass [] = "Transfrm" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE │ FS_SIZEBORDER │ FS_TITLEBAR │ FS_SYSMENU │ FS_MINMAX │ FS_MENU, szClientClass, "GPI Transform Demo", 0L, NULL, ID_RESOURCE, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } /*------------------------------------- Functions for managing dialog boxes -------------------------------------*/ VOID SetFixedItem (HWND hwnd, USHORT idItem, FIXED fxValue) { CHAR szBuffer [20] ; sprintf (szBuffer, "%.3f", fxValue / 65536.0) ; WinSetDlgItemText (hwnd, idItem, szBuffer) ; } FIXED GetFixedItem (HWND hwnd, USHORT idItem) { CHAR szBuffer [20] ; WinQueryDlgItemText (hwnd, idItem, sizeof szBuffer, szBuffer) ; return (FIXED) (atof (szBuffer) * 65536) ; } VOID SetLongItem (HWND hwnd, USHORT idItem, LONG lValue) { CHAR szBuffer [20] ; ltoa (lValue, szBuffer, 10) ; WinSetDlgItemText (hwnd, idItem, szBuffer) ; } LONG GetLongItem (HWND hwnd, USHORT idItem) { CHAR szBuffer [20] ; WinQueryDlgItemText (hwnd, idItem, sizeof szBuffer, szBuffer) ; return atol (szBuffer) ; } /*------------------------------ Dialog box window procedures ------------------------------*/ MRESULT EXPENTRY AngleDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static PMATRIXLF pmatlf ; FIXED fxAngle, fxScale ; switch (msg) { case WM_INITDLG: pmatlf = (PMATRIXLF) mp2 ; break ; case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case DID_OK: fxAngle = GetFixedItem (hwnd, IDD_ANGLE) ; fxScale = GetFixedItem (hwnd, IDD_SCALE) ; pmatlf->fxM11 = (FIXED) (fxScale * cos (6.283 * fxAngle / 65536 / 360)) ; pmatlf->fxM22 = pmatlf->fxM11 ; pmatlf->fxM12 = (FIXED) (fxScale * sin (6.283 * fxAngle / 65536 / 360)) ; pmatlf->fxM21 = - pmatlf->fxM12 ; WinDismissDlg (hwnd, TRUE) ; break ; case DID_CANCEL: WinDismissDlg (hwnd, FALSE) ; break ; } break ; default: return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } MRESULT EXPENTRY MatrixDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static PMATRIXLF pmatlf ; switch (msg) { case WM_INITDLG: pmatlf = (PMATRIXLF) mp2 ; SetFixedItem (hwnd, IDD_FXM11, pmatlf->fxM11) ; SetFixedItem (hwnd, IDD_FXM12, pmatlf->fxM12) ; SetFixedItem (hwnd, IDD_FXM21, pmatlf->fxM21) ; SetFixedItem (hwnd, IDD_FXM22, pmatlf->fxM22) ; SetLongItem (hwnd, IDD_LM31, pmatlf->lM31) ; SetLongItem (hwnd, IDD_LM32, pmatlf->lM32) ; break ; case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case IDD_ANGLEPOP: if (!WinDlgBox (HWND_DESKTOP, hwnd, AngleDlgProc, NULL, IDD_ANGLEBOX, pmatlf)) break ; SetFixedItem (hwnd, IDD_FXM11, pmatlf->fxM11) ; SetFixedItem (hwnd, IDD_FXM12, pmatlf->fxM12) ; SetFixedItem (hwnd, IDD_FXM21, pmatlf->fxM21) ; SetFixedItem (hwnd, IDD_FXM22, pmatlf->fxM22) ; break ; case DID_OK: pmatlf->fxM11 = GetFixedItem (hwnd, IDD_FXM11); pmatlf->fxM12 = GetFixedItem (hwnd, IDD_FXM12); pmatlf->fxM21 = GetFixedItem (hwnd, IDD_FXM21); pmatlf->fxM22 = GetFixedItem (hwnd, IDD_FXM22); pmatlf->lM31 = GetLongItem (hwnd, IDD_LM31) ; pmatlf->lM32 = GetLongItem (hwnd, IDD_LM32) ; WinDismissDlg (hwnd, TRUE) ; break ; case DID_CANCEL: WinDismissDlg (hwnd, FALSE) ; break ; } break ; default: return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } MRESULT EXPENTRY AboutDlgProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { switch (msg) { case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case DID_OK: case DID_CANCEL: WinDismissDlg (hwnd, TRUE) ; break ; default: break ; } break ; default: return WinDefDlgProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } /*------------------- Drawing functions -------------------*/ VOID DrawLine (HPS hps, SHORT x1, SHORT y1, SHORT x2, SHORT y2) { POINTL ptl ; ptl.x = x1 ; ptl.y = y1 ; GpiMove (hps, &ptl) ; ptl.x = x2 ; ptl.y = y2 ; GpiLine (hps, &ptl) ; } VOID DrawRuler (HPS hps) { SHORT sIndex ; DrawLine (hps, -350, 0, 350, 0) ; DrawLine (hps, 0, -350, 0, 350) ; for (sIndex = -300 ; sIndex <= 300 ; sIndex += 100) { DrawLine (hps, sIndex, -5, sIndex, 5) ; DrawLine (hps, -5, sIndex, 5, sIndex) ; } } VOID DrawFigure (HPS hps) { static POINTL aptlFigure [] = { -20, 100, 20, 100, 20, 90, 10, 90, 10, 85, 20, 85, 20, 75, 35, 75, 35, 70, 20, 70, 20, 65, 5, 65, 5, 60, 20, 60, 10, 50, 10, 40, 25, 40, 85, 100, 100, 100, 100, 85, 25, 10, 25, -25, 50, -85, 75, -85, 75, -100, 35, -100, 0, -15, -35, -100, -75, -100, -75, -85, -50, -85, -25, -25, -25, 10, -85, -50, -100, -50, -100, -35, -25, 40, -10, 40, -10, 50, -20, 60, -20, 100 } ; static POINTL aptlBox [] = { -100, -100, 100, -100, 100, 100, -100, 100 } ; GpiMove (hps, aptlFigure) ; GpiPolyLine (hps, sizeof aptlFigure / sizeof aptlFigure [0] - 1L, aptlFigure + 1) ; GpiSetLineType (hps, LINETYPE_DOT) ; GpiMove (hps, aptlBox + 3) ; GpiPolyLine (hps, 4L, aptlBox) ; GpiSetLineType (hps, LINETYPE_DEFAULT) ; } /*------------------------- Client window procedure -------------------------*/ MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static HPS hps ; static MATRIXLF matlfModel ; static POINTL aptl [3] = { -100, -100, 100, -100, -100, 100 } ; HDC hdc ; MATRIXLF matlf ; POINTL ptl ; SIZEL sizl ; enum { LowerLeft, LowerRight, UpperLeft } ; switch (msg) { case WM_CREATE: hdc = WinOpenWindowDC (hwnd) ; sizl.cx = sizl.cy = 0 ; hps = GpiCreatePS (hab, hdc, &sizl, PU_LOENGLISH │ GPIF_DEFAULT │ GPIT_MICRO │ GPIM_NORMAL │ GPIA_ASSOC) ; GpiQueryModelTransformMatrix (hps, 9L, &matlfModel) ; break ; case WM_SIZE: ptl.x = SHORT1FROMMP (mp2) / 2 ; /* Half window width */ ptl.y = SHORT2FROMMP (mp2) / 2 ; /* Half window height */ GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptl) ; GpiQueryDefaultViewMatrix (hps, 9L, &matlf) ; matlf.lM31 = ptl.x ; matlf.lM32 = ptl.y ; GpiSetDefaultViewMatrix (hps, 9L, &matlf, TRANSFORM_REPLACE) ; break ; case WM_BUTTON1DOWN: ptl.x = MOUSEMSG(&msg)->x ; ptl.y = MOUSEMSG(&msg)->y ; GpiConvert (hps, CVTC_DEVICE, CVTC_MODEL, 1L, &ptl) ; if (WinQueryKeyState (HWND_DESKTOP, VK_CONTROL) < 0) aptl [UpperLeft] = ptl ; else if (WinQueryKeyState (HWND_DESKTOP, VK_SHIFT) < 0) aptl [LowerRight] = ptl ; else /* neither shift nor control */ aptl [LowerLeft] = ptl ; matlfModel.fxM11 = 65536 * (aptl [LowerRight].x - aptl [LowerLeft].x) / 200 ; matlfModel.fxM12 = 65536 * (aptl [LowerRight].y - aptl [LowerLeft].y) / 200 ; matlfModel.fxM21 = 65536 * (aptl [UpperLeft].x - aptl [LowerLeft].x) / 200 ; matlfModel.fxM22 = 65536 * (aptl [UpperLeft].y - aptl [LowerLeft].y) / 200 ; matlfModel.lM31 = (aptl [LowerRight].x + aptl [UpperLeft].x) / 2 ; matlfModel.lM32 = (aptl [LowerRight].y + aptl [UpperLeft].y) / 2 ; WinInvalidateRect (hwnd, NULL, FALSE) ; return TRUE ; case WM_COMMAND: switch (COMMANDMSG(&msg)->cmd) { case IDM_SELECT: if (WinDlgBox (HWND_DESKTOP, hwnd, MatrixDlgProc, NULL, IDD_MATRIXBOX, &matlfModel)) { GpiSetModelTransformMatrix (hps, 9L, &matlfModel, TRANSFORM_REPLACE) ; aptl [LowerLeft].x = -100 ; aptl [LowerLeft].y = -100 ; aptl [LowerRight].x = 100 ; aptl [LowerRight].y = -100 ; aptl [UpperLeft].x = -100 ; aptl [UpperLeft].y = 100 ; GpiConvert (hps, CVTC_WORLD, CVTC_MODEL, 3L, aptl) ; WinInvalidateRect (hwnd, NULL, FALSE) ; } break ; case IDM_ABOUT: WinDlgBox (HWND_DESKTOP, hwnd, AboutDlgProc, NULL, IDD_ABOUTBOX, NULL) ; break ; default: break ; } break ; case WM_PAINT: /* WinBeginPaint (hwnd, hps, NULL) ; */ /* temporary fix for graphics field */ { static RECTL rcl ; GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ; GpiSetGraphicsField (hps, &rcl) ; } /* end of temporary fix */ GpiErase (hps) ; GpiSetModelTransformMatrix (hps, 0L, NULL, TRANSFORM_REPLACE) ; DrawRuler (hps) ; GpiSetModelTransformMatrix (hps, 9L, &matlfModel, TRANSFORM_REPLACE) ; DrawFigure (hps) ; /* WinEndPaint (hps) ; */ WinValidateRect (hwnd, NULL, FALSE) ; /* temporary fix */ break ; case WM_DESTROY: GpiDestroyPS (hps) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return FALSE ; } TRANSFRM.H Header File /*------------------------ TRANSFRM.H header file ------------------------*/ #define ID_RESOURCE 1 #define IDM_SELECT 10 #define IDM_ABOUT 11 #define IDD_ABOUTBOX 1 #define IDD_MATRIXBOX 2 #define IDD_ANGLEBOX 3 #define IDD_FXM11 10 #define IDD_FXM12 11 #define IDD_FXM21 12 #define IDD_FXM22 13 #define IDD_LM31 14 #define IDD_LM32 15 #define IDD_ANGLEPOP 16 #define IDD_ANGLE 20 #define IDD_SCALE 21 TRANSFRM.RC Resource Script File /*---------------------------------- TRANSFRM.RC resource script file ----------------------------------*/ #include <os2.h> #include "transfrm.h" MENU ID_RESOURCE { SUBMENU "~Transform", -1 { MENUITEM "~Select...", IDM_SELECT MENUITEM SEPARATOR MENUITEM "E~xit", SC_CLOSE, MIS_SYSCOMMAND MENUITEM "A~bout Transfrm...", IDM_ABOUT } } DLGTEMPLATE IDD_ABOUTBOX { DIALOG "", 0, 10, 20, 160, 100, WS_SAVEBITS │ FS_DLGBORDER { CTEXT "Transfrm" -1, 10, 72, 140, 10 CTEXT "GPI Transformation Demo" -1, 10, 46, 140, 10 CTEXT "Copyright(C) Charles Petzold, 1988" -1, 10, 30, 140, 10 DEFPUSHBUTTON "OK" DID_OK, 64, 8, 32, 14, WS_GROUP } } #define XFORMULA "x' = (fxM11) x + (fxM21) y + (lM31)" #define YFORMULA "y' = (fxM12) x + (fxM22) y + (lM32)" DLGTEMPLATE IDD_MATRIXBOX { DIALOG "", 0, 8, 72, 176, 140, WS_SAVEBITS │ FS_DLGBORDER { LTEXT "fxM11 (x scale):" -1, 32, 122, 80, 10, EDITTEXT "" IDD_FXM11, 116, 122, 32, 8, ES_MARGIN LTEXT "fxM12 (y shear):" -1, 32, 108, 80, 10, EDITTEXT "" IDD_FXM12, 116, 108, 32, 8, ES_MARGIN LTEXT "fxM21 (x shear):" -1, 32, 94, 80, 10, EDITTEXT "" IDD_FXM21, 116, 94, 32, 8, ES_MARGIN LTEXT "fxM22 (y scale):" -1, 32, 80, 80, 10, EDITTEXT "" IDD_FXM22, 116, 80, 32, 8, ES_MARGIN LTEXT "lM31 (x translate):" -1, 32, 66, 80, 10, EDITTEXT "" IDD_LM31, 116, 66, 32, 8, ES_MARGIN LTEXT "lM32 (y translate):" -1, 32, 52, 80, 10, EDITTEXT "" IDD_LM32, 116, 52, 32, 8, ES_MARGIN CTEXT XFORMULA -1, 8, 36, 160, 10 CTEXT YFORMULA -1, 8, 26, 160, 10 PUSHBUTTON "Angle..." IDD_ANGLEPOP, 8, 8, 48, 14, WS_GROUP DEFPUSHBUTTON "Ok" DID_OK, 64, 8, 48, 14, WS_GROUP PUSHBUTTON "Esc=Cancel" DID_CANCEL, 120, 8, 48, 14, WS_GROUP } } DLGTEMPLATE IDD_ANGLEBOX { DIALOG "", 0, 0, -64, 176, 64, WS_SAVEBITS │ FS_DLGBORDER { LTEXT "Angle in degrees:" -1, 32, 46, 80, 10, EDITTEXT "0.00" IDD_ANGLE, 116, 46, 32, 8, ES_MARGIN LTEXT "Scaling factor:" -1, 32, 32, 80, 10, EDITTEXT "1.00" IDD_SCALE, 116, 32, 32, 8, ES_MARGIN DEFPUSHBUTTON "Ok" DID_OK, 24, 8, 48, 14, WS_GROUP PUSHBUTTON "Esc=Cancel" DID_CANCEL, 104, 8, 48, 14, WS_GROUP } } TRANSFRM.DEF Module Definition File ;------------------------------------- ; TRANSFRM.DEF module definition file ;------------------------------------- NAME TRANSFRM DESCRIPTION 'Demonstrates GPI Transforms(C) Charles Petzold, 1988' HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc MatrixDlgProc AngleDlgProc AboutDlgProc Figure 22: The cumulative effect of two transforms is equivalent to the product of the two matrices. This example first doubles the height of an object and then rotates it 45° counterclockwise. │ 1.0 0.0 0 │ │ 0.707 0.707 0 │ │ 0.707 0.707 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ 0.0 2.0 0 │ * │ ─0.707 0.707 0 │ = │ ─1.414 1.414 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ 0 0 1 │ │ 0 0 1 │ │ 0 0 1 │ Figure 23: Matrix operation necessary to first rotate an object 45°, then double the height. The result is due to the fact that matrix multiplication is associative, but not commutative. │ 0.707 0.707 0 │ │ 1.0 0.0 0 │ │ 0.707 1.414 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ ─0.707 0.707 0 │ * │ 0.0 2.0 0 │ = │ ─0.707 1.414 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ 0 0 1 │ │ 0 0 1 │ │ 0 0 1 │ Figure 24: A combination of matrices such as those shown here are used with rotation to choose the point around which the object is rotated. │ 1.0 0.0 0 │ │ 0.0 1.0 0 │ │ 1.0 0.0 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ 0.0 1.0 0 │ * │ ─1.0 0.0 0 │ * │ 0.0 1.0 0 │ │ │ │ │ │ │ │ │ │ │ │ │ │ ─100 ─100 1 │ │ 0 0 1 │ │ 100 100 1 │ Figure 25: Source Code for the Revolve Program REVOLVE Make File #------------------- # REVOLVE make file #------------------- revolve.obj : revolve.c cl -c -G2sw -W3 -Zp revolve.c revolve.exe : revolve.obj revolve.def link revolve, /align:16, NUL, os2, revolve REVOLVE.C Source Code /*------------------------------------------ REVOLVE.C -- Demonstrates GPI Transforms ------------------------------------------*/ #define INCL_WIN #define INCL_GPI #include <os2.h> #include <math.h> #include <stddef.h> MRESULT EXPENTRY ClientWndProc (HWND, USHORT, MPARAM, MPARAM) ; HAB hab ; int main (void) { static CHAR szClientClass [] = "Revolve" ; HMQ hmq ; HWND hwndFrame, hwndClient ; QMSG qmsg ; hab = WinInitialize (0) ; hmq = WinCreateMsgQueue (hab, 0) ; WinRegisterClass (hab, szClientClass, ClientWndProc, CS_SIZEREDRAW │ CS_SYNCPAINT, 0) ; hwndFrame = WinCreateStdWindow (HWND_DESKTOP, WS_VISIBLE │ FS_SIZEBORDER │ FS_TITLEBAR │ FS_SYSMENU │ FS_MINMAX, szClientClass, "GPI Transform Demo", 0L, NULL, 0, &hwndClient) ; while (WinGetMsg (hab, &qmsg, NULL, 0, 0)) WinDispatchMsg (hab, &qmsg) ; WinDestroyWindow (hwndFrame) ; WinDestroyMsgQueue (hmq) ; WinTerminate (hab) ; return 0 ; } VOID SetModelTransform (HPS hps, FIXED fxYScale, FIXED fxAngle, FIXED fxScale) { MATRIXLF matlf ; GpiSetModelTransformMatrix (hps, 0L, NULL, TRANSFORM_REPLACE) ; GpiQueryModelTransformMatrix (hps, 9L, &matlf) ; matlf.fxM22 = fxYScale ; GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_REPLACE) ; matlf.fxM11 = (FIXED) (cos (6.283*fxAngle/65536/360) * 65536) ; matlf.fxM22 = matlf.fxM11 ; matlf.fxM12 = (FIXED) (sin (6.283*fxAngle/65536/360) * 65536) ; matlf.fxM21 = -matlf.fxM12 ; GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_ADD) ; matlf.fxM11 = matlf.fxM22 = fxScale ; matlf.fxM12 = matlf.fxM21 = 0 ; GpiSetModelTransformMatrix (hps, 9L, &matlf, TRANSFORM_ADD) ; } VOID DrawFigure (HPS hps) { static POINTL aptlFigure [] = { -20, 100, 20, 100, 20, 90, 10, 90, 10, 85, 20, 85, 20, 75, 35, 75, 35, 70, 20, 70, 20, 65, 5, 65, 5, 60, 20, 60, 10, 50, 10, 40, 25, 40, 85, 100, 100, 100, 100, 85, 25, 10, 25, -25, 50, -85, 75, -85, 75, -100, 35, -100, 0, -15, -35, -100, -75, -100, -75, -85, -50, -85, -25, -25, -25, 10, -85, -50, -100, -50, -100, -35, -25, 40, -10, 40, -10, 50, -20, 60, -20, 100 } ; GpiMove (hps, aptlFigure) ; GpiPolyLine (hps, sizeof aptlFigure / sizeof aptlFigure [0] - 1L, aptlFigure + 1) ; } MRESULT EXPENTRY ClientWndProc (HWND hwnd, USHORT msg, MPARAM mp1, MPARAM mp2) { static double dScaler = 1.1, dYScaleAngle = 0.0, dYScaleAngleInc = 0.25 ; static FIXED fxAngle, fxAngleInc = 0xA0000L, fxYScale, fxScale = 0x2000L ; static HPS hps ; HDC hdc ; MATRIXLF matlf ; POINTL ptl ; SIZEL sizl ; switch (msg) { case WM_CREATE: hdc = WinOpenWindowDC (hwnd) ; sizl.cx = sizl.cy = 0 ; hps = GpiCreatePS (hab, hdc, &sizl, PU_LOENGLISH │ GPIF_DEFAULT │ GPIT_MICRO │ GPIM_NORMAL │ GPIA_ASSOC) ; GpiSetMix (hps, FM_INVERT) ; if (!WinStartTimer (hab, hwnd, 1, 100L)) WinAlarm (HWND_DESKTOP, WA_ERROR) ; break ; case WM_SIZE: ptl.x = LOUSHORT (mp2) / 2 ; /* Half of window width */ ptl.y = HIUSHORT (mp2) / 2 ; /* Half of window height */ GpiConvert (hps, CVTC_DEVICE, CVTC_PAGE, 1L, &ptl) ; GpiQueryDefaultViewMatrix (hps, 9L, &matlf) ; matlf.lM31 = ptl.x ; matlf.lM32 = ptl.y ; GpiSetDefaultViewMatrix (hps, 9L, &matlf, TRANSFORM_REPLACE) ; break ; case WM_TIMER: /* temporary fix for graphics field */ { static RECTL rcl ; GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ; GpiSetGraphicsField (hps, &rcl) ; } /* end of temporary fix */ SetModelTransform (hps, fxYScale, fxAngle, fxScale) ; DrawFigure (hps) ; fxYScale = (FIXED) (65535 * cos (dYScaleAngle)) ; if ((dYScaleAngle += dYScaleAngleInc) > 6.283) dYScaleAngle -= 6.283 ; if ((fxAngle += fxAngleInc) > (FIXED) 0x1680000L) fxAngle -= (FIXED) 0x1680000L ; if ((FIXED) (fxScale *= dScaler) >= (FIXED) 0x30000L ││ fxScale <= (FIXED) 0x2000L) dScaler = 1 / dScaler ; SetModelTransform (hps, fxYScale, fxAngle, fxScale) ; DrawFigure (hps) ; break ; case WM_PAINT: /* WinBeginPaint (hwnd, hps, NULL) ; */ /* temporary fix for graphics field */ { static RECTL rcl ; GpiQueryPS (hps, (PSIZEL) &rcl.xRight) ; GpiSetGraphicsField (hps, &rcl) ; } /* end of temporary fix */ GpiErase (hps) ; SetModelTransform (hps, fxYScale, fxAngle, fxScale) ; DrawFigure (hps) ; /* WinEndPaint (hps) ; */ WinValidateRect (hwnd, NULL, FALSE) ; /* temp fix */ break ; case WM_DESTROY: WinStopTimer (hab, hwnd, 1) ; GpiDestroyPS (hps) ; break ; default: return WinDefWindowProc (hwnd, msg, mp1, mp2) ; } return NULL ; } REVOLVE.DEF Module Definition File ;------------------------------------ ; REVOLVE.DEF module definition file ;------------------------------------ NAME REVOLVE DESCRIPTION 'Demonstrates GPI Transforms(C) Charles Petzold, 1988' PROTMODE HEAPSIZE 1024 STACKSIZE 8192 EXPORTS ClientWndProc ████████████████████████████████████████████████████████████████████████████ Microsoft Macro Assembler Version 5.1 Simplifies Macros and Interfacing Ross M. Greenberg☼ Interrupt service routines. High-speed async routines. Direct screen memory writes. Direct low-level disk routines. Precise control. The C language. Which item seems out of place? Until recently, you probably would have said C. I know I would have. Whenever it was necessary to create a routine to do something out of the ordinary quickly and efficiently, the idea of using a high-level language like C never crossed my mind. All that overhead-PUSH BPs when not doing stack frame referencing, volatile variables changed by a well-meaning optimization routine, lengthy compile times-contributed to my looking to Microsoft(R) Macro Assembler (MASM) for true efficiency. Of course, that meant going through the headache of creating MASM preambles and postambles to each MASM routine, not to mention the extra work required if my code exceeded 64Kb and I needed to go to the next memory model. I admit it. I always felt it was too much work to create MASM code as a model-independent piece of work-until I started using macros. Macros are probably the most underutilized assembly language programmer's tool. A macro is a simple shorthand for using the same concepts again and again; PUSH_ALL and POP_ALL are certainly concepts all programmers use, so why not create a shorthand that will later be expanded into the appropriate instructions? With the introduction of MASM 5.0, writing macros was made easier by a set of well-written macros that came with the package. My productivity picked up substantially, my code became clearer to read, I didn't spend hours trying to avoid a memory model change, and I had plenty of time to think up excuses for not documenting my work. With the release of MASM 5.1, many of my macros have become obsolete because their functionality is now part of the assembler itself. MASM 5.1 includes the changes I would have made to the assembler if I had written this version. Some substantial rewrites of your code may therefore be in order. This article will examine the changes needed to take advantage of the new features and enhancements in MASM 5.1. Limitations of C The overhead in certain routines in C can be a problem when trying to write good, clean, fast code. Think about the movmem routine, for example. Now consider overlapping source and destination pointers. You can't really write dual-purpose code that is as clean and fast as single-purpose code. Even calling DOS interrupts directly can be inefficient. Although you know that only one or two registers must be set before the interrupt routine is called, the int86 routine doesn't know that. It saves all the registers, sets them from your REGS structure, calls the interrupt, stuffs all the registers on return into the REGS (or possibly another) structure, then pops all the registers back. Think of all the overhead necessary for doing something as simple as setting the AH register where the return value isn't even important. Aside from making your code bigger than it has to be, all those extra instructions add up to more work for the processor, and that translates into processor overhead──exactly what you're trying to avoid by calling the interrupt routines directly. Certain specific operations also cry out for MASM efficiency: intersegment memory moves, taking over a hardware interrupt (such as the timer tick, keyboard, or serial comm), or something esoteric like an overlay loader. Interfacing MASM and C There are some problems involved with getting the most from your MASM routines. Having a MASM routine call a C routine from an interrupt can certainly cause confusion. You have to remember to provide your own stack and to restore the data segment register from someplace, and you must assume that even calling one C routine will cause every register to change. Pulling variables off the stack when a MASM routine is called from C is also an example of heuristic programming. I almost never remember the offsets into the stack (which depend on the memory model and the number of registers I save before the address), and I used to end up with monsters that did work but somehow embarrassed me when I showed someone my code (see Figure 1). With the new MASM 5.1, the same code can simply be rewritten as routine proc uses ax, var1:int mov ax, var1 <other code> ret routine endp which is quite a bit cleaner, even though the code generated is virtually identical. Another bad habit is forgetting to pop registers in the same order that you push them. This results in bugs that usually show up pretty quickly. MASM 5.1 comes to the rescue with the utilization of the "uses" clause. The code produced is not necessarily more efficient, but at least one problem is no longer in the check-this-out-in-the-debugger phase of development. Introducing MASM 5.1 The new features of MASM 5.1 represent both a formalization of what one does with macros as well as enhancements to the macro capability itself. If you're moving from 5.0 to 5.1, you won't really have to modify much of your source code. If you're moving up from 4.0, the upgrade will astonish you with its power and ease of use. Macros Macros essentially enable you to define new "operands" within MASM; in order to create one you assign a symbolic name (associated with one or more variables or arguments, or none at all) with a block of source statements. When the assembler sees a macro reference, it expands it to the predefined sequence of instructions. Macros can have some intelligence built into them, letting you generate a different set of instructions depending upon such conditions as the type and number of arguments and the memory model. However, the macro parser only looks at the conditions at compilation time. You cannot use the value of a register at run time, for example, to define macro expansion. As you build your own set of macros, you'll typically keep them in an include file. MASM 5.1 includes the ability to have "text macros," such as using the right-hand value of an EQU statement as a macro and allowing for its expansion. In 5.0, Figure 2 would have required a call to a "regular" macro in order to be properly evaluated. A macro might have been: msg_print macro do_print %errprint endm do_print macro msg_list irp msg, <msg_list> push ax push dx mov ah, 9 mov dx, offset msg int 21h pop dx pop ax endm endm In 5.1, however, the middle macro isn't required, and a macro such as this one is perfectly valid: msg_print macro % irp msg, <msg_list> push ax push dx mov ah, 9 mov dx, offset msg int 21h pop dx pop ax endm endm The percent (%) sign at the beginning of the IRP line forces text macro evaluation to take place. There is no saving in code size, but the structure is certainly clearer to read. Another nice feature is the ability to use the ampersand (&) operator for string concatenation outside a macro just as you did inside the macro body: label equ <test_stuff> %table_entry_&label db "tester" In MASM 5.0 this was illegal and the table_entry line without the ampersand was seen by the compiler as table_entry_label and not evaluated. You can now also do substring searches and culls, string concatenation, and determine the argument size of the text macro argument(s). Using the SUBSTR operator, the line in Figure 3 will push the appropriate register, and the following will push ax and bx on the stack: reg_push 1 reg_push 2 The second argument is the starting position within the string, starting at position 1, and the third argument is the length of the string. CATSTR allows you to concatenate a number of text strings into one: big_string CATSTR <string1>,<string2> which causes big_string to be set to string1string2. SIZESTR enables you to do a strlen on the text and would cause len_big_string SIZESTR big_string to evaluate to 14. Finally, you can determine the offset within a text string of the substring you're searching for by using the INSTR directive. For example, to determine if an argument contains a segment as well as an offset, you could search for the colon in the third position: print_msg macro addr colon INSTR <addr>,<:> if colon gt 0 msg_seg SUBSTR <addr>,1,2 len SIZESTR <addr> lab SUBSTR <addr>,4,len -3 push ax push dx push ds mov dx, msg_seg mov ds, dx mov dx, offset lab mov ah, 9h int 21h pop ds pop dx pop ax else push ax push dx mov dx, offset addr mov ah, 9h int 21h pop dx pop ax endif endm start: print_msg cs:msg1 print_msg msg1 int 20h ret msg1 db 'msg1$' end start Predefined Macros MASM 5.1 includes some additional macros that are predefined by the assembler itself. @WordSize will return the size of the segment word size in bytes, returning a 4 for 32-bit word sizes and a 2 for 16-bit word sizes. This is very useful when you have the .386 directive turned on and need to create a 386-specific macro. @Cpu returns a weird bit pattern that, when interpreted, can tell you what processor you're running on, what mode the 286 or 386 is in (protected or real), and what type of math coprocessor is installed, if any. Remember that this is a compile-time macro and that the actual run- time machine may have a different configuration. The @Version macro returns a three-character identifier in order to let you know which version of MASM you're running: the newest release returns "510". It is undefined in earlier versions, so you can test for it with code such as: IFNDEF @Version ELSE Extensions Finally, at long last, no more IF kludges that act like brain-damaged case statements! You can now test with a host of ELSEIF directives that include those listed in Figure 4. Some of the additions to conditional assembly directives can make your job a lot easier, too. Consider having a piece of code such as: zb proc uses bx es, p1:ptr ; zero out the first byte here, regardless of memory model if @datasize les bx, p1 ; if large model else mov bx, p1 ; if small model endif mov byte ptr [bx], 0 ret zb endp Local Labels I hate thinking up names for variables. I'm a strong believer in self- documenting code; variable and label names ought to be mnemonic and make some sense when reading code. This certainly helps during the documentation phase of a project. However, thinking up unique names is tough, and always trying to use position-relative code can be cumbersome. But with MASM 5.1 you can now use the double at-sign (@@) convention to define jumps in both the forward and backward directions. The jumps are evaluated at compilation time (see Figure 5). TYPE Directives Based on the type of an argument or variable, you can choose which operations should take place on that argument or variable. The TYPE directive has been expanded considerably to include the ability to ascertain whether an object is program-related (code), data-related, a constant, addressing mode, whether it is a register or not, whether it is defined or not, and whether it is a local or public symbol. The complete definition of the bit positions of the TYPE directive are listed in the MASM manual. As an example of how handy the TYPE directive can be, consider how easily it lets you do something like: print_me macro arg if ((.TYPE arg) AND REGISTER) push arg push dx mov dx, arg call do_print pop dx pop arg elseif push dx mov dx, offset arg call do_print pop dx endm CodeView(R) Enhancements When you are using CodeView to help debug your code, it helps if the object type itself is known to CodeView. You can enable this by using the enhanced and extended PTR data definition word shown in Figure 6. High-level Support First and foremost, I am a C programmer. I sometimes find myself entering C syntax when forced to use assembler and have been hesitant to delve into assembler, always thinking beforehand how much trouble it is to do so. Setting up the segments properly, remembering where and what the data segment is, doing mixed-model programming, getting variables off the stack, problems with underscores: it just wasn't worth the hassle. In MASM 5.1, almost all of my complaints are in the past; it is now significantly easier to interface with C. Though I focus on C, it is important to note that interfacing to other high-level languages, including BASIC, has become much easier. As discussed previously, MASM 5.1 lets you employ the "uses" word as part of your PROC directive to let the assembler know what registers your routine is going to use. Using my_proc proc uses ax dx mov dx, offset cs:my_msg mov ah, 9h int 21h ret my_proc endp actually generates the push ax/pop ax and push dx/pop dx pairs for you automatically and in the right order. The basic syntax is to separate each of the registers by white space. When I was converting my code to MASM 5.1, that caused some trouble. I was using a routine to save the register values and to swap the AX and BX registers. Obviously, if you are playing any games with the values you pop off the stack, you'll have to use the older method. Or you can go back and figure out what the kludge was that had you doing any strange things and either document it or change it to a more reasonable approach. One item shocked me a bit and does not appear to be documented anywhere: you must use the .MODEL directive with both of its arguments, or the uses clause will not be processed properly. The first argument that the .MODEL directive takes indicates the memory model you wish to use, and the second one indicates which language you wish to "configure" for. You can choose the small, medium, compact, large, or huge memory models. Using the language specification as in .MODEL small,C will set up the calling and returning conventions for you, make all symbols publicly accessible, and append to the beginning of the ubiquitous underline character so the linker doesn't get confused. You need to specify BASIC, C, FORTRAN, or Pascal, as each one has its own requirements regarding parameter and naming conventions. The update to MASM can really be helpful in developing transportable code-simply change the .MODEL language specifier from, say, 'C' to 'BASIC' and MASM automatically changes the calling and parameter conventions with no additional work. This is useful for people who produce commercial libraries. A library for compiled BASIC is now as easy to produce as one for C-simply change one specifier and recompile. Underscores will magically appear and disappear as the appropriate language compiler requires for the link process. Stack Frame Addressing As already mentioned, you can use the uses clause to specify which registers to save and restore on the stack on procedure entry and exit. But, when interfacing with a high-level language, you'll often want to pass parameters to the assembly routine. In most languages, and in C in particular, this parameter passing is done on the stack: each argument to a function is pushed on the stack in either left-to-right or right-to-left order (depending on whether or not you have specified the routine as a Pascal routine). The enhancements to MASM allow you very easy access to these parameters, regardless of the memory model chosen. This will save you hours in debugging and source code rewrites, if you change memory models frequently. Let's assume that you're not using the Pascal calling convention, which forces the parameters to be placed on the stack in left-to-right order. If you've specified the C language with the .MODEL directive, then you can use a line such as the one in Figure 7, which not only specifies that ax should be saved on program entry, but also allows you to access three items on the stack directly, as in: mov ax, arg1 It can be done without regard to the memory model used and without having to stick the usual preamble for saving and restoring BP; that's done automagically for you. You can select the following "types" for your argument list: word, dword, fword, qword, tbyte, or even the name of a STRUC. The default is either a word (for 8086/88/286/386) or a dword if you've turned the .386 directive on. You can also qualify the type with the far, near, or ptr convention, as shown by my_proc proc uses ax, arg1:far ptr dword which specifies a variable that is 4 bytes long on the stack and indicates what you'll be using it as (in this case, a far ptr). That permits you to specify later in your code something like: lds dx, arg1 The pointer information isn't really used by MASM for much, except that additional information is generated for CodeView debugging if the ptr keyword is seen at assembly time. The routine in Figure 8, for example, resulted in the debug output in Figure 9 (to which I added comments). The same kind of code you'd generate by hand, right? Obviously, I used the small model for this example. Local Variables In a high-level language like C, when you need a local variable you declare it at the top of the routine, or inside a block, and space is automatically allocated from the stack for you to play with. This space is returned to the stack when you return. You just use the variable name, and the compiler does the rest. MASM has always had this capability. A macro such as tmp_buf equ <byte ptr [bp - size1]> allows you to include the tmp_buf name as if it were a true variable. However, such variables have global scope, and I always ended up using names like "tmp_buf_serial_routine" to differentiate it from other tmp_buf's. Also, it never actually saved any space on the stack, and an inadvertent push/pop sequence could change the stack in strange and less- than-wonderful ways. With MASM 5.1, you can now allocate local variables with only a local scope. The names don't exist outside of the procedure in which they are defined. For example, consider Figure 10, which causes MASM to set up a text macro almost magically behind the scenes. Here's a simple example: HEADER_SIZE equ 3 DATA_SIZE equ 128 CRC_SIZE equ 2 The source code generated by MASM 5.1 is listed in Figure 11 (the casting of tmp_buf is because tmp_buf is being defined as a byte array). The dump, with the calls to the other routines fabricated for the purposes of this example, is shown in Figure 12. Conversion Guidelines Conversion of your nonmacro MASM code is very simple: ■ First convert all of your push/pop sequences into their appropriate uses statements. MASM isn't too smart in one regard; each return will generate the appropriate pop sequence, so you should convert all of the routines to use only one return, with jumps directed toward that return. This is, in any case, good programming practice. ■ Next, convert all of your argument processing conventions into the new "proc line" sequence. Also, remember to drop the "PUSH BP/MOV BP, SP/.../POP BP" sequences you've generated; MASM will do all that automatically. ■ Look for the areas where you've allocated space off the stack for local variables (try a global search for "SUB SP,"), and convert them over to the new local calling definition convention. ■ Now the hard task: converting all parts of your code into the appropriate text macro sequences to take advantage of the new features and enhancements. You have to decide for yourself if it's worth converting over to the new sequences. Personally, I just looked for the word ";kludge" in my macro files and fixed it up. ■ Remember to include the .MODEL directive or MASM 5.1 will basically revert back to its old style and give you a lot of error lines about "extra characters" and syntax errors galore. MASM 5.1 is basically just an upgrade──an evolutionary not a revolutionary step. A variety of other enhancements have been made to the entire package as well as the assembler itself. These include the new CodeView, the ability to link easily with OS/2 systems dynamic-link libraries, and updates to the BIND, IMPLIB, MAKE, ILINK, and EXEHDR utilities. Also, the Microsoft Editor is now part of the MASM distribution package. I was able to reduce my MASM source code line count by about 30 percent, and I'm less hesitant to use MASM routines now when I program in C. Also, I spend a lot less time debugging my MASM routines. As for using all that extra time the upgrade gives me to document my code, well, as soon as I finish this one routine, all right? Figure 1: Under earlier versions of MASM, the programmer needed to create user stacks and was responsible for the pushing/popping of appropriate registers. routine proc <whatever> push bp mov bp, sp push ax mov ax, ss:[bp + MEMORY_MODEL + OFS_VAR1] <other code> pop ax pop bp ret routine endp Figure 2: Example of a Text Macro errprint equ <err_msg1, err_msg2, err_msg3> Figure 3: Sample Code Using SUBSTR reg_push macro start reg substr <axbxcsdxsidib>,(start - 1) * 2 + 1, 2 push reg endm Figure 4: New ELSEIF Options ELSEIF <test> if <test> evaluates to true ELSEIF <test> if <test> evaluates to false ELSEIF1 if pass1 ELSEIF2 if pass2 ELSEIFB <arg> if blank ELSEIFNB <arg> if not blank ELSEIFDEF <arg> if defined ELSEIFNDEF <arg> if not defined ELSEIFDIF <arg1>, <arg2> if different ELSEIFDIFI <arg1>, <arg2> if different, regardless of case ELSEIFIDN <arg1>, <arg2> if identical arguments ELSEIFIDNI <arg1>, <arg2> if identical, regardless of case Figure 5: Sample Code Using the New @@ Convention \print_me macro args irp print_arg, <args> mov dx, print_arg call print_it jnz @F ; error, leave "loop" call print_it_part_two <some_other_convoluted_macro_calls> endm @@: endm Figure 6: Defining Object Types for CodeView print_msg db "This is a test message" near_ptr dw byte ptr print_msg ; a near pointer far_ptr dd byte ptr print_msg ; a far pointer Figure 7: This sample code specifies that ax must be saved and lets you directly access three items on the stack. Note that you can specify a type for each item on the list. my_proc proc uses ax, arg1:word, arg2:dword, arg3:ptr dword Figure 8: The "ptr" keyword causes information to be generated that can be used by CodeView. See the debug output in Figure 9 below. my_proc proc uses ax cx dx ds es,arg1:word, arg2:dword, arg3: far ptr dword mov ax, arg1 lds dx, arg2 les cx, arg3 ret my_proc endp Figure 9: Code fragment produced by CodeViewfrom the code shown in Figure 8 above. PUSH BP ; standard preamble for stack frame MOV BP,SP PUSH AX ; save the "uses" registers PUSH CX PUSH DX PUSH DS PUSH ES MOV AX,[BP+04] ; arg1 LDS DX,[BP+06] ; arg2 LES CX,[BP+0A] ; arg3 POP ES ; restore the "uses" registers POP DS POP DX POP CX POP AX POP BP ; and the stack frame register RET ; the calling routine removes vars ; from the stack in C, so we don't Figure 10: Sample code defining local variables with local scope. LOCAL tmp_buf[size1]:byte, tmp_buf2[size2]:word, var1:dword Figure 11: Sample Source Utilizing Local Variables. make_xmodem_block proc uses cx si di ds es,source:far ptr dword, data_len:word local tmp_buf[HEADER_SIZE + DATA_SIZE + CRC_SIZE]:byte lds si, source push es push ss pop es mov di, word ptr (tmp_buf + HEADER_SIZE) mov cx, data_len rep movsb call do_rest call do_send ret make_xmodem_block endp Figure 12: Actual MASM listing generated from the code shown in Figure 11. PUSH BP ; standard preamble MOV BP,SP SUB SP,0086 ; reserve some space for loc var tmp_buf PUSH CX ; save registers PUSH SI PUSH DI PUSH ES PUSH DS LDS SI,[BP+04] ; pointer to the source PUSH ES ; cause ES to address stack PUSH SS POP ES MOV DI,[BP+FF7D] ; pointer to the local var MOV CX,[BP+08] ; get the length off the stack REPZ MOVSB ; move the bytes in CALL DO_REST CALL DO_SEND POP DS ; restore the registers POP ES POP DI POP SI POP CX MOV SP,BP ; restore the stack POP BP ; and finally BP RET ████████████████████████████████████████████████████████████████████████████ Color Mixing Principles and How Color Works in the Raster Video Model Kevin P. Welch☼ In the last few years a dramatic revolution in computer graphics has occurred. When I first started working with computers, I was content to use a small 80-character by 25-line monochrome terminal with no graphics capabilities. Now I feel confined if the display has a resolution of less than 640 by 480 pixels. Today displays with resolutions of 1,280 by 960 pixels are common, and some are even higher. We used to be satisfied with a monochrome display; however, now we expect at least eight or 16 colors from even the simplest display subsystem. This emergence of color graphics on personal computers has had a profound effect on the software currently being written. Desktop publishing systems, presentation graphics aids, drawing applications, and image processing tools are partly made possible by this technology. Yet despite these advances, we still take for granted the way that computers use color. An analysis of the situation reveals that even the best display systems available today fall far short of the real world. Color Modeling Color is very subjective and depends entirely on our perceptions. The colors we see depend on the physical properties of the object we are looking at, the light sources that illuminate that object, and the physiological and perceptual characteristics of the human eye. The process of perception is very complex, and no single physical interpretation has yet been able to explain adequately even the simplest observations. For example, from a physical perspective we are able to perceive subtle differences in the intensity of any particular color but our ability to detect small changes in hue, or subtle changes from one color to another, varies greatly. In fact, many different energy distributions of light appear, for all practical purposes, to be identical. For centuries man has attempted to characterize these physical observations and harmonize them with the colors we perceive. Two general characterizations have emerged from this investigation. The first approach is that used by artists: tints, shades, and tones (see Figure 1☼). This subtractive model results from the way in which an artist might paint an image on white paper. Progressive layers of pigment are added until the desired effect is achieved. White is achieved simply by the absence of pigment, and black by the combination of all pigments. A tint results from reducing the saturation of a pure pigment, or in effect mixing it with white. A shade comes from mixing a pure pigment with black, effectively reducing its brightness. Finally, a tone results from the addition of both black and white to a pure pigment. These steps combined produce different colors of the same hue but with varying degrees of brightness and saturation. The next approach involves hue, saturation, and brightness. From a descriptive point of view this adds a different dimension to the traditional tint, shade, and tone characterization used by artists. A hue refers to a particular color, such as red, green, or blue. Saturation is a little more complicated in that it refers to purity of color, that is, how strong it appears. For example, if you mix pure red light with white light you get pink. This pink color is like red, only less saturated. Finally, brightness refers to the physical intensity or luminance of the color and is independent of the actual hue and saturation involved. From a mathematical perspective, both of these models are difficult to handle when it comes to describing several different colors. Often the most practical means to measure color involves comparing an unknown with a known set of samples, just like the color cards you get when you purchase a can of paint. A more objective approach to comparing colors is to classify the perception (usually visible light) in terms of dominant wavelength, purity, and luminance. The resulting spectral energy distribution then determines a "color." The term dominant wavelength is derived from the fact that our eye tends to respond to one particular wavelength when viewing light, which determines the color we see. Purity refers to the mathematical combination of colored and white light. A completely pure color contains no white light and is often called 100 percent saturated. Luminance refers to the overall energy of a particular color. This involves both the purity and dominant wavelength of the light we see──the more energy, the more visible the particular color. Color Mixing Models From a physiological perspective, it has been theorized that the retina of the human eye contains three kinds of cones, each sensitive to red, green, and blue light, respectively. This hypothesis, despite other recent vision theories, has been extended to the field of computer graphics. Most color monitors use red, green, and blue phosphor in an attempt to simulate the human eye. The extension of the red-green-blue hypothesis to color monitors and other hardware-oriented systems has sparked considerable interest in the development of models that facilitate the convenient specification of color. The resulting models shape our view of how these systems utilize color. Although one model may do particularly well in one situation and poorly in another, in the end all the color mixing models are theoretically equivalent in that they are capable of producing the same results, albeit with various degrees of ease. RGB Mixing Model The red-green-blue (RGB) mixing model (see Figure 2☼) is hardware-oriented and is a simple extension of the physical properties of color monitors. In this model the three primary colors are mixed together in a Cartesian coordinate system. The final result of the mixture can be considerably different from the primaries involved. For example, the lack of color is black (defined as the RGB triple 0-0-0), and the presence of all color is white (1-1-1). The combination of two primary colors produces a secondary color mixture. This can be demonstrated by the creation of yellow (1-1-0) from pure red (1-0-0) and pure green (0-1-0). Note that in the theoretical RGB mixing model each primary color is specified by a continuous value in the range {0,1}. Most physical implementations of this scheme use some sort of discrete mapping from this range. Microsoft Windows, for example, via the Graphics Device Interface (GDI), only supports colors on the integral range {0,255}. Palette, a Windows color-control program detailed in the accompanying article ("Creating User-Defined Controls for Your Own Windows Applications,"), for example, supports the RGB mixing model but converts the selected color to a CRT equivalent supported by GDI. This conversion is performed via the following simple formula: crtValue = rgbValue * 255 rgbValue = crtValue / 255 Using Palette you can easily experiment with varying amounts of primary color. In Figure 3☼ you can see that by combining pure red and green you can obtain pure yellow, assuming, of course, that you have a color display. If you search you may be able to find some other pure colors that do not occur at the corners of the RGB color cube. If you try this same experiment with different display subsystems you will probably get different results. If you are using a monochrome system the colors will hopefully appear as a varying mixture of black and white pixels. On a color system the number of pure colors supported depends on the underlying hardware characteristics and the implementation dependencies imposed by the GDI display driver you are using. Some display drivers are very good at approximating colors that fall between primary ones; others simply use the closest possible primary color. As you experiment you'll see that the red, green, and blue spectra displayed are themselves samples from the color cube. These samples were created with a simple algorithm that traverses an edge of the RGB color cube: for ( i=0; i<16; i++ ) { rgbValue = i / 15.00; crtValue = rgbValue * 255; } You could create your own mixing table by using a different algorithm and substitute it in place of the one displayed. CMY Mixing Model The cyan-magenta-yellow (CMY) mixing model is a variant of the RGB model that uses subtractive primaries instead of additive ones; in other words, its effect is to subtract color from white light. The presence of all color results in black and the absence in white. It is important to understand the CMY model since it is used in many hard- copy output devices. The notion of adding color or pigment to white paper is employed in plotters, ink jet, and laser printers. Mathematically, the CMY mixing model can also be described using a Cartesian color cube (see Figure 4☼). The resulting subspace is identical to the RGB cube except that white is at the origin instead of black. While examining the CMY color cube you should notice the diagonal line between the black and white corners. Color triples on this line result in various mixtures of gray. Using Palette you can produce a gray in both the RGB and CMY models by picking the same intensity value for each of the primary colors. With Palette you will notice something else about the CMY and RGB mixing models: a gray value component is always produced whenever a color mixture involves all of the primaries. For example, suppose you have an equal mixture of cyan and magenta that results in a tint of blue. If you progressively add yellow (up to the level of the original colors), you can fade the tint into gray. Unfortunately this is a little more difficult when you start with a tint of unequal amounts of cyan and magenta. Fortunately the CMY and RGB models are simply related: Cyan = 1 - Red Magenta = 1 - Green Yellow = 1 - Blue Using this relationship you can easily create a transformation that converts any CMY value to an RGB value and vice versa. YIQ Mixing Model The YIQ mixing model, like the CMY and RGB models, can also be described by using a Cartesian coordinate cube. This model is of particular interest as it represents a recoding of the RGB model, optimized for transmission efficiency and downward compatibility with monochrome display subsystems, for example, a black-and-white television. The Y component of this model represents a primary whose spectral energy distribution matches the luminosity response curve, implying that the Y component is a gray-scale equivalent for the defined color. This resolves an important television broadcasting problem in which the same signal is received by both color and black-and-white television sets. If the RGB mixing model is used, two distinguishable shades on a color television can result in the same luminances on a black-and-white television. The YIQ mixing model resolves this problem by mapping different colors to different intensity levels. Although Palette does not implement the YIQ mixing model (you can easily do it if you are so inclined), you can calculate the YIQ equivalent of an RGB color value with the empirical formula shown in Figure 5. HSV Mixing Model The HSV mixing model, unlike the three previous hardware-oriented models, is a user model based upon the artist's concept of hue, saturation, and value. This model has several distinct advantages over the other models. From a topological point of view, the HSV model can be described as a hexcone, or a six-sided cone (see Figure 6☼). The bottom of the hexcone corresponds to V = 0 and represents black. The top of the hexcone represents white with V = 1. Each vertex of the hexcone corresponds to one of the primary or secondary colors and is referenced by H in degrees. For example, red corresponds to a hue of 0 degrees, yellow to 60 degrees, and so on around the cone. The remaining parameter, saturation, corresponds to a vector that extends horizontally from the value axis. The defined color is always a gray, ranging from black to white, when S = 0. When S is greater than 0, a particular tone is defined. This tone becomes increasingly more saturated until S = 1. Any color obtained when S = 1 and V = 1 corresponds to a pure pigment that an artist would use. By maintaining a constant saturation, say equal to 1, you can easily traverse all the primary and secondary tints by cycling the hue from 0 to 360 degrees. Once a given tint is produced, it can easily be faded to white by gradually scaling the saturation back to zero. Alternatively, any tint can be faded to black by reducing V until black is reached. If you experiment with Palette using the HSV mixing model, you can easily see how intuitive this approach is. In fact, some of the more trivial color transitions obtained with this model are difficult to achieve when using one of the other models. The default spectrum provided for you when using this mixing mode was selected by traversing various paths through the hexcone. The default hue spectrum was calculated by holding S = 1 and V = 1 and cycling H from 0 to 360 degrees. Note that the values 0 and 360 are equivalent-once you've traversed a full 360 degrees on a circle you return to your starting point. With Palette, the highest value you can obtain on a linear scale is 340. The next step is back to 0 (or 360). The saturation spectrum was calculated at H = 240 degrees (blue) and V = 1. In this case, various S values were used, ranging from 0 to 1. Finally, the default value spectrum was determined by slowly increasing V from 0 to 1 with S = 0 and H = 0. One interesting aspect of the HSV approach is its relationship to the RGB mixing model. If you view the RGB color cube along its principal diagonal, it becomes like a constant V slice through the hexcone. Each plan of constant V corresponds to a subcube in the RGB cube (see Figure 7☼). Windows Palette In the accompanying article, you will note that at the end of the PALETTE.C module there are a number of color conversion routines that Palette uses when working with a particular mixing model. Whenever you define a new color the program automatically converts it to an equivalent representation in one of the other mixing models. Also, when you select a new color mixing model (using one of the radio buttons), Palette searches for the best possible match to the current color using the new model. This mathematical interpolation-and-search process is somewhat inexact; however, it gives you a good idea of how to create a similar color using a different model. Although in most cases the match is exact, the quantized nature of the spectrum control may result in some inconsistencies. The routines in PALETTE.C that are used to perform this color conversion are shown in Figure 8. In the previous discussion we presented all of the algorithms except for the HSV model. This algorithm is a little more complicated (mathematically it is a form of topological convolution) and is best left for another discussion. If you are interested, several texts on computer graphics are available that discuss this model and contain the RGB-to-HSV conversion algorithm. Besides being an interesting program that shows the use of customized dialog box controls, Palette also provides insight into color mixing models and their device-specific dependencies. This discussion has, we hope, given you a better understanding of these fundamentals and removed some of the uncertainties surrounding Windows GDI. Furthermore, the color conversion routines presented might be of good use when you build your next Windows application. Many good texts contain a thorough presentation of color, color mixing models, and interactive computer graphics in general. One of the more helpful books is Fundamentals of Interactive Computer Graphics, by J.D. Foley and A. Van Dam, which is published by Addison-Wesley. Figure 5: This formula calculates YIQ equivalents of RGB values. Y = (0.30*Red) + ( 0.59*Green) + ( 0.11*Blue) I = (0.60*Red) + (-0.28*Green) + (-0.32*Blue) Q = (0.21*Red) + (-0.52*Green) + ( 0.31*Blue) Figure 8: The routines shown are used by Palette to convert color values from one mixing model to another. RGBtoCRT() convert from RGB to CRT (GDI RGB) model RGBtoCMY() convert from RGB to CMY model RGBtoHSV() convert from RGB to HSV model CRTtoRGB() convert from CRT to RGB model CMYtoRGB() convert from CMY to RGB model HSVtoRGB() convert from HSV to RGB model ████████████████████████████████████████████████████████████████████████████ Creating User-Defined Controls for Your Own Windows Applications Kevin P. Welch☼ Microsoft(R) Windows, like other graphical environments, appears to suffer from a limitation that programmers frequently complain about──they are restricted to a predefined set of tools that virtualize the host machine. However, buried deep within the Windows Software Development Kit (SDK) are clues to suggest that this may not be the case. The cooperative nature of Windows offers a number of mechanisms that can be employed to create new and dramatically different software objects. One of the most exciting of these mechanisms is the ability to define your own dialog box controls. This lets you functionally extend Windows, making your application more appealing visually and enhancing the user interface. For example, you can define controls to represent switches, gauges, rulers, and other useful tools. We will explore the workings of a particular control called Spectrum and investigate its use in a color mixing program called Palette. In the preceding article, "Color Mixing Principles and How Color Works in the Raster Video Model," we explored various theoretical color mixing models. Palette can be used to demonstrate three of those representations. Control Fundamentals Almost everything of importance in the Windows environment is some kind of window. Standard dialog box controls, as you would expect, are predefined windows that can be used throughout your application. These controls are similar to the very windows registered and created by your own programs. However, unlike most of the windows you create, controls are by necessity object-oriented in nature and consist of completely reentrant code. This implies that a control should not access static data nor be dependent on the fact that only one thread of execution may be present. Also, in order for the control to be correctly handled by the dialog box manager, it must process all keyboard or mouse input received in a consistent manner. Although the details involved in implementing a control can get rather complicated, the basic steps are simple. Come up with a class name. Each control must be referenced by a unique name. This same class name is also used in your RC file whenever you wish to use the control in a dialog box. Be careful not to use one of the predefined class names provided by Windows; the results can be quite spectacular if you accidentally duplicate one of these names. Define all instance variables. Instance variables are private static data associated with each instance of your control. This data must be allocated dynamically and be readily accessible to each thread of execution. Although you could develop your own scheme for instance variables, you should probably consider using local or global atoms, property lists, or perhaps defining extra bytes to be associated with every instance of the window class data structure. Define your control window function. Each control must have an associated window function responsible for processing all related messages. Since your control will usually be managed by the dialog box manager, you should consider implementing both a keyboard and a mouse user interface. At the very least, your window function should handle the following messages: ■ WM_CREATE. When you receive this message you should initialize any instance variables and perform all other necessary initialization steps. ■ WM_PAINT. After receiving this message, you should perform any painting desired in the client region of your window. You should be careful to tune the results of your paint operation for a variety of display devices, especially those of radically different pixel-aspect ratios. This might be the most crucial and time-consuming part of the entire control development process. ■ WM_DESTROY. This message indicates that you should release any memory associated with the control instance and perform any other cleanup needed before the window is destroyed. If your control must be able to receive input (both mouse and keyboard), you should consider handling one or more of the following messages: ■ WM_GETDLGCODE. Your response to this message determines how the dialog box manager handles your control. By responding to this message with one of the return codes shown in Figure 1, you can manage a particular type of input and process the data yourself. ■ WM_SETFOCUS. When you receive this message you should create and display some sort of caret to indicate to the user that you have possession of the input focus. ■ WM_KILLFOCUS. This message is received whenever some other window receives the input focus. If you have a caret defined you should destroy it at this time. ■ WM_KEYDOWN. This message is received whenever a key is pressed or held down. By processing the virtual key codes you can perform any necessary actions. Be sure to notify the parent window whenever an action is taken that may result in a new system state, such as a button being pressed or an item selected from a list box. ■ WM_LBUTTONDOWN. This message is received whenever the user presses the left mouse button inside the control window. In most cases you will want to capture the system focus, dis-play your caret, and capture mouse movements. If the mouse click resulted in a new selection, you may also want to notify the parent window of the change. ■ WM_MOUSEMOVE. Although in most situations you will want to ignore this message, it may sometimes be desirable to perform an action if it is received in the context of a mouse-drag operation. ■ WM_LBUTTONUP. This message is received whenever the user releases the left mouse button. If you are capturing mouse movements, you should be careful to release the capture at this time. Note that it is entirely possible to receive extra WM_LBUTTONDOWN and WM_LBUTTONUP messages, so you should make sure to exclude those that are out of context. Register the control class. Before using the control in an application, you have to register the control window class. Any attempt to use the control before it has been registered will result in a serious system error and may even prevent your application from running. In practice it is a good idea to define a specific function as part of the entire control package that performs this task. Export the control window function. As is the case with any other window function in your application, you must export it before use. Failure to do so is a very common problem encountered by even the most experienced Windows programmers. Define related utility functions. The creation of even a simple control often requires the definition of a number of associated utility functions that enable the host application to define or retrieve various control-related parameters. Although a carefully defined message scheme will sometimes suffice, in more complicated situations it is better to create a specific function that performs each desired action. This provides an even greater degree of encapsulation for the control and promotes the development of self-contained software objects. Include the control in desired RC files. Generally, you want to use the dialog box editor (part of the Windows SDK) to create the templates for inclusion in your RC file; unfortunately, the dialog box editor does not directly support user-defined controls. I have found it helpful to define the size and location of the control using a static frame. When completed I edit the resulting DLG file contents and replace the frame with the appropriate control. Be sure to note that the dialog box editor is not capable of handling RES files that contain references to such user-defined controls. Spectrum Control The best way to explain the definition and implementation of user-defined controls is to look at a particular example. The Spectrum control is a particularly interesting one to study as it has a well-developed user interface that utilizes both local and global instance data. The Spectrum control was a result of my frustration with the lack of a system-defined mechanism for selecting colors. At the time, many of the applications I was developing required the user to be able to choose one or more colors from a predefined palette. After several attempts using list boxes and scroll bars I decided that it would be advantageous to design a series of general-purpose controls to solve this and several other related user-interface problems. The result of this development was the creation of a library of controls ranging from gauges, switches, and buttons to sliders and other selection tools. The Spectrum control is a simplification of a tool developed for this library. With the Spectrum control you can define a sequence of colors from which the user can select. Each color maintained by the control is associated with a numerical value that is returned to the parent window each time a new selection is made. This numerical value can be a character, integer, long, or float. In certain situations this numerical value may even be the same red-green-blue (RGB) color value that is displayed. The currently selected color is indicated by a small rectangle that surrounds the color in question. When the Spectrum control has the input focus, the system caret is also displayed inside this rectangle. If the caret is visible, the selector can be moved with the cursor keys or by dragging it to the left or right with the mouse. Before using the Spectrum control in a dialog box it must be registered, which is done by calling the RegisterSpectrum function. See Figure 2 for a listing of several of the functions referred to for Spectrum. (Complete source code listings and executables for Spectrum and Palette may be downloaded from any one of our bulletin boards-Ed.) The only unusual aspect of this function is the use of the cbWndExtra field in the WndClass data structure. The value specified defines the number of extra bytes to be allocated with each instance of the Spectrum window. These extra bytes can then be accessed with the functions SetWindowWord and GetWindowWord. Note the various definitions at the beginning of SPECTRUM.C that provide access to these extra bytes of data. If you compare the offsets used to those predefined for GetWindowWord, you will discover that those defined in WINDOWS.H are negative offsets. This means that those bytes allocated under the cbWndExtra specification are always positive and start at offset zero. The SetSpectrum function is a small utility function that permits you to define the selected color patch from the palette currently available. Note how the selector index is saved as an instance variable using a predefined window offset. Like SetSpectrum, GetSpectrum is an analog function that lets you retrieve an index to the currently selected color patch. Although this function is extremely simple, it is useful because it allows the host application to remain unaware of the internal implementation details of the Spectrum control. As I mentioned previously, when an instance of the Spectrum window is created, a default set of colors is automatically generated. The SetSpectrumColors function allows you to replace this default set of colors and associated numerical values with ones that you define. In doing so, you reset the selected color to the first one in the range and the parent is notified of the change. Note that the array of colors offered consists of a series of RGB colors followed by a series of associated numerical values that are returned when the color is selected. The GetSpectrumColors function permits you to retrieve the current color and value table maintained by the Spectrum control. No check is made to verify that sufficient space is available for the list of values. This can easily be added but the host application is usually well aware of the range of values it might be expected to support. The SpectrumWndFn function is possibly the most complicated part of the code associated with the Spectrum control. Although simple in structure, it is carefully designed to operate in conjunction with the dialog box manager. This is illustrated by the way in which it responds to the WM_GETDLGCODE message. In this case, DLGC_WANTARROWS, the value returned, informs the dialog box manager that the control wants to process all cursor-related keystrokes. This enables the control to provide a keyboard user interface for color selection. Note how the WM_SIZE command is processed. Under most normal circumstances controls are not resized; however, the handling of this message supports this possibility. Throughout the function SpectrumWndFn you will see several SendMessage function calls, which notify the parent whenever the current selection is changed. As with most other controls, information is returned to the parent window through a WM_COMMAND message. The wParam portion of this message is equal to the child window ID (this value is retrieved by using one of those negative window offsets), and the lParam portion is equal to the actual numerical value associated with the selected color. DrawSelector is the last function in SPECTRUM.C. This internal utility function is responsible for drawing the selector that highlights the selected color patch. Note how a TRANSPARENT drawing mode is used with the R2_NOT operator. This permits subsequent calls to the function to effectively restore the display to its original condition, eliminating the need to paint part of Spectrum between movements of the selector. Windows Palette Now that you have a reasonable understanding of how a typical user- defined control operates, we will investigate its use in the context of a small application. The Windows Palette is a logical extension of the COLORSCR program presented by Charles Petzold (see "A Simple Windows Application for Custom Color Mixing," MSJ, Vol. 2, No. 2). Like COLORSCR, Palette enables you to mix RGB colors to create a custom color mix. However, unlike COLORSCR, Palette uses the Spectrum control and allows you to mix colors with two additional color mixing models (see Figure 3☼). Furthermore, when you switch mixing models, Palette will attempt to match your currently defined color with a mixture from the new model (see Figure 4☼). If you examine PALETTE.H you will see the data structures used by Palette when it operates using the four different color mixing models it supports: CRT GDI red-green-blue RGB red-green-blue CMY cyan-magenta-yellow HSV hue-saturation-value The CRT model is directly analagous to the RGB one supported by the Graphics Device Interface (GDI); note that the GDI RGB model is different from the theoretical RGB model supported by Palette. In the process of mixing colors the RGB, CMY, and HSV models are used to define an equivalent GDI RGB (hereafter referred to as CRT) value, which is displayed in the upper right corner of the Palette window. For more on the color mixing models, refer to "Color Mixing Principles and How Color Works in the Raster Video Model,". The window used by the application is defined in PALETTE.RC as the Palette dialog box. Although this window was originally designed using the dialog box editor (part of the Windows SDK), it was manually changed in a few important areas. The first of these changes was adding WS_MINIMIZEBOX to the list of dialog box-style attributes. In combination with a minor change to the window class data structure, this enables the dialog box to become iconic. The second change was made to the ID_SAMPLE button. The button style was modified so that it would be initially disabled and the parent window would be responsible for painting the button contents. This was accomplished by changing the button style to BS_USERBUTTON| WS_DISABLED| WS_CHILD. The third change was to add three instances of the Spectrum control to the dialog box definition. When using the dialog box editor, the size and location of the Spectrum control were initially determined by using a static frame. Each frame was subsequently removed, and the Spectrum control inserted as a replacement using the original dimensions. At the start of PALETTE.C. nine small arrays that contain the default color tables for the Spectrum control windows are defined. The first three tables define the default values for the red, green, and blue spectra. Note that each table consists of 16 CRT values followed by 16 long representations of their floating-point equivalents. (In the theoretical RGB model each color value is in the continuous range {0,1}, whereas the CRT model utilizes discrete values in {0,255}.) The second and third triplets of tables define, respectively, the default values for the CMY and HSV mixing models. Although the Spectrum control does not put an upper bound on the size of the color table, each color patch must be sufficiently large in order to be easily visible. When considered along with the upper size limits suggested for dialog boxes in the Windows Style Guide, this means that a practical upper bound is about 30 color values. The WinMain function in PALETTE.C (see Figure 5 for listings of various functions) is different from those you have experienced in the past. Unlike a conventional WinMain, this one does not have a message processing loop──the actual retrieval and dispatching of messages is left to the dialog box manager. Apart from creating the Palette dialog box, the only other purpose of this routine is to register the Spectrum control by using the current instance handle as a parameter. Note that if a previous instance of the application exists, the Spectrum control is not reregistered. The PaletteDlgFn function, like most other dialog functions, is responsible for processing all messages relating to the Palette dialog box. Most of this function consists of relatively standard Windows code with exceptions in a few critical areas. The first exception is the way the WM_INITDIALOG message is handled. In this section the dialog box icon (initially NULL) is defined with a SetClassWord function call. This subtle change, when combined with the addition of WS_MINIMIZEBOX to the window style, allows the dialog box to be made iconic. The second exception is the handling of the Spectrum child window IDs in the processing of the WM_COMMAND message. As noted in the discussion of the SpectrumWndFn function, a WM_COMMAND message (with wParam equal to the child window ID and the lParam equal to the associated numerical value) is generated whenever the user selects a new color from the Spectrum. When this message is received by the dialog function, the appropriate color value is defined (using a long-to-float conversion), the corresponding color values calculated, and the numerical value displayed in the text field to the right of the Spectrum control. The third critical area of this function is the handling of the ID_SAMPLE ID when processing the WM_COMMAND message. The Palette dialog box contains a user-defined button; whenever this button needs repainting, HIWORD(lParam) is equal to BN_PAINT and LOWORD(lParam) to the button window handle. By retrieving the display context for this button we can create a sample of the currently selected color mixture. The SelectMixingModel function is responsible for switching the Palette dialog box to a different color mixing model whenever a new one is selected. In doing so it updates the related text fields and displayed color spectrum to reflect the new model. It also searches for the closest possible match, using the new model, to the currently defined color. Although this matching process will usually result in a nearly identical color, there are situations when the integral nature of the Spectrum control hinders this process, resulting in a slight shift in color. The MatchColor function is responsible for determining the best possible match by using the new color spectrum table. This matching process, although only involving local optima or extreme points, is relatively robust and produces a reasonable solution in most situations. The DlgPrintf function responds like sprintf except that the output is directed to a dialog box text control. For those unfamiliar with the constructs used in this routine, the vsprintf function is a standard Microsoft C Version 5.0 library call that performs an sprintf using a variable number of arguments. The remainder of the Palette module consists of six color conversion procedures that are responsible for converting one color value into another. Each of these routines is described in detail in the preceding article. Building Palette To build PALETTE.EXE, you need the Microsoft C 5.0 Compiler (or a later version) and the Windows Version 2.03 Software Development Kit. Before building the application you may need to make a few changes to the LNK and DEF files in order to account for the specific organization of your hard disk. Once you have made these changes, you can create PALETTE.EXE by entering the following command: MAKE PALETTE After compiling and linking the program you can experiment with the Palette on your own computer. As you can see, designing your own dialog box controls is a simple task that can greatly enhance any user interface. With a little effort you can easily rework Spectrum into several other interesting controls. Before long you'll be well on your way to developing your own toolbox of software objects for that next exciting project. Figure 1: Return Codes for WM_GETDLGCODE DLGC_WANTALLKEYS intercept all keyboard input DLGC_WANTARROWS intercept only cursor/direction keys DLGC_WANTCHARS intercept only WM_CHAR messages DLGC_WANTMESSAGE intercept all messages DLGC_WANTTAB intercept tab key Figure 2: Code Fragments from SPECTRUM.C /* * WINDOWS SPECTRUM CONTROL * * LANGUAGE : Microsoft C 5.0 * TOOLKIT : Windows 2.03 SDK * MODEL : Small or Medium * STATUS : Operational * * 03/20/88 - Kevin P. Welch - initial creation. * */ #include <windows.h> #include "spectrum.h" ∙ ∙ ∙ /* * RegisterSpectrum( hAppInstance ) : BOOL * * hAppInstance application instance handle * * This function is responsible for the definition and registration * of the spectrum window class. Note that this function should * only be called ONCE (typically during the initialization phase * of the host application) within a program. * * A value of TRUE is returned if the registration operation was * performed sucessfully. * */ BOOL FAR PASCAL RegisterSpectrum( hAppInstance ) HANDLE hAppInstance; { /* local variables */ WNDCLASS WndClass; /* window class data structure */ /* Define spectrum window class. Note that no check is made to * see if it has been previously registered! It is up to the host * application to prevent multiple registrations. */ memset( &WndClass, 0, sizeof(WNDCLASS) ); WndClass.lpszClassName = (LPSTR)"Spectrum"; WndClass.hCursor = LoadCursor( NULL, IDC_ARROW ); WndClass.lpszMenuName = (LPSTR)NULL; WndClass.style = CS_HREDRAW|CS_VREDRAW; WndClass.lpfnWndProc = SpectrumWndFn; WndClass.hInstance = hAppInstance; WndClass.hIcon = NULL; WndClass.cbWndExtra = SPECTRUM_EXTRA; WndClass.hbrBackground = (HBRUSH)(COLOR_WINDOW + 1 ); /* register spectrum window class & return */ return( RegisterClass( (LPWNDCLASS)&WndClass ) ); } ∙ ∙ ∙ /* * SetSpectrumColors( hWnd, pwRange, prgbList ) : BOOL * * hWnd handle to spectrum control in dialog box * pwRange number of color entries in rgb list * prgbList list of rgb colors & values for spectrum * * This function enables the caller to define a new spectrum * with associated lookup values for the control. It is left * up to the calling routine to determine the appropriate rgb * colors and lookup values. Note that the selected color is * always reset to the first available entry whenever a new * range is defined. * * A value of TRUE is returned if a new rgb color list was * sucessfully defined. * */ BOOL FAR PASCAL SetSpectrumColors( hWnd, pwRange, prgbList ) HWND hWnd; WORD * pwRange; LONG * prgbList; { /* local variables */ WORD wEntry; /* temporary color entry */ HANDLE hrgbList; /* handle to rgb list */ LONG FAR *lprgbEntry; /* pointer to rgb list */ RECT rectClient; /* client area rectangle */ /* release previous table from memory */ GlobalFree( TABLE ); hrgbList = GlobalAlloc( GMEM_MOVEABLE, sizeof(LONG)*(*pwRange)*2L); if ( hrgbList ) { /* define initial rgb colors & values */ lprgbEntry = (LONG FAR *)GlobalLock( hrgbList ); for ( wEntry=0; wEntry < *pwRange; wEntry++ ) { lprgbEntry[wEntry] = prgbList[wEntry]; lprgbEntry[(*pwRange)+wEntry] = prgbList[(*pwRange)+wEntry]; } GlobalUnlock( hrgbList ); /* retrieve current window dimensions */ GetClientRect( hWnd, &rectClient ); /* re-define instance variables */ SET_RANGE( *pwRange ); SET_TABLE( hrgbList ); SET_WIDTH( (rectClient.right-rectClient.left)/(*pwRange) ); SET_HEIGHT( rectClient.bottom-rectClient.top ); SET_CHOICE( 0 ); /* update window & notify parent of new selection */ InvalidateRect( hWnd, NULL, TRUE ); SendMessage(PARENT, WM_COMMAND,ID, lprgbEntry[RANGE+CHOICE]); /* normal return */ return( TRUE ); } else return( FALSE ); } ∙ ∙ ∙ /* * SpectrumWndFn( hWnd, wMsg, wParam, lParam ) : LONG * * hWnd handle to spectrum window * wMsg message number * wParam single word parameter * lParam double word parameter * * This function is responsible for processing all the messages * which relate to the spectrum control window. Note how the * code is written to avoid potential problems when re-entrancy * happens - this involves the use of extra bytes associated * with the window data structure. * * The LONG value returned by this function is the conventional * result of the default window procedure or the internal * handling of a message. * */ LONG FAR PASCAL SpectrumWndFn( hWnd, wMsg, wParam, lParam ) HWND hWnd; WORD wMsg; WORD wParam; LONG lParam; { /* local variables */ LONG lResult; /* temporary result variable */ /* initialization */ lResult = TRUE; /* process message */ switch( wMsg ) { case WM_GETDLGCODE : /* capture all key strokes */ lParam = DLGC_WANTARROWS; break; case WM_CREATE : /* create pallette window */ { /* temporary variables */ HANDLE hrgbList; /* handle to rgb list */ LONG FAR * lprgbEntry; /* pointer to rgb list */ /* allocate space for rgb color list */ hrgbList=GlobalAlloc( GMEM_MOVEABLE, sizeof(LONG)*16L ); if ( hrgbList ) { /* define initial rgb color & value list - note that * eight default colors are selected with the values * matching each of the colors. */ lprgbEntry = (LONG FAR *)GlobalLock( hrgbList ); lprgbEntry[0] = RGB( 0x00, 0x00, 0x00 ); lprgbEntry[1] = RGB( 0x00, 0x00, 0xFF ); lprgbEntry[2] = RGB( 0x00, 0xFF, 0x00 ); lprgbEntry[3] = RGB( 0xFF, 0x00, 0x00 ); lprgbEntry[4] = RGB( 0x00, 0xFF, 0xFF ); lprgbEntry[5] = RGB( 0xFF, 0xFF, 0x00 ); lprgbEntry[6] = RGB( 0xFF, 0x00, 0xFF ); lprgbEntry[7] = RGB( 0xFF, 0xFF, 0xFF ); lprgbEntry[8] = RGB( 0x00, 0x00, 0x00 ); lprgbEntry[9] = RGB( 0x00, 0x00, 0xFF ); lprgbEntry[10] = RGB( 0x00, 0xFF, 0x00 ); lprgbEntry[11] = RGB( 0xFF, 0x00, 0x00 ); lprgbEntry[12] = RGB( 0x00, 0xFF, 0xFF ); lprgbEntry[13] = RGB( 0xFF, 0xFF, 0x00 ); lprgbEntry[14] = RGB( 0xFF, 0x00, 0xFF ); lprgbEntry[15] = RGB( 0xFF, 0xFF, 0xFF ); GlobalUnlock( hrgbList ); /* define instance variables */ SET_RANGE( 8 ); SET_TABLE( hrgbList ); SET_WIDTH( ((LPCREATESTRUCT)lParam)->cx / 8 ); SET_HEIGHT( ((LPCREATESTRUCT)lParam)->cy ); SET_CHOICE( 0 ); SET_CAPTURE( FALSE ); } else DestroyWindow( hWnd ); } break; case WM_SIZE : /* window being resized */ /* redefine width & height instance variables */ SET_WIDTH( LOWORD(lParam) / 8 ); SET_HEIGHT( HIWORD(lParam) ); break; case WM_PAINT : /* paint control window */ { PAINTSTRUCT Ps; /* paint structure */ WORD wEntry; /* current color entry */ HANDLE hBrush; /* handle to new brush */ HANDLE hOldBrush; /* handle to old brush */ LONG FAR * lprgbEntry; /* pointer to rgb list */ /* start paint operation */ BeginPaint( hWnd, (LPPAINTSTRUCT)&Ps ); /* iteratively paint each color patch */ lprgbEntry = (LONG FAR *)GlobalLock( TABLE ); for ( wEntry=0; wEntry<RANGE; wEntry++ ) { /* create solid brush for patch & select */ hBrush = CreateSolidBrush( lprgbEntry[wEntry] ); hOldBrush = SelectObject( Ps.hdc, hBrush ); /* draw rectangle with brush fill */ Rectangle( Ps.hdc, wEntry*WIDTH, 0, (wEntry*WIDTH)+WIDTH, HEIGHT ); /* unselect brush and delete */ SelectObject( Ps.hdc, hOldBrush ); DeleteObject( hBrush ); } GlobalUnlock( TABLE ); /* define current selection & end paint operation */ DrawSelector( hWnd, Ps.hdc ); EndPaint( hWnd, (LPPAINTSTRUCT)&Ps ); } break; case WM_KEYDOWN : /* key being pressed */ { /* local variables */ HDC hDC; /* display context handle */ LONG FAR * lprgbEntry; /* pointer to rgb list */ /* retrieve display context & unmark current selection*/ hDC = GetDC( hWnd ); DrawSelector( hWnd, hDC ); /* process virtual key codes */ switch( wParam ) { case VK_HOME : /* home key */ SET_CHOICE( 0 ); break; case VK_LEFT : /* left cursor key */ SET_CHOICE( (CHOICE > 0) ? CHOICE-1 : RANGE-1 ); break; case VK_RIGHT : /* right cursor key */ case VK_SPACE : /* space bar - move right */ SET_CHOICE( (CHOICE < RANGE-1) ? CHOICE+1 : 0 ); break; case VK_END : /* end key */ SET_CHOICE( RANGE-1 ); break; default : /* some other key */ lResult = FALSE; break; } /* mark new selection & release display context */ DrawSelector( hWnd, hDC ); ReleaseDC( hWnd, hDC ); /* move caret to new position */ SetCaretPos( CARET_XPOS, CARET_YPOS ); /* notify parent of new selection */ lprgbEntry = (LONG FAR *)GlobalLock( TABLE ); SendMessage(PARENT,WM_COMMAND,ID,lprgbEntry [RANGE+CHOICE]); GlobalUnlock( TABLE ); } break; case WM_SETFOCUS : /* get focus - display caret */ /* create caret & display */ CreateCaret( hWnd, NULL, CARET_WIDTH, CARET_HEIGHT ); SetCaretPos( CARET_XPOS, CARET_YPOS ); ShowCaret( hWnd ); break; case WM_LBUTTONDOWN : /* left button depressed - fall through */ { /* local variables */ HDC hDC; /* display context handle */ LONG FAR * lprgbEntry; /* pointer to rgb list */ /* retrieve display context */ hDC = GetDC ( hWnd ); /* unmark old selection & mark new one */ DrawSelector( hWnd, hDC ); SET_CHOICE( LOWORD(lParam)/WIDTH ); DrawSelector( hWnd, hDC ); /* release display context & move caret */ ReleaseDC( hWnd, hDC ); /* capture focus & move caret */ if ( hWnd == SetFocus(hWnd) ) SetCaretPos( CARET_XPOS, CARET_YPOS ); /* notify parent of new selection */ lprgbEntry = (LONG FAR *)GlobalLock( TABLE ); SendMessage(PARENT,WM_COMMAND,ID,lprgbEntry [RANGE+CHOICE]); GlobalUnlock( TABLE ); /* activate capture */ SetCapture( hWnd ); SET_CAPTURE( TRUE ); } break; case WM_MOUSEMOVE : /* mouse being moved */ /* track mouse only if capture on */ if ( CAPTURE ) { /* local variables */ HDC hDC /* display context handle */ WORD wNewChoice; /* new mouse selection */ LONG FAR * lprgbEntry; /* pointer to rgb list */ /* calculate new selection */ wNewChoice = ( *((int*)&lParam) <= 0 ) ? 0 : ( LOWORD(lParam)/WIDTH >= RANGE ) ? RANGE - 1 : LOWORD(lParam) / WIDTH; /* update display if different */ if ( wNewChoice != CHOICE ) { /* retrieve display context */ hDC = GetDC ( hWnd ); /* unmark old selection & mark new one */ DrawSelector( hWnd, hDC ); SET_CHOICE( wNewChoice ); DrawSelector( hWnd, hDC ); /* release display context & move caret */ ReleaseDC( hWnd, hDC ); SetCaretPos( CARET_XPOS, CARET_YPOS ); /* notify parent of new selection */ lprgbEntry = (LONG FAR *)GlobalLock( TABLE ); SendMessage(PARENT,WM_COMMAND,ID, lprgbEntry[RANGE+CHOICE]); GlobalUnlock( TABLE ); } } break; case WM_LBUTTONUP : /* left button released */ /* release capture if active */ if ( CAPTURE ) { SET_CAPTURE( FALSE ); ReleaseCapture(); } break; case WM_KILLFOCUS : /* kill focus - hide caret */ DestroyCaret(); break; case WM_DESTROY : /* window being destroyed */ GlobalFree( TABLE ); break; default : /* default window message processing */ lResult = DefWindowProc( hWnd, wMsg, wParam, lParam ); break; } /* return final result */ return( lResult ); } ∙ ∙ ∙ Figure 5: Code Fragments from PALETTE.C /* * WINDOWS COLOR PALETTE UTILITY - SOURCE * LANGUAGE : Microsoft C 5.0 * TOOLKIT : Windows 2.03 SDK * MODEL : Small * STATUS : Operational * 03/20/88 - Kevin P. Welch - initial creation. */ #include <windows.h> #include <math.h> #include "spectrum.h" #include "palette.h" ∙ ∙ ∙ /* * WinMain( hInst, hPrevInst, lpszCmdLine, wCmdShow ) : int * * hInst handle of current instance * hPrevInst handle to previous instance (if any) * lpszCmdLine pointer to command line arguments * wCmdShow initial ShowWindow command * * This function is the system entry point for the application and is * responsible for defining the appropriate window classes and for * processing all the messages. Note how the dialog box manager is * responsible for the operation of the palette window. */ int PASCAL WinMain( hInst, hPrevInst, lpszCmdLine, wCmdShow ) HANDLE hInst; HANDLE hPrevInst; LPSTR lpszCmdLine; WORD wCmdShow; { /* local variables */ FARPROC lpProc; /* temporary function */ /* register window if first instance */ if ( hPrevInst || RegisterSpectrum(hInst) ) { /* display palette dialog box */ lpProc = MakeProcInstance( (FARPROC)PaletteDlgFn, hInst ); DialogBox( hInst, "Palette", NULL, lpProc ); FreeProcInstance( lpProc ); } /* end program */ return( FALSE ); } /* * PaletteDlgFn( hWnd, wMsg, wParam, lParam ) : BOOL * hWnd handle to palette window * wMsg message number * wParam single word parameter * lParam double word parameter * * This function is responsible for processing all the messages * which relate to the color palette dialog box. This mainly * involves the definition and retrieval of the various colors * selected by the user. * */ BOOL FAR PASCAL PaletteDlgFn( hDlg, wMsg, wParam, lParam ) HWND hDlg; WORD wMsg; WORD wParam; LONG lParam; { /* local variables */ BOOL bResult; /* result of function */ /* initialization */ bResult = TRUE; /* process messages */ switch( wMsg ) { case WM_INITDIALOG : /* initialize dialog box */ /* select crt mixing model */ wModel = ID_RGB; SelectMixingModel( hDlg, wModel ); /* define icon for dialog box */ SetClassWord( hDlg, GCW_HICON, LoadIcon( INSTANCE, (LPSTR)"PaletteIcon" ) ); break; ∙ ∙ ∙ case WM_COMMAND : /* window command */ /* process sub-message */ switch( wParam ) { case ID_RGB : /* RGB mixing model */ case ID_CMY : /* CMY mixing model */ case ID_HSV : /* HSV mixing model */ SelectMixingModel( hDlg, wParam ); break; case ID_SPECTRUM1 : /* 1st spectrum */ switch( wModel ) { case ID_RGB : rgbColor.fRed = *((float*)&lParam); RGBtoCMY( &rgbColor, &cmyColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE1, "%.3f", rgbColor.fRed ); break; case ID_CMY : cmyColor.fCyan = *((float*)&lParam); CMYtoRGB( &cmyColor, &rgbColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE1, "%.3f", cmyColor.fCyan ); break; case ID_HSV : hsvColor.fHue = *((float*)&lParam); HSVtoRGB( &hsvColor, &rgbColor ); RGBtoCMY( &rgbColor, &cmyColor ); DlgPrintf( hDlg, ID_VALUE1, "%.0f", hsvColor.fHue ); break; } InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL ); break; case ID_SPECTRUM2 : /* 2nd spectrum */ switch( wModel ) { case ID_RGB : rgbColor.fGreen = *((float*)&lParam); RGBtoCMY( &rgbColor, &cmyColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE2, "%.3f", rgbColor.fGreen ); break; case ID_CMY : cmyColor.fMagenta = *((float*)&lParam); CMYtoRGB( &cmyColor, &rgbColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE2, "%.3f", cmyColor.fMagenta ); break; case ID_HSV : hsvColor.fSaturation = *((float*)&lParam); HSVtoRGB( &hsvColor, &rgbColor ); RGBtoCMY( &rgbColor, &cmyColor ); DlgPrintf( hDlg, ID_VALUE2, "%.3f", hsvColor.fSaturation ); break; } InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL ); break; case ID_SPECTRUM3 : /* 3rd spectrum */ switch( wModel ) { case ID_RGB : rgbColor.fBlue = *((float*)&lParam); RGBtoCMY( &rgbColor, &cmyColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE3, "%.3f", rgbColor.fBlue ); break; case ID_CMY : cmyColor.fYellow = *((float*)&lParam); CMYtoRGB( &cmyColor, &rgbColor ); RGBtoHSV( &rgbColor, &hsvColor ); DlgPrintf( hDlg, ID_VALUE3, "%.3f", cmyColor.fYellow ); break; case ID_HSV : hsvColor.fValue = *((float*)&lParam); HSVtoRGB( &hsvColor, &rgbColor ); RGBtoCMY( &rgbColor, &cmyColor ); DlgPrintf( hDlg, ID_VALUE3, "%.3f", hsvColor.fValue ); break; } InvalidateRect( GetDlgItem(hDlg,ID_SAMPLE), NULL, NULL ); break; case ID_SAMPLE : /* sample color patch */ /* update sample only if necessary */ if ( HIWORD(lParam) == BN_PAINT ) { /* local variables */ HDC hDC; CRT crtValue; HANDLE hOldBrush; RECT rectClient; /* display crt equivalent numerical values */ RGBtoCRT( &rgbColor, &crtValue ); DlgPrintf( hDlg, ID_RED, "RED %u", crtValue.cRed ); DlgPrintf( hDlg, ID_GREEN, "GREEN %u", crtValue.cGreen); DlgPrintf( hDlg, ID_BLUE, "BLUE %u", crtValue.cBlue ); /* paint color sample */ hDC = GetDC( LOWORD(lParam) ); GetClientRect( LOWORD(lParam), &rectClient); /* setup display context */ hOldBrush = SelectObject(hDC, CreateSolidBrush( *((DWORD*)&crtValue) ) ); /* draw color sample */ Rectangle( hDC, rectClient.left, rectClient.top, rectClient.right, rectClient.bottom ); /* cleanup & release display context */ DeleteObject( SelectObject(hDC,hOldBrush) ); ReleaseDC( LOWORD(lParam), hDC ); } break; default : /* something else */ break; } break; case WM_NCLBUTTONDBLCLK : /* non-client double click */ EndDialog( hDlg, TRUE ); break; default : bResult = FALSE; break; } /* return result */ return( bResult ); } /* * SelectMixingModel( hDlg, wNewModel ) * hDlg handle to palette dialog box * wNewModel id of new mixing model * * This function is responsible for changing the displayed * color mixing model to a new one. In doing so the color * spectra are changed and an attempt is made to find a * match for the currently defined color under the new * mixing model. */ void SelectMixingModel( hDlg, wNewModel ) HANDLE hDlg; WORD wNewModel; { /* local variables */ WORD wRange; WORD wEntry; /* define new range & check button */ wRange = 16; wModel = wNewModel; CheckRadioButton( hDlg, ID_RGB, ID_HSV, wModel ); /* perform initialization depending on model */ switch( wNewModel ) { case ID_RGB : SetDlgItemText( hDlg, ID_TITLE1, "Red.......:" ); SetDlgItemText( hDlg, ID_TITLE2, "Green.....:" ); SetDlgItemText( hDlg, ID_TITLE3, "Blue......:" ); wEntry = MatchColor(rgbRed,rgbColor.fRed); SetSpectrumColors( SPECTRUM1, &wRange, rgbRed ); SetSpectrum( SPECTRUM1, &wEntry ); wEntry = MatchColor(rgbGreen,rgbColor.fGreen); SetSpectrumColors( SPECTRUM2, &wRange, rgbGreen ); SetSpectrum( SPECTRUM2, &wEntry ); wEntry = MatchColor(rgbBlue,rgbColor.fBlue); SetSpectrumColors( SPECTRUM3, &wRange, rgbBlue ); SetSpectrum( SPECTRUM3, &wEntry ); break; case ID_CMY : SetDlgItemText( hDlg, ID_TITLE1, "Cyan......:" ); SetDlgItemText( hDlg, ID_TITLE2, "Magenta...:" ); SetDlgItemText( hDlg, ID_TITLE3, "Yellow....:" ); wEntry = MatchColor(cmyCyan,cmyColor.fCyan); SetSpectrumColors( SPECTRUM1, &wRange, cmyCyan ); SetSpectrum( SPECTRUM1, &wEntry ); wEntry = MatchColor(cmyMagenta,cmyColor.fMagenta); SetSpectrumColors( SPECTRUM2, &wRange, cmyMagenta ); SetSpectrum( SPECTRUM2, &wEntry ); wEntry = MatchColor(cmyYellow,cmyColor.fYellow); SetSpectrumColors( SPECTRUM3, &wRange, cmyYellow ); SetSpectrum( SPECTRUM3, &wEntry ); break; case ID_HSV : SetDlgItemText( hDlg, ID_TITLE1, "Hue.......:" ); SetDlgItemText( hDlg, ID_TITLE2, "Saturation:" ); SetDlgItemText( hDlg, ID_TITLE3, "Value.....:" ); wEntry = MatchColor(hsvHue,hsvColor.fHue); SetSpectrumColors( SPECTRUM1, &wRange, hsvHue ); SetSpectrum( SPECTRUM1, &wEntry ); wEntry = MatchColor(hsvSaturation,hsvColor.fSaturation); SetSpectrumColors( SPECTRUM2, &wRange, hsvSaturation ); SetSpectrum( SPECTRUM2, &wEntry ); wEntry = MatchColor(hsvValue,hsvColor.fValue); SetSpectrumColors( SPECTRUM3, &wRange, hsvValue ); SetSpectrum( SPECTRUM3, &wEntry ); break; } } /* * MatchColor( plTable, fValue ) : iEntry * plTable table of color values * fValue value to search for * This utility function searches a spectrum table for * a particular entry using the key value provided. An * index to the closest match is returned for subsequent * use when switching color mixing modes. * * Note that an exact match may not be present (due to the * discrete nature of the spectrum table). As a result * some variations in the resulting color may by visible. */ static int MatchColor( plTable, fValue ) LONG * plTable; float fValue; { int iEntry; int iMinimum; float fMinimum; /* initialization */ iMinimum = 0; fMinimum = ABS(*((float*)&plTable[16])-fValue); /* search for best fit */ for ( iEntry=16; iEntry<32; iEntry++ ) { if (ABS(*((float*)&plTable[iEntry])-fValue) < fMinimum ) { iMinimum = iEntry - 16; fMinimum = ABS(*((float*)&plTable[iEntry])-fValue); } } /* return best entry */ return( iMinimum ); } ∙ ∙ ∙ ████████████████████████████████████████████████████████████████████████████ SQL Server Brings Distributed DBMS Technology to OS/2 Via LAN Manager ─────────────────────────────────────────────────────────────────────────── Also see the related article: SQL: A Short Primer ─────────────────────────────────────────────────────────────────────────── Marc Adler☼ The OS/2 systems open up many possibilities for building distributed applications. Separate processes can perform different tasks, and, by using the kind of interprocess communication (IPC) offered by OS/2, a highly coordinated system can be constructed from different parts. A sophisticated front end that can communicate with a back-end process to form a single application, for example, can be developed by using the OS/2 Presentation Manager. With the addition of the Microsoft(R) LAN Manager, OS/2 can also be used to develop distributed applications. Front- end and back-end processes will run on totally separate machines hooked together on a network and will be able to easily pass information back and forth. This approach is frequently called client-server computing. The client- server approach allows large numbers of PC-based user workstations (called clients) to access one or many server machines across a local area network. Client machines provide an intelligent and flexible user interface. Servers bring minicomputer- and mainframe-style shared services to the PC world and, because they are accessed through intelligent clients, are not limited to the "dumb" terminal-based applications characteristic of minis and mainframes. Among the first of these distributed, client-server applications is SQL Server. SQL Server SQL Server is a high-performance, multiuser DBMS designed specifically for MS(R) OS/2-based local area networks, especially those that utilize the Microsoft OS/2 LAN Manager software. It is based on technology originally developed by Sybase for minicomputers and optimized by Microsoft and Sybase for OS/2 LANs. It incorporates such features as stored procedures, triggers, and an advanced transaction-oriented SQL kernel. These features are essential for guaranteeing data integrity in the multiuser LAN environment. The advances, along with SQL Server's high- volume performance, mean that SQL Server will enable LANs to become a viable platform for business-critical applications, which until now were limited to the mainframe and minicomputer environment. SQL Server is a server-based implementation of the SQL language (Structured Query Language, usually pronounced "sequel"), the language by which applications communicate with relational databases. It is the underlying "technology" that will work in conjunction with the OS/2 LAN Manager and end-user software created by third-party developers to transparently: ■ package SQL requests from any user workstation ■ send those requests to a server across a LAN ■ perform whatever requests were made via the SQL statements ■ package the results in a suitable manner ■ return the results from the server to the user workstation SQL Server is an "open platform" that will give developers the ability to create SQL Server-compatible front-end applications. Users can always have direct access to the SQL language; however, most will access some front-end program that hides the underlying SQL. This gives developers the opportunity to create new and exciting applications and user interfaces that offer the power of SQL without requiring users to know the language. SQL Server is the first SQL-based database management system designed from the ground up that supports high-volume transaction processing, as well as the less demanding decision support applications that other SQL databases have targeted──it has the ability to support the entire application spectrum, from simple to complex. SQL Server runs on any OS/2-based server machine, and multiple instances of SQL Server can be on any number of servers on a given network. Users can concurrently and transparently access shared databases through SQL Server from any OS/2- based, as well as any DOS-based, workstation on a network (see Figures 1 and 2). SQL Server is a joint development effort of Microsoft and Sybase and includes technological contributions from Ashton-Tate as well. Ashton- Tate will make its dBASE(R) IV product compatible with SQL Server so that existing dBASE III(R) and new dBASE IV applications can run unchanged with SQL Server. SQL Server will be sold by Ashton-Tate through its retail channels under the name Ashton-Tate/Microsoft SQL Server. Microsoft will license SQL Server to hardware OEMs, who will resell the product through their standard channels. Industry Needs The corporate world has come to depend on data for every decision that needs to be made; data is now the most important resource of a company after its people. As the base of corporate data grows, so does the complexity of the data, and as corporate users become more hungry for data, they become more demanding as well. More information from more sources and substantial decreases in access time are demands high on the list of every user. At the top of the list is the ability to conduct reliable, high-speed transactions against a given database. To handle this data demand, centralized corporate MIS departments turned to the technology of relational database systems such as IBM's DB2 and used mainframe and minicomputer systems as their hardware backbones. With DB2 and other relational systems came the emergence of SQL, a high- level database language that programmers could embed within their COBOL and PL/1 programs to access data and perform other powerful database functions. Meanwhile at the departmental level, PCs, networked workstations, and micro-to-mainframe links were becoming commonplace. The need for more local distributed processing of data and for a powerful SQL engine that could run as a back-end server on a network became obvious. A Typical Scenario Wall Street is an excellent example of an industry in great need of PC- based distributed processing. Financial institutions are constantly engaged in putting together more powerful workstations for their traders, since any increase in a trader's productivity can translate into substantial revenue increases for a firm. Recent workstation and PC developments have provided considerably improved windowing environments and analytical tools. However, these advances do not address the Wall Street trader's most urgent need: quick access to large amounts of real-time information, as well as the ability to process transactions safely. The financial industry has long needed a database system that would run on networked PCs and workstations and yet be powerful enough to handle a large number of transactions. Such a system would also include the ability to have several users perform actions against this data. The emergence of networked workstations, relational database systems, SQL, and the high-speed transaction processing needs of the corporate world indicate that SQL Server's design goals──to provide high transaction volume, guaranteed data integrity, and support for SQL-were correct. SQL Server, together with the emergence of OS/2 and the OS/2 LAN Manager, finally brings distributed, SQL-based database power to the PC community (see Figure 3). The SQL Standard SQL, the language through which actual database queries are made, grew out of IBM's pioneering research into database models. The relational database model was developed by E.F. Codd, a research scientist with IBM, in the early 1970s. SQL was intended to be the standard query language for the relational model. It was first used in System R, a research database system created at IBM's San Jose research facility. SQL has since been commercialized in IBM's two mainframe database products, DB2 and SQL/DS. IBM has announced that SQL will be part of its Systems Application Architecture (SAA), one of the reasons why SQL has essentially become the standard for relational-based query languages. As many large corporations are IBM shops, vendors will have to support SQL to sell to these corporations. There is now an ANSI standard for SQL, which can be considered further proof of the language's acceptance. To compete in the marketplace, any dialect of SQL should comply with this standard, and SQL Server fully supports the ANSI standard. SQL Server's dialect of SQL is called Transact-SQL, and is, in fact, a superset of the ANSI SQL standard, support-ing all of the SQL commands and offering important additions for developers. The Client-Server Model When the phrase "front end" is used, you think of user interfaces, and when "back end" is mentioned, you think of a black box that invisibly stores and retrieves data. The front and back ends are considered two distinct parts of the same program; the front end gathers data from the user and prepares it for the back end to process. The back end manipulates the data and sends it back to the front end when finished. The front end then takes the processed data and presents it as a completed transaction to the user. The client-server model represents the concept of a front end──back end system (see Figure 4). A client is a requester of services, and a server performs these services at the behest of a client. A server sits and waits for a request to be delivered to it by a client. When the request comes, the server performs some actions and may deliver some data back to the client. In a multiprocessing or networked environment, the client and the server are completely separate processes and may even run on different machines. Under the OS/2 LAN Manager, any networked machine can be either a workstation or a server, depending on how the station is configured. Any server station can also simultaneously function as a workstation, that is, the server is not dedicated. SQL Server can reside on any machine configured as a server. Client applications reside on any machines configured as workstations. When a client, or front-end, application requests a service from SQL Server, the underlying SQL Server software transparently handles the request (performing its role as the back end or server), provides the service, and returns information or data if any was requested. The client and server may both reside on the same machine, or more commonly, the server will be on some machine unknown to the user. The user only needs to understand how the client program works; SQL Server and the network software take care of everything else. Multiple front-end (client) processes can access a single SQL Server simultaneously. In the SQL Server environment, there can be a many-to-many relationship between clients and servers. On the client side, there might be a spreadsheet, a forms manager, and a report writer, each requiring access to records from a database. On the server side, you can have multiple SQL Servers running over a network, with each server providing access to a different set of databases (see Figure 5). SQL Server guarantees referential integrity, ensuring that multiple processes accessing the same data will not corrupt the integrity of the data. A major concern of database users is the ability of the database to withstand crashes. Ideally, one user mishandling the system should not affect other users. The client-server model gives the system a certain amount of fault tolerance. By having the front and back ends in separate processes, the entire system does not have to die if one process crashes. For instance, if a spreadsheet client goes down, the database server can still service the other clients. By having a functionally complete server like SQL Server, software vendors can concentrate on developing powerful front-end tools for the server. This is especially true with SQL Server; database engines are difficult to build, consuming substantial manpower for any company. With a powerful database engine already in place, corporations can instead invest their R&D dollars in new and better client/front-end (or user) interface technology such as expert systems and natural language processing. Features of SQL Server SQL Server provides users with a complete SQL-database environment while offering an unprecedented level of security and integrity checking. Rules, defaults, triggers, stored procedures, and views, the necessary components of a complete database system, all contribute to SQL Server's efforts to make your data error-free. Transact-SQL, SQL Server's superset of the ANSI standard, gives the user increased expressiveness by offering flow-of-control constructs among its numerous extensions to the standard SQL language. Let's look at the most important features of SQL Server and outline the ways in which the user can access these enhancements through SQL (see Figure 6). Referential Integrity One of the most pressing problems in database technology is the enforcement of referential integrity, which concerns keeping data consistent across the various tables in a database. Let's say that you had two tables in your company's database: one table contains the names, addresses, and identification numbers of all your customers, and the other table contains the current orders for each customer. If you delete a record from the customer table, you must first ensure that there are no outstanding orders for that customer. If the customer had an order pending and you mistakenly removed the customer's vital statistics from the customer table, then the pending order would refer to a nonexistent customer ID. In database terminology, you must ensure that when you delete a primary key from a table (such as a customer record), the corresponding foreign keys in the dependent table (such as the customer id field in the orders table) are also removed. A trigger is a stored procedure that is fired when a specific modification action is applied to a table in a database. The three modification actions that you can trigger on are INSERT, DELETE, and UPDATE. Using triggers, it is possible to "trap" a modification before the database has committed to the new data. If an error condition is discovered, a user-defined action can be performed to enforce integrity, including a rollback of the database. Stored Procedures Before an SQL statement is executed, it must go through several steps: ■ the command must be parsed ■ all the names must be validated through the data dictionary ■ protections are checked through the data dictionary ■ an execution plan is formulated, which involves determining the proper index to use to access the data, among other optimization measures ■ the query is compiled into a form that is easier for SQL Server to execute Stored procedures let you save compiled SQL statements for later recall, thus avoiding the overhead described above when the procedures are subsequently executed. Stored procedures also help in enforcing database integrity, since you can incorporate your "business rules" into a stored procedure, for example, if this is a customer's first order, then set the CreditGiven field to FALSE. Candidates for stored procedures are statements that you execute frequently and complicated statements that you will use in the future but do not want to retype. In fact, any series of SQL statements that can be executed through Transact-SQL can be automated through a stored procedure. Stored procedures have optional parameters that will be "bound" when you call the procedure. Rules and Defaults To further enforce data integrity, you can specify rules that apply to columns. For instance, if you run a furniture store, you might want to prevent someone from entering "rocketship" in the ItemOrdered field of the orders table. In SQL Server, as data is added to a column by an INSERT or UPDATE action, all rules associated with the column are checked. If the data does not conform to the constraints that have been set by the rules, then the transaction will be rolled back. You use the CREATE RULE statement to establish a new rule. To further enforce data integrity, SQL Server supports default values for columns, which are entered into the column whenever you do not enter an explicit data value for the column. A default value can be associated with one or more columns, or with all the columns in the database that have a user-defined type. A default value is created by the CREATE DEFAULT statement. Flow-of-Control SQL Server provides conventional flow-of-control constructs within Transact-SQL to facilitate writing more complex commands. By using these constructs along with stored procedures and triggers, the SQL Server programmer will rely less on coding external programs in languages such as C. SQL Server supports IF/THEN/ELSE statements, WHILE loops with CONTINUE and BREAK jumps, GOTOs and labels, and the declaration of local variables. A simple PRINT statement enables the output of messages and local variables. You can assign values to local variables with a special form of the SELECT statement. The SELECT statement should produce a single value, which will be assigned to the local variable. If the SELECT statement produces multiple values, then the last value returned will be assigned to the local variable. Transaction Processing A transaction is defined as both a unit of work and a unit of recovery. Substantial efforts have been made to ensure that SQL Server has a complete safety mechanism. Users can define their own transactions and specify the method of recovery that will take place should the transaction fail. If planned correctly, recovery from crashes can take minutes as opposed to hours for other DBMSs. System overhead is also reduced because the system is accessed only at the completion of the transaction instead of at the end of every statement. To create a user-defined transaction, the statements comprising the transaction are surrounded by the BEGIN TRANsaction and COMMIT TRANsaction statements. The statements can contain any SQL statements, including stored procedures that you define. You can cancel the entire transaction or just a part of it. SQL Server lets you put place markers, called "savepoints," anywhere inside a transaction. To restore a database image to a previous state, you must "rollback" the database with the ROLLBACK command. Views Suppose you have a database of employee information on your system. This database might contain some sensitive data such as salary and social security number that you wouldn't want all the users of the database to see. It would be helpful if you could define a "virtual" table consisting only of the nonsensitive fields of the database. SQL Server lets you do this by using "views." A view is a customized way to look at the records of a database. You are able to specify a subset of the rows and columns of a table for the view, choose items selected from multiple tables, and even group data in ways that differ from the data's physical storage. A view is treated just like a separate table as far as the user is concerned; the SELECT statement can always reference a view. INSERT and UPDATE can only reference a view that reflects a single table. The result from any SELECT statement is employed to create a view. The SELECT statement serves as the "extraction" part of the CREATE VIEW statement in order to define the columns and records used in the view. For a more detailed look at the common SQL commands, as well as their structure and syntax, see the sidebar, "SQL: A Short Primer." Developer Support SQL Server gives software developers a database engine that they won't soon outgrow. Since the engine is complete and implemented as a server, developers can concentrate on creating advanced front-end tools to access data more quickly and more easily. A software development kit, function libraries, a rich and open set of APIs, and published specs will be available to software developers. Programmers will be able to write programs in C to access and manipulate an SQL Server database by utilizing the APIs that will be supplied in DB- Lib, part of the SDK (see Figure 7 for a simple example). Unlike "embedded-SQL" interfaces, the developer builds SQL statements and sends them directly to SQL Server──the developer does not have to precompile a program with a special SQL compiler. A Platform for the Future SQL Server's triggers, rules, defaults, and rollbacks of transactions relieve much of the worry of database integrity. Corporate users will especially appreciate SQL Server's ability to handle the large volume of their on-line transactions. The next several years should be interesting for database users as more and more vendors adopt the SQL Server platform and begin developing innovative and exciting distributed applications. Together with the open architecture of OS/2, the graphical user interface of Presentation Manager, and LAN Manager/SQL Server networking support, developers now have the platform necessary to build new and exciting networked applications. Figure 1: SQL Server will be compatible with any OS/2 or DOS-based workstation. ╔═════════════╤═════════════╗ ╔═════════════╤═════════════╗ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│▒Application▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│▒Application▒║ ║▒Application▒├─────────────╢ ║▒Application▒├─────────────╢ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░Windows░░░░║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│Presentation░║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░░░░░░░░░░░░║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒│░░Manager░░░░║ ╟─────────────┴─────────────╢ ╟─────────────┴─────────────╢ ║ DOS ║ ║ OS/2 ║ ╚═════════════╦═════════════╝ ╚═════════════╦═════════════╝ ║ ║ ══════════════╩═══════════════╦═══════════════════╩═════════════════════ ║ ╔══════════════════╩══════════════════╗ ║ OS/2 ║ ╟─────────────────────────────────────╢ ║ LAN Manager ║ ╟─────────────────────────────────────╢ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒SQL Server▒▒▒▒▒▒▒▒▒▒▒▒▒║─────┐ ┌─────────────┐ ╚═════════════════════════════════════╝ │ │ Shared │ Data Integrity └─┤ Database │ High Performance └─────────────┘ Figure 2: Independent software vendors (ISVs) will be able to build front-end applications and user interfaces that will utilize SQL Server. DOS or OS/2 front ends: PC Databases Spreadsheets ╔════════════════════════════════╗ Languages ║░░░░░░Front-end Application░░░░░║ Vertical Markets ╟────────────────────────────────╢ Corporate/MIS Applications ║▒▒▒▒▒▒▒▒▒▒▒DB-Library▒▒▒▒▒▒▒▒▒▒▒║ ∙ ╚═══════════════╦════════════════╝ ∙ ║ ∙ ║ ═════════════════╩═══════════════╦════════════════════════════════════ ║ True Database Server ║ Data Integrity ╔══════════════╩═══════════════╗ Security ║ OS/2 ║ ╔══════════╗ High Performance ╟──────────────────────────────╢ ║ ║ ║ LAN Manager ║ ║ Database ║ ╟──────────────────────────────╢ ┌──╢ ║ ║▒▒▒▒▒▒▒▒▒▒SQL Server▒▒▒▒▒▒▒▒▒▒╟──┘ ╚══════════╝ ╚══════════════════════════════╝ Figure 3: New Applications specifically designed for network environments and mission-critical industry applications that formerly required mainframe and minicomputer systems can now be run on PCs utilizing the LAN Manager and SQL Server Software. ╔══════════════════════╤══════════════════════╤══════════════════════╗ ║ Traditional PC │ New Network-oriented │ Mission-critical ║ ║ Applications │ Applications │ Industry Applications║ ╟──────────────────────┼──────────────────────┼──────────────────────╢ ║ ▒▒▒▒PC Databases▒▒▒▒▒│▒▒Document Libraries▒▒│▒▒▒▒▒▒Accounting▒▒▒▒▒▒║ ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║ ▒▒▒▒Spreadsheets▒▒▒▒▒│▒▒▒▒▒▒▒CAD/CAM▒▒▒▒▒▒▒▒│▒▒▒▒▒Manufacturing▒▒▒▒║ ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║ ▒▒▒▒▒Languages▒▒▒▒▒▒▒│▒▒Desktop Publishing▒▒│▒▒▒▒▒▒▒▒Banking▒▒▒▒▒▒▒║ ║ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║ ▒▒▒Report Writers▒▒▒▒│▒▒▒▒Electronic Mail▒▒▒│Transaction Processing║ ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║ ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║ ║ ▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒│▒▒▒▒▒▒▒▒▒▒∙▒▒▒▒▒▒▒▒▒▒▒║ ╟──────────────────────┼──────────────────────┼──────────────────────╢ ║░Enhanced by powerful░│░Designed especially░░│░░░Now possible on░░░░║ ║░░░network database░░░│░░░░for networks░░░░░░│░░░░░PC networks░░░░░░║ ║░░░░░░░systems░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░│░░░░░░░░░░░░░░░░░░░░░░║ ╚══════════════════════╧══════════════════════╧══════════════════════╝ Figure 4: Client-Server Model ╔═════════════════════════════════════════╗ ║ Users ║ ╟─────────────────────────────────────────╢ Intelligent ║ Front-end Applications──DOS and OS/2 ║ User Interface ╟───────────╥───╥───────────╥──╥──────────╢ ║▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒║ ║Accounting║ ║PC Database║ ║Spreadsheet║ ║▒▒System▒▒║ • • • ║▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒║ ╚═════╦═════╝ ╚═════╦═════╝ ╚═════╦════╝ ║ ║ ║ ═LOCAL AREA NETWORK═══╦═══╩════════════╦══╩════════════╦═╩═════════════════ ║ ║ ║ ║ ║ ║ ║ ║ ║ Powerful ╔════╩═════╗ ╔═════╩════╗ ╔════╩═════╗ Mulituser ║File/Print║ ║▒Database▒║ ║▒▒▒Mail▒▒▒║ Services ║▒▒Server▒▒║ ║▒▒Server▒▒║ ║▒▒Server▒▒║ • • • ╚══════════╝ ╚══════════╝ ╚══════════╝ Back-end OB/2 Servers Figure 5: Typical Client-Server Applications Database Communications gateways ╔══════════════╗ ╔══════════════╗ Mail store/forward ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Name/directory ║▒▒▒Server 1▒▒▒║∙∙∙║▒▒▒Server N▒▒▒║ database ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Document Library ╚═══════╦══════╝ ╚═══════╦══════╝ ∙ ║ ║ ∙ ║ ║ ∙ ════════╦═════════════════╦══╩══════════════════╩══════════════════════ ║ ║ Spreadsheet ║ ║ Word Processor ╔═══════╩══════╗ ╔══════╩═══════╗ Mail front end ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Database query/ ║▒▒▒Client 1▒▒▒║∙∙∙║▒▒▒Client N▒▒▒║ report package ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Desktop Publishing ╚══════════════╝ ╚══════════════╝ ∙ ∙ ∙ Figure 6: SQL Server and Server Intelligence ╔════════════════╗ ╔═════════════════╗ ╔═════════════════╗ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒C Language ▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║Data Application║ ║▒▒▒Application▒▒▒║ ║▒▒▒Spreadsheet▒▒▒║ ∙ ∙ ∙ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ ╚═══════╦════════╝ ╚════════╦════════╝ ╚═════════╦═══════╝ ║ ║ ║ ║ ║ ║ ════════╩═════════════════════╩══════════╦═══════════╩═══════════════ ║ ║ Server Intelligence ╔════════╩════════╗ Programmable Server ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Rules protect data ║▒▒▒▒SQL Server▒▒▒║ Stored Procedures, ║▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒║ Triggers, Views ╚═════════════╤═══╝ ╔════════╗ │ ║ Rules ║ │ ║┌──────┐║ └──────╢│ Data │║ ║└──────┘║ ╚════════╝ Figure 7: Code Sample Using the DB-Lib APIs /* ** DBLIBDEM.C - DB-Lib/C EXAMPLE ** ** This is an example program which illustrates the use of ** sending two queries to the DataServer, in a batch, and ** then binds each of the statements and prints the rows. ** ** */ #include <stdio.h> #include <sybfront.h> #include <sybdb.h> extern char *dbgetmsg(); extern LOGINREC *dblogin(); extern DBPROCESS *dbopen(); #define DATELEN 25 #define TYPELEN 2 main(argc, argv) int argc; char *argv[]; { DBPROCESS *dbproc; /* Our connection with the DataServer */ LOGINREC *login; /* Our login information */ /* These are the variables used to store the returning data. */ DBDATETIME crdate; char datebuf[DATELEN]; int datelen; DBINT id; DBCHAR name[MAXNAME+1]; /* MAXNAME is defined in * sybdb.h, as the maximum * length for names of database * objects, such as tables, * columns, and procedures. */ RETCODE result_code; char type[TYPELEN+1]; /* ** Get a LOGINREC structure, and fill it with the necessary ** login information. */ login = dblogin(); DBSETLAPP(login, "example1"); /* ** Get a DBPROCESS structure for our communication with the DataServer. ** Using NULL for the servername will use the server specified by ** the DSQUERY environment variable. */ if ((dbproc = dbopen(login, (char *)NULL)) = = NULL) { dbperror(dberrno()); printf("Couldn't connect with server.\n"); exit(0); } printmsgs(dbproc); /* ** We are going to retrieve some information from a table ** named 'sysobjects', regarding names of system tables and ** stored procedures. ** We will submit two queries. The first finds all rows ** which describe system tables. The second finds all the rows ** which describe stored procedures. The program will only look ** at the first 10 rows which describe stored procedures. */ /* First put the commands into the command buffer. */ dbcmd(dbproc, "select name, type, id, crdate from sysobjects"); dbcmd(dbproc, " where type = 'S' "); dbcmd(dbproc, "select name, type, id, crdate from sysobjects"); dbcmd(dbproc, " where type = 'P' "); /* ** The DataServer processes the command batch in the following ** order: ** 1) It will check for syntax errors (i.e. 'use database pubs' is ** syntactically incorrect, it should be 'use pubs'). ** 2) The second check is a semantic check (i.e. 'select * from titles' ** will be incorrect because the spelling should be 'titles.') ** 3) The third check is the actual execution phase. This will check ** for things like permissions or memory problems. ** ** Bn the execution phase, dbsqlexec and dbresults can return the ** value 'SUCCEED', which means there are more commands in the batch ** to process and that that command was successful. A value of 'FAIL' ** means that the query failed, but there may be more commands in the ** batch to process. A value of 'NO_MORE_RESULTS' means that there are ** no more commands in the batch to process. ** Therefore, the programmer must check for the return values ** after dbsqlexec and dbresults as illustrated below. */ /* Now send the commands to the DataServer & start execution. */ if (dbsqlexec(dbproc) == FAIL) printf("dbsqlexec FAILed.\n"); printmsgs(dbproc); /* Initialize the last character of our "type" array - * dbbind() does not append a terminating NULL to indicate * the end of character arrays, when the type is CHARBIND. */ type[TYPELEN] = '\0'; /* Process each command until there are no more. */ while ((result_code = dbresults(dbproc)) != NO_MORE_RESULTS) { printmsgs(dbproc); if (result_code = = SUCCEED) { /* Bind program variables. */ dbbind(dbproc, 1, NTBSTRINGBIND, 0, name); dbbind(dbproc, 2, CHARBIND, 0, type); dbbind(dbproc, 3, INTBIND, 0, &id); dbbind(dbproc, 4, DATETIMEBIND, 0, &crdate); /* Print header according to which data is * coming back. */ printf("\n %s Objects: \n\n", DBCURCMD(dbproc) = = 1 ? "System Table": "Procedure"); /* Now print the rows. */ while (dbnextrow(dbproc) != NO_MORE_ROWS) { /* ** If this is the 2nd command and ** 10th row, flush the rest of the ** rows for that command. */ if ((DBCURCMD(dbproc) = = 2) && (DBCURROW(dbproc) > 10)) continue; /* Convert DataServer datetime datatype * to ascii. */ datelen = datetochar(&crdate, datebuf, DATELEN); datebuf[datelen] = '\0'; printf ("%s %s %ld %s\n", name, type, id, datebuf); } printmsgs(dbproc); } else printf("dbresults FAILed.\n"); } /* Close our connection and exit the program. */ dbexit(); exit(0); } /* ** PRINTMSGS ** Check the DBPROCESS for any error or informational messages ** returned from the DataServer. ** ** Parameters: ** dbproc - the DBPROCESS to check for messages. ** ** Returns: ** none. */ printmsgs(dbproc) DBPROCESS *dbproc; { char *msg; while ((msg = dbgetmsg(dbproc)) != NULL) { printf("%s \n", msg); } } ─────────────────────────────────────────────────────────────────────────── SQL: A Short Primer ─────────────────────────────────────────────────────────────────────────── SQL is a relatively straightforward and easy language to use──at least for basic database access. Though it's not reasonable to expect to actually learn SQL from the brief introduction provided here, you can get a good feel for its simplicity as well as its power by going through a demonstration of the major SQL commands and how they are used. Suppose, for example, that you are the new proprietor of the Kaps & Mugs Brewery and that you have decided to create a computerized employee database. Further, suppose that as your first project you have decided to put all of your employee information into a SQL Server system. We will create two tables to hold the employee information. The first table is called "employees" and consists of each employee's name, address, social security number, and employee identification number. A second table called "wages" contains the employee ID number, the employee's department number, and the employee's current salary. In both cases, let's say that you decide that the tables will be indexed by each employee's unique ID number. The CREATE verb is the basis for adding new objects to a SQL Server database. CREATE is used to make new tables, rules, triggers, defaults, stored procedures, and views. To add a new table to your database, the CREATE TABLE statement is used. The syntax is: CREATE TABLE [database.] table-name (column-name datatype [, column-name datatype]...) The employees table is created by the statement: CREATE TABLE employees (name char(20), address char(50), socsec char(9), id integer) The wages table is added by: CREATE TABLE wages (id integer, dept integer, salary money) If you want to create an index on a field, you use the CREATE INDEX index-name ON column-name statement. You can therefore create an index on the employees' unique id field by using: CREATE INDEX id_index ON employees(id) Indexing substantially speeds up certain operations. The field that is indexed is the primary key and must contain unique values. In order to further maximize the speed of operations, secondary indexes can be created on other fields as needed. SQL's basic data retrieval mechanism is the SELECT statement. The syntax of this statement is: SELECT [DISTINCT] expr-list FROM table-list [WHERE conditional-expr] [GROUP BY columns] [HAVING expr] [ORDER BY columns] The DISTINCT clause allows you to eliminate any duplicate records from the selection process. The GROUP BY clause lets you group the selected records together by columns; records whose column values are identical are printed together in the same group. The HAVING clause can apply more filtering to the data that is the subject of a GROUP BY. Let's examine some SELECT statements to see how complex queries can be built up. To select all the records in the employees table: SELECT * FROM employees To select an employee's name and social security number: SELECT name, socsec FROM employees To show the name and salary of each employee: SELECT name, salary FROM employees, wages WHERE employees.id = wages.id The above example performs an implicit database operation called a "join," which merges two or more tables by joining the records at a common field (in this case the ID field common to both tables). This is an illustration of the most common type of join, an "equi-join." You can print out the salaries and tax information for each employee and group them together by department: SELECT name, salary, salary * .25 FROM employees, wages WHERE employees.id = wages.id GROUP BY wages.deptnum Data is added to a table by using the INSERT statement. Its syntax is: INSERT table-name [(column-list)] VALUES (constant-expr [, constant-expr]) To add a record into the employees table: INSERT employees VALUES ('Marc Adler', 'New York City', '123-44-5667', 666) Besides inserting data manually, you can use the result of a SELECT statement to provide the necessary data: INSERT employees SELECT * FROM new_employees WHERE deptnum = 150 You can change the values in tables quite easily by using the UPDATE statement, whose syntax is: UPDATE table-list SET column1=expr [column2=expr]... [FROM table-list ] [WHERE expression] To give every employee in department 666 a 50 percent increase in salary: UPDATE wages SET salary = salary * 1.5 WHERE deptnum = 666 Finally, you can remove certain rows from a table by using the DELETE statement. The syntax for DELETE is: DELETE table-name [FROM table-list] [WHERE expression] If you decided that everyone in department 666 earned too much, you could: DELETE employees FROM employees, wages WHERE employees.id = wages.id AND wages.deptnum = 666 So far, the discussion has been limited to SQL statements that fall within the ANSI standard. Now let's discuss some of the powerful extensions of Transact:SQL that go beyond this standard. A Sample Trigger You make a trigger with the CREATE TRIGGER statement. The syntax of this statement is: CREATE TRIGGER [owner.]trigger-name ON [owner.]table-name FOR {INSERT | UPDATE | DELETE} [, {INSERT | UPDATE | DELETE}]... AS <SQL statements> There are several keywords that can be used inside the CREATE TRIGGER statement, which greatly enhance its power. The keyword "deleted" refers to a temporary table containing records that have just been DELETEd from the trigger table. The keyword "inserted" refers to another temporary table holding records that have just been INSERTed into the trigger table. The keywords "IF UPDATE(column-name)" can test if data in the specified column has been modified in any way. For example, suppose you want to enforce uniqueness on the primary key "id". When data is inserted into the "employees" table, you would like to see if the id field has been duplicated, and if it has, print an error message and rollback the entire transaction: CREATE TRIGGER id_trig ON employees.id FOR INSERT AS IF (SELECT count(*) FROM employees, inserted WHERE employees.id == inserted.id) != (SELECT count(*) from inserted) BEGIN rollback transaction print "You inserted a duplicate employee-id value." print "The transaction has not been completed." END You can remove a specific trigger from a table with the DROP TRIGGER command. The syntax is: DROP TRIGGER [owner.]trigger-name Creating Procedures The CREATE PROCEDURE statement adds a new stored procedure to the database. The syntax of the CREATE PROCEDURE statement is: CREATE PROCedure [owner.]proc-name [;number] [[(]@parameter-name datatype [= default] [, @parameter datatype [= default]]...[)]] [WITH RECOMPILE] AS SQL-statements A procedure can contain one or more SQL commands, as in the following example: CREATE PROC show_new_emp AS select id, name from employees where id >= 5000 Stored procedures are precompiled, and an "execution plan" is stored in a system table. When a stored procedure is called, you are guaranteed that the syntax is correct and little time will be spent on query optimization. To execute a stored procedure, you can either call the procedure by name or use the EXECUTE command. The syntax for this command is [EXECute] proc-name[;number] [value [,value] ...] [WITH RECOMPILE] or: [EXECute] proc-name[;number] [@parameter = value] [, @parameter = value] ... [WITH RECOMPILE] As with traditional high-level programming languages, you are permitted to pass arguments to a stored procedure. In the above example, if you wanted to replace the number 5000 with an integer variable, you could code the procedure as CREATE PROC show_new_emp @limit integer = 5000 AS select id, name from employees where id >= @limit and execute the procedure with the call: show_new_emp @limit=2000 In the second example, the integer variable @limit is the lower bound for the id comparison. The procedure call above will pass the value 2000 as the argument to this procedure. If no argument is passed, then the procedure will use the value 5000 as the default value for @limit. Rules and Defaults Rules are used to constrain the data values that a column(s) can contain. The syntax of the CREATE RULE statement is: CREATE RULE [owner.]rule-name AS conditional-expression As an example, suppose you want to create a rule that forces the wages.dept field to accept only department numbers 100, 200, 300, 400, 500, or a department number that begins with 99 and contains two additional digits. The rule that you would write in this case is: CREATE RULE deptrule AS @deptnum in ("100", "200", "300", "400", "500") or @deptnum like "99[0-9][0-9]" As with stored procedures, a rule can accept an "argument"; in this case the argument is @deptnum. By using run-time arguments, the same rule can apply to more than one column simultaneously. To bind a column name to a particular rule, you would use the system procedure sp_bindrule. To bind the above rule to the wages.dept column, invoke the command: sp_bindrule empidrule, "wages.dept" You can unbind a rule by using the sp_unbindrule procedure, and you can remove a rule from the database with the DROP RULE statement. A DEFAULT is a value associated with a particular column or datatype. The system will insert this default value into the target column if no value is provided explicitly by the user. The syntax for the DEFAULT statement is: CREATE DEFAULT [owner.]default-name AS constant-expression Once a default is created, you associate it with a column by binding it to that column with the "sp_bindefault" column. As an example, if you want the default value of field "salary" in the table "wages" to be 15000.00, you would enter the commands: CREATE DEFAULT sal_def AS 15000.00 sp_bindefault sal_def, "wages.salary" As with rules, you can unbind and drop defaults when you don't need them anymore. Flow-of-Control Example Transact:SQL provides the common flow-of-control constructs that appear in high-level languages like C. Included in the Transact:SQL extensions are IF/ELSE condition testing, WHILE/BREAK/CONTINUE loop control, GOTOs and labels, and local variable declaration. An example of these extensions is shown below, in which we ask the system to alert us if the brewery grows beyond 100 people: DECLARE @numemps int SELECT @numemps = count(*) from employees IF @numemps > 100 PRINT "Our company has grown too large. Time to move!" The WAITFOR statement is useful for scheduling commands to take place at a certain time. The syntax is: WAITFOR {DELAY "time" | TIME "time" | ERROREXIT | PROCESSEXIT} As an example, this code fragment waits until 2:20 in the afternoon, inserts a new record in the employees table, and calls a procedure to notify user Marc of the event: BEGIN WAITFOR TIME "17:00" IF select count(*)from employees >=1000 EXEC sendmessage 'adler','Too many employees-commence firing!' END Rollback Rollbacks restore the database to a previous state. Until a transaction is COMMITed, the database can be restored to the state it was in before the transaction was started or to a savepoint that existed during the transaction. The syntax of the ROLLBACK statement is: ROLLBACK TRANsaction [transaction-name | savepoint-name] If the transaction name or savepoint name is omitted, then the entire transaction will be rolled back. A skeleton of a typical transaction might look like this: BEGIN TRAN trans-name statement procedure SAVE TRAN savepoint-name statement IF <condition> ROLLBACK TRAN savepoint-name procedure statement IF <condition> ROLLBACK TRAN savepoint-name statement COMMIT TRAN As an example of a transaction, suppose we want to double the salary of all upper management (people in departments 99xx). We also want to experiment to see what happens if we promote all VPs who earn more than $50,000 to the highest salary grade. This experiment should not actually change the main database, so we will save the state of the database before we try the experiment and restore the state afterwards. BEGIN TRAN give_raise UPDATE wages SET salary = salary*2 WHERE dept >= 900 AND dept <= 9999 SAVE TRAN chkpt1 UPDATE wages SET dept = 9999 WHERE dept = 9998 AND salary >= 50000.00 ROLLBACK TRAN chkpt1 COMMIT TRAN Note that the above is an unconditional rollback, which will always restore the original state of the database. Views Views allow you to construct "virtual" tables whose fields and records are selectively derived from the fields and records in the master database. The syntax of the CREATE VIEW statement is: CREATE VIEW [[database.]owner]view-name [(column-name [, column-name]...)] AS select-statement For example, let's create a view that permits a user to see only the names and department numbers of all employees who earn under $50,000. To create this view, you use the following statement: CREATE VIEW emps(name, empdept) AS SELECT name, dept FROM employees, wages WHERE wages.salary < 50000.00 Once a view is created, you can use the GRANT and REVOKE statements to tell the system which users may manipulate this view (emps) and which users may manipulate the view's underlying table (employees). When a query is issued on a view, SQL Server combines the query with the stored conditions in the view to form the complete query on the underlying tables, a process called "view resolution." For example, if you have the query SELECT * FROM emps WHERE empdept = 942 then, after the view is resolved, the query SELECT name, dept FROM employees, wages WHERE salary < 50000.00 AND dept = 942 would be issued to SQL Server. The examples shown here are all fairly simple to read and understand, yet they perform some very powerful database manipulations across entire tables of information, either globally or selectively. In most cases, a simple SQL command will be able to perform operations that in a language such as dBASE III Plus(R) may require many lines of code, or even entire programs, to execute. As the proud new proprietor and database administrator of the Kaps & Mugs Brewery, you have successfully set up your employee database by using the standard SQL commands, and integrity-preserving constraints by using the powerful Transact:SQL extensions provided by SQL Server. Have one on me! Summary of SQL and Transact-SQL Commands and Syntax ╓┌─────────────────────┌─────────────────────────────────────────────────────╖ Command Syntax CREATE DEFAULT [owner.]default-name AS constant-expression CREATE INDEX index-name ON column-name CREATE PROCedure [owner.]proc-name [;number] [[(]@parameter-name datatype [= default] [, @parameter datatype [= default]]...[)]] [WITH RECOMPILE] AS SQL-statements CREATE RULE [owner.]rule-name Command Syntax CREATE RULE [owner.]rule-name AS conditional-expression CREATE TABLE [database.] table-name (column-name datatype [, column-name datatype]...) CREATE TRIGGER [owner.]trigger-name ON [owner.]table-name FOR {INSERT | UPDATE | DELETE} [, {INSERT | UPDATE | DELETE}]... AS <SQL statements> CREATE VIEW [[database.]owner]view-name [(column-name [, column-name]...)] AS select-statement DELETE table-name [FROM table-list] [WHERE expression] DROP TRIGGER [owner.]trigger-name Command Syntax DROP TRIGGER [owner.]trigger-name [EXECute] proc-name[;number] [value [,value] ...] [WITH RECOMPILE] [EXECute] proc-name[;number] [@parameter = value] [, @parameter = value] ... [WITH RECOMPILE] INSERT table-name [(column-list)] VALUES (constant-expr [, constant-expr]) ROLLBACK TRANsaction [transaction-name | savepoint-name] SELECT [DISTINCT] expr-list FROM table-list [WHERE conditional-expr] [GROUP BY columns] [HAVING expr] [ORDER BY columns] Command Syntax [ORDER BY columns] UPDATE table-list SET column1=expr [column2=expr]... [FROM table-list] [WHERE expression] WAITFOR {DELAY "time" | TIME "time" | ERROREXIT | PROCESSEXIT} GOTO and labels Flow-of-control IF/THEN/ELSE Condition testing Variables Local declarations WHILE/BREAK/CONTINUE Loop control ████████████████████████████████████████████████████████████████████████████ Ask Dr. Bob! DS and SS Segments Dear. Dr. Bob, I have been hearing a great deal about the DS and SS segment issue, specifically that DS and SS cannot be the same for Windows and OS/2. Why is "DS!=SS" so important and why is it necessary?──WFB Dear WFB, Anyone who has built a dynamic-link library (DLL) has run into the DS!=SS issue. Most C code is compiled under the assumption that the Data Segment (DS) and Stack Segment (SS) registers have the same value. They both point to the application's main data segment, called DGROUP. In small and medium models, this is almost a necessity because of the use of 16-bit NEAR pointers. These pointers do not contain any segment information, only the offset within a segment. The DS register provides the segment value needed to address the actual data. C allows you to take the address of any variable with the "&" operator, but note that some variables (statics and externs) are located in your data segment, while others (automatics, i.e., most local variables) are in the stack segment. If DS and SS have the same value, there is no problem because a 16-bit offset is enough to identify either kind of variable. However, if DS and SS are different (DS!=SS), a 16bit NEAR pointer does not provide enough information to address an automatic variable. For most MS-DOS(R), Windows, and OS/2 applications, this is never a problem. It's customary for DS and SS to contain the same value, which simply avoids the entire issue Dynamic-link libraries such as the ones used in Windows and OS/2 are another story. Most DLLs have their own data segments. for example, USER.EXE, one of the DLLs in Windows, stores the data structure that describes each window in its own data segment. Every time a function in a DLL is called, the prolog code saves the old DS register value and sets up DS to point to its own data segment. The catch is that the SS register still points to the stack of the calling program, which it must to be able to address the parameters that were pushed onto the stack before the function call. Dynamic-link libraries, therefore, generally run in the DS!=SS environment, thus imposing several restrictions on the code. first, the C code must be compiled with the -Aw switch. This tells the compiler not to make its usual assumption that DS and SS are equal. Second, any time the DLL uses the address of data on the stack that address must be a full 32-bit FAR pointer, not a NEAR pointer. finally, some of the C run-time functions are "off limits" because they assume that DS equals SS. The Microsoft(R) Windows Software Development Kit contains a list of those functions that can and cannot be used. for example, the scanf function (and its relatives) can't be used when DS!=SS. There are a few other situations where you will run into the DS!=SS problem. One such problem occurs when using the Windows function SetWindowsHook with one of the "system level" hooks, such as WH- SYSMSGFILTER. Although the rest of your application runs in a normal DS==SS environment, the hook function itself gets called from some other application's stack, so that function (and anything it calls) is running with DS!=SS. Bells and Graphics Dear Dr. Bob, I have two questions for you that concern the Microsoft(R) C Compiler shipped with the OS/2 Development Kit. First, can you tell me whether it supports any of the graphics primitives now available with Version 5.0 of the C Compiler? Second, how does one control the bell? I'd like to have a C function that turns on the sound generator for a specified duration, pitch, and volume.──RB Dear RB, The Microsoft C Compiler shipped with the OS/2 Development Kit does not yet support the graphics primitives available in Version 5.0. These primitives will be available for OS/2 in a future version of Microsoft C. With regard to the function you are looking for to control the bell, please look at the DosBeep() function, which accepts frequency and duration. The function is one of the family API functions, or a dualmode OS/2 function──one that will work under both MS-DOS 3.x and OS/2. FAR and NEAR Memory Objects Dear Dr. Bob, A recent issue of MSJ described how Microsoft C 5.0 can address memory objects, declared as FAR, but accessed as NEAR (with the ES register). My question is, can I use small-model library functions (memset, memchr, strcpy, etc.) with these objects, and should I preserve ES in my assembler functions when I use this feature?──AF Dear AF, With the information provided by the same-seg pragma, Microsoft C 5.0 knows that several FAR data objects share the same segment address. Consequently this segment address is kept in ES and not reloaded for each new FAR reference. Think of the compiler as enregistering a segment value when it knows that several far data objects are in the same segment. Can you use the small-model library functions with these objects? No, you cannot because small-model library functions assume the segment address is kept in DS, not in ES or passed on the stack. Should you preserve ES in your assembler function when you use this feature? No, you don't need to. When the compiler enregisters a value, it takes care to save and restore this value through function calls. Optimization Blues Dear Dr. Bob, I have a bad case of the Optimization Blues. Please consider the following code fragment: int compare (n1, n2) char *n1, *n2; { char i; for (i = 0; i < 8; if (*n1++ != *n2++) return 0; return 1; } Compiling this code under Microsoft C 4.0 resulted in 46 bytes using the /Ox/G1 switch. With Microsoft C 5.0 it's 57 bytes using /Oails/Gs/G1, and 62 bytes with/Ox/G1. That' s an increase in code size of 25 percent even if I specify "favor code Size?" Overall, code size increases 10-15 percent on some relatively small and not too complex programs even when turning off loop optimization. This might not seem such a big issue, but since I have to squeeze a large application into 256Kb of EPROM space, an increase of 10 percent means I'd have to use microsoft C 4.0. Looking at the generated code, at least 9 bytes could be saved by a simple reordering of statements, and another 9 if the compiler were smart enough to recognize that it does not have to store the values of temporary variables before returning from a function. I can accept code size increases for option /Ox. If I say "favor speed," then it's OK with me if the compiler triples code to double speed. But code increases in this magnitude when specifying "favor size" are not what I would call "optimizing." Help!──TW Dear TW, The problems with code size are interesting. Basically, -O1 is totally a "speed favored over size" consideration. The reason for this is that the nature of loops is such that they tend to be executed often. For example speed is the overriding consideration. A command like cl -Ols test.c is essentially an oxymoron. Unfortunately, this is probably not as well documented as it ought to be. Your example also illustrates another problem. One new optimization (intended for speed) is to pull the exit code for a return up to the return statement, instead of jumping to the exit code whenever there is a return statement. It turns out that avoiding the jump increases speed, especially on benchmarks dominated by function call/return overhead. Obviously, this generates larger code. Unfortunately, it is not controlled by -Os, but by a different switch, -Or, which may not be well documented either. -Os should disable this optimization and will in future releases. In any event, if you compile your code fragment using cl -Osr -G1 test.c then the difference in code size with Microsoft C 4.0 is 3 bytes. Two of the 3 bytes are caused by a change in the way code is generated for loops (even without -Ol) in order to make the loop optimizer's job easier. The last byte is a nop used to pad routines with an odd number of bytes so that CodeView(R) will be able to disassemble the last instruction correctly. ════════════════════════════════════════════════════════════════════════════

Vol. 3 No. 5 Table of Contents

Bridge/386: A Tool for Integrating Applications Under Windows/386 Bridge/386 from Softbridge Microsystems is a powerful control system built specifically for the Microsoft(R) Windows/386 environment. It allows the end user to create dialog boxes and menus and to control and automate access to both Windows and non-Windows programs. A Guide to Understanding Even the Most Complex C Declarations C declarations and declaration extensions are particularly perplexing to both the novice and experienced programmer. A knowledge of these extensions, as well as an understanding of the structure and syntax of standard declarations, is essential for the professional programmer. Techniques for Debugging Multithread OS/2 Programs with CodeView(R) 2.2 Debugging an OS/2 program with multiple threads of execution running concurrently within the same program can seem daunting. CodeView 2.2, a powerful tool for the OS/2 programmer, provides new and enhanced commands for debugging multithread and protected-mode programs. Exchanging Data Between Applications Using the Windows Clipboard The Windows clipboard is the standard mechanism for data exchange between applications. Diverse data formats and asynchronous events can make using the clipboard tricky. This article explores some techniques, tricks, and traps for applications that exploit the clipboard. Using Microsoft(R) C Version 5.1 to Write Terminate-and-Stay-Resident Programs Terminate-and-stay-resident (TSR) programs have traditionally been written in assembly language. Microsoft C Version 5.1 includes new features that make it easier to create TSRs in C. We examine the new interrupt keyword and some tricks for writing TSRs. Customizing the Features of the M Editor Using Macros and C Extensions The Microsoft(R) Editor (M) is a powerful tool that runs under both MS-DOS(R) and OS/2. Traditional editing features are enhanced by the ability to run compilers from within the editor and the ability to create macros and extend the editor by adding user-written C functions. Dynamically Creating Dialog Boxes Using New Windows 2.x Functions Previously, dialog boxes had to be created statically in an resource file. Windows 2.x includes two new functions that allow programmers to create dialog boxes dynamically. These two new functions are detailed, and suggestions are offered for building them into your code. EDITOR'S NOTE The introduction of any advance in computer hardware or operating system technology is usually approached by software developers with healthy skepticism and caution. The advent of OS/2 has been no different. In the twelve months since the announcement of OS/2, thousands of developers around the world have attended training seminars, and many are hard at work on OS/2 applications. But at the same time much of the computer community has, like Vladimir and Estragon in Waiting for Godot, lamented the absence of a protagonist. Where is the single "killer app" that will establish OS/2? We suspect that theatrics aside, OS/2 is really an evolutionary change in which no single application dominates the stage. The importance of OS/2 is that it makes possible an entire class of cooperating applications. The large memory, multitasking, and interprocess communications capabilities of OS/2 allow applications to co-reside and communicate. For instance, a word processor might use a chart or a table from a spreadsheet that in turn receives its input from a database application. Dynamic data exchange (DDE) and the clipboard are among the facilities that will make such scenarios possible. The adoption of a standard user interface in which "cut" and "paste" mean the same thing in different applications and data is viewed in a consistent manner will make these connections between applications comprehensible to users. We have explored this theme in a number of articles. In our last issue we looked at DARWIN from Merrill Lynch & Co., Inc., which links a SQL data manager with Microsoft(R) Excel. In this issue, Bridge/386(c) from Softbridge demonstrates how even "uncooperating" applications can be combined to create a whole that is more powerful than the sum of its parts. We also offer a practical guide to exploiting the clipboard. While these explorations have focused on Microsoft Windows, they are even more appropriate for OS/2. In the future, the successful applications will not be one-man shows but instead solid team efforts. The star performers will be applications that work seamlessly with others, permitting the user to mix and match capabilities. For the developer the challenge is to maximize functionality and compatibility.──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor KAREN STRAUSS Assistant Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor KIM HOROWITZ Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CIRCULATION STEVEN PIPPIN Circulation Director WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher JAANA NIEUWBOER Administrative Assistant Copyright(C) 1988 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 Multiplan are registered trademarks of Microsoft Corporation. Bridge/286, Bridge/386, Flint, Positioning Decision Support System, Softbridge Commercial Banker, and Softbridge Financial Planner are trademarks of Softbridge Microsystems. ClickArt is a registered trademark of T/Maker Company. dBASE III is a registered trademark of Ashton-Tate Corporation. Inboard is a trademark of Intel Corporation. Intel is a registered trademark of Intel Corporation. Lotus and 1-2-3 are registered trademarks of Lotus Development Corporation. Manuscript is a trademark of Lotus Development Corporation. Macintosh is a registered trademark of Apple Computer, Inc. Illustrator is a trademark of Adobe Systems, Inc. PostScript is a registered trademark of Adobe Systems, Inc. ProKey is a trademark of RoseSoft, Inc. SideKick is a registered trademark of Borland International, Inc. UNIX is a registered trademark of American Telephone and Telegraph Company. ████████████████████████████████████████████████████████████████████████████ Bridge/386: A Tool for Integrating Applications Under Windows/386 Matt Trask☼ In the past year, a novel Windows application has been designed by Softbridge Microsystems. Originally a by-product of in-house development, Bridge/386(TM) has taken on a life of its own by allowing end users to customize the Windows environment. Until recently, Softbridge, based in Cambridge, Mass., has specialized primarily in vertical market applications products that include software for shipping management (the Positioning Decision Support System(TM)), financial planning (the Softbridge Financial Planner(TM)), insurance (the Flint(TM) expert system), and banking (the Softbridge Commercial Banker(TM)). In the process of developing these applications, Softbridge programmers often built their own proprietary system tools, many of which were developed for the Microsoft(R) Windows programming environment. Softbridge has been working with Windows since March 1984, making it one of the first independent software vendors to do so. In the last 4 years Softbridge has gained a great deal of experience with Windows, and it was this experience that led the company to focus its attention beyond its traditional vertical market products. Softbridge concluded that although Windows offered an excellent array of presentation objects, such as windows, scroll bars, and dialog boxes, it offered the user little in terms of accessing these objects. A programmer could certainly take advantage of these Windows facilities, but an end user could only use these objects as an application allowed. For example, there was no way for an end user to design a dialog box. Under DOS, a user has the system's batch language available, with which rudimentary menus can be created or repetitious tasks can be automated. Under Windows, the user loses even this simple capability. Softbridge wanted to give some of this functionality back to the user and began to work on developing a set of tools with which to control the Windows environmentnot at the programming level, but at the user level. Softbridge soon elaborated on this idea and began to develop a system that would permit users to integrate not only Windows applications but standard applications such as Lotus(R) 123(R) and dBASE III(R) as well. Eventually what emerged was a tool that would allow an end user to automate the operation of both Windows and non-Windows programs under the Windows environment, in a manner similar to that available under the DOS batch language. The result is a complete system that permits a company's information center, for example, to control, manage, and integrate applications for its users. Perhaps more importantly, the software enables the integration of different software products under the common user interface of Windows. It gives the PC manager, and the user as well, the ability to create customized software to control applications. When Microsoft introduced the most recent versions of Windows, Windows/286 and Windows/386, Softbridge was ready to announce Bridge/286(TM) and Bridge/386. Furthermore, in the fourth quarter of 1988, Softbridge will introduce Bridge/OS, a version of Bridge for OS/2 and the Presentation Manager. This will allow workstations being developed using Bridge and Windows/386 or Windows/286 to migrate easily to OS/2. Unlike Softbridge's vertical applications, the Bridge/286 and /386 products are systems software that let a user extend and build Windows applications. According to Fred Ciaramaglia, vice president of technology, "Softbridge is now interested in earning a reputation as a provider of systems software, not just applications." Thus, the Bridge product, which began as an internal applications development tool, takes on new life as a retail systems product. One particularly exciting aspect of Bridge is that it can operate across any NetBIOS-compatible LAN, which enables it to transparently integrate applications across a network, while the user has what seems like a local view. PC managers can then spare the user much of the inherent complexity of networks. The real power of Bridge/386, however, lies in its Windows interface, its ability to give some of the Windows tools directly to the user, and, of course, its ability to multitask applications on 80386-based machines. Bridge/386 Bridge/386 is best described as a "batch language" built specifically for Windows. It allows the user to automate repetitive Windows procedures and to use different application programs without exiting Windows. I used Bridge/386 on an Intel(R) InBoard(TM) 386 to build a Windows application that I call the integrated programmer's workstation (PWS), which I will examine later. Bridge/386 starts with capabilities very similar to those available with the DOS batch language. It adds the following to Windows: ■ user-defined menus ■ user-defined dialog boxes ■ enhanced conditional processing ■ subroutine calls ■ array variables ■ messaging between applications ■ network capabilities ■ "on" conditions (branching and special actions that occur due to some future condition) ■ parameters that can be passed to called files ■ support for named arrays (enhancing the DOS %1-%9 variable capability) ■ an ERROR variable serving as a more comprehensive version of the DOS ERRORLEVEL variable ■ the ability to place more than one command on a line (separated by semicolons) As with the DOS batch facility, with Bridge/386 you first create a plain ASCII text file that contains the Bridge/386 commands. Instead of the BAT extension, however, you create a file with a BR extension, which serves to identify the file as a Bridge/386 batch file. From Windows/386 you then run BRIDGE.EXE, which is the control program for Bridge/386. BRIDGE.EXE is an actual Windows program (see Figure 1☼) that looks like any other Windows program. It has its own window and serves as the control center for the application you are developing. The Bridge/386 window can be customized so that it appears at any place on the screen a user chooses. It can be any size or shape, and the Bridge window caption bar can say anything the user wants it to say (see Figure 2☼). This customizing facility allows the user to camouflage Bridge/386 while it operates and makes it appear that the user is executing his or her own Windows application. In fact, by including a standard WIN.INI line such as BR=BRIDGE.EXE ^.BR in the WIN.INI file, a user can call any application from the MSDOS Executive that has an extension of BR, and it will appear to be a Windows program. Only one Bridge/386 batch file can be executed at a time, but if you give commands for more, Bridge will queue their execution until previous batch files are completed. When Bridge is first started, you will see the standard system menu in the top left corner of the window (see Figure 3☼). This menu allows direct entry of Bridge commands or the execution of batch files. Two of the more interesting options that appear on this menu are Browser and Preferences. Browser allows you to examine and control the current Bridge process, and Preferences is used to select such options as colors, scroll bars, Bridge font, and whether executing batch programs will have trace output sent to the Bridge window. Browser will be explained in more detail when I discuss how it was used to test and debug PWS. Components of Bridge Bridge has two major parts: the Bridge engine and the various APIs that permit access to this engine. The kernel is the main part of this engine. It can open, activate, and close applications, as well as capture and play back keystrokes between applications. The kernel also manages menus and dialogs that can be used to create user interfaces for Bridge programs. On-key and on-message conditions can be registered with the kernel so that action can be taken when a hot key is pressed or a message is received from another program. The Bridge window is directly controlled by the kernel and, as mentioned earlier, can be recaptioned under program control and used to display menus for user interaction. The Bridge message manager is the second component of the engine. It handles the opening, monitoring, and closing of mailboxes. These mailboxes are used to send, receive, and read messages that are sent between application programs in a system or across a network to applications running on another system. Finally, the DOS supervisor is the part of the Bridge kernel that controls the execution of nonWindows applications. Its capabilities include reading from the DOS screen, capturing and playing back keystrokes, and managing data exchange via the Windows clipboard for nonWindows programs. This supervisor permits the use of dialog boxes while running ordinary DOS applications in fullscreen mode. With Windows/286 and Bridge/286 this control is limited to sending a single line of keyboard input and the ability to call the Bridge engine from DOS batch files using BR.COM. Figure 4 summarizes the various parts of Bridge and what they do, and Figure 5☼ shows the interactions between these parts. There are APIs under development for Microsoft Excel, dBASE III, and the C programming language. These APIs are not fully supported in the first release of the product. The beta versions that I saw provide functionality that is very similar to the Batch API, allowing Microsoft Excel macros to call Bridge functions such as MSG SEND and MENU LOAD. Programming Bridge Bridge Batch, like any other programming language, provides various control structures such as FOR, NEXT, IF, and GOSUB, as well as the infamous GOTO. Figure 6 shows a partial list of the Bridge commands. Variables do not have to be defined before they are used, and Bridge/386 data types include strings, integers, arrays, and floating point. Among the objects that can be manipulated by Bridge are menus, windows, dialog boxes, and mailboxes for interprocess communication. All the normal arithmetic functions are available, and comparisons can be done with data objects of the same type. Variables can be passed in function calls by using a syntax that is similar to the %1 naming convention of the DOS batch language. Windows can be positioned and sized by using screen_units, a measure that is based on a percentage of screen size. Individual applications can be minimized, restored, or maximized under control of a batch program. Many functions are provided for operating on the various data objects (see Figure 7☼ for a partial list). One example is FILE_PARSE( ), which is used to extract the drive, path, name, and extension parts of a filename. The array and string operators are reminiscent of PostScript(R) programming because the data types are similar. Much of Bridge's power lies in the commands for manipulating menus and dialog boxes. These are used to build sophisticated user interfaces and to handle error conditions that arise while batch programs are running. See Figure 8 for a partial list of dialog commands. Developing Bridge Several conditions attracted Softbridge to develop Bridge for the Windows environment. The main attraction, according to Ciaramaglia, was "the world-class applications such as Microsoft Excel that are being created for the Windows presentation manager." This, combined with a long-standing relationship between Softbridge and Microsoft that goes back to Multiplan(R) in the p-code days, was enough encouragement. Bridge was developed in C and assembly with DOS as the development environment. Ciaramaglia says that the most important part of the development effort was the relationship between Softbridge and Microsoft. "Lots of DIAL, access to early beta releases, and an occasional phone call to Phil Barrett [the author of Windows/386] were what it took to create Bridge/386," he says. One of the problems Softbridge encountered was the strange way that Windows handles time-slicing when multitasking. The foreground task gets about 60 percent of all available CPU cycles, and the remaining cycles are split between all background tasks. This made it difficult to provide predictable performance when Bridge was controlling other applications. A number of enhancements were added to Windows at Softbridge's request; in particular, a few of the "debug hooks" that were scheduled for removal after beta testing were left in because they were so useful to Bridge/386. Developing PWS Figure 9 is the complete source code to a simple integrated programmer's workstation. The ASCII file PWSHELP.TXT, also shown here, is displayed in NOTEPAD when Help is selected from the PWS menu. The time to develop this application was about one day, including the time it took to learn Bridge and test and debug PWS. I found the interactive windowed environment to be extremely useful when debugging PWS. The individual windows that were started by PWS could be sized or minimized so that the program execution trace could be watched in the main Bridge Window. As mentioned earlier, this trace option can be enabled by using Bridge's Browser to select program trace, message trace, or both. Browser can be used at any time to examine Bridge variables and mailboxes. Among the parts of PWS worth noting are the three lines that set up hot keys. These commands define Ctrl-E, Ctrl-D, and Ctrl-C as hot keys that will activate the editor, debugger, and communications program, respectively. The majority of the PWS program is contained in the approximately 30 lines following MENU DEFINE PWS. This menu definition creates the various pull-down menus and other menu choices that drive the programmer's workstation. After starting PWS, the programmer would usually select Set Current Drive and Set Current Directory to set defaults for the Edit, Make, and Debug commands. Edit and Debug show the use of dialog boxes to select a file from the current directory. PWS is a fairly simple example of the Bridge/386 batch facility. It can further be enhanced by using BRDOS (the DOS supervisor) instead of PIF files to run the Edit, Make, and Debug programs, offering greater control and interprocess communication possibilities. Also, the application code could be written to test for the existence of necessary files such as EDITOR.PIF and automatically invoke PIFEDIT if they are not defined. Such automatic installation can be provided in any Bridgebased application. Conclusion Bridge/286 and Bridge/386 are probably best suited for use by systems integrators who are designing complete applications that include Windows/286 or Windows/386. Large corporations could use Bridge/286 and Bridge/386 to configure individual PCs with simplified user interfaces that are more meaningful to workers. Software vendors could use Bridge for automating installation and online tutorials for their own products. PWS is intended as a quick and easy example of Bridge/386 programming, not as a complete demonstration of Bridge's capabilities. I can imagine a workstation that changes personality under Bridge/386 mediation as I find myself wearing the various hats of programmer, writer, and accountant. One fascinating possibility would be the design of an automated stock trading system that reads quotes from an on-line ticker machine, makes buy/sell decisions after massaging the data in a Microsoft Excel worksheet, and uses BRDOS to control a communications program that calls in the orders to a stock broker's computer. Although I haven't yet figured out how to make Reversi compete with another copy of itself, there does seem to be adequate power in Bridge to do most anything else I could want in Windows. With Bridge, Softbridge Microsystems has something to add to Windows in a way that is going to interest many personal computer users. Bridge/386 and Bridge/286 may very well change the way people use Microsoft Windows/386 and Windows/286 presentation managers. Figure 4: The Various Parts of Bridge BRIDGE.EXE The Bridge kernel and Bridge batch interface. BRDYN.EXE APIs for other Bridge programming interfaces. BRMSG.EXE The Bridge Message Manager. BRDOS.EXE The Bridge DOS Supervisor and DOS batch interface. BR.COM Used to issue commands to the Bridge Kernel while running DOS.Commands can also be given by programs using an Interrupt 16H interface. Figure 5: The interrelationships and possible connections for Bridge/386. ╔═══════════════╗ ╔════════════════╗ ║ DOS 3.x ║ ║ Windows 2.x ║ ║Virtual Machine║ ╔═══════════════════╗ ╔══════════ ║ ┌────────────┐ ║ ║ ┌───────────┐ ║ ║ Dos 3.x Machine ║ ║ Other ║ │ Windows │ ║ ║ │ DOS │ ║ ╟───────────────────╢ ║Window/386 ║ │ Application│ ║ ║ │Application│ ║ ║ DOS Application ║•••║or DOS 3.x ┌─►│ (Microsoft │ ║ ║ ├───────────┤ ║ ╟───────────────────╢ ║ Machines │║ │ Excel) │ ║ ║ │ Bridge/386│◄──┐ ║ Bridge/386 or ║ ╚══════════ │║ └────────────┘ ║ ║ │ DOS Module│ ║ │ ║ Bridge/286 DOS ║ │║ ┌────────────┐ ║ ╚═══════════════╝ │ ║ Module and ║ │║ │ Windows │◄─┐ ╔═══════════════╗ │ ║ Message Controller║ │║ │ Application│ ║│ ║ DOS 3.x ║ │ ╚═══════════════════╝ │║ └────────────┘ ║│ ║Virtual Machine║ │ │║ ┌────────────┐ ║│ ║ ┌───────────┐ ║ │ │ │║ │ Bridge/386 │ ║│ ║ │ DOS │ ║ │ │ └─►│ Master │◄─┘ ║ │Application│ ║ │ │ ║ │ Controller │ ║ ║ ├───────────┤ ║ │ │ ║ └────────────┘ ║ ║ │ Bridge/386│ ║ │ │ ║ ║ ║ │ DOS Module│ ║ │ │ ╚═══════│════════╝ ╚═══════════════╝ │ │ ▼ ↕ ▼ │ ░░░░░ Bridge/386 Message Controller ░░░░░░░ │ ↕ ▼ ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ LOCAL AREA NETWORK ▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒▒ Figure 6: Sample Bridge Batch Commands Programming Constructs call for gosub goto halt if return Batch-Style Programming set shift Debug Support trace warning File/DOS Commands rename chdir delete rmdir Mailbox Management msg create msg delete msg netclose msg netinit msg query msg read msg send Bridge Window Control rows columns background font vscroll hscroll Task Control foreground background fullscreen maximize minimize window Figure 7: Sample Bridge Functions abs app.exist array.len exact file.size find left upper Figure 8: Sample Bridge Dialog Box Commands DIALOG button_option [default_option] prompt [caption] where button_option is: OK OKCANCEL RETRYCANCEL YESNO YESNOCANCEL ABORTRETRYIGNORE and default_option is: DEFBUTTON1 button_number DIALOG FILE file_spec prompt [caption][RESULT variable_name] DIALOG INPUT prompt [NUMBER][INTEGER][caption][RESULT variable_name] DIALOG LOAD BEGIN [AT x y] [SIZE width height] control ... END where control is: CAPTION text CHECKBOX [AT x y] text [DEFAULT value][RESULT variable_name] EDIT [WIDTH characters][AT x y][DEFAULT text][NUMBER] [INTEGER][RESULT variable_name] ICON {HAND|EXCLAMATION|QUESTION|ASTERISK}[AT x y] PUSHBUTTON [AT x y][DEFAULT value][CANCEL] text [WIDTHcharacters]TEXT [AT x y][text][WIDTH characters] Figure 9: PWS.BR, An Integrated Programmer's Workstation PWS.BR Source Code Listing REM PWS: A Programmer's Workstation Using Win/386 and Bridge/386 REM By: Matt Trask On: 23 APR 88 REM This program assumes that you have defined your REM editor, make program, debugger, and comm program as REM EDITOR.PIF, MAKE.PIF, DEBUGGER.PIF, and COMMS.PIF. REM *** make the bridge window big enough for the menu bar *** select bridge; size 100 12; cls REM *** load main menu for programmer's workstation *** caption "Programmer's Workstation" menu load pws REM *** set up hot keys *** select windows; on key ^e gosub callprog editor editor.pif select windows; on key ^d gosub callprog debugger debugger.pif select windows; on key ^c gosub callprog comms comms.pif return REM *** main menu for programmer's workstation *** menu define pws begin popup En&vironment begin menuitem "Set Current D&rive" gosub set_drive menuitem "Set Current D&irectory" gosub set_cwd menuitem "Online Communication" gosub comm menuitem "DOS &Command Line" gosub doscmd end popup "&Programming Tools" begin menuitem " &Edit " gosub edit menuitem " &Make " gosub make menuitem " &Debug " gosub debug menuitem " &SCCS " gosub sccs end popup "&Windows Tools" begin menuitem &Calculator gosub callprog calc calc menuitem Calen&dar gosub callprog calendar calendar menuitem Cloc&k gosub callprog clock clock menuitem &Notepad gosub callprog notepad notepad end popup &Email begin menuitem "&Check Email" gosub pwsemail Check menuitem "&Send Email" gosub pwsemail Send end menuitem &Help gosub pwshelp menuitem "E&xit PWS" goto pwsexit end REM *** Set the current working directory *** :set_cwd dialog input "Directory name?" result cwd if %error% == 2 return ; REM cancel button if %cwd% == "" return ; REM none specified cd %cwd% return REM *** Set the current drive *** :set_drive dialog input "New drive name?" result drive if %error% == 2 return ; REM cancel button if %drive% == "" return ; REM none specified %drive%: return REM *** Run COMMAND.COM *** :doscmd exec /N:doscmd commandw.pif return REM *** Run A Communication Session *** :comm exec /N:comms comms.pif return REM *** Edit a file *** :edit dialog file *.asm "Edit which file?" result edfile if %error% == 2 return ; REM cancel button exec editor.pif %edfile% return REM *** Build a program *** :make exec make.pif return REM *** Debug a program *** :debug dialog file *.exe "Debug which program?" result dbgfile if %error% == 2 return ; REM cancel button exec debugger.pif %dbgfile% return REM *** Run Source Code Control System *** :sccs dialog ok "Not implemented yet" "SCCS" return REM *** Subroutine to invoke a Windows App *** :callprog REM if application already exists, just activate it and return if app.exist(%1) select %1; activate; return REM otherwise start it up exec /N:%1 %2 return REM *** Stub for Email System *** :pwsemail dialog ok "Email system not available in PWS demo" "%1 Email" return REM *** Display help text using Notepad *** :pwshelp menu load ; REM load Exit Help menu into Bridge Window begin menuitem "E&xit Help" menu load pws; warning off; select help; close end exec /N:help notepad.exe pwshelp; move 0 12 100 88 return REM *** Clean up apps and exit *** :pwsexit if app.exist(comms) then select comms; activate dialog ok "Exit halted, you must close this app first" Comms halt end if if app.exist(editor) then select editor; activate dialog ok "Exit halted, you must close this app first" Comms halt end if if app.exist(debugger) then select debugger; activate dialog ok "Exit halted, you must close this app first" Comms halt end if REM *** clear hot keys *** warning off; select windows; on key ^c warning off; select windows; on key ^d warning off; select windows; on key ^e REM *** close Windows apps *** warning off; select calc; close warning off; select calendar; close warning off; select clock; close warning off; select notepad; close REM *** exit from Windows *** warning off; select msdos; close PWS Help File (called from PWS.BR) Help Text for PWS, the Programmer's Workstation This is an example of a programmer's workstation including: ■ Setting the default drive and working directory ■ Running a communications program as a background task ■ Running a DOS shell and command line ■ Some desktop tools ■ A link to an electronic mail system ■ Help documentation specific to the workstation ■ "Hot keys" for quickly switching between applications COMMANDS: Set Current Drive - use this to log the drive containing your project's source tree. Set Current Directory - specify the current directory for your project. Online Communication - start a comm program that can be used for background file transfers while working. DOS Command Line - runs COMMAND.COM in a virtual machine for normal access to DOS capabilities. Programming Tools - edit, make, debug, and SCCS the parts of your current project. These commands assume that you have already set the current drive and directory. You must create PIF files for each of these choices that define your particular tools. Windows Tools - Provides access to four desktop tools-calculator, calendar, clock, and notepad. Email - Allows checking or sending of electronic mail messages. (The email system is not available for this demo.) Help - Displays this help information. Exit - Closes all open applications and exits from PWS. HOT KEYS: The following hot keys are available to quickly switch to a specific application. Ctrl-C (for Comms) - switch to the communication session Ctrl-D (for Debugger) - switch to the current debugging session Ctrl-E (for Editor) - switch to the current edit session ████████████████████████████████████████████████████████████████████████████ A Guide to Understanding Even the Most Complex C Declarations Greg Comeau☼ The C language has been around for a good number of years, but many parts of its syntax are still not clearly understood, even by professional programmers. The reason for this ambiguity is the lack of unified documentation covering all the options, especially concerning pointers, that are now available within the language, including the American National Standards Institute (ANSI) and Microsoft extensions to the language. Unfortunately, as long as these features are not clearly understood, or worse, misunderstood, programmers will not be utilizing C to its full capacity. Instead, code is being written, even by talented programmers, that is more complicated than it needs to be and may in some instances be incorrect. The purpose of this article is to clarify some of the typical constructs of C declarations that baffle novice and expert C programmers alike. As a starting point, let's look at a declaration that many C programmers find difficult to comprehend: struct vtag far * (far * const far var[5])(); If you know for certain what this declaration is saying, just skip to the end of this article and work on the declaration offered there as a challenge. If you are at that stage of the game where I was just a short time ago, that is, you think you know but aren't quite sure, then stay around. You will be able to read and make use of the preceding identifier by the end of this article, and you should even be able to explain reading and writing C declarations to others. Only then can you use the complete power of the language. Declaration Syntax To use a language, you must know something about its structure and syntax. The first thing to look at regarding C declarations is how they are organ- ized. Within a given declaration arrangement, different attributes can be specified. Depending on what attributes are used, the type of an identifier can be determined. The syntax for explicitly declaring identifiers in C is shown in Figure 1. A declaration can contain many of the specifiers shown in Figure 1, but it must contain at least a type and a declarator. Note that some compilers will allow only a declarator to be specified if the identifier is declared outside any function; C programmers should not use such coding practices, because it is bad programming style and may lead to routine errors and maintenance problems. Furthermore, the draft ANSI proposal has made this practice obsolete; therefore, this article will not discuss cases where more than one identifier is being declared in a declaration. Declarations: Theory Many of us can read declarations such as int i; char *p; and we can even decipher int *ia[3]; int (*ia)[3]; thanks to Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language (Prentice-Hall, Inc., 1978)-or K&R, as I will refer to it from here on. By now we've all simply memorized them. Our good fortune is that these probably account for 85 percent of the declarations you will encounter. However, understanding the other 15 percent can be an uphill battle, and that's where the trouble begins. Many programmers suffer through complex declarations and the usage of the identifiers declared by them. Guessing is usually the only recourse one has on short notice because of poor documentation. Sooner or later it will burn you since guessing means making generalizations that are not necessarily true. Even though your guess may be close, the low-level code generated by your compiler may be completely different from what you wanted. I wish that I had not been forced to play the guessing game when I first learned C. This is especially bothersome now since the actual theory behind reading declarations is very simple. You only need to understand that declarations are based on the C operator precedence chart, the same one you use to evaluate expressions. In the case of declarations this means bindings in the order of ( ) or [ ] highest, associatively from left to right, lowest with parentheses overriding normal bindings. That's all there is to it. Knowing this, all we have to do is formulate a set of rules such as the one suggested in Figure 2. Let's go through some sample declarations. Using page 200 of K&R as a reference point, study the declarations shown in Figure 3, which are parenthesized according to the C language precedence chart. As you can see, once a declaration has been parenthesized, deciphering it is merely a case of stating what each parenthesized expression is. This is similar to parenthesizing arithmetic expressions, for instance, where a certain addition may have precedence over a certain multiplication. The only difference is that multiplication and division are binary operators (they must work on two operands) whereas here we are dealing with unary tokens, which need only one operand. Note that every parenthesized declaration in Figure 3 is also a valid declaration syntactically; they are not shown merely for their equivalence. Declarations: Usage Although the rule stated in Figure 1 is simple, its disadvantage is that you've got to sit there and parenthesize the declaration. Instead of spending a lot of time parenthesizing "subdeclarations," it would be helpful to generalize the theory into something more useful. The set of rules in Figure 4, sections 1, 2, 3, and 5 permit you to read declarations "on the fly." At first glance these rules appear to be more complex than the rule in Figure 1, but they are merely expanded versions of that rule. After going through several sample declarations, you'll see how natural these rules are. They are really the same as the rule shown in Figure 1, but since we know about operator precedence, there's no sense in actually parenthesizing the declaration. Taking the examples from Figure 3, we will get the results shown in Figure 5. Creating several obvious derivations from K&R's declarations we get declarations such as the ones shown in Figure 6. And using derivations from examples in the Microsoft(R) C Compiler (MSC) Version 5.0 Language Reference we get the declaration in Figure 7. We know this declaration must be different from "char *(*(abc()))[10]", in which abc is a function returning a pointer to a pointer to an array of 10 pointers to char, which can also be written as "char *(**abc())[10]". Finally, Figure 8 shows an example of a union declaration. Writing Declarations Rules for writing declarations from English descriptions are every bit as simple since it's exactly what we've done so far, only in reverse. Figure 4, sections 1, 2, 4, and 5 show the rules for accomplishing this. The examples from Figures 5, 6, 7, and 8 are listed in Figure 9, but in reverse translation. ANSI Extensions The X3J11 committee of the ANSI has enhanced the K&R definition of C by adding keywords (see Figure 10) and nomenclature (mainly function prototypes, which we will not get into) to its C language standardization proposal. The keywords we will discuss are const and volatile. The const type qualifier specifies that the object associated with that type will not be modified, that is, it will not be assigned, incremented, or decremented. The volatile type qualifier specifies that the object associated with that type must be evaluated according to the sequence rules of C, which guarantee that C statements and objects used by these statements must follow a specific order of execution. Since the sequence rules generally rule out a large class of optimizations that can be applied to a given piece of code, the values of volatile objects can be modified by something other than the program "owning" the object that has been declared without fear of inconsistency. Points to note regarding keywords include: ■ a const cannot be assigned to ■ a nonvolatile const can be placed in ROM ■ a volatile can be changed by such mechanisms as DMA, asynchronous processes, or shared memory ■ a volatile cannot be "optimized out"─the real machine must follow the abstract C machine involving these identifiers ■ an object without any of these attributes, that is, an unqualified object, can be read from or written to without fear of distortions of any kind, except synchronously via another object, such as a pointer To understand these keywords better let's look at a practical example, an excellent one from the proposed standard that provides a declaration for a memory-mapped input port connected to a real-time clock. This is given as extern const volatile int real_time_clock; which declares an integer that cannot be modified by the program (const), but can be modified by some external event (volatile), the clock. Now that we have some idea of what these keywords accomplish, we must integrate their usage into our rules. An addendum to our previous rules is shown in Figure 11. The ramifications of this are that type qualifiers modify the type of an object, whether it is the base type or a pointer type. So, in the declaration const int i; i is declared as an integer that cannot and will not be modified, for example, through assignment. However, in such a situation, i may be initialized when it is being declared since it is an error to have a statement assigning i = some value after you've declared a given identifier as a const. It is essential to understand that const will modify what one can do with i, but it will not change anything about the basic type directly; this is true of all the qualifiers. An analogy would be that painting a big green house another color (changing one of its characteristics) would not change its size or the fact that it is a house. Besides qualifying an identifier or object, qualifiers can also qualify pointers. You must understand here how to code a qualified pointer, which is probably one of the most difficult aspects of reading declarations. For instance, would a constant pointer be coded as "int const * p" or "int * const p"? If "int * p" says that p is a pointer to an int, then "int const * p" says p is a pointer to a const int. So, since p is a pointer to a constant, then it must be a constant pointer, right? Maybe, since in the case of "int * const p," p itself is a const that is a pointer to an int, so it is also a constant pointer, right? It can only be one or the other. Let's explore this in more detail, as a similar problem will come up later on when we discuss the Microsoft extensions (near and far keywords) to C. The point is to understand the difference between a "constant pointer" and a "const pointer." Although it is a small difference, it resolves an ambiguity that crops up in using generic terms like constant pointer. The difference is that a constant pointer does not change; it is constant, a pointer whose value cannot change. Pointers that are not constant are variable. A const pointer (const *), on the other hand, is a pointer to a constant of some given type. Please note the use of the English word constant in one explanation and the keyword const in the other; they are not the same. Here are some examples to reinforce this: int i; i is a variable integer; it can be assigned various values. const int i; i is a constant integer; it cannot be assigned any values after it has been declared. int *p; p is a variable pointer to a variable integer; both p and the integer that it points to can be assigned values. int * const p; p is a constant pointer to a variable integer; p cannot change value, the integer it points to can. const int *p; p is a variable pointer to a constant integer; p can be modified, the integer it points to cannot. int const *p; p is a variable const pointer to an int. Since a const pointer is a pointer to a constant integer, p is a variable pointer to a constant integer, the same as const int *p. const int * const p; p is a constant pointer to a constant integer. Neither p nor the integer it points to can be modified. const *p; p is a variable pointer to a constant integer. This confirms const int * p and int const *p since there is no other way to read it. However, note that ANSI considers the lack of an explicit type declaration to be bad form as it encourages sloppiness in coding declarations. With these examples under your belt, you can now go back and look at some derivations of the examples used earlier on. For simplicity, I will only use the const attribute in the examples below. Microsoft C only acknowledges the volatile keyword syntactically, not semantically. Also, we will leave out the so-called variable attribute that I introduced since that is understood. const int i; i is a constant int. const int *i; i is a pointer to a constant int. int * const i; i is a constant pointer to an int. const int * const i; i is a constant pointer to a constant int. const int *i[3]; i is an array of three pointers to constant int. int * const i[3]; i is an array of three constant pointers to int. const int * const i[3]; i is an array of three constant pointers to constant int. const int (*i)[3]; i is a pointer to an array of three constant ints. int (* const i)[3]; i is a constant pointer to an array of three ints. int (const *i)[3]; This is an error since const cannot immediately follow a left parenthesis. const int (const * const i)[3]; This is also an error since const cannot immediately follow a left parenthesis. const int (* const i)[3] i is a constant pointer to an array of three constant ints. const int *i(); i is a function returning a pointer to a constant int. int * const i(); i is a constant function returning a pointer to an int. Many compilers will choose to ignore the qualification of function types. To ease parsing, ANSI does allow it without requiring that a compiler issue a diagnostic; however, ANSI has classified this as "undefined behavior." At first glance, qualification of function types appears to be meaningless but it might be used to specify that the function be placed in ROM. const int (*i)(); i is a pointer to a function returning a constant int. int (* const i)(); i is a constant pointer to a function returning an int. int (const *i)(): This is an error since const cannot immediately follow a left parenthesis. const int **i; i is a pointer to a pointer to a constant int. int ** const i; i is a constant pointer to a pointer to an int. int * const *i; i is a const pointer to a pointer to an int; i is a pointer to a constant pointer to an int. int const **i; i is a pointer to a pointer to a constant int. int const * const *i; i is a pointer to a constant pointer to a constant int. const int const * const *i; This is a syntax error, since the first two const keywords are being applied to the type and not to the first asterisk. const int * const * const i; i is a constant pointer to a constant pointer to a constant int. You may have noticed in the descriptions above, in particular those of "const int *i[3]", "int * const i[3]", and "const int * const i[3]", that the terminology used is not "constant array," but is "array of constant ..." instead. A brief explanation is in order. If, while parsing a declaration, the last type is an array type and the current type is a qualified type, we say that the elements of the array should inherit the qualified type and not the array type itself. This makes sense when you consider some of the idiosyncrasies of array types and arrays, specifically that an array represents an address and does not have many other attributes beyond that since the address is merely some memory location. Besides, it's actually the elements of the array that we want to give the qualification to because we do not want anything to be assigned to them. In addition, you should keep in mind that an array invocation is also a constant. MSC Extensions Along with ANSI, Microsoft has also considered it necessary to add new keywords to the C language (see Figure 12). Four of them are applicable to attributes given to functions. The first three, cdecl, pascal, and fortran, deal with calling functions written in a language other than C (BASIC, FORTRAN, MASM, or Pascal) or letting C functions be called from other languages. These special keywords are really compiler directives that tell the compiler to generate code so that parameters to a function are handled as if they were in the language used in the function being called. So, in order to declare a function from C that might be written in Pascal, you would use: extern int pascal func(long, int); A pointer to such a function might look like this: int (pascal *fp)(long, int); The interrupt keyword is also a compiler directive and thus informs the compiler to generate special code. In this case the code would allow the function that is given the interrupt attribute to function according to a certain sequence of events that interrupt handlers should follow. The other three special keywords, near, far, and huge, deal with addressing an object on a machine that supports a segmented architecture such as that of the Intel microprocessor family, the 80286/88 and 8086/88 series). These keywords, especially the far attribute, will be used often in the mixed-memory model programming typical of this architecture. Microsoft C treats all the special keywords as if they were declarator (not declaration) qualifiers, that is, they can only modify syntactic entities on the right-hand side of a declaration and cannot modify base types, such as int, directly. The Microsoft C manual states that the special keywords modify the "item" immediately to their right in a manner similar to the const keyword. The manual's description is incomplete on this, though. Recalling the discussion on the ANSI additions, this means that the special keywords either modify an identifier or pointer, that is, they modify objects or pointers to objects. There is a difference between the ways these are handled, however, which is not clear from the manual. In Microsoft C, the syntax is organized so that pointers can be modified by one of the special keywords. Therefore a sequence such as int (far *p); is valid, but since ANSI prohibits such syntax int (const * p); is not. What this boils down to is that Microsoft C will accept a declarator syntax of MODIFIER * more_declarator_info while ANSI only accepts * QUALIFIER more_declarator_info where MODIFIER and QUALIFIER are optional positional parameters. Keep in mind, though, that both far and near were made part of the Microsoft C language long before const and volatile came along. The reason for this unfortunate difference is that ANSI borrowed the syntax of type qualifiers from C++ (a superset of C supporting some object-oriented programming features), which was being developed independently around the same time that Microsoft introduced its modifiers. The discussion concerning the difference between const pointer and constant pointer holds true here as well. In the case of these attributes, say far, the situation is even more ambiguous since far is already an English word. In other words, is int * far p a far pointer (since p is a pointer located in far memory), or is int far * p a far pointer (since it is a pointer that points to a far address)? We resolve it in exactly the same manner as we do the const case. We remain consistent and declare that "far *" (a keyword modifying a pointer) means far pointer. As for the other case, we say that a pointer located in far memory is called a distant pointer; this is similar to the variable versus constant idea used to clarify the const keyword. One final point to remember is that the Microsoft C memory model keywords cannot be applied to automatic variables. This makes sense since they are stack-based variables and there is no mechanism for you to control their nearness or farness. This does not imply that you cannot have local static variables that are near or far, nor does it mean that you cannot have automatics that are pointers to data in segments other than the segment that the function containing the automatic variable is located in. All it says is that the automatic itself cannot have the attribute. Finally, here are several more examples to emphasize these points: int * far p; p is a distant pointer to an int. int far * p; p is a far pointer to int. Since a far pointer is a pointer to far, p is a pointer to a far int. int far * far p; p is a distant pointer to a far int. Down the Road As of this writing I do not have Version 5.1 of the Microsoft C Compiler. It will be interesting to see if it has any new surprises. In the meantime, you should certainly be familiar enough with C declarations and the rules of the road by now to be able to go back and solve the declaration put forth at the beginning. Try to figure it out before reading on. Just to be sure we are all in agreement, struct vtag far * (far * const far var[5])(); says: var is a constant distant array of five pointers to far functions returning pointers to far struct vtags. It's not really all that difficult, is it? Now, let's see if you really understand it. What does the following declare? unsigned long(far * (far * const(far * far const V[2])[4])())[6]; As a final thought, consider the possibility of writing a program that takes as its input either a C declaration or an English declaration and produces either the appropriate English translation or the appropriate C declaration. Remember to take into consideration the case where there may be more than one declarator, such as int far * p, far q;──should this occur, immediately stop all processing and issue a stern warning declaring this to be poor programming style. Have fun. Figure 1: Standard Syntax for C Declarations The syntax of a C declaration is: storage-class type qualifier declarator = initializer; where storage-class is only one of the following: typedef extern static auto register Type could be one or more of the following: void char short, int, long float, double signed, unsigned struct ... union ... typedef type A declarator contains an identifier and one or more, or none at all, of the following in a variety of combinations: * () possibly grouped within parentheses to create different bindings. Figure 2: Rules for Reading and Writing K&R Declarations 1. Parenthesize declarations as if they were expressions. 2. Locate the innermost parentheses. 3. Say "identifier is" where identifier is the name of the variable. Say "an array of X" if you see [X]. Say "pointer to" if you see *. 4. Move to the next set of parentheses. 5. If more, go back to 3. 6. Else, say "type" for the remaining type left (such as short int). Figure 3: Interpreting Declarations by Parenthesizing int i; int (i); │ │ │ ▼ │ i is ▼ an int ─────────────────────────────────────────── int *i; int (*(i)); │ │ │ │ │ ▼ │ │ i is │ ▼ │ a pointer to ▼ an int ─────────────────────────────────────────── int *i[3]; int (*((i)[3])); │ │ │ │ │ ▼ │ │ i is an array of 3 │ ▼ │ pointers to ▼ int ─────────────────────────────────────────── int (*i)[3]; int ((*(i))[3]); │ │ │ │ │ ▼ │ │ i is │ ▼ │ a pointer to ▼ an array of 3 ints ─────────────────────────────────────────── int *i(); int (*((i)())); │ │ │ │ │ ▼ │ │ i is a function returning │ ▼ │ a pointer to ▼ an int ─────────────────────────────────────────── int (*i)(); int ((*(i))()); │ │ │ │ │ ▼ │ │ i is │ ▼ │ a pointer to a function returning ▼ an int Figure 4: Reading and Writing K&R Declarations 1. Given: Intermediate attributes are [], (), and *, implying array, function, and pointer, respectively. 2. Memorize right-to-left rule: Look to the right (within parentheses), pick up intermediate attributes if any, then look to the left and pick up intermediate attributes, if any. 3. To convert a C declaration to English: a. Locate the identifier in the declaration. Say "identifier is" where identifier is the name of the variable. b. Look to the right of the identifier for the intermediate attributes () or []. Note there may be none. Say "an array of" if you see []. Say "an array of x" for each [x] you see. Say "an x-by-y array of" if you see [x][y]. Say "an x-by-y by ... array of" if you see [x][y][...]. Say "functions returning" for () if the last right attribute found was []. Say "a function returning" for (). c. Now look to the left of the identifier (per the right-to-left rule), and look for any further intermediate attributes. We're only concerned about asterisks here, and any other attribute would be an error. Note there may be none. Also, be aware of parentheses. So, for each *, say "pointers to" if the last attribute found on the right was [] and the current attribute is *, otherwise, say "a pointer to" if you see *. d. Look to the right again for any more intermediate attributes. There may be none. Be careful of parentheses here. If any, go back to b. e. You should be left with terminating attributes: char, int, short, long, float, double, struct, union, and/or their respective modifiers-signed, unsigned, static, register, and extern. Say "struct of type y" for struct y. Say "union of type y" for union y. Say "attribute" read left-to-right (verbatim) for the terminating attributes. 4. Rules for converting English to a C declaration: a. Write "identifier", where identifier will be the name of the variable. b. We will need to keep track of whether the last attribute in our processing was an asterisk by using a flag. We will call our flag "active-*" and set it equal to 0. c. Write "*" to the left of what you've written down as long as you see "pointer to" or "pointers to" in the English description. Also set active-*=1 (signal that the last attribute was an asterisk). d. Write "(previous_written_down_attributes )" if active-*=1. Write "[x]" to the right if you see "array of x." Write "[x][y]..." to the right if you see "an x-by-y array of." Write "[]" to the right if you see "array of." Write "()" to the right if you see "function returning" or "functions returning." e. Go to b if there are still more intermediate attributes in the list. f. Write "simple attribute" to the left of everything, where simple attribute is one of the combinations of terminating attributes (keywords) shown in Figure 1. Write ";" to right of everything. 5. Notes: a. You cannot have an array of functions. You can have an array of pointers to functions, though. The declaration "int a[5]()" is invalid. b. A function cannot return an array. A function can return a pointer to an array, though. The declaration "int a()[]" is invalid. c. A function cannot return another function, only a pointer to one, meaning that "int a()()" is invalid. Figure 5: Interpreting Declarations Using Rules int i; │ │ │ ▼ │ i is ▼ an int ─────────────────────────────────────────────────────────────────────────── int *i; │ ││ │ │▼ │ │i is │ ▼ │ a pointer to (nothing to the right, so look to the left) ▼ an int (terminating attribute is all that's left) ─────────────────────────────────────────────────────────────────────────── int *i[3]; │ ││ │ │▼ │ │i is an array of 3 (first look to the right) │ ▼ │ pointers to (no more attributes to right, so look to left) ▼ an int (terminating attribute is all that's left) ─────────────────────────────────────────────────────────────────────────── int (*i)[3]; │ ││ │ │▼ │ │i is │ ▼ │ a pointer to (found parentheses-nothing to right, * to left) │ │ an array of 3 (finished paren attributes, look to right) ▼ ints (terminating attribute is all that's left) ─────────────────────────────────────────────────────────────────────────── int *i(); │ ││ │ │▼ │ │i is a function │ │returning (found () to the right) │ ▼ │ a pointer to (nothing more to right, * on the left) ▼ an int (terminating attribute is all that's left) ─────────────────────────────────────────────────────────────────────────── int (*i)(); │ ││ │ │▼ │ │is is │ ▼ │ a pointer to (* is within parentheses) │ a function │ returning ( () is attribute to the │ right of the parenthesis) ▼ an int (terminating attribute is all that's left) Figure 6: Interpreting Declarations Using Rules int **i; │ │││ │ ││▼ │ ││i is │ │▼ │ │a pointer to (no attributes to right, * to left) │ ▼ │ a pointer to (another * to the left) ▼ an int ─────────────────────────────────────────────────────────────────────────── int *(*i)(); │ │ ││ │ │ │▼ │ │ │i is │ │ ▼ │ │ a pointer to a (nothing to right, * to left) │ │ function returning (finished with parentheses, () is to right) │ ▼ │ a pointer to (no more attributes on right, try left) ▼ an int ─────────────────────────────────────────────────────────────────────────── int *(*i[])(); │ │ ││ │ │ │▼ │ │ │i is an array of (stay with parens, look to right first) │ │ ▼ │ │ pointers to (now to left) │ │ functions returning (finished with parens so look to their right) │ ▼ │ pointers to ▼ an int Figure 7: Interpreting Declarations Using Rules char *(*(*i)())[10]; │ │ │ ││ │ │ │ │▼ │ │ │ │i is │ │ │ ▼ │ │ │ a pointer to a function returning │ │ ▼ │ │ a pointer to an array of 10 │ ▼ │ pointers to ▼ a char Figure 8: Interpreting Declarations Using Rules union sign *(*i[5])[5]; │ │ ││ │ │ │▼ │ │ │is is an array of 5 │ │ ▼ │ │ pointers to arrays of 5 │ ▼ ▼ pointers to union sign Figure 9: Deriving Declarations from English i is -► an int │ │ ▼ │ i │ ▼ int i; ─────────────────────────────────────────────────────────────────────────── i is -► a pointer -► to an int │ │ │ ▼ │ │ i │ │ ▼ │ *i │ ▼ int *i; ─────────────────────────────────────────────────────────────────────────── i is -► an array -► of 3 pointers -► to int │ │ │ │ ▼ │ │ │ i ▼ │ │ i[] ▼ │ *i[3] │ ▼ int *i[3]; ─────────────────────────────────────────────────────────────────────────── i is -► a pointer -► to an array -► of 3 ints │ │ │ │ ▼ │ │ │ i ▼ │ │ *i ▼ │ (*i)[] │ ▼ int (*i)[3]; ─────────────────────────────────────────────────────────────────────────── i is a function returning -► a pointer -► to an int │ │ │ ▼ │ │ i() ▼ │ *i() │ │ ▼ int *i(); ─────────────────────────────────────────────────────────────────────────── i is a pointer -► to a function returning -► an int │ │ │ ▼ │ │ *i │ │ ▼ │ (*i)() │ ▼ int (*i)(); ─────────────────────────────────────────────────────────────────────────── i is a pointer -► to a pointer -► to an int │ │ │ ▼ │ │ *i │ │ ▼ │ **i │ ▼ int **i; ─────────────────────────────────────────────────────────────────────────── i is a pointer -► to a function returning -► a pointer -► to an int │ │ │ │ ▼ │ │ │ *i ▼ │ │ (*i)() ▼ │ *(*i)() ▼ int *(*i)(); ─────────────────────────────────────────────────────────────────────────── i is array-► of pointers-► to functions returning-► pointers-► to int │ │ │ │ │ ▼ │ │ │ │ i[] ▼ │ │ │ *i[] ▼ │ │ (*i[])() ▼ │ *(*i[])() ▼ int*(*i[])(); ─────────────────────────────────────────────────────────────────────────── i is a pointer-► to a -► returning-► to an array -► to char │ function a pointer of 10 pointers │ │ │ │ │ │ ▼ │ │ │ │ *i ▼ │ │ │ (*i)() ▼ │ │ *(*i)() ▼ │ (*(*i)())[] │ (*(*i)())[10] │ *(*(*i)())[10] ▼ char*(*(*i)())[10]; ─────────────────────────────────────────────────────────────────────────── i is an array-► of 5 pointers-► to arrays of 5-► union signs │ │ pointers to │ ▼ │ │ │ i[] ▼ │ │ i[5] ▼ │ *i[5] (*i[5])[] │ (*i[5])[5] │ *(*i[5])[5] ▼ union sign *(*i[5])[5]; Figure 10: ANSI Extensions The syntax of a C declaration as mandated by the Draft ANSI proposal: storage-class type qualifier declarator = initializer; Additional new qualifiers can be one or more of the following: const volatile Additional new types can be one of the following: void signed char unsigned char unsigned int unsigned long long double enum ... Figure 11: Reading and Writing ANSI Declarations Reading and writing ANSI declarations is similar to reading and writing K&R declarations. However, there are a few additional concerns due to the addition of type qualifiers. They are as follows: 1. Our main concern with proposing an extension to the rules given in Figure 4 is that the ANSI additions are keywords and don't necessarily have a precedence. Therefore keywords don't follow a consistent pattern per se. 2. Disregarding other type specifications, each qualifier (const, volatile) has a corresponding pointer type (const *, volatile *). 3. A missing type specifier is taken to be int. For example, "const x" means "int const x". However, this should be considered obsolescent style. 4. Type qualifiers and type specifiers may be intermixed without concern for their order. It will not change the resulting type in question. For instance, "const int var" and "int const var" are the same. 5. Intermixing type qualifiers within declarators (the part of a declaration specifying the identifier and function, array, and pointer attributes) does change the meaning of a declaration. Therefore the binding of qualifiers changes depending upon context. 6. To further clarify (d) and (e) and to make clear a case such as "const int * p", we need to propose the following: type qualifiers modify the type of a declarator. Also, the case of a declaration involving pointers such as the case of "type * type-qualifier(s) declarator" (say, "int * const var") is said as "declarator qualifier(s) pointer to type" (var is a constant pointer to int). 7. Nonqualifier declarations (those using default qualifications) can be read as such. So, nonconst is "variable" and nonvolatile is "nonvolatile." It is usually good practice to include the default qualifications of an identifier when translating declarations, even though the default qualification is not a keyword. Figure 12: Microsoft Extensions Microsoft has extended the K&R and ANSI definitions with extensions of two keywords in the area of qualifiers, as follows: far near huge cdecl pascal fortran interrupt ████████████████████████████████████████████████████████████████████████████ Techniques for Debugging Multithread OS/2 Programs with CodeView 2.2 Charles Petzold☼ Debugging is never a pleasant job, even when the program being debugged is a relatively simple one. But imagine what it's like debugging an OS/2 program with multiple threads of execution. All the threads run concurrently within the same program. Sometimes a thread works independently of the other threads, but the threads often interact with each other. At first, the very idea of attempting to debug a multithread OS/2 program seems like a nightmare, and at times it certainly is. However, Microsoft(R) CodeView(R)Version 2.2, included with the OS/2 versions of Microsoft's language products (BASIC 6.0, C 5.1, FORTRAN 4.1, Macro Assembler 5.1, and Pascal 4.0), has some new and enhanced commands to aid the debugging of multithread programs. Though I encountered some problems, this release of CodeView, Version 2.2, is a powerful tool for the OS/2 programmer. We'll examine this new support of multithread debugging by using a program called THREADS and going through some debugging exercises. Multithread Demo The THREADS program shown in Figure 1 creates four additional threads of execution. Each of these four threads writes its thread ID number in random locations on a quadrant of the video display. You can exit THREADS by pressing the Escape key. THREADS makes use of the new multithread (MT) header files and run-time library (LLIBCMT.LIB) included with Microsoft C 5.1. All the C library functions in this library are re-entrant and can safely be called from multiple threads in a C program. THREADS creates the four threads that are in its main function, each of which uses the same thread function, called ThreadFunction. Rather than call DosCreateThread to start each of these threads, THREADS calls the function _beginthread, a new function in C 5.1 that must be used in C programs that make use of the multithread library. Besides providing support for the multithread library functions, _beginthread has another advantage for the programmer──it allows a far pointer to be passed to the thread function. In the case of THREADS, this far pointer addresses the THREADPARAM structure and is used to pass to each thread the row and column coordinates of its screen quadrant. Each thread running in ThreadFunction obtains its thread ID from the local information segment. The thread ID is stored in an automatic (stack) variable. Because each thread has its own stack, all stack variables are private to each thread. (Any static variables in a function are shared among all threads that call the function.) The rand function is used to write the thread ID in a random location within a quadrant of the display. The rand function in the LLIBCMT.LIB library generates an independent pseudo-random sequence for each thread that calls it, which means that the function would normally return the same pseudo-random sequence for each thread. In the THREADS program, this is not quite what we want. To obtain different random sequences for each thread, ThreadFunction sets a different seed (by calling srand), which is set equal to the thread ID number. Note that the two functions CheckKey and WriteChar are called from only one place in the program and need not be separate functions. This was done so that breakpoints could be set at these functions. The THREADS make file shows the -Zi switch used in the compile step and the /CO switch used in the link step. These two CodeView switches add debugging information to the THREADS.EXE file. Freezing and Unfreezing When debugging a multithread program, it may be desirable to run only one thread at a time or to stop the execution of one thread while you let all the others run. For this reason CodeView introduces the concepts of "freezing" and "unfreezing" threads. A frozen thread does not run. You freeze and unfreeze threads explicitly with commands entered in the dialog window at the bottom of the CodeView screen. Other commands implicitly freeze and unfreeze threads temporarily while the command is in progress. We'll examine these other commands shortly. To load THREADS.EXE into the OS/2 version of CodeView, you use the command: CVP THREADS You can use the /2 switch to run CodeView on a second monitor if you have one. The CodeView dialog window displays a prompt: 001> This new CodeView prompt indicates the ID number of the "current thread." When an OS/2 program begins execution, it only has one thread, which always has an ID number of 1. (Thread 1 has some special characteristics; for example, it is the thread that handles signals if a signal handler has been installed by calling DosSetSigHandler.) As a program starts up additional threads, the thread IDs of the new threads are 2, 3, 4, and so forth. The _beginthread function and the new multithread library permit a program to have 32 threads going at once. Now try the new CodeView Status command: 001>~ This command lists all threads in the process and their current status. In this case, the command lists only thread 001 and the status Runnable. Set a breakpoint at the CheckKey function and enter the Go (G) command: 001>BP CheckKey 001>G The program creates the four threads, and they run normally, displaying their thread IDs in random locations on the screen. Suspend the program by pressing the spacebar. Because you've set a breakpoint on the CheckKey function, pressing any key will cause CodeView to break at the beginning of the function. Enter the Status command again: 001>~ You'll see the display shown in Figure 2☼. Threads 001 through 005 are all listed as Runnable. If you use a second monitor for CodeView, you'll notice that the program being debugged is entirely suspended when CodeView has control. (If you're using one monitor, you can switch to the session in which THREADS is running to ensure that it is indeed suspended.) A program being debugged does not run when CodeView has control. Now try freezing thread 2. The command is: 001>~2F All the new CodeView thread commands begin with the tilde (~). The 2 refers to thread 2; F stands for Freeze. You can see that thread 2 is frozen by entering the Status command again: 001>~ Thread 002 is now listed as "Frozen." When you enter the Go command again: 001>G you'll see that thread 2 has stopped. Now press the spacebar again to break at the CheckKey function. You can freeze all threads by using an asterisk rather than a number: 001>~*F The Status command: 001>~ now lists all threads as Frozen. Let's Go again: 001>G You may wonder what the Go command is doing in this case if all threads are frozen. Actually, Go temporarily unfreezes the current thread, which is indicated by the prompt as thread 001, before letting the program run. You can thus break again by pressing the spacebar. Now unfreeze thread 5 and Go: 001>~5U 001>G The Unfreeze command is U; it uses the same syntax as the Freeze command. Only the first and fifth threads run now. Press the spacebar, unfreeze all threads, and Go: 001>~*U 001>G All threads are now running normally. Press the Escape key to end the THREADS program. CodeView will break at the CheckKey function. You can exit normally by another Go command: 001>G and then a Q command to quit CodeView: 001>Q Command Syntax All the new multithread commands in CodeView begin with a tilde (~). The Status command is either a tilde by itself or followed by a number, period, pound sign, or asterisk, as shown in Figure 3. Thus, the ~ and ~* commands are the same-they list all active threads. You can get the status for only one thread by following the tilde by a thread number. The period gives the status of the current thread, which is the thread indicated by the CodeView prompt. The ~# command gives the status of the "last executed" thread. This is usually the same as the current thread but may not be if you change the current thread without executing any code. The Freeze and Unfreeze commands are shown in Figure 4. They permit selective freezing or unfreezing of a specific thread, the current thread, the last executed thread, and all threads. Breakpoints In the exercise with freezing and unfreezing, we used the normal BP command to set a breakpoint at the CheckKey function. The CheckKey function is called only from thread 1, so there was no ambiguity about how CodeView interprets this command. But what if you want to set a breakpoint at a code line shared by several threads (such as a line in ThreadFunction)? Does the breakpoint apply to one thread or all threads? The answer is whatever you want. Load THREADS into CodeView again: CVP THREADS and set a breakpoint at the CheckKey function: 001>BP CheckKey Also set a breakpoint at ThreadFunction: 001>BP ThreadFunction The BP command by itself applies to all threads. You can easily confirm this by entering the Go command: 001>G CodeView will return control to you when one thread (probably but not necessarily thread 2) starts up and reaches the beginning of ThreadFunction. (Although CodeView breaks at the beginning of ThreadFunction, this does not mean that thread 2 has not executed any code before it breaks. Before reaching ThreadFunction, the thread has already executed some code associated with the function _beginthread.) When CodeView returns control to you, you'll see a prompt indicating that thread 2 (or whatever) is now current: 002> The display in CodeView's code window shows the cursor placed at the beginning of ThreadFunction, as demonstrated in Figure 5☼ You can continue by entering another Go command: 002>G Now thread 3 will break at the ThreadFunction. Continue execution with another Go: 003>G Thread 4 breaks. Enter another Go command: 004>G Thread 5 breaks. Another Go: 005>G Now all five threads of the program are running normally. Press the spacebar to break thread 1 at the CheckKey function. Now set a breakpoint at the WriteChar function. Although the WriteChar function is called from threads 2 through 5, this breakpoint will apply only to thread 5: 001>~5BP WriteChar As with all thread-specific commands, the command begins with a tilde. It is followed by the thread ID number and BP, the normal breakpoint command. Go again: 001>G Now thread 5 will break at WriteChar. The code window shows the cursor at the beginning of WriteChar and the prompt indicates thread 5: 005> You can get a list of all breakpoints with the normal BL command: 005>BL CodeView displays the information shown in Figure 6☼. For breakpoints that apply to a specific thread (such as breakpoint 2 in this example), the listing shows a tilde and thread ID. A breakpoint that applies to all threads, such as breakpoints 0 and 1, does not have this information. Clear breakpoint 2 and set a new breakpoint at WriteChar that breaks when thread 5 executes WriteChar 100 times: 005>BC 2 005>~5BP WriteChar 100 005>G The program will run for awhile (more slowly than normal) and will then break at the WriteChar function. Clear the breakpoint and set one that breaks when WriteChar is executed 100 times regardless of the thread that executes it: 005>BC 2 005>BP WriteChar 100 005>G This time CodeView returns faster because the count applies to all threads rather than just thread 5. When the breakpoint occurs, the current thread might be 2, 3, 4, or 5. (I've used a question mark below to indicate this.) You can clear all the breakpoints by using the BC command and Go: 00?>BC* 00?>G Press the Escape key to end the program and quit CodeView: 001>Q The Breakpoint Command Figure 7 shows the variations of the CodeView breakpoint commands. The BP and ~*BP commands are equivalent and apply to all threads. For example, when you set a breakpoint at the CheckKey function you can use either: 001>BP CheckKey or: 001>~*BP CheckKey Because only thread 1 calls the CheckKey function, you can also use: 001>~1BP CheckKey If thread 1 is the current thread (as indicated by the CodeView prompt), you can also use: 001>~.BP CheckKey You can set a breakpoint for a thread that does not yet exist. For example, when you first load THREADS into memory, you can use: 001>~5BP ThreadFunction CodeView will only break when thread 5 begins execution of ThreadFunction. You can use line numbers or absolute addresses with any variation of the breakpoint command. You can also use the pass count and command options of BP. When you set a breakpoint using the mouse (by clicking on a line of code) or the F9 key, that breakpoint applies to all threads. Only by using the BP command in the dialog window can you set a breakpoint for a particular thread. The Go Command We've used the Go command in the exercises involving freezing and unfreezing and breakpoints, and it's been working as we might expect. The Go command simply allows the program to run; control returns to CodeView when a breakpoint is encountered. The only oddity encountered was in the freezing and unfreezing exercise when we froze all the threads and discovered that the Go command allowed thread 1 to run. Figure 8 shows the variations of the CodeView Go command. As indicated by the last column in Figure 8, the Go command always temporarily unfreezes a particular thread if the thread is frozen. When control returns to CodeView, it restores the status of the thread to its former state. When using the thread-specific versions of the Go command (~nG, ~.G, or ~#G), CodeView temporarily freezes all threads except the thread you've specified. This lets you use the Go command to control execution of a single thread while other threads are prevented from running. Let's run through a short exercise to see how this works. Load THREADS into CodeView: CVP THREADS Set a breakpoint at the function WriteChar that applies only to thread 5 and Go: 001>~5BP WriteChar 001>G CodeView will break when thread 5 reaches the WriteChar function. The prompt indicates thread 5 as the current thread: 005> You can enter a few successive Go commands and observe that all threads run between the breaks: 005>G Now try a Go command that applies only to thread 5: 005>~5G In this case, only thread 5 runs. CodeView temporarily freezes all other threads before letting the program run. Clear the breakpoint and Go: 005>BC* 005>G End the program by using the Escape key and exit CodeView: 005>Q The Go command executed from the menu or the F5 key works the same as G entered in the dialog window. However, when a breakpoint occurs, it will not be immediately apparent which thread caused the break. For this reason, it's often preferable to use the G command in the dialog window when debugging multithread programs. Breakpoints and Singlestepping Before proceeding, it will be helpful to review the debugging facilities supported by the 80286 microprocessor. A debugger can set a breakpoint at an assembly language instruction by replacing the first byte of the instruction with CCH, which is an Interrupt 3 instruction. When the program executes that instruction, the resulting Interrupt 3 passes control to the debugger. The debugger can then restore the original byte before allowing the program to continue. A debugger can single-step through a program's code by setting the Trap flag. When the Trap flag is set, an Interrupt 2 will be generated after every instruction. In OS/2, a debugger does not set the Trap flag directly. This is handled by the DosPTrace function, the OS/2 debugging function. Similarly, when an Interrupt 2 or 3 is generated, control passes to OS/2, which then returns from an earlier DosPTrace call made by the debugger. DosPTrace is documented in the Programmer's Reference manual included in the Microsoft OS/2 Programmer's Toolkit. When you set a breakpoint with the BP command, CodeView sets the breakpoint interrupt in your code, which is obvious. When you trace code using the T (Trace), P (Program Step), and E (Execute) commands, you may think that CodeView uses the single-step facility, but that's not necessarily true. When you trace through source code statements, CodeView uses a breakpoint at the end of the group of assembly language instructions that constitute a particular statement. When using the P command to trace through assembly language instructions, CodeView uses a breakpoint to skip over function calls. For this reason, the Trace, Program Step, and Execute commands work somewhat differently in source mode and assembly language (or mixed) mode when debugging multithread programs. Tracing Let's load THREADS into CodeView for an experiment with tracing: CVP THREADS Set a breakpoint at ThreadFunction that applies only to thread 5 and Go: 001>~5BP ThreadFunction 001>G You should still be in source mode at this time. To trace through source code statements you can enter this command several times: 005>~5T Each time you enter this command, CodeView allows one statement in your source code to run. You can also use: 005>~.T The period indicates the current thread, which is thread 5. You'll note that none of the other threads runs when you execute this trace command. When you trace through a single thread with ~.T and ~nT CodeView temporarily freezes all threads except the one you're tracing. This is true in both source code mode and assembly mode. The most difficult command is T used by itself, which works differently in source mode and assembly mode. To explore this, first switch to mixed mode: 005>S& Now just enter successive T instructions: 005>T At the calls to the OS/2 or C library functions you'll want to use the P command to avoid tracing through function code. Note that none of the other threads runs while you're tracing. The T command in assembly or mixed mode works the same as ~.T. Now switch back to source mode: 005>S+ and enter several T instructions: 005>T In source mode, the T instruction allows all the other threads to run. Basically, CodeView sets a breakpoint at the end of the group of assembly language instructions that make up the current source code instruction and then lets the program run. However, CodeView does not check which thread breaks at the breakpoint. In the THREADS program, threads 2 through 5 share the same code. So while you're using the T instruction in source mode, you'll find that it often breaks with a different current thread. Watch out for this when you're tracing through source code that is shared by multiple threads. It's tempting to use the T command by itself, but you'll probably want to use ~.T or ~nT instead to prevent the current thread from changing. To end this exercise, just execute Go: 00?>G press the Escape key, and enter Q to exit CodeView: 001>Q The syntax of the various Trace, Program Step, and Execute commands are shown in Figures 9 and 10. Running the Trace and Program Step commands from the menu or using the F8 and F10 keys work the same as the T command entered by itself. If you ever find that the current thread is not the thread you expect or want, you can change the current thread with the CodeView Select command ~nS. This and other variations of the Select command are shown in Figure 11. As you've seen, all the thread-specific CodeView commands can use the pound sign (#) to indicate the last executed thread, which, defined more precisely, is always the current thread unless you change the current thread with one of the Select commands. The last executed thread is not changed by the Select command, only by executing a Go, Trace, Program Step, or Execute command, in which case the last executed thread is the current thread after one of these commands returns control back to CodeView. Windows and Threads Although the new CodeView commands are obviously very helpful in debugging multithread applications, they are not entirely satisfactory. For the most part, the commands are restricted to the command line interface in the CodeView dialog window. The use of a single display that is switched among different threads is also not an adequate visual analog of the organization of a multithread program. As CodeView evolves to keep pace with the increasingly complex architecture of OS/2 applications, a more suitable visual interface might involve multiple windows to view and control the code in each thread. The OS/2 Presentation Manager would be an ideal environment for such a debugger. Figure 1: The THREADS Program #------------------- # THREADS make file #------------------- threads.obj : threads.c cl -c -Alfw -G2s -W3 -Zi threads.c threads.exe : threads.obj threads.def link threads, /align:16, NUL, /nod llibcmt doscalls, threads /CO /*---------------------------------------------------------- THREADS.C -- Multithread OS/2 Program for CodeView Demo Programmed by Charles Petzold, 6/88 ----------------------------------------------------------*/ #define INCL_DOS #define INCL_VIO #define INCL_KBD #include <os2.h> #include <mt\process.h> #include <mt\stdlib.h> /*------------------------------------------------ Structure definition and function declarations ------------------------------------------------*/ typedef struct { SHORT xLeft ; SHORT yTop ; SHORT xRight ; SHORT yBottom ; } THREADPARAM ; typedef THREADPARAM FAR *PTHREADPARAM ; BOOL CheckKey (CHAR ch) ; VOID _CDECL FAR ThreadFunction (PTHREADPARAM ptp) ; VOID WriteChar (CHAR ch, USHORT usRow, USHORT usCol) ; VOID PASCAL FAR ExitRoutine (USHORT usTermCode) ; VOID ClearScreen (BOOL fCursorOff) ; /*----------------- main() function -----------------*/ INT _CDECL main (VOID) { static INT iStack [4][2048] ; static CHAR szErrorMsg[] = "THREADS: Cannot start thread" ; KBDKEYINFO kbci ; SHORT i ; THREADPARAM tp[4] ; VIOMODEINFO viomi ; /*--------------------------------------------- Clear screen and get video mode information ---------------------------------------------*/ DosExitList (EXLST_ADD, ExitRoutine) ; ClearScreen (TRUE) ; viomi.cb = sizeof viomi ; VioGetMode (&viomi, 0) ; /*----------------------- Start up four threads -----------------------*/ for (i = 0 ; i < 4 ; i++) { tp[i].xLeft = i % 2 ? viomi.col / 2 : 0 ; tp[i].yTop = i > 1 ? viomi.row / 2 : 0 ; tp[i].xRight = tp[i].xLeft + viomi.col / 2 ; tp[i].yBottom = tp[i].yTop + viomi.row / 2 ; if (-1 == _beginthread (ThreadFunction, iStack[i], sizeof iStack[i], tp + i)) { VioWrtTTY (szErrorMsg, sizeof szErrorMsg - 1, 0) ; return 1 ; } } /*-------------------------- Wait for Esc key to exit --------------------------*/ do KbdCharIn (&kbci, IO_WAIT, 0) ; while (!CheckKey (kbci.chChar)) ; return 0 ; } /*----------------------------------------------------- CheckKey() function -- Returns TRUE if ch is Escape -----------------------------------------------------*/ BOOL CheckKey (CHAR ch) { } /*------------------ ThreadFunction() ------------------*/ VOID FAR _CDECL ThreadFunction (PTHREADPARAM ptp) CHAR ch ; TID tid ; USHORT usRow, usCol ; /*---------------------------------------- Get thread ID and convert to character ----------------------------------------*/ DosGetInfoSeg (&selGlobal, &selLocal) ; tid = ((PLINFOSEG) MAKEP (selLocal, 0)) -> tidCurrent ; ch = (CHAR) tid + '0' ; srand (tid) ; /*--------------------------------------- Display thread ID in random locations ---------------------------------------*/ while (TRUE) { usCol = ptp->xLeft + rand () % (ptp->xRight - ptp->xLeft) ; usRow = ptp->yTop + rand () % (ptp->yBottom - ptp->yTop) ; WriteChar (ch, usRow, usCol) ; } } /*---------------------------------------------------------- WriteChar() function -- Displays TID, pauses, then blank ----------------------------------------------------------*/ VOID WriteChar (CHAR ch, USHORT usRow, USHORT usCol) { VioWrtCharStr (&ch, 1, usRow, usCol, 0) ; DosSleep (100L) ; VioWrtCharStr (" ", 1, usRow, usCol, 0) ; } /*------------------------------------------------ ExitRoutine() function -- Exit list processing ------------------------------------------------*/ VOID FAR PASCAL ExitRoutine (USHORT usTermCode) { usTermCode ; /* to prevent compiler warning */ ClearScreen (FALSE) ; DosExitList (EXLST_EXIT, NULL) ; } /*-------------------------------------------------------- ClearScreen() function -- Also turns cursor off and on --------------------------------------------------------*/ VOID ClearScreen (BOOL fCursorOff) { USHORT usAnsi ; VIOCURSORINFO vioci ; VioGetAnsi (&usAnsi, 0) ; VioSetAnsi (ANSI_ON, 0) ; VioWrtTTY ("\33[2J", 4, 0) ; VioSetAnsi (usAnsi, 0) ; VioGetCurType (&vioci, 0) ; vioci.attr = fCursorOff ? -1 : 0 ; VioSetCurType (&vioci, 0) ; } ;------------------------------------ ; THREADS.DEF module definition file ;------------------------------------ NAME THREADS WINDOWCOMPAT DESCRIPTION 'Multithread Demo Program by Charles Petzold, 1988' PROTMODE HEAPSIZE 1024 STACKSIZE 4096 Figure 3: Variations of the CodeView Status command. Command Applies to ~ All threads ~n Thread n ~. Current thread ~# Last executed thread ~* All threads Figure 4: Variations of the CodeView Freeze and Unfreeze commands. Freeze Unfreeze Command Command Applies to to ~nF ~nU Thread n ~.F ~.U Current thread ~#F ~#U Last executed thread ~*F ~*U All threads Figure 7: Variations of the CodeView Breakpoint command. Breakpoint Command Command Applies to BP All threads ~nBP Thread n ~.BP Current thread ~#BP Last executed thread ~*BP All threads Figure 8: CodeView Go command variations. Temporarily Temporarily Command Freezes Unfreezes G ── Current thread ~nG All threads Thread n except n ~.G All threads Current thread except current ~#G All threads Last executed except last thread executed ~*G(+) All threads Current thread except current (+)This is a bug. The ~*G command should work the same as G. Figure 9: Variations of the CodeView Trace command in source mode. (The P and E commands work similarly.) Trace Command (Source Mode) Temporarily Temporarily Returns Command Freezes Unfreezes Control to T - Current thread Not predictable y~nT All threads Thread n Next statement except n in thread n ~.T All threads Current thread Next statement except current in current thread ~#T All threads Last executed Next statement except last thread in last executed executed thread ~*T☼ All threads Current thread Next statement except current in current thread Figure 10: Variations of the CodeView Trace command in assembly or mixed mode. (The P and E commands work similarly.) Trace Command (Assembly Mode or Mixed Mode) Temporarily Temporarily Returns Command Freezes Unfreezes Control to T All threads Current thread Next instruction except current in current thread ~nT All threads Thread n Next instruction except n in thread n ~.T All threads Current thread Next instruction except current in current thread ~#T All threads Last executed Next instruction except last thread in last executed executed thread ~*T☼ All threads Current thread Next instruction except current in current thread Figure 11: Variations of the CodeView Select command. Select Thread Command Command Current Thread Set to ~nS Thread n ~.S Current thread ~#S Last executed thread ████████████████████████████████████████████████████████████████████████████ Exchanging Data Between Applications Using the Windows Clipboard ─────────────────────────────────────────────────────────────────────────── Also see the related article: Encapsulated Postscript ─────────────────────────────────────────────────────────────────────────── Kevin P. Welch and David E. West☼ Transferring data between applications has long been a problem for computer users. Even novices want to move information from one program to another. In the early days of personal computers the only effective way to do this was to transfer the information manually. The next level of power and usability arrived when file standards such as Comma Separated Variable (CSV) and Data Interchange Format (DIF) were developed, facilitating the information transfer through intermediate data files. Later, integrated packages became available in which the most commonly used applications were encapsulated into one monolithic program. Although this reduced the need for data exchange between programs, the central issue of how to transfer data still remained. Continuing the movement toward greater power and usability, Microsoft(R) Windows expanded on the concepts pioneered by the Macintosh(R) and offered developers and users a well-connected desktop environment within which applications could naturally cooperate. Within Windows two methods rapidly emerged as the de facto standard mechanisms for interprogram communication and data exchange──the clipboard and dynamic data exchange (DDE). Of these two, the clipboard is used much more often. With the clipboard a user can effortlessly select a range of cells in a spreadsheet, copy them to the clipboard, and subsequently paste them into a document being edited with a word processor. Despite the possibility that the two programs may have been developed by competitive vendors, they communicate effortlessly and appear to the user as one well-integrated desktop. The clipboard is by nature a shared system resource. In order to function correctly it is vitally important that each application interact with the clipboard properly to prevent system crashes, endless message loops, and other strange and disconcerting events from occurring. This article will explain from a development perspective how to properly use the clipboard and prevent some of the more common mistakes. The techniques and insights that are provided here were developed and utilized by the authors during the creation of T/Maker Company's ClickArt(R) Scrapbook+, a powerful system utility that provides many enhancements to the Windows clipboard. Clipboard Fundamentals From the user's perspective the clipboard is a temporary data storage area accessible through an application's Edit pull-down menu. The following commands, as defined in the Windows Style Guide, enable the user to work with the clipboard: Cut copy selection to the clipboard and delete Copy copy selection to the clipboard Paste paste clipboard data into application Unfortunately things are a little more complicated for the software developer. Besides the Cut, Copy, and Paste operations the developer must contend with such issues as clipboard ownership, multiple rendering, delayed rendering, and the clipboard viewer chain. One clipboard fundamental is clipboard ownership. Because the clipboard is a shared system resource, Windows lets applications temporarily block other programs from using it while changes are made. After completing the changes the clipboard is immediately released so that other applications can access it. Conceptually, from a user's perspective, the clipboard appears to contain only one data item at a time. In reality though it is capable of maintaining several data items simultaneously, each supposedly representing the same object but in a different format. This concept of multiple rendering enables applications such as Microsoft Excel to supply the clipboard with up to thirteen different formats or representations of the same information when a copy operation is performed. The pasting application can then choose the most desirable format from those available on the clipboard. Figure 1☼ shows an example of multiply rendered data. When the clipboard contains multiply rendered data it is left up to the pasting application to decide which format best suits the needs of the user. In most cases astute applications place data on the clipboard in priority order, starting with the format of greatest information density. The order of enumeration is usually random, however, and should not be relied on. The pasting application can then enumerate these formats and obtain alternative representations in priority order as determined by the originator. As you can imagine, maintaining multiply rendered clipboard data can consume prodigious amounts of precious system memory. Fortunately another concept known as delayed rendering attempts to limit the allocation of memory to only those formats that are actually used. Delayed rendering in effect allows the copying application to place a "promise" on the clipboard for each potential rendering. These promises remain on the clipboard until some application requests a particular data item. When this request is received, the copying application must fulfill the promise by supplying the data in the format requested, even if it was copied to the clipboard some time before. The end result is that data is allocated for only those formats that are actually used, thus consuming less system memory and eliminating the computation necessary to produce unwanted formats. Delayed rendering will also save processing time for simply rendered items. In addition to the multiple and delayed rendering concepts, the Windows clipboard provides a mechanism whereby interested applications can be automatically notified whenever clipboard changes occur. This facility, which is called the clipboard viewer chain and is shown in Figure 2, causes the system to send a WM_DRAWCLIPBOARD message to all participating windows. Receipt of this message indicates that a clipboard change has occurred that might be of interest to the application. After processing this message the application is also given the responsibility of passing this message to the next application in the chain. Clipboard API Applications that wish to use the clipboard can do so by calling functions provided by the Windows Application Programming Interface (API) and handling certain related messages. Figure 3, extracted in part from the Windows Software Development Kit (SDK), briefly describes the available functions. Besides these functions, the clipboard-related messages listed in Figure 4 are supported by Windows. Clipboard Data Formats Windows predefines seven different clipboard formats, ranging from standard ASCII text to graphical data in binary form. Besides these predefined formats, Windows offers a mechanism with which applications can create additional user-defined formats. Among these user-defined extensions are six that are commonly supported by most major applications. Figure 5 briefly describes each of these formats. Note that those formats whose names begin with the CF_ prefix refer to predefined clipboard formats. User-defined clipboard formats are registered with Windows by calling RegisterClipboardFormat with a unique character string. Each application that uses this format is expected to reregister this format and in doing so obtain the same format handle. Once defined, other applications can retrieve this registered name by calling the GetClipboardFormatName function with the format handle as a parameter. Note that calling GetClipboardFormatName with a predefined clipboard format will return a NULL string. Also, when using a CF_OWNERDISPLAY object, the actual name of the format can be retrieved by sending the clipboard owner a WM_ASKCBFORMATNAME message. Of the commonly used clipboard formats, all but CF_BITMAP and Printer_Bitmap are represented by handles to global blocks of system memory. Memory for these formats is allocated by using GlobalAlloc with the GMEM_MOVEABLE flag as a parameter. When the resulting handle is transferred to the clipboard (via a SetClipboardData call), the system changes the memory attributes to GMEM_DDESHARE with a GlobalReAlloc and assigns ownership of the block to Windows. This enables the memory to be retained after the copying application is terminated, even though in normal circumstances memory allocated by an application is marked for deletion when that application terminates. Note that once an application has placed an object on the clipboard it becomes the property of the system and the application should not continue to use the data. Now let's look at clipboard formats and analyze their potential uses and abuses. CF_BITMAP. This format is used for the transfer of GDI bitmap images. Bitmaps can be created by using CreateBitmap, CreateBitmapIndirect, or CreateCompatibleBitmap. The resulting handle can be placed on the clipboard with SetClipboardData without any further modification. Note that the CreateDiscardableBitmap function should not be used when creating bitmaps for transfer through the clipboard. Due in part to GDI, the CF_BITMAP format comes in two flavors. The first of these is a conventional monochrome bitmap. The byte arrangement of this format is fixed, supporting the direct manipulation of the bitmap data. In addition, this standardized byte arrangement permits monochrome bitmaps to be transported from one machine to another, albeit subject to display/aspect ratio dependencies. The second flavor is the color bitmap which, unfortunately, is strictly device-dependent in Windows, and the portability of such an object between systems is not assured. Furthermore, color bitmaps must be considered to be in an unknown byte arrangement, precluding the use of any direct manipulation of the data. CF_DIF. Software Arts' DIF format consists of ASCII text whose lines are separated by a single carriage return-line feed (CR-LF) character pair. Unlike CF_TEXT, the DIF data block is not null-terminated as the end is encoded within the data. The simplest method to determine the extent of the data without parsing the information is to use GlobalSize and retrieve the block size. With this method a single function can be written to handle several different text-related clipboard formats. CF_METAFILEPICT. This format refers to a block of global memory containing a METAFILEPICT structure. This structure contains four fields: mm, the mapping mode to be used when displaying the metafile; xExt and yExt, from which the requested size of the image drawn by the metafile can be calculated (see the Windows SDK Programmer's Reference manual for more information); and hMF, a handle to the actual metafile. To make a copy of this data, use GlobalAlloc to allocate memory for the METAFILEPICT structure, then use CopyMetafile to make a copy of the metafile referred to by the hMF field. CF_OWNERDISPLAY. The CF_OWNERDISPLAY clipboard format is designed to enable applications to maintain their own display representation of internal data. In order to display such information, the clipboard viewer is limited to sending messages to the supplying application whenever the viewport needs repainting. This unusual painting method is how the clipboard viewer application (CLIPBRD.EXE) displays Write-formatted text. When using this format the clipboard owner receives a series of messages that correspond to those normally encountered by the clipboard viewer. The owner is thus responsible for performing the appropriate action whenever a paint, size, or scroll operation is performed. One annoying feature of the CF_OWNERDISPLAY format is the way in which the client area of the clipboard viewer is handled. In the case of Windows Write it automatically changes the style of the viewport to include an additional set of horizontal and vertical scroll bars, regardless of the fact that they may be already present as child windows. Furthermore, the CF_OWNERDISPLAY data provided by most applications is typically in some proprietary internal format and is of little use to other applications. CF_SYLK. This clipboard format, a memory representation of the SYLK file format, is similar to the CF_DIF format in that each line of ASCII text is terminated by a CR-LF pair. As is the case with CF_DIF, the data block is not null-terminated; hence GlobalSize should be used to determine the actual block size, subject to the usual 16-byte paragraph limitations. CF_TEXT. The CF_TEXT format is one of the simplest supported by the clipboard. Each line of text is terminated with a CR-LF pair and the entire text block with a null character. Any application supporting the transfer of textual data via the clipboard should consider using this format in addition to any others, such as Rich Text, that provide for the exchange of character-, paragraph-, and document-formatting information. CF_TIFF. The CF_TIFF format is used for transferring monochrome, gray- scale, or color images between cooperating applications. Although designed for use with very large images, this format is rapidly gaining acceptance for images of all sizes as it represents an exact memory image of the original Microsoft/Aldus file specification. Due to memory limitations, applications that support this format should also be able to import TIFF files directly from disk. In recent months a number of software developers have been proposing extensions to the CF_TIFF clipboard specification to include the definition of a file-assisted mechanism for the clipboard transfer of large images. However, it is unlikely that this proposal will be supported by Windows as it would require modifications to the internal clipboard manager to handle the associated temporary files shared between applications. Refer to the Windows SDK Extensions Manual for additional information on the CF_TIFF file and clipboard specification. BIFF. This user-defined clipboard format is currently supported by a number of applications, the most notable of which is Microsoft Excel. BIFF (for Binary File Format) is the file format in which Excel documents are saved on disk. A BIFF file is a complete self-contained package consisting of a sequence of variable-length records. For example, one record type describes the size and location of a window into a document, another describes a formula entered into a cell, while another describes a picture format. Although individual BIFF record types contain different information, each record follows the same format: record type, record length, and record data (variable-length). In the future the BIFF clipboard and file format will probably become a standard much as TIFF has. Several independent software developers are already supporting this format for the interapplication transfer of binary financial data. Contact Microsoft for more information about this file and clipboard format. CSV. Software developers have long been familiar with the CSV file format; it is perhaps the least common denominator of data exchange formats (others may argue that TEXT is). Pioneered on the clipboard by Microsoft Excel, this user-defined format is treated in Windows exactly like CF_TEXT; each ASCII text line is separated by CR-LF pairs with the entire block terminated by a single null character. Within each line of text every data item is comma separated with text strings enclosed between quotation marks. Due in part to its simplicity, CSV is widely used, and any application that utilizes the kind of data suited to this format should support this standard. PostScript(R). Encapsulated PostScript (EPS) is an emerging standard for the exchange of text and graphics between cooperating applications. The PostScript user-defined format was developed by its authors for the transfer of EPS images via the clipboard. Currently supported by Scrapbook+ and several other Windows applications, the PostScript text is usually accompanied by one or more display representations, typically a CF_TIFF or CF_METAFILEPICT image. The PostScript format is conceptually identical to CF_TEXT, except that each text line is terminated with a single CR character and the entire block by a null character. Refer to the accompanying sidebar, "Encapsulated PostScript," for more information on the transfer of EPS images via the clipboard. Printer_Bitmap. This user-defined format is identical to CF_BITMAP except that it is limited to the output characteristics of the currently selected printer. For example, using this format a color bitmap would be rendered in monochrome when a laser printer was defined as the default output device. The use of this format enables the copying application to perform any necessary conversions that might be difficult for other programs. Printer_Picture. The Printer_Picture format, like Printer_Bitmap, is identical to CF_METAFILEPICT but is limited to the output characteristics of the currently selected printer. The application supplying this format is responsible for performing any and all necessary conversions for the default output device. Although simple in theory, the definition of this format can be difficult when it involves the mapping of fonts, colors, and other GDI objects to a specific output device (not unlike printing). Rich Text. The Rich Text Format (RTF), a relative newcomer to Windows, was developed by Microsoft as a method of encoding formatted text and graphics for easy transfer between applications. Using RTF, text and graphics can be exchanged between different applications, environments, or even operating systems. An RTF object consists of unformatted 7-bit ASCII text that embeds specially formatted commands in the document. These control words are used to define printer commands and provide document management information. Copying to the Clipboard The general method for copying data to the clipboard is to allocate global memory, format memory with desired data, and transfer the memory handle to the clipboard. There are two models for copying data to the clipboard, the replacement model and the supplemental model. The replacement model requires that the clipboard be emptied before copying data to it, ensuring that only the data from that particular copy operation is on the clipboard. The supplemental model requires that when copying data to the clipboard, the clipboard should not be emptied; rather, only data of the same format as the data you are copying should be replaced, leaving all other formats intact. In almost all circumstances the replacement model is preferable because it takes up less memory and is much simpler conceptually. There are cases, however, where the supplemental model is appropriate; one such case will be discussed below. The code fragments shown below and in Figures 6, 7, 8, and 9 demonstrate how to copy multiply rendered data to the clipboard by using both normal and delayed rendering schemes. Note that the copy routine uses the replacement clipboard model, calling EmptyClipboard before calling SetClipboardData. The replacement model must be employed when providing data with a delayed rendering scheme because Windows sends the WM_RENDERFORMAT message to the clipboard owner whenever a particular format is requested. The supplemental model, not using an EmptyClipboard call, does not transfer clipboard ownership. If it were used in such a situation the format-rendering messages requesting the data might never be received. Both the copy and paste code fragments use the following data structure and definitions: /* clipboard format data table */ typedef struct { WORD wFormat; HANDLE hGlobalData; } CLIPBOARD; /* global application data */ WORD wFormats; CLIPBOARD Clipboard[6]; The code fragment shown in Figure 6 defines three alternative global data blocks for use in a multiply rendered copy operation to the clipboard. Alternatively, if a delayed rendering scheme is desired, the data formatting operations can be postponed until a request is made for the information, as is shown in Figure 7. Once the data has been defined the copy operation can be performed by using the code fragment shown in Figure 8. Note that Windows automatically reallocates the memory blocks supplied, changing the memory options used to include the GMEM_DDESHARE flag. If a delayed rendering scheme is used, the promised data must be supplied at some undetermined time in the future. Although the actual details of creating the data can be rather complicated, the code that supplies this request is simple. The main window function shown in Figure 9 demonstrates how such format requests might be handled. Pasting Pasting data from the clipboard is usually a little easier than an equivalent copy operation. The general method for pasting data from the clipboard is as follows: ■ open the clipboard ■ determine if the clipboard does contain the desired rendering ■ obtain the handle to the object, the clipboard, or the GDI object handle ■ make a local copy of the data ■ close the clipboard Using the data structure defined above, the code fragment in Figure 100 demonstrates how the paste operation would be accomplished using the constructs established for the examples in Figures 6, 7, 8, and 9. In Figure 10 EnumClipboardFormats is repeatedly called until all available clipboard formats are enumerated. Through the use of an alternative technique, IsClipboardFormatAvailable could also be repeatedly called with the desired format as a parameter. This function returns TRUE when the requested format is available and FALSE otherwise. Note that the clipboard does not have to be open to use this function. Another item to consider is the manner in which the handle returned by GetClipboardData is treated. Due to the shared nature of clipboard data, this handle is the property of Windows and the data it represents must not be modified or released. Furthermore, this handle becomes invalid once CloseClipboard is called. Clipboard Viewer Chain As mentioned previously, Windows offers a mechanism to inform interested applications whenever changes to the clipboard occur. This linked list of window handles, called the Clipboard Viewer Chain, is maintained in a cooperative fashion by each of the participating applications. An application enters this chain by calling the SetClipboardViewer function with its window handle as a parameter. In response Windows defines this window as the "current clipboard viewer" and returns a handle to the previous viewer, if one exists. The participating application must save this handle as it represents the only link to the remainder of the chain. The chain is built in reverse with the most recent entry at the head of the list. Figure 11 illustrates an addition to the viewer chain. When changes occur to the clipboard, a WM_DRAWCLIPBOARD message is sent by Windows to the current clipboard viewer. This window is responsible for passing the message down the chain by using an explicit SendMessage call and then performing any action deemed necessary. Failure to pass this message on to the next window effectively destroys the viewer chain and can sometimes result in abnormal behavior from those applications further down the list. Note that the WM_DRAWCLIPBOARD message is generated during the CloseClipboard call, following one or more calls to SetClipboardData, and that control does not return until the entire viewer chain has processed the message. An application can remove itself from the viewer chain by calling the ChangeClipboardChain function through the use of its handle and the handle to the next application in the chain as parameters. If the window in question is the current clipboard viewer, Windows simply resets the current viewer to the next window in the list; this is analogous to removing the first entry in a linked list. In all other cases a WM_CHANGECBCHAIN message is sent to the current clipboard viewer for subsequent transmission down the chain. When an application receives this message it must check to see if the window being removed is the one next in the viewer chain. If this is the case, the hNextWnd variable is reset to reference the window following the one that is being removed. Figure 12 demonstrates the removal of a window. The code fragment in Figure 13 demonstrates these concepts within the context of a traditional window function. In this example the window joins the clipboard viewer chain at creation and removes itself when destroyed. Tips, Tricks, and Traps Using the clipboard effectively can be a challenging exercise for any software developer, especially when large or complicated formats are involved. Clipboard support becomes even more difficult when several large programs compete for limited system resources. When using the clipboard a number of techniques can make the entire process operate more efficiently. Here are some programming tips, tricks, and traps to note. Tip──Limit code segment size. When one or more large applications compete for resources, system performance rapidly deteriorates; this is especially true when working with the clipboard. This situation can be improved by designing your applications to suit Windows' memory management style. In particular, dramatic improvements in performance can sometimes be obtained by marking each code segment as movable and discardable while limiting its size to 8Kb. When this is done, Windows can efficiently rearrange memory when resources become scarce. This rearrangement is made much easier when code segments are uniformly small in size. Be aware however that the desired 8Kb segment size also applies to the _TEXT code segment. This segment usually contains a large portion of the C run-time library. In order to effectively reduce the size of this segment you will have to purchase the run-time library source code and recompile it into different segments depending on your needs. Trick──Build in recovery techniques. Many of the code fragments accompanying this article use less-than-ideal error recovery techniques. When working with the clipboard, it is especially important to incorporate several robust mechanisms that can be invoked when a particular operation fails. In almost all cases you would be well advised to incorporate them into your application early in the development cycle before error recovery becomes an issue. An example of the need for such an error recovery technique occurs when data is copied to the clipboard. If the initial memory allocation and formatting fails, then an alternative approach could be invoked that releases some discardable memory and uses a less demanding, and perhaps less efficient, method. The use of such techniques distinguishes great applications from average ones. Trap──Using fixed code or data segments. The use of fixed code or data segments can impair Windows memory management. Programs that employ such techniques run the risk of precluding the use of other well-behaved applications. Although Windows attempts to locate fixed code or data segments away from critical areas, situations exist where they can cause serious degradation of system performance. This is especially obvious when using the clipboard to exchange large contiguous data items between applications. Tip──supplied by Windows return a value when completed. In most circumstances it is inadvisable to ignore these return values, as unusual conditions sometimes occur when functions fail. Furthermore, many Windows functions have numerous side effects that can lead to unforseen results. In addition, it is likely that the API code might itself change with a new version of Windows, resulting in new and unanticipated situations. Trick──Batch up error messages. When using the clipboard, it is possible to consume all available system memory. In such a situation almost any complicated operation will fail, possibly resulting in a rapid-fire sequence of annoying message boxes. Wherever possible, it is desirable to batch up these messages and concatenate them into a single error. The typical user is often uninterested in the exact cause of the failure, instead being more concerned with the current state of the application and what he or she can do to make it work. Trap──Inaccessible error recovery code. One easily encountered trap occurs when an out-of-memory condition is reached that requires the use of complex error recovery routines. If these routines, and their associated resources, need even small amounts of extra memory they too are likely to fail. The end result can be as benign as a garbled error message or as disastrous as a system crash. Generally speaking, it is advisable to keep error recovery code simple, keep it in memory whenever possible, and make it independent of discardable system resources. Tip──Avoid long periods of dead air. Applications that work with the clipboard sometimes require long periods when intense computation is being performed. An example of this would be when a large compressed gray-scale TIFF image is displayed on a monochrome display. In this case the application must uncompress the image in bands while converting the gray-scale to monochrome. As one would imagine, this process can consume a considerable amount of system resources and take several seconds. In such situations it is best to use one of the following techniques: small jobs change the mouse pointer to an hourglass medium jobs display a modal dialog box with a cancel button large jobs display a modeless dialog box with a cancel button Trap──Null rendering failure. In order for the delayed rendering process to be transparent to other applications, the clipboard owner must handle all interactions correctly. When the clipboard owner receives a WM_RENDERFORMAT or WM_RENDERALLFORMATS message, it should attempt to render the requested data. If it cannot render the data because of insufficient memory or some other condition, the clipboard owner should not call the SetClipboardData function with a NULL handle. Doing so will cause a spurious WM_DRAWCLIPBOARD message to be sent down the viewer chain as Windows will assume that the clipboard has changed yet again, while in actuality the data will still be null rendered. If the data cannot be rendered, doing nothing is advisable. The next time the null- rendered data is requested, Windows will send another WM_RENDERFORMAT message. The request may succeed this time as the exact memory configuration may be different from the time of the last request. Trap──Using delayed rendering when not ready. When CloseClipboard is called after copying data to the clipboard, a WM_DRAWCLIPBOARD message is immediately sent down the viewer chain. If one of the clipboard viewers immediately requests the data, a WM_RENDERFORMAT message will be sent to the current clipboard owner (note the re-entrancy as the CloseClipboard function has not yet completed). This immediate response by one or more of the applications in the viewer chain may occur before the copying application is ready to provide the data. In this case an incorrect response is made to the WM_RENDERFORMAT message, leaving the clipboard in an undetermined state and causing a number of other problems. The way to avoid duplicating this problem is to call CloseClipboard only after the data can be satisfactorily rendered. Trap──Extra WM_DRAWCLIPBOARD messages. Another trap to be avoided occurs when a member of the clipboard viewer chain requests null-rendered data. When the application calls GetClipboardData, Windows sends a WM_RENDERFORMAT message to the current clipboard owner. The clipboard owner then formats the requested data and places it on the clipboard using SetClipboardData. Behind the scenes Windows sends an extra WM_DRAWCLIPBOARD message down the viewer chain in response to the SetClipboardData call. The pasting application receives this spurious message because it is also a member of the chain and can be tricked into thinking that the clipboard contents have changed yet again. The only way of preventing this is maintaining a semaphore that separates meaningful WM_DRAWCLIPBOARD messages from spurious ones. Conclusion Although incorporating good clipboard support into an application can be challenging and time-consuming, the benefits are worth it. As more software becomes available for Windows, products are increasingly evaluated on their connectivity as well as their features. Comprehensive clipboard support is a natural and logical way to provide the kind of interconnectivity that users have come to expect. Despite the complexity of this article a considerable amount of information has not yet been discussed. The more complicated clipboard formats each have their own specifications. Several good reference materials are available that cover clipboard programming in great detail. The best of these is Programming Windows by Charles Petzold (Microsoft Press, 1988). With this and other reference materials even the most difficult clipboard problems can be resolved with a little patience. ─────────────────────────────────────────────────────────────────────────── Encapsulated Postscript ─────────────────────────────────────────────────────────────────────────── Encapsulated PostScript (EPS) is rapidly becoming the standard means for transferring graphics and text between dissimilar software applications in diverse environments. The industrywide acceptance of this standard has resulted in the integration of EPS support into many popular programs. EPS has been enthusiastically received in the Microsoft(R) Windows environment for IBM-compatible personal computers. Windows has proven itself to be a highly consistent and user-friendly environment that facilitates the development of graphically oriented applications. EPS is of special interest to such applications as it can serve as a device- independent means for transporting graphical objects between applications and environments. Several applications now exist under Windows that support this standard, and many more are in development. These applications are currently limited to importing and exporting EPS files directly from disk, which normally results from an explicit user command and is somewhat cumbersome to use. Also, it further complicates the seamless, permissive environment of Windows. This situation would be greatly improved with the specification and definition of a clipboard data format for the EPS standard. This would enable PostScript images to be transferred through the clipboard, as well as through Dynamic Data Exchange (DDE). Applications would then be able to share text and graphics through the clipboard by using the same standard mechanisms available for other predefined data formats. Requirements Any definition of a new clipboard data format for EPS must consider several design issues and requirements, which serve to focus the specification. These include, but are not limited to, the following issues: 1. The specification should consider the multiple-rendering capabilities of the clipboard and offer as many standard representations of the image as possible for use by existing Windows applications. This would give such programs as Windows Write and Paint access to display and printer renderings of PostScript images. 2. The specification does not necessarily have to handle arbitrarily large and complex PostScript images. As is the case with the CF_TIFF clipboard format, large images might be best transferred with another mechanism. 3. The interapplication transfer of small EPS images, say, between 10Kb and 64Kb, should be performed as efficiently as possible, occurring completely in memory by using standard clipboard operations. 4. The future definition of a bitmap mask for any display renderings provided as part of this specification should be allowed for. This would enable the destination application to overlay nonrectangular display renderings over existing graphics and text. 5. There should be some separability between the PostScript text and the various display renderings included as part of the encapsulated standard. This would provide for possible future hardware platforms that would use Display Postscript, reducing the need for special screen representations. Also, a preliminary analysis of EPS art available on IBM-compatible PCs suggests that the images range in size from approximately 2Kb to in excess of 3Mb. Most of the less complicated images are smaller than 64Kb and are thus consistent with the design goals of this specification. Specification Proposal The following proposal for a specification describes the mechanisms and standards used to transfer EPS images via the clipboard. This discussion employs the existing EPS File (EPSF) definition and does not address any future device-independent variations of this standard. 1. EPSF objects are a concatenation of standard PostScript text with one or more supplemental display renderings, such as a Windows Metafile, a TIFF image, or possibly a Macintosh PICT representation. 2. Each application supporting the clipboard transfer of EPSF images would be required to preregister a PostScript clipboard format. This would be accomplished by passing the following character string to the Windows RegisterClipboardFormat routine: Postscript Multiple registration of this format would result in the return of the same ID number with an internal reference count being incremented. Several cooperating applications could then share the same format numbers, although this number might vary from session to session. 3. An application transferring an EPSF image through the clipboard would supply a PostScript text with one or more display renderings. The donor application would be responsible for determining whether sufficient memory is available for the copy operation and which of the supplemental renderings to provide. The supplemental renderings would normally be simple clipboard copies of the display renderings provided with the PostScript text. In some situations the donor application may wish to generate additional representations from the display renderings. For example, a Windows Metafile rendering could be worked into a monochrome bitmap, enabling applications such as Windows Paint to gain access to a crude representation of the PostScript image. The donor application in this example would be responsible for determining the dimensions of this supplemental bitmap representation. Note that in most cases the donor application would provide the PostScript and display renderings on a delayed basis so as to avoid the production of unwanted formats and the consumption of precious memory. 4. The PostScript clipboard object would consist of standard PostScript text taken directly from the EPSF file. Applications wishing to display the PostScript text could do so by using standard ASCII text-handling mechanisms. 5. In certain situations it would be desirable to provide a shadow mask for any bitmap representation of the EPSF image. This would permit pasting applications to overlay the bitmap onto existing text and graphics. Despite the appeal of this capability, we suggest that at the present time an additional format not be defined for this mask. Note that many CF_TIFF display renderings, including those created by Adobe Illustrator(TM), do contain a TIFF mask as part of the entire representation. 6. The copying application would be responsible for releasing any global memory associated with the PostScript text when the data is replaced by another entry, although this is usually the responsibility of Windows itself. The receipt of a WM_DESTROYCLIPBOARD message would indicate that this has occurred. The copying application, which in this scheme does not have to be part of the clipboard viewer chain, can then release the memory associated with the clipboard PostScript data object. Implementation This specification is just a proposal at this point. The definition will probably be changed once all interested parties have commented on it. The final acceptance and implementation of this standard can be achieved in one of three ways, which are discussed here in descending order of desirability. First, Microsoft defines a new clipboard format, for example, CF_POSTSCRIPT, and makes it part of a future Windows Software Development Kit (SDK). This would require an updated version of WINDOWS.H and revised documentation. Microsoft would then become the keeper of the definition and be responsible for its support and possible future extension. The Windows clipboard would then become responsible for releasing the global memory consumed by the PostScript text, relieving the donor application of this task and ensuring that unused resources are not accumulated. Second, the most likely scenario is that a representative group of software developers, after reaching consensus on the specification, makes the definition public and physically implements it in several complementary products. Each of these applications would be responsible for preregistering the PostScript clipboard format and supporting it however necessary. In time the clipboard protocol for handling this format would become an industry standard accepted by all until something better displaced it. Under this approach the keeper of the definition would be a committee of developers responsible for the specification until it warranted inclusion in the Windows SDK. The last and most undesirable means for establishing the standard is for a small number of software developers to reach consensus on the specification, make it part of their products, but not release it to the general public. This might in the short run give these complementary programs a slight market advantage until the standard was made public or a new public one replaced it. However, from a historical perspective, under such an approach the standard would probably be short-lived and never reach the level of usage necessary to qualify it as an accepted industry standard. Future Possibilities The development of a means for transferring EPS images through the Windows clipboard opens up some exciting possibilities. The specification and proposed definition of a standard means for accomplishing this task is an attempt to solve the attendant problem and make it a reality. Many applications currently in use and under development could use this specification and further entrench PostScript as the page description language of choice for Microsoft Windows. This document does not represent the last word on the topic. Interested developers are urged to respond as quickly as possible to this document and offer specific recommendations on how to improve it. Please forward any comments, suggestions, or criticisms to the following address: Kevin P. Welch, President Eikon Systems, Inc. 989 East Hillsdale Blvd., Suite 260 Foster City, CA 94404 (415) 349-4664 Figure 2: Clipboard Viewer Chain ╔══╤═══════════════════════════════╤═══╗ current ╠══╧═══════════════════════════════╧═╤═╣ clipboard viewer ║ ██████████████████████████████████ ├─╢ ┌───────────────┐ ┌────────────── ║ █████████████████████████████████ ───────► hNextWind ──────► hNextWind ║ ██████████████████████████████████ │ ║ │ │█ │ ║ █████████ Windows Kernal █████████ │ ║ │ 3 │█ │ 2 ║ ██████████████████████████████████ │ ║ └───────────────┘█ └────────────│─ ║ ██████████████████████████████████ │ ║ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀│▀ ║ ██████████████████████████████████ ├─╢ ┌────────────▼─ ╟─┬────────────────────────────────┬─┼─╢ │ hNextWind ╚═╧════════════════════════════════╧═╧═╝ │ │ 1 └────────────── ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ Figure 3: Available Functions for the Clipboard ╓┌─────────────────────────────┌─────────────────────────────────────────────╖ Function Description ChangeClipboardChain Removes a window from the clipboard viewer chain. Function Description chain. CloseClipboard Closes the clipboard. CountClipboardFormats Returns the number of available formats on clipboard. Clipboard need not be open. EmptyClipboard Empties the clipboard and reassigns clipboard ownership. Clipboard must be open. EnumClipboardFormats Enumerates the available clipboard formats. Clipboard must be open. GetClipboardData Retrieves data from the clipboard (in requested format). Clipboard must be open. GetClipboardFormatName Retrieves the ASCII name of a nonpredefined format. GetClipboardOwner Retrieves the window handle associated with Function Description GetClipboardOwner Retrieves the window handle associated with the current clipboard owner. GetClipboardViewer Retrieves the handle of the first window in the clipboard viewer chain. IsClipboardFormatAvailable Returns TRUE if the data in the given format is available on the clipboard. OpenClipboard Opens the clipboard. RegisterClipboardFormat Registers a new clipboard format. SetClipboardData Copies a data handle to the clipboard. Clipboard must be open. SetClipboardViewer Adds a window handle to the clipboard viewer chain. Figure 4: Clipboard-related Messages ╓┌────────────────────────┌──────────────────────────────────────────────────╖ Message Description WM_ASKCBFORMATNAME☼ Requests the name of the CF_OWNERDISPLAY format. WM_CHANGECBCHAIN Notifies viewer chain members of a change in the chain. WM_DESTROYCLIPBOARD Signals the clipboard owner that the clipboard contents are being destroyed. WM_DRAWCLIPBOARD Notifies an application in the viewer chain of a change in the clipboard. WM_HSCROLLCLIPBOARD☼ Requests horizontal scrolling for the CF_OWNERDISPLAY format. WM_PAINTCLIPBOARD☼ Requests painting of the Message Description WM_PAINTCLIPBOARD☼ Requests painting of the CF_OWNERDISPLAY. WM_RENDERALLFORMATS Notifies the clipboard owner that it must render all promised clipboard data. WM_RENDERFORMAT Notifies the clipboard owner to format the data copied to the clipboard. WM_SIZECLIPBOARD☼ Notifies the clipboard owner that the viewer window size has changed. WM_VSCROLLCLIPBOARD☼ Requests vertical scrolling for the CF_OWNERDISPLAY format. Figure 5: Clipboard Data Formats Format Description CF_BITMAP☼ Windows GDI bitmap CF_DIF☼ Software Arts╒ Data Interchange Format CF_METAFILEPICT☼ Windows metafile picture (METAFILEPICT) CF_OWNERDISPLAY Owner display format CF_SYLK☼ Microsoft Symbolic Link format (spreadsheet data) CF_TEXT☼ Conventional ASCII text CF_TIFF☼ Tagged Image File Format BIFF Binary Interchange File Format CSV☼ Comma Separated Variable Postscript☼ PostScript text Printer_Bitmap☼ Printer bitmap (first defined by Microsoft Excel) Printer_Picture☼ Printer metafile picture (first defined by Microsoft Excel) Rich Text☼ Rich Text Format [private formats] Other user-defined formats Figure 6: Defining Global Data Blocks /* local data */ LPSTR lpMemory; LPMETAFILEPICT lpMetaFile; /* define number of formats */ wFormats = 3; /* define text data */ Clipboard[0].wFormat = CF_TEXT; Clipboard[0].hGlobalData = GlobalAlloc( GHND, (DWORD)100 ); if ( Clipboard[0].hGlobalData ) { lpMemory = GlobalLock( Clipboard[0].hGlobalData ); lstrcpy( lpMemory, "Some sample text for the clipboard." ); GlobalUnlock( Clipboard[0].hGlobalData ); } else <...warn user - insufficient memory for copy...> /* define bitmap data */ Clipboard[1].wFormat = CF_BITMAP; Clipboard[1].hGlobalData = CreateBitmap( 50, 50, 1, 1, NULL); if ( Clipboard[1].hGlobalData ) { <...define bitmap bits...> } else <...warn user - insufficient memory for copy...> /* define metafile data */ Clipboard[2].wFormat = CF_METAFILEPICT; Clipboard[2].hGlobalData = GlobalAlloc(GHND,(DWORD)sizeof(METAFILEPICT)); if ( Clipboard[2].hGlobalData ) { lpMetaFile = (LPMETAFILEPICT)GlobalLock(Clipboard[2].hGlobalData ); lpMetaFile->mm = MM_ANISOTROPIC; lpMetaFile->xExt = 0; lpMetaFile->yExt = 0; lpMetaFile->hMF = <...define a metafile and initialize it...> GlobalUnlock( Clipboard[2].hGlobalData ); } else <...warn user - insufficient memory for copy...> Figure 7: Data Format Definitions /* define number of formats */ wFormats = 3; /* define text data */ Clipboard[0].wFormat = CF_TEXT; Clipboard[0].hGlobalData = NULL; /* define bitmap data */ Clipboard[1].wFormat = CF_BITMAP; Clipboard[1].hGlobalData = NULL; /* define metafile data */ Clipboard[2].wFormat = CF_METAFILEPICT; Clipboard[2].hGlobalData = NULL; Figure 8: Code Fragment for the Clipboard Copy Operation /* local variables */ WORD i; if ( OpenClipboard(hWnd) ) { EmptyClipboard(); for ( i=0; i<wFormats; i++ ) SetClipboardData( Clipboard[i].wFormat, Clipboard[i].hGlobalData ); } else <...warn user - unable to open clipboard...> Figure 9 LONG FAR PASCAL WndProc( HWND hWnd, WORD wMessage, WORD wParam, LONG lParam) { /* local variables */ WORD i; LPSTR lpMemory; LPMETAFILEPICT lpMetaFile; /* process messages */ switch(wMessage) { case WM_RENDERFORMAT : /* render a single data item */ /* search for entry in table - wParam = requested format */ for (i=0; (i<wFormats)&&(Clipboard[i].wFormat!=wParam);i++); if (i < wFormats) { /* define data */ switch(wParam) { case CF_TEXT : /* request for text data */ Clipboard[i].hGlobalData=GlobalAlloc(GHND, (DWORD)100); if (Clipboard[i].hGlobalData) { lpMemory = GlobalLock(Clipboard[i].hGlobalData ); lstrcpy(lpMemory, "Some sample text for the clipboard." ); GlobalUnlock(Clipboard[i].hGlobalData ); } else <...warn user - insufficient memory for copy...> break; case CF_BITMAP : /* request for bitmap data */ Clipboard[i].hGlobalData = CreateBitmap(50, 50, 1, 1, NULL ); if (Clipboard[i].hGlobalData) { <...define bitmap bits...> } else <...warn user - insufficient memory for copy...> break; case CF_METAFILEPICT : / *request for metafile data */ Clipboard[i].hGlobalData = GlobalAlloc(GHND, (DWORD)sizeof(METAFILEPICT) ); if (Clipboard[i].hGlobalData) { lpMetaFile = (LPMETAFILEPICT)GlobalLock(Clipboard[i].hGlobalData ); lpMetaFile->mm = MM_ANISOTROPIC; lpMetaFile->xExt = 0; lpMetaFile->yExt = 0; lpMetaFile->hMF = <...define a metafile and initialize it..> GlobalUnlock(Clipboard[i].hGlobalData ); } else <...warn user - insufficient memory for copy...> break; } /* copy data to the clipboard */ SetClipboardData(wParam,Clipboard[i].hGlobalData); } else <...warn user - unfulfilled request...> break; case WM_RENDERALLFORMATS:/*render all promised formats */ /* open the clipboard */ if (OpenClipboard(hWnd)) { for (i=0; i<wFormats; i++) if (Clipboard[i].hGlobalData == NULL) { /* define data */ switch(Clipboard[i].wFormat) { case CF_TEXT : /* request for text data */ Clipboard[i].hGlobalData = GlobalAlloc(GHND,(DWORD)100); if (Clipboard[i].hGlobalData) { lpMemory = GlobalLock( Clipboard[i].hGlobalData ); lstrcpy(lpMemory, "Some sample text for the clipboard." ); GlobalUnlock(Clipboard[i].hGlobalData ); } else <...warn user - insufficient memory for copy...> break; case CF_BITMAP : /*request for bitmap data*/ Clipboard[i].hGlobalData = CreateBitmap(50, 50, 1, 1, NULL); if (Clipboard[i].hGlobalData) { <...define bitmap bits...> } else <...warn user - insufficient memory for copy...> break; case CF_METAFILEPICT : /* request for metafile data */ Clipboard[i].hGlobalData = GlobalAlloc(GHND,(DWORD)sizeof(METAFILEPICT)); if (Clipboard[i].hGlobalData) { lpMetaFile =(LPMETAFILEPICT)GlobalLock( Clipboard[i].hGlobalData); lpMetaFile->mm = MM_ANISOTROPIC; lpMetaFile->xExt = 0; lpMetaFile->yExt = 0; lpMetaFile->hMF = <...define a metafile and initialize it...> GlobalUnlock(Clipboard[i].hGlobalData); } else <...warn user - insufficient memory for copy...> break; } /* copy data to the clipboard */ SetClipboardData(Clipboard[i].wFormat, Clipboard[i].hGlobalData ); } } else <...warn user - unable to open clipboard...> break; default : /* some other message - pass through */ return(DefWindowProc(hWnd, wMessage, wParam, lParam)); break; } /* normal return */ return(0L ); } Figure 10: Code Fragment of a Typical Paste Operation /* local variables */ WORD wCrntFormat; if ( OpenClipboard(hWnd) ) { /* initialization */ wFormats = 0; wCrntFormat = EnumClipboardFormats(0); /* enumerate available formats */ while ( wCrntFormat ) { /* paste format if desired */ switch( wCrntFormat ) { case CF_TEXT : /* text available - assume < 64kb */ { LPSTR lpSrce; LPSTR lpDest; HANDLE hSrceText; HANDLE hDestText; hSrceText = GetClipboardData( CF_TEXT ); if ( hSrceText ) { hDestText = GlobalAlloc( GHND, GlobalSize(hSrceText) ); if ( hDestText ) { /* duplicate text block */ lpSrce = GlobalLock( hSrceText ); lpDest = GlobalLock( hDestText ); lstrcpy( lpDest, lpSrce ); GlobalUnlock( hDestText ); GlobalUnlock( hSrceText ); /* add block to format list */ Clipboard[wFormats].wFormat = CF_TEXT; Clipboard[wFormats++].hGlobalData = hDestText; } else <...warn user - insufficient memory for paste...> } else <...warn user - unable to paste text...> } break; case CF_BITMAP : /* bitmap available */ { HDC hDC; HDC hSrceDC; HDC hDestDC; HBITMAP hSrceBitmap; HBITMAP hDestBitmap; HBITMAP hOldSrceBitmap; HBITMAP hOldDestBitmap; hSrceBitmap = GetClipboardData( CF_BITMAP ); if ( hSrceBitmap && GetObject(hSrceBitmap, sizeof(BITMAP),(LPSTR)&Bitmap) ) { /* define two compatible memory DCs */ hDC = GetDC( hWnd ); hSrceDC = CreateCompatibleDC( hDC ); hDestDC = CreateCompatibleDC( hDC ); ReleaseDC( hWnd, hDC ); if ( hSrceDC && hDestDC ) { hDestBitmap = CreateBitmapIndirect( &Bitmap ); if ( hDestBitmap ) { /* duplicate bitmap */ hOldSrceBitmap = SelectObject( hSrceDC, hSrceBitmap); hOldDestBitmap = SelectObject( hDestDC, hDestBitmap); BitBlt(hDestDC,0,0,Bitmap.bmWidth, Bitmap.bmHeight,hSrceDC,0,0,SRCCOPY); SelectObject(hDestDC, hOldDestBitmap ); SelectObject(hSrceDC, hOldSrceBitmap ); /* add bitmap to data list */ Clipboard[wFormats].wFormat = CF_BITMAP; Clipboard[wFormats++].hGlobalData = hDestBitmap; } else <...warn user - insufficient memory for paste...> } else <...warn user - unable to paste bitmap...> /* delete DCs */ if ( hDestDC ) DeleteDC( hDestDC ); if ( hSrceDC ) DeleteDC( hSrceDC ); } else <...warn user - unable to paste bitmap...> } break; case CF_METAFILEPICT : /* metafile available */ { HANDLE hSrceMF; HANDLE hDestMF; LPMETAFILEPICT lpDestMF; LPMETAFILEPICT lpSrceMF; hSrceMF = GetClipboardData( CF_METAFILEPICT ); if ( hSrceMF ) { hDestMF = GlobalAlloc( GHND, (DWORD)sizeof(METAFILEPICT) ); if ( hDestMF ) { /* duplicate metafile */ lpSrceMF = (LPMETAFILEPICT)GlobalLock( hSrceMF ); lpDestMF = (LPMETAFILEPICT)GlobalLock( hDestMF ); lpDestMF->mm = lpSrceMF->mm; lpDestMF->xExt = lpSrceMF->xExt; lpDestMF->yExt = lpSrceMF->yExt; lpDestMF->hMF =CopyMetaFile(lpSrceMF->hMF,(LPSTR)NULL); /* add block to format list */ if ( lpDestMF->hMF ) { GlobalUnlock( hDestMF ); Clipboard[wFormats].wFormat = CF_METAFILEPICT; Clipboard[wFormats++].hGlobalData = hDestMF; } else { GlobalUnlock( hDestMF ); GlobalFree( hDestMF ); <...warn user - unable to duplicate metafile...> } GlobalUnlock( hSrceMF ); } else <...warn user - insufficient memory for paste...> } else <...warn user - unable to paste metafile...> } break; default : /* something else - ignore */ break; } /* retrieve next format */ wCrntFormat = EnumClipboardFormats( wCrntFormat ); } /* cleanup */ CloseClipboard(); } else <...warn user - unable to open clipboard...> Figure 13 This code fragment represents a way of both joining a window to the clipboard chain and removing it when the window is destroyed. LONG FAR PASCAL ViewerWndProc( HWND hWnd, WORD wMessage, WORD wParam, LONG lParam ) { /* global variables */ static HWND hNextWnd; /* message processing */ switch( wMessage ) { case WM_CREATE : /* window being created */ hNextWnd = SetClipboardViewer( hWnd ); break; case WM_DRAWCLIPBOARD : /* clipboard contents changing */ if ( hNextWnd ) SendMessage( hNextWnd, wMessage, wParam, lParam ); <...do your own processing here...> break; case WM_CHANGECBCHAIN : /* viewer chain changing */ if ( hNextWnd == (HWND)wParam ) hNextWnd = LOWORD(lParam); else SendMessage( hNextWnd, wMessage, wParam, lParam ); break; case WM_DESTROY : /* window being destroyed */ ChangeClipboardChain( hWnd, hNextWnd ); PostQuitMessage( 0 ); break; default : return( DefWindowProc(hWnd,wMessage,wParam,lParam) ); break; } return( 0L ); } Figure 11: Addition to the Viewer Chain ╔══╤═══════════════════════════════╤═══╗ ╠══╧═══════════════════════════════╧═╤═╣ ║ ██████████████████████████████████ ├─╢ ┌───────────────┐ ┌────────────── ║ █████████████████████████████████ ───────► hNextWind ──────► hNextWind ║ ██████████████████████████████████ │ ║ │ │█ │ ║ █████████ Windows Kernal █████████ │ ║ │ 3 │█ │ 2 ║ ██████████████████████████████████ │ ║ └──────────────┘█ └────────────│─ ║ ██████████████████████████████████ │ ║ ▀▀▀▀▀▀▀│▀▀▀▀▀▀▀▀▀ ▀▀▀▀▀▀▀▀▀▀▀▀│▀ ║ ███████████████████████████████│██ ├─╢ ┌────────────▼─ ╟─┬────────────────────────────────┬─┼─╢ │ │ hNextWind ╚═╧══════════════════════════════│═╧═╧═╝ │ ┌──────────────│┐ │ 1 └ ─► hNextWind │█ └────────────── │ │█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ │ 4 │█ └───────────────┘█ ▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀▀ new current clipboard viewer Figure 12: Removal from the Viewer Chain ╔══╤═══════════════════════════════╤═══╗ current window ╠══╧═══════════════════════════════╧═╤═╣ clipboard viewer being removed ║ ██████████████████████████████████ ├─╢ ┌───────────────┐ ┌────────────── ║ █████████████████████████████████ ───────► hNextWind ──────► hNextWind ║ ██████████████████████████████████ │ ║ │ │█ │ ║ █████████ Windows Kernal █████████ │ ║ │ 4 │█ │ 3 ║ ██████████████████████████████████ │ ║ └────────────│──┘█ └────────────│─ ║ ██████████████████████████████████ │ ║ ▀▀▀▀▀▀▀▀▀▀▀▀ ▀▀▀▀ ▒▒▒▒▒▒▒▒▒▒▒▒│▒ ║ ██████████████████████████████████ ├─╢ │ │ ╟─┬────────────────────────────────┬─┼─╢ New link ┌────────────▼─ ╚═╧════════════════════════════════╧═╧═╝ in chain└ ─ ─ ─ ► hNextWind │ │ 2 └────────────│─ ▀▀▀▀▀▀▀▀▀▀▀▀│▀ ┌────────────▼─ │ hNextWind │ │ 1 └────────────── ▀▀▀▀▀▀▀▀▀▀▀▀▀▀ ████████████████████████████████████████████████████████████████████████████ Using Microsoft C Version 5.1 to Write Terminate-and-Stay-Resident Programs By Kaare Christian☼ TSRs have their own mystique among PC software programs. This is in part due to the fact that many of them pop up, as if by magic, at the press of a button and then vanish when no longer needed. However, the TSR mystique comes more from the macho philosophy behind them. TSR programmers have a "damn the operating system, I'll code it my way" attitude, and they take this attitude to the limit. TSR stands for terminate-and-stay resident. TSRs are programs that extend the services provided by DOS. Some TSRs work silently; they provide a useful service, but they don't need to interact directly with the user. Other TSRs are more interactive; when they sense that their hot key has been struck, they pop up onto the screen, manage a user dialogue, perform some service, and then vanish, restoring the machine to its original state. The most famous example of a TSR program is Borland International's SideKick(R). The first commercially successful TSR was RoseSoft's ProKey(TM), which was introduced in 1982, but it was SideKick that really started the TSR mania. Even today it remains one of the most important DOS software programs. What the software engineers behind these programs noticed, before most of the rest of us, is that DOS has a mechanism for growth. By executing Interrupt 27H (or DOS system call 31H) a program can exit to DOS but keep its memory image in core. (Normally, of course, when a program exits, DOS reclaims its memory, so that it can be used by the next application.) Such a program is now an extension to DOS because, like DOS, it exists to service hardware and software interrupts. The only distinction between a TSR and DOS itself is that DOS services are usually dull things like file I/O and keyboard I/O. These services are vital to the operation of the PC, but they certainly aren't glamorous enough to inspire users to reach for their checkbooks. Pop-ups such as SideKick, on the other hand, are the stuff that dreams are made of. TSRs in C TSRs are one of the last aces in the hole for the crowd that argues that assembly language is best. UNIX(R) has demonstrated that efficient operating systems could be written predominantly in a high-level language. And although the first generation of DOS apps were written primarily in assembler, current practice is to write mostly in C, with pieces in assembly language added as necessary. But in the TSR business, assembly language is still king, and there are good reasons for it to continue its reign. This article will show you how to write TSRs in C, but you should know what you will gain and what you will lose. C is not a silver bullet; even if you program in C, to write a TSR you must understand how the PC hardware, DOS, interrupts, and the stack all work. Furthermore, most published information about TSRs is written by assembly language programmers. True, you won't have to buy the assembler or learn how it works, but whether in C or assembly, writing a TSR is not like writing the Hello World program. As you might expect, TSRs written in C take more space than assembly language equivalents. There are several reasons for this. One is that compilers, even good ones such as Microsoft's, just don't generate code that is as compact as that of a first-rate human coder. For most programs the difference is insignificant, but in a TSR, every byte counts and is counted by reviewers and users. The more important reason for the extra size of C TSRs is that most C programs drag in a lot of the C library, usually more than you realize. Look at a MAP file of something simple, like Hello World, to see what I mean. Again as you might expect, C TSRs tend to be less time efficient than assembly language TSRs. This isn't usually a problem, but I am, after all, pretty firmly on the side of high-level languages for most applications. The benefit of writing TSRs in C is similar to the benefit of using C in other areas. C is easier to write, debug, and maintain than assembly language. Also, there are more good C programmers than good assembly language programmers. And many excellent programmers, though they are able to work in assembly, prefer to tap the extra productivity of C. If you are writing the next SideKick, the traditional assembly language approach is probably best, but for less ambitious projects, such as a TSR for in-house use, C should be seriously considered. We will detail the Microsoft(R) C features and techniques for writing TSRs and discuss a few of the often misunderstood but necessary C features, such as function pointers. The goal is not to provide a generic primer on TSR techniques and pitfalls, although some of that information does appear at the end of the article. A sample TSR is provided that pops up and displays the time on the screen. It is a good example because of its brevity and simplicity, but it is not a comprehensive TSR that demonstrates every possibility. Although Microsoft C possesses the basic features required for writing a TSR, it is not a convenient entry to the TSR world. If you use Microsoft C, you will need to develop your own safety strategies for dealing with the deficiencies of DOS and coexisting with other TSRs. One alternative to this is to obtain the /*resident_C*/ subroutine package from Essential Software, located in Maplewood, New Jersey ((201) 762-6965). /*resident_C*/ contains about a dozen subroutines in to facilitate writing TSRs. Although Essential Software has handled some of the details, the information presented here will still be useful even if you do look into /*resident_C*/. Traditional TSRs DOS is not known for software that plays by the rules. In more comprehensive and controlled operating systems, such as UNIX and MS(R) OS/2, programs are forced to work within the model provided by the system. But almost all PC programs play it fast and loose, ignoring DOS except when they want to access files. The structure of a normal DOS application is that it is loaded into memory by DOS, and then DOS transfers control to it. Once the application starts to run, it works under the assumption that it has rights to the machine. It just waits around idly if there is nothing to do and continues to run until it decides to exit. It only relinquishes control to DOS when there is something it wants DOS to do, such as read or write a chunk of a file. The entire structure and set of assumptions is different for a TSR. In the beginning the TSR and normal applications are the same. Like an ordinary application, the TSR is loaded by DOS, and then DOS transfers control to the TSR. The TSR then initializes itself and permanently relinquishes control to DOS, but in such a way that its load image is preserved-the program continues to be resident in memory. It is at this point that the magic begins. The TSR is now like a voyeur at the grand ball; it watches what is going on, but must not do anything to call attention to itself. However, if its services are later requested, it springs into action, confidently performs its task, and then slips back into the shadows to wait for another request. Interrupts Even novice PC users quickly learn about two kinds of interrupts, hardware and software. Hardware interrupts occur when a device asserts its interrupt line. Each hardware device is programmed to request interrupts at certain times, and handlers must exist to service the interrupts. Software interrupts occur when the INT instruction is executed. In either case, when an interrupt occurs, control passes to an interrupt handler, whose address is stored in a table in low memory on the computer. The interrupt handler routine does whatever is necessary and then issues an IRET instruction that returns control to whatever routine was active when the interrupt occurred. Interrupt handlers for hardware interrupts are generally forbidden to alter aspects of a machine's state, such as its register contents, stack, or screen contents. Obviously, though, some aspect of the machine state is changed by a hardware interrupt. There are specific variables or locations in memory that are reserved for communication between interrupt handlers and ordinary software, which the interrupt handlers can change. For example, the timer interrupt alters the clock ticks variable. It would be intolerable for the timer tick handler to change the AX register, because that would make most software appear to fail randomly. Interrupt handlers for software interrupts are called explicitly and often return results by modifying registers. For example, the AX register is modified by most DOS calls. This behavior is allowable and expected. However, when a TSR intercepts a software interrupt, it should be careful not to alter any registers. In assembly language it is relatively easy to create a routine that doesn't change any registers. The key is to note which registers are used by the routine so that they can be PUSHed on entry and POPed before exit. C routines, however, aren't so scrupulous about saving registers. In a normal Microsoft C subroutine, the AX and DX registers can return values, the flags register can be altered, the ES register can access FAR data, and so on. Actually, all that is required is that a routine must save SI, DI, DS, SS, BP, and SP. This behavior is permissible because the C compiler generates code that respects these conventions. Microsoft C has a little-known and not thoroughly documented function declaration keyword called interrupt. When a function is declared as interrupt, then additional register saving may occur, and the return instruction will be IRET instead of RET. Another feature of an interrupt function is that you can declare function parameters that correspond to the registers. This lets you use the value of a register, or assign a value to a register. Figure 1 shows a C language interrupt routine and the corresponding assembly code that is emitted by the Microsoft C compiler. Note that interrupt routines must always be declared far, because both hardware and software interrupt handlers are called by reloading both CS and IP. An interrupt procedure can be called as if it were an ordinary procedure. The compiler is able to deduce that an interrupt procedure should be called by pushing flags, CS, and then IP. On exit, these items are popped off the stack by the IRET instruction. However, the compiler does not realize that an interrupt procedure, even though it may declare 13 formal parameters, should be called without any actual parameters. Using the old-style K&R declaration [Brian W. Kernighan and Dennis M. Ritchie, The C Programming Language (Prentice-Hall, Inc., 1978)] shown in Figure 1, the compiler will accept any number of actual parameters. If you instead use the new ANSI-style declaration list, such as the following: void interrupt far intfarfn(unsigned es, unsigned ds, unsigned di, unsigned si, unsigned bp, unsigned sp, unsigned bx, unsigned dx, unsigned cx, unsigned ax, unsigned ip, unsigned cs, unsigned flags) { } the compiler will expect you to supply exactly 13 parameters. In either case, any parameters that you mistakenly supply will be ignored. You must be very careful to supply the register parameter list in the correct order. The C compiler doesn't match things by name, instead the first parameter is ES, the second is DS, and so on. For example, if you need to access DI but you don't care about ES or DS, you could declare the function as: void interrupt far intfarfn (unsignedjunk1, junk2, di) { } The junk1 and junk2 parameters are necessary to put di in its proper place in the parameter list. Hooking Interrupt Vectors When a TSR first starts to operate, it is a normal DOS task and has control of the machine. It can do whatever it wants, within the thin framework of DOS. However, once it terminates, the only way that it will execute is in response to interrupts. Thus, before exiting, every TSR must copy a far pointer to one of its functions into one of the slots in the interrupt table. Then when the interrupt occurs, the given function will be activated. In most situations a further requirement is that the function must also pass control to the original handler for the interrupt; this operation is called hooking a vector. Most TSRs hook multiple vectors; for example, they may hook the keyboard interrupt so that they can look for their hot key and hook the DOS interrupt so that they can keep track of which DOS calls are in progress. Hooking vectors isn't limited to TSRs, though; ordinary programs also use the facility. For example, communications programs often hook the serial data interrupts, and many programs hook both the critical error interrupt and the Ctrl-Break interrupt. Microsoft C contains two functions that make it easy to hook an interrupt vector, _dos_getvect and _dos_setvect. The _dos_getvect function, which reads a vector, is usually used first. Its parameter indicates which vector is to be read, and its return value, a far pointer to a function, is usually stored for later use. The _dos_setvect function takes two parameters: the first indicates a specific vector, and the second is a far pointer to a function that will be the new value for the given vector. The use of _dos_getvect and _dos_setvect is shown in the main routine of Figure 2. Notice that the program is not a TSR; it is an ordinary program that spies on the DOS interrupt. The program is careful to restore the vector before exiting; if this were not done, the machine would crash as soon as the next application was loaded into memory. Declaring a variable that can store a far pointer to an interrupt function is a bit tricky. That C allows such a declaration is one of its admirable strengths, but most agree that the notation is structured poorly. The two declarations at the top of Figure 2 are: void far interrupt newint21; /* DOS /* void (interrupt far *oldint21); The first declares that newint21 is a far interrupt function that doesn't return a value. This declaration doesn't set aside any storage; it just tells the compiler about newint21, so that the compiler can generate correct code. The second declaration looks similar but is actually very different. It creates a variable called oldint21 and states that oldint21 holds a far pointer to an interrupt function that does not return a value. Both sets of parentheses in the oldint21 declaration are necessary. The first set makes "interrupt far *" pertain to oldint21, and the second (empty) set makes it a pointer to a function. C declarations are read from the inside out: the asterisk (*) is read as "pointer to," the parentheses (( )) are read as "function returning," and the brackets ([]) are read as "array of." Also, items to the right of an identifier (parentheses and array brackets) bind tighter than items to the left (the indirection operator and keywords such as far, near, and interrupt). The declaration of oldint21 is read "oldint21 is an interrupt far pointer to a function returning a void." That's just what we want. When a TSR hooks a vector, one of three possible interactions with the original handler for that vector may occur: ■ ignore the original handler ■ CALL the original handler ■ chain to the original handler TSRs often decide which of these approaches to take based on the current situation. For example, a routine waiting for a specific hot key might chain to the original keyboard handler each time it sees a keystroke other than its hot key, but CALL the original handler when it detects its hot key. The first option, ignoring the original handler, is only possible in a few situations. Typically the original handler must be invoked in order for the machine to operate correctly. The second possibility, CALLing the original handler, permits a TSR to gain control after the original handler has completed operation. This is not very common, but it does occur. You can call a function whose address has been stored in a function pointer such as oldint21 as follows: (*oldint21)(); Both sets of parentheses are necessary. Without the seemingly superfluous first set of parentheses, oldint21 would look like a function, instead of a pointer to a function. It is critical that oldint21 be declared as a far pointer to an interrupt function, instead of a plain far pointer to a function, because different calling conventions must be used to call interrupt functions. The third option, chaining to the original handler, is very common. The idea is that the TSR handler JMPs to the original handler. Before the JMP the TSR must restore any registers that it modified and ensure that the stack pointer and stack segment register are exactly what they were when the TSR handler was activated. In an assembly language program, chaining is not hard to accomplish, given a clear understanding of the machine's register state. In C it is necessary to use the _chain_intr procedure call, a function that should only be called from within an interrupt procedure. This is because it understands how the stack has been managed by an interrupt procedure and uses that knowledge to unravel the stack and then jump to the specified far location. When the chained-to routine starts to execute, the register contents and stack layout are exactly as they would have been if it had been called directly. A rudimentary example is shown in the newint21 function of Figure 2. When the program in Figure 2 is executed, it usually prints the message "1 syscalls" to indicate exactly one DOS Interrupt 21H system call has been executed. Of course, that system call is the getch call contained in the function itself. If while the program is waiting, you run a pop-up that executes additional system calls, its count will be displayed when the program exits. Stack Considerations For the most part, C programmers don't have to worry much about the stack. It does help to keep in mind that, on a hardware architecture like that of the PC, C auto variables are stored on the stack. A good general rule in C is that you shouldn't allocate huge data structures inside a function. (One common mistake by novices is to declare a program's major data structure in the main routine.) If the stack is too small, you simply recompile your program to use a larger stack. Recursive programs usually need a larger stack than the 2Kb default. When an ordinary EXE program is executing, it is using its own stack, but when a DOS system call is invoked, DOS switches to its own stack. Actually there are three DOS stacks, one for system calls 0 through 0CH, another for functions above 0CH, and a third for use during critical errors. A TSR has two choices for stacks: it can hope that the active stack at the time it is called has a few extra bytes or it can switch to its own stack. Some assembly language TSRs switch just to be safe. There are two very important things to remember if you choose the easy way, using the current stack. The first is that you must ensure that stack checking is turned off in your software by using the -Gs compiler option and that you don't invoke any library routines that use stack checking. Some library routines do, some don't. When you compile, use the -Fm compiler option to generate a MAP file, and examine that file for references to chkstk. If you find any such references, systematically eliminate library routines until you have excised chkstk from your program. The second thing to remember is to be extremely careful in your use of stack space. Making local variables static (instead of the default, auto) will avoid using the stack for local variables. Remember that static locals persist and must be initialized explicitly each time a routine starts; you can't rely on the initialization in a declaration for any function activation except the first. To minimize stack use, don't use recursion, don't allocate more than a handful of variables on the stack, and don't deeply nest your procedures. If your TSR treads lightly on the stack, you probably won't need to switch stacks. Another difficulty caused by the stack dilemma is that DS and SS may not be the same while a TSR is executing. You may want to use the -Aw compiler option so that the compiler will generate code that does not assume DS and SS are the same. If you do specify -Aw, you'll also need to specify -Asn to create a small-model program. Usually the compiler doesn't create code that relies on DS==SS, so you can often get away with omitting -Awsn. Because of this problem, you should avoid taking the address of auto local variables. As mentioned previously, local variables themselves should be avoided in TSRs to save wear and tear on the stack, so refraining from taking their addresses is not much more of a sacrifice. A few library routines contain code that relies on the DS==SS assumption, but the only way to find these routines is by trial and error. If a library routine doesn't work correctly when used in a TSR, but does work in the same situation when used in a normal program, then the DS==SS assumption is probably the culprit. Library Routines One of the best features of Microsoft C is its library. In previous versions of Microsoft C the library contained extensive I/O routines, a versatile math library, routines for manipulating strings, dynamic memory allocation functions, a generic OS interface, and a few PC-specific routines. Versions 5.0 and later add graphics and more complete PC- specific routines. Much of the Microsoft C library cannot be used in a TSR. Some routines cause problems because they directly or indirectly call malloc. When a program is running, malloc manages a memory pool that it parcels out upon request. But that memory is not available to a TSR, and any use of malloc, or its variants, is forbidden in a TSR. Some routines use the stack too much, while other routines call chkstk, which will usually fail because TSR stacks are not where chkstk expects them to be. The routines that are definitely safe are the ones you can duplicate fairly easily. One possibility is purchasing the Microsoft C library source code, so that you have a more detailed guide to what happens in each function. Owning the source also lets you recompile a routine so that it won't do any stack checking; although Microsoft routines that have stack checking on often have it on for a good reason, they need more than a couple of bytes on the stack. (Another way to disable chkstk is to modify CHKSTK.ASM, which is supplied on the Microsoft C Utilities disk.) Program Size Although Microsoft C provides a convenient subroutine, _dos_keep, to transform the active program into a background program, there is one small hitch. You must tell _dos_keep how many 16-byte paragraphs to keep. One way to figure it out is to cheat, either by looking at the MAP file or by asking to keep more than you could possibly need. A better way is to figure it out exactly within your program. MS-DOS(R) C programs have the following general layout: ■ program segment prefix ■ text ■ data ■ stack For our current purposes, the size of the stack segment is irrelevant because it is not used after the program terminates but stays resident. The task is to find the size of the other parts. The program segment prefix (PSP) is easy; it's always 256 bytes (100H). To refine a method for automatically determining the size of the text and data segments, I wrote the LOCS.C program, which is shown in Figure 3. Its key is a pair of huge pointers, startofitall and endofitall. I set startofitall to point at the start of the PSP and endofitall to point at the end of the data segment. Finding the start is easy; the variable _psp always contains the segment address of the start of the PSP, and the offset of the start address is always 0. The end of the data segment is located by taking the address of a variable called end, which is always the last datum in the data segment. Subtracting the huge pointers (they must be huge not far pointers because their segments are different) reveals the program size, 8064 bytes in this case. Figure 4 shows an example of the output from the LOCS program. To verify LOCS.C's results, I consulted its MAP file. It showed that the first text address was 0000:0000H, and the last data address was 016D:07B0H, which converts to 1E80H. (To make the segment:offset to bytes conversion, shift segment left 4 and add to offset. 016DH shifted left 4 is 016D0H; that plus 07B0H is 1E80H.) In decimal, 1E80H bytes is 7808, and 7808 plus 256 for the size of the program segment prefix yields 8064, exactly the size reported by the LOCS program. The only subtlety in the LOCS program is converting bytes to paragraphs. Remember to round up; for example, 10 bytes is 1 paragraph, 17 bytes is two paragraphs, and so on. The simple logic at the end of LOCS rounds up (if necessary) and then converts to bytes by shifting right 4 (dividing by 16). Terminating and Staying Resident Figure 5 shows a simple TSR that will pop up the time whenever you strike its hot key. This simple routine only intercepts the keyboard interrupt. When it detects its hot key, it pops up the time of day on the screen. In this sample TSR, all of the work is done with BIOS calls and there is no file I/O. Many useful TSRs are this easy, but others require more extensive use of DOS. The main routine in Figure 5 contains a blend of the code from Figure 2 and 3. At the beginning of the routine, the old interrupt 9 handler's location is saved, and a new interrupt 9 handler is installed. Next there is the location arithmetic that calculates how much space to save when _dos_keep is called. Following that, is the actual call to _dos_keep, which makes the program resident. _dos_keep doesn't restore the machine state to exactly what it would be if exit, the normal way out of a C program, were called. One difference is obviously that the memory for the program text and data is preserved. Another difference is that several vectors relating to floating point operations that were taken over by the C startup routines aren't restored by _dos_keep as they would be by exit. The interrupt hooking and chaining occurs in the newint9 function. Each time a keyboard interrupt occurs, newint9 is activated. As soon as it is sure that the hot key has not been struck, it chains to the original keyboard interrupt handler, but when it detects the hot key, it CALLs the original handler. This allows the original handler to retrieve the keystroke normally and to perform any of its routine processing. However, as soon as the normal keyboard handler returns, newint9 calls popup, which reads in the hot key, displays the time message, waits for another keystroke, and then finally restores the original screen. The newint9 procedure maintains a flag variable called active that is set to true before popup is called and then cleared afterward. This prevents newint9 from calling popup a second time, if the hot key is detected while popup is active. This isn't farfetched; it would occur if the user struck the hot key once to pop up the time message, and then struck it again to take down the message. Figures 6 and 7 show a file called tsrbios.c, which contains several BIOS video support subroutines that will display the date string and then restore the original screen, and TSRBIOS.H, which contains the function prototypes for TSRBIOS.C. These routines work via the BIOS for simplicity and universality, and for the short message displayed in this example the speed penalty of using the BIOS video routines is irrelevant. However, the approach taken here will not mesh smoothly with programs that bypass the BIOS cursor services, such as Lotus 1-2-3(R), because these routines assume that the cursor position is truly represented by the BIOS's cursor position variable. The only difficulty in these BIOS video routines is preserving the cursor location. This is done by picking up its value from the BIOS data area using the memory address returned by scr_get_curs_addr. Advanced TSRs TSRs that read or write disk files, list directories, or perform other tasks using DOS must be more sophisticated than the simple TSR discussed here. Remember that DOS is not a multitasking system. When a request comes in, DOS automatically switches to one of its internal stacks. Because a normal application program is inactive while DOS is servicing one of its requests, it is impossible in the absence of TSRs for another request to come in while the first is being serviced. However, when there are TSRs lurking in the shadows, it is possible for one request to occur while another is being serviced. Unfortunately, DOS is coded in such a way that it will crash or misbehave whenever a new request comes in while another is underway. It isn't hard to design a more flexible system call scheme, but it didn't seem very important way back when DOS was being developed. Because of this limitation, DOS maintains a busy flag. A program can determine the location of the flag by executing DOS system call 34H. The far pointer result is returned in ES:BX. While DOS is servicing a request, it sets the busy flag. That doesn't mean that DOS is smart enough to avoid problems by noticing that it is busy, but it does mean that it is possible for a TSR to notice that DOS is busy and avoid making a deadly request. Unfortunately, the DOS busy flag is set during certain times so that it is possible to execute most DOS file I/O calls. For example, when DOS is waiting for you to enter a keystroke, it could accept a file I/O request. One way to detect this situation is to hook the Interrupt 21H vector and keep track of exactly which system call is in progress. Then the TSR could, if it did notice that a system call was in progress, decide if it was in fact safe to do file I/O. The easiest solution is to avoid all DOS calls when a call above 0CH is in progress, but allow calls above 0CH when a call below 0CH is in progress. Many TSRs also use the status of Interrupt 13H, the BIOS I/O routines, to help determine when it is safe to make file I/O requests. Another way to solve this problem is to employ a resource provided by DOS, Interrupt 28H. When the DOS busy flag is set, but functions above 0CH are safe, DOS periodically activates Interrupt 28H. The original use of Interrupt 28H was for the resident part of PRINT.COM, but it is a general mechanism now used by many TSRs. In fact, some TSRs even activate Interrupt 28H when they ascertain that it is safe for other TSRs to pop up over them. Many TSRs also use the periodic 18 Hz interrupt. When their hot key is detected, the routine that hooks the keyboard interrupt sets a flag variable indicating that a pop-up request has been issued. A short time later the periodic timer interrupt will occur. If DOS is idle, the routine that hooks the timer interrupt will call the pop-up routine; otherwise, the timer handler will try again next time. This is often combined with a handler that responds to Interrupt 28H. Whichever handler gets there first (and safely) will activate the pop-up routine, and then cancel the pop-up request flag. Two further considerations are how to handle the keyboard interrupt and the critical error interrupt. It shouldn't be possible to break out of a TSR by striking Ctrl-Break-havoc will ensue. Similarly a robust pop-up should hook the critical error interrupt, Interrupt 24H, while it is doing file I/O. If it fails to do this, a critical error (those pesky "Abort, Retry, Ignore?" messages) may have unforeseen consequences. Both of these interrupts should only be hooked while a pop-up is popped up and then restored when the pop-up returns to the shadows. Another complication for many TSRs is detecting whether they are already loaded. It is very undesirable to load a TSR twice, but the hidden, background nature of TSRs often leads people to try to load them a second time when they have difficulties after the first load. There are several ways for a TSR to detect that it is already loaded. One simple way is to search memory (from 0 to the start of its PSP) for something distinctive, such as its copyright notice. If the notice is found, the TSR must already be loaded. Another technique is to use interrupts; for example, you can add a new feature to an existing interrupt. Before terminating, the TSR executes that interrupt with the special request code. If the TSR is already loaded, it will return a special code. If the TSR is not loaded, the ordinary BIOS handler will get control and do something different (usually it will do nothing). Conclusion Whatever language you use, the difficulty of writing a TSR lies primarily in DOS's inadequacies, not in the language. Assembly language programmers have a head start, because the same tricks and techniques that they use all the time are applicable to TSRs. Many of the Microsoft C features that you need to write a TSR are unfamiliar because they are rarely used for other types of programming. However, writing a TSR in C is fairly simple once you understand the Microsoft C tools and the basic tricks of the TSR trade. The bottom line is this-for straightforward TSRs use whichever language, C or assembly, is most familiar. For many programmers, that means C. But for the most demanding TSRs, assembly language is still the way to go. Figure 1: Sample C Interrupt Routine and Resulting Assembler Code A C Interrupt Function void interrupt far intfarfn(es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags) unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags; { } The Assembler Code Emitted by the C Compiler PUBLIC _intfarfn _intfarfn PROC FAR push ax push cx push dx push bx push sp push bp push si push di push ds push es mov ax,SEG _DATA mov ds,ax cld ; es = 0 ; ds = 2 ; di = 4 ; si = 6 ; bp = 8 ; sp = 10 ; bx = 12 ; dx = 14 ; cx = 16 ; ax = 18 ; ip = 20 ; cs = 22 ; flags = 24 pop es pop ds pop di pop si pop bp pop bx pop bx pop dx pop cx pop ax iret _intfarfn ENDP Figure 2: A simple program that hooks a vector and then chains to the original handler. #include <dos.h> #define DOS_INT 0x21 void far interrupt newint21(); /* DOS */ void (interrupt far * oldin21)(); unsigned long syscalls = 0; void interrupt far newint21(es, ds, di, si, bp, sp, bx, dx, cx, ax,ip, cs, flags) unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags; { syscalls++; _chain_intr(oldint21); } main() { /* save old int21 vector */ oldint21 = _dos_getvect(DOS_INT); _dos_setvect(DOS_INT, newint21); getch(); printf("%lu syscalls\n", syscalls); /* restore old int21 vector */ _dos_setvect(DOS_INT, oldint21); } Figure 3: The LOCS.C program, which shows how to determine the size of a program. #include <dos.h> extern unsigned int _psp, end; extern unsigned char _osmajor; main() { char huge *startofitall; char huge *endofitall; unsigned blength; /* byte length: psp+text+data */ unsigned plength; /* paragraph length */ printf("Dos version %d\n", _osmajor); FP_SEG(startofitall) = _psp; FP_OFF(startofitall) = 0; endofitall = (char huge *)&end; blength = endofitall - startofitall; /* bytes */ plength = blength; if (plength & 0xf) /* round up to next 16 byte para */ plength += 0x10; plength >>= 4; /* convert to paragraphs */ printf("start %Fp, end %Fp, size %u (bytes) %u (paragraphs)\n", startofitall, endofitall, blength, plength); } Figure 4: Sample LOCS Output Dos version 3 start 0C76:0000, end 0DF3;07B0, size 8064 (bytes) 504 (paragraphs) Figure 5: A TSR to Pop Up the Time */ * tsr to pop up the time */ #include "tsrbios.h" #include <dos.h> #include <bios.h> extern int _psp, end; extern unsigned _osmajor; extern union REGS regs; #define HOTKEY_CODE 0x11 /* W key */ #define HOTKEY_STAT 0x4 /* ctrl */ #define KEYPORT 0x60 /* keyboard i/o port */ #define DOS_INT 0x21 #define KEYSVC_INT 0x9 #define DOS_PRINTSTRING 0x9 #define StartOfTime 6 /* 6 characters into msg string */ char msg[25] = "**** HH:MM:SS ****"; char buf[25]; /* variables for time calculations */ unsigned long ticks; int h,m,s; char *sptr; void (interrupt far * oldint9)(); /* look for the hot key, and call popup when you see it */ void interrupt far newint9(es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags) unsigned es, ds, di, si, bp, sp, bx, dx, cx, ax, ip, cs, flags; { static unsigned char keycode; static char active = 0; if (active) _chain_intr(oldint9); keycode = inp(KEYPORT); if (keycode != HOTKEY_CODE) _chain_intr(oldint9); if (_bios_keybrd(_KEYBRD_SHIFTSTATUS) & HOTKEY_STAT) { /* got it */ active = 1; (*oldint9)(); /*let normal routine process hot key*/ popup(); active = 0; } else _chain_intr(oldint9); } main() { char huge *startofitall; char huge *endofitall; unsigned length; /* check dos ver */ if (_osmajor < 2) { dos_puts("Not loaded, requires dos v2 or over.\r\n$"); exit(); } /* save old int9 */ oldint9 = _dos_getvect(KEYSVC_INT); _dos_setvect(KEYSVC_INT, (void (interrupt far *) ())newint9); /* some location work */ FP_SEG(startofitall) = _psp; FP_OFF(startofitall) = 0; endofitall = (char huge *)&end; length = endofitall - startofitall; /* bytes */ if (length & 0xf) /* round up to next 16 byte para */ length += 0x10; length >>= 4; /* convert to paragraphs */ dos_puts("Loaded ok\r\n$"); _dos_keep(0,length); /*discards stack, keeps all else*/ } /* print a $ terminated string */ dos_puts(s) char *s; { regs.x.dx = (int)s; regs.h.ah = DOS_PRINTSTRING; intdos(&regs,&regs); } popup() { static int oldcursor; /* swallow hot key keystroke */ (void)_bios_keybrd(_KEYBRD_READ); /* save cursor */ oldcursor = *scr_get_curs_addr(); /* put time into message */ bigben(); /* popup message */ scr_row = 12; scr_col = 30; scr_swapmsg(msg,buf); /* wait for keystroke */ (void)_bios_keybrd(_KEYBRD_READ); /* erase message */ scr_row = 12; scr_col = 30; scr_swapmsg(buf,msg); /* restore cursor */ *scr_get_curs_addr() = oldcursor; } */ * Read ticks, convert to cumulative seconds, and then fill msg. * cum_secs = ticks/18.2065 * (but I don't want to use floating point) * or, cum_secs = (ticks * 10,000) / 182065 * (but that would overflow in a long) * or, cum_secs = (ticks * 2,000) / 36413 */ bigben() { _bios_timeofday(_TIME_GETCLOCK, &ticks); ticks *= 2000L; /* convert ... */ ticks /= 36413L; /* ... to seconds */ h = ticks / 3600; m = ticks / 60; m = m % 60; s = ticks % 60; sptr = msg + StartOfTime; *sptr++ = h/10 + '0'; *sptr++ = h%10 + '0'; *sptr++ = ':'; *sptr++ = m/10 + '0'; *sptr++ = m%10 + '0'; *sptr++ = ':'; *sptr++ = s/10 + '0'; *sptr++ = s%10 + '0'; } Figure 6: Simple Video BIOS Routines tsrbios.c - Simple video bios routines */ * video BIOS routines for tsrs */ #include "tsrbios.h" #include <dos.h> #define BIOS_VIDEOINT 0x10 #define BIOS_VIDEOINT_RDCH 0x8 #define BIOS_VIDEOINT_WRCH 0x9 #define BIOS_CURSOR_ARRAY (int far *)(0x450) #define BIOS_VIDEO_PAGE_NUM (char far *)(0x462) union REGS regs; int scr_row, scr_col; /* used throughout to denote screen row, col */ static int far *scr_cursaddr; static unsigned scr_ch; */ * write msg onto screen, while saving screen contents into savbuf * ( scr_row, scr_col must be set before calling scr_swapmsg() ) */ scr_swapmsg(msg, savbuf) unsigned char *msg, *savbuf; { static unsigned val; scr_cursaddr = scr_get_curs_addr(); while(*msg) { *savbuf++ = scr_ch = scr_read_ch(); scr_ch &= 0xff00; /* preserve attribute */ scr_ch |= *msg++; scr_write_ch(); scr_col++; } *savbuf = 0; } */ * read the screen character addressed by scr_row, scr_col into * scr_ch */ unsigned scr_read_ch() { /* poke cursor val into bios loc */ *scr_cursaddr = scr_row<<8 | scr_col; /* use bios int to get char */ regs.h.ah = BIOS_VIDEOINT_RDCH; regs.h.bh = *BIOS_VIDEO_PAGE_NUM; int86(BIOS_VIDEOINT, &regs, &regs); return regs.x.ax; } */ * write scr_ch into the screen location addressed by scr_row, * scr_col */ void scr_write_ch() { /* poke cursor val into bios loc */ *scr_cursaddr = scr_row<<8 | scr_col; /* use bios int to get char */ regs.h.ah = BIOS_VIDEOINT_WRCH; regs.h.al = scr_ch; /* char */ regs.x.cx = 1; /* one char */ regs.h.bh = *BIOS_VIDEO_PAGE_NUM; regs.h.bl = scr_ch>>8; /* attr */ int86(BIOS_VIDEOINT, &regs, &regs); } */ * put the cursor address of the current page into the scr_cursaddr * pointer */ static int far * scr_get_curs_addr() { return BIOS_CURSOR_ARRAY + (int)(*BIOS_VIDEO_PAGE_NUM); } Figure 7: Function and Global Data Declarations for the TSRBIOS.C Routines */ * external declarations for the tsrbios subroutines */ extern int scr_row, scr_col; extern int scr_swapmsg(unsigned char *, unsigned char *); extern unsigned scr_read_ch(); extern void scr_write_ch(); extern int far * scr_get_curs_addr(void); ████████████████████████████████████████████████████████████████████████████ Customizing the Features of the M Editor Using Macros and C Extensions ─────────────────────────────────────────────────────────────────────────── Also see the related article: C 5.2 Library Routines and Editor Extensions ─────────────────────────────────────────────────────────────────────────── Leo A. Notenboom The Microsoft(R) Editor is a powerful text editor that runs under MS-DOS(R) and the OS/2 systems. Its features include the ability to edit more than one file at a time, split the screen into multiple windows, and run tools, including Microsoft compilers, from within the editor. This article will look at the fundamental concepts and methods for customizing and extending the functionality of the Microsoft Editor. Familiarity with the basic operation of the Microsoft Editor is assumed. One of the most important features of current text editors is programmability and customization. Microsoft Editor supports three types of programmability: the definition of the meanings of individual keystrokes, the definition of macros, and the creation of editor extension functions. We will examine each of these, with emphasis on and examples of the latter two. The Keyboard Programming the keyboard in Microsoft Editor is the process of associating or "assigning" editing functions to keystrokes. When a keystroke is pressed, the function assigned to it is then executed. Every editing function that Microsoft Editor can perform has a name associated with it, which is used to assign it to one or more keystrokes or execute the function in macros. Any editing function can be assigned to any keystroke that Microsoft Editor recognizes. The editor assigns a default function to every valid keystroke. It assigns the graphic function to all keys that you use to type in text, such as alphanumeric keys, the unassigned function to all unused keys, and other editing functions to the remaining keys. For example, the exit function is assigned to F8. Programming the keyboard simply means making new key assignments. For example, you can assign the exit function to Alt-X. To do this you place the following in TOOLS.INI, the initialization file (M is used here to reference the TOOLS.INI section for editor defaults. In practice the section name is that of the actual editor filename and would be [MEP] if MEP.EXE were used. [M MEP] could be used to define a single section for both.): [M] exit:alt+x Any number of such assignments may be contained in the [M] section of TOOLS.INI. You can also use the assign function by entering the following commands: arg exit:alt+x assign Using default key assignments, this means you would press Alt-A to execute the arg function, type "exit:alt+x", and press Alt-equal sign to execute the assign function. The Microsoft Editor User's Guide has more details on TOOLS.INI and the assign function, and I'll refer to them throughout the article. The same function can be assigned to more than one keystroke. For example, the sdelete (stream delete) function can be assigned to both Del and Ctrl-G: [M] sdelete:del sdelete:ctrl+g Macros and extension functions, as you'll see, are treated just like standard editing functions in that they can also be assigned to any keystroke. Programming Macros A macro is a sequence of editing functions and text, which is treated as a single editing function. The editing functions used can be standard functions, extension functions, or previously defined macros. Macros, unlike extension functions, do not add functionality to the editor. Instead, they enable you to combine existing editing functions to perform larger and more complex tasks easily or reduce the number of keystrokes to perform common tasks. Figure 1 lists Microsoft Editor's standard editing functions. Macros can be defined "on the fly" in an editing session by using the assign function. It is more common, however, to store macros in TOOLS.INI. You define a macro by stating its name and its function sequence in the following format: name:=function-sequence where each item in "function-sequence" is either a function or macro name or a string of text enclosed in quotes. Note the use of the equal sign, which differentiates a macro definition from a keystroke assignment. A macro to save the current file without exiting might be defined as: savefile:=arg arg setfile This example defines the macro savefile to be the sequence of editing functions arg arg setfile, which saves the current file without exiting. To assign Shift-F2 to the savefile function, you would make the following assignment: savefile:shift+f2 Now whenever Shift-F2 is pressed the current file will be saved to disk. The default keystrokes for this operation (without using the macro) are Alt-A, Alt-A, and F2. A macro is in fact a new editing function. The name of the macro can be used anywhere that the name of an editing function can be used. It can be used in the function-sequence of another macro, or assigned to a keystroke, as shown above. Arguments to Macros Macros have no explicit syntax for accepting arguments. However, any argument that has been selected at the time the macro is executed is passed to the first function of the macro that takes an argument. One way for a macro to accept an argument is to begin with the copy function, which copies its argument to the clipboard. After using copy, you can then manipulate this text by loading a special file named <clipboard>. The file <clipboard> is a special type of file known as a pseudo-file. These files function as regular text files, except that they cannot be saved to disk without an explicit filename. Pseudo-files are defined by filenames that are enclosed in angle brackets (<>) and can be employed as temporary scratch buffers during editing. Some pseudo-files are created automatically by the editor and have special meanings. The pseudo-file <clipboard> is written to whenever you use the copy function or delete a block of text. Defining Macros As an example of defining and using a simple macro, we'll create a print function, which sends a selected region of text to the printer. The copy function will capture the text to be printed so the macro's syntax is as shown in Figure 2. The macro will execute the following steps: ■ copy the selected text to the clipboard ■ save the clipboard to a temporary file on disk ■ send the temporary file to the printer ■ delete the temporary file This macro will be defined in pieces by defining four smaller macros that correspond to the steps above. Writing larger macros as sequences of smaller macros improves readability and makes the parts of one macro available for use in other macros. The first step is easy; you just execute the copy function to place the selected text into the clipboard. Then the setfile function will change the current file to <clipboard>. print1:=copy arg "<clipboard>" setfile% To perform the second step, you use setfile again to save the current file, <clipboard>, to a temporary file. Then you use setfile a third time to restore the original current file: print2:=arg arg "TEMP.DAT" setfile setfile Note that even though you have changed the current file twice in these first two steps, the screen is not updated until input is required of the user. The user does not see the screen switch to the clipboard and back, as he would if the functions were executed individually from the keyboard. The third step uses the shell function to execute a DOS command to print the file: print3:=arg "PRINT TEMP.DAT" shell The last step is to use shell again to delete the file: print4:=arg "DEL TEMP.DAT" shell% Now you put the pieces together. The completed macro looks like this: print:=print1 print2 print3 print4 Finally, in order to use the macro, you assign it to a key: print:alt+p Now whenever Alt-P is pressed, portions of the current file are printed, based on the type of argument specified beforehand. Flow Control in Macros The editor's macro language supports conditional and unconditional branching. Every function in the editor has a TRUE or FALSE return value that can be used for branching. As a simple example, we'll create a new macro, bigprint, which does everything the print macro does and also recognizes the following syntax: meta bigprint - print the entire file Since the use of meta does not fit into the method of using the copy function, as we did in the print macro, you'll have to check first for meta and act a little differently if it is on. You can just select the entire file for copy and execute the print macro. Note that once you've selected the entire file, you simply use the print macro to print the file. Let's assume that we have already defined a macro called selectall, which selects the entire file in preparation for the copy function that starts the print macro. The bigprint macro then looks like this : bigprint:=meta +>nometa cancel selectall meta :>nometa meta print Step by step, here's what this macro does. The meta function toggles between on and off; it has a return value to indicate the new state. Thus you start the macro with meta to test its previous value. Note that this action does not affect the selected argument. +> is the macro-language "branch if TRUE" statement. This step causes the macro to branch to the label nometa if the previous function returned TRUE. In this example, the previous function, meta, returns TRUE if meta is now on, which would mean meta was off when the macro was entered. If meta was off when we entered, then the user did not specify meta, and we would branch around the next step. Then, cancel eliminates any argument that the user may have selected. Since we are about to select the entire file, any user-selected argument would be inappropriate. Next, selectall is reached only if the initial meta function returned FALSE, indicating that meta was on when the macro began. We now execute the selectall macro, defined below, which selects the entire file as input to the copy function. The second meta turns meta back on. The macro will execute meta one more time near the end of this sequence. Since we will flow through that step, we turn meta on now so that it will be turned off at that time. :> is the macro-language label-definition statement. This defines the label nometa, which we can branch to from the +>nometa statement above or fall through from the meta function above. In case the macro was executed with meta off, we will have turned it on in the process of testing it. Here we execute meta once more to turn it back off. Then print executes the previously defined print macro. By the time the print macro is executed, one of two things has happened: the user turned meta on, so we selected the entire file by executing selectall, or the user left meta off, so we've branched around the selectall and preserved the user's argument. In the latter case, the user's argument is passed to copy at the beginning of the print macro. Finally, you must define selectall. To select the entire file: ■ go to the end of the file ■ define a mark there ■ go to the top of the file ■ select from that mark Each of these steps is then translated into appropriate macros: select1:=arg ppage select2:=arg arg "endoffile" mark select3:=arg mpage select4:=arg "endoffile" selectall:=select1 select2 select3 select4 Note that this macro ends with an "open arg," an argument that has not yet been passed to a function. At the end of selectall, arg has been executed and the mark name specified, but no function has been specified to process the argument. When used with the bigprint macro above, the next function executed that processes an argument will be copy. The net result will be arg "endoffile" copy, which copies from the current location (top of file) to the mark location (end of file), placing the entire file in the clipboard. Now you can assign the bigprint macro to Alt-P. Assuming meta is assigned to F9, pressing F9 followed by Alt-P prints the entire current file. Programming Extensions An extension function is a new editing function written in a programming language such as C or assembly. Extension functions call internal editor functions to manipulate text, read and write to files, and control the editing environment. Extensions are the most powerful and flexible approach to programming the editor. They permit you to add completely new functions to the editor that do not rely on existing functions, can be customized to a much higher degree than macros, and can execute significantly faster since they are compiled code. The Filter Extension We will examine the structure of an extension and some common programming techniques by using the filter extension as an example. The source to this extension is shown in Figure 3 and its make file in Figure 4. Note that the extension requires Version 1.01 or later of the Microsoft Editor. Filter takes as an argument a region within the file being edited and replaces that region with the results of executing an arbitrary filter program on that region. For example, the filter program we'll use as an example will be the DOS SORT program. After selecting a region of lines and executing filter, the lines will be replaced in sorted order. The filter extension demonstrates how to handle some of the issues encountered when writing extensions, such as dealing with multiple types of arguments, using switches, reading and writing physical files from disk, and executing other editor commands from within an extension. Filter does its job by: ■ copying the user-selected data to a pseudo-file ■ writing that pseudo-file to a physical file on disk ■ executing the filter command on that file and saving its output in a new file ■ replacing the contents of the pseudo-file with the saved output of the filter command ■ copying the new data in the pseudo-file back to the original file being edited Before examining filter more closely, we should first look at what exactly comprises an extension. Editor Extension Structure An editor extension consists of four sections: ■ extension function and support routines, the code that implements extension editing functions ■ extension initialization routine, which is called when the editor first loads the extension ■ editing function definition table, which defines the functional content of the extension ■ switch definition table, which defines any switches created by the extension Extensions also require certain support files and must be compiled and linked with certain options, as outlined in the User's Guide. Extension Initialization When the editor loads an extension, the initialization routine WhenLoaded, which must be present, is called. It need not do anything and can return immediately. WhenLoaded is useful for making default keystroke assignments and performing any other extension initialization desired. An extension can be loaded at any time. Normally the user places a load: assignment in TOOLS.INI, which is executed when the editor starts up. For example: [M] load:filter.mxt The load: assignment, like any other assignment, can be executed at any time by using the assign function. In the filter extension, WhenLoaded performs three functions: ■ outputs an informative "sign-on" message to the editor's dialog line ■ gets a file handle for the pseudo-file it will use, creating that file if necessary ■ creates a default key assignment by assigning the filter function to the keystroke Alt-F The second function deserves some explanation. This is the code that implements it: pFileFilt = FileNameToHandle(szNameFilt,szNameFilt); if (!pFileFilt) { pFileFilt = AddFile(szNameFilt); FileRead(szNameFilt,pFileFilt); } The FileNameToHandle call, which is a call to an internal editor function, returns a handle to a file that has already been opened. If the file has not been opened, it returns NULL. You do this first, rather than just create the file, in case the user or some other extension or macro has already created a file of this name. If the file is already open, you just use the existing file handle. If the file isn't already open, you use another internal function, AddFile, to create it. This call creates the internal structures the editor uses to manipulate the file but does not actually read a file from disk. The internal function FileRead is used to load the file with data from disk. Because this is a pseudo-file, the call to FileRead results in no data actually being read. It is necessary here to complete the editor's initialization of the pseudo-file. Function Definition Table The function definition table, cmdTable, defines for the editor the functions contained in the editor extension. Each entry contains a pointer to a string of text that contains the function name, a pointer to the function itself, a reserved field that should be set to 0, and a field containing flags. The flags define: ■ The types of user-specified arguments this function accepts. ■ Whether this function is a cursor-movement function or a text- editing function. Cursor-movement functions are those that, rather than taking an argument, can be used to help define the region of an argument after arg is executed. For example, down is a cursor- movement function that does not take an argument but can be used to extend a box or line argument downward. ■ Whether this function is sensitive to the meta prefix. ■ Whether this function is a window-movement function. Window- movement functions are similar to cursor-movement functions; they do not affect screen highlighting. Window-movement functions differ in that they may take arguments. The editor's ppage function is a window-movement function. The filter function is defined by the following table entry: {"filter",filter,0,KEEPMETA|NOARG|BOXARG|NULLARG|LINEARG|MARKARG| NUMARG|TEXTARG|MODIFIES} This entry defines the name of the function as seen by the user to be "filter". The second field contains a pointer to the actual function. The third is reserved and must be null. The fourth parameter contains flags that indicate that the setting of the meta indicator is not to be affected by using this function (KEEPMETA), all the allowed argument types that the user can specify (NOARG through TEXTARG), and the fact that the function may modify text (MODIFIES). Switch Definition Table The switch definition table, like the function table, defines for the editor the switches contained in the extension. Each entry contains a pointer to the text containing the name of the switch, a pointer to the location of either the switch itself or a routine to interpret textual switches, and flags defining the characteristics of the switch. There are three types of switches: numeric, Boolean, and textual. Numeric and Boolean switches are changed directly by the editor when a new assignment is made. Textual switches require a support routine in the extension to interpret the text. The filter function contains a single textual switch: {"filtcmd",(PIF)(long)(void far *)SetFilter,SWI_SPECIAL} The string "filtcmd" defines the function's name as seen by the user. SetFilter is a pointer to the routine to be executed when that switch is set. (The elaborate casting performed here avoids compiler warning messages.) SWI_SPECIAL indicates that you have a textual switch, requiring special processing. The editor calls SetFilter whenever a new assignment to the filtcmd switch is made. It receives as a single parameter a pointer to the new text assigned to the switch, which SetFile can process as desired. In this case, the switch sets the name of the filter program we want filter to use. We copy away the entire text of the switch to a static location for use when filter is executed. By providing a switch for the filter command, the user can include a default value in TOOLS.INI. For example: [M] filtcmd:SORT < When the filter function is executed, the SORT command will be employed as the filter program, using redirected input. You can also change filtcmd on the fly. Extension Functions When the user executes an extension function, the editor calls the routine defined in the function definition table. The editor passes arguments to the C function that describe the user's arguments to the function, a flag indicating whether the meta prefix was in effect, and the value of the keystroke invoking the function. In our example, they would be: flagType pascal EXTERNAL filter (unsigned int argData, ARG far *pArg,flagType fMeta) The argData parameter is the keystroke that the command was invoked with. For example, if the key A is used, then argData is passed as 65, the ASCII value for A. However, filter doesn't use this parameter. The graphic function, which is the function that handles keystrokes that you use to type in text, simply places the value of argData as a character into the file being edited. The fMeta parameter is a TRUE/FALSE flag indicating the state of the meta command prefix at the time the command was executed. Most of the information normally used by an editing function is present in the pArg parameter, the pointer to the argument structure. This structure is defined as a type indicator followed by a union of six different structures; only one of these structures is used at a time. The type of structure used is based on the type of argument the user passed to the function. In our example, the filter function begins by taking actions and accessing the appropriate structure elements, based on the type of argument passed: switch (pArg->argType) { case NOARG: ∙ ∙ ∙ case NULLARG: ∙ ∙ ∙ case LINEARG: ∙ ∙ ∙ case BOXARG: ∙ ∙ ∙ case TEXTARG: ∙ ∙ ∙ } At this point there is an interesting discrepancy. In the command description table, we specified several different argument types that the actual function does not seem prepared to handle. NUMARG, for example, is included in the cmdDesc table, but not in the function itself. Some arguments, such as NUMARG and MARKARG, are converted by the editor to another, more basic argument type prior to calling the extension. For example, a numarg, which is a count of the lines specified by the user, is converted to a LINEARG, which explicitly enumerates the range of lines. While the way they are specified by the user is different, they both define a range of lines. Therefore the manner in which they are passed to an extension function is the same; they are both passed as a LINEARG. See Figure 5 for a list of all user argument types and Figure 6 for a list of the base types they can be mapped into. The extension function's access to the files being edited and the editing environment is defined by the internal editor functions, or "call-back" routines, which are listed in Figure 7. An extension function generally operates by calling internal editor functions to gain access to the file being edited and to retrieve, add, replace, or modify text therein. The Filter Function Up to this point, we've defined everything that goes around our function-the tables, the initialization routines, and the function parameters-and we've mentioned the routines that can be called by the extension function. Let's now take a look at the body of the filter function. Remember that the purpose of filter is to replace selected text with the results of running that text through an arbitrary filter program. The arguments to the filter function are shown in Figure 8. Note that we rely on markarg and numarg being converted to one of the other argument types by the editor before the function is called. Because the editor performs this conversion automatically, any function that takes a linearg and boxarg can easily take numarg and markarg by just setting the appropriate flag in the cmdTable. We noted earlier that filter operates in five basic steps. The function will begin by calling FileNameToHandle to get a handle to the file currently being edited. This is a common first step in any extension function, as the handle to the current file is required in several internal editor function calls to identify the file that is operated on. The function will then call DelFile to delete the contents of the pseudo- file, which was created in the initialization routine WhenLoaded. DelFile does not remove the file, since we have to continue to operate on it, but simply deletes the contents of the file. The first step copies the region selected by the user from the user's file (handle pFile) to our pseudo-file (handle pFileFilt). The type of copy used is based on the type of argument that the user specified. For example: case BOXARG: CopyBox(pFile,pFileFilt,pArg->arg.boxarg.xLeft, pArg->arg.boxarg.yTop,pArg->arg.boxarg.xRight, pArg->arg.boxarg.yBottom,(COL) 0,(LINE) 0); break; This code fragment is executed only if the user argument defines a BOXARG. You use CopyBox to copy the box specified by the user into the pseudo-file. The other argument types are handled similarly, except for TEXTARG, which is treated as a special case, so that by entering: arg text-argument filter the user can specify the actual command to be used as the filter. This is the same command as specified by the filtcmd switch. For example, assuming the default keystroke for arg and Alt-F for filter, to use SORT with redirected input as the filter command, the user would type: Alt-A "SORT <" Alt-F When this type of argument is detected, the filter routine just calls SetFilter to save the command string and returns. The second step just uses a call to FileWrite to write the contents of the pseudo-file to a disk file named FILTER1.TMP. The third step begins by building the text of the actual filter command using calls to the C run-time routines strcpy and strcat, which can be used because they fall into the category of acceptable C run-time routines available to extensions. The user-specified filter command is concatenated with the two temporary filenames defined in the data area. The net result of the concatenation, using the SORT command above, is: SORT < filter1.tmp >filter2.tmp This sorts the contents of FILTER1.TMP and creates FILTER2.TMP. The DoSpawn routine, described below, is called to execute the filter command we created, and then the disk file filter2.tmp is created. In the fourth step, before you read in the results of the filter, you use DelFile again to delete the previous contents of the pseudo-file. Then FileRead, the counterpart to FileWrite, is used to read in the physical file filter2.tmp to the pseudo-file. The net result is that the pseudo- file now contains the results of running its original contents through the filter. The final step begins by calculating the maximum width of the text that resulted from the filter. In some cases, this width will be different from the width of the original text, and for some argument types, such as boxarg, the correct width should be used when replacing the text. You determine the maximum width by using the following code: cLines = FileLength(pFileFilt); cbLineMax = 0; for (iLineCur = 0;iLineCur < cLines;iLineCur++) cbLineMax = max(cbLineMax,GetLine(iLineCur,buf,pFileFilt)); FileLength returns the current number of lines in the file referenced by the handle pFileFilt. The loop reads each line of the file into a buffer. The text is ignored, but the GetLine routine returns the length of each line it has read. Then, depending on the argument type again, you delete the original text in the user's file and replace it with the contents of the pseudo-file. Using BOXARG as an example again: case BOXARG: DelBox(pFile,pArg->arg.boxarg.xLeft,pArg->arg.boxarg.yTop, pArg->arg.boxarg.xRight,pArg->arg.boxarg.yBottom); CopyBox(pFileFilt,pFile,(COL) 0,(LINE) 0,cbLineMax,cLines-1, pArg->arg.boxarg.xLeft,pArg->arg.boxarg.yTop); break; DelBox deletes the box specified by the user, and CopyBox copies in the contents of the pseudo-file as a box. Finally, before leaving, the function uses DoSpawn again to execute DOS commands to delete the two temporary files that you created along the way. DoSpawn itself could be implemented in a number of ways; unfortunately, using the C run-time process-control routines is not one of them. An alternative method is to use the intdosx call under DOS or call DosExecProgram under OS/2 to have the operating system execute the program. Our approach is to use the fExecute function instead to execute the editor's shell function. DoSpawn begins by using strcpy and strcat to create the following string: arg "command" shell where "command" is the text pointed to by the szCmd parameter passed to DoSpawn. By then passing this string to fExecute, the editor shell function causes the command to be executed by the operating system. Conclusion We've taken a close look at the three types of programmability that the Microsoft Editor provides: keystroke assignments, which permit editor functions to be tied to keystrokes and executed when the keystrokes are pressed; macros, which enable editor functions to be combined in complex sequences and executed as a single function; and extensions, which create entirely new editing functions. Each is an important and useful tool for customizing and maximizing the power and efficiency of text editing with the Microsoft Editor. ──────────────────────────────────────────────────────────────────────────── C 5.2 Library Routines and Editor Extensions ──────────────────────────────────────────────────────────────────────────── Because of the mechanics of building Microsoft Editor extensions, there are certain restrictions on the use of C run-time library routines. These restrictions stem from two issues. Extensions must be compiled with SS != DS, that is, when executing, theextension function will operate with the same stack as the editor itself but will have access to its own local data segment, which is not the same as the stack segment or the editor's own data segment. This restricts C run-time library routines to the compact model set of libraries. Extensions do not have C start-up support. The C start-up is responsible for initializing a fair number of data items and the C run-time environment. This restricts the use of C run-time library functions to those that do not rely on this initialization. The following list summarizes functions available to extensions from the standard compact model library. This list uses the function categories from Chapter 4 of the Run-Time Library Reference. Category Availability Buffer Manipulation All Character Classification and Conversion All Data Conversion All except strtod Directory Control All except getcwd File Handling All Graphics None Input and Output: Stream None Input and Output: Low-level All except write in binary mode Input and Output: Console and Port All except cgets, cprintf, cscanf Math None Memory Allocation None Process Control None Searching and Sorting All except qsort String Manipulation All except strdup System Calls: BIOS All System Calls: MS-DOS All except int86, int86x Time All except ctime, gmtime, localtime, and utime Miscellaneous All except assert, getenv, perror, putenv,_searchenv Figure 1: Microsoft Editor 1.01 Functions ╓┌───────────────────┌───────────────────────────────────────────────────────╖ Name Function Performed arg Introduce argument to subsequent function assign Define keystroke or define macro backtab Move to previous tab position begline Move to beginning of line cancel Cancel operation or argument cdelete Character delete Name Function Performed cdelete Character delete compile Execute compiler and/or browse results copy Copy selected text to clipboard down Move down one line emacscdel Delete character (joins lines) emacsnewl Move to new line (breaks lines) endline Move to end of line execute Execute other editor functions by name exit Exit the editor or the current file graphic Place character into file being edited home Go to top or top left position on screen information Display file history initialize Initialize the editor and read portions of TOOLS.INI insertmode Toggle insert/typeover mode lasttext Redisplay previous text arg for editing ldelete Delete lines or boxes left Move cursor left linsert Insert lines mark Place or go to a bookmark meta Modify action of subsequent function Name Function Performed meta Modify action of subsequent function mlines Move backward lines mpage Move backward pages mpara Move backward paragraphs msearch Search backward mword Move backward words newline Move to next line paste Copy contents of clipboard to current cursor location pbal Balance unmatched parentheses, braces, or brackets plines Move forward lines ppage Move forward pages ppara Move forward paragraphs psearch Search forward pword Move forward words qreplace Query search and replace quote Treat character assigned to other function as text refresh Reread or discard current file replace Search and replace restcur Restore cursor to saved location right Move cursor right Name Function Performed right Move cursor right savecur Save cursor location sdelete Delete stream of characters setfile Move to new file and/or save current file setwindow Redisplay or reorient text in window shell Execute operating system command sinsert Insert stream tab Move cursor to next tab stop unassigned Print error message undo Reverse effects of previous edits up Move cursor up window Manipulate multiple windows Figure 2: Arguments Used in the Print Macro noarg Print current line nullarg Print cursor to end of current line textarg Print the text argument boxarg Print the selected box linearg Print the selected lines numarg Print the next num lines markarg Print text between cursor and mark Figure 3: Microsoft Editor Filter Extension /*********************************************************************** * * Purpose: * Provides a new editing function, filter, which replaces its * argument with the argument run through an arbitrary operating * system filter program. * *************************************************************************/ #define ID " filter ver 1.00 "##__DATE__##" "##__TIME__ #include <stdlib.h> /* min macro definition */ #include <string.h> /* prototypes for string fcns */ #include "ext.h" /************************************************************************* * * Internal function forward declarations */ flagType pascal DoSpawn (char *); void pascal id (char *); void pascal EXTERNAL SetFilter (char far *); /************************************************************************* * * global data */ PFILE pFileFilt = 0; /* handle for filter-file */ char *szNameFilt = "<filter-file>"; /* name of filter-file */ char *szTemp1 = "filter1.tmp"; /* name of 1st temp file */ char *szTemp2 = "filter2.tmp"; /* name of 2nd temp file */ char filtcmd[BUFLEN] = ""; /* filter command itself */ /*** filter - Editor filter extension function * * Purpose: * Replace selected text with that text run through an arbitrary * filter. * * NOARG - Filter entire current line * NULLARG - Filter current line, from cursor to end of line * LINEARG - Filter range of lines * BOXARG - Filter characters with the selected box * * NUMARG - Converted to LINEARG before extension is called. * MARKARG - Converted to Appropriate ARG form above before * extension is called. * * STREAMARG - Treated as BOXARG * * TEXTARG - Set new filter command * * Input: * Editor Standard Function Parameters * * Output: * Returns TRUE on success, file updated, else FALSE. * *************************************************************************/ flagType pascal EXTERNAL filter ( unsigned int argData, /* keystroke invoked with */ ARG far *pArg, /* argument data */ flagType fMeta /* indicates preceded by meta */ ) { char buf[BUFLEN]; /* buffer for lines */ int cbLineMax; /* max line length in filtered */ LINE cLines; /* count of lines in file */ LINE iLineCur; /* line being read */ PFILE pFile; /* file handle of current file */ */ ** Initialize: identify ourselves, get handle to current file, and ** discard the contents of the filter-file. */ id (""); pFile = FileNameToHandle ("", ""); DelFile (pFileFilt); */ ** Step 1, based on the argument type, copy the selected region into the ** (upper leftmost position of) filter-file. ** Note that TEXTARG is a special case that allows the user to change ** the name of the filter command to be used. */ switch (pArg->argType) { case NOARG: /* filter entire line */ CopyLine (pFile, pFileFilt, pArg->arg.noarg.y, pArg->arg.noarg.y, (LINE) 0); break; case NULLARG: /* filter to EOL */ CopyStream (pFile, pFileFilt, pArg->arg.nullarg.x, pArg->arg.nullarg.y, 255, pArg->arg.nullarg.y, (COL) 0, (LINE) 0); break; case LINEARG: /* filter line range */ CopyLine (pFile, pFileFilt, pArg->arg.linearg.yStart, pArg->arg.linearg.yEnd, (LINE) 0); break; case BOXARG: /* filter box */ CopyBox (pFile, pFileFilt, pArg->arg.boxarg.xLeft, pArg->arg.boxarg.yTop, pArg->arg.boxarg.xRight, pArg->arg.boxarg.yBottom, (COL) 0, (LINE) 0); break; case TEXTARG: SetFilter (pArg->arg.textarg.pText); return 1; } */ ** Step 2, write the selected text to disk. */ if (!FileWrite (szTemp1, pFileFilt)) { id ("** Error writing temporary file **"); return 0; } */ ** Step 3, create the command to be executed: ** user specified filter command + " " + tempname 1 + " >" + ** tempname 2 ** Then perform the filter operation on that file, creating a second ** temp file. */ strcpy (buf,filtcmd); strcat (buf," "); strcat (buf,szTemp1); strcat (buf," >"); strcat (buf,szTemp2); if (!DoSpawn (buf)) { id ("** Error executing filter **"); return 0; } */ ** Step 4, delete the contents of the filter-file, and replace it by ** reading in the contents of that second temp file. */ DelFile (pFileFilt); if (!FileRead (szTemp2, pFileFilt)) { id ("** Error reading temporary file **"); return 0; } */ ** Step 5, calculate the maximum width of the data we got back from ** the filter. Then, based again on the type of region selected by ** the user, DISCARD the users select region, and copy in the ** contents of the filter-file in an equivalent type. */ cLines = FileLength (pFileFilt); cbLineMax = 0; for (iLineCur = 0; iLineCur < cLines; iLineCur++) cbLineMax = max (cbLineMax, GetLine (iLineCur, buf, pFileFilt)); switch (pArg->argType) { case NOARG: /* filter entire line */ DelLine (pFile, pArg->arg.noarg.y, pArg->arg.noarg.y); CopyLine (pFileFilt, pFile, (LINE) 0, (LINE) 0, pArg->arg.noarg.y); break; case NULLARG: /* filter to EOL */ DelStream (pFile, pArg->arg.nullarg.x, pArg->arg.nullarg.y, 255, pArg->arg.nullarg.y); CopyStream (pFileFilt, pFile, (COL) 0, (LINE) 0, cbLineMax, (LINE) 0, pArg->arg.nullarg.x, pArg->arg.nullarg.y); break; case LINEARG: /* filter line range */ DelLine (pFile, pArg->arg.linearg.yStart, pArg->arg.linearg.yEnd); CopyLine (pFileFilt, pFile, (LINE) 0, cLines-1, pArg->arg.linearg.yStart); break; case BOXARG: /* filter box */ DelBox (pFile, pArg->arg.boxarg.xLeft, pArg->arg.boxarg.yTop, pArg->arg.boxarg.xRight, pArg->arg.boxarg.yBottom); CopyBox (pFileFilt, pFile, (COL) 0, (LINE) 0, cbLineMax-1, cLines-1, pArg->arg.boxarg.xLeft, pArg->arg.boxarg.yTop); break; } */ ** Clean-up: delete the temporary files we've created */ strcpy (buf, "DEL "); strcat (buf, szTemp1); DoSpawn (buf); strcpy (buf+4, szTemp2); DoSpawn (buf); return 1; /* end filter /*} /*** DoSpawn - Execute an OS/2 or DOS command * * Purpose: * Send the passed string to OS/2 or DOS for execution. * * Input: * szCmd = Command to be executed * * Output: * Returns TRUE if successful, else FALSE. * *************************************************************************/ flagType pascal DoSpawn ( char *szCmd ) { char cmd[BUFLEN]; strcpy (cmd, "arg \""); strcat (cmd, szCmd); strcat (cmd, "\" shell"); return fExecute (cmd); /* end DoSpawn /*} /*** SetFilter - Set filter command to be used * * Purpose: * Save the passed string parameter as the filter command to be used * by the filter function. Called either because the "filtcmd:" * switch has been set or because the filter command received a * TEXTARG. * * Input: * szCmd = Pointer to asciiz filter command * * Output: * Returns nothing. Command saved. * *************************************************************************/ void pascal EXTERNAL SetFilter ( char far *szCmd ) { strcpy (filtcmd,szCmd); /* end SetFilter /*} /*** WhenLoaded - Extension Initialization * * Purpose: * Executed when extension gets loaded. Identify self, create * <filter-file>, and assign default keystroke. * * Input: * none * * Output: * Returns nothing. Initializes various data. * *************************************************************************/ void WhenLoaded (void) { pFileFilt = FileNameToHandle (szNameFilt,szNameFilt); if (!pFileFilt) { pFileFilt = AddFile (szNameFilt); FileRead (szNameFilt, pFileFilt); } SetKey ("filter", "alt+f"); id ("text filter extension:"); /* end WhenLoaded /*} /*** id - identify extension * * Purpose: * identify ourselves, along with any passed informative message. * * Input: * pszMsg = Pointer to asciiz message, to which the extension name * and version are appended prior to display. * * Output: * Returns nothing. Message displayed. * *************************************************************************/ void pascal id ( char *pszFcn /* function name */ ) { char buf[BUFLEN]; /* message buffer */ strcpy (buf,pszFcn); strcat (buf,ID); DoMessage (buf); /* end id /*} /************************************************************************* ** ** Switch communication table to the editor. */ struct swiDesc swiTable[] = { {"filtcmd", (PIF)(long)(void far *)SetFilter, SWI_SPECIAL}, {0, 0, 0} }; /************************************************************************* ** ** Command communication table to the editor. ** Defines the name, location, and acceptable argument types. */ struct cmdDesc cmdTable[] = { {"filter",filter,0, KEEPMETA | NOARG | BOXARG | NULLARG | LINEARG | MARKARG | NUMARG | TEXTARG | MODIFIES}, {0, 0, 0} }; Figure 4: Make File for the Filter Extensions # # makefile for the filter extensions # filter.obj: filter.c cl /c /Gs /Asfu filter.c filter.mxt: filter.obj cl /Lr /AC /Fefilter.mxt exthdr.obj filter.obj filter.dll: filter.obj cl /Lp /AC /Fefilter.dll exthdrp.obj filter.obj skel.def Figure 5: Microsoft Editor User Arguments ╓┌────────────────┌──────────────────────────────────────────────────────────╖ User Argument Argument Meaning no arg No argument specified. Default action of the function is executed. nullarg A null argument. Often used to modify the action of a function or to specify the range over which the function occurs as "cursor to end of line." textarg Typed-in textual argument. Used to specify textual information, such as search strings. numarg Typed in numeric argument. Often used to specify a specific line number or a number of lines over which a function is to be applied. markarg Typed-in argument that references a named mark elsewhere in the current file. Besides moving to a marker position, this is often used to specify the range over which a function is to be executed (current position to marker User Argument Argument Meaning function is to be executed (current position to marker position). streamarg Arg created by moving the cursor, consisting of a range of characters on a single line or a range of characters that spans multiple lines but includes all characters between the beginning and end of the range, regardless of position. boxarg Arg created by moving the cursor, consisting of a box of characters spanning multiple lines. Only those characters within the box are included. (The distinction between boxarg and multiline streamarg is made by the function invoked.) linearg Arg created by moving the cursor, consisting of a range of lines, in their entirety. meta The meta command prefix is not an argument but is used to modify an editing function. In general it is used to User Argument Argument Meaning modify an editing function. In general it is used to either take a command to some extreme (home goes to first nonblank character on a line, while meta home goes to column one, regardless), or to cause a function to lose some information (exit saves the current file, if changed, and exits the editor, while meta exit immediately exits the editor without saving any changes). Figure 6: User Arguments as Passed to Extension Functions ARG Received Possible User Argument Specified NOARG No argument was specified. NULLARG A null argument was specified. TEXTARG A textual argument specified. May be the result of: ∙ having been typed in explicitly, ∙ a one-line boxarg, if the BOXSTR flag is set in the cmdDesc table ∙ a nullarg, if NULEOL flag is set (text is cursor to end of line) ∙ a nullarg, if NULEOW flag is set (text is cursor to end of word) STREAMARG An arg was created by moving the cursor. Can also be the result of a mark arg. BOXARG An arg was created by moving the cursor, consisting of a box of characters. Can also be the result of a mark arg. LINEARG Arg created by moving the cursor, consisting of a range of lines, in their entirety. Can also be the result of a num arg or mark arg. Figure 7: Microsoft Editor Internal Functions ╓┌──────────────────┌────────────────────────────────────────────────────────╖ Name Function AddFile Add file to editor's file list BadArg Display "bad argument" message CopyBox Copy BOX from one file to another CopyLine Copy LINEs from one file to another Name Function CopyLine Copy LINEs from one file to another CopyStream Copy STREAM from one file to another DelBox Delete a BOX of characters from a file DelFile Delete entire contents of a file DelLine Delete a range of LINEs from a file DelStream Delete a STREAM of characters from a file DoMessage Display message on dialog line Display Cause display to be redrawn fExecute Execute macro string FileLength Return length of requested file FileNameToHandle Return handle to current or named file FileRead Read disk file into file being edited FileWrite Write disk file from file being edited GetCursor Return current cursor position GetLine Return line of text in file KbHook Restore editor's control of keyboard KbUnHook Release editor's control of keyboard MoveCur Move cursor to new position in current file pFileToTop Make specified file the "current" file being displayed PutLine Place line of text into file Name Function PutLine Place line of text into file RemoveFile Remove file from editor's list. ReadChar Read character from keyboard ReadCmd Read editing command interpreted from keyboard Replace Replace character in file SetKey Make keystroke assignment Figure 8: Arguments Used in the Filter Extension noarg Filter the current line nullarg Filter text from cursor to end of line boxarg Filter the selected box linearg Filter the selected lines numarg Filter a number of lines markarg Filter between cursor and a mark textarg Specify new filter command ████████████████████████████████████████████████████████████████████████████ Dynamically Creating Dialog Boxes Using New Windows 2.x Functions Don Hasson Creating dialog boxes or input forms dynamically, or "on the fly," was not possible in Windows Version 1.x. The only way to define an input form or dialog box was to predefine it in your resource file. Microsoft(R) Windows Version 2.x, however, includes two new functions that let programmers dynamically create dialog boxes or forms such as those found in Microsoft Excel's sample macro sheets. DialogBoxIndirect and CreateDialogIndirect The two functions that are new to Microsoft Windows 2.x are DialogBoxIndirect and CreateDialogIndirect. Close inspection shows that the only difference between the standard DialogBox/CreateDialog and the DialogBoxIndirect/CreateDialogIndirect functions is that the latter two take a handle as their second parameter instead of a long pointer. By adding these routines to an application, the program or the user has the ability to create or modify dialog boxes as needed. A dialog box with about 20 different items or controls is shown in Figure 1☼ The unusual part of this dialog is that it was created dynamically by calling either the DialogBoxIndirect or CreateDialogIndirect function and did not require extra lines in the resource file or a call to the CreateWindow function. A table for the DialogBoxIndirect or CreateDialogIndirect function is the only necessary component. Microsoft Excel's Dialogs One of the many powerful features of Microsoft Excel is that it allows users to define their own custom dialog boxes. This is done by defining a table of the desired controls in a macro sheet and making a call to the table. The same type of table is necessary for windows applications; however, this table can take any form or shape you want for your application. It can be a text file that is read in or predefined in your program, or just a parameter list that is created by prompting the user to enter values in a form. Either way, your application can now dynamically create and change dialog boxes. The DLGBOX Program The DLGBOX program consists of three modules, called DLGBOX.H, DLGBOX.C and DLGTEMP.C (see Figures 2, 3, and 4). DLGBOX is the main part of the program that makes a call to the DLGTEMP module for each dialog item to be placed inside the dialog box. DLGTEMP builds these items into a table that is later passed back to the DLGBOX program. The DLGTEMP module was created as a standalone module that you could include with your application. Now let's look at the different parts of this program. The make file (see Figure 5) contains all the parts necessary to build this application. Remember that two of these functions are only found in Windows 2.x, so you must use the Microsoft(R) Windows Software Development Kit Version 2.x and the Microsoft C 5.0 compiler or higher. The Resource file shown in Figure 6 contains our icon and two menus. The first menu allows you to select whether the dialog box is to be created as a modal or modeless dialog box. The differences will be explained later on. The second menu defined will be used in the dynamic dialog box as an example of how to make it part of the dialog box. New Data Structures Using the DialogBoxIndirect and CreateDialogIndirect functions, you only need two data structures to build a dialog box. The first is the DialogHeader structure (see Figure 7). It is used as a header to the table that is being built. The second data structure is DialogItem (see Figure 8). The DialogItem structure is used for each control that is to be placed inside the dialog box. For ease of reading in this example, the prefix DH for DialogHeader fields and DI for DialogItem fields have been added. Only three functions need to be called in the DLGTEMP module in order to perform all the work to create a dialog structure in global memory. They are the CreateDialogHeader, CreateDialogItem, and EndDialogHeader functions. CreateDialogHeader The CreateDialogHeader function passes parameters to fill in the different parts of the DialogHeader structure, as shown in Figure 7. After the structure is completed, a piece of global memory is allocated to store the DialogHeader information for later use. Looking at the DialogHeader structure, you will notice that it consists of most of the parameters needed for the CREATESTRUCT structure used by the CreateWindow function, and this is indeed what will happen when all the fields are filled in. The cDHItemCount field is the only new field and defines the number of items or controls inside the dialog box. However, this field can only be filled in when the number of items is known. The first field, lDHStyle, will contain the window style attributes for the dialog box. These are the same values that are normally OR'd together for the dwStyle field (see Table 3.4 in the Microsoft(R) Windows Software Development Kit Programmer's Reference Version 2.x) for the CreateWindow function. The iDHX and iDHY fields are used to place the upper left corner of the dialog box in the parent's client area. The iDHCX and iDHCY fields are then used to define the width and height of the dialog box. To determine how wide the dialog box should be, multiply the number of characters by four and add about 10 to create an extra margin on both sides. For the depth, multiply the number of lines by eight, adding an extra 10 to provide a deeper margin for the top and bottom. As a shortcut, the width and height values from the Dialog Editor, DIALOG.EXE, can be used directly without conversion. The next part of the structure consists of three variable-length fields. They are called variable length because the field can range in size from 1 to many bytes. Since we don't know ahead of time how long these fields are going to be, we cannot allocate space for them. We could have defined them to be the longest string that could be handled, but we chose not to. In order to use these fields, we follow a simple rule: if it is not being used, the first byte is 00H. The first byte after the DialogHeader structure is for the resource name, szDHResourceName, and defines the name of the menu resource in the application's resource file. If the first byte is 00H, then no menu is used with the dialog box. If you want to use the ordinal number of the menu resource, then the first byte must be set to FFH, followed by 3 bytes for the ordinal number (in ASCII). If the first byte is not 00H or FFH, then a null-terminated string is expected. The szDHClassName field defines the window class for the dialog box. When the first byte is 00H, the default class of DialogClass is used. If you want to define your own window class, provide a null-terminated string for szDHClassName. This is the same value that you use as lpClassName for the CreateWindow function. The szDHCaptionText field defines the string used for the caption bar of the dialog box if we choose to OR the WS_CAPTION attribute into the window lDHStyle field above. If the first byte is 00H, then nothing is displayed in the caption bar; otherwise, a null-terminated string is required. This string is the same value used in the lpWindowName field of the CreateWindow function. Now that we are finished filling in the DialogHeader structure, you can see that we have all the necessary fields to call the CreateWindow function. In our example, we have simplified this by passing the parameters in the CreateDialogHeader function. We are now ready to fill in the individual items to be placed inside the dialog box. CreateDialogItem The next function in our program is the CreateDialogItem function. It will pass all the information necessary to fill in the different parts of the DialogItem structure, as shown in Figure 8. After the structure is filled, the current global memory block is expanded with the GlobalRealloc function to the required size, and the information copied into the newly expanded memory block. Another benefit of the CreateDialogItem function is that you already have most of the predefined window controls and their styles set so that you can reference them with a single number. This saves you from having to pass all the different attributes for a predefined control like Microsoft Excel. The DialogItem structure consists of the same fields as the DialogHeader structure since these values must be used to call the CreateWindow function. The iDIX, iDIY, iDICX, and iDICY fields are used to define the upper left corner and the width and height of the control. The iDIID field defines the control's ID (for example, IDOK). This value is the same one used for the hMenu field in the CreateWindow function. The behavior for the control is defined by the lDIStyle field. This can be any of the predefined window styles and corresponds to the same value used for the hStyle field in the CreateWindow function. In addition, you are allowed to OR in all of the applicable window style and control style attributes that you are accustomed to. (See Table 3.4 and 3.5 in the Microsoft(R) Windows Software Development Kit Programmer's Reference Version 2.x for a complete listing.) We then have cbDIControlClass to define the specific control class of the item. Figure 9 is a list of the predefined control Classes Windows offers. (See Table 3.3 in the Microsoft(R) Windows Software Development Kit Programmer's Reference Version 2.x for a more complete description of each control class.) The szDIText field is used to hold the initial text to be displayed in the control. If the first byte is 00H, then no text will be displayed. If the first byte is not 00H, then a null-terminated string is required. The cbDIExtra field is just like the last field in the WNDCLASS structure, cbWndExtra. It allows us to tack on extra bytes to the WNDCLASS structure. We can then store and retrieve additional information from the window by using the same GetWindowWord, GetWindowLong, SetWindowWord, and SetWindowLong functions. Once we have all the information, we are ready to pass these as parameters in the CreateDialogItem function. If everything proceeds correctly, the global memory is expanded, the new additional information is copied to the end, and the item count, iItem, is bumped up by one. This way, when we are finished putting in the last control item in the dialog, we do not have to count how many items are in the dialog box. EndDialogHeader The last function used in our sample application is the EndDialogHeader function. Here we need to go in and directly modify the DialogHeader cDHItemCount field that is now in global memory. We do this first by locking down the memory, then moving the pointer over the correct number of bytes. Then we change the memory value with the correct number of items stored in iItems that we kept track of in the CreateDialogItem function. We then unlock the memory. If the EndDialogHeader function is successful in changing the memory, it will return a handle to the global memory that contains our newly created dialog table. Now that the dialog structure is built, we are ready to pass the dialog table to one of our dialog box functions. If we want a modal dialog box, then all we have to do is call the DialogBoxIndirect function with the handle to global memory. However, if we want to create a modeless dialog box, one additional step is necessary──we have to lock down the global memory so that we can pass a long pointer to the memory as the second parameter of the CreateDialogIndirect function. Other than having to lock down the global memory, the main difference between the DialogBoxIndirect and CreateDialogIndirect functions is that DialogBoxIndirect also makes a call to EnableWindow to disable the window handle that has been passed as the third parameter. If this handle is not the handle to the parent, then the DialogBoxIndirect function will find the parent and then disable it. Conclusion Dynamic dialog boxes are not significantly different from their static counterparts. However, the ability to create them on an as-needed basis is a very desirable feature that provides extra flexibility. After learning to create the dialog table that the two new functions require and understanding what the functions provided in DLGTEMP.C do, you have all the tools necessary to begin building applications that include dialog boxes built on the fly. Figure 2: Header File /* DLGBOX.H-Header File */ #define IDM_MODAL 100 #define IDM_MODELESS 110 #define IDM_ONE 120 #define IDDEDIT 3025 #define IDCHECK 3026 /* Control class codes */ #define BUTTONCLASS 0x80 /* 128 */ #define EDITCLASS 0x81 /* 129 */ #define STATICCLASS 0x82 /* 130 */ #define LISTBOXCLASS 0x83 /* 131 */ #define SCROLLBARCLASS 0x84 /* 132 */ typedef struct DialogHeader{ long dtStyle; BYTE dtItemCount; int dtX; int dtY; int dtCX; int dtCY; } DLGHDR; /* The following 3 header items are variable strings so not part of DLGHDR */ /* char dtResourceName[]; */ /* char dtClassName[]; */ /* char dtCaptionText[]; */ typedef struct DialogItem { int dtilX; int dtilY; int dtilCX; int dtilCY; int dtilID; long dtilStyle; BYTE dtilControlClass; } DLGITEM; /* The following control items are variable so not part of DLGITEM */ /* char dtilText[]; */ /* BYTE dtilInfo; */ #define DI0 0L #define DI1 1L #define DI2 2L #define DI3 3L #define DI4 4L #define DI5 5L #define DI6 6L #define DI7 7L #define DI8 8L #define DI9 9L #define DI10 10L #define DI11 11L #define DI12 12L #define DI13 13L #define DI14 14L #define DI15 15L #define DI16 16L #define DI17 17L #define DI18 18L #define DI19 19L #define DI20 20L #define DI21 21L Figure 3: DLGBOX.C Code Fragments /* DLGBOX.C ─ version 1.00 Demonstrates how to use the DialogBoxIndirect and CreateDialogIndirect functions. */ #include <windows.h> #include "dlgbox.h" /* List of functions used in this module. */ BOOL FAR PASCAL InitDiabox (HANDLE , HANDLE , int); LONG FAR PASCAL DlgboxWndProc (HANDLE, unsigned, WORD, LONG); BOOL FAR PASCAL DialogBoxWindowProc (HANDLE, unsigned, WORD, LONG); BOOL BuildDialog(HWND); /* The following functions are found in the DLGTEMP module. They are used to build the dialog structure that will be passed to the DialogBoxIndirect or CreateDialogIndirect function. */ ∙ ∙ ∙ /* ────── parent's window procedure ────── */ LONG FAR PASCAL DlgboxWndProc( hWnd, message, wParam, lParam ) HWND hWnd; unsigned message; WORD wParam; LONG lParam; { PAINTSTRUCT ps; int iResult; switch (message) { case WM_COMMAND: switch (wParam) { case IDM_MODAL: if ( hDlgTest == NULL ) {/* allow only one dialog up at a time */ nModeless = FALSE; if ( BuildDialog(hWnd) == TRUE) { lpDlgTest = MakeProcInstance( (FARPROC)DialogBoxWindowProc,hInst); if (lpDlgTest == NULL) { MessageBox(GetFocus(), "MakeProcInstance failed!", "ERROR", MB_OK ); GlobalFree( hDlgTemp ); return FALSE; } iResult = DialogBoxIndirect(hInst,hDlgTemp,hWnd,lpDlgTest); } } break; case IDM_MODELESS: if (hDlgTest == NULL) {/* allow only one dialog up at a time */ nModeless = TRUE; if ( BuildDialog(hWnd) == TRUE) { lpDTemplate = GlobalLock( hDlgTemp ); if (lpDTemplate == NULL) { MessageBox( GetFocus(), "Could not lock memory.", "Diabox", MB_OK ); GlobalFree( hDlgTemp ); return FALSE; } lpDlgTest = MakeProcInstance( (FARPROC) DialogBoxWindowProc, h if (lpDlgTest == NULL) { MessageBox( GetFocus(), "MakeProcInstance failed!", "ERROR", MB_OK ); GlobalFree( hDlgTemp ); return FALSE; } hDlgTest = CreateDialogIndirect( hInst, lpDTemplate, hWnd, lpD } /* exit if BuildDialog didn't work */ } break; default: return DefWindowProc(hWnd, message, wParam, lParam); break; } case WM_PAINT: BeginPaint(hWnd, (LPPAINTSTRUCT)&ps); EndPaint (hWnd, (LPPAINTSTRUCT)&ps); break; case WM_DESTROY: FreeProcInstance ((FARPROC)lpDlgTest); PostQuitMessage(0); break; default: return(DefWindowProc(hWnd, message, wParam, lParam)); } return(0L); } /* ────── Build dialog routine ───────────── */ /* * This routine passes all the necessary information to the * dialog functions in DLGTEMP.C that will build the dialog table. */ BOOL BuildDialog(hWnd) HWND hWnd; { BOOL bResult; HCURSOR hOldCursor; /* Handle to old mouse cursor */ /* Build the Dialog header */ bResult = CreateDialogHeader( WS_BORDER | WS_CAPTION | WS_DLGFRAME | WS_VSCROLL | WS_HSCROLL | WS_SYSMENU | WS_GROUP | WS_TABSTOP | WS_SIZEBOX | WS_VISIBLE | WS_POPUP, /* window style */ (BYTE)12, 24, 12, 180, 160, /* coordinates of Dialog box.*/ "dlgtemp", /* menu */ "", /* class name */ "A test Dialog Box" ); /* Caption bar */ if (bResult == FALSE) { MessageBox( GetFocus(), "Not enough memory.","Diabox", MB_OK ); return FALSE; } hOldCursor = SetCursor( LoadCursor(NULL, IDC_WAIT) ); /* Each call to CreateDialogItem is an item to be placed inside the dialog box */ /* This next comment line is the value that is being passed */ /* id, style, class, x, y, cx, cy, text, extrabytes */ CreateDialogItem( IDCHECK, DI0, 0, 60, 90, 45, 12, "&Ck box",0x00 );/* c CreateDialogItem( 3001, DI1, 0, 44, 107, 0, 0, "dlgicon",0x00 );/* i CreateDialogItem( 3002, DI2, 0, 73, 107, 16, 17, "",0x00 );/* b CreateDialogItem( 3003, DI3, 0, 6, 107, 25, 15, "",0x00 );/* r CreateDialogItem( 3004, DI4, 0, 6, 132, 40, 10,"L Justify",0x00 );/* l CreateDialogItem( 3005, DI5, 0, 122, 64, 40, 38, "M Edit",0x00 );/* m CreateDialogItem( 3006, DI6, 0, 60, 8, 54, 32, "lb",0x00 );/* l CreateDialogItem( 3007, DI7, 0, 43, 8, 8, 34, "v s",0x00 );/* v CreateDialogItem( 3008, DI8, 0, 6, 47, 47, 8, "h s",0x00 );/* h CreateDialogItem( 3009, DI9, 0, 6, 8, 32, 36, "group",0x00 );/* g CreateDialogItem( IDCANCEL,DI10, 0, 102, 112, 24, 14, "&Push",0x00 );/* p CreateDialogItem( 3011, DI11, 0, 6, 61, 45, 12, "&radio",0x00 );/* r CreateDialogItem( IDOK, DI12, 0, 136, 112, 24, 14, "&Ok",0x00 );/* d CreateDialogItem( 3013, DI13, 0, 6, 90, 44, 12,"&L Ck box",0x00 );/* l CreateDialogItem( 3014, DI14, 0, 6, 76, 30, 12, "&3auto",0x00 );/* 3 CreateDialogItem( 3015, DI15, 0, 122, 27, 40, 12, "C Edit",0x00 );/* e CreateDialogItem( 3016, DI16, 0, 122, 44, 40, 12, "R Edit",0x00 );/* r CreateDialogItem( IDDEDIT, DI17, 0, 122, 8, 40, 12, "L Edit",0x00 );/* L CreateDialogItem( 3018, DI18, 0, 60, 45, 54, 32, "no sort",0x00 );/* l CreateDialogItem( 3019, DI19, 0, 66, 132, 50, 10, "Center",0x00 );/* C CreateDialogItem( 3020, DI20, 0, 122, 132, 46, 10,"R Justify",0x00 );/* R SetCursor( hOldCursor ); /* Get the handle to the new dialog table */ hDlgTemp = EndDialogHeader(); /* end dialog template, return hDlgTemp. */ if (hDlgTemp == NULL) { GlobalFree(hDlgTemp); MessageBox( GetFocus(), "End dialog routine.", "Error!", MB_OK ); return FALSE; } return TRUE; } ∙ ∙ ∙ Figure 4: DLGTEMP.C Code Fragments /* DLGTEMP.C ─ version 1.00 This module supplies the necessary functions used to create a dialog box template in global memory and to pass the handle on to either the DialogBoxIndirect or CreateDialogIndirect functions. */ #include <windows.h> #include "dlgbox.h" /* forward references */ WORD lstrlen( LPSTR ); void SetStyleClass(int); HANDLE hDlgTemplate; /* Handle to current dialog template memory */ WORD wOffset; /* Current memory offset (updated by CDH & CDI) */ BYTE iItems; /* number of items in dialog */ DLGITEM DlgItem; /* Dialog item structure */ /* ────── Create Dialog Header ─────────────*/ */ * This routine allocates a piece of global memory * and then fills in the dialog header structure and saves the * information in global memory. */ BOOL FAR PASCAL CreateDialogHeader( Style, ItemCount, X, Y, cX, cY, Resource, Class, Caption) LONG Style; /* Dialog box Style */ BYTE ItemCount; /* Control count for dialog box */ int X; /* Dialog box top left column */ int Y; /* Dialog box top row */ int cX; /* Dialog box width */ int cY; /* Dialog box height */ LPSTR Resource; /* Dialog box resource string */ LPSTR Class; /* Dialog box class string */ LPSTR Caption; /* Dialog box caption */ ∙ ∙ ∙ * ────── Create Dialog Item ─────────────*/ */ * This routine fills in the dialog item structure and * saves the information in global memory after resizing it. */ BOOL FAR PASCAL CreateDialogItem( iCtrlID, lStyle, Class, X, Y, cX, cY, Text, ExtraBytes ) int iCtrlID; /* Control ID */ LONG lStyle; /* Control style */ BYTE Class; /* Control class */ int X; /* Control top left column */ int Y; /* Control top row */ int cX; /* Control width */ int cY; /* Control height */ LPSTR Text; /* Control text */ BYTE ExtraBytes; /* Control extra bytes count */ ∙ ∙ ∙ /* ────── End Dialog Header ─────────────── */ */ * This routine changes the number of items in the * dialog header and then returns a handle to the global memory. */ HANDLE FAR PASCAL EndDialogHeader() /* end dialog header function */ ∙ ∙ ∙ /* ────── List of predefined dialog items ───────── */ */ * This routine defines a list of predefined dialog items. * This allows the user to just pass a number to get the * attributes of a pre-defined control. * You can add to this list or modify this to best fit your needs. * * Warning: If you change one of the styles, it could affect * other controls that expect the old behavior. */ void SetStyleClass(iStyle) int iStyle; { /* if an iStyle is given then dtilControlClass will take on the default value. */ switch ( iStyle ) { case DI0: /* check box */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_CHECKBOX | WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI1: /* icon */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_ICON | WS_BORDER |WS_CHILD |WS_VISIBLE ; break; case DI2: /* black box */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_BLACKRECT |WS_CHILD |WS_VISIBLE ; break; case DI3: /* rectangle */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_BLACKFRAME |WS_CHILD |WS_VISIBLE ; break; case DI4: /* left static text */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_LEFT |WS_CHILD |WS_VISIBLE ; break; case DI5: /* multiline edit box */ DlgItem.dtilControlClass = EDITCLASS ; DlgItem.dtilStyle = ES_LEFT | ES_MULTILINE | ES_NOHIDESEL | ES_AUTOVSCROLL | ES_AUTOHSCROLL | WS_VSCROLL | WS_HSCROLL | WS_BORDER | WS_TABSTOP |WS_CHILD |WS_VISIBLE ; break; case DI6: /* list box - sorted */ DlgItem.dtilControlClass = LISTBOXCLASS ; DlgItem.dtilStyle = LBS_STANDARD |WS_CHILD |WS_VISIBLE ; break; case DI7: /* vertical scrollbar */ DlgItem.dtilControlClass = SCROLLBARCLASS ; DlgItem.dtilStyle = SBS_VERT |WS_CHILD |WS_VISIBLE ; break; case DI8: /* horizontal scrollbar */ DlgItem.dtilControlClass = SCROLLBARCLASS ; DlgItem.dtilStyle = SBS_HORZ | WS_CHILD |WS_VISIBLE ; break; case DI9: /* group box */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_GROUPBOX | WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI10: /* Push button */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_PUSHBUTTON | WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI11: /* radio button */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_RADIOBUTTON | WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI12: /* default push button */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_DEFPUSHBUTTON |WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI13: /* left check box */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_LEFTTEXT |BS_CHECKBOX |WS_TABSTOP | WS_CHILD |WS_VISIBLE; break; case DI14: /* 3 auto state button */ DlgItem.dtilControlClass = BUTTONCLASS ; DlgItem.dtilStyle = BS_AUTO3STATE |WS_TABSTOP |WS_CHILD | WS_VISIBLE ; break; case DI15: /* centered edit control */ DlgItem.dtilControlClass = EDITCLASS ; DlgItem.dtilStyle = ES_CENTER | ES_MULTILINE | WS_BORDER | WS_TABSTOP | WS_CHILD | WS_VISIBLE ; break; case DI16: /* right edit control */ DlgItem.dtilControlClass = EDITCLASS ; DlgItem.dtilStyle = ES_RIGHT | ES_MULTILINE | WS_BORDER | WS_TABSTOP | WS_CHILD | WS_VISIBLE ; break; case DI17: /* left edit control */ DlgItem.dtilControlClass = EDITCLASS ; DlgItem.dtilStyle = ES_LEFT | WS_BORDER | WS_TABSTOP | WS_CHILD | WS_VISIBLE ; break; case DI18: /* listbox w/out sort */ DlgItem.dtilControlClass = LISTBOXCLASS ; DlgItem.dtilStyle = LBS_NOTIFY | WS_BORDER | WS_VSCROLL | WS_CHILD | WS_VISIBLE ; break; case DI19: /* center static text */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_CENTER |WS_BORDER |WS_CHILD |WS_VISIBLE ; break; case DI20: /* right static text */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_RIGHT | WS_CHILD | WS_VISIBLE ; break; default: /* left edit control */ DlgItem.dtilControlClass = STATICCLASS ; DlgItem.dtilStyle = SS_LEFT | WS_CHILD | WS_VISIBLE ; break; } /* end switch */ } WORD lstrlen( lpszString ) LPSTR lpszString; /* String to check */ { WORD Length; /* Length of string */ for ( Length = 0; *lpszString++ != '\0'; Length++ ); return( Length ); } Figure 5: DLGBOX.MAK is the Make Script for DLGBOX dlgtemp.obj: dlgtemp.c dlgbox.h cl -W2 -d -c -AM -Gsw -Os -Zpe dlgtemp.c dlgbox.obj: dlgbox.c dlgbox.h cl -W2 -d -c -AM -Gsw -Os -Zpe dlgbox.c dlgbox.res: dlgbox.rc dlgbox.h rc -r dlgbox.rc dlgbox.exe: dlgbox.obj dlgbox.def dlgbox.res dlgtemp.obj link4 dlgbox dlgtemp,dlgbox/align:16,dlgbox,mlibw,dlgbox.def rc dlgbox.res Figure 6: The Resource File for DLGBOX #include <style.h> #include "dlgbox.h" dlgicon ICON dlgbox.ico dlgbox MENU BEGIN POPUP "&DialogBox" BEGIN MENUITEM "&Modal", IDM_MODAL MENUITEM "M&odeless", IDM_MODELESS END END dlgtemp MENU BEGIN POPUP "&Top Menu" BEGIN MENUITEM "&One", IDM_ONE END END Figure 7: The DialogHeader Structure struct DialogHeader { LONG lDHStyle; BYTE cDHItemCount; int iDHX; int iDHY; int iDHCX; int iDHCY; /* The Variable part of the record starts here. */ char szDHResourceName[]; char szDHClassName[]; char szDHCaptionText[]; }; Figure 8: The DialogItem Structure struct DialogItem { int iDIX; int iDIY; int iDICX; int iDICY; int iDIID; LONG lDIStyle; BYTE cbDIControlClass; /* The Variable part of the record starts here. */ char szDIText[]; BYTE cbDIExtra[]; }; Figure 9: Predefined Control Classes BUTTONCLASS 0x80 EDITCLASS 0x81 STATICCLASS 0x82 LISTBOXCLASS 0x83 SCROLLBARCLASS 0x84 ════════════════════════════════════════════════════════════════════════════

Vol. 3 No. 6 Table of Contents

Building a Device-Independant Video Display I/O Library in Microsoft(R) C Though memory-mapped video is an important part of PC programming, the standard C run-time I/O library contains no specific support for the video subsystems of the IBM PC. This article offers a generalized approach to managing screen I/O. Developing SQL Server Database Applications Through DB-Library SQL Server's DB-Library provides programmers with a set of functions that can be used to issue SQL-based commands to access local or remote databases. This article demonstrates how to develop true distributed processing client/server applications for SQL Server. OS/2 Protected-Mode Programming with Forth, LISP, Modula-2, and BASIC A variety of interpreters and compilers are now available for OS/2 protected-mode programming. We examine how several of these languages handle function calls to the OS/2 API, data type conversion, and especially the use of dynamic-link libraries. The High Memory Area: Addressing 64Kb More Memory In Real Mode It is commonly believed that in real mode, the Intel 80286 and 80386 processors can only address 1Mb of memory, but it is possible to address almost 64Kb more. We explore the High Memory Area and its use with the Microsoft-Intel -Lotus Extended Memory Specification. Developing and Debugging Embedded Systems Applications Embedded systems are ROMable computer applications that perform dedicated control tasks, typically without an operating system. This article offers a high-level overview of the special techniques and tools needed to build embedded systems applications. C Scope and Linkage: The Keys to Understanding Identifier Accessibility Taking full advantage of the C language requires comprehending the semantics and usage of identifiers and the attributes that can be given to them. This article discusses scope, the visibility of an identifier in code, and linkage, the ability to resolve identifiers regardless of scope. Extending the Functions of the Windows Clipboard with Scrapbook+ The Windows clipboard, used to exchange data between applications, is restricted to one item at a time with no way to save its contents. ClickArt(R) Scrapbook+ acts as an agent for the clipboard and lets users store, retrieve, and examine data in a variety of formats. EDITOR'S NOTE Nearly every book, technical document, and article written about OS/2 to date uses C and Assembly language to provide programming examples, creating the illusion that these are the only viable languages for OS/2. Actually, in addition to C and Macro Assembler, Microsoft currently offers Pascal, COBOL, and FORTRAN for OS/2. It is interesting to note that rather than limiting the languages one can choose, OS/2's multitasking capabilities and extensibility via dynamic-link libraries (DLLs) are enhanced by the availability of additional languages represented by the compilers, interpreters, and integrated programming environments that are being ported to OS/2. In this issue, we examine four of these languages. Modula-2, built around the idea of the standalone module and multiple process, is a perfect match for OS/2 and interfaces very nicely with C. Access to the OS/2 Application Program Interface (API) fits naturally into UR/FORTH, a multitasking threaded-code interpreter and a stack oriented language. The interactive nature of LISP not only lets you type in OS/2 calls and immediately see their results, but LISP also knows about dynamic linking; access to the OS/2 API is coordinated through a few functions rather than scattered across hundreds of new ones. BASIC, traditionally requiring a run-time environment, fits right into OS/2, where the run-time environment is simply a DDL. The very nature of OS/2, coupled with these languages, challenges programmers to carry out ad hoc experiments to find the right tool for the job at hand or to create dynamic new applications. The article about OS/2 compilers proves that we don't only talk about C. But for our constituency of C aficionados we offer three articles in this issue──"Building a Device-Independent VIdeo Display I/O Library in Microsoft C;" "C Scopes and Linkage: The Keys to Understanding Identifier Accessibility;" and "Developing and Debugging Embedded Systems Applications."──Ed. Masthead JONATHAN D. LAZARUS Editor and Publisher EDITORIAL TONY RIZZO Technical Editor KAREN STRAUSS Assistant Editor JOANNE STEINHART Production Editor GERALD CARNEY Staff Editor MATTHEW LIGHT Copy Editor KIM HOROWITZ Editorial Assistant ART MICHAEL LONGACRE Art Director VALERIE MYERS Associate Art Director CARLOS BOLL Designer CIRCULATION STEVEN PIPPIN Circulation Director WILLIAM B. GRANBERG Circulation Manager L. PERRIN TOMICH Assistant to the Publisher JAANA NIEUWBOER Administrative Assistant Copyright(C) 1989 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 GW-BASIC are registered trademarks of Microsoft Corporation. IBM, PC/AT, and PS/2 are registered trademardk of International Business Machines Corporation. PC/XT is a trademark of International Business Machines Corporation. Apple and Macintosh are registered trademarks of Apple Computer, Inc. MultiFinder is a trademark of Apple Computer, Inc. PostScript is a registered trademark of Adobe Systems, Inc. Aldus and PageMaker are registered trademarks of Aldus Corporation. UNIX is a registered trademark of American Telephone & Telegraph Company. COMPAQ is a registered trademark of COMPAQ Computer Technology. Intel is a registered trademark of Intel Corporation. Epsilon is a trademark of Lugaru Software, Inc. Micrografx and Micrografx Designer are registered trademarks of Micrografx, Inc. The Norton Utilities is a trademark of Peter Norton Computing. ClickArt is a registered trademark of T/Maker Company. ████████████████████████████████████████████████████████████████████████████ Building a Device-Independent Video Display I/O Library in Microsoft C Jeff Prosise☼ Nearly every experienced C programmer who is developing applications for DOS has, at some time, had to write his own set of character-mode video functions. C's standard I/O library contains no specific support for the memory-mapped video subsystems of the IBM(R) PC. This generic quality of C preserves its portability across different operating systems and different machines. However, memory-mapped video is an inseparable component of PC programming. The outdated teletype interface has given way to carefully crafted color screens that distinguish microcomputer user interfaces from those of their mainframe cousins and are a mark of well-designed application programs. The professional C developer must have at his disposal a suite of high- performance video I/O routines. OS/2 provides these services through its VIO class of functions. For DOS developers, the Microsoft(R) C run-time library offers a large-model graphics package containing a powerful set of functions for character-mode video output, but it is neither as robust nor as concise in its syntax as its OS/2 counterpart. Generalized programming libraries rarely include everything you need for the task at hand, but a customized library can be tailored to meet specific needs, especially when core routines can be developed in assembly language. This article will lay the foundations for a high-performance, device- independent video I/O library and present a demonstration program that illustrates its use. The development platform is DOS and the Microsoft C Compiler Versions 5.0 and 5.1. When possible, functions are written in C; others are coded in assembly language using the Microsoft Macro Assembler (MASM) Versions 5.0 and 5.1. Design Criteria Performance and device independence are the two most important criteria in the design of video functions. Performance is the raw measure of the functions' speed of execution; device-independence is the measure of their ability to insulate the programmer from the video hardware. Speed is critical in the operation of video functions. Video output using BIOS routines does not compare with the instantaneous screen updates obtained when characters and attributes are written directly to the video buffer. The video BIOS writes directly to the buffer, but limits its access to the relatively infrequent intervals when the scanning electron beam is not displaying data. As a result, high performance can't be achieved with its inefficient character I/O services. But performance doesn't come without a price. When a programmer forsakes the BIOS, he also abandons the layer of firmware that shields him from the nuances of individual video adapters. Although most adapters can be treated the same in character mode once the color boards are sorted from the monochrome, certain architectures require special consideration. The design of IBM's Color Graphics Adapter (CGA), which does nothing to arbitrate requests for access to video RAM arriving simultaneously from the CPU and character-generating hardware, is a striking example. In the context of an all-around video function library, device independence means more than simply running on any adapter; it also means adapting to the different environments that the video adapters offer. Programs could once count on 25 lines of text per screen, but modern adapters permit more. A program must be able to assess its environment in terms of video mode, number of displayed rows and columns, monochrome or color, and other modals. Developing a common set of routines to provide this service eliminates the needless duplication of code when new applications are put together. Device Independence When IBM introduced the PC in 1981, it offered buyers a choice of two video adapters: the Monochrome Display Adapter (MDA) and the Color Graphics Adapter (CGA). The MDA featured sharp, readable text with characters formed from a 9-by-14-character matrix, but lacked graphics capability. The CGA offered three dot-addressable color graphics modes but could only display text by using a fuzzy 8-by-8-character matrix. As a result, the majority of users who dealt primarily with text opted for a monochrome display and adapter. Both the MDA and the CGA were built around the Motorola 6845 CRT Controller (CRTC). The 6845 is a highly programmable device designed to drive raster scan displays in a variety of modes and configurations. Since the two video adapters shared a common CRTC, their architectures were very similar. Both organized character-mode video memory as an interleaved sequence of ASCII characters and attribute bytes. IBM located the MDA video buffer at segment address B000H and the CGA buffer at B800H so that one system could accommodate both adapters simultaneously. Each was assigned unique and nonconflicting I/O addresses; however, only one video subsystem could be active at once. Recognizing that the middle ground between the CGA and MDA was wide open, in 1982 Hercules introduced the Hercules(R) Graphics Card (HGC). Based on the same CRTC as its IBM predecessors, the HGC offered MDA compatibility plus dot-addressable graphics on a monochrome display. For all practical purposes, an HGC running in text mode was identical to an MDA. Applications written especially for the HGC, however, enjoyed graphics capabilities previously available only on more expensive color systems. IBM countered with the Enhanced Graphics Adapter (EGA) in 1985. The EGA redefined the state of the art in video adapters by blending a crisp text mode with extensive color graphics support and the ability to produce graphics on a monochrome display. Although its designers gave up the 6845 in favor of a custom CRTC chip, the EGA retained a high degree of compatibility with the CGA and MDA. A distinguishing feature of the EGA was its ability to hardware-relocate its video buffer and I/O port addresses to emulate a CGA or an MDA. An EGA connected to a monochrome display located its video buffer at B000H. The same adapter paired with a color display mapped its video buffer to segment B800H. A properly configured EGA could be placed into a PC alongside an MDA to drive the color half of a dual monitor system or next to a CGA to drive the monochrome half. Another feature of the EGA was its ability to display more than 25 lines of text on a single screen. Both the CGA and MDA were limited to 25, but the EGA could be reprogrammed to display almost any number of rows permitted by its 350 scan lines of resolution. Programs were suddenly confronted with a new variable in the video environment. Because the EGA BIOS offered no direct support in helping a program determine the display density, a new level of hardware dependence was established. MCGA and VGA The most recent advance in mainstream video hardware came from IBM in the form of the PS/2(R) Multi-Color Graphics Array (MCGA) and Video Graphics Array (VGA). Both are functionally similar to the EGA, but unlike earlier IBM adapters, the MCGA and VGA generate analog RGB signals capable of driving analog color and monochrome monitors. Since an analog monochrome monitor can display up to 64 shades of gray using essentially the same signal as a color monitor, the MCGA and VGA are capable of driving both at once. What appears on one screen in color will appear in black-and- white on the other. On an EGA, the video modes available depended on the type of display attached. Modes 7 and 15 were reserved for monochrome. The VGA is capable of switching to any supported mode, monochrome or color, regardless of the monitor. Both the MCGA and the VGA employ a gray-scale summing algorithm that enables black-and-white displays to be treated as color and vice versa. A VGA running in mode 7 or 15 will locate its video buffer and I/O ports in monochrome address space, while a VGA in any other mode will configure itself as a color adapter. From the standpoint of a program wanting to access the video buffer for high-speed screen updates, all of the adapters obey a common set of rules. Monochrome video memory is always found at B000H, color at B800H. Determining whether an adapter is configured for color or monochrome is as simple as checking the word at 0040:0063 in the BIOS Data Area, which holds the I/O port address of the CRTC. A value of 3B4H indicates the system is monochrome──either an MDA, an HGC, an EGA connected to a monochrome display, or a VGA running in mode 7 or 15-while 3D4H identifies a color system. If more than one adapter is installed, the CRTC address reflects the mode the system is currently running in. If it weren't for the CGA, a program's concerns for device independence would begin and end with the location of the video buffer. However, the CGA poses an additional problem: when a program tries to read from or write to the video buffer in text mode, the CPU must contend with the adapter's display refresh circuitry for access to video RAM. Since the CPU is given priority, an ugly, snowlike interference results. A program can sidestep this interference problem in one of three ways: by blanking the screen before accessing video memory, by limiting memory accesses to the vertical blanking interval, or by limiting them to the horizontal blanking interval. Blanking intervals are the brief periods of time when the electron beam scanning the CRT screen is inactive while moving from the end of one display row to the beginning of the next or from the bottom of the screen to the top. These movements are called retraces, and blanking intervals are often synonymously referred to as retrace intervals. Each method is best suited for a particular situation. The video BIOS disables video output entirely-effectively blanking the display──when it scrolls the screen, producing a brief but perceptible flicker. It uses horizontal blanking intervals to move single character/attribute pairs to or from the video buffer. Using horizontal retraces is preferable to using vertical retraces because CGA horizontals occur 200 times per refresh frame, while verticals occur only once. To synchronize video memory accesses with vertical or horizontal retraces, an application uses the CRT Status Register, located at I/O address 3DAH on a CGA. Two bits in the CRT Status Register reflect the status of the moving electron beam: bit 0 for the horizontal retrace and bit 3 for the vertical. A high bit means a retrace is in progress. By monitoring the desired bit and looping until its polarity changes from low to high, a program can detect the beginning of a blanking interval and update the video buffer at a time when the CPU will have uncontested access to video RAM. Video routines that manipulate the contents of the video buffer without the help of the BIOS must be sensitive to the environment they're running in. Video RAM accesses must be timed to occur during nondisplay periods in the refresh cycle on adapters that lack dual-ported RAM or arbitration circuitry. Reads and writes must be directed to the proper video buffer address. All of this must be made transparent through a set of function protocols that eliminates the need for the calling program to have any intimate contact with the video hardware. Performance If the CGA were the only adapter whose BIOS synchronized video RAM accesses with the horizontal retrace, C video functions might simply rely on the BIOS for character I/O. But in the design of the original PC, the video BIOS was part of the larger ROM BIOS that provided a variety of system-related services. The same performance penalty imposed on systems equipped with a CGA was imposed on monochrome systems as well. Even the EGA, which packed its own on-board video BIOS, needlessly restricted video buffer activity to the horizontal blanking interval. The BIOS' blind insistence on using the horizontal retrace isn't all that slows it down. The original PC BIOS offered I/O routines for individual characters and attributes only, and nothing for strings. When a string is output a character at a time, the video buffer address must be generated separately for each individual character. An efficient string output routine eliminates redundancy by calculating the address corresponding to the first character in the string, then offsetting that address for succeeding characters. Also, with character-oriented operations, the cursor must be repositioned before every I/O call. Overhead is increased while performance is markedly decreased. A true high-performance, device-independent video library must bypass the video BIOS. Its functions must also distinguish between CGA and non-CGA adapters and treat them differently. The best strategy is to move character data into and out of the video buffer during the horizontal blanking interval on a CGA and to read and write in an uninhibited manner to all other adapters. The adapters are easily enough identified; the trick lies in dealing properly with the CRT Status Register when a CGA is detected. Because of the stack overhead and compiler inefficiency involved in executing and returning from a C function call, a C program cannot act in the short amount of time after the Status Register's horizontal retrace bit goes high but before the horizontal retrace is completed. A CGA horizontal retrace lasts for approximately 7 microseconds. On a 4.77-MHz PC, that's enough time to move one 16-bit word to or from the video buffer. Figure 1 shows the assembly code needed to write a word to the buffer. It assumes that, on entry, AX holds the word to be written. In actuality, only every other horizontal retrace is used. The first loop waits for the CRT Status Register's rightmost bit to go low to eliminate the possibility of entering the thread at the exact moment when a retrace is in progress but almost over. In most instances, the first comparison will fall right through. The second loop waits for the bit to go high, indicating that a horizontal retrace has begun. The instruction STOSW transfers the data. Interrupts are disabled to prevent an external event from interrupting this time-critical sequence. The instruction used to retrieve the word that was originally stored in AX from CX just before the move: XCHG AX,CX was not chosen arbitrarily. As Richard Wilton points out in his book, Programmer's Guide to PC and PS/2 Video Systems (Microsoft Press, 1987), using a MOV instruction in the same place on a 4.77-MHz 8088-based machine will produce snow. The reason: the 1-byte XCHG and STOSW instructions will fit together with the instruction immediately preceding them in the 8088's 4-byte instruction prefetch queue; MOV and STOSW will not. The delay resulting from an intervening RAM access is just enough to cause the code to overshoot the horizontal blanking interval. Were it not for the severe time constraints placed on this routine, the same end could be achieved with the snippet of C code shown in Figure 2, but implementing this on a CGA-equipped system will produce snow. Why? Because there are so many machine code instructions executed after the Status Register is read, the horizontal retrace has long since ended when the word is finally moved. Figure 3 shows the unoptimized code Microsoft C 5.0 generates for the line "while ((inp(0x3DA) & 0x01) == 0);", with the code for the inp function inserted and labels renamed for clarity. After the Status Register is read with an IN instruction, eight more instructions consuming 65 clock cycles on an 8088 (equivalent to almost 14 microseconds at 4.77 MHz) are executed before the code that transfers the data is reached. Even if we're lucky enough to have caught the very beginning of the blanking interval, we have no chance of getting anything done before it's over. The solution is to resort to assembly language when speed is of the essence. The majority of library functions can be implemented in C if a small kernel of fast and efficient assembler routines is available for time-critical operations. Source Modules The video functions presented here are divided between two source files, CVIDEO.C and ASMVIDEO.ASM. Figure 4 shows CVIDEO.C, which is a collection of C functions. ASMVIDEO.ASM, shown in Figure 5, supplies a set of assembly language functions compatible with small-model C programs. Both are designed to be compiled or assembled into independent OBJ files and linked with a primary C program module. ASMVIDEO.ASM requires the use of MASM 5.0 or later, as it takes advantage of Version 5.0 and 5.1's simplified segment directives for interfacing MASM-generated code with high-level languages. The source code for DEMO.C, a demonstration program, appears in Figure 6. DEMO.C uses functions from the video library to clear the screen, fill it with text, and open and close a series of windows. DEMO runs equally well on any type of video hardware. If you are using a monochrome monitor, the program will automatically adjust its display attributes to accommodate it. Instructions for compiling CVIDEO.C and DEMO.C, assembling ASMVIDEO.ASM, and linking the three together appear in Figure 7v3n6a1f7. MASM is invoked by using the /ML command line switch to conform to C conventions where names are case-sensitive. For the same reason, LINK is run with the parameter /NOI. Video Functions A summary of the nine video functions and their calling conventions appears in Figure 8. The character and string I/O functions distinctly resemble OS/2's VIO functions. DispString, for instance, is similar to OS/2's VioWrtCharStrAtt. Both eliminate the need to position a logical cursor by taking a row and column address as arguments, both accept a string address and attribute byte (or the address of an attribute byte), and both require an auxiliary parameter fundamental to their operation. With VioWrtCharStrAtt, it's a Vio handle; with DispString, it's a pointer to a structure. As you'll see in a moment, this structure plays a vital role in video function performance. Three functions are provided to manipulate a specific region of the screen without disturbing what surrounds it: ClrRegion, SaveRegion, and RestRegion. ClrRegion clears a region, SaveRegion copies the contents of a region to a buffer in word-sized chunks, and RestRegion complements SaveRegion by restoring a region whose contents were previously saved with SaveRegion. A related function, ClrScr, clears the entire viewing area, regardless of the number of rows and columns displayed. TextBox is thrown in to draw borders surrounding screen regions, which is handy if you're using SaveRegion and RestRegion for windowing support. The Region functions take the parameters row1, col1, row2, and col2 as input. Row1 and col1 represent the row and column of the upper left corner of the region, while row2 and col2 give the screen coordinates of the lower right corner. SaveRegion and RestRegion also accept the address of an integer array where screen contents are stored. VideoAttr is a standard attribute byte, where the upper nibble represents background color and the lower nibble represents foreground color. Screen coordinates are zero-based in all functions that are passed row and column parameters. Two functions are provided for the display of individual character/attribute pairs and strings. DispCharAttr and DispString are implemented in assembly language for performance reasons; both write directly to the video buffer for fast display speeds. GetCharAttr returns an integer representing the character and attribute at a specified row and column address. GetCharAttr is analogous to the OS/2 VioReadCellStr function with a buffer length of 1. Like DispCharAttr and DispString, GetCharAttr is written in assembly language and accesses the video buffer directly. On a CGA, GetCharAttr synchronizes itself with the horizontal retrace. GetVideoParms must be called before any other video function. It is passed a pointer to a structure of type VideoInfo, defined in the header file VIDEO.H as follows: struct VideoInfo { unsigned char mode; unsigned char rows; unsigned char columns; unsigned char ColorFlag; unsigned char SyncFlag; unsigned int BufferStart; unsigned int SegAddr; }; The byte-sized variable mode represents the current video mode. Rows holds the number of displayed screen rows, and columns holds the number of characters per row. ColorFlag is set to 0 on a monochrome system or 1 on a color system. SyncFlag returns 0 for all adapters except a CGA. SegAddr holds the current video buffer segment address, and BufferStart holds the offset into the video buffer segment of the active video page. Any of these parameters can be used directly by the calling program to make critical determinations about the video environment. If a program finds it's running in a 43-line mode, for example, it may choose to take advantage of all 43 rows rather than limiting itself to the first 25. Also it may use ColorFlag to adjust video attributes for monochrome or color displays. Inserting a piece of code at the beginning of a program similar to the one below will make its display functions independent of display type: GetVideoParms(&video); if (video.ColorFlag) { color1 = 0x1F; color2 = 0x1E; } else { color1 = 0x07; color2 = 0x70; } If the video environment is color, the variables color1 and color2 are assigned color attributes; if monochrome, they're assigned the more generic values 0x07 and 0x70. However, GetVideoParms and the VideoInfo structure have an even greater significance: all of the remaining eight functions except one, ClrRegion, are passed a pointer to the structure initialized by GetVideoParms. ClrScr has to know how many rows and columns are displayed to clear the screen; VideoInfo supplies those parameters. DispCharAttr, DispString, and GetCharAttr, the three functions that move data between conventional RAM and video RAM, use SyncFlag to determine whether or not to synchronize buffer accesses with the horizontal blanking interval. SegAddr and BufferStart are used to determine the segment:offset address of the active video page, and columns is used to calculate addresses within a page. In short, the VideoInfo structure offers a mechanism for supplying commonly needed information to a variety of otherwise independent routines. GetVideoParms normally needs to be called only once in the lifetime of a program. The one possible exception to this rule is when it is used in a TSR environment. Since most memory-resident programs can be triggered by an asynchronous external event like a keystroke or timer interrupt, the video environments they inherit may be different each time. The solution is to call GetVideoParms to reinitialize the VideoInfo structure each time your TSR is brought to the foreground. Demonstration Program DEMO is a short demonstration program that is designed to run in any 80- column text mode that illustrates the proper techniques for using the video I/O routines presented in this article. It includes the header file VIDEO.H, which contains the VideoInfo structure definition and function prototypes for all library routines. The listing for VIDEO.H is shown in Figure 9. DEMO begins by declaring the structure video to be of type VideoInfo and passing GetVideoParms a pointer to it. The structure is initialized, and video.ColorFlag is checked to determine whether the current video environment is monochrome or color. The attributes WinColor1, WinColor2, and WinColor3 are set accordingly. The screen is cleared and filled from top to bottom with text. Rather than assuming 25 rows are displayed, the program uses video.rows to determine how many times DispString should be called. Then three windows are opened, one after another, with a brief pause between each one. SaveRegion copies the underlying contents of each region to a buffer, ClrRegion clears the region, TextBox borders it, and DispString fills it. The windows are closed in the reverse order in which they were opened with RestRegion. Finally, the screen is recleared and control handed back to DOS. Once GetVideoParms is invoked, DEMO intrinsically has all the information that it needs to handle its assorted video tasks. It doesn't need to know what sort of video adapter it's running on, how many adapters might be installed, or whether the adapter needs the special kind of treatment accorded a CGA. It doesn't have to hunt around to find out whether it's running on a monochrome or color display, and the dimensions of the screen are immediately available to it. The programmer is left free to concentrate on the program's design and functionality and isn't burdened by the fact that his code may be used on any of a number of different pieces of video hardware. Video Parameters The process GetVideoParms must go through to determine what type of video hardware it's running on and establish resultant video parameters merits a closer look. It begins by calling Function 12H of Interrupt 10H with BL set to 10H. If an EGA, VGA, or MCGA is installed, the BL register will return with a value other than the 10H it held on entry. If BL returns unchanged, GetVideoParms assumes either a CGA, MDA, or HGC is present and active and tests for a CGA by checking the CRTC address held in the BIOS Data Area. If the CRTC address corresponds to that of a color adapter, GetVideoParms concludes it has just found a CGA and sets SyncFlag to 1. If the Function 10H test indicates an EGA or equivalent is installed, GetVideoParms tests bit 3 of the byte at 0040:0087 to determine if it's the adapter currently active, just in case more than one is installed. If the EGA is the active adapter, SyncFlag is set to 0; if it's not and the CRTC address indicates the video environment is monochrome, SyncFlag is still 0; if the EGA isn't active and the CRTC address corresponds to color, then SyncFlag is set to 1. The latter case is symptomatic of a CGA sharing a system with an EGA configured for monochrome use. If an EGA, VGA, or MCGA is installed and active, the number of displayed rows is pulled from the BIOS Data Area at address 0040:0084. The number of characters per row, the current video mode, and the offset address in the video segment of the active video page are likewise taken from the BIOS Data Area. ColorFlag is simply set to 0 if the CRTC address is 3B4H, or set to 1 if it's 3D4H. All these methods will probably remain compatible with future video hardware. There will, we hope, be no more video adapters with the quirky characteristics of the CGA, and since so many programs rely on the BIOS Data Area, particularly the video portion of it, IBM has promised to leave it intact. MASM Functions The three MASM functions DispCharAttr, DispString, and GetCharAttr are designed to be used with small-model C programs but are easily adapted to other memory models. MASM's new MODEL directive makes the changeover relatively simple in terms of keeping naming conventions consistent with those used by the C compiler. The only other consideration is adjusting references to parameters passed on the stack when a memory model with far return addresses is used. Both DispCharAttr and DispString employ a subroutine named WriteWithWait to write a word to the CGA video buffer. WriteWithWait is nearly identical to the code in Figure 1. Writes are only performed during the horizontal blanking interval. A second MASM subroutine, AddressOf, is called by all three functions to calculate the video buffer address that corresponds to a given row and column on the active page. Neither WriteWithWait nor AddressOf is accessed directly by C functions, so neither is declared public. All three functions are passed a pointer to the VideoInfo structure. To establish addressability, the BX register is loaded with the pointer and all references to data within the structure are made relative to BX using based mode addressing. Since BX and BP are used to address data, neither is used for any other purpose in these functions. Once the register values are set on entry, they are preserved until exit. Figure 10 shows the layout of the VideoInfo structure in the program's data segment and how its individual elements are referenced with BX. Note that the compiler places an empty byte between SyncFlag and BufferStart to align the latter on a word boundary. Building a Video Library The functions presented here can serve as the groundwork for a more extensive video I/O library. OS/2's VioScrollDn and VioScrollUp functions are relatively easy to duplicate in the DOS environment using straight BIOS calls. Another useful function would be a counterpart to OS/2's VioWrtNAttr, which permits a number of consecutive cells to be painted with a specified attribute. The function could use VideoInfo as its nucleus and AddressOf to calculate video addresses. VideoInfo itself could even be expanded to provide more information such as CRTC addresses, number of scan lines per character, or the amount of on-board display memory. DOS invites creative and time-saving solutions such as these; OS/2 eliminates them. Programmers should welcome the arrival of an operating system that provides device independence and high performance in its kernel routines and wholly insulates them from the vagaries of hardware programming. Nevertheless, DOS will continue to be around for the forseeable future, and tools such as those presented here will remain a required part of every programmer's toolkit. Figure 1: MASM code to transfer a word to the video buffer during the horizontal blanking interval. mov cx,ax ; store word value in CX mov dx,3DAH ; CRT Status Register cli ; interrupts off wait1: in al,dx ; wait for non-retrace test al,1 ; period jnz wait1 wait2: in al,dx ; wait for start of test al,1 ; next horizontal jz wait2 ; retrace xchg ax,cx ; retrieve word stosw ; then write it sti ; interrupts on Figure 2: C code to transfer a word to the video buffer during the horizontal blanking interval. while ((inp(0x3DA) & 0x01) != 0); while ((inp(0x3DA) & 0x01) == 0); *WordData1 = WordData2; Figure 3: Code generated by the Microsoft C Compiler 5.0 for "while ((inp(0x3DA) & 0x01) == 0);". _main1: MOV AX,03DAH PUSH AX CALL _inp PUSH BP MOV BP,SP MOV DX,Word Ptr [BP+04] IN AL,DX XOR AH,AH MOV SP,BP POP BP RET ADD SP,+02 TEST AL,01 JZ _main2 JMP _exit _main2: JMP _main1 Figure 4: Source Code for CVIDEO.C /*================================================================= CVIDEO.C - C video modules. ================================================================== */ #include <dos.h> struct VideoInfo { unsigned char mode; unsigned char rows; unsigned char columns; unsigned char ColorFlag; unsigned char SyncFlag; unsigned int BufferStart; unsigned int SegAddr; }; void GetVideoParms(struct VideoInfo *); void ClrScr(char, struct VideoInfo *); void ClrRegion(char, char, char, char, char); void TextBox(char, char, char, char, char, struct VideoInfo *); void SaveRegion(char, char, char, char, int *, struct VideoInfo *); void RestRegion(char, char, char, char, int *, struct VideoInfo *); extern int GetCharAttr(char, char, struct VideoInfo *); extern void DispCharAttr(char, char, char, char, struct VideoInfo *); struct VideoInfo video; union REGS inregs, outregs; /* ====================================================================== GetVideoParms() fills a video structure of type VideoInfo with data. ======================================================================= */ void GetVideoParms(struct VideoInfo *sptr) { char far *CrtMode = (char far *) 0x00400049; char far *CrtCols = (char far *) 0x0040004A; int far *CrtStart = (int far *) 0x0040004E; int far *CrtAddr = (int far *) 0x00400063; char far *CrtRows = (char far *) 0x00400084; char far *CrtEgaInfo = (char far *) 0x00400087; /* --- Determine whether video writes must be synchronized --- */ inregs.h.ah = 0x12; inregs.h.bl = 0x10; int86(0x10, &inregs, &outregs); if (outregs.h.bl == 0x10) { if (*CrtAddr == 0x3D4) sptr->SyncFlag = 1; else sptr->SyncFlag = 0; sptr->rows = 25; } else { if ((*CrtEgaInfo & 0x08) && (*CrtAddr == 0x3D4)) sptr->SyncFlag = 1; else sptr->SyncFlag = 0; sptr->rows = *CrtRows + 1; } /* ---- Determine whether video is color or monochrome ----*/ if (*CrtAddr == 0x3D4) { sptr->ColorFlag = 1; sptr->SegAddr = 0xB800; } else { sptr->ColorFlag = 0; sptr->SegAddr = 0xB000; } /* ----- Copy remaining parameters from BIOS ----- */ sptr->mode = *CrtMode; sptr->columns = *CrtCols; sptr->BufferStart = *CrtStart; } /* ================================================================== ClrScr () clears the entire viewing area. =================================================================== */ void ClrScr (char VideoAttr, struct VideoInfo *sptr) { inregs.x.ax = 0x0600; inregs.h.bh = VideoAttr; inregs.x.cx = 0x0000; inregs.h.dh = sptr->rows - 1; inregs.h.dl = sptr->columns - 1; int86(0x10, &inregs, &outregs); } /* ================================================================== ClrRegion () clears the specified screen region. =================================================================== */ void ClrRegion (char row1, char col1, char row2, char col2, char VideoAttr) { inregs.x.ax = 0x0600; inregs.h.bh = VideoAttr; inregs.x.ch = row1; inregs.h.cl = col1; inregs.h.dh = row2; inregs.h.dl = col2; int86(0x10, &inregs, &outregs); } /* ================================================================== TextBox () draws a box around the specified region. =================================================================== */ void TextBox (char row1, char col1, char row2, char col2, char VideoAttr, struct VideoInfo *sptr) { char i; DispCharAttr (0xDA, row1, col1, VideoAttr, sptr); for (i=col1+1; i<col2; i++) DispCharAttr (0xC4, row1, i, VideoAttr, sptr); DispCharAttr (0xBF, row1, col2, VideoAttr, sptr); for (i=fow1+1; i<row2; i++) DispCharAttr (0xB3, i, col2, VideoAttr, sptr); DispCharAttr (0xD9, row2, col2, VideoAttr, sptr); for (i=cold2-1; i>col1; i--) DispCharAttr (0xC4, row2, i, VideoAttr, sptr); DispCharAttr (0xC0, row2, col1, VideoAttr, sptr); for (i=row2-1; i>row1; i--) DispCharAttr (0xB3, i, col1, VideoAttr, sptr); } /* ================================================================== SaveRegion () saves the contents of a specified screen region. =================================================================== */ void SaveRegion (char row1, char col1, char row2, char col2, int *ScreenBuffer, struct VideoInfo *sptr) { char i, j; for (i=row1; i<=row2; i++) for (j=col1; j<=col2; j++) *ScreenBuffer++ = GetCharAttr (i, j, sprt); } /* ====================================================================== RestRegion () restores the contents of a specified screen region. ========================================================================= */ void RestRegion (char row1, char col1, char row2, char col2, int *ScreenBuffer, struct VideoInfo *sptr) { char i,j; for (i=row1; i<=row2; i++) for (j=col1; j<=col2; j++) DispCharAttr ((char) (ScreenBuffer++ & 0x00FF), i, j, (char) (ScreenBuffer >> 8), sptr); } Figure 5: Source Code for ASMVIDEO.ASM ;==================================================================== ; ASMVIDEO.ASM - Macro Assembler video modules for ; small model C programs. ;==================================================================== .MODEL SMALL .CODE PUBLIC _DispString, _DispCharAttr, _GetCharAttr ;-------------------------------------------------------------------- ; _DispString writes a string to the active video buffer. ;-------------------------------------------------------------------- _DispString PROC push bp ; save registers and establish mov bp,sp ; stack frame addressability push si push di mov bx,[bp+12] ; get pointer to VideoInfo mov si,[bp+4] ; get pointer to string mov dh,[bp+6] ; calculate video address mov dl,[bp+8] call AddressOf mov es,[bx+8] ; point ES to video buffer mov ah,[bp+10] ; get video attribute in AH cld ; clear DF dstr1: lodsb ; get next character or al,al ; terminate on null byte jz dstr3 test byte ptr [bx+4],1 ; branch if SyncFlag is clear jz dstr2 call WriteWithWait ; write during retrace jmp dstr1 dstr2: stosw ; write one character/attribute jmp dstr1 ; loop back for more dstr3: pop di ; restore registers and exit pop si pop bp ret _DispString ENDP ;-------------------------------------------------------------------- ; _DispCharAttr writes a character and attribute to the active ; video buffer. ;-------------------------------------------------------------------- _DispCharAttr PROC push bp ; save registers and establish mov bp,sp ; stack frame addressability push di mov bx,[bp+12] ; get pointer to VideoInfo mov dh,[bp+6] ; calculate video address mov dl,[bp+8] call AddressOf mov es,[bx+8] ; point ES to video buffer mov al,[bp+4] ; get character/attribute mov ah,[bp+10] ; in AX test byte ptr [bx+4],1 ; branch if SyncFlag is clear jz dca1 call WriteWithWait ; write during retrace jmp short dca2 ; then exit dca1: stosw ; write one character/attribute dca2: pop di ; restore registers and exit pop bp ret _DispCharAttr ENDP ;-------------------------------------------------------------------- ; _GetCharAttr returns the character and attribute at the ; specified address. ;-------------------------------------------------------------------- _GetCharAttr PROC push bp ; save registers and establish mov bp,sp ; stack frame addressability push di mov bx,[bp+8] ; get pointer to VideoInfo mov dh,[bp+4] ; calculate video address mov dl,[bp+6] call AddressOf ; offset in DI mov es,[bx+8] ; point ES to video buffer test byte ptr [bx+4],1 ; branch if SyncFlag is clear jz gca3 mov dx,3DAh ; point DX to CRTC status reg gca1: in al,dx ; wait for non-retrace period test al,1 jnz gca1 gca2: in al,dx ; wait for next horizontal test al,1 ; retrace jz gca2 gca3: mov ax,es:[di] ; get character and attribute pop di ; restore registers and exit pop bp ret _GetCharAttr ENDP ;-------------------------------------------------------------------- ; AddressOf returns the video buffer address that ; corresponds to the specified row and column. ; ; Entry: DS:BX - address of VideoInfo structure ; DH,DL - row and column ; Exit: DI - offset address ;-------------------------------------------------------------------- AddressOf PROC NEAR mov al,[bx+2] ; get columns per row mul dh ; multiply by row number xor dh,dh ; add column number add ax,dx shl ax,1 ; multiply by 2 add ax,[bx+6] ; add offset of active page mov di,ax ; transfer result to DI ret ; and exit AddressOf ENDP ;-------------------------------------------------------------------- ; WriteWithWait writes a character and attribute to a CGA during ; the horizontal blanking interval. ; ; Entry: ES:DI - video buffer address ; AH,AL - character/attribute ;-------------------------------------------------------------------- WriteWithWait PROC NEAR mov cx,ax ; save character/attribute mov dx,3DAh ; point DX to Status Register cli ; disable interrupts www1: in al,dx ; wait for non-retrace period test al,1 jnz www1 www2: in al,dx ; wait for next horizontal test al,1 ; retrace jz www2 xchg ax,cx ; retrieve character/attribute stosw ; write them to video memory sti ; enable interrupts ret WriteWithWait ENDP END Figure 6: Source Code for DEMO.C /* ================================================================== DEMO.C - Program demonstrating the use of video functions. Copyright(C) 1988 Microsoft Systems Journal Compile with: cl /c demo.c link /noi demo+cvideo+asmvideo; ================================================================== */ #include "video.h" #include <time.h> struct VideoInfo video; int buffer[3][510]; main() { char TextColor = 0x07; char BorderColor = 0x0F; char WinColor1, WinColor2, WinColor3; int i, j; /* ----- Initialize video structure -----*/ GetVideoParms(&video); /* ----- Set attributes for monochrome or color -----*/ if (video.ColorFlag) { WinColor1 = 0x1F; WinColor2 = 0x4F; WinColor3 = 0x6F; } else { WinColor1 = 0x0F; WinColor2 = 0x70; WinColor3 = 0x0F; } /* ----- Clear the screen and fill it with text -----*/ ClrScr(TextColor, &video); for (i=0; i<video.rows; i++) for (j=0; j<=52; j+=26) DispString("Microsoft Systems Journal", i, j, TextColor, &video); delay(2.0); /* ----- Open a window -----*/ SaveRegion(5, 2, 14, 34, buffer[0], &video); ClrRegion(6, 3, 13, 33, WinColor1); TextBox(5, 2, 14, 34, BorderColor, &video); for (i=6; i<14; i++) DispString("Open the first window here...", i, 4, WinColor1, &video); delay(2.0); /* ----- Open a second window -----*/ SaveRegion(2, 48, 12, 74, buffer[1], &video); ClrRegion(3, 49, 11, 73, WinColor2); TextBox(2, 48, 12, 74, BorderColor, &video); for (i=3; i<12; i++) DispString("Then the second here...", i, 50, WinColor2, &video); delay(2.0); /* ----- Open a third window overlapping the first two -----*/ SaveRegion(9, 25, 22, 60, buffer[2], &video); ClrRegion(10, 26, 21, 59, WinColor3); TextBox(9, 25, 22, 60, BorderColor, &video); for (i=10; i<22; i++) DispString("And finally a third window here.", i, 27, WinColor3, &video); delay(4.0); /* ----- Close all windows and exit -----*/ RestRegion(9, 25, 22, 60, buffer[2], &video); delay(1.0); RestRegion(2, 48, 12, 74, buffer[1], &video); delay(1.0); RestRegion(5, 2, 14, 34, buffer[0], &video); delay(1.0); ClrScr(TextColor, &video); } /* ----- Timed delay function -----*/ delay(double sec) { time_t StartTime, EndTime; time(&StartTime); time(&EndTime); while (difftime(EndTime, StartTime) < sec) time(&EndTime); } Figure 7: Instructions to compile and link the library modules and demonstration program. ASMVIDEO.ASM masm /ml asmvideo; CVIDEO.C cl /c cvideo.c DEMO.C cl /c demo.c link /noi demo+cvideo+asmvideo; Figure 8: Video Function Summary void GetVideoParms(struct VideoInfo *sptr) Fills a structure of type VideoInfo with information reflecting the current video mode, the number of displayed rows and columns, whether video is color or monochrome, whether or not video buffer accesses should be synchronized with blanking intervals, and the video buffer address. void ClrScr(char VideoAttr, struct VideoInfo *sptr) Clears the screen using the specified video attribute. void ClrRegion(char row1, char col1, char row2, char col2, char VideoAttr) Clears a region using the specified video attribute. void TextBox(char row1, char col1, char row2, char col2, char VideoAttr, struct VideoInfo *sptr) Draws a box around a region using single-line graphics characters and the specified video attribute. void SaveRegion(char row1, char col1, char row2, char col2, int *buffer, struct VideoInfo *sptr) Saves the contents of a screen region to a buffer. void RestRegion(char row1, char col1, char row2, char col2, int *buffer, struct VideoInfo *sptr) Copies the contents of a buffer to a screen region. void DispCharAttr(char asciicode, char row, char col, char VideoAttr, struct VideoInfo *sptr) Displays a single character at the specified row and column address using the specified video attribute. void DispString(char *sptr, char row, char col, char VideoAttr, struct VideoInfo *sptr) Displays a string at the specified row and column address using the specified video attribute. int GetCharAttr(char row, char col, struct VideoInfo *sptr) Returns the character and attribute at the specified row and column address. Figure 9: Source Code for VIDEO.H /* VIDEO.H header file */ struct VideoInfo { unsigned char mode; unsigned char rows; unsigned char columns; unsigned char ColorFlag; unsigned char SyncFlag; unsigned int BufferStart; unsigned int SegAddr; }; /* ----- C function prototypes -----*/ extern void GetVideoParms(struct VideoInfo *); extern void ClrScr (char, struct VideoInfo *); extern void ClrRegion (char, char, char, char, char); extern void TextBox (char, char, char, char, char, struct VideoInfo *); extern void SaveRegion(char, char, char, char, int *, struct VideoInfo *); extern void RestRegion(char, char, char, char, int *, struct VideoInfo *); /* ----- Macro Assembler function prototypes -----*/ extern int GetCharAttr(char, char, struct VideoInfo *); extern void DispCharAttr(char, char, char, char, struct VideoInfo *); extern void DispString(char *, char, char, char, struct VideoInfo *); Figure 10: Layout of the VideoInfo structure in the data segment. ┌─────────────┐ 2 │ SegAddr │ BX+8 ├─────────────┤ 2 │ BufferStart │ BX+6 ├─────────────┤ 1 │ SyncFlag │ BX+4 ├─────────────┤ 1 │ ColorFlag │ BX+3 ├─────────────┤ 1 │ columns │ BX+2 ├─────────────┤ 1 │ rows │ BX+1 ├─────────────┤ 1 │ mode │ BX+0 └─────────────┘ ████████████████████████████████████████████████████████████████████████████ Developing SQL Server Database Applications Through DB-Library Marc Adler☼ SQL Server is one of the latest applications in the growing set of Microsoft(R) systems software products for the new generation of personal computers and OS/2 systems. In "SQL Server Brings Distributed DBMS Technology to OS/2 Via LAN Manager" (MSJ, Vol. 3 No. 4), we discussed the concepts and features found in SQL Server and the ramifications it has for the future of database processing. In this issue, we will demonstrate how to access SQL Server from within C programs by examining SQL Server's Application Program Interface (API), DB-Library, which is part of the SQL Server Software Developers Pack. DB-Library provides a set of functions that can be used in C programs to access databases, issue SQL-based commands and queries, and extract data. Unlike database APIs offered by some manufacturers, there is no "embedded SQL" or SQL precompiler to worry about. With an embedded SQL interface, the programmer codes actual SQL commands into a C program by writing those commands and prefixing them with a certain token, in much the same way that the C preprocessor recognizes directives that are prefixed with the pound sign (#). This means that the C program containing the SQL statements must be preprocessed with a manufacturer's embedded SQL preprocessor before the program is actually compiled; the preprocessor will generate a series of function calls with the manufacturer's API. DB-Library eliminates this step, allowing you to send any Transact:SQL command to SQL Server by issuing a simple function call. The functionality of DB-Library, combined with the APIs for Microsoft's LAN Manager, Presentation Manager, and OS/2, will enable programmers to design new applications that were previously incapable of running on personal computers. With these products, true distributed database processing along with sophisticated graphical interfaces will be available to every PC user. Main DB-Library Structures Almost every function in DB-Library uses a DBPROCESS structure. This structure contains information about your SQL Server connection, the current state of pending commands, data resulting from the commands, and error and information messages. To communicate with SQL Server, you must have an active DBPROCESS structure that is returned when a dbopen function call is made. The DBPROCESS structure is shown in Figure 1. You can have more than one DBPROCESS structure active in the same program. Two DBPROCESS structures can be connected to the same server or to different servers. Each DBPROCESS structure functions much like a cursor; just as a cursor holds information about the current position on the screen or within a file, each DBPROCESS structure contains information about the current processing state on each server. For example, if you want to transfer data from one server to another, you can issue a SELECT statement on one server and an INSERT command on the other. As you fetch a row from the first server, an INSERT command could be issued on the other server. Having multiple cursors on different servers will help to provide true distributed database processing to the SQL Server user. Two-phase Commit The first major step in achieving a true distributed processing environment involves the concept of two-phase commit, a service that essentially guarantees that either all databases on a set of participating servers that are affected by a given set of transactions are updated or that none of them are. Such a two-phase commit service is already fully supported in DB-Library. Briefly, all transactions, some or all of which might be on different servers, are treated by the service as one transaction. One SQL Server is used as a "commit server," a sort of central record-keeper that an application uses to determine whether or not transactions should be committed or rolled back (in case of failure). Here is a summary of the process: ■ An application establishes a session with one or more servers and issues a BEGIN TRANSACTION along with information that identifies the application, the transaction, and the commit server. ■ The application issues Transact-SQL commands through DB-Library routines. ■ A PREPARE TRANSACTION command is sent indicating that the server is ready to commit. ■ The two-phase commit begins. In the first phase all servers agree that they are ready to commit. In the second phase they all commit via a COMMIT TRANSACTION that is issued to all participating servers. ■ All servers coordinate with the commit server to determine if the transaction should be committed or rolled back. There is a good deal more to this procedure but the basic two-phase commit service described here is the key to true distributed processing. LAN Manager/SQL Server To connect to a SQL Server, you need to know which servers you are connected to on the network and which of those servers is a SQL Server. The Microsoft LAN Manager API provides a function called NetServerEnum to do just this. NetServerEnum will fill a buffer with information about all of the servers you are connected to. Several LAN Manager API functions will allow you to specify an information level; the higher the information level, the more information will be returned about the requested item. For instance, with the NetServerEnum function, specifying level 0 will return only the name of each server, while level 1 returns not only the name, but the type (workstation, server, or SQL server), version, and comments associated with each server as well. Level 2 will return even more information. See the LAN Manager API Reference for more details. We are going to issue the NetServerEnum call with information level 1. We can only log on to servers that have the SV_TYPE_SQLSERVER bit set in their "type" field. Logging On The first step in establishing a connection to SQL Server is to allocate a login structure and to fill it with such information as your user ID and your password, as follows: LOGINREC *login; if ((login = dblogin())== NULL) { /* out of memory ... */ } DBSETLUSER(login, "MARC"); DBSETLPWD(login,"thunk"); DBSETLAPP(login,"AdlerApp"); SQL Server must validate your credentials before permitting you access to any database. You use the dblogin function to create a login record and then use DBSETLAPP, DBSETLHOST, DBSETLPWD, and DBSETLUSER to fill in the various fields of the record. You must fill in your user name and password as SQL Server requires these. The application name, AdlerApp, will be used by SQL Server in the sysprocesses table to identify your application. After the login structure has been filled, you use it in the dbopen call to pass the login information to SQL Server, as follows: DBPROCESS *DBProc; if ((DBProc = dbopen(login, "SQLSERV")) == NULL) { <error processing> } You also need to pass the name of the server to dbopen, or a NULL pointer if SQL Server is being used on a standalone workstation. If everything goes well, a DBPROCESS structure will be returned. Once you have obtained a DBPROCESS structure, you can communicate with SQL Server. Building a Query You can issue queries and commands directly to SQL Server by inserting the text of the command into the command buffer pointed to by the DBPROCESS structure. Two functions, dbcmd and dbfcmd, let you build up a command string for subsequent execution (see Figure 2). The text that you pass to these functions is added to the end of DBPROCESS's command buffer. With dbcmd, you can build up an arbitrarily long command string. You can even build a command string that is comprised of two or more separate SQL commands. After the command is executed, the command buffer will be cleared so that the next invocation of dbcmd will build a completely new command string. You are responsible for inserting blanks properly when calling dbcmd more than once; notice the blank preceding "where" in the second call to dbcmd. The only difference between dbcmd and dbfcmd is that dbfcmd can accept a printf-style format string and an arbitrary number of arguments (see Figure 3). Executing a Query Once the DBPROCESS command buffer has a command string, you can then send the command to SQL Server for execution. SQL Server provides functions to ship a command to the server that the DBPROCESS structure is connected to and to wait for a response from that server. The dbsqlsend function will send the command to the appropriate SQL Server, but will not wait for a response from the server. If you are managing multiple I/O streams or want to do other processing before the server responds, you can perform this processing directly after the call to dbsqlsend. The dbsqlok function waits for the response from the server, and returns a code that tells the user whether the query succeeded or failed. The dbsqlexec function is equivalent to a call to dbsqlsend followed by a call to dbsqlok: if (dbsqlexec( DBProc) == FAIL) { <error processing> } else { <process results> } If you don't need to do any processing between the time the command is sent to the server and the time the server responds, you can use dbsqlexec. Obtaining Results At this point in the process, you have logged on to a SQL Server, built a command, sent the command to the server, and received a reply that the command was executed successfully. You are now ready to receive the results from SQL Server and process them. For each command that was in the DBPROCESS command buffer, you must call dbresults. This function prepares the DBPROCESS structure to receive the output of the next command that is in the command batch. If you do not know how many commands were sent in the command batch, you will call dbresults in a loop until it returns a code of NO_MORE_RESULTS. An example of a dbresults loop is shown below: /* Loop to process each command that was sent in the command buffer */ while ((rc = dbresults(DBProc))!= NO_MORE_RESULTS && rc != FAIL) { <Fetch each row> ∙ ∙ . } Once the DBPROCESS structure has been prepared to receive the results, you can access the row data in a number of ways. The most convenient method to access the data is to use a combination of dbbind and dbnextrow. Dbnextrow will retrieve the next data row from SQL Server. If a command didn't return any rows, or if all the rows have been read, then dbnextrow returns a result of NO_MORE_ROWS. The data will be read into internal buffers that the DBPROCESS structure maintains, and you can use various DB-Library functions to extract and convert the data to a format that you can use. You can use the dbbind function to ask DB-Library to extract and convert the data automatically every time dbnextrow is called: DBINT num; ∙ ∙ ∙ dbbind(DBProc, 1, INTBIND, 0, &num); Before the dbnextrow loop, you would call dbbind to bind each column that you were interested in to a variable within your C program. For each call to dbbind, you specify the column number that you want bound, the address of the C variable that receives the converted result, the length of the variable, and the type of conversion you want. For instance, executing the call made by the code shown above would tell DB-Library that the data associated with column 1 should be converted to type DBINT and should be transferred to the variable num. The fourth parameter is the length of the variable; if it is 0, then you tell DB- Library that you are positive that there is enough room to accommodate the entire converted result. We now have all the necessary tools to build and execute a simple query such as that shown in Figure 4. The completed query will fetch the names and department numbers of all of the members of the employees table. Row Buffering The DBPROCESS structure will normally keep the data associated only with the current row. If you want to be able to access previously read rows, then you must use "row buffering." The row buffering option must be enabled and the number of rows to be buffered must be set by calling: dbsetopt(DBProc, DBBUFFER, "500"); This call will enable buffering and will cause DB-Library to allocate buffer space to accommodate 500 rows. Once you begin reading results with dbnextrow, you can access a previous row by using the dbgetrow function. For example, if you are currently reading row 100 and you want to access row 50, you would issue the call: dbgetrow(DBProc, 50); Dbgetrow will change the "current row pointer" associated with the DBPROCESS structure. For instance, when dbnextrow is next called, it will retrieve rows starting at row number 51. You can clear row buffering by making the call: dbclropt(DBProc, DBBUFFER, "0"); Other Functions DB-Library has other functions that give you information about the columns that result from a SELECT statement. These functions give you the name of a column (dbcolname), the maximum length of a column (dbcollen), the type of a column (dbcoltype), the number of columns (dbnumcols), the actual size of the data in a column (dbdatlen), and a pointer to the actual data (dbdata). This information is useful if you need to format the results in a certain way. In the programming example later in this article, we will illustrate the use of some of these functions. Exiting After you have finished your session with SQL Server, you need to log off and clean up. You use dbclose to deallocate a DBPROCESS structure and to sever the corresponding connection with SQL Server, as follows: dbclose(DBProc); If you want to close several DBPROCESS structures, you must use dbexit. Error Handling DB-Library keeps track of system or DB-Library errors; the error code can be retrieved by using dberrno. DB-Library will also return a printable string that corresponds to the error number by using the dberrstr and dboserrstr functions. You can also install your own error handler that DB-Library will call whenever an error is detected. The dberrhandle function passes the address of your error handler pointer to DB-Library. When an error is detected, DB-Library calls your routine, passing it the DBPROCESS structure, the severity code, the DB-Library error code, and the operating system error code: int myhandler(); ∙ ∙ ∙ dberrhandle(myhandler); ∙ ∙ ∙ int myhandler(dbproc, severity, errno, oserr) DBPROCESS *dbproc; int severity, errno, oserr; { <myhandler code> } Your error handler must return one of three possible return codes shown in Figure 5. A SQL Server Program We will use the techniques and DB-Library functions that have been discussed along with some features of OS/2 to create a SQL Server program, SQLPOP (see Figure 6 for the source code). This simple program is a memory-resident ad hoc query facility that the user can invoke at the press of a key. To accomplish this we will need to use an OS/2 facility known as a "device monitor." SQLPOP will be executed as a detached process, that is, will run in the background and will not be activated until a certain event takes place. The event that SQLPOP will watch for is a press of the Alt-S key combination. When Alt-S is detected, SQLPOP will take over the screen from the current foreground process and will present the user with a one- line edit field. The user can issue any SQL Server command, as long as the command is under 80 characters, and examine the results. After the results are shown, SQLPOP relinquishes control to the foreground process and sits quietly in the background waiting for another Alt-S combination. The first step in the program will be to log on to SQL Server. We call the function InitDB to handle the initialization. We first install our own error handler as the default error handler for DB-Library. We have to find out which SQL Servers are available, so we use the LAN Manager's NetServerEnum function in order to find any servers that have the SV_TYPE_SQLSERVER flag set. For the sake of simplicity, we will log on to the first SQL Server that we find. Next we allocate a login structure by using dblogin, fill in the user name, password, and application name, and call dbopen to establish a connection. For a production program, you would most likely prompt the user for the user ID and password. FALSE is returned if the connection could not be established, and the program is exited without any further processing. Back in the main routine, we know that there is a valid connection established to SQL Server, so we can safely install our device monitor. Before we proceed, let's take a quick look at terminate-and-stay-resident (TSR) programs. Under DOS, the process of writing a TSR program is confusing at best. Esoteric and undocumented DOS calls are necessary to find out if the TSR can be activated safely without interfering with the operation of DOS, the hardware, or the program currently running. Usually the TSR is activated when the user presses a certain key combination; this entails writing an interrupt routine that puts itself in front of Interrupt 16H or Interrupt 9H, depending on how early in the processing chain you want to examine the keystroke. Even then you are not assured of getting first dibs on the keystroke; other TSR programs may have decided to install themselves before you are in the processing chain. This is messy business all around. OS/2 lets the programmer implement the concept of TSRs in a more friendly manner. A device monitor lets you examine the information that comes from a character device driver before it is returned to the application program. A detached process can install a monitor on the keyboard device driver, examine the keystroke information packets from the keyboard, and choose to activate itself when a certain keyboard event is detected. However, what do we do about a foreground process that is currently running and has control of the screen? Fortunately, OS/2 provides the VIOPOPUP function. (For a detailed description of the OS/2 VIO subsystem, see "Character-Oriented Display Services Using OS/2's VIO Subsystem," MSJ Vol. 2, No. 4). When a program calls VIOPOPUP, it will preempt the foreground process and will take over the input focus (keyboard, mouse, and screen). The detached process can now interact with the user, unfettered by any other processes running in that screen group. When the detached process is done, it returns the input focus and control to the old foreground process by issuing a VIOENDPOPUP call. We will use these techniques to monitor the keyboard device driver in SQLPOP. The DOSMONOPEN call will provide a handle (hMonitor) into the keyboard device, and the DOSMONREG call will register the monitor with the device driver. After the monitor is installed, we will just sit in an infinite loop, reading each keyboard packet that comes in by using DOSMONREAD. We will call DoSQLPopup if a key has been pressed and not released, the Alt shift state is on, and the scan code of the key is an S. If the keyboard packet does not indicate that this event has occurred, we will then pass it back to the device driver with DOSMONWRITE. DoSQLPopup will use the VIOPOPUP function to take over the screen group. We draw a small box with room for a single line of input, position the cursor at the first column in the box, and use KBDSTRINGIN to get the user's input. If the user enters the command "quit" or "exit", then the detached process will be terminated, along with the corresponding device monitor. The user's SQL command is passed to DB-Library with dbcmd and then sent to the SQL Server that we are connected to by using dbsqlexec. Dbresults will then prepare the results that SQL Server sends back; since the user might have issued more than one SQL command, dbresults must be called before the results for each command are processed. We find out the number of columns in the result by calling dbnumcols and set up the address in our results buffer where the data from each column will be placed with dbbind. By using the CHARBIND argument in dbbind, we will convert the data into a readable ASCII character format. Finally, we use dbnextrow to read each row of data from SQL Server and print out the results below the command box. After all the results are read, we use the VIOENDPOPUP call in order to return control to the old foreground process. VIOENDPOPUP will automatically restore the contents of the foreground process' screen and the rest of the information that constitutes the input focus. ─────────────────────────────────────────────────────────────────────────── How SQLPOP functions ┌──────────────────────┐ │ Load SQLPOP │ ┌──Local OS/2 System──┐ ├──────────▼───────────┤ │ ┌────────────────┐ │ │SQLPOP logon to server│ │ │ Kernel │ │ ├──────────▼───────────┤ │ ├────────────────┤ │ │ Register KBD monitor │ ┌──────────┼──► SQL Server │ │ └──────────────────────┘ │ │ ├────────────────┤ │ │ │ │ LAN Manager │ │ ┌──────────────────────┐ │ │ └────────────────┘ │ │ Alt S keystroke │ │ └──┬──────────────────┘ │ encountered │ Via │ ├──────────▼───────────┤ DBCMD │ │ Call VIOPOPUP │ DBSQL │ ┌─────────────────────┐ ├──────────▼───────────┤ EXEC │ │Local OS/2 or DOS 3.x│ │ Get user input │ │ │ │ Workstations │ ├──────────▼───────────┤ │ │ └─────────┬───────────┘ │ Send command to SQL ◄─────┘ │ │ │ Server and display │ ◄───┬────┴──Network──────────┴──────────► │ results │ │ ├──────────▼───────────┤ ┌─┴────────────────────────┐ │ VIOENDPOPUP │ │ Other OS/2 or DOS 3.x │ │ and return │ │Systems and/or SQL Servers│ └──────────────────────┘ └──────────────────────────┘ ─────────────────────────────────────────────────────────────────────────── Compile, Link, Run To compile SQLPOP.C, you issue the following compiler command (all on one line, of course): cl /c /AM /Zp /W1 /Lp sqlpop.c and to link the program, issue the following linker command (again, all on one line): link sqlpop,sqlpop,, mdblib+mlibcep+doscalls; To run the program, simply issue the following command from the OS/2 CMD prompt: detach sqlpop Now, whenever you press Alt-S in that screen group, SQLPOP will be activated. You must ensure that you have started a SQL Server before you run SQLPOP. What's To Come The simple programming example shown here only begins to touch on the capabilities of DB-Library and SQL Server, and we really haven't explored the SQL end of things. However, you should at least have a good sense of the possibilities becoming available as all the pieces of the MS(R) OS/2 environment come together. DB-Library offers a consistent set of functions that permit the C programmer to access a SQL Server database running locally or on another computer. Using the Microsoft LAN Manager, the user can write programs that provide for distributed processing. Because the user does not have to worry about the database engine robbing the program of precious memory, the application programmer can concentrate on developing advanced features that go beyond the present state of the art. Figure 1: The DBPROCESS Structure struct dbprocess { struct *dbfile; /* dataserver connection */ servbuf DBUSMALLINT dbstatus; /* status field for dbprocess */ BYTE dbtoken; /* current dataserver token */ DBSTRING *dbcmdbuf; /* command buffer */ int dbcurcmd; /* number of the current cmd results */ DBINT dbprocid; /* procid, if any, of current cmd */ DBCOLINFO *dbcols; /* linked list of column info */ DBALTHEAD *dbalts; /* linked list of alt column info */ DBROW *dbfirstdata; /* doubly linked list of returned row data */ DBROW *dbcurdata; /* current row in dbfirstdata */ DBROW *dblastdata; /* last row in dbfirstdata, usually dbcurdata */ DBOFF *dboffsets; /* list of offsets and controls in dbcmdbuf */ int dboffadjust; /* adjustment factor for offsets */ DBSMALLINT dbcuroffset; /* active offset for results */ DBOPTION *dbopts; DBSTRING *dboptcmd; /* option string to send to server */ DBINFO *dbmsgs; /* linked list of info and error messages */ DBINFO *dbcurmsg; /* last message read by dbgetmsg() */ DBDONE dbdone; /* done information */ char dbcurdb[MAXNAME+1]; /* the name of the current database */ INTFUNCPTR (*dbbusy)(); /* Called when waiting on dataserver */ int (*dbidle)(); /* Called when waiting on dataserver */ int (*dbchkintr)(); /* user's function to call to check for queued interrupts */ int (*dbhndlintr)(); /* user's interrupt handler */ int dbbufsize; /* the size of the row buffer, if enabled */ NULLBIND dbnullbind; /* what to bind for nulls */ int dbsticky; /* sticky flags like attn */ int dbnumorders; /* number of columns in the query's "order by" clause */ int *dbordercols; /* array of the column numbers found in query's "order by" clause */ BOOL dbavail; /* is this dbproc available for general use? */ int dbftosnum; /* this id is used when recording frontend-to-Server SQL traffic of this DBPROCESS */ BOOL dbdead; /* TRUE if this DBPROCESS has become useless, usually due to a fatal Server error, or a communications failure */ BOOL dbenabled; /* TRUE if this DBPROCESS is allowed to be used in DB-LIBRARY functions. The user may set this flag FALSE, possibly within an error handler, if execution of further commands would just cause further errors. DB-LIBRARY initially sets this flag TRUE. The user may set and re-set this flag at will */ BOOL dbloginfailed; /* TRUE if the Server has rejected our user name and password */ BOOL dbsqlsent; /* TRUE if the SQL in the command buffer has already been sent to the Data- Server */ struct *dbnext; /* DBPROCESSes are kept track of in a big dbprocess linked-list */ }; typedef struct dbprocess DBPROCESS; Figure 2: Using dbcmd() dbcmd(DBProc, "select * from employees"); dbcmd(DBProc, " where dept = 9999"); Figure 3: Example Using dbfcmd() char *tblname; int limit; ∙ ∙ ∙ /* An example of building two commands in one command buffer - Note the printf-like feature of dbfcmd() */ dbfcmd(DBProc, "select * from %s where dept > %d", tblname, limit); dbcmd (DBProc, "select * from wages"); Figure 4: A Complete Example of a Database Query char name[80]; DBINT dept; dbcmd(DBProc, "select name, dept from employees"); if (dbsqlexec(DBProc) == FAIL) { fprintf(stderr, "The query failed.\n"); } else { while (dbresults(DBProc) != NO_MORE_RESULTS) { dbbind(DBProc, 1, CHARBIND, sizeof(name), name); dbbind(DBProc, 2, INTBIND, 0, &dept); while (dbnextrow(DBProc) != NO_MORE_ROWS) { printf("%-30s\t%ld", name, dept); } } } Figure 5: Error Handler Return Codes INT_EXIT Print an error message and abort the program INT_CANCEL Return FAIL from the DB-Library routine that caused the error INT_CONTINUE For time-out errors, wait another time-out period; at the end of that period, call the error handler again Figure 6: Source Code for SQLPOP.C /********************************************************************** * SQLPOP - allows the user to hot-key into an SQL Server query window * * Created by Marc Adler *********************************************************************/ #include <stdio.h> #include <stdlib.h> #include <memory.h> #include <string.h> #include <sqlfront.h> #include <sqldb.h> #include <netcons.h> #include <server.h> #include <os2def.h> #include <doscalls.h> #include <subcalls.h> #include <infoseg.h> typedef unsigned char BOOL; typedef unsigned char BYTE; typedef unsigned int WORD; typedef unsigned long DWORD; /* Structure of the keyboard packet which the device monitor receives */ typedef struct kbdpacket { WORD monitor_flags; BYTE ascii_code; BYTE scan_code; WORD nls; WORD shift_code; DWORD time; WORD devdrv_flags; } KBDPACKET; #define KEY_RELEASE 0x40 #define ALT 0x08 #define IDNO FALSE #define IDYES TRUE #define MB_YESNO 1 #define ERR_DBSQLEXEC_FAILED -100 #define ERR_DBRESULTS_FAILED -101 DBPROCESS *DBProc = NULL; /* Our connection with the DataServer*/ WORD hMonitor; /* Monitor handle */ BOOL bIsResident = FALSE; /* TRUE once we become resident */ struct server_info_1 ServerInfo[65]; /* server_info_1 buffer area */ /*********************************************************** * Main() * * Logs into SQL Server, installs the monitor, and calls * DoSQLPopup() when the user presses the ALT-S key. **********************************************************/ main() { KBDPACKET packet; int packet_len; char inbuf[128], outbuf[128]; /* Try to login */ if (!InitDB()) exit(1); bIsResident = TRUE; /* Open a keyboard monitor and register it */ DOSMONOPEN((char far *) "KBD$", (unsigned far *) &hMonitor); inbuf[0] = sizeof(inbuf); inbuf[1] = 0; outbuf[0] = sizeof(outbuf); outbuf[1] = 0; DOSMONREG(hMonitor, (char far *) inbuf, (char far *) outbuf, 0, GetCurrScreenGroup()); /* Monitor loop - watches for the ALT+S keystroke */ for (;;) { packet_len = sizeof(packet); DOSMONREAD((char far *) inbuf, 0, (char far *) & packet, (unsigned far *) &packet_len); if (!(packet.devdrv_flags & KEY_RELEASE)) { /* Check for an ALT-S keystroke. */ if ((packet.shift_code & ALT) && packet.ascii_code == 0 && packet.scan_code == 31) { DoSQLPopup(); continue; } } /* Pass all other keys through */ DOSMONWRITE((char far *) outbuf, (char far *) &packet, packet_len); } } /********************************************************** * TerminateMonitor() * Logs off the server and terminates the monitor. *********************************************************/ TerminateMonitor() { Logout(); DOSMONCLOSE(hMonitor); DOSEXIT(1, 0); } /*********************************************************** * DoSQLPopup() * Puts up a rudimentary one-line query box, gets the user's * query, sends it to SQL Server, and prints the results. * This is done when the user presses the proper hot-key * sequence. **********************************************************/ char *BoxStr[] = { "IMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM ": :", "HMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMMM }; DoSQLPopup() { unsigned i; int row; char buf[82]; struct KbdStringInLength ksl; unsigned waitflag; unsigned attr = 0x07; int nc, col, len; char *more = "Press a key for more..."; /* Take over the screen group for a while */ waitflag = 0; if (VIOPOPUP((unsigned far *) &waitflag, 0)) return; /* Draw the query box */ for (i = 0; i <= 2; i++) { VIOWRTCHARSTRATT((char far *) BoxStr[i], strlen(BoxStr[i]), i, 0, (char far *) &attr, 0); } VIOSETCURPOS(1, 1, 0); /* Get the user input. Get rid of trailing carriage returns. */ ksl.Length = sizeof(buf); KBDSTRINGIN((char far *) buf, (struct KbdStringInLength far *) &ksl, 0, 0); if (ksl.LengthB) { if (buf[ksl.LengthB] == '\r') buf[ksl.LengthB] = '\0'; } /* If the user wants to exit the program, logout and deinstall the monitor */ if (!strcmpi(buf, "quit") || !strcmpi(buf, "exit")) { VIOENDPOPUP(0); TerminateMonitor(); } /* Ship the query to SQL Server */ dbcmd(DBProc, buf); if (dbsqlexec(DBProc) != FAIL) { while (dbresults(DBProc) != NO_MORE_RESULTS) { char results_buf[256], *rb; /* Find out how many columns each row has */ nc = dbnumcols(DBProc); /* Insert the row data one column at a time into the buffer */ for (rb = results_buf, col = 1; col <= nc; col++) { dbbind(DBProc, col, CHARBIND, 0, rb); len = dbcollen(DBProc, col); rb += len; } *rb = '\0'; /* Print out the results */ for (row = 3; dbnextrow(DBProc) != NO_MORE_ROWS; row++) { VIOWRTCHARSTRATT((char far *) results_buf, strlen(results_buf), row, 0, (char far *) &attr, 0); if (row > 22) { VIOWRTCHARSTRATT((char far *) more, strlen(more), row, 0, (char far *) &attr, 0); getch(); row = 3; } } } } VIOWRTCHARSTRATT((char far *) more, strlen(more), 24, 0, (char far *) &attr, 0); getch(); VIOENDPOPUP(0); } /********************************************************** * InitDB() * Installs the error handler and tries to login to SQL * Server. **********************************************************/ InitDB() { int DBErrorHandler(); /* Install our error handler */ dberrhandle(DBErrorHandler); /* Try to login to SQL Server */ if (!DoLogin()) return FALSE; return TRUE; } /********************************************************** * DoLogin() * Executed at startup and when user chooses LOGIN * from the menu. * * RETURNS * * TRUE if the login is successful, FALSE if not. **********************************************************/ DoLogin() { LOGINREC *login; /* Our login information */ int iServer; /* Find all servers we are connected to who are running SQL SERVER */ if ((iServer = LocateSQLServer()) == FALSE) return FALSE; /* Get a login structure and set the app name, the login id, and the pwd. */ login = dblogin(); DBSETLAPP(login, "Adler"); DBSETLUSER(login, "sa"); DBSETLPWD(login, ""); /* Open a new DBProc structure with the new login info. If we fail, then DB_LIB will print out a message through the error handler. */ if ((DBProc = dbopen(login, ServerInfo[iServer].sv1_name)) == NULL) return FALSE; return TRUE; /* Successful login ! */ } /********************************************************** * Logout() * Closes the DBProc upon exit. We call this when we end * our keyboard monitor. **********************************************************/ Logout() { if(DBProc) { dbclose(DBProc); DBProc = NULL; } } /********************************************************** * DBErrorHandler() * Handles error conditions from SQL Server. Puts up a * message box and asks the user for an action. **********************************************************/ int DBErrorHandler(dbproc, severity, errno, oserr) DBPROCESS *dbproc; int severity, errno, oserr; { /* Print the SQL Server error message */ if (MessageBox("SQL Server Error!!!", dberrstr(errno), "Continue the program?", MB_YESNO) == IDNO) { TerminateMonitor(); return INT_EXIT; } /* See if we have an operating system error */ if (oserr != DBNOERR) if (MessageBox("OS/2 Error!!!", dboserrstr(oserr), "Continue the program?", MB_YESNO) == IDNO) { TerminateMonitor(); return INT_EXIT; } /* If for some reason the DBProc structure is dead, exit immediately */ if (DBDEAD(dbproc)) { TerminateMonitor(); return INT_EXIT; } /* Tell DB-LIB to cancel the command and continue. */ return INT_CANCEL; } /********************************************************** * printmsgs() * Fetches any pending messages from SQL Server and * inserts them into the results buffer. * * RETURNS - nothing **********************************************************/ void printmsgs(dbproc) DBPROCESS *dbproc; { char *msg; while ((msg = dbgetmsg(dbproc)) != NULL) fprintf(stderr, "%s\n", msg); } MessageBox(s1, s2, s3, action) char *s1, *s2, *s3; unsigned action; { int c; if(s1) { fprintf(stderr, "%s", s1); if(s2) { fprintf(stderr, "\n%s", s2); if(s3) { fprintf(stderr, "\n%s", s3); } } } switch(action) { case MB_YESNO : if ((c = getch()) == 'Y' || c == 'y') return IDYES; return IDNO; } } /********************************************************** * GetCurrScreenGroup() * * Returns the id of the current screen group. **********************************************************/ #define MAKEPGINFOSEG(sel) (MAKEP(sel, 0)) GetCurrScreenGroup() { unsigned ldt_seg, gdt_seg; struct InfoSegGDT far *pGdt; DOSGETINFOSEG((unsigned far *) &gdt_seg, (unsigned far *) &ldt_seg); pGdt = MAKEPGINFOSEG(gdt_seg); /* Get a pointer to the info */ return pGdt->CurScrnGrp; /* return the screen group */ } LocateSQLServer() { unsigned entriesread; /* number of entries read by NetServerEnum */ unsigned totalentries; /* total number of entries available */ int i, rc; rc = NetServerEnum((char far *) NULL, 1, (char far *) ServerInfo, sizeof(ServerInfo), (unsigned far *) &entriesread, (unsigned far *) &totalentries); if (!rc) { for (i = 0; i < entriesread; i++) if (ServerInfo[i].sv1_type & SV_TYPE_SQLSERVER) return i + 1; /* use offset of 1 so we don't return a '0' */ } fprintf(stderr, "Could not find an SQL Server\n"); return FALSE; } ████████████████████████████████████████████████████████████████████████████ OS/2 Protected-Mode Programming with Forth, LISP, Modula-2, and BASIC Andrew Schulman☼ If you were going to be stranded on a desert island, what programming language would you make sure you had with you? Well yes, if forced into such a position I would undoubtedly take C too. Fortunately we do not have to make such a choice. OS/2 programmers are anything but stranded on a desert island and are definitely not limited to working with one language. A wide selection of compilers and interpreters have become available for OS/2's protected mode, and this article will examine four of them──Forth, LISP, Modula-2, and BASIC──each with its own unique features and each one able to do things that cannot be done directly in C. UR/FORTH, from Laboratory Microsystems, Inc. (LMI), is available for OS/2 protected mode, 386 machines, MS-DOS(R) real mode, and 68000 UNIX(R). Th Modula-2 development system for OS/2 used in this article is available from Stony Brook Software, Inc., which also makes a version for real-mode DOS. Microsoft(R) BASIC Version 6.0 compiles code for MS-DOS and OS/2. Finally, the LISP interpreter discussed here is an extended version of David Betz's freely-distributed XLISP, which I ported to OS/2 protected mode, and can be found on several electronic bulletin boards. There are other languages available as well; COBOL and FORTRAN come immediately to mind. The attitude taken in this article reflects that of The Tao of Programming, an ancient text discovered and translated by Geoffrey James: "The assembler gave birth to the compiler. Now there are ten thousand languages. Each language has its purpose, however humble. Each language expresses the yin and yang of software. Each language has its place within the Tao. But do not program in COBOL if you can avoid it." [Geoffrey James, The Tao of Programming, InfoBooks, 1987, p. 7.] We will focus on several aspects of each product: ■ How does it let the user make function calls to the OS/2 Application Programming Interface (API)? How well are OS/2 calls integrated into the rest of the language? ■ What conversions are required between OS/2 data types and the data types intrinsic to the language? How easy is it to create OS/2 data structures? ■ Unlike C, which was never designed for concurrent programming, some languages have built-in multitasking capabilities. When these languages are ported to OS/2, threads should be used to implement the languages' process abstraction. What is OS/2 programming like when using a language that has inherent multitasking capabilities? ■ How different is the product from its MS-DOS (or 386) equivalent? What OS/2 features does the product take advantage of? Along the way, we will see that some of these languages offer useful services, such as an in-line assembler and other forms of mixed-language programming, OS/2 graphics (without Presentation Manager), access to ports, and built-in debugging facilities. An OS/2 Rosetta Stone In 196 BC, during the reign of Ptolemy V, a clerk transcribed a petty decree in three scripts: Greek, Egyptian demotic, and hieroglyphics. The discovery of this slab, known as the Rosetta Stone, led to the decipherment of Egyptian hieroglyphics in the early nineteenth century. In the same way, the code shown in Figure 1 presents an otherwise unimportant OS/2 program transcribed in C, Forth, LISP, Modula-2, and BASIC. The idea is to help the C programmer decipher the possibly cryptic notations used by the other languages. The program simply loops through all possible module handles, printing out the names of all currently loaded executables and dynamic-link libraries in all sessions. The program makes 65,535 calls to DosGetModName, and takes a long time to run, no matter which language is used (DosGetModName could be OS/2's slowest function). Figure 1 also shows sample output from the program, from which you can see that I boot OS/2 off floppy drive A, that I use a command line editor (ALIAS.DLL), Lugaru Software's Epsilon(TM) text editor, and that I was running UR/FORTH and OS2XLISP. Each of these products has its own way of providing OS/2 function calls. Calling OS/2 from Microsoft(R) C involves using an include file containing function prototypes, and a comparable technique is used in Modula-2 and BASIC. The way to declare external functions differs, however. Because C does not use the Pascal calling convention, it requires the APIENTRY (far pascal) modifier for OS/2 functions. Both Modula-2 and BASIC already use the Pascal function calling convention, and therefore require no special handling for OS/2 functions. UR/FORTH and OS2XLISP are both interactive programming environments, rather than batch-mode compilers, and neither includes header files. In UR/FORTH every OS/2 API call, except for a few that Forth cannot use effectively (such as KbdRegister and VioRegister), has a header built into the OS/2 version. OS2XLISP uses run-time dynamic linking: we get a handle to the DOSCALLS module with the (loadmodule) function, link to the function with (getprocaddr), and invoke the function with (call). Neither UR/FORTH nor OS2XLISP checks what you pass to the OS/2 function, though in both cases users can supply their own parameter checking. Another issue that comes up in this small program is passing OS/2 a far pointer to a buffer. In C, the function prototype casts buf to PCHAR (a far pointer to a char), while in Modula-2, the procedure ADR takes a variable or procedure and returns its address. OS2XLISP's (call) function automatically treats string parameters as far pointers. In Forth, offspring of CREATE (like BUF) automatically return their address. However, the address is the offset of BUF within the Forth dictionary, and in order to get a full far pointer, we also need the segment/selector of the Forth data segment, which the UR/FORTH word DS0 supplies. Like Forth, BASIC handles the segment and offset separately. The declaration for DosGetModName% treats the far pointer as two separate integer parameters; VARSEG retrieves the segment and SADD retrieves the offset of a variable-length string. The percent sign at the end of the BASIC name indicates that this function returns an integer. DosGetModName moves an ASCIIZ string into the buffer, but ASCIIZ is not the intrinsic string type of every programming language. In UR/FORTH, the expression BUF -ASCIIZ COUNT TYPE converts BUF from ASCIIZ to a Forth "counted string" and then prints it. In BASIC, we can use the PRINT command, but we have to RTRIM$ the string and refill the buffer with spaces before using it again. With Modula-2, strings are similar to Pascal strings, where each string has a definite length, but Modula-2 also recognizes null terminators, which are designated 0C. Finally, OS2XLISP is written in C, and uses the C ASCIIZ string format. UR/FORTH UR/FORTH is a multitasking threaded-code interpreter. In contrast to DOS's register- and interrupt-based system services, parameters to the OS/2 APIs are placed on the stack, and a far CALL invokes any given service. Because it is stack-based, access to the OS/2 APIs fits naturally into Forth, a stack-oriented language. To call OS/2 functions from UR/FORTH, parameters are placed on the stack, followed by the name of the OS/2 function which will use them. This is just like any other version of Forth. Here's the real treat: 880 100 + . 980 ok 880 100 DOSBEEP . 0 ok Note that just as 880 100 + can be keyed in interactively by using Forth like an RPN calculator, so can OS/2 function calls. OS/2 functions can be called from UR/FORTH without having to write a "program," which makes it an ideal tool for experimenting with and learning OS/2. Just as + uses the top two numbers on the stack, replacing them with their sum, the DOSBEEP operator uses the top two numbers on the stack, replacing them with an OS/2 error code. In this example, it also happens to have the side effect of playing the first note of a Bach toccata. The dot is not idle punctuation, but the Forth "word" which prints the top number on the stack. After Forth has responded to a command, it prints "ok" and waits for your next command. What exactly is Forth? Essentially it is an interactive interpreter, compiler, and assembler rolled into one. Forth maintains a dictionary of words, in which "." and "+" and "DOSBEEP" are all words. Any new words you create, such as ENUMDLL in Figure 1, consist either of previously defined words strung or "threaded" together, or of native object code from the Forth assembler. An outer interpreter reads input from the keyboard or from a file, a compiler strings together instructions, and an inner interpreter chugs along the thread of instructions carrying them out. The user can directly access any of these components (e.g., incorporating the outer interpreter in your own programs). UR/FORTH for OS/2 is an OS/2 protected-mode task, not its own operating system like some other Forths. UR/FORTH is designed for multitasking and protected-memory environments. In addition to providing direct access to the OS/2 APIs, UR/FORTH has a multilevel machine-independent operating system interface so that you can also use MALLOC instead of DOSALLOCSEG, or FOPEN instead of DOSOPEN. Factoring Forth Words The examples in Figure 1 are as alike as possible. In Forth, one might break up even this miniprogram into several words. Everything except a number in Forth is a word, including control structures. This means you can create new control structures and user programmable syntax. Forth words are defined with the word : (a colon), and ; (a semicolon) terminates the definition. In Figure 1, we defined ENUMDLL, which contained a DO LOOP, and executed it by typing ENUMDLL. We can include ENUMDLL inside other words, but it does too much to be generally useful. A more realistic Forth version of Figure 1 is shown in Figure 2. One of our new words, MODULE?, expects a number on the stack; it tests this with DOSGETMODNAME and leaves a boolean flag on the stack. This (before-after) stack effect is documented inside the parenthetical comment, which is a Forth convention for commenting words. The new word .ASCIIZ prints out an ASCIIZ string whose address is on the top of the stack. Since the Forth word . (dot) prints, a dot often precedes the name of a word which prints anything. The word .MODULE then prints out a module name and number. It calls our newly defined word .ASCIIZ. Finally, the new version of ENUMDLL uses the new words MODULE? and .MODULE. The structure of IF in Forth takes a little getting used to at first, but it is consistent with Forth's postfix notation. Note that THEN means afterward, and has no connection to the if...then construct found in other programming languages. Interacting with Threads There isn't anything particularly remarkable about this code; after all, as Figure 1 demonstrates, it can also be written in C, LISP, Modula-2, and BASIC. Figure 3 presents a more interesting bit of Forth code for OS/2, a tiny interactive laboratory for experimenting with multiple threads of execution. If you are familiar with Forth, it might surprise you that Figure 3 can be run from UR/FORTH simply by typing INCLUDE THREADS.4TH. In addition to the old-style 1024-byte screen blocks used in most Forths (for which UR/FORTH provides a very good editor), UR/FORTH also offers INCLUDE to compile Forth source code from normal text files. In THREADS.4TH several threads of execution are launched, each one executing through the same piece of code, with each thread displaying its thread ID number on a portion of the screen. An area of the screen is left free for you to enter commands, and a HELP message is displayed. If threads 5, 6, and 7 are executing, for instance, you can type SUSPEND 6 and, sure enough, thread 6 will halt. Type RESUME 6 and it starts again. If you type SET-IDLE 6, it appears to halt again; but if you SUSPEND 5 and SUSPEND 7, then thread 6, which is executing at idle priority, begins to display on the screen again because it's getting more timeslices. You should note that nowhere is there a call to DOSCREATETHREAD. Yet there are threads here. Like many other versions of Forth, UR/FORTH has a multitasking facility that allows creation of multiple background tasks. In versions of UR/FORTH for MS-DOS and the 386, Forth task control blocks and semaphores are set up and maintained entirely by Forth. In the most recent version of UR/FORTH for OS/2, however, LMI has implemented Forth tasks as OS/2 threads and Forth semaphores as OS/2 semaphores (UR/FORTH comes with a huge amount of source code, including code for the tasker, so you can see how it's done). Forth tasks under OS/2 are therefore truly timesliced, whereas in other versions a switch depends on someone calling PAUSE, much more like nonpreemptive multitasking under either Microsoft Windows or Apple(R) MultiFinder(TM). Aside from the inclusio of the API calls in the OS/2 version, this is probably the major difference between the OS/2 and non-OS/2 versions of UR/FORTH. We can employ the standard UR/FORTH multitasking words (such as TCB to create a task control block, SLEEP to suspend a task, or SGET to acquire a semaphore), and know that we are actually using the underlying multitasking services of OS/2. Likewise, we can use OS/2 function calls like DOSSUSPENDTHREAD in order to manipulate Forth task control blocks. Each task should execute PAUSE somewhere in its main loop, as PAUSE is responsible for killing the thread when a task is terminated by STOP. Since OS/2 lacks a DosKillThread call, STOP has to do some funny business to convince a thread to execute a DOSEXIT. Even though UR/FORTH is an interpreter, it comes with an assembler and native-code compiler. It is therefore capable of creating object code whose address can be passed to DOSCREATETHREAD. Note that all the threads execute through the same code. Each thread can get its own ID number, because each thread has its own view of the local information segment. THREADS.4TH creates a tiny command language for playing with threads/tasks. In Forth, it is a trivial matter to create such "little languages." Rather than force the user to conform to Forth's postfix notation and enter commands such as 6 SUSPEND, in this example we define GETNUM, which takes a number, not from the stack but from the input stream, so instead you can type SUSPEND 6. This also shows how easy it is for Forth programmers to use the language parser in their own programs- something you can't do with a C compiler. Graphics, IOPL, and Ports UR/FORTH for OS/2 has a good set of video and graphics primitives, and provides a good environment in which to do non-Presentation Manager-based graphics programming. Its graphics can be used inside UR/FORTH tasks, so you can create multithreaded graphics programs. One reason graphics programming under OS/2 is difficult is IOPL protection, which can make direct port access under OS/2 tricky. However, UR/FORTH makes this easy, as the small program in Figure 4, XY.4TH, illustrates. XY.4TH directly programs the 6845 controller to deposit the current X and Y cursor position on the stack. UR/FORTH comes with the function ?XY which does the same thing by calling VIOGETCURPOS; however, XY.4TH does help to illustrate a few points. To send a byte to a port from UR/FORTH, use PC!, and to read a byte from a port, use PC@. In our example, we send 0EH to port 3D4H, and then read a byte from port 3D5H. The first time you use one of these words, the disk light comes on as OS/2 loads in an IOPL segment. Note that each PC@ and PC! involves a far call into the IOPL segment, so they aren't as fast as in real-mode UR/FORTH. You need not call DOSPORTACCESS, since it is called for all ports by UR/FORTH at initialization. The word CUR-LOC puts the contents of registers 14 and 15 of the 6845 controller on the stack, MK-WORD forms the bytes into a word, and COL-ROW splits out the current X and Y position of the cursor. These three words are threaded together in the definition of CURS, and all without any variables; all communication occurs on the stack. Because of this, Forth words can easily return multiple values-another thing that can only be simulated in C. Some other "goodies" for UR/FORTH include: the file OS2.SCR, which is an extensive demonstration of shared memory, queues, pipes, and so on; a file with the unlikely name URPOUN.ARC, with which you can create OS/2 data structures; also, the LMI BBS has applications like SEE (a Forth decompiler); a turtle graphics package for UR/FORTH; and URTCO (a threaded code optimizer). What does UR stand for? Don't ask! OS2XLISP In some respects, LISP is the opposite of Forth. For instance, in Forth we said 880 100 + . whereas with the OS2XLISP interpreter, one might have the following exchange (the LISP prompt is a simple angle bracket): > (+ 880 100) 980 While many programming languages use infix like 880 + 100, and while Forth uses postfix, functions in LISP are always at the beginning of the list. A LISP function call is a list, marked with LISP's infamous parentheses. The LISP evaluator uses the first item in a list (the car) as a function applied to the remainder of the list (the cdr). For example, + is applied to 880 and 100. Whereas in Forth we had to explicitly ask to print out a result, the LISP interpreter does this automatically; LISP's top level is a read-eval-print loop. Actually, Forth and LISP aren't completely different. Both, for example, use the idea that, in an expression, the function or operator should appear in some consistent place. And like UR/FORTH, OS2XLISP is interactive. This means you can type in OS/2 calls and immediately see their results. LISP parentheses make it easy to nest lists inside of lists, thus making it easy to pass the results of one function call to an enclosing function call. In the following example, we pass the result of an addition to a macro which creates variables: > (define tmp (+ 880 100)) 980 To examine an object in LISP, type its name (without parentheses): > tmp 980 Educating XLISP Following what we did in Forth, the next step is obvious. We want to see if DOSBEEP works the same as + (this is our litmus test for ease of interaction with the OS/2 API): > (dosbeep 880 100) error: unbound function - DOSBEEP Ugh! OS2XLISP doesn't even know what dosbeep is! In fact, OS2XLISP.EXE doesn't really know much about OS/2 API calls. But we can teach XLISP what dosbeep is. OS2XLISP does know about DLLs, and this is its main difference from XLISP running under other operating systems. For now, let's see if we can tell OS2XLISP what dosbeep is "on the fly," while the program is running: > (define doscalls(loadmodule "DOSCALLS")) 1910 > (define dosbeep (getprocaddr doscalls "DOSBEEP")) 1216544768 What's going on here? We have created a numeric variable, dosbeep, and set it to some strange large number. But what exactly is this number? Maybe it will look more reasonable in hex: > (ultoa dosbeep 16) "48830000" C programmers may recognize (ultoa) as a function from the C standard library. By displaying dosbeep in hexadecimal, you should see that this variable contains an address in memory──the proc address of DosBeep, to be exact. The LISP numeric variable dosbeep is actually a function pointer, through which we can invoke the function: > (call dosbeep (word 880) (word 100)) ; speaker beeps 0 Great, it worked, but remember that we really wanted to say (dosbeep 880 100), just as we say (+ 880 100). Having to cast LISP numbers, which are 4-byte longs, to the 2-byte words DosBeep wants is too much trouble, and so is having to explicitly say (call). We can write a new LISP function that does what we want: > (define (dosbeep freq dur)(call dosbeep(word freq)(word dur))) DOSBEEP > (dosbeep 800 100) ; beep! 0 Since I've been making it sound like dosbeep is some kind of patch you slap on at run time, I should explain that OS/2 access is integrated into normal LISP. Let's say that a note is a list of two items: a frequency and a duration. We can assign a list of notes to the symbol bach, and write a function (play), which takes such a list and calls (dosbeep) with each note: (define bach '((880 100)(784 100) (880 1200))) (define (play notes) (mapcar(lambda (note)(DOSBEEP(car note)(cadr note)))notes)) > (play bach) ; da-da-dum (0 0 0) In the function (play), (mapcar) maps over the list notes, calling another function for each note in the list. That other function has no name, being a temporary nameless function created with (lambda), but it takes a note, passes its first element (car) to (dosbeep) as a frequency, and its second element (cadr) as a duration. This is similar to a C program passing strcmp to qsort. Variables and Symbols Note that we have used (define), both to create variables and to create functions. This is borrowed from the Scheme dialect of LISP. However, if you are familiar with Common LISP or are working from a Common LISP textbook, you can also use (setf) or (setq) for variables, and (defun) for functions. It is important to note that there isn't much of a difference between data and functions. Dosbeep is a number, but also a function; in LISP, you're really working with symbols, not variables. Symbols can have multiple values attached to them. For example, the symbol dosbeep has a numeric value (the address of DosBeep) and a functional value (the code we just defined). A symbol can also have a property list (Microsoft Windows gets the idea of window property lists from LISP). With all these different values hanging off the data object, it shouldn't be difficult to imagine how such a data object would be implemented. In the C implementation of XLISP, a LISP data object is a C union that is tagged with the object's type and has slots for the numeric value, function value, object name, property list, and so on. Since these symbols are intended to be linked together into lists, the symbol also has a slot in which to put a pointer to the next object. In OS2XLISP, therefore, even a "simple" number like 880 is represented as one of these complex unions. When we pass 880 to the function (dosbeep), we are really passing a pointer to an XLISP node. Run-time Dynamic Linking Because LISP objects are so different from "normal" variables, if OS/2 were passed one of these, it would become completely confused. So conversions have to be done back and forth on the LISP side. When I started porting XLISP to OS/2, I considered creating a new LISP function for every OS/2 API call. It quickly became clear that, due to the conversions needed between LISP data types and OS/2, I would have ended up with a huge OS2XLISP.EXE. Also, I am lazy and I certainly did not want to type in several hundred new functions. OS/2 itself came to the rescue, with run-time dynamic linking. This is quite different from the type of dynamic linking usually used in OS/2. With the OS/2 functions DosLoadModule and DosGetProcAddr (Windows programmers should know these as incarnations of LoadLibrary and GetProcAddress), a program can pass ASCIIZ strings to OS/2 and get back a function pointer. The function can then be invoked through this pointer. In some programming languages, this technique is called "string invocation" (because all you need to call a function is its ASCII string name). By using run-time dynamic linking, OS2XLISP can provide access to any OS/2 API call simply by providing a handful of functions. The function (loadmodule) gets the module handle of a dynamic-link library (DLL), (getprocaddr) gets a pointer to a function in the DLL, (call) calls the function through its pointer, (c-call) does the same for functions that use the CDECL calling convention, and (freemodule) relinquishes access to a DLL. Let's look at an example: > (define dosgetinfoseg(getprocaddr doscalls"DOSGETINFOSEG")) 1201340416 The function DosGetInfoSeg expects to be called with two arguments: first, a pointer to 2 bytes (a word), in which OS/2 will place the selector of the global info segment (GIS), and second, a pointer to a word, in which OS/2 will place the selector of the local info seg (LIS). We also need to create some 2-byte variables for OS/2: > (setf gis (word 0) lis (word 0)) 0 I used (setf) here instead of (define) because it can perform multiple assignments in one fell swoop. XLISP numbers are generally 4-byte longs, so we use the OS2XLISP function (word) to cast them. To call the function, we need to pass not gis and lis, but their addresses. We use the macro ^ which is shorthand for the OS2XLISP (addr) function, and which returns a pointer to the data inside an XLISP node: > (call dosgetinfoseg ^gis ^lis) 0 The (call) function returned zero (success), because that's what DosGetInfoSeg returned. So, how could DosGetInfoSeg fail? Now we can examine the contents of our LISP variables: > gis 96 > lis 15 OS/2 has changed these LISP objects, right out from under LISP's nose. Generally, these numbers would be worthless by themselves. But since OS2XLISP knows about segmented protected memory, we can examine not just the contents of these segments, but the segment attributes themselves. For instance, (lsl) corresponds to the protected-mode instruction LSL, which gives the last legal offset within a segment (the size minus one): > (lsl gis) ; how big is it? 69 ; size - 70 bytes > (verr gis); look at it? T ; yes > (verw gis); can I touch it? NIL ; no way! NIL is the LISP symbol for the empty list, or false; T is the symbol for true. The functions (verr) and (verw) correspond to other protected-mode instructions, and will be discussed later in the article. As with UR/FORTH, in order to look inside these info segs, we need to make far pointers and examine what they point to. Suppose we want the process ID of whatever process is running in the foreground. Since we're typing away at OS2XLISP, it's a good bet that OS2XLISP is running in the foreground. This information is kept in the 2 bytes at offset 28 of the GIS: > (peek (mk-fp gis 28) 'word) 11 (mk-fp) takes a segment and an offset and in turn creates a far pointer. The function (peek) takes a far pointer and an optional directive which tells it what to fetch (you can fetch bytes, words, longs, IEEE floating- point numbers, and ASCIIZ strings). The apostrophe in front of a symbol tells LISP that we're actually interested in the symbol itself, not in any value it may possibly have. Chatting with DLLs Run-time dynamic linking turned out to be a big plus; not only is all access to the OS/2 API bottlenecked through a few functions rather than scattered across hundreds of new ones, but it also means that users can access anything from a dynamic-link library, not just the APIs supplied in DOSCALLS, VIOCALLS, and KBDCALLS. When new functions come along, they're instantly available, without users having to wait for an OS2XLISP update to be issued. Figure 5 shows OS2XLISP calling CHATLIB.DLL, a program presented in "Design Concepts and Considerations in Building an OS/2 Dynamic-Link Library" (MSJ Vol. 3, No. 3). OS2XLISP.EXE obviously has had no prior knowledge of CHATLIB.DLL. It makes all the connections at run time, which is a surefire way to build an extensible interpreter. What LINK normally does, we defer until the last possible moment. As all good programmers know, never do today what you can put off until tomorrow. We have been using OS2XLISP interactively, by typing expressions directly in from the keyboard, but you can also evaluate plain text files of expressions using the (load) function. If, for example, the code in Figure 5 existed in a text file called CHATLIB.LSP, you would tell XLISP: > (load 'chatlib) Teaching C to XLISP OS2XLISP can also call C functions in the dynamic-link library CRTLIB.DLL, which comes with OS2XLISP. Earlier we used ultoa, though in LISP it looked like (ultoa): > (define crtlib(loadmodule "CRTLIB")) 2520 > (define ultoa(getprocaddr crtlib "_ultoa")) 97984426 > (define (ultoa val base &aux (str(make-string 32 80))) (string-upcase (c-call ultoa val str (word base) 'str))) ULTOA > (ultoa dosgetinfoseg 16) "479B0000" Like LINK, (getprocaddr) is case sensitive. Functions that use the Pascal calling convention (such as DosBeep or KbdStringIn) should be presented to (getprocaddr) in uppercase, whereas functions like ultoa that use the cdecl calling convention should appear in lowercase with a leading underscore. Again, the symbol ultoa has both a numeric and a functional value. We can even pass the numeric value to the function, in order to print out the address in memory of the C ultoa function: > (ultoa ultoa 16) "5D71FAA" There are several ways to create local variables in LISP. Here I used &aux; str starts out as a local string of 80 spaces. After being passed to (c-call), which passes it to the C function ultoa, it will contain something useful, like "5D71FAA." Since ultoa takes three arguments, why was (c-call) passed a fourth argument, the symbol 'str? Most OS/2 functions by convention return an unsigned short error code, but with a function like ultoa that returns something else, like a char far *, we must tell (call) or (c-call) about it. The function (c-call) returns a string from ultoa; this string is then passed to the XLISP function (string-upcase). Even the most famous of C functions, printf-which takes a variable number of arguments-can be called from LISP, again through the magic of run-time dynamic linking (and some minor miracles performed by the Common LISP defmacro facility): > (define printf(getprocaddr crtlib "_printf")) 97980568 > (defmacro printf(mask &rest args)'(c-call printf , mask ,@args)) PRINTF > (printf "printf lives at %Fp\n" printf) printf lives at 05D7:1098 28 The Price of Flexibility While (getprocaddr) has clearly given us a lot of flexibility, it's a pain to use it every time we want to call a new function. Therefore OS2XLISP has an (enumproc) function to enumerate all the functions exported from a DLL; there is also an (install) function, which automatically creates procaddr variables for all the functions exported from a DLL: > (install "CHATLIB" t) SEND_MSG GET_MSG_CNT LOGIN GET_MSG LOGOUT T The install function even works for the DOSCALLS "module," which is actually the OS/2 kernel masquerading as a DLL. For DOSCALLS.DLL routines, DosGetProcAddr only works for "ordinal numbers," but OS2XLISP contains a table of function names for DOSCALLS. Because LISP code consists simply of LISP nodes consed together into lists, it is easy to write code that generates code. Whenever we've wanted to produce a new LISP function based on a routine in a DLL, we've gone through pretty much the same steps: get the procaddr, then write a function to (call) or (c-call) this procaddr with some parameters. We can make LISP go through these steps for us, using the short macro (make- call), shown in Figure 6. The function (make-call) takes an OS/2 module handle, the name of the routine, and any arguments to be passed to the routine. Nothing needs to be quoted. For example: > (make-call viocalls viogetphysbuf buf(word handle)) VIOGETPHYSBUF OS2XLISP has created a new function, (viogetphysbuf), which takes two parameters, buf and handle, and which will, when invoked, call the OS/2 function VioGetPhysBuf. Note that the (getprocaddr) operation happens only once, when (viogetphysbuf) is created, not each time that (viogetphysbuf) is called. So far, the DLL-based functions we've used have needed only simple parameters. What about OS/2 data structures? Consider (viogetphysbuf), which requires a pointer to a PhysBufStruct. The OS2XLISP function (make- struct), written in LISP, constructs OS/2 data structures from symbolic directives in LISP lists, and (unpack-struct) constructs LISP association lists from OS/2 data structures: ; the structure template (define PhysBufStruct '((ptr bufaddr)(long size)(word sel)(word sel2))) ; an instance of the structure, initialized (define physbuf(make-struct PhysBufStruct'(#xb8000 #x4000 0 0))) Now we can call the function that (make-call) generated for us: > (viogetphysbuf physbuf 0) 0 > (define sel(assoc 'sel(unpack-struct PhysBufStruct physbuf))) (SEL 847) > (cadr sel) ; get second element of list 847 We can verify that 847 really is a selector to the physical screen buffer: > (define (writechar x y char)(poke (mk-fp 847(* 2 (+ x(* 80 y))))char)) WRITECHAR > (writechar 79 24 #\a) 97 This will display the letter 'a' in the bottom right corner of a CGA screen. Protection Faults How does OS2XLISP protect itself from protection faults? While we were using (poke) just now, you might have thought, "Peek, poke, protected mode?" As the name implies, memory access in protected mode is protected, but functions like (peek) and (poke) let the user try any place in memory. If an OS2XLISP user asks to (peek) somewhere they have no business looking, OS/2 will not only want to shut down the user's incorrect (or malicious) program; it will send OS2XLISP itself a GP (general protection) fault. This presents a major problem for anyone writing an interpreter for protected mode. Protected mode is great, but a user's illegal command should simply cause the user's incorrect program to be terminated with an error message. Having the LISP interpreter itself shut down with a GP fault is not acceptable. OS2XLISP offers two solutions to this problem. By default, OS2XLISP.EXE will run itself as a debugged process under DosPTrace (full C and ASM source code for OS2XLISP is part of the package, soif you are really curious you can see how it's done), which is the only way to "catch" GP faults in OS/2: > (poke #xb8000000 666) break: Segmentation Violation This will enter OS2XLISP's built-in debugger where you can correct the problem or back-trace in order to figure out what went wrong (though in this case it's pretty obvious). Another way to enter the OS2XLISP debugger is by hitting Ctrl-C or Ctrl-Break. There is a problem with DosPTrace. DosPTrace is essentially the Microsoft CodeView(R) "engine," opened up for OS/2 programmers. Unfortunately, only one instance of DosPTrace can be running at one time. This means that if you want to run OS2XLISP under CodeView, either at the same time as CodeView or, more likely, run multiple sessions of OS2XLISP (perhaps to experiment with system semaphores or shared memory), you need some way to disable DosPTrace. OS2XLISP can be invoked with the -x command line, which disables use of DosPTrace and kicks in OS2XLISP's backup technique for self-preservation against GP faults committed by XLISP programs──using the 286 protected-mode instructions LAR, LSL, VERR, and VERW to check out a user's pointers before trying to dereference them. LAR returns a segment's access rights, LSL returns its size, VERR tells whether you can read it, and VERW tells whether you can write to it. If you pass an invalid segment number to all of these instructions, you do not get a GP fault. Heapwalking I found these instructions so useful that I brought them up to the surface for LISP programmers to use too. Figure 7 is a program called SEGS.LSP, which walks through all segments visible from OS2XLISP. After loading SEGS.LSP, you can run the function (total-segs), which will grind away and eventually print a statement of the number of segments that are visible from OS2XLISP and how much memory they take. This is easily run in the background, and if you instruct (total-segs) that OS2XLISP will run in the background, it will switch to idle priority and put up a pop-up when it's done. (Unfortunately, multithreaded applications can't yet be written in OS2XLISP.) The other function that can run from SEGS.LSP is (print-segs), which passes a callback function to the function (enumsegs). Every time the callback gets called, it prints out a segment's size, what it is (readable code, writable data, or whatever), and whether the segment is present in memory (see Figure 8). Why are the segments in this example grouped by fours? In 286 protected mode, a segment number (the numbers we got from DosGetInfoSeg, like 96 or 15) is not just any old number, but a structure composed of a number of fields, as shown in Figure 9. Thus, the numbers alone say something about these memory segments. For example, the GIS has an RPL of 0 and is, naturally enough, in the global descriptor table (GDT): > (ultoa gis 2) ; binary 96 "1100000" The LIS has an RPL of 3 (11 in binary) and occupies the first slot in the local descriptor table (LDT): > (ultoa lis 2) ; binary 15 "1111" Since the requested protection level is in the bottom 2 bits of this structure, which we think of as a segment number, clearly there will be four different segment numbers which differ only by their protection level. For example, segment 15 is overlayed by segments 12, 13, and 14. The other segment attributes are kept in the segment access-rights byte, which we retrieve with (lar). SEGS.LSP defines several macros to interpret the bits returned by (lar). For instance, is the GIS writable data? Is it present in memory? > (write? (lar gis)) NIL > (present? (lar gis)) T We can make certain this works by checking one of OS2XLISP's own code segments. First we verify that a given segment is not present in memory by getting the address of a function, extracting the segment from the far pointer with (fp-seg), taking the (lar) of the segment, and passing it to (present?). We call the function, then we check the present bit again: > (present? (lar (fp-seg (addr(function sin))))) NIL ; maybe even see disk light come on as segment loads in > (sin (/ 3.14159 2)) 1 > (present? (lar (fp-seg (addr (function sin))))) T It works! And──admit it──all those parentheses aren't so intimidating after all, are they? Modula-2 In many ways, Modula-2 is a perfect match for OS/2. Just as OS/2 is built around the ideas of dynamic linking and multiple threads, Modula-2 is built around the ideas of the module and multiple processes. Some of the parallels between OS/2 and Modula-2 are really quite remarkable. For instance, the Modula-2 NEWPROCESS procedure, which creates a coroutine that maintains its own context and can execute in parallel with other coroutines, takes the following parameters: PROCEDURE NEWPROCESS ( (* a procedure without parameters *) Code : PROC; (* stack *) Workspace : ADDRESS; (* size of stack *) WsSize : CARDINAL; (* will receive handle to coroutine *) VAR process : ADDRESS); Anyone familiar with OS/2 will recognize that this is almost identical to: DosCreateThread(void (far *procaddr)(void), unsigned far *thread_id, char far *stack); Unlike C, concurrent programming is built into the language definition for Modula-2. Niklaus Wirth designed Modula-2 as a high-level language that could be used to write operating systems, device drivers, and so on. This required a wide range of low-level facilities, as well as multitasking. Processes are defined in Modula-2 itself, rather than in extensions (such as Concurrent C developed at Bell Labs), making portability possible. The Modula-2 StartProcess call would be implemented in OS/2 using DosCreateThread, in UNIX using fork, and perhaps you could implement processes in MS-DOS by writing your own scheduler; but if a program calls StartProcess rather than calling the underlying facility, it has a chance of being ported to other environments. Wirth recognized that a language like Pascal that insists on recompiling an entire program each time a change is made is not suitable for large development projects. Modula-2 thus features separate compilation, allowing systems to be broken into individual components, but maintaining complete type checking across module boundaries. Note that separate compilation of dependent modules is not the same as independent compilation. Even the ability to check the compilation dates of dependent modules is built into the language. Consider the difference between this and the ad hoc approach to modularity found in C. The forthcoming ANSI C standard views function prototyping as optional, and the inclusion of prototypes in include header files is still rather a weak convention. The Module As might be expected from its name, Modula-2 raises the module to fundamental importance. Looking back at the Modula-2 example in Figure 1, note the statement "IMPORT DosCalls" and the call to DosCalls.DosGetModName. If that function call looks like it is accessing a field in a data structure, it was meant to. I could have said "FROM DosCalls IMPORT DosGetModName," and then said DosGetModName without qualifying it, but I wanted to emphasize that in Modula-2 modules are really akin to data structures. A module can export procedures, variables, constants, records, and types. Looking ahead to SEM.DEF in Figure 10, we see that the module SEM exports two types, which are called CountingSemaphore and BinarySemaphore, and various operations on these types, such as P, V, Request, and Clear. Equally important, a module can choose not to export something. A module usually hides a set of data and provides a set of operators in order to manipulate this data. For instance, while SEM.DEF exports the new type CountingSemaphore, this is an "opaque type" in that its internals in SEM.MOD are not exported (SEM.MOD also takes care of memory allocation for any objects created of this type). Information hiding is as important as information sharing; the goal is to keep the connections between modules as small as possible. This modularization also pays off later when trying to break up an application into several well defined dynamic-link libraries. There are two important components: definition modules and implementation modules. A definition module represents the "Ministry of the Exterior," that is, the face that a module shows to the outside world (you might say that hypocrisy is a language feature of Modula-2). This .DEF file is precompiled into a symbol table (.SYM file), which is used to do type checking across module boundaries. Because a module can only import another module's definition part, I need only write the definition part for it to work. Definition modules can be used as a "language" in which to write functional specifications. The implementation module, of course, contains the code. In addition to what I said earlier about information hiding, this is a perfect place to put all operating-system-dependent code. An implementation module can contain auto-initialization code as well, comparable to the INITINSTANCE option for an OS/2 DLL. A module can contain submodules, such as Self within SEM.MOD (see Figure 10). Finally, an implementation module would be the place to bury the details of handling mutual exclusion for a limited resource like VIO or KBD. Because it has built-in multiprogramming, the Modula-2 standard library expects to be called from multiple threads of execution. It is therefore quite easy to write multithreaded applications and OS/2 or Windows dynamic-link libraries in Modula-2. By contrast, until releasing the MTDYNA (multithread) libraries in C 5.1, Microsoft has had to publish a list of those C standard library functions that could not be called from multithreaded applications, Windows, or OS/2 dynamic-link libraries. Semaphores Modula-2 does not offer any semaphore/signal functions that represent an improvement over the services available in OS/2. In the Stony Brook Modula-2 implementation, the Processes module-as described by Niklaus Wirth in Programming in Modula-2 (Springer-Verlag, 1983)-uses OS/2 threads and semaphores. The module Processes defines the type Signal and the operations Send, Wait, Init, Awaited, and StartProcess. The Stony Brook documentation recommends using signals only if you are seeking compatibility with other versions of Modula-2, since OS/2 offers a more effective means of synchronization. Although it is easy enough to write multithread code, and we can get the StartProcess procedure-a portable way to launch multiple threads of execution-Modula-2 doesn't provide much in the way of process synchronization. On the other hand, simply having the Send and Wait functions in Modula-2 does encourage us to think about higher-level synchronization than is provided by OS/2 primitives like DosSemRequest. There is a great gap between the powerful semaphore primitives supplied by OS/2, and the great body of classical "academic" concurrency theory. Modula-2 helps bridge that gap. Dining Philosophers In Figure 10, I have used Stony Brook Modula-2 to implement a solution to the Five Dining Philosophers problem, which is a vivid demonstration of the problems of multiprogramming first posed by the Dutch computer scientist E.W. Dijkstra in 1965. The following is a summary from M. Ben-Ari's Principles of Concurrent Programming (Prentice-Hall, 1982), p. 109-110 from which our solution borrows as well: "The problem is set in a monastery whose five monks are dedicated philosophers. Each philosopher would be happy to engage only in thinking were it not occasionally necessary to eat. Thus the life of a philosopher is an endless cycle: repeat think; eat forever... In the center of the table is a bowl of spaghetti that is endlessly replenished; there are five plates and five forks. A philosopher wishing to eat enters the dining room, takes a seat, eats, and then returns to his cell to think. However, the spaghetti is so hopelessly entangled that two forks are needed simultaneously in order to eat. The problem is to devise a ritual (protocol) that will allow the philosophers to eat... Each philosopher may use only the two forks adjacent to his plate. The protocol must satisfy the usual requirements: mutual exclusion (no two philosophers try to use the same fork simultaneously) and freedom from deadlock and lockout (absence of starvation──literally!)." Obviously, the five philosophers represent five processes (or, in OS/2 terms, threads). The five forks represent critical resources over which five semaphores will ensure mutual exclusion. Since there are five philosophers (threads) and five forks (resources), and each philosopher needs two forks to eat, two philosophers can eat at any given time; the forks can't be protected with a simple critical section, because then only one philosopher could be eating at one time. Since the five philosophers do the same thing, the five processes all execute the same piece of code: in Figure 10, the procedure philosopher. Because both OS/2 and Modula-2 treat concurrent processes as parameterless procedures (and because philosophers ought to know who they are), each thread executing through the procedure philosopher determines its own identification by calling self. In SEM.MOD, self is implemented by pulling the thread ID number out of the local info seg. Life is an endless circle, therefore the philosopher procedure consists mostly of an endless loop. However, for any given philosopher, certain properties do not change: the philosopher's identity, and which forks he should use. These are determined once and deposited in the philosopher's memory (that is, left on each thread's stack). Once inside its loop, each philosopher sits in a cell and thinks for awhile, then tries to enter the dining room. If there are five philosophers (the constant NUM_PHILOSOPHERS can be changed), then there can be four philosophers in the room. The philosopher executes the code P(room). Here room is a counting semaphore, and P is the Wait operation (the Dutch for try is "proberen"). When the philosopher is ready to return to a cell, he will execute the code V(room); V is the Release operation (the Dutch for release is "vrygeven"). The implementation of counting semaphores is contained in the module SEM.MOD. It borrows heavily from the OS/2 simulation of counting semaphores in "Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution" (MSJ, Vol. 3, No. 3). No OS/2-dependent code appears in DINING.MOD. All system-dependent code is isolated within SEM.MOD (and in the library modules written by Stony Brook Software, of course; all source code for the libraries comes with the development package). Yet, this is simply an OS/2 program with threads and semaphores. Once in the dining room, a philosopher waits until he can pick up his left fork, then waits again until the right fork is available. Deadlock is not a problem here. The counting semaphore room keeps the number of philosophers trying to access forks below the number of forks. Therefore, at least one philosopher may eat at any given time. An array of binary semaphores represent the forks. (Binary semaphores are simpler than counting semaphores, and can be implemented as straight calls to DosSemRequest and DosSemClear.) After eating, each philosopher releases the forks, leaves the room, and starts over again. To start the five philosopher threads in the initialization code in DINING.MOD, we say: FOR i := 1 TO NUM_PHILOSOPHERS DO StartProcess(philosopher, 2048); END; In order to ensure that all the philosophers start together, we simulate the cobegin-coend construct of concurrent programming. In SEM.MOD, these are implemented as simple calls to DosEnterCritSec and DosExitCritSec. As long as the main process is in its critical section, it can start processes, but they can't run; as soon as it exits its critical section, all the processes start together. I kept running into a problem while writing DINING.MOD. All my threads would start properly, but for some reason the program would exit back to OS/2 almost immediately. The problem was that the main thread, having brought the philosophers to life, had nothing else to do and would exit. I introduced another semaphore called fini; the main thread waits on this semaphore, but none of the philosophers ever release it. I suppose one could say that this signifies that philosophizing always goes on and has no end. To finish off the otherwise endless cycle of thinking and eating, Ctrl-C needs to be hit. The philosophers show what they're doing by issuing little statements to the screen: thinking: 4 eating: 2 eating: 1 eating: 4 thinking: 5 thinking: 3 thinking: 2 thinking: 1 eating: 3 ∙ ∙ ∙ To keep these strings from interfering with each other on the screen (e.g. "thieateateating: 4inginging: 2 : 1 : 4"), we have to use yet another semaphore, called iosem. Since this has no bearing on the dining philosophers problem itself, all code concerned with I/O is isolated in an internal module within DINING.MOD. Another problem cropped up while writing DINING.MOD: occasionally the program would be terminated with a GP fault. After spending some time trying to decipher the OS/2 register dump, I remembered that another feature of Modula-2 is optional run-time error checking, so I turned on the /CHECK compiler switch and recompiled. The next time I ran the program, it provided a message about an out-of-bounds array access from line 79 of DINING.MOD and, sure enough, that's where the bug was. This alone makes Modula-2 worthy of consideration. After getting the dining philosophers program running, the next step was to split off SEM.MOD as an OS/2 dynamic-link library, SEM.DLL. This required writing a DEF file for LINK-not to be confused with the Modula-2 DEF file also shown in Figure 10, along with the necessary OS/2 commands (M2 is the Stony Brook Modula-2 compiler). In addition to running several concurrent copies of DINING.EXE, all of which are dynamically linked to SEM.DLL, I also called SEM.DLL from C (remember that SEM.MOD takes care of allocating space for the counting semaphore itself): typedef void far *CountingSemaphore; ∙ ∙ ∙ CountingSemaphore room; /* pointer to pointer */ CSInit(&room, 4); ∙ ∙ ∙ P(room); Stony Brook Modula-2 comes with what must be the world's largest collection of options for interfacing with other languages. Whereas most C compilers will give you the simple division of CDECL and Pascal calling conventions, Stony Brook Modula-2 has the optional keywords REVERSED (push parameters in reverse order), LEAVES (procedure leaves parameters on the stack), ALTERS (specify which registers a procedure alters), RETURNS (specify the registers in which a procedure returns its value), and VARIABLE (procedure accepts a variable number of arguments). For instance, to import the C function fprintf into Modula-2, you would use REVERSED, LEAVES, VARIABLE, and ALTERS (AX,BX,CX,DX,ES). But the real advantage is in being able to use, for instance, LEAVES without having to use REVERSED as well; this would be just the ticket for writing a push function that would intentionally leave data on the stack to be consumed by other functions. Stony Brook Modula-2 interfaces very nicely with C──unusual for a product whose ads feature Marc Antony saying, "I have come to bury C, sir, not to praise it." BASIC BASIC has grown up a lot since John Kemeny and Tom Kurtz invented the language in 1964. You no longer need line numbers, there are genuine functions and subroutines, and certainly it's a sign of maturity that Microsoft BASIC 6.0 is a compiler (not an interactive interpreter) that comes with a full range of inscrutable job-control-languagelike command- line switches like /Zi, /ah, and /d/e/x. In one way, BASIC fits right into OS/2; given the BASIC tradition of creating programs that require a run-time environment──BRUN.COM in earlier versions of Microsoft BASIC, for example──in OS/2 it's natural to make this run-time environment a dynamic-link library (BRUN60EP.DLL). Compare this to the more shaky transition that C, with its heritage of standalone applications, is making towards DLLs; CRTLIB.DLL is a rather under-publicized option in C 5.1. Even the run-time environment for real-mode BASIC 6.0 seems to have been redone to closely resemble a DLL. In any event, BASIC is still a beginner's language. But for precisely this reason, BASIC has many powerful features. It may not occur to a beginner that space must be allocated for strings in memory. Therefore, BASIC takes care of this itself: print "hello " + "world" This is equivalent to saying: puts(strcat("hello ","world")); which you most certainly cannot say, as "world" will overwrite some memory which does not belong to it. In contrast, BASIC takes total control over managing string space, making BASIC strings easy to use, intuitive, and therefore powerful. Note also that the one line of BASIC is a complete program, whereas the equivalent program in Modula-2 or even C takes several lines (or perhaps 100 if you're programming for Windows). Complex programs that can easily be written in C, on the other hand, are more difficult to write in BASIC. Flexible Syntax The PRINT statement is another example. It has many optional parameters, but if you just say to PRINT something you get reasonable output. Back in Figure 1, printing out the module number and name in BASIC was more concise than in any of the other languages. The BASIC example in Figure 1 also shows that, for the most common cases, variables are automatically declared "by appearance." The first occurrence of a variable named i% means it is an integer variable; buf$ is a string variable because its name ends with a dollar sign. The symbol at the end of a name is called a "type tag." BASIC strings aren't ASCIIZ strings, so in those rare moments when you get an ASCIIZ string back from OS/2, you must figure out how to print it (in Figure 1, I used rtrim$ to remove trailing blanks). Creating an ASCIIZ string, on the other hand, is no problem. You simply append a zero delimiter at the end: asciiz$ = "hello world" _ + chr$(0) Being able to make string assignment and concatenation this intuitively does mean that BASIC may have to move strings around in memory. Normally this doesn't matter, but if you need to get the address of a variable- length string, you better use it immediately. In Figure 1, note that taking the address of buf$ requires two function calls; first is the segment with varseg, then we get the string address with sadd. This is a good example of the trade-off involved in BASIC programming. Tasks that should be simple are simple, but a more complex task can be a real chore. It is, however, valuable to have at least one language that makes this trade-off. Nothing to Declare? Advanced BASIC programs rely heavily on Microsoft BASIC 6.0's DECLARE statement, which is the gateway to other languages such as C, assembler, and Modula-2, to dynamic-link libraries, and the OS/2 operating systems. Figure 1 showed a typical declaration from the header files supplied by Microsoft. Once declared, DosGetModName can be used like any other function. The BYVAL keyword in the declaration is very important. By default, BASIC passes the offset (near address) of a variable, so BYVAL instructs it to pass "by value." Another keyword, SEG, tells BASIC to pass "by reference," but to use a full 4-byte segmented address. Once an argument has been declared this way, you don't need to worry about taking its address; see INFOSEG.BAS, the little program shown in Figure 11. There are several things to note here. Since we usually ignore any error code from DosGetInfoSeg, we can DECLARE it as a subroutine rather than a function, and since it's a subroutine, we don't need parentheses around its arguments when we call it. By using the SEG keyword in the declaration, BASIC will take the far address of the variables for us. We don't need to declare these variables-we just use them (what a novel idea). All in all, it can lead to some very natural-looking code. By declaring DosGetInfoSeg as a subroutine and calling it without parentheses, we have in effect created a new BASIC keyword. Another useful OS/2 subroutine in which we might ignore an error code is DosBeep, an especially handy function since the SOUND and PLAY statements have been disabled for protected-mode BASIC: declare sub Honk alias _ "DOSBEEP" _ (byval freq%, byval dur%) Honk 880, 100 Once again, we've created something that works just like a built-in BASIC statement. Because of the ALIAS option to DECLARE, we can link to DosBeep, but call it something else in our BASIC programs. A Taste of "Macro BASIC" The OPEN statement is fairly typical of BASIC, having lots of parameters that are optional, and a reasonable default behavior if you don't use any options. For example, open "foo.bar" as #1 simply opens the file for read/write random access. This is fine for beginners and for simple applications. But you can also say something like: open "foo.bar" for output _ access write lock _ write as #1 Furthermore, the string used for the filename can also hold parameters that completely change what OPEN does: open _ "com1:9600,n,8,1,bin" _ as #1 One of the new features for protected-mode BASIC adds another option to the OPEN statement, OPEN "PIPE:", making it easy to use the OS/2 pipe facility in which programs communicate via standard file I/O calls: open _ "pipe:exehdr brun60ep.dll _ 2>nul" as pipe The program in Figure 12, ENUMPROC.BAS, prints out the names of all functions exported from an OS/2 DLL. For instance, C>enumproc \dll\brun60ep prints out all the functions in the BASIC run-time library (which have names like B$DRAW and B$POKE). ENUMPROC.BAS doesn't know anything about the structure of a DLL file, but it does know that EXEHDR.EXE knows about these files, so it makes EXEHDR's output its input. Since the output from EXEHDR is made into a BASIC variable, we could manipulate it in any way we want. Thus, the OPEN "PIPE:" statement permits one program to use another program as a subroutine. In ENUMPROC.BAS we also use OS/2 redirection ("2>nul") to discard a few lines of output that EXEHDR sends to stderr, as well as BASIC's important ability to determine when EXEHDR has finished feeding us input. While this is often difficult to determine when using OS/2 pipes (resulting in hung programs), in BASIC all we need to say is: on error goto closefile (showing that BASIC can occasionally look amazingly like psuedocode). We get input from EXEHDR in an endless loop (do while 1), but when our attempt to get input from EXEHDR eventually fails, BASIC raises an error. Because we've set up an error trap, our error handler (labeled closefile) gets called and we can close up shop. The Three E's One powerful feature of BASIC is its handling of "the three E's"; errors, exceptions, and events. Perhaps derived from the similar ON statement found in PL/I, the ON statement in Microsoft BASIC allows you to create "daemons" that check for a wide range of run-time errors, asynchronous events, and signals. At no point in Figure 12 do we explicitly check for an end-of-output from EXEHDR. BASIC does all checking behind the scenes──though not as a background thread; instead, the compiler inserts CALLs to an error- checking routine. This makes the code easier to read since the normal course of events is not cluttered up with a lot of IF statements. Code that uses an error daemon is generally a lot easier to understand than code written using defensive programming. Stony Brook Modula-2 has a similar facility in its Error module, and something like this can certainly be cooked up in C using setjmp/longjmp. However, BASIC's ON statement provides the easiest-to-use error handling, and it uses the same technique to handle nonerror conditions, such as a timer going off (ON TIMER), a key being struck (ON KEY), data arriving on a COM port (ON COM), or, in protected mode, a signal being received (ON SIGNAL). The program listed in Figure 13, PEEKER.BAS, presents another interesting aspect of error-checking in protected-mode BASIC. As you may remember from the above discussion of OS2XLISP, PEEK and POKE statements present a problem in OS/2 protected mode. PEEKER.BAS runs in an endless loop, asking the user to input a segment and an offset. If the byte at segment:offset can be read, its value is printed out. Otherwise, BASIC catches the deadly GP fault, and fields it to the error handler, which prints out "Permission denied." To represent an illegal memory access in protected mode, BASIC uses the same error number, 70, that it uses to represent an illegal file access. It makes sense to treat memory as a shareable, but partially locked resource like a file. If you run PEEKER.EXE under CodeView, you'll see that BASIC uses VERR, VERW, and LSL before performing the PEEK so the error is being prevented rather than caught. Porting to OS/2 Speaking of PEEK and POKE, a lot of what's called machine-dependent code is actually dependent on the operating system. The manual for BASIC 6.0 and Microsoft QuickBASIC 4.0 is a case in point, containing a note such as "This program contains hardware-specific instructions. It works correctly on IBM(R) PC/XT(R), and PC/AT(R) computers." In fact, such programs will work correctly under MS-DOS, but not on an IBM AT under OS/2. The manual entry for DEF SEG shows how to use DEF SEG, PEEK, and POKE to toggle the Caps Lock key. This program, which peeks and pokes into memory locations outside the program's purview, will not work under OS/2 (you can verify this by keying the numbers into PEEKER.EXE). In order to toggle the Caps Lock key in OS/2, you should use DosDevIOCtl or KbdSetStatus. The manual entry for CALL ABSOLUTE uses INT 11H to determine if a math coprocessor is present. The easiest way to do this under OS/2 is to use the DosDevConfig function, which is a very close match to the equipment- list service provided by ROM BIOS interrupt 11H. However, since the aim of the manual example is to demonstrate how CALL ABSOLUTE can execute a machine-language program stored in a BASIC variable, we can do that too under OS/2. In the code listed in Figure 14, SMSW.BAS, a machine-language subroutine, is stuffed into a BASIC string and the string is executed. The executable string, named code$, contains the protected mode SMSW instruction, which retrieves the 286 machine status word. If bit 1 of the word is set, a coprocessor is present; if bit 2 is set, there is no coprocessor and/or someone has installed a handler for INT 7 (no numeric processor), so the coprocessor is being emulated in software. Before executing the string with CALL ABSOLUTE, we must remember that protected mode prevents us from loading a data segment selector into the CS register. Therefore, in SMSW.BAS I called the OS/2 function DosCreateCSAlias, which takes a data segment selector and returns a selector to an alias which is executable. It turns out that when you are using CALL ABSOLUTE in BASIC under OS/2, this isn't strictly necessary. Running PEEKER.EXE under CodeView, it becomes clear that the compiler calls DosCreateCSAlias for you. Since this isn't documented, it makes sense to call DosCreateCSAlias anyway. If you do, the second call will fail when CALL ABSOLUTE calls it again because you cannot get a CS alias for something that's already a code segment. This is probably a good a time to point out that CodeView knows all about BASIC, which not only helps debug BASIC programs, but also helps in snooping around the code generated by the compiler. CodeView has a built- in BASIC expression evaluator, to make queries like: > ? varseg(code$) &H002f Bringing Up BASIC There is now a much easier way to add new capabilities to BASIC than stuffing machine language code bytes into strings. Because the Microsoft BASIC compiler produces standard .OBJ files, and because of the important DECLARE statement, we can link in any code contained in standard .OBJ files. Figure 15 shows the code for both PROTMODE.ASM and SMSW2.BAS, and shows a better way to port the SMSW instruction to BASIC. Notice how BASIC's DECLARE SUB lets me create the natural-looking SMSW x%, rather than having to write x% = SMSW. Because MASM 5.1 has built-in support for high level languages, once we give the .MODEL MEDIUM,BASIC directive, MASM will take care of almost all the details of creating BASIC-compatible code. Knowing that we can easily use the SMSW instruction in BASIC, you won't be surprised to hear that we can just as easily add our old friends LSL, LAR, VERR, and VERW to BASIC's repertoire. PROTMODE.ASM also contains code for an LSL function. Used from BASIC it looks like: declare function LSL& _ alias "_LSL" (byval x%) print "LSL for some BASIC _ data segment is"; _LSL&(varseg(x%)) Since BASIC integers are always signed, if the LSL function returned a 2- byte integer, we would see negative numbers whenever we print the LSL of a segment larger than 32767 bytes. Therefore the BASIC LSL function returns a 4-byte long, as indicated by the ampersand type tag. What's In, What's Out A number of things were left out of the OS/2 version of BASIC. The BASIC functions and statements that provide access to ports are missing. (We could add these ourselves, just as we added other assembler instructions, though we would also need an IOPL statement in an OS/2 .DEF file.) Also omitted were the BASIC sound statements (we've already seen we can use DosBeep), and the IOCTL statement (we can use DosDevIOctl instead). Further, no header files for the OS/2 VIO and KBD functions are provided even though we can dynamically link to them, because going outside BASIC's standard I/O statements might pull the rug out from under BASIC. Fortunately, the extremely rich collection of BASIC graphics routines has been implemented for OS/2 protected mode, providing another way to do graphics under OS/2 until Presentation Manager is available. The bad news is that only CGA screen modes are available, and graphics will not be drawn to the screen while a BASIC program is in the background. You can forget about plotting the Mandelbrot set in the background, at least for now. Furthermore, while a program called GW-BASIC(R) is included with OS/2, the famous line-numbered BASIC interpreter does not run in protected mode. While Microsoft QuickBASIC 4.0 is included with BASIC 6.0, this interactive programming environment also will not run in protected mode. Finally, the current BASIC run-time library is not reentrant, which means you can't use BASIC 6.0 to write multithreaded applications. The Narrow View We have taken a rather parochial view of these four products, discussing UR/FORTH without mentioning process control, and somehow discussing LISP without using the phrase "artificial intelligence." And I don't know how it happened, but the word "education" didn't appear once during the discussion of BASIC. We did get a clue to the perennial question, "Why are there so many languages?" Each of the languages we've examined has a number of really superb ideas. Forth provides user-programmable syntax, user control of the compiler and interpreter, words that communicate via the stack, and everything is a word. LISP code takes the same form as lists and the language manipulates symbols rather than variables. Dynamic linking is deferred until run time. In Modula-2, the module itself is almost a new data type, concurrent programming is built into the language, and there is high-level synchronization between modules. BASIC manages string space for you, handles errors without cluttering your program with error-checking, and most common operations have optional parameters with variables automatically declared. Figure 16 presents a comparison chart of the products we have discussed. OS/2 promotes interactive software development, even with batch-mode compilers. You keep your protected-mode editor running in its own session, and toggle instantly between editor, debugger, compiler, and your running/crashing program. Because crashes are less deadly than in real mode, you are more likely to carry out ad hoc experiments. OS/2's multitasking capabilities, and its extensibility via DLLs, means a whole new generation of incredible compilers, interpreters, and integrated programming environments. We are already beginning to see this happen. In fact, rather than being stranded on a desert island, we may be returning from the island to civilization. Vendor Information UR/FOTH Version 1.1: Laboratory Micosystems, Inc. P.O. Box 10430 Marins Del Rey, CA 90295 (213) 306-7412 OS2XLISP (XLISP Version 2.0, OS/2 extensions Version 1.10): Available on CompuServe Microsoft Systems Forum (GO MSSYS) (will soon be available on BIX as well) Stony Brook Modula-2 Development System Version 1.12: Sony Brook Software Inc. Forest Road, P.O. Box 219 Wilton, NH 03086 (603) 654-2525 Microsoft BASIC Compiler 6.0: Microsoft Corporation 16011 NE 36th Way Box 97017 Redmond, WA 98073-9717 (800) 426-9400 Figure 1: An OS/2 Rosetta Stone In C (using Microsoft C 5.1) /* from bsedos.h */ USHORT APIENTRY DosGetModName(HMODULE, USHORT, PCHAR); /* enumdll.c */ #include <stdio.h> #define INCL_DOSMODULEMGR #include "os2.h" main() { char buf[128]; register unsigned int i; for (i=0; i<=0xFFFF; i++) if (! DosGetModName(i, 128, buf)) printf("%u\t%s\n", i, buf); } ─────────────────────────────────────────────────────────────────────────── In Forth (Laboratory Microsystems 80286 UR/FORTH 1.1 for OS/2) \ no header files CREATE BUF 128 ALLOT \ create a buffer : ENUMDLL \ define a new word 65535 0 DO \ for i=0 to 65535 do I 128 DS0 BUF DOSGETMODNAME \ DosGetModName(i,128,ds:buf) 0= IF \ if no error CR \ carriage return I 5 U.R 3 SPACES \ display i nicely BUF -ASCIIZ COUNT TYPE \ convert ASCII string and print it THEN LOOP ; ENUMDLL \ invoke the word ─────────────────────────────────────────────────────────────────────────── In LISP (OS2XLISP, Version 1.10) ; no header files (define doscalls (loadmodule "DOSCALLS")) "DOSGETMODNAME")) (define dosgetmodname (getprocaddr doscalls "DOSGETMODNAME")) (define buf (make-string 32 128)) ; string of 128 spaces (dotimes (i #xFFFF) (if (zerop (call dosgetmodname (word i) (word 128) buf)) ; then (format stdout "~A\t~A\n" i buf))) ─────────────────────────────────────────────────────────────────────────── In Modula 2 (Stony Brook Software Modula-2 for OS/2): (* from doscalls.def *) PROCEDURE DosGetModName ['DOSGETMODNAME'] ( ModuleHandle : CARDINAL; (* the module handle to get name for *) BufferLength : CARDINAL; (* the length of the output buffer *) Buffer : Asciiz (* the address of output buffer *) ) : CARDINAL; (* enumdll.mod *) MODULE ENUMDLL; FROM SYSTEM IMPORT ADR; FROM InOut IMPORT Write, WriteLn, WriteString, WriteInt; IMPORT DosCalls; VAR i : CARDINAL; buf : ARRAY [0..128] OF CHAR; BEGIN FOR i := 0 TO 65535 DO IF DosCalls.DosGetModName (i, 128, ADR(buf)) = 0 THEN WriteInt(i, 5); Write(CHAR(9)); WriteString(buf); WriteLn; END END; END ENUMDLL. ─────────────────────────────────────────────────────────────────────────── In BASIC (Microsoft BASIC Compiler 6.0) ' enumdll.bas ' compile with /d so you can Ctrl-Break ' from bsedospe.bi DECLARE FUNCTION DosGetModName%( _ BYVAL P1 AS INTEGER,_ BYVAL P2 AS INTEGER,_ BYVAL P3s AS INTEGER,_ BYVAL P3o AS INTEGER) ' include file for Program Execution Support REM $INCLUDE: 'bsedospe.bi' buf$ = SPACE$(128) FOR i% = 0 TO 32765 ' only signed integers available Result = DosGetModName%(i%, 128, VARSEG(buf$), SADD(buf$)) IF Result = 0 THEN PRINT i%, RTRIM$(buf$) buf$ = SPACE$(128) ' have to reset buffer, because END IF ' BASIC doesn't know about ASCIIZ NEXT i% ─────────────────────────────────────────────────────────────────────────── Sample Output 140 A:\HARDERR.EXE 220 D:\OS2\DLL\BMSCALLS.DLL 380 D:\OS2\SYS\SHELL.EXE 630 A:\SWAPPER.EXE 750 D:\OS2\DLL\BKSCALLS.DLL 930 D:\OS2\DLL\ANSICALL.DLL 1210 D:\OS2\DLL\EPS-LIB.DLL 1230 D:\OS2\DLL\MOUCALLS.DLL 1240 D:\OS2\DLL\QUECALLS.DLL 1330 D:\OS2\DLL\SESMGR.DLL 1340 D:\OS2\DLL\BVSCALLS.DLL 1380 D:\OS2\DLL\VIOCALLS.DLL 1390 D:\OS2\DLL\KBDCALLS.DLL 1480 D:\OS2\DLL\DOSCALL1.DLL 1490 D:\OS2\DLL\NLS.DLL 1550 D:\OS2\DLL\MSG.DLL 1590 D:\OS2\E\EPSILON.EXE 2240 D:\OS2\DLL\MONCALLS.DLL 2320 D:\URFOS2\FORTH.EXE 2960 D:\OS2\DLL\CRTLIB.DLL 2980 E:\XLISP\NEW\OS2XLISP.EXE 3190 D:\OS2\DLL\ALIAS.DLL 3630 D:\OS2\SYS\CMD.EXE Figure 2: A better Forth implementation of the Rosetta Stone program. FORGET ENUMDLL \ get rid of previous definition CREATE BUF 128 ALLOT : MODULE? ( n -- flag ) 128 DS0 BUF DOSGETMODNAME 0= ; : .ASCIIZ ( addr -- ) -ASCIIZ COUNT TYPE ; : .MODULE ( n -- ) CR 5 U.R 3 SPACES BUF .ASCIIZ ; : ENUMDLL ( -- ) 65535 0 DO I MODULE? IF I .MODULE THEN LOOP ; Figure 3: Code Listing for Threads.4th \ threads.4th \ Andrew Schulman 23-July-1988 \ revised 27-July: using GETNUM from input stream, not stack \ revised 3-August: using new TASKER.BIN (08-02-88) TASKER \ ============================================================ \ note that each thread has its own view of the LDT VARIABLE GIS VARIABLE LIS DS0 GIS DS0 LIS DOSGETINFOSEG DROP : SELF ( -- thread# of current thread ) LIS @ 6 @L ; : CHAR ( n -- char ) DUP 9 > IF 55 ELSE 48 THEN + ; \ ============================================================ \ stolen from os2demo.scr VARIABLE SEED : (RAND) SEED @ 259 * 3 + 32767 AND DUP SEED ! ; : RANDOM (RAND) 32767 */ ; : RY ( -- y ) ?YMAX 1+ RANDOM ; : RX ( -- x ) SELF 4 MOD 20 * 20 + RANDOM SELF 4 MOD 20 * MAX; \ ============================================================ \ use UR/F semaphores, instead of directly calling OS/2 SEMAPHORE SEM VARIABLE ID VARIABLE ATTR 12 ATTR ! \ display thread ID -- multiple threads will execute through \ this same code if the semaphore is removed, you can see \ each thread grab wrong thread id# : SHOW-ID ( -- ) BEGIN SEM SGET SELF CHAR ID ! \ use VioWrtCharStrAtt because it is properly \ reentrant and also does not affect the cursor \ position DS0 ID 1 RY RX DS0 ATTR 0 VIOWRTCHARSTRATT DROP SEM SREL PAUSE AGAIN ; \ ============================================================ \ simulate the cobegin-coend construct, so all threads start together : COBEGIN ( -- ) DOSENTERCRITSEC DROP ; : COEND ( -- ) DOSEXITCRITSEC DROP ; \ ============================================================ 2048 128 TCB THREAD1 2048 128 TCB THREAD2 2048 128 TCB THREAD3 2048 128 TCB THREAD4 : (START) ( tcb -- ) DUP START SHOW-ID WAKE ; : START-IT ( -- ) COBEGIN THREAD1 (START) THREAD2 (START) THREAD3 (START) THREAD4 (START) COEND ; \ ============================================================ \ important to not stop thread while it's holding the semaphore : (STOP) ( tcb -- ) SEM SGET STOP SEM SREL ; : STOP-IT ( -- ) THREAD1 (STOP) THREAD2 (STOP) THREAD3 (STOP) THREAD4 (STOP) ; \ ============================================================ \ get a number from input stream, put on stack : GETNUM ( -- n ) BL WORD NUMBER? 2DROP ; \ ============================================================ \ UR/F tasks are OS/2 threads, so we can manipulate a task \ using OS/2 calls \ important to claim semaphore, so we don't suspend a thread \ while it's holding on to the semaphore ! : SUSPEND ( -- ) GETNUM SEM SGET DOSSUSPENDTHREAD SEM SREL DROP ; : RESUME ( -- ) GETNUM DOSRESUMETHREAD DROP ; \ if entered from keyboard, suspend/resume all background tasks : CRIT ( -- ) DOSENTERCRITSEC DROP ; : XCRIT ( -- ) DOSEXITCRITSEC DROP ; \ if entered from keyboard, also suspend/resume all background tasks : GETSEM ( -- ) SEM SGET ; : RELSEM ( -- ) SEM SREL ; \ ============================================================ VARIABLE PRTY : GET-PRTY ( -- ) 2 DS0 PRTY GETNUM DOSGETPRTY DROP PRTY @ . ; : SET-IDLE ( -- ) 2 1 0 GETNUM DOSSETPRTY DROP ; : SET-REG ( -- ) 2 2 0 GETNUM DOSSETPRTY DROP ; \ warning: if you set a background thread to time-critical \ from the keyboard, you never get the keyboard back! \ ============================================================ : HELP ( -- ) CR ." Commands:" CR ." HELP" CR ." CRIT" CR ." XCRIT" CR ." GETSEM" CR ." RELSEM" CR ." SUSPEND <thread #>" CR ." RESUME <thread #>" CR ." GET-PRTY <thread #>" CR ." SET-IDLE <thread #>" CR ." SET-REG <thread #>" CR ." STOP-IT" CR ; \ ============================================================ CLS START-IT THREAD1 (STOP) \ leave room for typing in commands PAUSE CLS HELP Figure 4: Code Listing for xy.4th \ xy.4th for UR/Forth \ get current cursor location, by programming the 6845 CRTC 80 CONSTANT COLUMNS HEX : CUR-LOC ( -- reg14 reg15 ) E 3D4 PC! 3D5 PC@ F 3D4 PC! 3D5 PC@ ; : MK-WORD ( w1 w2 -- w ) SWAP FF * + ; : COL-ROW ( w -- x y ) DUP COLUMNS MOD 1+ SWAP COLUMNS / 1+ ; : CURS CUR-LOC MK-WORD COL-ROW ; DECIMAL Figure 5: Dynamically Linking to a DLL from OS2XLISP ; chat.lsp ; Run-time dynamic linking to CHATLIB.DLL from OS2XLISP (define chatlib (loadmodule "CHATLIB")) (define login (getprocaddr chatlib "LOGIN")) (define get-msg-cnt (getprocaddr chatlib "GET_MSG_CNT")) (define my-id (call login)) (define msg-cnt (call get-msg-cnt (word my-id))) Figure 6: Code Listing for makecall.lsp ; makecall.lsp ; use LISP symbolic manipulation ; to create OS/2 API functions ; "I would rather write code to write code, than write code" ; Andrew Schulman 2-August-1988 ; filter sets up formal argument list to be passed to (define) (define (filter list &aux (lst (list nil)) elem) (dotimes ; for each element in list (i (length list)) (setf elem (nth i list)) ; if it's a list, like (word freq) (if (listp elem) ; if it's not a retval directive, like 'word (if (not (eq 'QUOTE (car elem))) (nconc lst (list (cadr elem)))) ; else (nconc lst (list elem)))) (cdr lst)) (defmacro make-call (module func &rest args) `(define ,(append (list func) (filter args)) (call ,(getprocaddr (eval module) (if (eq module 'CRTLIB) (strcat "_" (string-downcase (symbol-name func))) (symbol-name func))) ,@args))) ; e.g.: ; (make-call doscalls dosbeep (word freq) (word dur)) ; produces: ; (define (dosbeep freq dur) ; (call <procaddr> (word freq) (word dur))) ; can then be called: ; (dosbeep 880 100) Figure 7: Source Code Listing for segs.lsp ; SEGS.LSP ; for OS2XLISP ; to examine OS/2 memory segments ; Andrew Schulman 2-August 1988 ; requested protection level of seg (defmacro rpl (x) `(logand ,x 3)) ; in LDT or GDT? (defmacro global? (x) `(zerop (logand ,x 4))) ; does LAR represent accessed segment? (defmacro accessed? (x) `(= 1 (logand ,x 1))) ; does LAR represent present segment? (defmacro present? (x) `(= 128 (logand ,x 128))) ; does LAR represent code segment? (defmacro code? (x) `(= 8 (logand ,x 8))) ; does LAR represent call gate? (defmacro call-gate? (x) `(= #xE4 ,x)) ; does LAR represent readable code? (defmacro read? (x) `(and (code? ,x) (= 2 (logand ,x 2)))) ; does LAR represent writable data? (defmacro write? (x) `(and (not (code? ,x)) (= 2 (logand ,x 2)))) (define (enumsegs func) (dotimes ; for each possible segment.... (i #xFFFF) ; if there's really a segment there.... (if (not (zerop (lar i))) ; call the callback function, ; passing it the segment number, ; its access rights (LAR), and ; its size (LSL, remembering to ; add 1 because LSL is zero-based) (apply func (list i (lar i) (1+ (lsl i))))))) ; this is a callback function, passed to (enumsegs) by (print-segs) ; below (define (show-seg seg seglar segsize) (if (< (rpl seg) 3) (format stdout "~A\n" seg) ; only show details for one of the four identical segments (progn (format stdout "~A\t~A\t~A\t" ; print segment/selector number seg ; print whether global (GDT) or local (LDT) (if (global? seg) "GDT" "LDT") ; print what it is: data, code, or call gate (cond ((call-gate? seglar) "CALL GATE") ((code? seglar) (if (read? seglar) "READABLE CODE" "EXECUTE-ONLY CODE")) (t(if (write? seglar) "WRITABLE DATA" "READ-ONLY DATA")))) ; if not a call gate, print its size (if (not (call-gate? seglar)) (format stdout "~A ~A" segsize (if (= 1 segsize) "byte" "bytes"))) ; if segment not present in memory, say so (if (not (present? seglar)) (format stdout " (not present)")) ; print ruler at the bottom (format stdout "\n~A\n" (make-string #\. 70))))) (define (print-segs) (enumsegs #'show-seg)) ; non-transparent popup, invoke func inside popup, ; wait for keystroke (define (popup func) (call (getprocaddr viocalls "VIOPOPUP") ^(word 1) (word 0)) (apply func nil) (read-char) (call (getprocaddr viocalls "VIOENDPOPUP") (word 0))) ; takes optional keyword position-independent parameters, ; e.g., (total-segs :bkgr t) ; ; or (total-segs :bkgr t :pflag nil) (define (total-segs &key (pflag t) (bkgr nil) &aux (numsegs 0) (numbytes 0) (gnum 0) (lnum 0) (gbytes 0) (lbytes 0)) ; if running in background, make idle priority class (if bkgr (call (getprocaddr doscalls "DOSSETPRTY") (word 0) (word 1) (word 0) (word (getpid)))) (enumsegs ; pass enumsegs a "nameless" function, created with (lambda) (lambda (seg seglar segsize) ; only do something for one out of the four identical segments (if (zerop (rpl seg)) (progn (setf numsegs (1+ numsegs) numbytes (+ numbytes segsize)) (if (global? seg) (setf gnum (1+ gnum) gbytes (+ gbytes segsize)) ; else if local (setf lnum (1+ lnum) lbytes (+ lbytes segsize))))))) ; since the operation takes a long time, ; they'll get popup notification when it's done. (if bkgr (popup (lambda () (call (getprocaddr doscalls "DOSSETPRTY") (word 0) (word 2) (word 0) (word (getpid))) (format stdout "OS2XLISP finished computing TOTAL-SEGS!\n\n") (format stdout "SEGS: ~A\tBYTES: ~A\n\n" numsegs numbytes) (format stdout "Press any key to continue\n\n")))) ; either print out the results, or return them as a list (if pflag (not (format stdout "Segs: ~A (GDT ~A, LDT ~A)\tBytes: ~A (GDT ~A, LDT ~A)\n" numsegs gnum lnum numbytes gbytes lbytes)) ; else (list numsegs numbytes gnum gbytes lnum lbytes))) Figure 8: An example of OS2XLISP using (print-segs). > (print-segs) 12 13 14 15 LDT READ-ONLY DATA 16 bytes ................................... 28 29 30 31 LDT READABLE CODE 3503 bytes .................................... ∙ ∙ ∙ Figure 9: Under 286 protected mode, segment numbers are actually structures with a number of different fields, as shown here. Bit Position Field Definition 0,1 Requested protection level (RPL) 2 Table indicator (global/GDT=0 local/LDT=1) 3-15 Index into table Figure 10: Source Code Listing for the Dining Philosphers MODULE Dining; (* The Five Dining Philosophers in Modula-2 adapted from M. Ben-Ari, Principles of Concurrent Programming (Prentice- Hall, 1982), p.113 This module has no operating system dependencies, but depends upon Sem, which has only been implemented for OS/2 *) FROM Processes IMPORT StartProcess; FROM InOut IMPORT WriteString, WriteInt, WriteLn; FROM Sem IMPORT CountingSemaphore, P, V, CSInit, BinarySemaphore, Request, Clear, BinInit, cobegin, coend, self; CONST NUM_PHILOSOPHERS = 5; NUM_FORKS = NUM_PHILOSOPHERS; VAR fork : ARRAY [1..NUM_FORKS] OF BinarySemaphore; room : CountingSemaphore; fini : CountingSemaphore; i : CARDINAL; MODULE io; IMPORT BinarySemaphore, Request, Clear, BinInit, WriteString, WriteInt, WriteLn; EXPORT think, eat; VAR (* iosem has no bearing on problem itself; just for printing strings *) iosem : BinarySemaphore; PROCEDURE think (p : CARDINAL) ; BEGIN Request(iosem); WriteString('thinking: '); WriteInt(p,1); WriteLn; Clear(iosem); END think; PROCEDURE eat (p : CARDINAL) ; BEGIN Request(iosem); WriteString('eating: '); WriteInt(p,1); WriteLn; Clear(iosem); END eat; BEGIN (* initialization for internal module *) BinInit(iosem); END io; PROCEDURE philosopher () ; VAR i : CARDINAL; left, right : CARDINAL; BEGIN (* these don't have to be done each time through loop, since each philosopher/process has its own stack *) i := self() - 1; left := i; right := (i MOD NUM_FORKS) + 1; P(fini); LOOP think(i); P(room); Request(fork[left]); Request(fork[right]); eat(i); Clear(fork[left]); Clear(fork[right]); V(room); END; END philosopher; BEGIN CSInit(room, NUM_FORKS - 1); CSInit(fini, NUM_PHILOSOPHERS); FOR i := 1 TO NUM_FORKS DO BinInit(fork[i]); END; cobegin(); FOR i := 1 TO NUM_PHILOSOPHERS DO StartProcess(philosopher, 2048); END; coend(); P(fini); (* never cleared! *) END Dining. DEFINITION MODULE Sem; TYPE CountingSemaphore; PROCEDURE P ['P'] (VAR cs : CountingSemaphore); PROCEDURE V ['V'] (VAR cs : CountingSemaphore); PROCEDURE CSInit ['CSINIT'] (VAR cs : CountingSemaphore; count : CARDINAL); TYPE BinarySemaphore; PROCEDURE Request ['REQUEST'] (VAR s : BinarySemaphore); PROCEDURE Clear ['CLEAR'] (VAR s : BinarySemaphore); PROCEDURE BinInit ['BININIT'] (VAR s : BinarySemaphore); PROCEDURE cobegin ['COBEGIN'] () ; PROCEDURE coend ['COEND'] () ; PROCEDURE self ['SELF'] () : CARDINAL ; END Sem. IMPLEMENTATION MODULE Sem; (* contains: Counting Semaphores Binary Semaphores assorted thread-related procedures *) FROM SYSTEM IMPORT ADDRESS, ADR; FROM Storage IMPORT ALLOCATE; FROM DosCalls IMPORT DosSemClear, DosSemSet, DosSemRequest, DosSemWait, DosEnterCritSec, DosExitCritSec, DosGetInfoSeg; TYPE CountingSemaphore = POINTER TO RECORD cs : LONGINT; ms : LONGINT; count : CARDINAL; countsem : ADDRESS; mutexsem : ADDRESS; END; (* note: P() and V() adapted from Kevin Ruddell, "Using OS/2 Semaphores to Coordinate Concurrent Threads of Execution," Microsoft Systems Journal, May 1988, Figure 9: "Simulating a Counting Semaphore under OS/2," *) PROCEDURE P ['P'] (VAR cs : CountingSemaphore); VAR blocked : BOOLEAN; BEGIN blocked := TRUE; WHILE blocked DO DosSemWait(cs^.countsem, -1); DosSemRequest(cs^.mutexsem, -1); IF (cs^.count = 0) THEN DosSemSet(cs^.countsem); ELSE DEC(cs^.count); blocked := FALSE; END; DosSemClear(cs^.mutexsem); END; END P; PROCEDURE V ['V'] (VAR cs : CountingSemaphore); BEGIN DosSemRequest(cs^.mutexsem, -1); INC(cs^.count); DosSemClear(cs^.countsem); DosSemClear(cs^.mutexsem); END V; PROCEDURE CSInit ['CSINIT'] (VAR cs : CountingSemaphore; count : CARDINAL); BEGIN NEW(cs); cs^.cs := 0; cs^.ms := 0; cs^.count := count; cs^.countsem := ADR(cs^.cs); cs^.mutexsem := ADR(cs^.ms); END CSInit; TYPE BinarySemaphore = POINTER TO RECORD s : LONGINT; sem : ADDRESS; END; PROCEDURE Request ['REQUEST'] (VAR s : BinarySemaphore); BEGIN DosSemRequest(s^.sem, -1); END Request; PROCEDURE Clear ['CLEAR'] (VAR s : BinarySemaphore); BEGIN DosSemClear(s^.sem); END Clear; PROCEDURE BinInit ['BININIT'] (VAR s : BinarySemaphore); BEGIN NEW(s); s^.s := 0; s^.sem := ADR(s^.s); END BinInit; (* simulate the cobegin-coend construct *) PROCEDURE cobegin ['COBEGIN'] (); BEGIN DosEnterCritSec(); END cobegin; PROCEDURE coend ['COEND'] (); BEGIN DosExitCritSec(); END coend; MODULE Self; IMPORT ADDRESS, DosGetInfoSeg; EXPORT self; VAR A : ADDRESS; LocalInfo : POINTER TO ARRAY [0..6] OF CARDINAL; (* return a thread's ID number *) PROCEDURE self ['SELF'] () : CARDINAL; BEGIN RETURN LocalInfo^[3]; END self; BEGIN (* module initialization code *) DosGetInfoSeg(A.OFFSET, A.SEGMENT); A.OFFSET := 0; LocalInfo := A; END Self; END Sem. ; SEMAPH.DEF LIBRARY SEM INITINSTANCE DESCRIPTION 'Counting and Binary Semaphores' DATA MULTIPLE EXPORTS SEM ; init P V CSINIT REQUEST CLEAR BININIT COBEGIN COEND SELF ; START.ASM -- auto-init entry point EXTRN SEM:FAR DOSSEG .MODEL large .CODE START PROC FAR call SEM mov ax,1 ; return success ret START ENDP END START set m2lib=\os2\mod2\lib m2 sem.def/data:l/thread && m2 sem/data:l/thread m2 dining/thread ' to make DLL version: masm start.asm; link /dosseg start sem junk,sem.dll,,,semaph.def && imblib sem.lib semaph.def copy sem.dll \os2\dll 'copy to LIBPATH link dining,dining,,sem; ' to make non-DLL version: link dining sem,dining; Figure 11: Creating a BASIC Utility declare sub DosGetInfoSeg (seg arg1%, seg arg2%) DosGetInfoSeg gis%, lis% def seg = gis% print "The time is"; peek(8); ":" peek(9); ":" peek(10) The OS/2 command-line to compile, link, and run INFOSEG.BAS is: C>bc infoseg.bas; && link infoseg; && infoseg Figure 12: ENUMPROC.BAS, a basic program that prints out a list of all functions exported from a DLL. ' enumproc.bas ' bc /e enumproc; && link enumproc; && enumproc brun60ep if command$ = "" then dll$ = "BRUN60EP.DLL" elseif instr(command$, ".") then dll$ = command$ else dll$ = command$ + ".DLL" endif pipe = freefile ' makes it easy to detect end of pipe's output on error goto closefile foundit = 0 open "pipe:exehdr " + dll$ + " 2>nul" as pipe foundfile = 1 ' found EXEHDR ' get rid of initial EXEHDR output do while instr(buf$, "Exports:") = 0 line input #pipe, buf$ loop line input #pipe, buf$ foundfile = 2 ' found named DLL print "Routines exported by "; dll$ do while 1 line input #pipe, buf$ buf$ = mid$(buf$,16) procname$ = left$(buf$, instr(buf$, " ") - 1) print procname$ loop closefile: if foundfile = 0 then print "Can't find EXEHDR" elseif foundfile = 1 then print "Can't find "; dll$ endif close pipe system Figure 13: Code Listing for PEEKER.BAS ' peeker.bas ' compile with bc peeker.bas /d/e/x; && link peeker; on error goto errorhandler do while 1 input; "seg"; sgm% print chr$(9); input; "offset"; ofs% print chr$(9); def seg = sgm% print peek(ofs%) loop errorhandler: if err = 70 then print "Permission denied" resume next else print "Unknown error #"; err system endif Figure 14: Code Listing for SMSW.BAS ' smsw.bas ' compile with bc smsw.bas; && link smsw; declare sub DosCreateCSAlias (byval p1%, seg p2%) code$ = _ chr$(&hc8) + chr$(0) + _ chr$(0) + chr$(0) + _ ' enter 0,0 chr$(&h8b) + chr$(&h5e) + _ chr$(&h06) + _ ' mov bx,word ptr [bp+6] chr$(&h0f) + chr$(&h01) + _ chr$(&h27) + _ ' smsw word ptr [bx] chr$(&hc9) + _ ' leave chr$(&hca) + chr$(&h02) + chr$(&h00) ' retf 2 DosCreateCSAlias varseg(code$), csalias% def seg = csalias% call absolute(x%, sadd(code$)) if x% and 2 then print "Coprocessor present" elseif x% and 4 then print "No coprocessor -- emulate" else print "Something is very wrong" endif Figure 15: Source Code for SMSW2.BAS and PROTMODE.ASM ' smsw2.bas ' ' To compile and link (all on the same line): ' bc smsw2.bas; && masm protmode.asm; && link smsw2 ' protmode, smsw2; declare sub SMSW alias "_SMSW" (x%) smsw x% if x% and 2 then print "Coprocessor present" elseif x% and 4 then print "Using emulator" else print "Something is wrong!" endif ; protmode.asm ; requires MASM 5.1 .model medium,basic .286p .code _smsw proc far arg1:near ptr word mov bx,arg1 smsw word ptr [bx] ret _smsw endp _lsl proc far segm:word sub dx,dx sub ax,ax lsl ax,segm ret _lsl endp end Figure 16: Language Comparison Chart │ │ │Stony Brook│Microsoft│ │UR/FORTH │OS2XLISP│ Modula-2 │BASIC 6.0│ C ────────────┼─────────┼────────┼───────────┼─────────┼──────── interactive │ x │ x │ │ │ ────────────┼─────────┼────────┼───────────┼─────────┼──────── multitasking│ x │ │ x │ │ x ────────────┼─────────┼────────┼───────────┼─────────┼──────── graphics │ x │ │ │ x │ ────────────┼─────────┼────────┼───────────┼─────────┼──────── GP fault │ │ │ │ │ handling │ │ x │ │ x │ ────────────┼─────────┼────────┼───────────┼─────────┼──────── make DLL │ │ │ x │ │ x ────────────┼─────────┼────────┼───────────┼─────────┼──────── run-time │ │ │ │ │ error chk │ │ │ x │ x │ ────────────┼─────────┼────────┼───────────┼─────────┼──────── debugging │roll your│built-in│M2DEBUG-EXE│ CVP │ CVP │ own │ │ │ │ ────────────┼─────────┼────────┼───────────┼─────────┼──────── link with C │ │ x │ x │ x │ ████████████████████████████████████████████████████████████████████████████ The High Memory Area: Addressing 64Kb More Memory in Real Mode Chip Anderson Everyone knows that the greatest possible amount of memory that the Intel(R) 8086 and 8088 microprocessors can address is 1Mb. It is also commonly believed that in real mode the Intel 80286 and 80386 processors can only address the same 1Mb, but this is not true. A careful study of the architecture of the 80286 and 80386 processors demonstrates that it is possible to address almost 64Kb more memory. With a bit of extra effort, programs running under DOS can take advantage of this extra space. This article will present the principles needed to gain access to this extra memory, and an analysis of the problems involved. High Memory Area The extra memory space is called the High Memory Area (HMA). Its maximum size is 64Kb minus 16 bytes, and in order to access it, one needs to enable the 80286's and the 80386's A20 address line while in real mode. The 8086 and 8088 processors each have 20 address lines, A0──A19, which produces an addressable range of 220 bytes, or 1Mb. For this discussion, it is extremely important to remember the difference between a "memory location" and a "memory address." Memory locations are numbered sequentially starting at 0 and progress up to the top of addressable memory. There is a natural one-to-one correspondence between memory locations and actual physical memory. Memory addresses are just a different way of referring to that same physical memory. They consist of two parts, a 16-bit segment and a 16-bit offset. In real mode a new segment starts every 10H bytes in memory. For example, segment 1234H starts 1234H 5 10H or 12340H bytes from the start of physical memory. If an offset is given, it is simply added to the start of the segment. Consequently memory address 1234:5678H refers to memory location 12340H + 5678H, or 179B8H (see Figure 1). The 80286/386 processors have more address lines and can thus access more memory when running in protected mode. However, when in real mode, the 21st address line of these processors, the A20 line, is disabled, thus limiting the available address space to the same 1Mb as the 8086/88. Anytime the processors attempt to access memory beyond 1Mb they "wrap" back to the beginning of system memory. When such machines as the IBM(R) PC/AT(R) and the COMPAQ(R) 386(TM) are in real mode, you can selectively enable and disable the A20 line. When the A20 line is enabled, attempts to access memory beyond 1Mb do not cause a wrap back to the beginning of system memory. Instead it becomes possible to go beyond the 1Mb boundary into extended memory. Consider the following example. Using the standard Intel real-mode addressing convention, the address FFFF:0FH refers to memory location FFFFFH, which is the byte just before the 1Mb boundary (see Figure 2A). Notice that this is the highest location that can be reached with a 20- line, or 20-bit, addressing scheme. The address FFFF:10H should, logically, refer to location 100000H, which is the byte at the 1Mb boundary. But since we can only look at 20 bits worth of information, the leading 1, which is located at the 21st bit position (or the 21st line, A20), is ignored. Therefore, address FFFF:10H leaves us with a value of 0H (see Figure 2B and Figure 3), in effect a wrap to the beginning of system memory. When the A20 line is enabled, there isn't a wrap because the required 21st bit is no longer masked off. This means that location FFFF:10H actually refers to 100000H, or the first byte of extended memory (see Figure 2C). The Intel real-mode addressing convention limits the maximum possible address to FFFF:FFFFH, so we end up with a real location of 10FFEFH, which is 64Kb minus 16 bytes above the 1Mb boundary (see Figure 2D). This extra addressable memory is called High Memory Area (HMA). Unique HMA Qualities Several aspects of the HMA distinguish it from normal memory segments. Whereas normal memory segments are always available, the HMA can only be accessed while the A20 line is enabled. Another difference is that the HMA does not start at the beginning of a segment but 16 bytes after the beginning of segment FFFFH. The most important difference is that the HMA cannot be subdivided. Because no segment starts inside the HMA-remember it resides entirely inside the highest valid segment, FFFFH-the HMA cannot be split up into smaller chunks and therefore cannot be used by more than one program at a time. In theory, using the HMA should be as simple as moving some code into the first part of extended memory, enabling the A20 line, and jumping to some address above FFFF:10H. However, its unique qualities impose several restrictions on its use. HMA Problems Let's look closely at some of these HMA issues. Managing the machine's A20 line. The procedure for enabling and disabling the A20 line differs from machine to machine; some machines may not even have an A20 line. Determining which machine you are running on and how to enable and disable its A20 line reliably is a serious obstacle. A consistent, machine-independent facility for managing this resource is needed. Reserving the HMA. The HMA can be considered a "grabbable" resource. Because the HMA can only reside in one segment it cannot be shared among several programs at the same time. While DOS is not a multitasking system, there may very well be TSRs and smart device drivers that could cause conflicts for resources such as the HMA. Clearly, a scheme is required for reserving and releasing the HMA. Preventing the HMA from being accidentally overwritten. Some TSRs and device drivers, for example, IBM's memory disk program, VDISK, simply grab the lower part of extended memory (including the HMA) and store their data there. If one of these drivers is installed, the HMA cannot be used. Other drivers, such as older versions of RAMDrive, Microsoft's memory disk program, allocate extended memory from the top of extended memory down. As long as these programs leave 64Kb free, the HMA will be available, but once the HMA is in use, these programs must be prevented from overwriting it. Detecting the existence of the HMA. Obviously at least 64Kb of extended memory must be present in order to provide HMA support; this condition must therefore be checked. A check must also be made to ensure that the A20 line can be reliably enabled and disabled. Prioritizing HMA usage. If a TSR or DOS device driver moves itself into the HMA, the space that it occupied below the 640Kb line becomes available for use by all other programs. Now consider the case where two TSRs that could use the HMA are installed. Remember that only one program can use the HMA at any one time. Let's say that the first TSR can move 50Kb of itself into the HMA while the second can move 62Kb of its code up (see Figure 4). Obviously there would be an extra 12Kb of available memory below the 640Kb line if the second TSR was moved into the HMA instead of the first. Some facility is needed to help users get the maximum benefit from the HMA in such situations. The unique qualities of the HMA present some formidable problems for the DOS programmer. When Microsoft first tried to take advantage of this memory, it became obvious that a standard method for accessing the HMA had to be developed before it could be widely used. XMS to the Rescue In July 1988, Microsoft, Intel, Lotus, and several other leading software firms published the DOS Extended Memory Specification (XMS) Version 2.0. XMS helps DOS programmers use all parts of extended memory effectively, including the HMA. An XMS driver solves two of the toughest HMA usage problems by providing a machine-independent method of switching the A20 line and by preventing two programs from using the HMA at the same time. In addition to HMA support, XMS allows programs to store large amounts of data in extended memory. However, the rest of this article will only discuss the XMS functions that permit access to the HMA. Programmers can obtain complete XMS documentation along with Microsoft's own XMS driver, HIMEM.SYS, at no charge by contacting Microsoft's Information Center at (800) 426-9400. Utilizing the HMA In order for DOS programs to use the HMA, they need to do the following: ■ determine if an XMS driver is installed ■ get the address of the driver's control function ■ request the HMA If the XMS driver grants access to the HMA, the program can then: ■ enable the A20 line ■ copy up to 64Kb of code into the HMA ■ execute ■ disable the A20 line ■ release the HMA ■ terminate Interrupt 2FH, the multiplex interrupt, is used both for determining if an XMS driver is installed and for obtaining the address of the driver's control function. Executing Interrupt 2FH with AX=4300H returns a value of 80H in AL if an XMS driver is present (see Figure 5). Executing an Interrupt 2FH instruction with AX=4310H returns the address of the XMS driver's control function in ES:BX (see Figure 6). Programs can use XMS functions by setting AH to one of 18 XMS function numbers and then calling the control function (see Figure 7). Only functions 0 through 4 are needed to access the HMA. Figure 8 lists the 18 XMS functions. DOS application programs can request access to the HMA by calling the control function with AH=01H (Request HMA) and DX=FFFFH (TSR programs must set DX differently, as you'll see later). If AX is set to 1 after the call, the program has been given access to the HMA and can then use XMS function 3 (Global Enable A20) to turn on the machine's A20 line. Once the A20 line has been successfully enabled, the program can use the extra memory in the HMA anyway it wants. Typically it will move as much of its resident code and data as possible, thereby freeing space below the 640Kb line for dynamic code and data storage. When the program is about to terminate, it must use XMS function 4 (Global Disable A20) to turn off the A20 line. Several older DOS programs rely on the memory wrap, and will crash the system if they are run when the A20 line is enabled. Before terminating, the program must also release the HMA using XMS function 2 (Release HMA). Prioritizing HMA Usage Revisited Now let's return to the problem of prioritizing HMA usage when several TSRs that use HMA are installed. XMS drivers are installed by including a line similar to DEVICE=HIMEM.SYS in the machine's CONFIG.SYS file. That line can have an optional parameter on it (/HMAMIN=) that specifies the least amount of space a TSR must use before it is given access to the HMA. In addition, before a TSR calls XMS function 1 (Request HMA), it must set DX equal to the amount of space in the HMA it would use (in bytes). The XMS driver compares this number with the amount that is specified by the /HMAMIN= parameter and only grants the TSR access if it requests more memory than the parameter specifies. For example, if DEVICE=HIMEM.SYS /HMAMIN=55 was included in CONFIG.SYS, only programs requesting 55Kb or more would be allowed access to the HMA. Notice that this problem does not apply to DOS applications that use the HMA, but are not TSRs, since they release the HMA before terminating. Because these programs are not concerned with HMA priority, they should set DX=FFFFH before calling XMS function 1 (Request HMA), thus bypassing the /HMAMIN= check. Since some programs do rely on memory wrap, TSRs that use HMA must disable the A20 line before returning control to the system. This implies that these TSRs must have a small routine located below the 640Kb line that enables the A20 line and then jumps to the code in the HMA when the TSR is activated. When the TSR finishes, it must jump to another small routine below the 640Kb line that disables the A20 line and then returns control to DOS. HIMEM.SYS Microsoft's own XMS driver, HIMEM.SYS, is included with the XMS Developer's Kit. HIMEM.SYS Version 2.04 is a full XMS driver that offers A20 support for a wide variety of DOS machines, including the IBM PC and PS/2(R) families and compatibles. An early version of HIMEM.SYS (Version 1.1) is also being distributed with Microsoft(R) Windows/286 and Microsoft Windows/386 Version 2.1. It only ysupports the first nine of XMS's 18 functions, but these include all of the functions needed for accessing the HMA. The XMS API specification allows the resourceful programmer to gain an extra 64Kb of memory on many of the machines currently in use without requiring special hardware. Properly used, the HMA can significantly improve speed and increase the amount of data programs can process. Figure 1: Memory Locations vs. Memory addresses. ╔══════════════╗ ╔══════════════╗ ║ FFFFFh ║ ║ FFFF:000Fh ║ ╠══════════════╣ ╠══════════════╣ ║ FFFFEh ║ ║ FFFF:000Eh ║ ╠══════════════╣ ╠══════════════╣ ║ FFFFDh ║ ║ FFFF:000Dh ║ ╠══════════════╣ ╠══════════════╣ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ┌──────┐ ║ ∙ ║ ║ ∙ ║ │12340H│ ║ ∙ ║ ╠══════════════╣ │+5678H│ ╠══════════════╣ ║ 179B8h ║◄═════╡──────╞═══════║ 1234:5678h ║ ╠══════════════╣ │179B8H│ ╠══════════════╣ ║ ∙ ║ └──────┘ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ║ ∙ ║ ╠══════════════╣ ╠══════════════╣ ║ 00002h ║ ║ 0000:0002 ║ ╠══════════════╣ ╠══════════════╣ ║ 00001h ║ Bottom ║ 0000:0001h ║ ╠══════════════╣ of ╠══════════════╣ ║ 00000h ║ Memory ║ 0000:0000h ║ ╚══════════════╝ ╚══════════════╝ Memory Locations Memory Addresses Figure 2: Intel Addressing Convention A) FFFF:000FH = (FFFFH*10H)+000FH = FFFF0H+0000FH = FFFFFH = 11111111111111111111B [with 20 bits] B) FFFF:0010H = (FFFFH*10H)+0010H = FFFF0H+00010H = 100000H = 00000000000000000000B [with 20 bits] (Results in wrap back to memory location 0000:0000H) C) FFFF:0010H = 100000000000000000000B [with 21 bits] (Results in extending access beyond the 1Mb boundary) D) FFFF:FFFFH = (FFFFH*10H)+FFFFH = FFFF0H+0FFFFH = 10FFEFH Figure 3: Memory Wrap ┌──────────────┐─┐ │ FFFF:FFFFh │ │ ├──────────────┤ │ │ ∙ │ │ │ ∙ │ │ │ ∙ │ ├──────────┐ ├──────────────┤ │ │ │ FFF:0011h │ │ ┌────────┴────────┐ ├──────────────┤ │ │Noramlly, memory │ │ FFFF:0010h │ │ │locations are │ 1Mb┌──────────────┐ ├──────────────┤─┘ │limited to 20 │ │ FFFFFh │ │ FFFF:000Fh │ │bits causing │ ├──────────────┤ ├──────────────┤ │them to wrap │ │ FFFFEh │ │ FFFF:000Eh │ │back to the │ ├──────────────┤ ├──────────────┤ │bottom of memory.│ │ FFFFDh │ │ FFFF:000Dh │ └────────┬────────┘ ├──────────────┤ ├──────────────┤ │ │ ∙ │ │ ∙ │ │ │ ∙ │ │ ∙ │ │ │ ∙ │ │ ∙ │ │ ├──────────────┤ ├──────────────┤ ┌──────▼───────┐ │ 0FFEFh │ │ 0000:FFEFh │◄───►│ FFFF:FFFh │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ ∙ │ │ ∙ │ │ ∙ │ │ ∙ │ │ ∙ │◄───►│ ∙ │ │ ∙ │ │ ∙ │ │ ∙ │ ├──────────────┤ ├──────────────┤ ├──────────────┤ │ 00001h │ │ 0000:0001h │◄───►│ FFFF:0011h │ ├──────────────┤ ├──────────────┤ ├──────────────┤ Bottom of│ 00000h │ │ 0000:0000h │◄───►│ FFFF:0010h │ Memory └──────────────┘ └──────────────┘ └──────────────┘ Memory Locations Memory Addresses "Wrapped" Memory Address Figure 4: Optimizing HMA Usage by TSRs ┌────────────────┐ ┌────────────────┐ │ 14 Kb Unusable │ │ │ HMA├────────────────┤ HMA├────────────────┤ │ 50Kb TSR │ │ 62Kb TSR │ └────────────────┘ └────────────────┘ 640Kb╔═══════════════╗ 640Kb╔════════════════╗ 640Kb╔════════════════╗ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ 468Kb Free ║ ║ 518Kb Free ║ ║ 530Kb Free ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ ║ 172Kb╟───────────────╢ ║ ║ ║ ║ ║ 62Kb TSR ║ ║ ║ ║ ║ 110Kb╟───────────────╢ ║ ║ ║ ║ ║ 50Kb TSR ║ 122Kb╟────────────────╢ 110Kb╟────────────────╢ 60Kb╟───────────────╢ ║ 62Kb TSR ║ ║ 50Kb TSR ║ ║ ║ 60Kb╟────────────────╢ 60Kb╟────────────────╢ ║ DOS ║ ║ ║ ║ ║ ║ ║ ║ DOS ║ ║ DOS ║ 0Kb╚═══════════════╝ 0Kb╚════════════════╝ 0Kb╚════════════════╝ No HMA Moving 1st TSR up Moving 2nd TSR up Figure 5: Determining if an XMS Driver is Installed ; Is an XMS driver installed? mov ax,4300h int 2Fh cmp al,80h jne NoXMSDriver Figure 6: Finding the XMS Driver's Control Function mov ax,4310h int 2Fh mov word ptr [XMSControl],bx ; XMSControl is a DWORD mov word ptr [XMSControl+2],es Figure 7: Calling an XMS Function ; Get the XMS driver's version number mov ah,00h call [XMSControl] ; Get XMS version number Figure 8: XMS API Functions Function Description 00H Get XMS Version Number 01H Request High Memory Area 02H Release High Memory Area 03H Global Enable A20 04H Global Disable A20 05H Local Enable A20 06H Local Disable A20 07H Query A20 08H Query Free Extended Memory 09H Allocate Extended Memory Block 0AH Free Extended Memory Block 0BH Move Extended Memory Block 0CH Lock Extended Memory Block 0DH Unlock Extended Memory Block 0EH Get Handle Information 0FH Reallocate Extended Memory Block 10H Request Upper Memory Block 11H Release Upper Memory Block ████████████████████████████████████████████████████████████████████████████ Developing and Debugging Embedded Systems Applications Y. P. Chien, Ph.D☼ Operating systems are necessary for most general computing environments──from the largest mainframe to the smallest PC──but one environment which does not require an operating system is that of embedded system applications. Typically an embedded system is a microprocessor-based computer system that performs dedicated control tasks. Airborne flight controllers and satellite instrumentations are exotic examples of embedded systems; a less exotic application is the cruise control on a car. Traditionally assembly language has been the language of choice for developing embedded system applications, as it is closest to the instruction set of the microprocessor. Assembly language offers execution speed and microprocessor-level access, such as memory and I/O port addressing and bit manipulation. It is not, however, a natural language for expressing algorithms. As microprocessor applications become more complex and demanding, there is a growing need for more efficient software development tools. Recently programmers have turned to C for developing applications that run on embedded systems. C offers a large set of operators for microprocessor-level access, so C programs can produce very efficient machine code. Furthermore, C has the traditional high-level structured programming constructs-iteration, selection, and decision-including data structuring capabilities. These features make it easier to write and document C programs. Embedded systems are characterized by the need to store the application programs in read-only memory (ROM) chips, so that when system power is turned on, the application programs can immediately take control. They are usually standalone systems having no operating system support. For example, a satellite data logger has no need for a sophisticated file system, nor does it need to provide a fancy user interface or shell. The primary task of a satellite data logger is to gather data through its sensors, process the data, and transmit it back to a tracking station on earth. Embedded system software must be able to handle the asynchronous software/hardware interactions typical of real-time control applications. For example, an automatic braking system may use interrupts to signal the controller when the braking wheels stop turning. The controller must respond to this asynchronous stimulus as soon as possible, releasing the brakes to avoid an uncontrolled skid. This article addresses issues related to the development of embedded system applications using the Microsoft(R) C optimizing compiler. The embedded systems we'll look at are based on the 80286/386 family of microprocessors. We will first compare the special needs of embedded system applications versus MS-DOS(R) applications, then describe how to generate C programs for embedded systems. Finally, we will tackle the topic of debugging software/hardware interactions on target software. Code Generation An executable file for the MS-DOS run-time environment does not contain absolute addresses. The MS-DOS linker uses a load-time relocatable format to organize the executable file. That is, the linker does not bind the program (both code and data) with fixed addresses. The MS-DOS loader determines the absolute location when it loads the program from disk to memory for execution. An embedded system application, on the other hand, has fixed addresses. When designing the target hardware, specific memory locations for ROM and RAM chips must be reserved. During development of the application program, program code is assigned to ROM and program data to RAM. Usually ROM chips are placed in high memory close to the end of the 1Mb address range because the physical addresses starting at FFFF0H contain the bootstrap code. This section of code normally consists of an 8086 machine instruction, which performs a far jump to the entry point of the application program. Upon processor reset, the microprocessor will execute the bootstrap code to pass execution control to the application program. Figure 1 compares a typical memory configuration of an embedded system to the standard MS-DOS memory configuration. Initialized Variables In C a program begins execution by calling its main function. The program assumes that all initialized extern (global) and static (local) variables contain initial values. All other uninitialized variables contain the value zero. In the MS-DOS environment, when the MS-DOS loader loads a C program from disk to memory, it also loads all the initialized variables with proper values, then passes control to the program. Before the C program starts executing, the loader passes control to a start-up routine. The start-up routine sets up the run-time environment. During start-up, the routine sets all the uninitialized variables to zero, and then passes control to the main function. In the embedded environment the program code and initializers are stored in ROM space. Upon processor reset, a start-up routine initializes the hardware and sets up the run-time environment for the C program. During initialization, the start-up routine copies the initializers stored in ROM to the program variables located in RAM, sets the uninitialized variables to zero, then passes program control to the main function. Since hardware must be manipulated directly, start-up routines should always be written in assembly language. Figure 2 shows a sample start-up routine for embedded system applications. C Run Time Under MS-DOS, the Microsoft C run-time library contains a set of functions for use in C programs, including functions that interface to the MS-DOS operating system, as well as many other common functions. Generally, these functions fall into two categories: MS-DOS dependent and MS-DOS independent. The dependent functions make system calls to MS-DOS, such as opening and closing files. The independent functions, such as the string manipulation functions, do not invoke MS-DOS calls. Since embedded systems do not for the most part contain MS-DOS, only the independent functions can be used in embedded applications without modification. Figure 3 lists examples of the DOS dependent and DOS independent functions in the C run-time library. Loading/Testing In MS-DOS, you can test C programs with a resident debugger such as Microsoft CodeView(R). Program development facilities are usually not available in the embedded environment. Therefore, you have to develop C programs on a host computer, such as an IBM(R) PS/2(R), PC/AT(R), or compatible. You can then use an in-circuit emulator to debug the program on target hardware. An in-circuit emulator, which consists of both hardware and software, is an ideal debugging tool for integrating application software with the target hardware. The hardware includes a probe that plugs into the microprocessor chip socket and duplicates the behavior of the target microprocessor in real time, either full speed or in single-step execution. The software provides an interface that lets users control the interactive debugging sessions. So an in-circuit emulator system provides a controlled environment for exercising the hardware design and monitoring the results of software running on the actual hardware. Other supplementary debugging tools, such as resident debuggers, simulators, and monitor debuggers, are available. C Programs for Embedded Systems Due to the special requirements of an embedded system, we need an absolute linker that can satisfy these requirements. An example of such a tool is Systems & Software, Inc.'s Link&Locate++ Firmware Generation Package, which supports the Microsoft C Optimizing Compiler 5.1 and Microsoft Macro Assembler (MASM) 5.1. We will discuss how such a tool can be used to build C programs for embedded systems. Figure 4 shows a functional diagram of the Link&Locate++ package. The package contains a linker, locator, object formatter, hex formatter, and other support utilities. Combining Object Files Usually a C program consists of a collection of functions. The C compiler and macro assembler compile and assemble the source files into object files, then the special linker is used to combine all the object files and produce a relocatable object file and a map file. The map file is a text file which lists the names of all the object modules in the relocatable object file. The standard MS-DOS LINK linker cannot be used because it only produces an executable (EXE) file for the MS-DOS run-time environment. During the link process, the linker resolves external references with public declarations and combines program segments that have the same attributes. The linker also supports incremental link──that is, external references need not be resolved in a single link. The linker can resolve them incrementally in multiple links. Absolute Addresses The relocatable object file generated by the linker does not contain absolute address information. A special locator assigns absolute addresses to the program code and data to generate an absolute object file. The 8086 family of microprocessors has a segmented architecture. The microprocessor contains four segment registers: CS, DS, SS, and ES. Each segment register points to the base of a physical segment, which is a 64Kb section of memory locations. Memory locations within a physical segment may be referenced by an offset with respect to the segment's base, which is contained in each respective segment register. The Microsoft C optimizing compiler translates source files into 8086 machine code, and places the code in multiple logical segments, pieces of code 64Kb in size at most. For example, the C compiler places program code in a logical segment called _TEXT, and certain program data in a logical segment called _DATA. For embedded system applications, program code is stored in ROM and program data is placed in RAM. Both ROM and RAM occupy fixed memory locations, so the logical segments of a C program must be mapped into physical segments. A locator assigns absolute addresses to the logical segments. The locator will allow placement of the logical segments of a C program anywhere in the 1Mb address space of the 8086 family of microprocessors. The locator will process the relocatable object file to produce an absolute object file and usually a map file as well. The map file contains a summary of segment and symbol addresses. The absolute object file generated by the locator is in standard Intel(R) OMF format. If the input relocatable object file contains CodeView debug information, further processing of the absolute object file to convert CodeView debug information into Intel OMF debug information will be required. Finally, you can now load the absolute object file into an in-circuit emulator, such as the Intel I²ICE emulator, which lets you test the program on target hardware in real time. Meanwhile, to store the program in ROM, you must convert the absolute object file into a format for programming ROMs, the most widely supported of which is Intel Hex (without Extended Address Record). A hex formatter will be required to generate Intel Hex files from the absolute object file for ROM programming. Debugging The in-circuit emulator is useful for debugging real-time C programs. Since we are using C to develop the embedded system applications, we will want to debug programs at the C source level as well. Remember that these applications are typically I/O-intensive-that is, the microprocessor must frequently interact with external devices. Therefore, the application programs should be debugged directly on the actual target hardware. In particular, it is important to be able to catch problems related to real time I/O interaction, such as the real-time response to nested interrupts. It is desirable to generate absolute object files in Intel OMF, as the resulting file will then be fully compatible with the Intel I²ICE emulator. This emulator provides a window-oriented user interface similar to CodeView called ICEVIEW, which supports C source level and symbolic debugging on the target system. Figure 5 shows the programming environment for developing embedded system applications in Microsoft C. The Intel I²ICE emulator can also isolate problems with real-time I/O interaction. For example, let us say a hardware interrupt signals the availability of an input byte. Figure 6 shows the C source code for an interrupt handling routine, which gets the input byte and places it in the ring buffer, and for an output routine, which reads data from the ring buffer one byte at a time. ICEVIEW ICEVIEW is the user interface that makes it easy to use the emulator. Its pull-down menus with context-sensitive help assist you in setting up and controlling the emulation environment; its multiple windows let you look at program source, execution trace, register contents, and other debug information all at the same time. ICEVIEW provides a full source-level debug capability. A source window displays the program listing of the current execution module, and lets you single-step through the program one source line at a time. Up to ten watch symbols are available to display current values of symbols. You can watch data change at execution breakpoints or during single-step execution. Hardware Breakpoints The I²ICE emulator delivers full speed, real-time microprocessor emulation at speeds up to 10MHz. Probes for the 8086, 8088, 80186, 80188, and 80286 are available, and the emulator also supports the 8087 and 80287 coprocessors. The I²ICE emulator lets you specify conditions for the emulator to halt program execution (breakpoints). You can set breakpoints based on instruction execution as well as microprocessor bus activities. The bus activities include fetches, as well as reads and writes on the address lines, data lines, status lines, and the emulation clip pod's input lines. To see examples, break during a read or write to a specific address, a data or I/O port, or on a combination of events. One method of defining a breakpoint condition is to use one or more debug registers. These are user-named software registers containing user-defined debug procedures. There are five types: arm, break, system, trace, and event. The first, the arm register, sets conditional breakpoints that allow breaking within arm windows. The break register defines breakpoints on the address of executable statements, and the system register defines them on operand access, operand data, logic clips, system breaking, and coprocessor cycles. The trace register sets conditions to selectively collect program execution history; and last the event register controls the event machine of the I²ICE emulator. The debug register feature lets you specify very complex breakpoint conditions. As an example, we set up an event register to detect an input- overrun error condition (see Figure 7☼). For any array element in our ring buffer example, a read must follow a write before another write can take place. Otherwise, an input-overrun error has occurred and we want to halt program execution. The I²ICE emulator contains a trace buffer which automatically collects trace data during emulation. Specifying a trace window sets the start and stop conditions for trace data collection. You can define trace specifications either directly or through a trace debug register, and display trace data in either instructions mode or cycles mode. These modes display the trace buffer in disassembled mnemonics and bus cycles, respectively; the trace information is important in determining the cause of errors (see Figure 8☼). You can also program the emulator to collect trace information within an arm window, which is a period of time delimited by a begin condition and an end condition. A begin condition is defined by an arm specification, an end condition by a disarm specification. For example, if your program crashes after writing to a specific output port, you may open an arm window to start collecting trace data when a write to that port occurs. Close the arm window by defining a disarm specification; in this example, the disarm specification may be 100 bus cycles after the arm condition becomes true. In this way, you can examine the collected trace data and find out what causes the program to crash. Performance Analyzer The I²ICE emulator interfaces with the Intel Performance Analysis Tool (iPAT) for examining software execution speeds and code coverage in real time. With the iPAT, you can gather timing-and-count data on code execution, code interaction, execution flow, interrupts, and interrupt latency, as well as display the data in either tables or histograms. With these capabilities, you can easily locate performance problems such as software bottlenecks, unused code, and worst case interrupt latency. Software bottlenecks occur when code is heavily used but relatively slow; where performance can be enhanced by improving the algorithms or rewriting critical routines in assembly language. Worst case interrupt latency is the delay between origination of an interrupt and the target systems' response to the interrupt. Worst case interrupt latency information is important in order to be sure that system performance is within given specifications. We have discussed the special requirements for developing and debugging embedded system applications. A typical development environment includes the Microsoft C Optimizing compiler Version 5.0 or later; Microsoft MASM Version 5.0 or later; a tool such as the SSI Link&Locate++ firmware development package; and the Intel I²ICE emulator, which gives you source level debugging capability to test your C programs in target hardware. This programming environment effectively addresses the issues of creating ROMable code generation using the Microsoft C compiler, software integration, and software/hardware debugging. The only thing missing is a good idea for an embedded system, which we leave to the reader as an assignment. [Tracy Kidder's The Soul of a New Machine, Little, Brown and Company, Inc., 1981, contains several excellent chapters, that detail the trials and tribulations of real-time hardware and software debugging. Readers interested in this topic should make the effort to read it.──Ed.] Figure 1: A comparison of the memory configurations of embedded and MS-DOS environments. Memory Map of the MS-DOS Typical Memory Configuration Environment of an Embedded System ┌─┌─────────────┐◄FFFF:000FH ┌─┌─────────────┐◄FFFF:000FH │ │ │ (1 MByte) │ │ Bootstrap │ (1 MByte) ROM │ ROM BIOS │ │ ├─────────────┤◄FFFF:0000H │ │ │ ROM │ Initializer │ └─├─────────────┤◄F000:0000H │ │ │ │ Other ROM │ │ │ and Code │ ≈ ≈ └─├─────────────┤◄E000:0000H │ and RAM │ │ Memory │ (Assume 128K ┌─├─────────────┤◄Top of RAM ≈ Mapped ≈ ROM) │ │ COMMAND.COM │ (A000:0000H │ I/O │ │ │ (transient) │ for IBM PC) ┌─├─────────────┤ │ ├─────────────┤ │ │ Initialized │ │ │ Free │ │ │ Data │ │ ≈ ≈ │ ├─────────────┤ │ │ RAM │ │ │Uninitialized│ │ ├─────────────┤ RAM │ Data │ │ │ Application │ │ ├─────────────┤◄0000:0200H │ │ Program │ │ │ Interrupt │ │ ├─────────────┤◄Location of │ │ Vectors │ │ │ COMMAND.COM │ application └─└─────────────┘◄0000:0000H │ │ (resident) │ program is │ ├─────────────┤ determined │ │ MS-DOS │ at load time │ │ Operating │ │ │ System and │ │ │ Installable │ │ │ Device │ │ │ Drivers │ │ ├─────────────┤◄0000:0600H │ │ RAM BIOS │ │ │ Tables │ │ ├─────────────┤◄0000:0400H │ │ Interrupt │ │ │ Vectors │ └─└─────────────┘◄0000:0000H Figure 2: Sample Microsoft C Start-Up Routine for Embedded Applications NAME STARTUP_CODE ; THIS IS A SAMPLE START-UP PROGRAM FOR AN EMBEDDED SYSTEM. ; THE MEMORY LAYOUT CONFORMS TO THE NAMING CONVENTION USED ; IN MICROSOFT C, VERSION 4.X AND 5.X. ; THIS ASSEMBLY FILE SHOULD BE ASSEMBLED USING MICROSOFT ; MACRO ASSEMBLER USING THE FOLLOWING COMMAND, ASSUMING THIS ; FILE IS CALLED STARTUP.ASM: ; >MASM STARTUP/MX,STARTUP,STARTUP,NUL ; THE /MX OPTION IS USED TO PRESERVE LOWER-CASE IN PUBLIC ; AND EXTERNAL NAMES. ; ; SETUP OF SEGMENT CLASSES IN MICROSOFT C 5.X PROGRAMS. ; ; HIGH ADDRESS ; - ──────────── ; | |'BOOTSTRAP'| ; | ──────────── ; | : : ; | : : ; R ──────────── - THESE TWO CLASSES ARE TO BE ; O | 'CONST' | ^ COPIED TO THEIR RAM ; M ──────────── | LOCATIONS AT POWER-UP ; | | 'DATA' | v ; | ──────────── <- ROMCODE_END SEGMENT ; | | 'CODE' | ; - ──────────── <- ROMCODE_BEG SEGMENT ; : : ; : : ; - ──────────── <- UDATA_END SEGMENT ; | | 'STACK' | ; | ──────────── ; | | 'BSS' | ; | ──────────── <- IDATA_END & UDATA_BEG ; | | 'CONST' | ^ SEGMENTS ; R ──────────── |- THESE TWO CLASSES ARE TO BE ; A | 'DATA' | v INITIALIZED BY START-UP ; M ──────────── <- ROUTINE AT POWER-UP ; | : : | ; | : : |- IDATA_BEG SEGMENT ; | ──────────── ; | | 'INT_PTR' | ; - ──────────── ; LOW ADDRESS ; ; CHOOSE THE MEMORY MODEL BY SETTING THE APPROPRIATE LABEL ; TO 1. ; SMALL EQU 0 COMPACT EQU 0 MEDIUM EQU 0 LARGE EQU 1 HUGE EQU 0 STACK_SIZE EQU 400H ; RESERVE 1Kb WORD OF STACK OR ANY SIZE YOU WANT __acrtused EQU 1 ; IT IS USED TO SATISFY THE EXTERNAL REFERENCE ; FOUND IN EVERY OBJECT FILE GENERATED ; BY THE MICROSOFT C COMPILER, VER 5.X PUBLIC __acrtused ; THE SYMBOL HAS TO BE IN LOWER-CASE DGROUP GROUP IDATA_BEG,_DATA,CONST,IDATA_END,UDATA_BEG,_BSS,STACK,UDATA_END ; THIS SEGMENT MARKS THE BEGINNING OF ROM CODE ROMCODE_BEG SEGMENT PARA 'ROMCODE_BEG' ROMCODE_BEG ENDS IF SMALL OR COMPACT _TEXT SEGMENT BYTE PUBLIC 'CODE' EXTRN _main:NEAR ;C main() ASSUME CS:_TEXT ENDIF IF MEDIUM OR LARGE OR HUGE EXTRN _main:FAR ;C main() STARTUP_TEXT SEGMENT BYTE PUBLIC 'CODE' ASSUME CS:STARTUP_TEXT ENDIF ASSUME DS:DGROUP, SS:DGROUP PUBLIC START START: START_I2ICE LABEL BYTE ; ADDRESS LABEL FOR INTEL I2ICE CLI CLD ; ************************************************************ ; *** YOUR CODE HERE TO DO HARDWARE INITIALIZATION AND RAM CHECK *** ;************************************************************ PUBLIC INIT_RAM_DATA INIT_RAM_DATA: INIT_RAM_DATA_I2ICE LABEL BYTE ; ADDRESS LABEL FOR INTEL I2ICE ; *** TRANSFER INITIALIZATION DATA FROM ROM TO RAM *** ; COMPUTE THE SIZE OF 'DATA' AND 'CONST' CLASS IN NO. OF PARAGRAPHS: MOV AX,SEG IDATA_END ; FRAME NO. OF IDATA_END SEGMENT ; = END OF 'CONST' CLASS IN RAM MOV BX,SEG IDATA_BEG ; FRAME NO. OF IDATA_BEG SEGMENT ; = START OF 'DATA' CLASS IN RAM SUB AX,BX ; AX = SIZE OF 'DATA' & 'CONST' CLASSES MOV CL,3 ; SET UP TO MULTIPLE BY 8 SHL AX,CL ; COMPUTE NO. OF WORDS TO BE TRANSFERRED MOV CX,AX ; TRANSFER COUNTER JCXZ NO_IDATA_DATA ; OBTAIN THE FRAME NO. OF 'DATA' CLASS PLACED IN ROM MOV AX,SEG ROMCODE_END ; FRAME NO. OF ROMCODE_END SEGMENT ; = BEGINNING OF 'DATA' CLASS IN ROM MOV DS,AX ; SOURCE STRING MOV SI,0 ; OBTAIN THE FRAME NO. OF 'DATA' CLASS LOCATED IN RUN-TIME RAM SPACE MOV AX,SEG IDATA_BEG ; FRAME NO. OF IDATA_BEG SEGMENT ; = BEGINNING OF 'DATA' CLASS IN RAM MOV ES,AX ; DESTINATION STRING MOV DI,0 ; INITIALIZE THE 'DATA' AND 'CONST' CLASSES IN ; RUN-TIME RAM SPACE REP MOVSW ; BEGIN WORD TRANSFER PUBLIC NO_IDATA_DATA NO_IDATA_DATA: NO_IDATA_DATA_I2ICE LABEL BYTE ; ADDRESS LABEL FOR INTEL I2ICE ;*** CLEAR UNINITIALIZED DATA ; COMPUTE THE SIZE OF 'BSS' & 'STACK' CLASSES IN NO. OF PARAGRAPHS: MOV AX,SEG UDATA_END ; FRAME NO. OF UDATA_END SEGMENT ; = END OF 'STACK' CLASS IN RAM MOV BX,SEG UDATA_BEG ; FRAME NO. OF UDATA_BEG SEGMENT ; = START OF 'BSS' CLASS IN RAM SUB AX,BX ; AX = SIZE IN PARAGRAPHS MOV CL,3 ; SET UP TO MULTIPLE BY 8 SHL AX,CL ; COMPUTE NO. OF WORDS TO BE INITIALIZED MOV CX,AX ; SETUP COUNTER JCXZ NO_UDATA MOV ES,BX ; SETUP DESTINATION POINTER MOV DI,0 MOV AX,0 ; INITIALIZE TO 0 REP STOSW PUBLIC NO_UDATA NO_UDATA: NO_UDATA_I2ICE LABEL BYTE ; ADDRESS LABEL FOR INTEL I2ICE ; *** SETUP DATA AND STACK SEGMENT *** MOV AX,DGROUP MOV DS,AX ; SETUP DATA SEGMENT POINTER MOV SP,OFFSET DGROUP:STACK_TOP STI ; ENABLE INTERRUPT NOW JMP _main ; ENTER C main() FUNCTION HLT IF SMALL OR COMPACT _TEXT ENDS ENDIF IF MEDIUM OR LARGE OR HUGE STARTUP_TEXT ENDS ENDIF ; THIS SEGMENT MARKS THE END OF ROM CODE ROMCODE_END SEGMENT PARA 'ROMCODE_END' ROMCODE_END ENDS ; *** THE FOLLOWING SEGMENTS ARE ALL LOCATED IN RAM *** INT_PTR SEGMENT AT 0H ;'INT_PTR' ; INTERRUPT POINTER TABLE, LOCATE AT 0H IN RAM. PUBLIC TYPE_0,TYPE_1,TYPE_2,TYPE_3,TYPE_4,TYPE_32 TYPE_0 DD ? ;DIVIDE-ERROR TYPE_1 DD ? ;SINGLE-STEP TYPE_2 DD ? ;NON-MASKABLE INTERRUPT TYPE_3 DD ? ;BREAKPOINT TYPE_4 DD ? ;OVERFLOW ; SKIP INTEL RESERVED VECTORS ORG 32 * 4 TYPE_32 DD ? ;MAY PLACE YOUR VECTORS HERE ; MORE INTERRUPT VECTORS INT_PTR ENDS ; THIS SEGMENT MARKS THE BEGINNING OF DGROUP AND ; INITIALIZED DATA IN RAM IDATA_BEG SEGMENT PARA PUBLIC 'IDATA_BEG' IDATA_BEG ENDS _DATA SEGMENT WORD PUBLIC 'DATA' _DATA ENDS CONST SEGMENT WORD PUBLIC 'CONST' CONST ENDS ; THIS SEGMENT MARKS THE END OF INITIALIZED DATA IN RAM IDATA_END SEGMENT PARA PUBLIC 'IDATA_END' IDATA_END ENDS ; THIS SEGMENT MARKS BEGINNING OF UNINITIALIZED DATA IN RAM UDATA_BEG SEGMENT PARA PUBLIC 'UDATA_BEG' UDATA_BEG ENDS _BSS SEGMENT WORD PUBLIC 'BSS' _BSS ENDS STACK SEGMENT PARA STACK 'STACK' DW STACK_SIZE DUP (?) STACK_TOP LABEL WORD STACK ENDS ; THIS SEGMENT MARKS THE END OF UNINITIALIZED DATA IN RAM UDATA_END SEGMENT PARA PUBLIC 'UDATA_END' UDATA_END ENDS ; THIS SEGMENT CONTAINS BOOTSTRAP CODE AT HARDWARE RESET OR POWER-UP ; FAR JUMP TO PROGRAM START, LOCATE AT FFFF0H IN ROM. BOOTSTRAP SEGMENT AT 0FFFFH ; 'BOOTSTRAP' JMP FAR PTR START BOOTSTRAP ENDS END START Figure 3: Examples of Functions in Microsoft C Run-time Library DOS Dependent Functions (Not suitable for embedded systems) fopen opens a file fclose closes a file malloc allocates a memory block printf formats and prints a series of characters DOS Independent Functions (Suitable for embedded systems) atoi converts string to int sprintf formats and stores a series of characters strcmp compares strings strcpy copies strings Figure 4: Block Diagram of the Link&Locate++ Package ╔════════╗ ┌───────────┐ ┌──────────────┐ ║ Object ╟──┬──►│ CREF86 ├─────►│ Cross │ ║ Files ║ │ │ Utility │ │ Reference │ ╚════════╝ │ └───────────┘ └──────────────┘ │ ┌───────────┐ ┌──────────────┐ ├──►│ XLIB86 ├─────►│ Intel OMF │ │ │ Librarian │ │ Library File │ │ └───────────┘ └──────┬───────┘ │ ┌───────────┐ │ └──►│ XLINK86 │◄────────────┘ │ Linker │ └────┬──────┘ ┌────▼──────┐ │ XLOC86 │ │ Locator │ └────┬──────┘ ┌───────────┐ ┌────▼──────┐ │ Intel OMF │ │ CV20MF ├─────►│ Absolute │ │ Formatter │ │ Code │ └────┬──────┘ └───────────┘ ┌────▼──────┐ ┌───────────┐ │ PROM86 ├─────►│ Intel Hex │ │ Formatter │ │ File │ └───────────┘ └───────────┘ Figure 5: The environment for developing embedded system applications in Microsoft C. ┌──────────────┐ ╔═════════▼══════════╗ │ ║ Microsoft Editor ║ │ ║ ║ │ ╚═════════╤══════════╝ │ ╔═════════▼══════════╗ │ ║ Microsoft C ║ │ ║Optimizing Compiler ║ │ ╚═════════╤══════════╝ │ ╔═════════▼══════════╗ │ ║SSI Link & Locate ++║ │ ║ Package ║ │ ╚═════════╤══════════╝ │ ╔═════════▼══════════╗ │ ║ Intel I²ICE ║ │ ║ Emulator ║ │ ╚═════════╤══════════╝ │ └──────────────┘ Figure 6: C Source for Functions that Implement a Ring Buffer /* To generate in-line 8086 opcodes for intrinsic functions, such as inp, outp, and _enable, specify /Oi in C compiler */ #define RING_BUF_SIZ 128 #define PICIER 0xC2 #define PICEOI 0xC0 #define IRQDIS 0x20 #define EOI 0x20 #define SIOPORT 0x10 #define OUTPORT 0x12 #define enable_sio() picmask = inp(PICIER); \\ picmask &= (~IRQDIS); \\ outp(PICIER, picmask); #define disable_sio() picmask = inp(PICIER); \\ picmask |= IRQDIS; \\ outp(PICIER, picmask); char ring_buf[RING_BUF_SIZ]; char *in_ptr = ring_buf; char *out_ptr = ring_buf; int count = 0; char picmask; void process_data(char); main() { while(1) { enable_sio(); /* enable serial i/o */ if (count > 0) { process_data(*out_ptr); disable_sio(); /* disable serial i/o */ count-; if (out_ptr++ == ring_buf + RING_BUF_SIZ) out_ptr = ring_buf; /* ring buffer */ enable_sio(); /* enable serial i/o */ } } } void process_data(c) char c; { outp(OUTPORT, c); } void interrupt far sio_handler() { _enable(); /* enable higher order interrupt */ *in_ptr = inp(SIOPORT); if (in_ptr++ == ring_buf + RING_BUF_SIZ) in_ptr = ring_buf; if (count != RING_BUF_SIZ) count++; outp(PICEOI, EOI); /* reset interrupt */ } ████████████████████████████████████████████████████████████████████████████ C Scope and Linkage: The Keys to Understanding Identifier Accessibility Greg Comeau☼ The proper use of data structures is one of the chief influences on how well you write your C programs. To program properly you must understand data structures from both a syntactical viewpoint and a semantic viewpoint. You must be able to read and write the C declarations associated with your programs' identifiers so well that you could describe them in English and write them in C from a description in English (see "Understanding C Declarations," MSJ, Vol. 3, No. 5). Furthermore, you need to be able to comprehend fully the semantics and usage of identifiers and their relationships so as to make better use of them in such common coding practices as structured programming and data hiding. Once you've grasped their usage, you will better understand the compiler and run-time errors associated with the data structures within your programs, which will make debugging substantially easier. The ideas discussed here build on those presented in the C declarations article in the last issue. We will discuss the various concepts pertaining to the use of identifiers both within and across your programs' different source files. You won't be using C to its full capacity until you know about all the various attributes that can be given to an identifier. Learning this will surely lead to better and more professional programming technique. Declaration or Definition Most of the time, programmers use the words declaration and definition interchangeably, which is a mistake. A declaration is a description of the various attributes of a given identifier, that is, its size, shape, and characteristics. A declaration does not reserve any space for the identifier; it only states something about it. A definition, on the other hand, is a declaration that will reserve storage when the given program is executed, an important difference that will become more apparent as you read on. Scope C has several rules that control when a given identifier is visible within your code. This visibility is called the scope of the identifier and determines the accessibility of the identifier by the other parts of the program that the identifier resides in. For example, function parameters cannot be accessed by name except within the function that they are declared in. Therefore the scope of these parameters is limited only to functions, that is, they are only visible inside of a function. There are four places where the scope of an identifier pertains: blocks, functions, function prototypes, and files. The applicable scope of an identifier will begin on entering one of these four places and will end on exiting them. For instance, an identifier with block scope begins at the beginning of a block and ends at the end of that block. Figure 1 is a simple table comparing the four scopes. The first column lists the types of scope and the second column lists the positional properties of the places where that scope may be used in your code. Let's look at each type of scope in more detail; refer to Figure 1 as the discussion proceeds. Function Scope The only type of identifier capable of having function scope is a label. Since labels must be unique within functions they may not appear within the same function more than once, even within a block. You can use them in other functions without conflict if you need to. You will see why this is not a problem when we discuss linkage. File Scope Identifiers with file scope are those with declarators or type specifiers outside any parameter list or function. File scope begins at the end of the declarator and does not end until the current source file has been analyzed. This means that once an identifier with file scope goes "into" scope, it can be referenced from any other variable or function that is below it but not above it. The code in Figure 2 illustrates this. Note that even though file_scope appears outside all the routines, it is not known to main because it is not declared until after main. In other words, its scope is not determined until its name becomes visible through its declaration. Only when the declaration has been completed can file_scope be referenced by the other parts of the program following it. An interesting question is whether or not it would make sense to put file_scope after main and f( ) regardless of whether file_scope was actually referenced in that particular file or not. (Part of the answer to this will appear later in the article.) Block Scope The philosophy of block scope is somewhat like that of file scope, but its universe is a bit smaller. An identifier with block scope has declarators or type specifiers that are in a block, that is, within braces ({}), or are parameters. Identifiers with block scope are typically called local variables because they are only known within the function that they appear in. This description of block scope is correct; however there is a confusing exception involving "local externals," which will be explained when we discuss the storage class. Function Prototype Scope Function prototype scope is the easiest scope to remember. It is simply an identifier declared within a function prototype. The scope of the identifier ends when the function declarator ends. Although the inclusion of an identifier in a function prototype is optional, this works out nicely since the identifier serves as a comment and encourages more readable code. Note, however, that an identifier with function prototype scope is only in effect in what is normally called a declaring or forward referencing prototype. If the prototype is a defining prototype (a prototype used at the same time that the code for the function is being presented and defined), then those identifiers are legitimate parameters and, as mentioned earlier, will have block scope if they are coded that way. Loose Ends on Scope To use scope to your advantage, there are a few more things you need to know. First, identifiers go in and out of scope. For instance, take a look at the code and output in Figure 3. In line 1, id comes into scope (file scope to be exact) and remains that way until line 5. Here there is another identifier with the same name with block scope. In such a situation, the previous identifier temporarily goes out of scope until the scope of the current identifier (the newly scoped one with the same name) terminates. In this case, that would be at the end of the function (line 15). Therefore at line 16, the id with file scope is visible again and will print out correctly on line 19 as there is no other identifier named id in scope. Now notice what is happening at line 10. A new block is introduced by being enclosed between braces in the middle of the main function. This is valid C and permits you to declare/define your identifiers closer to where they are actually used. Also, in some cases, it could generate more efficient use of the stack or, in the case of local and blocked register variables, more efficient code. So, at line 10 yet another instance of id is brought into scope, at the third scoping level, since the two id's at lines 1 and 5 are still waiting to come back into scope. At this point, the id in line 10 goes into scope, stacking up the id on line 5 until this "new" id goes out of scope on line 13. Again, this new id references a different identifier from the other ids. Linkage Linkage is a mechanism whereby the identifiers declared in your code as objects or functions can be resolved regardless of the similarity or difference of scope. The resolution involves breaking down each translation unit, that is, source file, such that references to given objects or functions, whether or not they are duplicates, can be acknowledged and linked with appropriate library routines and a resulting program image, typically an executable binary file, created. Just as in scope, there are various levels of linkage: external linkage, internal linkage, and no linkage. Figure 4 summarizes each type of linkage. External Linkage An identifier with external linkage has either an extern storage class or is an identifier with file scope that has not been explicitly declared static. Also, each reference to an identifier with external linkage is a reference to the same object or function regardless of how many or which source files or libraries reference it. Therefore, the identifier ext in Figure 5 would have external linkage. If the two routines, main and f( ), were compiled and linked, they would both use the same ext. So, if you were to execute the program you would get a predictable output as illustrated. Note that from point a to point b, ext still hasn't changed its value. But once f( ) sets it to 2, it will remain that way until another subsequent assignment to it, which hasn't happened when it reaches point d. A definition of a nonstatic identifier with file scope is implicitly taken to be an extern. Note though that there may only be one definition of an identifier regardless of how many source files are involved. Many compilers are relaxing the constraints of this rule. Internal Linkage Identifiers with internal linkage look very much like those with external linkage. The similarity is that they both reference identifiers with file scope; the difference is that those with internal linkage have been explicitly declared with the storage class static. Internals are useful for data-hiding situations. For instance, some of the routines in the standard library such as fopen, fclose, and malloc, usually need to use identifiers with internal linkage. The constraint against using internals is that information about them is not published to the linker for use by other modules. Thus they are not known to or accessed by any other source files. When they are known by the linker, or perhaps by a debugger, the linker will effectively treat internals as comments because they will not be put into the external symbol table. You are free to use an internal within a given source file without being concerned about its name clashing with another variable of the same name in another source module, regardless of whether that other identifier is external or internal. Figure 6 shows a program that illustrates identifiers with internal linkage. The program may be a bit hard to comprehend since this technique is not used very often, but it does work. I encourage you to experiment with the example in Figure 6 until you understand the concept of internal linkage. A brief explanation to help you: The program has two source files, one containing main and another containing f( ). As mentioned before, identifiers with internal linkage are those with file scope that have been explicitly declared with the static keyword. Also, we know that internals are not used in the linker's external symbol table when it begins resolving missing externals. Therefore, each source file in Figure 6 has its own copy of an identifier named internal (yes, they are two distinct ints). This is verified in the output since the values at points a and d are the same. The output from point b is a bit more tricky. We get a value of 0 because all variables with static storage, regardless of whether they have internal or external linkage, are automatically initialized to 0; this is just standard C. Finally, notice that when the internal in f( ) is assigned the value 2 (as seen in point c), it does not influence the value printed at d (again, they are two different memory locations). By combining the example in Figure 6 with the example in Figure 5, you could concoct something like the code and output shown in Figure 7. Pay careful attention to the program in Figure 7 (example1.c through example 4.c) and thoroughly examine the resulting output. A few comments are in order: points a, d, g, and j represent the variable messy, which is associated with main; points b and c represent a different variable messy, which is associated only with the function x; points e, f, h, and i represent yet another variable named messy, which is associated only with functions y and z. Note how points f and h print the same value as they both use the same variable messy. No Linkage Finally, there are identifiers that have no linkage: parameters, nonobjects and/or nonfunctions (including typedefs, aggregate tags, and function prototype identifiers), and nonexterns in blocks. This implies that only identifiers with file scope are candidates for linkage. Does this make sense? In the case of typedefs, structure tags, and union tags it does because they are not still around during execution time, that is, they are not objects. Also, because labels, parameters, and block identifiers are only found within functions, they are typically indexed off the stack, or they are register-based and need not be resolved. Hence the linker does not have to be told anything about them. At this point, take another look at the definition of linkage. It should be clear that there is no reason to resolve anything classified with no linkage because no other source file should attempt to access it by name. Linkage Problems Many of the advantages of using scope can be extended once the linkage of identifiers is understood. You now have a good framework of how to control instances and the lifetime of identifiers; however, there are some problems. For instance, what happens if you have a routine that accesses an identifier with both internal and external linkage? Such a program is shown in Figure 8. This example just doesn't seem right, yet C lets this get by. On the other hand, different compiler vendors treat this problem in different ways. With some it works; with others ext_int either remains external and never internal or vice versa. Still others will treat this as a syntax error. The draft ANSI C standard has recognized this problem and has called such coding practices "undefined behavior," which means stay away from it. Another problem with linkage occurs when there are identifiers with mismatched types across source files. This can happen very easily without your being aware of it and will cause serious difficulties if you don't understand what you've done. For instance, a C programmer (one not very experienced with using C, we hope) might decide to use an array in his or her program and at some point either abuses or misunderstands the relationships between arrays and pointers. Can you figure out why the following code will compile and link, yet not work? int array[10]; void func(); main { <...> array[5] = <...> <...> func(); <...> } with another source file containing: int *array; void func() { <...> array[5] = <...> <...> } Think about it for a bit. The reference to array in the source file containing main is fine. However, let's take a closer look at what is happening in the other source file. Does array[5] really do what it appears to be doing? The answer is no; this is due to the fact that array has been redeclared as a pointer. This is a problem because it will take the "thing" array points to, index 5 ints down, and place a value at that address according to the assignment, which is an error. Is it clear what value the array pointer points to? Assuming that sizeof(int) == sizeof(int pointer), that value is actually array[0]. For instance, if I had initialized array as follows: int array[10] = { 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11 }; in the source file containing main, the reference to array[5] in func, which maps into *(array + 5), would become *((int *)1 + 5). It produces (int *)1, or simply 1, because both references to array in the two source files will become resolved by the linker as beginning at the same address, which is wherever array resides. Since a pointer is a variable that contains the address of another variable, a reference to array in func is a reference to the object that array apparently points to; in this case, it is the 1. On many CPU architectures this error would be obvious the very first time array[5] was accessed in func because many of these machines do not allow the access of an integer on odd boundary addresses. Normally this would result in a memory addressing fault of some sort so that a programmer at least has a chance to debug it. However, if the value at array[0] is an even integer, a fault will not occur. Also, on such machines as the IBM(R) PC, which permit odd addressing of integers, the problem will not be apparent either. Debugging this sort of thing is generally futile because the programmer won't recognize the semantic difference between the accesses to array in the two source files. Yet another problem is that C is case-sensitive, while many linkers are not. This leads to situations in which the linker will map identifier names that are in lowercase to uppercase. Unfortunately this means an identifier named SampleIdentifier and another named sampleidentifier would be treated as if they were both at the same memory location──not a very pleasant scenario. Fortunately, many linkers are being updated to solve this problem. Along the same lines, another problem is one of identifier name length. Most compilers will usually allow up to 31 or 32 characters for constructing identifier names. However, this is a syntactical constraint chosen by your compiler, and even though your program may compile successfully, a linker may choose to truncate your identifiers to as little as six characters. For example, an identifier named Identifier1 and another named Identifier2 might end up truncated to Identi and will therefore (sadly) represent the same variable. Even worse, some systems will not even inform you that this has been done. This problem is directly related to your operating system and/or development tools. Microsoft(R) C does not do this, but you do need to be aware of this possibility when you port your programs to other systems. Another serious problem, which fortunately does not occur with MS(R) C, happens on systems that choose to publish internals to the linker in such a fashion that they will be resolved with externals of the same name. This means that the programs in Figures 6 and 7 will not work as I have described them on such systems. There aren't too many linkers that work in this manner, and they are usually found on specialized or proprietary systems. However, if you are writing code and intend to port it to other systems you need to be aware of this. If you do a lot of porting, I strongly advise you to write a few programs that test the linkage aspects of your programs before doing the actual port. Redeclarations Many C compilers do not like to parse duplicate declarations or definitions of identifiers. I do not know why this happens since C allows it as long as the declaration or definition defines the same object with the same sizes and types. This discussion may sound a bit silly since most people would not code something like: int a; int a; However, it does come in very handy in cases where you have programs that automate your development by writing other C programs. Or maybe you are including various header files and two of them end up including the same one, say STDIO.H. Also, consider the case of: char *p1 = &p2; char *p2 = &p1; Since the scope rules of C maintain that p2 is not known (that is, not in scope) while parsing p1, how can we then get the address of it? The only way is to create a forward reference to it with either: char *p2; or: extern char *p2; placed before p1. Some of you may argue that since C is a two-pass compiler, it should be able to pick up p2 the second time around. First, there is no guarantee that any particular C compiler will be implemented as a two-pass compiler. Second, even though it is technically possible to determine if p2 actually exists (for instance, many assemblers work this way), it is not within the definition of the C language to investigate it. Some of this thinking goes back to C's philosophy of keeping things simple and letting the compiler finish as quickly as possible. Compiler writers did not want their compilers using too many complex algorithms for the sake of efficiency. Today, the mentality behind these concerns is probably much different, but the choice usually comes down to keeping within the spirit of C, so many elements of C have remained intact. Storage Duration One final topic I've alluded to throughout this article is the storage duration of an identifier. The MS C documentation and most texts have an acceptable description of this, so I will only focus on those issues about storage classes that relate to our discussion. First, let's define what storage duration is and then how it relates to scope and linkage. The storage duration of an identifier mandates how long it will exist. It is worth noting that only objects can have a storage duration. The storage duration of an identifier is specified syntactically via its storage class and can assume either of two values: static or automatic. An object is either in existence all the time, meaning that it is static, or its lifetime can be sporadic and dynamic while the program is running, meaning that it is automatic. Objects with static storage are those with external linkage, internal linkage, or those objects that have no linkage but have been declared explicitly with the static storage class. Objects with automatic storage are those with no linkage and no explicit static storage class. You can see that the lifetime of an object directly influences its linkage and in most cases its scope. For instance, all identifiers in your programs with external and internal linkage are implicitly static. Also, all identifiers defined with the static storage class keyword are also static, regardless of their scope (file or block). Therefore, all static objects remain intact from program startup through program execution and termination. All automatics have limited and asynchronous lives, including new and multiple ones. Storage Classes There are a few points to clarify now that most of the characteristics of identifiers have been explained. First of all, there is a slightly confusing syntactical allowance that doesn't quite fit with what I've said. This is the ability to reference identifiers with file scope by referencing the declaration in such a way that they appear to have block scope and, perhaps worse, no linkage. In other words, sometimes it appears that a given identifier has mixed linkage. However, this is not really the case. For example, Figure 9 contains an identifier i within its main program that does not go into file scope until after the main routine is finished. C allows you to use a shortcut, the extern int i, to reference it. The question is, does i have block scope or file scope? Without the extern, it is an identifier with block scope and no linkage. However, we know it references an identifier with file scope and external linkage. C resolves this by doing the logical thing; the identifier is given block scope visibility. This permits the function to access the external without bothering any other routines. In addition to these considerations, some compilers will act differently. For instance, MS C treats all extern function declarations with file scope and visibility from within the rest of the program regardless of where they were declared, even if they were declared from within blocks that have been terminated. This is not a widely used mechanism because of its variety, but it is endorsed by the ANSI draft; since the compiler already has the information there is no reason to throw it away. The concepts of scope and linkage are not always obvious to programmers because of their diversity of implementation and use. However, they are powerful features of C and when well understood should lead to programs that are integrated correctly and thus behave more rationally. It is another piece of C knowledge the programmer needs in order to be as professional as possible and is even more important when working on large projects, especially those involving more than one programmer. Figure 1: Scope Types Types of Identifier Visibility Scope Function Labels File Declarations or type specifiers outside a function or parameter list Block Declarations or type specifiers inside a function or parameter list Function Declarations or prototype type specifiers within a parameter Prototype declarator list Figure 2: Example of File Scope void f(); main() { f(); printf("%d\n", file_scope); /* error */ } int file_scope = 1; void f() { printf("%d\n", file_scope); /* okay */ } Figure 3: Example of a variable going in and out of scope. 1 int id = 5; 2 3 main () 4 { 5 int id = 4; 6 7 printf("id at a%d\n", id); 8 f(); 9 { 10 int id =3; 11 12 printf("id at b=%d\n", id); 13 } 14 printf("id at c=%d\n", id); 15 } 16 17 f() 18 { 19 printf("id at d=%d\n", id); 20 } Resulting output: id at a=4 /* local variable within main - decl at line 5 */ id at d=5 /* variable with file scope even through f() is */ /* called from line 8 - decl at line 1 */ id at b=3 /* local variable within block within main - decl at line 10 */ id at c=4 /* local variable within main - decl at line 5 */ Figure 4: Linkage Types Types of Identifier Constraints Linkage External Extern or file scope Internal File scope but static None Parameters, nonobjects, nonfunctions, nonexterns in blocks Figure 5: Example of External Linkage extern int ext; void f(); main() { ext = 1; printf("ext at point a=%d\n", ext); f(); printf("ext at point d=%d\n", ext); } Function f( ) is located in a different source file: extern int ext; void f() { printf("ext at point b=%d\n", ext); ext = 2; printf("ext at point c=%d\n", ext); } Resulting output: ext at point a=1 ext at point b=1 ext at point c=2 ext at point d=2 Figure 6: Example of Internal Linkage static int internal; void f(); main() { internal = 1; printf("internal at point a=%d\n", internal); f(); printf("internal at point d=%d\n", internal); } Function f( ) is located in a different source file: static int internal; void f() { printf("internal at point b=%d\n", internal); internal = 2; printf("internal at point c=%d\n", internal); } Resulting output: internal at point a=1 /* use the internal from main */ internal at point b=0 /* use the internal from f */ internal at point c=2 /* use the internal from f */ internal at point d=1 /* use the internal from main * notice it has the same value * as in point a since it hasn't * been reassigned */ Figure 7: Output resulting from the compilation of example 1.c through example 4.c into one executable. /* example 1.c */ static int messy; void x(), y(), z(); main() { messy = 1; printf("a=%d\n", messy); x(); printf("d=%d\n", messy); y(); printf("g=%d\n", messy); z(); printf("j=%d\n", messy); } /* example 2.c */ static int messy; void x() { printf("b=%d\n", messy); messy = 2; printf("c=%d\n", messy); } /* example 3.c */ int messy; void y() { printf("e=%d\n", messy); messy = 3; printf("f=%d\n", messy); } /* example 4.c */ extern int messy; void z() { printf("h=%d\n", messy); messy = 4; printf("i=%d\n", messy); } Resulting output: a=1 b=0 c=2 d=1 e=0 f=3 g=1 h=3 i=4 j=1 Figure 8: C allows the same variable to be both external and internal; however, the result is compiler dependent. The draft ANSI C standard labels this practice as undefined behavior. int ext_int; /* this establishes external linkage */ void f(); main() { ext_int = 1; f(); } static int ext_int: /* this establishes internal linkage */ void f() { ext_int = 2; } Figure 9: If i has file or block scope, C gives i block scope visibility. void f(); main() { extern int i; i = 5; f(); } int i; void f() { printf("i=%d0, i); } ████████████████████████████████████████████████████████████████████████████ Extending the Functions of the Windows Clipboard with Scrapbook+ Kevin P. Welch and David E. West☼ The Windows clipboard is probably the most commonly used mechanism for the exchange of data between applications. With the clipboard, a spreadsheet or drawing created in one program can easily be transferred to another program without the need for an intermediate file. Windows comes with a seldom used application called the clipboard viewer, CLIPBRD.EXE. This program, as its name suggests, allows you to view the current clipboard contents. Unfortunately, it provides little additional functionality. After using the Windows clipboard and associated viewer for a while, several limitations become obvious: ■ the clipboard is restricted to one data item (albeit multiply rendered) ■ you cannot erase or save the current clipboard contents ■ you often need the originating program to copy its data to the clipboard ■ there is no system-supported mechanism for clipboard format manipulation ■ you cannot control the data renderings presented by the clipboard ClickArt(R) Scrapbook+ is a sophisticated Windows application that was developed to try to overcome some of these limitations. Acting as an agent for clipboard data, Scrapbook+ lets you place frequently used data in easily accessible files. When you need the data, it is available to you in any Windows program simply by pasting from the clipboard. By automatically creating miniature renderings of the contents in a visual index, Scrapbook+ allows you to scan quickly through pages of images and data of all types, then transfer them almost instantly via the clipboard to other Windows applications. Scrapbook+ was developed using the techniques described in "Exchanging Data Between Applications Using the Windows Clipboard" (MSJ, Vol. 3. No. 5). Figure 1☼ demonstrates selecting artwork from Scrapbook+ and Figure 2☼ demonstrates pasting the artwork into Aldus(R) PageMaker(R). Design Concepts Behind every product there are requirements, issues, frustrations, and dreams, and Scrapbook+ is no exception. The original idea for the product arose in the spring of 1987 from a series of informal discussions between ourselves and the staff at T/Maker, the publisher of Scrapbook+. Originally conceived as a clip-art manager, Scrapbook+ rapidly evolved into a full-fledged clipboard data storage and retrieval tool. From the very start we wanted to build a product that was simple, easy to learn, and intuitive while also fast and reliable. More importantly, we sought to create a program that users felt was indispensable, much like The Norton Utilities(TM). Data Storage Tool One of the main problems with the Windows clipboard is its temporal nature. We wanted to give users a means to save clipboard data for later use. Initially we hoped to store any kind of data, but midway through the development of Scrapbook+ we limited it to commonly used and well- understood formats. Besides pasting data into Scrapbook+ from the clipboard, we felt it was necessary to be able to import data directly from disk. This would extend its functionality to include other types of information available to the user, some of which were inaccessible within Windows. Although we realized that this would require the development of some difficult file- to-clipboard conversion routines, we felt it was necessary for a well- integrated product. Taking the data storage concept beyond file-to-clipboard data conversion, we also wanted to offer several enhanced mechanisms whereby users could automatically save their clipboard and display state. After several incarnations this finally evolved into the AutoPaste, Screen Capture, and the camera features. Data Retrieval Tool As frequent users of Aldus PageMaker and Microsoft(R) Excel, we were frustrated by the inability to access things without the original application. We wanted to be able to browse visually through large quantities of text, drawings, or images in search of just the right addition to the work in progress. From this need to review and retrieve data rapidly came thumbnails, small representations of an entire Scrapbook+ page. The thumbnail concept permitted us to concentrate information and present very large amounts of data efficiently. By using this visual index a particular page could be easily found, the correct representation selected, and the resulting format copied to the clipboard. Since typical users would probably create their own Scrapbook+ files, we assumed that they would have access to the program(s) that produced the data, so we decided not to provide any editing tools. Besides, creating an editor for each supported format would guarantee that many things would be done poorly. Eventually we incorporated some limited tools that permitted selection of a portion of the data which could then be copied to the clipboard or formed into a new thumbnail representation of the entire page. As another data retrieval enhancement, since data could be imported directly from disk, we considered it appropriate to provide export capabilities as well. A natural consequence of this decision is that data could be exported to disk from both the clipboard and the active Scrapbook+ file. Agent for Clipboard Data From the very beginning one aspect of Windows that attracted us was the ability to exchange information easily between applications. Our concept for Scrapbook+ extended this notion to include the exchange of data between disk and application via the clipboard, using Scrapbook+ as an agent in this process. Building on this idea, we wanted the tool to be able to circumvent some of the more troublesome clipboard roadblocks while seamlessly handling the richness it provided. Although the current version of Scrapbook+ does not directly support remote data requests and submissions, future editions may address this issue. With dynamic data exchange (DDE), applications could request or submit a specific data item. Furthermore, applications could use Scrapbook+ as an engine for converting to and from the various clipboard formats and external disk files. By doing so, many developers would be freed from the tedious task of supporting the ever increasing number of file and format standards, focusing instead on creating new and innovative applications. Visual Components Scrapbook+ was specifically designed as a visual application. Most users respond visually to a program, and their understanding is greatly enhanced if the visual metaphors used reflect the internal operation of the application. When you first bring up Scrapbook+ you will see something like what is shown in Figure 3☼. Although most of the components of the Scrapbook+ window are familiar to experienced Windows users, some require further explanation (the numbers following the component names are the numbers with which they are labeled in Figure 3☼). The description box (6) shows an account of the current page, including the date and time it was added to the current Scrapbook+ file. The toolbox (8) is a movable child window that lets you select the current viewing mode or one of four possible tools. The viewing area (9) displays one page of the current Scrapbook+ file, as well as allowing you to block select or lasso a portion of it for copying to the clipboard. The thumbnail size box (11) controls the number of thumbnail lines displayed in the Scrapbook+ window. The scroll bar (12) lets you scroll to thumbnails not visible in the viewing area. By clicking on a page in the thumbnail area you select it for display in the viewing area. If you choose to view the thumbnail area (13), the bottom of the Scrapbook+ window will display a thumbnail of each page in the file. Although each thumbnail may be too small to see page contents in detail, they're large enough to compare pages quickly when selecting the one you want to display in the viewing area. Viewing Modes Scrapbook+ operates in two viewing modes, clipboard mode and file mode. Under clipboard mode,contents can be displayed in the viewing area. When the clipboard is holding information, a small rectangle appears in the center of the icon (14). If the clipboard contains more than one rendering, you can select another by choosing one from the View pull-down menu. In clipboard mode certain menu options are automatically enabled or disabled. For example, you can erase the clipboard contents (thus freeing memory), but selection tools are only available while viewing a page in the file, not while viewing the clipboard. You can switch from clipboard mode to file mode by clicking on the page slider or selecting the Scrapbook option under the Page pull-down menu. In file mode most menu options are available for manipulating data, subject to the limitations imposed by the format you are viewing. The toolbox (see Figure 4☼) is used to choose a selection tool or change the current viewing mode. Selection tools are displayed on the left side of the toolbox and mode tools on the right. Four selection tools are supported: block, for the selection of rectangular areas from the viewing area; lasso, for the selection of arbitrary shapes; scroll, for rapidly scrolling the viewing area; and camera, for the capture of rectangular display regions to the clipboard. In any of the three viewing options in file mode, only two different mode tools are available. Clicking on the desired mode in the toolbox changes the viewing mode. Figure 5☼ shows Scrapbook+ in page only viewing mode, Figure 6☼ shows Scrapbook+ in page & thumbnails viewing mode, and Figure 7☼ shows Scrapbook+ in page & thumbnails viewing mode. Using Scrapbook+ Scrapbook+ was intended to be used in the same way as any other Windows application. When you start the program it automatically defaults to using the SCRAP.ART file. You can create a new file at any time by selecting the New command from the File pull-down menu. You can specify the maximum pages for the file you create and the desired thumbnail size with the resulting dialog box (see Figure 8☼). Being able to choose the size of each thumbnail is an important feature as Windows supports a wide variety of display subsystems. Once a file has been created you can use four standard editing functions: Cut, Copy, Paste, and Erase. When used in file mode they apply to the currently selected page, including all associated renderings. To provide more flexibility, we extended these standard editing functions through Special Edit commands. The Special Edit menu options are normally hidden and can only be accessed when the Edit menu is selected with the Shift key depressed. The standard Cut, Copy, Paste and Erase options then change to Cut Special, Copy Special, Paste Special and Erase Special. When you select one of these a dialog box is displayed (see Figure 9☼) listing the available formats, allowing you to specify which to use in the operation. The AutoPaste feature is related to the standard editing functions. When activated all clipboard changes are automatically pasted into the current Scrapbook+ file. This valuable feature can provide a log of all clipboard changes or simply act as a mechanism with which a series of editing commands can be recorded, for example, separating large "sheets" of raster artwork into individual bitmaps. Another feature often used in conjunction with AutoPaste is the ScreenCapture function. Using ScreenCapture lets you copy the client area, a window, the entire window, or even the display to the clipboard in bitmap form. It's activated by the Alt-PrtSc key combination, and when you use it with AutoPaste you can easily capture screen images, including pull-down menus, even while Scrapbook+ is iconic (see Figure 10☼). An alternative method for capturing screen images is the camera tool. It lets you select a rectangular region of the screen and copy it to the clipboard, from which it can be pasted into the current file. This is particularly useful for developers and users alike as it permits you to collect anything visible on the screen. Besides the standard edit functions, the import and export capabilities of Scrapbook+ enable you to transfer data between the current file or clipboard and disk. While doing so you can perform a limited amount of conversion between the standard clipboard formats and some of the more popular file standards (see Figure 11). One interesting item for Windows developers to note is the support of the BMP file format. With Scrapbook+ you can maintain large libraries of bitmap resources (greater in size than those supported by the icon editor) and export selected ones to disk for inclusion in your application's resource file. One of the more useful functions of Scrapbook+ is Merge (see Figure 12☼), which allows you to combine all or portions of one Scrapbook+ file into another. This is particularly helpful when building large files that are comprised of many smaller ones. Development Challenges Developing a program like Scrapbook+ in a new and challenging environment such as Windows meant encountering many interesting and difficult problems. One of the most arduous design challenges was to provide rapid access to a large number of pages without using inordinate amounts of memory. Each Scrapbook+ file consists of a fixed size header, followed by a variable size index with one entry per page, followed by the data for the pages themselves. To facilitate quick access to the page we kept both the header and index in memory. This consumes less than 8Kb of memory for the default 500-page file. The ability to peruse a file quickly in a graphical manner was another important design consideration for Scrapbook+, which led to the concept of thumbnails. First, we decided that thumbnails should be created and written to disk when a page was inserted, not "on the fly." We wanted to display thumbnails quickly, yet conserve system memory. Our solution to this problem was to use discardable bitmaps for thumbnails. Windows is then free to discard them as requests for memory are made, allowing for fast thumbnail scrolling, particularly when scrolling back to thumbnails that have been read in and not yet discarded. System Interdependencies One great benefit of the Windows environment is its device independence. Although software designed for Windows will run without modification on a variety of displays and under various memory configurations, including systems equipped with expanded memory, the software developer is not completely off the hook. Scrapbook+ had to be tested and adjusted for the most common display and system configurations to ensure that each resource had the proper characteristics and that the memory management techniques worked correctly. Also, comprehensive error handling had to be built in to handle situations where the display adapter in use was incompatible with the data maintained by Scrapbook+. Development of Scrapbook+ began under Microsoft(R) Windows Version 1.03; later, we switched to Windows 2.03. The change from a tiled windowing system to a pop-up windowing system required several modifications to our design. Also, the formal definition of keyboard hooks and the addition of expanded memory support forced us to make significant changes to the implementation of the screen capture function. This included the development of a dynamically linked library for our screen capture keyboard hook function in order to prevent it from being paged out under expanded memory. Diversity of Formats Our original design goal for Scrapbook+ was to handle any kind of clipboard data. In practice, this was very difficult, and in several cases simply impossible. Some clipboard formats, such as owner display, and several proprietary formats just had no meaning outside the context of the program that copied them to the clipboard. Other formats such as text, bitmaps, and metafiles formed the basis of clipboard data exchange in Windows, and there was never any doubt about their usefulness. Early in the design phase of Scrapbook+ we committed ourselves to supporting Encapsulated PostScript(R) (EPS). Since we were the first to handle this format we had to define a standard protocol for the exchange of EPS images via the clipboard. For those unfamiliar with this format, EPS is a combination of PostScript text with an optional display representation of the image, which can be a metafile, Tagged Image File Format (TIFF), or even a Macintosh(R) PICT file. Our commitment to EPS forced us to support TIFF, something which we privately hoped to avoid as it could have significantly delayed the project. However, despite the complexity of this format, supporting EPS and TIFF turned out to be a major selling point of the product, something we hadn't really anticipated. Interaction with Apps One phase of development that ended up taking more time than we thought was the interaction between Scrapbook+ and other Windows programs. Many of the subtleties and complexities of Windows become apparent in this area. We learned much about the correct use of the clipboard by making sure that Scrapbook+ worked well with a variety of other applications, including Microsoft Excel, Aldus PageMaker, and Micrografx Designer(R). We felt that users will more and more demand that applications not only work well by themselves, but that they work well together. Because of this we spent a lot of time understanding the interaction between Scrapbook+ and other major Windows applications, paying special attention to clipboard interaction and memory use. In the end, and not unexpectedly, Scrapbook+ performed considerably better when we kept each code segment including _TEXT under 8Kb in size and minimized the size of our data segment. Performance Optimization Because we envisioned Scrapbook+ as a system utility that would be present on the desktop at almost all times, we made an effort to characterize its performance and improve it in certain critical areas. We used both static and dynamic modeling approaches with several proprietary code analysis techniques to arrive at a good mathematical model of the application. This model was optimized, and resources were focused on the crucial areas. The result was a considerably improved application with well-balanced segments. In certain situations the time required for particular operations was reduced by a factor of five. Development Tool While working on Scrapbook+ we employed it extensively as a development tool for managing bitmap, cursor, and icon resources. Each incarnation of these resources was saved in a single file for review and possible retrieval. With the numbnail viewing mode we could rapidly compare recent designs with those created earlier in the development cycle. Figure 13☼ shows the handling resources of Scrapbook+. In addition to serving as a development tool, we used Scrapbook+ in many cases to test complicated clipboard interactions with itself or those supported supported by other applications. Furthermore, it was an invaluable tool when documenting the program. Using it we created a single file, approximately 1.5Mb in size, that contained every illustration, picture, screen image, and icon in the user's manual. Putting It in Perspective Now that the first version of Scrapbook+ is complete, some reflections on the entire process are in order. First of all, we are now even more acutely aware of the challenges provided by Windows software development. Although in part due to the complexity of the environment, the standards for software have reached the point where they have almost outstripped our ability to meet them. We remained firmly convinced that the monolithic approach to software development is a relic of a bygone era and that applications of the future will consist of small, well-integrated bundles combined into a functional desktop by the user. We also learned how much we were inclined to underestimate the quality assurance phase of Windows software development. The cooperative, shared resource, and multiprocessing nature of Windows makes applications very difficult to test, transforming simple errors into complicated interactions that can take weeks to resolve. Finally, we learned firsthand how insidious the trap of "creeping features" can be. It is so simple to say, "Let's just add this one extra feature──it will only take a little more time." The problem is that each of these features exacts its toll, and the delivery date of the product slips further into the future. All things considered, Scrapbook+ turned out to be a much better program than we had anticipated. On many occasions we found ourselves asking, "How did we ever get along without it?" Would we do it again? Yes, in an instant, just as soon as we find that one great idea. Figure 11: Import/Export Formats The following table lists the tools available with supprted import and export file types for each clipboard format: ╓┌────────────┌────────┌─────────┌─────────┌───────────┌─────────────────────╖ Format Block Lasso Scroll Import Export Text yes txt txt Rich text yes rtf rtf Bitmap yes yes yes msp, bmp msp, bmp Printer yes yes yes msp, bmp Format Block Lasso Scroll Import Export Printer yes yes yes msp, bmp Bitmap Picture yes wmf wmf Printer yes wmf Picture CSV yes csv csv SYLK yes syk syk DIF yes dif dif TIFF yes tif tif PostScript yes eps, ps eps,ps The following directory lists some of the common sources of the file types supported by Scrapbook+ txt almost any word processor rtf many word processors msp Windows Paint bmp Windows Bitmap Editor wmf some drawing programs csv most spreadsheet and database applications slk Microsoft Excel, Multiplan dif most spreadsheet and database applications tif Scanning Gallery, other applications eps Adobe Illustrator ps Adobe Illustrator